vizcontrol 0.1.0__py3-none-any.whl
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.
- vizcontrol/__init__.py +10 -0
- vizcontrol/__main__.py +3 -0
- vizcontrol/analysis.py +294 -0
- vizcontrol/capture.py +622 -0
- vizcontrol/cli.py +131 -0
- vizcontrol/config.py +105 -0
- vizcontrol/pytest_plugin.py +103 -0
- vizcontrol/runners.py +219 -0
- vizcontrol/serialize.py +311 -0
- vizcontrol/server.py +298 -0
- vizcontrol/store.py +291 -0
- vizcontrol/viewer/static/assets/index-D5C41TsO.js +43 -0
- vizcontrol/viewer/static/assets/index-Dsv2gkLb.css +1 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-400-normal-BLGc9T1a.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-400-normal-ZzOtrSSW.woff +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-600-normal-BGBWG807.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-600-normal-BuzJQFbW.woff +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-700-normal-Bc8_fv8J.woff +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-700-normal-bGtGjVdZ.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-ext-400-normal-BPnxn4xp.woff +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-ext-400-normal-Dc4VJyIJ.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-ext-600-normal-Bt9VVOA-.woff +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-ext-600-normal-CaqZN2hq.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-ext-700-normal-Ced3hgUT.woff +0 -0
- vizcontrol/viewer/static/assets/inter-cyrillic-ext-700-normal-ClVoMEGq.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-greek-400-normal-BZzXV7-1.woff +0 -0
- vizcontrol/viewer/static/assets/inter-greek-400-normal-DxZsaF_h.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-greek-600-normal-CwicyhtI.woff +0 -0
- vizcontrol/viewer/static/assets/inter-greek-600-normal-Dhlb-90d.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-greek-700-normal-BRYTaFLL.woff +0 -0
- vizcontrol/viewer/static/assets/inter-greek-700-normal-Cxpycf-U.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-greek-ext-400-normal-Bput3-QP.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-greek-ext-400-normal-DCpCPQOf.woff +0 -0
- vizcontrol/viewer/static/assets/inter-greek-ext-600-normal-C9WLioJ8.woff +0 -0
- vizcontrol/viewer/static/assets/inter-greek-ext-600-normal-Cnui8OiR.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-greek-ext-700-normal-DXvzx4Na.woff +0 -0
- vizcontrol/viewer/static/assets/inter-greek-ext-700-normal-SzCdnevJ.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-latin-400-normal-BOOGhInR.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-latin-400-normal-gitzw0hO.woff +0 -0
- vizcontrol/viewer/static/assets/inter-latin-600-normal-B5cFAncS.woff +0 -0
- vizcontrol/viewer/static/assets/inter-latin-600-normal-D273HNI0.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-latin-700-normal-B8MtJ_2k.woff +0 -0
- vizcontrol/viewer/static/assets/inter-latin-700-normal-Sckx8rpT.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-latin-ext-400-normal-C1t-h-pH.woff +0 -0
- vizcontrol/viewer/static/assets/inter-latin-ext-400-normal-hnt3BR84.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-latin-ext-600-normal-BnYJhD27.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-latin-ext-600-normal-CAF0vJDd.woff +0 -0
- vizcontrol/viewer/static/assets/inter-latin-ext-700-normal-6V9MnIL5.woff +0 -0
- vizcontrol/viewer/static/assets/inter-latin-ext-700-normal-CzikT_rs.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-vietnamese-400-normal-BUNmGMP1.woff +0 -0
- vizcontrol/viewer/static/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-vietnamese-600-normal-Cm6aH8_k.woff +0 -0
- vizcontrol/viewer/static/assets/inter-vietnamese-700-normal-CGpBpxLq.woff2 +0 -0
- vizcontrol/viewer/static/assets/inter-vietnamese-700-normal-dAnkLlTo.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-cyrillic-400-normal-CIEtRijs.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-cyrillic-600-normal-BB5xRk30.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-greek-400-normal-CNojz0t3.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-greek-600-normal-3KAm9r92.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-latin-400-normal-B11XCQ5g.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-latin-600-normal-DXv3JcnD.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-latin-ext-400-normal-DHMOLxAi.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-latin-ext-600-normal-DiQlUoE6.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-vietnamese-400-normal-BeU3T8wf.woff +0 -0
- vizcontrol/viewer/static/assets/jetbrains-mono-vietnamese-600-normal-CSpzN4cI.woff +0 -0
- vizcontrol/viewer/static/index.html +13 -0
- vizcontrol-0.1.0.dist-info/METADATA +65 -0
- vizcontrol-0.1.0.dist-info/RECORD +79 -0
- vizcontrol-0.1.0.dist-info/WHEEL +4 -0
- vizcontrol-0.1.0.dist-info/entry_points.txt +5 -0
- vizcontrol-0.1.0.dist-info/licenses/LICENSE +661 -0
vizcontrol/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""VizControl: observe real Python executions and explore them as graphs."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
FORMAT_VERSION = "1.0"
|
|
5
|
+
|
|
6
|
+
from vizcontrol.config import Config, load_config # noqa: E402
|
|
7
|
+
from vizcontrol.runners import trace, traced # noqa: E402
|
|
8
|
+
from vizcontrol.serialize import register # noqa: E402
|
|
9
|
+
|
|
10
|
+
__all__ = ["Config", "load_config", "register", "trace", "traced", "__version__"]
|
vizcontrol/__main__.py
ADDED
vizcontrol/analysis.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Turn stored, observed events into what the viewer navigates. Pure functions over a Reader.
|
|
2
|
+
|
|
3
|
+
Everything computed here (stages, groups, flow links, change attribution) is *inferred* and says so.
|
|
4
|
+
See docs/components/analysis.md.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import fnmatch
|
|
10
|
+
import json
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
|
|
13
|
+
from vizcontrol.serialize import preview
|
|
14
|
+
from vizcontrol.store import Reader
|
|
15
|
+
|
|
16
|
+
# -- nodes --------------------------------------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
def _duration(c: dict) -> int | None:
|
|
19
|
+
return c["end_ns"] - c["start_ns"] if c.get("end_ns") is not None else None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def call_node(c: dict, stage: str | None = None) -> dict:
|
|
23
|
+
"""The JSON shape of one call, as the viewer receives it."""
|
|
24
|
+
return {
|
|
25
|
+
"type": "call", "id": c["id"], "parent_id": c["parent_id"], "code_id": c["code_id"], "qualname": c["qualname"],
|
|
26
|
+
"module": c["module"],
|
|
27
|
+
"kind": c["kind"], "observed": bool(c["observed"]), "outcome": c["outcome"], "start_ns": c["start_ns"],
|
|
28
|
+
"duration_ns": _duration(c), "end_exact": bool(c["end_exact"]), "children": c["children"],
|
|
29
|
+
"depth": c["depth"], "seq": c["seq_start"], "call_line": c["call_line"], "exception_id": c["exception_id"],
|
|
30
|
+
"thread_id": c["thread_id"], "file": c.get("relpath") or c.get("path"), "first_line": c["first_line"],
|
|
31
|
+
"stage": stage,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def subtree(reader: Reader, call_id: int | None = None, depth: int = 1, limit: int = 200,
|
|
36
|
+
stage_of=None) -> list[dict]:
|
|
37
|
+
"""Children of `call_id` (roots if None), `depth` levels deep. Siblings running the same code fold into one
|
|
38
|
+
group node (FR-ANA-3): a 10,000-iteration loop is one node until expanded with `group_members`."""
|
|
39
|
+
groups = reader.grouped_children(call_id)
|
|
40
|
+
singles = [g["first_id"] for g in groups[:limit] if g["count"] == 1]
|
|
41
|
+
by_id = {c["id"]: c for c in reader.calls(singles)}
|
|
42
|
+
groups_code = {g["code_id"] for g in groups[:limit] if g["count"] > 1}
|
|
43
|
+
samples = {}
|
|
44
|
+
if groups_code: # one representative call per group, for its names and kind
|
|
45
|
+
rows = reader.calls([g["first_id"] for g in groups[:limit] if g["count"] > 1])
|
|
46
|
+
samples = {r["code_id"]: r for r in rows}
|
|
47
|
+
out = []
|
|
48
|
+
for g in groups[:limit]:
|
|
49
|
+
if g["count"] == 1:
|
|
50
|
+
c = by_id[g["first_id"]]
|
|
51
|
+
node = call_node(c, stage_of(c["module"], c["observed"]) if stage_of else None)
|
|
52
|
+
if depth > 1 and c["children"]:
|
|
53
|
+
node["items"] = subtree(reader, c["id"], depth - 1, limit, stage_of)
|
|
54
|
+
else:
|
|
55
|
+
s = samples[g["code_id"]]
|
|
56
|
+
node = {
|
|
57
|
+
"type": "group", "parent_id": call_id, "code_id": g["code_id"], "qualname": s["qualname"],
|
|
58
|
+
"module": s["module"], "kind": s["kind"], "observed": bool(s["observed"]), "count": g["count"],
|
|
59
|
+
"outcomes": {"raised": g["raised"], "unfinished": g["unfinished"],
|
|
60
|
+
"ok": g["count"] - g["raised"] - g["unfinished"]},
|
|
61
|
+
"total_ns": g["total_ns"], "first_call_id": g["first_id"], "seq": g["first_seq"],
|
|
62
|
+
"stage": stage_of(s["module"], s["observed"]) if stage_of else None, "inferred": True,
|
|
63
|
+
}
|
|
64
|
+
out.append(node)
|
|
65
|
+
if len(groups) > limit:
|
|
66
|
+
out.append({"type": "more", "parent_id": call_id, "remaining": len(groups) - limit})
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def group_members(reader: Reader, call_id: int | None, code_id: int, offset: int = 0, limit: int = 200) -> list[dict]:
|
|
71
|
+
return [call_node(c) for c in reader.group_members(call_id, code_id, offset, limit)]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# -- stages (Level 1) ---------------------------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def stage_namer(config: dict):
|
|
77
|
+
"""Map (module, observed) to a stage name: user rules first, then the module path cut to `stage_depth`."""
|
|
78
|
+
rules = config.get("stages") or {}
|
|
79
|
+
depth = config.get("stage_depth", 2)
|
|
80
|
+
cache: dict[tuple, str] = {}
|
|
81
|
+
|
|
82
|
+
def stage_of(module: str | None, observed) -> str:
|
|
83
|
+
key = (module, bool(observed))
|
|
84
|
+
name = cache.get(key)
|
|
85
|
+
if name is None:
|
|
86
|
+
name = "?" if module is None else next(
|
|
87
|
+
(stage for stage, globs in rules.items()
|
|
88
|
+
if any(fnmatch.fnmatch(module, g) or (g.endswith(".*") and module == g[:-2]) for g in globs)),
|
|
89
|
+
None)
|
|
90
|
+
if name is None:
|
|
91
|
+
parts = module.split(".")
|
|
92
|
+
name = ".".join(parts[: depth if observed else 1]) # libraries group under their top package
|
|
93
|
+
cache[key] = name
|
|
94
|
+
return name
|
|
95
|
+
|
|
96
|
+
return stage_of
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def stages(reader: Reader) -> dict:
|
|
100
|
+
"""Stage graph for the Level-1 overview (FR-ANA-2). One pass over all calls."""
|
|
101
|
+
stage_of = stage_namer(reader.run()["config"])
|
|
102
|
+
rows = reader.all_calls()
|
|
103
|
+
stage_by_call: dict[int, str] = {}
|
|
104
|
+
dur: dict[int, int] = {}
|
|
105
|
+
child_dur: dict[int, int] = defaultdict(int)
|
|
106
|
+
nodes: dict[str, dict] = {}
|
|
107
|
+
edges: dict[tuple[str, str], dict] = {}
|
|
108
|
+
roots: list[str] = []
|
|
109
|
+
for cid, parent, start, end, outcome, seq, module, observed, kind in rows:
|
|
110
|
+
stage = stage_by_call[cid] = stage_of(module, observed)
|
|
111
|
+
d = dur[cid] = end - start if end is not None else 0
|
|
112
|
+
n = nodes.get(stage)
|
|
113
|
+
if n is None:
|
|
114
|
+
n = nodes[stage] = {"name": stage, "calls": 0, "total_ns": 0, "self_ns": 0, "raised": 0,
|
|
115
|
+
"unfinished": 0, "first_seq": seq, "first_call": cid, "observed": bool(observed),
|
|
116
|
+
"modules": set(), "first_call_is_import": kind in ("module", "class")}
|
|
117
|
+
if n["first_call_is_import"] and kind not in ("module", "class"):
|
|
118
|
+
# where a stage "starts" for a reader is its first real call, not the import that defined it
|
|
119
|
+
n["first_call"], n["first_call_is_import"] = cid, False
|
|
120
|
+
n["calls"] += 1
|
|
121
|
+
n["raised"] += outcome == "raised"
|
|
122
|
+
n["unfinished"] += outcome == "unfinished"
|
|
123
|
+
n["modules"].add(module)
|
|
124
|
+
if parent is None:
|
|
125
|
+
roots.append(stage)
|
|
126
|
+
n["total_ns"] += d
|
|
127
|
+
else:
|
|
128
|
+
child_dur[parent] += d
|
|
129
|
+
pstage = stage_by_call.get(parent)
|
|
130
|
+
if pstage is not None and pstage != stage:
|
|
131
|
+
e = edges.get((pstage, stage))
|
|
132
|
+
if e is None:
|
|
133
|
+
e = edges[(pstage, stage)] = {"from": pstage, "to": stage, "calls": 0, "total_ns": 0,
|
|
134
|
+
"first_seq": seq, "calls_below": 0}
|
|
135
|
+
e["calls"] += 1
|
|
136
|
+
e["total_ns"] += d
|
|
137
|
+
nodes[stage]["total_ns"] += d # time spent in a stage counts from where it is entered
|
|
138
|
+
for cid, stage in stage_by_call.items():
|
|
139
|
+
nodes[stage]["self_ns"] += max(dur[cid] - child_dur.get(cid, 0), 0)
|
|
140
|
+
# subtree sizes (children come after parents in seq order): how much work each stage crossing leads to
|
|
141
|
+
size: dict[int, int] = defaultdict(lambda: 1)
|
|
142
|
+
for cid, parent, *_ in reversed(rows):
|
|
143
|
+
if parent is not None:
|
|
144
|
+
size[parent] += size[cid]
|
|
145
|
+
for cid, parent, *_ in rows:
|
|
146
|
+
if parent is not None and stage_by_call.get(parent) not in (None, stage_by_call[cid]):
|
|
147
|
+
edges[(stage_by_call[parent], stage_by_call[cid])]["calls_below"] += size[cid]
|
|
148
|
+
for n in nodes.values():
|
|
149
|
+
n["modules"] = sorted(m for m in n["modules"] if m)
|
|
150
|
+
del n["first_call_is_import"]
|
|
151
|
+
return {"nodes": sorted(nodes.values(), key=lambda n: n["first_seq"]),
|
|
152
|
+
"edges": sorted(edges.values(), key=lambda e: e["first_seq"]),
|
|
153
|
+
"main_path": _main_path(roots, edges), "inferred": True}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _main_path(roots: list[str], edges: dict[tuple[str, str], dict]) -> list[str]:
|
|
157
|
+
"""From the entry stage, keep following the outgoing edge that leads to the most calls (its subtree size; time
|
|
158
|
+
only breaks ties) to a stage not yet on the path. Call counts are deterministic, timings are noise in small runs."""
|
|
159
|
+
if not roots:
|
|
160
|
+
return []
|
|
161
|
+
out: dict[str, list[dict]] = defaultdict(list)
|
|
162
|
+
for e in edges.values():
|
|
163
|
+
out[e["from"]].append(e)
|
|
164
|
+
path = [roots[0]]
|
|
165
|
+
while True:
|
|
166
|
+
nxt = [e for e in out[path[-1]] if e["to"] not in path]
|
|
167
|
+
if not nxt:
|
|
168
|
+
return path
|
|
169
|
+
path.append(max(nxt, key=lambda e: (e["calls_below"], e["total_ns"], -e["first_seq"]))["to"])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# -- values and data flow -----------------------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def value_diff(a: dict, b: dict, path: str = "") -> list[dict]:
|
|
175
|
+
"""Field-level differences between two encoded value trees. Truncated parts compare as 'unknown', never equal."""
|
|
176
|
+
if a == b:
|
|
177
|
+
return []
|
|
178
|
+
ta, tb = a.get("t"), b.get("t")
|
|
179
|
+
if "trunc" in (ta, tb) or "redacted" in (ta, tb):
|
|
180
|
+
return [{"path": path or ".", "op": "unknown"}]
|
|
181
|
+
if ta == tb == "obj" and a.get("type") == b.get("type"):
|
|
182
|
+
return _diff_maps(a["attrs"], b["attrs"], path, ".")
|
|
183
|
+
if ta == tb == "dict":
|
|
184
|
+
def keyed(d):
|
|
185
|
+
return {preview(k, 40): v for k, v in d["items"]}
|
|
186
|
+
return _diff_maps(keyed(a), keyed(b), path, "[]")
|
|
187
|
+
if ta == tb and ta in ("list", "tuple") and "len" not in a and "len" not in b:
|
|
188
|
+
changes = []
|
|
189
|
+
for i in range(max(len(a["items"]), len(b["items"]))):
|
|
190
|
+
p = f"{path}[{i}]"
|
|
191
|
+
if i >= len(a["items"]):
|
|
192
|
+
changes.append({"path": p, "op": "added", "after": preview(b["items"][i])})
|
|
193
|
+
elif i >= len(b["items"]):
|
|
194
|
+
changes.append({"path": p, "op": "removed", "before": preview(a["items"][i])})
|
|
195
|
+
else:
|
|
196
|
+
changes.extend(value_diff(a["items"][i], b["items"][i], p))
|
|
197
|
+
return changes
|
|
198
|
+
if ta == tb and a.get("oid") is not None and a.get("oid") == b.get("oid") and ta in ("list", "set", "dict"):
|
|
199
|
+
return [{"path": path or ".", "op": "unknown"}] # truncated containers: can't tell what changed
|
|
200
|
+
return [{"path": path or ".", "op": "changed", "before": preview(a), "after": preview(b)}]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _diff_maps(a: dict, b: dict, path: str, style: str) -> list[dict]:
|
|
204
|
+
changes = []
|
|
205
|
+
for key in list(a) + [k for k in b if k not in a]:
|
|
206
|
+
p = f"{path}.{key}" if style == "." else f"{path}[{key}]"
|
|
207
|
+
if key not in b:
|
|
208
|
+
changes.append({"path": p, "op": "removed", "before": preview(a[key])})
|
|
209
|
+
elif key not in a:
|
|
210
|
+
changes.append({"path": p, "op": "added", "after": preview(b[key])})
|
|
211
|
+
else:
|
|
212
|
+
changes.extend(value_diff(a[key], b[key], p))
|
|
213
|
+
return changes
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def object_flow(reader: Reader, object_id: int) -> dict:
|
|
217
|
+
"""Where one object was seen (as an argument, return value, local...), how it moved and what changed.
|
|
218
|
+
ponytail: only top-level bindings are searched; an object nested inside another value is not found yet."""
|
|
219
|
+
rows = reader.object_bindings(object_id)
|
|
220
|
+
appearances, links, changes = [], [], []
|
|
221
|
+
prev = None
|
|
222
|
+
for r in rows:
|
|
223
|
+
data = json.loads(r["data"])
|
|
224
|
+
appearances.append({"call_id": r["call_id"], "qualname": r["qualname"], "seq": r["seq"], "role": r["kind"],
|
|
225
|
+
"name": r["name"], "value_id": r["value_id"], "preview": preview(data)})
|
|
226
|
+
if prev is not None:
|
|
227
|
+
if prev[0]["call_id"] != r["call_id"]:
|
|
228
|
+
links.append({"from": prev[0]["call_id"], "to": r["call_id"], "seq": r["seq"], "inferred": True})
|
|
229
|
+
diff = value_diff(prev[1], data)
|
|
230
|
+
if diff:
|
|
231
|
+
changes.append({"seq_before": prev[0]["seq"], "seq_after": r["seq"], "call_before": prev[0]["call_id"],
|
|
232
|
+
"call_after": r["call_id"], "changes": diff, "inferred": True})
|
|
233
|
+
prev = (r, data)
|
|
234
|
+
return {"object_id": object_id, "type": rows and json.loads(rows[0]["data"]).get("type"),
|
|
235
|
+
"appearances": appearances, "links": links, "changes": changes}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# -- errors -------------------------------------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
def _binding_view(b: dict) -> dict:
|
|
241
|
+
data = json.loads(b["data"])
|
|
242
|
+
return {"name": b["name"], "kind": b["kind"], "value_id": b["value_id"], "object_id": b["object_id"],
|
|
243
|
+
"type": b["type"], "preview": preview(data)}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def error_path(reader: Reader) -> dict | None:
|
|
247
|
+
"""The exception that ended the run: where it started, how execution got there, how it propagated,
|
|
248
|
+
and the latest observed changes to the objects in scope (FR-ANA-5)."""
|
|
249
|
+
escaped = [e for e in reader.exceptions() if e["handled_call_id"] is None and e["raised_call_id"] is not None]
|
|
250
|
+
if not escaped:
|
|
251
|
+
return None
|
|
252
|
+
exc = escaped[-1]
|
|
253
|
+
path = reader.ancestors(exc["raised_call_id"])
|
|
254
|
+
origin = path[-1]
|
|
255
|
+
stage_of = stage_namer(reader.run()["config"])
|
|
256
|
+
locals_at_raise = [b for b in reader.bindings(origin["id"]) if b["kind"] == "raise"]
|
|
257
|
+
recent = []
|
|
258
|
+
for b in locals_at_raise:
|
|
259
|
+
if b["object_id"] is not None:
|
|
260
|
+
for ch in object_flow(reader, b["object_id"])["changes"][-3:]:
|
|
261
|
+
recent.append({"name": b["name"], "object_id": b["object_id"], **ch})
|
|
262
|
+
recent.sort(key=lambda c: c["seq_after"])
|
|
263
|
+
chain, cause = [], exc["cause_id"]
|
|
264
|
+
while cause is not None and len(chain) < 10:
|
|
265
|
+
c = reader.exception(cause)
|
|
266
|
+
if c is None:
|
|
267
|
+
break
|
|
268
|
+
chain.append({k: c[k] for k in ("id", "type", "message", "raised_call_id", "raised_line")})
|
|
269
|
+
cause = c["cause_id"]
|
|
270
|
+
return {
|
|
271
|
+
"exception": {k: exc[k] for k in ("id", "type", "message", "raised_line", "value_id")},
|
|
272
|
+
"traceback": json.loads(exc["traceback"]),
|
|
273
|
+
"origin": call_node(origin, stage_of(origin["module"], origin["observed"])),
|
|
274
|
+
"path": [call_node(c, stage_of(c["module"], c["observed"])) for c in path],
|
|
275
|
+
"propagation": [c["id"] for c in reversed(path) if c["exception_id"] == exc["id"]],
|
|
276
|
+
"locals": [_binding_view(b) for b in locals_at_raise],
|
|
277
|
+
"recent_changes": recent[-5:],
|
|
278
|
+
"chain": chain,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# -- search -------------------------------------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
def search(reader: Reader, text: str, limit: int = 100) -> list[dict]:
|
|
285
|
+
if not text.strip():
|
|
286
|
+
return []
|
|
287
|
+
found = reader.search(text.strip(), limit)
|
|
288
|
+
hits = [{"kind": "function", "label": f["qualname"], "module": f["module"], "call_id": f["call_id"],
|
|
289
|
+
"count": f["count"], "observed": bool(f["observed"])} for f in found["functions"]]
|
|
290
|
+
for kind in ("variables", "values"):
|
|
291
|
+
for v in found[kind]:
|
|
292
|
+
hits.append({"kind": kind[:-1], "label": v["name"] or v["kind"], "call_id": v["call_id"],
|
|
293
|
+
"role": v["kind"], "preview": preview(json.loads(v["data"]))})
|
|
294
|
+
return hits[:limit]
|