Chronoscope 0.0.4.dev1__tar.gz → 0.0.5.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0/Chronoscope.egg-info}/PKG-INFO +1 -1
- {chronoscope-0.0.4.dev1/Chronoscope.egg-info → chronoscope-0.0.5.dev0}/PKG-INFO +1 -1
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/README.md +1 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/__init__.py +5 -2
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/chart.py +59 -30
- chronoscope-0.0.5.dev0/chronoscope/db.py +188 -0
- chronoscope-0.0.5.dev0/chronoscope/parser.py +175 -0
- chronoscope-0.0.5.dev0/chronoscope/tree.py +52 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/utils.py +16 -2
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/vcd.py +9 -3
- chronoscope-0.0.4.dev1/chronoscope/db.py +0 -125
- chronoscope-0.0.4.dev1/chronoscope/parser.py +0 -91
- chronoscope-0.0.4.dev1/chronoscope/tree.py +0 -35
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/COPYING +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/SOURCES.txt +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/dependency_links.txt +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/entry_points.txt +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/requires.txt +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/top_level.txt +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/__main__.py +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/drawer.py +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/hist.py +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/queue.py +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/setup.cfg +0 -0
- {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/setup.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|

|
|
2
2
|

|
|
3
|
+
[](https://colab.research.google.com/github/just-now/chronoscope/blob/feat/chronoscope-event-relation/examples/raft_demo.ipynb)
|
|
3
4
|
|
|
4
5
|
# chronoscope
|
|
5
6
|
Chronoscope, a cross-platform matplotlib-based observability tool
|
|
@@ -19,7 +19,7 @@ import argparse as arg
|
|
|
19
19
|
import errno
|
|
20
20
|
import sys
|
|
21
21
|
|
|
22
|
-
__version__ = '0.0.
|
|
22
|
+
__version__ = '0.0.5.dev0'
|
|
23
23
|
__author__ = 'Anatoliy Bilenko <anatoliy.bilenko@gmail.com>'
|
|
24
24
|
__license__ = 'LGPLv3'
|
|
25
25
|
|
|
@@ -47,6 +47,8 @@ def parse_args():
|
|
|
47
47
|
parser.add_argument("-t", "--trace", type=str, help="User's traces")
|
|
48
48
|
parser.add_argument("-D", "--depth", type=int, default=50,
|
|
49
49
|
help="limits output to given level of ticks")
|
|
50
|
+
parser.add_argument("-r", "--reverse", action='store_true',
|
|
51
|
+
help="follow state-machine relations to parents")
|
|
50
52
|
parser.add_argument("command", type=str,
|
|
51
53
|
choices=["create", "chart", "tree",
|
|
52
54
|
"hist", "queue", "vcd"],
|
|
@@ -73,7 +75,8 @@ def main() -> int:
|
|
|
73
75
|
db.close()
|
|
74
76
|
case "chart":
|
|
75
77
|
db.open(args.db, db_options, verbose=args.verbose)
|
|
76
|
-
chart.plot(args.tick_id, args.fig_size, args.depth
|
|
78
|
+
chart.plot(args.tick_id, args.fig_size, args.depth,
|
|
79
|
+
args.reverse)
|
|
77
80
|
case "tree":
|
|
78
81
|
db.open(args.db, db_options, verbose=args.verbose)
|
|
79
82
|
tree.plot(args.tick_id, args.depth)
|
|
@@ -12,34 +12,30 @@ import chronoscope.utils as utils
|
|
|
12
12
|
import matplotlib.cm as cm # type: ignore
|
|
13
13
|
import matplotlib.pyplot as pt # type: ignore
|
|
14
14
|
import matplotlib.ticker as ticker # type: ignore
|
|
15
|
-
from dataclasses import dataclass
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
16
|
import sys
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
Y_LINE_SPACING = 2
|
|
20
20
|
X_TICKS_MAX = 5
|
|
21
21
|
|
|
22
|
-
def plot_timeline(timeline, y_pos: int)
|
|
23
|
-
ret = []
|
|
22
|
+
def plot_timeline(timeline, y_pos: int):
|
|
24
23
|
y_pos_scaled = -Y_LINE_SPACING * y_pos
|
|
25
24
|
|
|
26
25
|
for current, current_tick in enumerate(timeline[:-1]):
|
|
27
26
|
next_tick = timeline[current + 1]
|
|
28
27
|
start_time, end_time = current_tick["time"], next_tick["time"]
|
|
29
|
-
event_label = current_tick["
|
|
28
|
+
event_label = current_tick["name"]
|
|
30
29
|
|
|
31
30
|
pt.hlines(y_pos_scaled, start_time, end_time, lw=4,
|
|
32
31
|
colors=cm.tab10(current % 7)) # type: ignore[attr-defined]
|
|
33
|
-
pt.text(start_time, y_pos_scaled, event_label, rotation=
|
|
34
|
-
ret += [{"x": start_time, "y": y_pos_scaled, "eid": current_tick["eid"]}]
|
|
32
|
+
pt.text(start_time, y_pos_scaled, event_label, rotation=90)
|
|
35
33
|
|
|
36
34
|
if len(timeline[:]) == 1:
|
|
37
35
|
pt.hlines(y_pos_scaled, timeline[0]["time"], timeline[0]["time"])
|
|
38
36
|
|
|
39
37
|
pt.text(timeline[-1]["time"], y_pos_scaled,
|
|
40
|
-
timeline[-1]["
|
|
41
|
-
ret += [{"x": timeline[-1]["time"], "y": y_pos_scaled, "eid": timeline[-1]["eid"]}]
|
|
42
|
-
return ret
|
|
38
|
+
timeline[-1]["name"], rotation=90)
|
|
43
39
|
|
|
44
40
|
@dataclass
|
|
45
41
|
class timeline_visitor:
|
|
@@ -47,18 +43,56 @@ class timeline_visitor:
|
|
|
47
43
|
y_pos: int
|
|
48
44
|
x_min: int
|
|
49
45
|
x_max: int
|
|
50
|
-
|
|
46
|
+
# sm_id -> y_pos, for drawing event_relation arrows
|
|
47
|
+
sm_to_y: dict = field(default_factory=dict)
|
|
48
|
+
# (from_time, from_y, to_time, to_y) pairs for thin arrows
|
|
49
|
+
arrows: list = field(default_factory=list)
|
|
51
50
|
|
|
52
51
|
def __call__(self, timeline: list[dict], origin: int, parent: None | int):
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if not timeline:
|
|
53
|
+
self.sm_to_y[origin] = self.y_pos
|
|
54
|
+
self.y_pos += 1
|
|
55
|
+
self.y_labels.append(f"?{utils.format_event_id(origin)} [empty]")
|
|
56
|
+
return
|
|
57
|
+
plot_timeline(timeline, self.y_pos)
|
|
58
|
+
self.sm_to_y[origin] = self.y_pos
|
|
55
59
|
duration = round((timeline[-1]["time"] - timeline[0]["time"]) / 1e6, 3)
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
sm_type = timeline[0].get("sm_type", "?")
|
|
61
|
+
label = f"{sm_type}{utils.format_event_id(origin)} [{duration}ms]"
|
|
62
|
+
self.y_labels.append(label)
|
|
58
63
|
|
|
59
64
|
times = [tick["time"] for tick in timeline]
|
|
60
65
|
self.x_min = min(self.x_min, min(times))
|
|
61
66
|
self.x_max = max(self.x_max, max(times))
|
|
67
|
+
self.y_pos += 1
|
|
68
|
+
|
|
69
|
+
def collect_arrows(self):
|
|
70
|
+
"""Query event_relation for (from_sm, from_time) -> (to_sm, to_time).
|
|
71
|
+
|
|
72
|
+
The sender's state machine and time come from the send event referenced
|
|
73
|
+
by each receive relation's from_event_id."""
|
|
74
|
+
sql = """
|
|
75
|
+
SELECT s.state_machine_id AS from_sm, s.time AS from_time,
|
|
76
|
+
r.to_sm_id AS to_sm, r.to_time AS to_time
|
|
77
|
+
FROM event_relation r
|
|
78
|
+
JOIN event s ON s.id = r.from_event_id
|
|
79
|
+
WHERE r.from_event_id IS NOT NULL
|
|
80
|
+
"""
|
|
81
|
+
for from_sm, from_time, to_sm, to_time in db.db.execute_sql(sql).fetchall():
|
|
82
|
+
if from_sm in self.sm_to_y and to_sm in self.sm_to_y:
|
|
83
|
+
self.arrows.append((
|
|
84
|
+
from_time, -Y_LINE_SPACING * self.sm_to_y[from_sm],
|
|
85
|
+
to_time, -Y_LINE_SPACING * self.sm_to_y[to_sm],
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
def plot_arrows(arrows: list):
|
|
89
|
+
for from_time, from_y, to_time, to_y in arrows:
|
|
90
|
+
pt.annotate("",
|
|
91
|
+
xy=(to_time, to_y), xytext=(from_time, from_y),
|
|
92
|
+
arrowprops=dict(arrowstyle="-|>", color="navy",
|
|
93
|
+
lw=0.6, alpha=0.7,
|
|
94
|
+
shrinkA=2, shrinkB=2),
|
|
95
|
+
zorder=10)
|
|
62
96
|
|
|
63
97
|
class chart_annotation:
|
|
64
98
|
def __init__(self, fig):
|
|
@@ -92,7 +126,7 @@ class chart_annotation:
|
|
|
92
126
|
cursor = chr(self.cur_mark)
|
|
93
127
|
if self.cur_mark % 2 == 0:
|
|
94
128
|
cursor = chr(self.cur_mark - 1) + cursor + ": "
|
|
95
|
-
cursor += utils.
|
|
129
|
+
cursor += utils.str_us_diff(int(abs(self.cur[-1] - self.cur[-2])))
|
|
96
130
|
self.cur_mark += 1
|
|
97
131
|
|
|
98
132
|
self.ann.append(ax.axvline(x=x, color="lightgray"))
|
|
@@ -101,24 +135,15 @@ class chart_annotation:
|
|
|
101
135
|
self.ann_mode = not self.ann_mode
|
|
102
136
|
event.canvas.draw()
|
|
103
137
|
|
|
104
|
-
def plot(origin: int, figsize=(16, 4), depth_max=50):
|
|
138
|
+
def plot(origin: int, figsize=(16, 4), depth_max=50, reverse=False):
|
|
105
139
|
fig = pt.figure(figsize=figsize)
|
|
106
140
|
pt.style.use("bmh")
|
|
107
141
|
pt.rcParams["font.size"] = 8
|
|
108
142
|
pt.subplots_adjust(top=0.75)
|
|
109
143
|
|
|
110
|
-
v = timeline_visitor([], 0, utils.MAX_INT, utils.MIN_INT
|
|
111
|
-
db.iterate(origin, None,
|
|
112
|
-
|
|
113
|
-
ev_relations = db.iterate_ev_relations([x["eid"] for x in v.ret])
|
|
114
|
-
ev_eid_to_xy = {}
|
|
115
|
-
for erel in v.ret:
|
|
116
|
-
ev_eid_to_xy[erel["eid"]] = (erel["x"], erel["y"])
|
|
117
|
-
|
|
118
|
-
for (orig, dest) in ev_relations:
|
|
119
|
-
(x0, y0) = ev_eid_to_xy[orig]
|
|
120
|
-
(x1, y1) = ev_eid_to_xy[dest]
|
|
121
|
-
pt.arrow(x0, y0, x1 - x0, y1 - y0, color='red')
|
|
144
|
+
v = timeline_visitor([], 0, utils.MAX_INT, utils.MIN_INT)
|
|
145
|
+
db.iterate(origin, None, v, 0, depth_max, reverse)
|
|
146
|
+
v.collect_arrows()
|
|
122
147
|
|
|
123
148
|
end = -Y_LINE_SPACING * v.y_pos
|
|
124
149
|
y_range = [float(x) for x in range(0, end, -Y_LINE_SPACING)]
|
|
@@ -131,14 +156,18 @@ def plot(origin: int, figsize=(16, 4), depth_max=50):
|
|
|
131
156
|
pt.gca().xaxis.set_major_formatter(ticker.FuncFormatter(
|
|
132
157
|
lambda value, pos: utils.str_ns(value, compact=True)))
|
|
133
158
|
|
|
159
|
+
plot_arrows(v.arrows)
|
|
134
160
|
pt.yticks(y_range, v.y_labels)
|
|
135
161
|
pt.xlabel("Time")
|
|
136
162
|
pt.autoscale(enable=True, axis="x", tight=True)
|
|
137
163
|
pt.margins(0.1)
|
|
138
|
-
|
|
164
|
+
# Keep the callback owner alive after non-blocking show() returns in
|
|
165
|
+
# notebook backends such as ipympl. Matplotlib stores weak references to
|
|
166
|
+
# bound-method callbacks.
|
|
167
|
+
setattr(fig, "_chronoscope_annotation", chart_annotation(fig))
|
|
139
168
|
|
|
140
169
|
pt.grid(True)
|
|
141
|
-
title = f"Request {utils.
|
|
170
|
+
title = f"Request {utils.format_event_id(origin)}\n"
|
|
142
171
|
title += f"[{utils.str_ns(x_range[0])}...{utils.str_ns(x_range[-1])}]"
|
|
143
172
|
pt.suptitle(title)
|
|
144
173
|
pt.show()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of Chronoscope.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-FileCopyrightText: 2024 Anatoliy Bilenko <anatoliy.bilenko@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
import chronoscope.parser as pr
|
|
11
|
+
from typing import Callable, cast
|
|
12
|
+
import subprocess as sp
|
|
13
|
+
import builtins as b
|
|
14
|
+
import peewee as p
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
db = p.SqliteDatabase(None)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class state_machine(p.Model):
|
|
21
|
+
id = p.IntegerField()
|
|
22
|
+
name = p.TextField()
|
|
23
|
+
type = p.TextField(null=True)
|
|
24
|
+
|
|
25
|
+
class Meta:
|
|
26
|
+
database = db
|
|
27
|
+
primary_key = p.CompositeKey("id")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class event(p.Model):
|
|
31
|
+
id = p.IntegerField()
|
|
32
|
+
state_machine_id = p.IntegerField()
|
|
33
|
+
time = p.IntegerField()
|
|
34
|
+
name = p.TextField()
|
|
35
|
+
|
|
36
|
+
class Meta:
|
|
37
|
+
database = db
|
|
38
|
+
primary_key = p.CompositeKey("id")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class event_relation(p.Model):
|
|
42
|
+
from_event_id = p.IntegerField(null=True)
|
|
43
|
+
to_event_id = p.IntegerField()
|
|
44
|
+
from_sm_id = p.IntegerField(null=True)
|
|
45
|
+
from_time = p.IntegerField(null=True)
|
|
46
|
+
to_sm_id = p.IntegerField()
|
|
47
|
+
to_time = p.IntegerField()
|
|
48
|
+
relation = p.TextField()
|
|
49
|
+
|
|
50
|
+
class Meta:
|
|
51
|
+
database = db
|
|
52
|
+
primary_key = p.CompositeKey("from_event_id", "to_event_id", "relation")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class state_machine_relation(p.Model):
|
|
56
|
+
from_sm_id = p.IntegerField()
|
|
57
|
+
to_sm_id = p.IntegerField()
|
|
58
|
+
relation = p.TextField()
|
|
59
|
+
|
|
60
|
+
class Meta:
|
|
61
|
+
database = db
|
|
62
|
+
primary_key = p.CompositeKey("from_sm_id", "to_sm_id", "relation")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class state_machine_attribute(p.Model):
|
|
66
|
+
state_machine_id = p.IntegerField()
|
|
67
|
+
key = p.TextField()
|
|
68
|
+
value = p.TextField()
|
|
69
|
+
|
|
70
|
+
class Meta:
|
|
71
|
+
database = db
|
|
72
|
+
primary_key = p.CompositeKey("state_machine_id", "key")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class event_attribute(p.Model):
|
|
76
|
+
event_id = p.IntegerField()
|
|
77
|
+
key = p.TextField()
|
|
78
|
+
value = p.TextField()
|
|
79
|
+
|
|
80
|
+
class Meta:
|
|
81
|
+
database = db
|
|
82
|
+
primary_key = p.CompositeKey("event_id", "key")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
TABLES = [state_machine, event, event_relation,
|
|
86
|
+
state_machine_relation, state_machine_attribute, event_attribute]
|
|
87
|
+
VERBOSE = False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def open(path: str, opts: None | dict[str, int | str] = None, create=False,
|
|
91
|
+
verbose=False):
|
|
92
|
+
global VERBOSE
|
|
93
|
+
VERBOSE = verbose
|
|
94
|
+
|
|
95
|
+
if create and os.path.exists(path):
|
|
96
|
+
raise FileExistsError(f"`{path}' exists!")
|
|
97
|
+
if not create and not os.path.exists(path):
|
|
98
|
+
raise FileNotFoundError(f"`{path}' not found!")
|
|
99
|
+
|
|
100
|
+
db.init(path, opts)
|
|
101
|
+
db.connect()
|
|
102
|
+
if create:
|
|
103
|
+
with db:
|
|
104
|
+
db.create_tables(TABLES)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def close():
|
|
108
|
+
db.close()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def mkidx():
|
|
112
|
+
db.execute_sql("CREATE INDEX event_sm_idx on event(state_machine_id);")
|
|
113
|
+
db.execute_sql("CREATE INDEX event_time_idx on event(time);")
|
|
114
|
+
db.execute_sql("CREATE INDEX event_relation_from_idx on event_relation(from_event_id);")
|
|
115
|
+
db.execute_sql("CREATE INDEX event_relation_to_idx on event_relation(to_event_id);")
|
|
116
|
+
db.execute_sql("CREATE INDEX sm_relation_from_idx on state_machine_relation(from_sm_id);")
|
|
117
|
+
db.execute_sql("CREATE INDEX sm_relation_to_idx on state_machine_relation(to_sm_id);")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def line_nr(file: str) -> int:
|
|
121
|
+
result = sp.run(['wc', file], stdout=sp.PIPE, text=True)
|
|
122
|
+
return int(result.stdout.split()[0])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def load(pr: pr.parser, trace_path: str, fd_chunk_size=900, db_chunk_size=100):
|
|
126
|
+
if not os.path.exists(trace_path):
|
|
127
|
+
raise FileNotFoundError("`{trace_path}' not found!")
|
|
128
|
+
|
|
129
|
+
with b.open(trace_path) as fd:
|
|
130
|
+
for fd_chunk in p.chunked(fd, fd_chunk_size):
|
|
131
|
+
records = pr.parse(fd_chunk)
|
|
132
|
+
with db.atomic():
|
|
133
|
+
for table in TABLES:
|
|
134
|
+
t_name: str = table._meta.name # type: ignore
|
|
135
|
+
if t_name not in records:
|
|
136
|
+
continue
|
|
137
|
+
for db_chunk in p.chunked(records[t_name], db_chunk_size):
|
|
138
|
+
table.insert_many(db_chunk).on_conflict_ignore().execute()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def iterate(origin: int, parent: None | int,
|
|
142
|
+
visit: Callable, depth: int, depth_max: int, reverse=False):
|
|
143
|
+
if depth_max < depth:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# pull events of this state machine, enriched with sm_type
|
|
147
|
+
timeline = (event
|
|
148
|
+
.select(event, state_machine.type.alias("sm_type"))
|
|
149
|
+
.join(state_machine,
|
|
150
|
+
on=(event.state_machine_id == state_machine.id))
|
|
151
|
+
.where(event.state_machine_id == origin)
|
|
152
|
+
.dicts())
|
|
153
|
+
visit(list(timeline), origin, parent)
|
|
154
|
+
|
|
155
|
+
relation_column = (state_machine_relation.to_sm_id if reverse
|
|
156
|
+
else state_machine_relation.from_sm_id)
|
|
157
|
+
related_column = (state_machine_relation.from_sm_id if reverse
|
|
158
|
+
else state_machine_relation.to_sm_id)
|
|
159
|
+
relations = state_machine_relation.select().where(
|
|
160
|
+
relation_column == origin)
|
|
161
|
+
for relation in relations.dicts():
|
|
162
|
+
relation_data = cast(dict[str, int], relation)
|
|
163
|
+
related = relation_data[related_column.name]
|
|
164
|
+
if VERBOSE:
|
|
165
|
+
print(f"@[{depth}] {hex(origin)} ... {hex(related)}")
|
|
166
|
+
iterate(related, origin, visit, depth + 1, depth_max, reverse)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def spans(event_begin: str, event_end: str, sm_type: str) -> list:
|
|
170
|
+
sql = f"""
|
|
171
|
+
SELECT (e2.time - e1.time) FROM event e1
|
|
172
|
+
JOIN event e2 ON e2.state_machine_id = e1.state_machine_id
|
|
173
|
+
JOIN state_machine sm ON sm.id = e1.state_machine_id
|
|
174
|
+
WHERE e1.name="{event_begin}" AND e2.name="{event_end}"
|
|
175
|
+
AND sm.type="{sm_type}";
|
|
176
|
+
"""
|
|
177
|
+
return db.execute_sql(sql).fetchall()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def queues(event_begin: str, event_end: str, sm_type: str) -> list:
|
|
181
|
+
sql = f"""
|
|
182
|
+
SELECT (time/1000)*1000 as timer,
|
|
183
|
+
COUNT(CASE WHEN name = "{event_begin}" THEN 1 END) as cc1,
|
|
184
|
+
COUNT(CASE WHEN name = "{event_end}" THEN 1 END) as cc2
|
|
185
|
+
FROM event JOIN state_machine sm ON sm.id = event.state_machine_id
|
|
186
|
+
WHERE sm.type="{sm_type}" GROUP BY timer;
|
|
187
|
+
"""
|
|
188
|
+
return db.execute_sql(sql).fetchall()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of Chronoscope.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-FileCopyrightText: 2024 Anatoliy Bilenko <anatoliy.bilenko@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
import chronoscope.utils as u
|
|
11
|
+
from typing import Callable
|
|
12
|
+
import yaml
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class parser:
|
|
17
|
+
def __init__(self, conf_path: str, verbose=False):
|
|
18
|
+
# type -> (dest_table, parser_func)
|
|
19
|
+
self.parsers: dict[str, tuple[str, Callable]] = {}
|
|
20
|
+
# the parser knows about table names
|
|
21
|
+
self.tables = ["state_machine", "event", "event_relation",
|
|
22
|
+
"state_machine_relation", "event_attribute"]
|
|
23
|
+
self.verbose = verbose
|
|
24
|
+
self.load_config(conf_path)
|
|
25
|
+
|
|
26
|
+
def load_config(self, conf_path: str):
|
|
27
|
+
with open(conf_path) as fd:
|
|
28
|
+
conf = yaml.safe_load(fd)
|
|
29
|
+
conf_tables = [t for t in conf.keys() if t[0] != '.']
|
|
30
|
+
if not all(t in self.tables for t in conf_tables):
|
|
31
|
+
raise SyntaxError(f"a few of {conf_tables} aren't known!")
|
|
32
|
+
|
|
33
|
+
for table in conf_tables:
|
|
34
|
+
for trace in conf[table]:
|
|
35
|
+
self.register_parser(table, trace["type"],
|
|
36
|
+
self.make_parser(table, trace["pos"]))
|
|
37
|
+
|
|
38
|
+
def make_parser(self, table: str, kwargs: dict[str, int]) -> Callable:
|
|
39
|
+
match table:
|
|
40
|
+
case "state_machine":
|
|
41
|
+
return self.make_state_machine_parser(**kwargs)
|
|
42
|
+
case "event":
|
|
43
|
+
return self.make_event_parser(**kwargs)
|
|
44
|
+
case "event_relation":
|
|
45
|
+
return self.make_event_rel_parser(**kwargs)
|
|
46
|
+
case "state_machine_relation":
|
|
47
|
+
return self.make_sm_rel_parser(**kwargs)
|
|
48
|
+
case "event_attribute":
|
|
49
|
+
return self.make_event_attribute_parser(**kwargs)
|
|
50
|
+
raise NotImplementedError()
|
|
51
|
+
|
|
52
|
+
def make_event_parser(self, type: int, time: int, event: int,
|
|
53
|
+
sm_id: int) -> Callable:
|
|
54
|
+
def parse(line: list[str], parse_type: str):
|
|
55
|
+
if type >= len(line) or parse_type != line[type]:
|
|
56
|
+
return None
|
|
57
|
+
eid = None
|
|
58
|
+
for token in line:
|
|
59
|
+
if token.startswith("eid="):
|
|
60
|
+
eid = int(token.split("=", 1)[1], 16)
|
|
61
|
+
break
|
|
62
|
+
if eid is None:
|
|
63
|
+
return None
|
|
64
|
+
sm = int(line[sm_id], 0)
|
|
65
|
+
return {
|
|
66
|
+
"state_machine": {"id": sm, "name": parse_type, "type": parse_type},
|
|
67
|
+
"event": {"id": eid, "state_machine_id": sm,
|
|
68
|
+
"time": u.ns(line[time]), "name": line[event]},
|
|
69
|
+
}
|
|
70
|
+
return parse
|
|
71
|
+
|
|
72
|
+
def make_event_rel_parser(self, type: int, sm_id: int,
|
|
73
|
+
peid: int, eid: int, time: int) -> Callable:
|
|
74
|
+
def parse(line: list[str], parse_type: str):
|
|
75
|
+
if type >= len(line) or parse_type != line[type]:
|
|
76
|
+
return None
|
|
77
|
+
raw_peid = line[peid].split("=", 1)[1]
|
|
78
|
+
from_eid = None if raw_peid == "None" else int(raw_peid, 16)
|
|
79
|
+
to_eid = int(line[eid].split("=", 1)[1], 16)
|
|
80
|
+
sm = int(line[sm_id], 0)
|
|
81
|
+
ts = u.ns(line[time])
|
|
82
|
+
return {
|
|
83
|
+
"event_relation": {
|
|
84
|
+
"from_event_id": from_eid,
|
|
85
|
+
"to_event_id": to_eid,
|
|
86
|
+
"from_sm_id": sm if from_eid is not None else None,
|
|
87
|
+
"from_time": ts if from_eid is not None else None,
|
|
88
|
+
"to_sm_id": sm,
|
|
89
|
+
"to_time": ts,
|
|
90
|
+
"relation": parse_type,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
return parse
|
|
94
|
+
|
|
95
|
+
def make_state_machine_parser(self, type: int, time: int,
|
|
96
|
+
sm_id: int, name: int, state: int) -> Callable:
|
|
97
|
+
def parse(line: list[str], parse_type: str):
|
|
98
|
+
if type >= len(line) or parse_type != line[type]:
|
|
99
|
+
return None
|
|
100
|
+
sm = int(line[sm_id].split("=", 1)[1], 0)
|
|
101
|
+
raw_name = line[name].split("=", 1)[1]
|
|
102
|
+
raw_state = line[state].split("=", 1)[1]
|
|
103
|
+
eid = None
|
|
104
|
+
for token in line:
|
|
105
|
+
if token.startswith("eid="):
|
|
106
|
+
eid = int(token.split("=", 1)[1], 16)
|
|
107
|
+
break
|
|
108
|
+
if eid is None:
|
|
109
|
+
return None
|
|
110
|
+
return {
|
|
111
|
+
"state_machine": {"id": sm, "name": raw_name, "type": raw_name},
|
|
112
|
+
"event": {"id": eid, "state_machine_id": sm,
|
|
113
|
+
"time": u.ns(line[time]), "name": raw_state},
|
|
114
|
+
}
|
|
115
|
+
return parse
|
|
116
|
+
|
|
117
|
+
def make_sm_rel_parser(self, type: int,
|
|
118
|
+
from_sm_id: int, to_sm_id: int,
|
|
119
|
+
relation: int) -> Callable:
|
|
120
|
+
def parse(line: list[str], parse_type: str):
|
|
121
|
+
if type >= len(line) or parse_type != line[type]:
|
|
122
|
+
return None
|
|
123
|
+
from_sm = int(line[from_sm_id].split("=", 1)[1], 0)
|
|
124
|
+
to_sm = int(line[to_sm_id].split("=", 1)[1], 0)
|
|
125
|
+
rel = line[relation].split("=", 1)[1]
|
|
126
|
+
record = {
|
|
127
|
+
"state_machine_relation": {
|
|
128
|
+
"from_sm_id": from_sm,
|
|
129
|
+
"to_sm_id": to_sm,
|
|
130
|
+
"relation": rel,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if rel == "top-to-raft":
|
|
134
|
+
record["state_machine"] = {"id": from_sm, "name": "top", "type": "top"}
|
|
135
|
+
return record
|
|
136
|
+
return parse
|
|
137
|
+
|
|
138
|
+
def make_event_attribute_parser(self, type: int,
|
|
139
|
+
eid: int, attribute: int) -> Callable:
|
|
140
|
+
def parse(line: list[str], parse_type: str):
|
|
141
|
+
if type >= len(line) or parse_type != line[type]:
|
|
142
|
+
return None
|
|
143
|
+
event_id = int(line[eid].split("=", 1)[1], 16)
|
|
144
|
+
key, value = line[attribute].split("=", 1)
|
|
145
|
+
return {
|
|
146
|
+
"event_attribute": {
|
|
147
|
+
"event_id": event_id,
|
|
148
|
+
"key": key,
|
|
149
|
+
"value": value,
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
return parse
|
|
153
|
+
|
|
154
|
+
def register_parser(self, dest_table: str, type: str, parse: Callable):
|
|
155
|
+
self.parsers[type] = (dest_table, parse)
|
|
156
|
+
|
|
157
|
+
def parse(self,
|
|
158
|
+
fd_chunk: list[str]) -> dict[str, list[dict[str, str | int]]]:
|
|
159
|
+
records: dict[str, list] = {t: [] for t in self.tables}
|
|
160
|
+
for line in fd_chunk:
|
|
161
|
+
for p_name, (dest_table, parser) in self.parsers.items():
|
|
162
|
+
try:
|
|
163
|
+
if record := parser(line.split(), p_name):
|
|
164
|
+
self._merge_record(records, record)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
if self.verbose:
|
|
167
|
+
print(f"{e}: {line=}", file=sys.stderr)
|
|
168
|
+
return records
|
|
169
|
+
|
|
170
|
+
def _merge_record(self, records: dict, record: dict):
|
|
171
|
+
for table, payload in record.items():
|
|
172
|
+
if isinstance(payload, list):
|
|
173
|
+
records[table].extend(payload)
|
|
174
|
+
else:
|
|
175
|
+
records[table].append(payload)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of Chronoscope.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-FileCopyrightText: 2024 Anatoliy Bilenko <anatoliy.bilenko@gmail.com>
|
|
6
|
+
#
|
|
7
|
+
# SPDX-License-Identifier: LGPL-3.0-only
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
import chronoscope.db as db
|
|
11
|
+
import chronoscope.utils as utils
|
|
12
|
+
from graphviz import Graph # type: ignore
|
|
13
|
+
from typing import cast
|
|
14
|
+
|
|
15
|
+
# https://graphviz.readthedocs.io/en/stable/examples.html#structs-py
|
|
16
|
+
FMT_NODE = """<
|
|
17
|
+
<table border="0" cellborder="1" cellspacing="0">
|
|
18
|
+
<tr><td>{}</td></tr><tr><td>{}</td></tr></table>>
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
class attr_visitor:
|
|
22
|
+
def __init__(self, graph: Graph):
|
|
23
|
+
self.graph = graph
|
|
24
|
+
|
|
25
|
+
def __call__(self, timeline: list[dict], current: int,
|
|
26
|
+
parent: None | int):
|
|
27
|
+
machine = cast(dict[str, object],
|
|
28
|
+
(db.state_machine
|
|
29
|
+
.select()
|
|
30
|
+
.where(db.state_machine.id == current)
|
|
31
|
+
.dicts()
|
|
32
|
+
.get()))
|
|
33
|
+
attributes = (db.state_machine_attribute
|
|
34
|
+
.select()
|
|
35
|
+
.where(db.state_machine_attribute.state_machine_id == current)
|
|
36
|
+
.dicts())
|
|
37
|
+
rows = [f"name={machine['name']}", f"type={machine['type']}"]
|
|
38
|
+
for attribute in attributes:
|
|
39
|
+
attribute_data = cast(dict[str, object], attribute)
|
|
40
|
+
rows.append(f"{attribute_data['key']}={attribute_data['value']}")
|
|
41
|
+
contents = "<br/>".join(rows)
|
|
42
|
+
label = FMT_NODE.format(utils.format_event_id(current), contents)
|
|
43
|
+
self.graph.node(str(current), label)
|
|
44
|
+
|
|
45
|
+
if parent is not None:
|
|
46
|
+
self.graph.edge(str(parent), str(current))
|
|
47
|
+
|
|
48
|
+
def plot(origin: int, depth_max=50):
|
|
49
|
+
g = Graph(strict=True, format="png", node_attr={"shape": "plaintext"})
|
|
50
|
+
db.iterate(origin, None, attr_visitor(g), 0, depth_max)
|
|
51
|
+
net_pid, boot_counter, id = utils.unpack_event_id(origin)
|
|
52
|
+
g.render(f"tree_{net_pid}_{boot_counter}_{id}", cleanup=True)
|
|
@@ -17,6 +17,9 @@ MAX_INT = sys.maxsize
|
|
|
17
17
|
MIN_INT = -sys.maxsize - 1
|
|
18
18
|
BITS_PER_PID = 16
|
|
19
19
|
DB_INT_SIZE = 64
|
|
20
|
+
NET_PID_BITS = 4
|
|
21
|
+
BOOT_COUNTER_BITS = 12
|
|
22
|
+
EVENT_COUNTER_BITS = 48
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def pack_unpack_init(p_bits_per_pid: int, p_db_int_size: int):
|
|
@@ -35,6 +38,17 @@ def unpack(id_pid: int) -> tuple[int, int]:
|
|
|
35
38
|
id = ((1 << (DB_INT_SIZE - BITS_PER_PID)) - 1) & id_pid
|
|
36
39
|
return pid, id
|
|
37
40
|
|
|
41
|
+
def unpack_event_id(event_id: int) -> tuple[int, int, int]:
|
|
42
|
+
net_pid = event_id >> (BOOT_COUNTER_BITS + EVENT_COUNTER_BITS)
|
|
43
|
+
boot_mask = (1 << BOOT_COUNTER_BITS) - 1
|
|
44
|
+
boot_counter = (event_id >> EVENT_COUNTER_BITS) & boot_mask
|
|
45
|
+
counter = event_id & ((1 << EVENT_COUNTER_BITS) - 1)
|
|
46
|
+
return net_pid, boot_counter, counter
|
|
47
|
+
|
|
48
|
+
def format_event_id(event_id: int) -> str:
|
|
49
|
+
net_pid, boot_counter, counter = unpack_event_id(event_id)
|
|
50
|
+
return f"({net_pid} {boot_counter} {counter})"
|
|
51
|
+
|
|
38
52
|
def ns(time: str) -> int:
|
|
39
53
|
if len(time) != NS_TIME_LEN:
|
|
40
54
|
raise ValueError("Not a nanosecond time format")
|
|
@@ -47,5 +61,5 @@ def str_ns(unix_time_ns: int, compact=False) -> str:
|
|
|
47
61
|
return dt.strftime(FMT_MS_COMPACT)
|
|
48
62
|
return dt.strftime(FMT_MS)
|
|
49
63
|
|
|
50
|
-
def
|
|
51
|
-
return str(unix_time_ns) + "
|
|
64
|
+
def str_us_diff(unix_time_ns: int) -> str:
|
|
65
|
+
return str(unix_time_ns // 1_000) + "us"
|
|
@@ -21,14 +21,18 @@ class timeline_visitor:
|
|
|
21
21
|
self.timetable = timetable
|
|
22
22
|
|
|
23
23
|
def __call__(self, timeline: list[dict], origin: int, parent: None | int):
|
|
24
|
+
if not timeline:
|
|
25
|
+
return
|
|
26
|
+
|
|
24
27
|
t0 = timeline[0]
|
|
25
|
-
var = "{}_{}_{}".format(
|
|
28
|
+
var = "{}_{}_{}_{}".format(
|
|
29
|
+
t0["sm_type"], *u.unpack_event_id(origin))
|
|
26
30
|
|
|
27
31
|
ctr = self.writer.register_var("c", var, "string")
|
|
28
32
|
self.gtkw.trace(f"c.{var}")
|
|
29
33
|
|
|
30
34
|
self.counters[var] = ctr
|
|
31
|
-
self.timetable += [{"var": var, "time": t["time"], "event": t["
|
|
35
|
+
self.timetable += [{"var": var, "time": t["time"], "event": t["name"]}
|
|
32
36
|
for t in timeline]
|
|
33
37
|
|
|
34
38
|
|
|
@@ -42,9 +46,11 @@ def plot(origin: int, depth_max=50):
|
|
|
42
46
|
counters: dict = {}
|
|
43
47
|
timetable: list[dict] = []
|
|
44
48
|
v = timeline_visitor(gtkw, writer, counters, timetable)
|
|
45
|
-
db.iterate(origin, None,
|
|
49
|
+
db.iterate(origin, None, v, 0, depth_max)
|
|
46
50
|
|
|
47
51
|
timetable.sort(key=lambda t: t["time"])
|
|
52
|
+
if not timetable:
|
|
53
|
+
return
|
|
48
54
|
t0 = timetable[0]["time"]
|
|
49
55
|
for t in timetable:
|
|
50
56
|
writer.change(counters[t["var"]], t["time"] - t0, t["event"])
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
#
|
|
3
|
-
# This file is part of Chronoscope.
|
|
4
|
-
#
|
|
5
|
-
# SPDX-FileCopyrightText: 2024 Anatoliy Bilenko <anatoliy.bilenko@gmail.com>
|
|
6
|
-
#
|
|
7
|
-
# SPDX-License-Identifier: LGPL-3.0-only
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
import chronoscope.parser as pr
|
|
11
|
-
from typing import Callable
|
|
12
|
-
import subprocess as sp
|
|
13
|
-
import builtins as b
|
|
14
|
-
import peewee as p
|
|
15
|
-
import os
|
|
16
|
-
|
|
17
|
-
db = p.SqliteDatabase(None)
|
|
18
|
-
|
|
19
|
-
class T(p.Model):
|
|
20
|
-
class Meta:
|
|
21
|
-
database = db
|
|
22
|
-
primary_key = False
|
|
23
|
-
|
|
24
|
-
class tick(T):
|
|
25
|
-
id = p.IntegerField()
|
|
26
|
-
eid = p.IntegerField()
|
|
27
|
-
time = p.IntegerField()
|
|
28
|
-
event = p.TextField()
|
|
29
|
-
type = p.TextField()
|
|
30
|
-
|
|
31
|
-
class attr(T):
|
|
32
|
-
id = p.IntegerField()
|
|
33
|
-
name = p.TextField()
|
|
34
|
-
val = p.TextField()
|
|
35
|
-
|
|
36
|
-
class relation(p.Model):
|
|
37
|
-
orig = p.IntegerField()
|
|
38
|
-
dest = p.IntegerField()
|
|
39
|
-
type = p.TextField()
|
|
40
|
-
|
|
41
|
-
class Meta:
|
|
42
|
-
database = db
|
|
43
|
-
primary_key = p.CompositeKey("orig", "dest")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
TABLES = [tick, attr, relation]
|
|
47
|
-
VERBOSE = False
|
|
48
|
-
|
|
49
|
-
def open(path: str, opts: None | dict[str, int | str] = None, create=False,
|
|
50
|
-
verbose=False):
|
|
51
|
-
global VERBOSE
|
|
52
|
-
VERBOSE = verbose
|
|
53
|
-
|
|
54
|
-
if create and os.path.exists(path):
|
|
55
|
-
raise FileExistsError(f"`{path}' exists!")
|
|
56
|
-
if not create and not os.path.exists(path):
|
|
57
|
-
raise FileNotFoundError(f"`{path}' not found!")
|
|
58
|
-
|
|
59
|
-
db.init(path, opts)
|
|
60
|
-
db.connect()
|
|
61
|
-
if create:
|
|
62
|
-
with db:
|
|
63
|
-
db.create_tables(TABLES)
|
|
64
|
-
|
|
65
|
-
def close():
|
|
66
|
-
db.close()
|
|
67
|
-
|
|
68
|
-
def mkidx():
|
|
69
|
-
db.execute_sql("CREATE INDEX tick_idx on tick(id);")
|
|
70
|
-
db.execute_sql("CREATE INDEX relation_idx on relation(orig,dest);")
|
|
71
|
-
db.execute_sql("CREATE INDEX attr_idx on attr(id);")
|
|
72
|
-
|
|
73
|
-
def line_nr(file: str) -> int:
|
|
74
|
-
result = sp.run(['wc', file], stdout=sp.PIPE, text=True)
|
|
75
|
-
return int(result.stdout.split()[0])
|
|
76
|
-
|
|
77
|
-
def load(pr: pr.parser, trace_path: str, fd_chunk_size=900, db_chunk_size=100):
|
|
78
|
-
if not os.path.exists(trace_path):
|
|
79
|
-
raise FileNotFoundError("`{trace_path}' not found!")
|
|
80
|
-
|
|
81
|
-
with b.open(trace_path) as fd:
|
|
82
|
-
for fd_chunk in p.chunked(fd, fd_chunk_size):
|
|
83
|
-
records = pr.parse(fd_chunk)
|
|
84
|
-
with db.atomic():
|
|
85
|
-
for table in TABLES:
|
|
86
|
-
t_name: str = table._meta.name # type: ignore
|
|
87
|
-
for db_chunk in p.chunked(records[t_name], db_chunk_size):
|
|
88
|
-
table.insert_many(db_chunk).execute()
|
|
89
|
-
|
|
90
|
-
def iterate(origin: int, parent: None | int, samples: type[tick] | type[attr],
|
|
91
|
-
visit: Callable, depth: int, depth_max: int):
|
|
92
|
-
if depth_max < depth:
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
# pull origin
|
|
96
|
-
if timeline := samples.select().where((samples.id == origin)):
|
|
97
|
-
visit(timeline.dicts(), origin, parent)
|
|
98
|
-
|
|
99
|
-
# pull children
|
|
100
|
-
orig_to_children = relation.select().where((relation.orig == origin))
|
|
101
|
-
for child in orig_to_children.dicts():
|
|
102
|
-
if VERBOSE:
|
|
103
|
-
print(f"@[{depth}] {hex(child['orig'])} ... {hex(child['dest'])}")
|
|
104
|
-
iterate(child["dest"], origin, samples, visit, depth + 1, depth_max)
|
|
105
|
-
|
|
106
|
-
def iterate_ev_relations(events: list[int]) -> list[(int, int)]:
|
|
107
|
-
relations = relation.select().where((relation.orig.in_(events))).dicts()
|
|
108
|
-
return [(rel["orig"], rel["dest"]) for rel in relations]
|
|
109
|
-
|
|
110
|
-
def spans(event_begin: str, event_end: str, tick_type: str) -> list:
|
|
111
|
-
sql = f"""
|
|
112
|
-
SELECT (tk.time - tick.time) FROM tick JOIN tick tk ON tk.id=tick.id
|
|
113
|
-
WHERE tick.event="{event_begin}" AND tk.event="{event_end}"
|
|
114
|
-
AND tick.type="{tick_type}";
|
|
115
|
-
"""
|
|
116
|
-
return db.execute_sql(sql).fetchall()
|
|
117
|
-
|
|
118
|
-
def queues(event_begin: str, event_end: str, tick_type: str) -> list:
|
|
119
|
-
sql = f"""
|
|
120
|
-
SELECT (time/1000)*1000 as timer,
|
|
121
|
-
COUNT(CASE WHEN event = "{event_begin}" THEN 1 END) as cc1,
|
|
122
|
-
COUNT(CASE WHEN event = "{event_end}" THEN 1 END) as cc2
|
|
123
|
-
FROM tick WHERE type="{tick_type}" GROUP BY timer;
|
|
124
|
-
"""
|
|
125
|
-
return db.execute_sql(sql).fetchall()
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
#
|
|
3
|
-
# This file is part of Chronoscope.
|
|
4
|
-
#
|
|
5
|
-
# SPDX-FileCopyrightText: 2024 Anatoliy Bilenko <anatoliy.bilenko@gmail.com>
|
|
6
|
-
#
|
|
7
|
-
# SPDX-License-Identifier: LGPL-3.0-only
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
import chronoscope.utils as u
|
|
11
|
-
from typing import Callable
|
|
12
|
-
import yaml
|
|
13
|
-
import sys
|
|
14
|
-
|
|
15
|
-
class parser:
|
|
16
|
-
def __init__(self, conf_path: str, verbose=False):
|
|
17
|
-
# type -> (dest_table, parser_func)
|
|
18
|
-
self.parsers: dict[str, tuple[str, Callable]] = {}
|
|
19
|
-
# the parser knows about table names
|
|
20
|
-
self.tables = ["tick", "attr", "relation"]
|
|
21
|
-
self.verbose = verbose
|
|
22
|
-
self.load_config(conf_path)
|
|
23
|
-
|
|
24
|
-
def load_config(self, conf_path: str):
|
|
25
|
-
with open(conf_path) as fd:
|
|
26
|
-
conf = yaml.safe_load(fd)
|
|
27
|
-
conf_tables = [t for t in conf.keys() if t[0] != '.']
|
|
28
|
-
if not all(t in self.tables for t in conf_tables):
|
|
29
|
-
raise SyntaxError(f"a few of {conf_tables} aren't known!")
|
|
30
|
-
|
|
31
|
-
for table in conf_tables:
|
|
32
|
-
for trace in conf[table]:
|
|
33
|
-
self.register_parser(table, trace["type"],
|
|
34
|
-
self.make_parser(table, trace["pos"]))
|
|
35
|
-
|
|
36
|
-
def make_parser(self, table: str, kwargs: dict[str, int]) -> Callable:
|
|
37
|
-
match table:
|
|
38
|
-
case "tick":
|
|
39
|
-
return self.make_req_parser(**kwargs)
|
|
40
|
-
case "relation":
|
|
41
|
-
return self.make_rel_parser(**kwargs)
|
|
42
|
-
case "attr":
|
|
43
|
-
return self.make_attr_parser(**kwargs)
|
|
44
|
-
raise NotImplementedError()
|
|
45
|
-
|
|
46
|
-
def make_req_parser(self, type: int, time: int, event: int,
|
|
47
|
-
pid: int, id: int, eid: int) -> Callable:
|
|
48
|
-
def parse(line: list[str], parse_type: str):
|
|
49
|
-
return {
|
|
50
|
-
"time": u.ns(line[time]),
|
|
51
|
-
"type": line[type], "event": line[event],
|
|
52
|
-
"eid": u.pack(int(line[eid]), int(line[pid])),
|
|
53
|
-
"id": u.pack(int(line[id]), int(line[pid]))
|
|
54
|
-
} if type < len(line) and parse_type == line[type] else None
|
|
55
|
-
return parse
|
|
56
|
-
|
|
57
|
-
def make_rel_parser(self, orig_id: int, dest_id: int,
|
|
58
|
-
orig_pid: int, dest_pid: int, type: int) -> Callable:
|
|
59
|
-
def parse(line: list[str], parse_type: str):
|
|
60
|
-
return {
|
|
61
|
-
"orig": u.pack(int(line[orig_id]), int(line[orig_pid])),
|
|
62
|
-
"dest": u.pack(int(line[dest_id]), int(line[dest_pid])),
|
|
63
|
-
"type": line[type]
|
|
64
|
-
} if type < len(line) and parse_type == line[type] else None
|
|
65
|
-
return parse
|
|
66
|
-
|
|
67
|
-
def make_attr_parser(self, id: int, pid: int,
|
|
68
|
-
name: int, value: int, type: int) -> Callable:
|
|
69
|
-
def parse(line: list[str], parse_type: str):
|
|
70
|
-
return {
|
|
71
|
-
"id": u.pack(int(line[id]), int(line[pid])),
|
|
72
|
-
"val": line[value],
|
|
73
|
-
"name": line[name],
|
|
74
|
-
} if type < len(line) and parse_type == line[type] else None
|
|
75
|
-
return parse
|
|
76
|
-
|
|
77
|
-
def register_parser(self, dest_table: str, type: str, parse: Callable):
|
|
78
|
-
self.parsers[type] = (dest_table, parse)
|
|
79
|
-
|
|
80
|
-
def parse(self,
|
|
81
|
-
fd_chunk: list[str]) -> dict[str, list[dict[str, str | int]]]:
|
|
82
|
-
records: dict[str, list] = {t: [] for t in self.tables}
|
|
83
|
-
for line in fd_chunk:
|
|
84
|
-
for p_name, (dest_table, parser) in self.parsers.items():
|
|
85
|
-
try:
|
|
86
|
-
if record := parser(line.split(), p_name):
|
|
87
|
-
records[dest_table].append(record)
|
|
88
|
-
except Exception as e:
|
|
89
|
-
if self.verbose:
|
|
90
|
-
print(f"{e}: {line=}", file=sys.stderr)
|
|
91
|
-
return records
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
#
|
|
3
|
-
# This file is part of Chronoscope.
|
|
4
|
-
#
|
|
5
|
-
# SPDX-FileCopyrightText: 2024 Anatoliy Bilenko <anatoliy.bilenko@gmail.com>
|
|
6
|
-
#
|
|
7
|
-
# SPDX-License-Identifier: LGPL-3.0-only
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
import chronoscope.db as db
|
|
11
|
-
import chronoscope.utils as utils
|
|
12
|
-
from graphviz import Graph # type: ignore
|
|
13
|
-
|
|
14
|
-
# https://graphviz.readthedocs.io/en/stable/examples.html#structs-py
|
|
15
|
-
FMT_NODE = """<
|
|
16
|
-
<table border="0" cellborder="1" cellspacing="0">
|
|
17
|
-
<tr><td>{}</td></tr><tr><td>{}</td></tr></table>>
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
class attr_visitor:
|
|
21
|
-
def __init__(self, graph: Graph):
|
|
22
|
-
self.graph = graph
|
|
23
|
-
|
|
24
|
-
def __call__(self, node_attrs: list[dict], current: int, parent: None | int):
|
|
25
|
-
contents = "<br/>".join([f"{na['name']}={na['val']}" for na in node_attrs])
|
|
26
|
-
self.graph.node(str(current), FMT_NODE.format(utils.unpack(current), contents))
|
|
27
|
-
|
|
28
|
-
if parent:
|
|
29
|
-
self.graph.edge(str(parent), str(current))
|
|
30
|
-
|
|
31
|
-
def plot(origin: int, depth_max=50):
|
|
32
|
-
g = Graph(strict=True, format="png", node_attr={"shape": "plaintext"})
|
|
33
|
-
db.iterate(origin, None, db.attr, attr_visitor(g), 0, depth_max)
|
|
34
|
-
pid, id = utils.unpack(origin)
|
|
35
|
-
g.render(f"tree_{pid}_{id}", cleanup=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|