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.
Files changed (25) hide show
  1. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0/Chronoscope.egg-info}/PKG-INFO +1 -1
  2. {chronoscope-0.0.4.dev1/Chronoscope.egg-info → chronoscope-0.0.5.dev0}/PKG-INFO +1 -1
  3. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/README.md +1 -0
  4. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/__init__.py +5 -2
  5. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/chart.py +59 -30
  6. chronoscope-0.0.5.dev0/chronoscope/db.py +188 -0
  7. chronoscope-0.0.5.dev0/chronoscope/parser.py +175 -0
  8. chronoscope-0.0.5.dev0/chronoscope/tree.py +52 -0
  9. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/utils.py +16 -2
  10. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/vcd.py +9 -3
  11. chronoscope-0.0.4.dev1/chronoscope/db.py +0 -125
  12. chronoscope-0.0.4.dev1/chronoscope/parser.py +0 -91
  13. chronoscope-0.0.4.dev1/chronoscope/tree.py +0 -35
  14. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/COPYING +0 -0
  15. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/SOURCES.txt +0 -0
  16. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/dependency_links.txt +0 -0
  17. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/entry_points.txt +0 -0
  18. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/requires.txt +0 -0
  19. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/Chronoscope.egg-info/top_level.txt +0 -0
  20. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/__main__.py +0 -0
  21. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/drawer.py +0 -0
  22. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/hist.py +0 -0
  23. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/chronoscope/queue.py +0 -0
  24. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/setup.cfg +0 -0
  25. {chronoscope-0.0.4.dev1 → chronoscope-0.0.5.dev0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Chronoscope
3
- Version: 0.0.4.dev1
3
+ Version: 0.0.5.dev0
4
4
  Summary: A cross-platform matplotlib-based observability tool
5
5
  Home-page: https://github.com/just-now/chronoscope
6
6
  Author: Anatoliy Bilenko
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Chronoscope
3
- Version: 0.0.4.dev1
3
+ Version: 0.0.5.dev0
4
4
  Summary: A cross-platform matplotlib-based observability tool
5
5
  Home-page: https://github.com/just-now/chronoscope
6
6
  Author: Anatoliy Bilenko
@@ -1,5 +1,6 @@
1
1
  ![pytest/systest workflow](https://github.com/just-now/chronoscope/actions/workflows/python-package.yml/badge.svg)
2
2
  ![systest workflow](https://github.com/just-now/chronoscope/actions/workflows/makefile.yml/badge.svg)
3
+ [![Open Raft demo in Colab](https://colab.research.google.com/assets/colab-badge.svg)](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.4.dev1'
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) -> list[dict]:
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["event"]
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=45)
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]["event"], rotation=45)
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
- ret: list[dict]
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
- self.ret += plot_timeline(timeline, self.y_pos)
54
- self.y_pos += 1
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
- type, id = timeline[0]["type"], timeline[0]["id"]
57
- self.y_labels.append(f"{type}{utils.unpack(id)} [{duration}ms]")
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.str_ns_diff(int(abs(self.cur[-1] - self.cur[-2])))
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, db.tick, v, 0, depth_max)
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
- _ = chart_annotation(fig)
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.unpack(origin)}\n"
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 str_ns_diff(unix_time_ns: int) -> str:
51
- return str(unix_time_ns) + "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(t0["type"], *u.unpack(t0["id"]))
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["event"]}
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, db.tick, v, 0, depth_max)
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)