@shadow-js/devtools 0.0.0-stage → 1.0.0
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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +576 -2
- package/dist/dev/agent.d.ts +239 -0
- package/dist/dev/agent.js +626 -0
- package/dist/dev/agent.js.map +1 -0
- package/dist/dev/backend.d.ts +53 -0
- package/dist/dev/backend.js +53 -0
- package/dist/dev/backend.js.map +1 -0
- package/dist/dev/hook.d.ts +83 -0
- package/dist/dev/hook.js +136 -0
- package/dist/dev/hook.js.map +1 -0
- package/dist/dev/index.d.ts +17 -0
- package/dist/dev/index.js +17 -0
- package/dist/dev/inspection.d.ts +226 -0
- package/dist/dev/inspection.js +263 -0
- package/dist/dev/inspection.js.map +1 -0
- package/dist/dev/inspector.d.ts +331 -0
- package/dist/dev/inspector.js +429 -0
- package/dist/dev/inspector.js.map +1 -0
- package/dist/dev/model.d.ts +418 -0
- package/dist/dev/model.js +559 -0
- package/dist/dev/model.js.map +1 -0
- package/dist/dev/mount.d.ts +207 -0
- package/dist/dev/mount.js +256 -0
- package/dist/dev/mount.js.map +1 -0
- package/dist/dev/overlay.d.ts +102 -0
- package/dist/dev/overlay.js +176 -0
- package/dist/dev/overlay.js.map +1 -0
- package/dist/dev/preview.d.ts +74 -0
- package/dist/dev/preview.js +122 -0
- package/dist/dev/preview.js.map +1 -0
- package/dist/dev/protocol.d.ts +288 -0
- package/dist/dev/protocol.js +109 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/ring.d.ts +50 -0
- package/dist/dev/ring.js +94 -0
- package/dist/dev/ring.js.map +1 -0
- package/dist/dev/transport.d.ts +347 -0
- package/dist/dev/transport.js +394 -0
- package/dist/dev/transport.js.map +1 -0
- package/dist/dev/ui/details.js +294 -0
- package/dist/dev/ui/details.js.map +1 -0
- package/dist/dev/ui/inspection-state.d.ts +43 -0
- package/dist/dev/ui/inspection-state.js +62 -0
- package/dist/dev/ui/inspection-state.js.map +1 -0
- package/dist/dev/ui/issues.js +85 -0
- package/dist/dev/ui/issues.js.map +1 -0
- package/dist/dev/ui/panel.d.ts +55 -0
- package/dist/dev/ui/panel.js +160 -0
- package/dist/dev/ui/panel.js.map +1 -0
- package/dist/dev/ui/rows.d.ts +44 -0
- package/dist/dev/ui/rows.js +132 -0
- package/dist/dev/ui/rows.js.map +1 -0
- package/dist/dev/ui/state-tab.js +88 -0
- package/dist/dev/ui/state-tab.js.map +1 -0
- package/dist/dev/ui/state.d.ts +192 -0
- package/dist/dev/ui/state.js +147 -0
- package/dist/dev/ui/state.js.map +1 -0
- package/dist/dev/ui/styles.d.ts +31 -0
- package/dist/dev/ui/styles.js +113 -0
- package/dist/dev/ui/styles.js.map +1 -0
- package/dist/dev/ui/timeline.js +135 -0
- package/dist/dev/ui/timeline.js.map +1 -0
- package/dist/dev/ui/tree.js +186 -0
- package/dist/dev/ui/tree.js.map +1 -0
- package/dist/prod/agent.js +2 -0
- package/dist/prod/agent.js.map +1 -0
- package/dist/prod/backend.js +2 -0
- package/dist/prod/backend.js.map +1 -0
- package/dist/prod/hook.js +2 -0
- package/dist/prod/hook.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/inspection.js +2 -0
- package/dist/prod/inspection.js.map +1 -0
- package/dist/prod/inspector.js +2 -0
- package/dist/prod/inspector.js.map +1 -0
- package/dist/prod/model.js +2 -0
- package/dist/prod/model.js.map +1 -0
- package/dist/prod/mount.js +2 -0
- package/dist/prod/mount.js.map +1 -0
- package/dist/prod/overlay.js +11 -0
- package/dist/prod/overlay.js.map +1 -0
- package/dist/prod/preview.js +2 -0
- package/dist/prod/preview.js.map +1 -0
- package/dist/prod/protocol.js +2 -0
- package/dist/prod/protocol.js.map +1 -0
- package/dist/prod/ring.js +2 -0
- package/dist/prod/ring.js.map +1 -0
- package/dist/prod/transport.js +2 -0
- package/dist/prod/transport.js.map +1 -0
- package/dist/prod/ui/details.js +2 -0
- package/dist/prod/ui/details.js.map +1 -0
- package/dist/prod/ui/inspection-state.js +2 -0
- package/dist/prod/ui/inspection-state.js.map +1 -0
- package/dist/prod/ui/issues.js +2 -0
- package/dist/prod/ui/issues.js.map +1 -0
- package/dist/prod/ui/panel.js +2 -0
- package/dist/prod/ui/panel.js.map +1 -0
- package/dist/prod/ui/rows.js +2 -0
- package/dist/prod/ui/rows.js.map +1 -0
- package/dist/prod/ui/state-tab.js +2 -0
- package/dist/prod/ui/state-tab.js.map +1 -0
- package/dist/prod/ui/state.js +2 -0
- package/dist/prod/ui/state.js.map +1 -0
- package/dist/prod/ui/styles.js +82 -0
- package/dist/prod/ui/styles.js.map +1 -0
- package/dist/prod/ui/timeline.js +2 -0
- package/dist/prod/ui/timeline.js.map +1 -0
- package/dist/prod/ui/tree.js +2 -0
- package/dist/prod/ui/tree.js.map +1 -0
- package/package.json +69 -4
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import "./protocol.js";
|
|
2
|
+
//#region src/inspector.ts
|
|
3
|
+
/**
|
|
4
|
+
* The display name of a node: its given name, or `label#id` when it has none.
|
|
5
|
+
*
|
|
6
|
+
* @param node Any node record or snapshot node.
|
|
7
|
+
* @returns The name, or `label#id`.
|
|
8
|
+
* @example
|
|
9
|
+
* import { nodeLabel } from "@shadow-js/devtools";
|
|
10
|
+
*
|
|
11
|
+
* nodeLabel({ name: "count", label: "signal", id: 3 }); // "count"
|
|
12
|
+
* nodeLabel({ name: undefined, label: "signal", id: 3 }); // "signal#3"
|
|
13
|
+
* @since 1.0.0
|
|
14
|
+
*/
|
|
15
|
+
function nodeLabel(node) {
|
|
16
|
+
return node.name !== void 0 && node.name !== "" ? node.name : `${node.label}#${node.id}`;
|
|
17
|
+
}
|
|
18
|
+
function toSnapshotNode(record, model) {
|
|
19
|
+
const computation = record.type === "memo" || record.type === "effect";
|
|
20
|
+
return {
|
|
21
|
+
id: record.id,
|
|
22
|
+
type: record.type,
|
|
23
|
+
label: record.label,
|
|
24
|
+
name: record.name,
|
|
25
|
+
parent: record.parent,
|
|
26
|
+
children: [],
|
|
27
|
+
depth: 0,
|
|
28
|
+
isComponent: record.isComponent,
|
|
29
|
+
component: model.componentOf(record.id),
|
|
30
|
+
sources: record.sources.slice(),
|
|
31
|
+
observers: record.observers === null ? [] : [...record.observers],
|
|
32
|
+
runs: record.runs,
|
|
33
|
+
updates: computation ? Math.max(record.runs - 1, 0) : 0,
|
|
34
|
+
lastDuration: record.lastDuration,
|
|
35
|
+
totalDuration: record.totalDuration,
|
|
36
|
+
preview: record.preview,
|
|
37
|
+
writes: record.writes,
|
|
38
|
+
state: record.state,
|
|
39
|
+
render: record.render
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Builds the graph snapshot: the owners tree, every live node with its sources and observers, and the
|
|
44
|
+
* per-node run counts. Disposed nodes are not included. O(nodes); the model is not changed.
|
|
45
|
+
*
|
|
46
|
+
* @param model The folded model.
|
|
47
|
+
* @returns A JSON-safe snapshot.
|
|
48
|
+
* @example
|
|
49
|
+
* import { foldEvents, snapshotOf } from "@shadow-js/devtools";
|
|
50
|
+
*
|
|
51
|
+
* const snap = snapshotOf(foldEvents([]));
|
|
52
|
+
* const roots = snap.roots.map((id) => snap.nodes.find((node) => node.id === id));
|
|
53
|
+
* @see ownerTree
|
|
54
|
+
* @since 1.0.0
|
|
55
|
+
*/
|
|
56
|
+
function snapshotOf(model) {
|
|
57
|
+
const nodes = [];
|
|
58
|
+
const byId = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const record of model.nodes.values()) {
|
|
60
|
+
const node = toSnapshotNode(record, model);
|
|
61
|
+
nodes.push(node);
|
|
62
|
+
byId.set(node.id, node);
|
|
63
|
+
}
|
|
64
|
+
const roots = [];
|
|
65
|
+
for (const node of nodes) {
|
|
66
|
+
const parent = node.parent === 0 ? void 0 : byId.get(node.parent);
|
|
67
|
+
if (parent === void 0) roots.push(node.id);
|
|
68
|
+
else parent.children.push(node.id);
|
|
69
|
+
}
|
|
70
|
+
const stack = [...roots].reverse();
|
|
71
|
+
while (stack.length > 0) {
|
|
72
|
+
const id = stack.pop();
|
|
73
|
+
if (id === void 0) break;
|
|
74
|
+
const node = byId.get(id);
|
|
75
|
+
if (node === void 0) continue;
|
|
76
|
+
for (const childId of node.children) {
|
|
77
|
+
const child = byId.get(childId);
|
|
78
|
+
if (child === void 0) continue;
|
|
79
|
+
child.depth = node.depth + 1;
|
|
80
|
+
stack.push(childId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const count = (type) => nodes.filter((node) => node.type === type).length;
|
|
84
|
+
return {
|
|
85
|
+
seq: model.lastSeq,
|
|
86
|
+
nodes,
|
|
87
|
+
roots,
|
|
88
|
+
overflow: model.overflow,
|
|
89
|
+
counts: {
|
|
90
|
+
nodes: nodes.length,
|
|
91
|
+
owners: count("owner"),
|
|
92
|
+
components: nodes.filter((node) => node.isComponent).length,
|
|
93
|
+
signals: count("signal"),
|
|
94
|
+
memos: count("memo"),
|
|
95
|
+
effects: count("effect"),
|
|
96
|
+
resources: count("resource"),
|
|
97
|
+
stores: count("store"),
|
|
98
|
+
runs: model.runs.size,
|
|
99
|
+
writes: model.writes.size,
|
|
100
|
+
flushes: model.flushes.size,
|
|
101
|
+
errors: model.errors.size,
|
|
102
|
+
hydration: model.hydration.size
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The owners as a nested tree, children in creation order. A node whose owner is no longer live
|
|
108
|
+
* becomes a root. Built without recursion, so deep ownership cannot overflow the stack.
|
|
109
|
+
*
|
|
110
|
+
* @param snapshot A snapshot from {@link snapshotOf}.
|
|
111
|
+
* @returns The roots, each with its children.
|
|
112
|
+
* @example
|
|
113
|
+
* import { foldEvents, ownerTree, snapshotOf } from "@shadow-js/devtools";
|
|
114
|
+
*
|
|
115
|
+
* const roots = ownerTree(snapshotOf(foldEvents([]))); // [] for an empty model
|
|
116
|
+
* @see snapshotOf
|
|
117
|
+
* @since 1.0.0
|
|
118
|
+
*/
|
|
119
|
+
function ownerTree(snapshot) {
|
|
120
|
+
const byId = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const node of snapshot.nodes) byId.set(node.id, {
|
|
122
|
+
id: node.id,
|
|
123
|
+
children: []
|
|
124
|
+
});
|
|
125
|
+
const roots = [];
|
|
126
|
+
for (const node of snapshot.nodes) {
|
|
127
|
+
const tree = byId.get(node.id);
|
|
128
|
+
if (tree === void 0) continue;
|
|
129
|
+
const parent = node.parent === 0 ? void 0 : byId.get(node.parent);
|
|
130
|
+
if (parent === void 0) roots.push(tree);
|
|
131
|
+
else parent.children.push(tree);
|
|
132
|
+
}
|
|
133
|
+
return roots;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Per-component update counts: how many times the effects and memos under each component re-ran
|
|
137
|
+
* after their first run. Components are the owners a `mount` event named. A component with no
|
|
138
|
+
* computations still gets an entry.
|
|
139
|
+
*
|
|
140
|
+
* @param model The folded model.
|
|
141
|
+
* @returns One entry per component, most updated first (ties by id).
|
|
142
|
+
* @example
|
|
143
|
+
* import { componentStats, foldEvents } from "@shadow-js/devtools";
|
|
144
|
+
*
|
|
145
|
+
* const top = componentStats(foldEvents([]))[0]; // undefined: no component is mounted yet
|
|
146
|
+
* @since 1.0.0
|
|
147
|
+
*/
|
|
148
|
+
function componentStats(model) {
|
|
149
|
+
const stats = /* @__PURE__ */ new Map();
|
|
150
|
+
const ensure = (record) => {
|
|
151
|
+
let stat = stats.get(record.id);
|
|
152
|
+
if (stat === void 0) {
|
|
153
|
+
stat = {
|
|
154
|
+
id: record.id,
|
|
155
|
+
name: record.name ?? `component#${record.id}`,
|
|
156
|
+
alive: record.alive,
|
|
157
|
+
mountedAt: record.mountedAt,
|
|
158
|
+
computations: 0,
|
|
159
|
+
updates: 0,
|
|
160
|
+
totalDuration: 0,
|
|
161
|
+
lastUpdateSeq: 0
|
|
162
|
+
};
|
|
163
|
+
stats.set(record.id, stat);
|
|
164
|
+
}
|
|
165
|
+
return stat;
|
|
166
|
+
};
|
|
167
|
+
for (const record of model.nodes.values()) if (record.isComponent) ensure(record);
|
|
168
|
+
for (const record of model.nodes.values()) {
|
|
169
|
+
if (record.type !== "memo" && record.type !== "effect") continue;
|
|
170
|
+
const componentId = model.componentOf(record.id);
|
|
171
|
+
if (componentId === 0) continue;
|
|
172
|
+
const owner = model.node(componentId);
|
|
173
|
+
if (owner === void 0) continue;
|
|
174
|
+
const stat = ensure(owner);
|
|
175
|
+
stat.computations += 1;
|
|
176
|
+
stat.updates += Math.max(record.runs - 1, 0);
|
|
177
|
+
stat.totalDuration += record.totalDuration;
|
|
178
|
+
if (record.runs > 1 && record.lastRunSeq > stat.lastUpdateSeq) stat.lastUpdateSeq = record.lastRunSeq;
|
|
179
|
+
}
|
|
180
|
+
return [...stats.values()].sort((a, b) => b.updates - a.updates || a.id - b.id);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Lays the runs of each flush out as flame-graph lanes: a run that started inside another run (a memo
|
|
184
|
+
* pulled by an effect) sits one lane deeper. Runs outside any flush are grouped as flush `0`.
|
|
185
|
+
*
|
|
186
|
+
* @param model The folded model.
|
|
187
|
+
* @param options `last`: only the newest N flushes (default: all kept). When `last` is set, flush `0`
|
|
188
|
+
* is left out, and `last: 0` returns no flushes.
|
|
189
|
+
* @returns One entry per flush, oldest first. Flush `0` comes first, when there are runs outside any
|
|
190
|
+
* flush and `last` is not set.
|
|
191
|
+
* @example
|
|
192
|
+
* import { flameTimeline, foldEvents } from "@shadow-js/devtools";
|
|
193
|
+
*
|
|
194
|
+
* const lanes = flameTimeline(foldEvents([]), { last: 10 }).map((f) => [f.flush, f.lanes]); // []
|
|
195
|
+
* @since 1.0.0
|
|
196
|
+
*/
|
|
197
|
+
function flameTimeline(model, options = {}) {
|
|
198
|
+
const byFlush = /* @__PURE__ */ new Map();
|
|
199
|
+
for (const run of model.runs) {
|
|
200
|
+
const bucket = byFlush.get(run.flush);
|
|
201
|
+
if (bucket === void 0) byFlush.set(run.flush, [run]);
|
|
202
|
+
else bucket.push(run);
|
|
203
|
+
}
|
|
204
|
+
const lay = (runs) => {
|
|
205
|
+
const sorted = runs.slice().sort((a, b) => a.start - b.start || b.duration - a.duration || a.seq - b.seq);
|
|
206
|
+
const open = [];
|
|
207
|
+
const spans = [];
|
|
208
|
+
let lanes = 0;
|
|
209
|
+
for (const run of sorted) {
|
|
210
|
+
while (open.length > 0 && (open[open.length - 1] ?? 0) <= run.start) open.pop();
|
|
211
|
+
const node = model.node(run.id);
|
|
212
|
+
const end = run.start + run.duration;
|
|
213
|
+
spans.push({
|
|
214
|
+
seq: run.seq,
|
|
215
|
+
id: run.id,
|
|
216
|
+
label: node === void 0 ? `#${run.id}` : nodeLabel(node),
|
|
217
|
+
kind: node === void 0 ? "unknown" : node.type,
|
|
218
|
+
start: run.start,
|
|
219
|
+
end,
|
|
220
|
+
duration: run.duration,
|
|
221
|
+
depth: open.length,
|
|
222
|
+
changed: run.changed,
|
|
223
|
+
component: model.componentOf(run.id)
|
|
224
|
+
});
|
|
225
|
+
open.push(end);
|
|
226
|
+
if (open.length > lanes) lanes = open.length;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
spans,
|
|
230
|
+
lanes
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
let flushes = model.flushes.toArray();
|
|
234
|
+
if (options.last !== void 0 && options.last >= 0) flushes = options.last === 0 ? [] : flushes.slice(-options.last);
|
|
235
|
+
const out = [];
|
|
236
|
+
const loose = byFlush.get(0);
|
|
237
|
+
if (loose !== void 0 && loose.length > 0 && options.last === void 0) {
|
|
238
|
+
const { spans, lanes } = lay(loose);
|
|
239
|
+
const start = spans[0]?.start ?? 0;
|
|
240
|
+
const end = spans.reduce((latest, span) => Math.max(latest, span.end), start);
|
|
241
|
+
out.push({
|
|
242
|
+
flush: 0,
|
|
243
|
+
start,
|
|
244
|
+
end,
|
|
245
|
+
duration: end - start,
|
|
246
|
+
render: 0,
|
|
247
|
+
user: 0,
|
|
248
|
+
lanes,
|
|
249
|
+
spans
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
for (const flush of flushes) {
|
|
253
|
+
const { spans, lanes } = lay(byFlush.get(flush.seq) ?? []);
|
|
254
|
+
out.push({
|
|
255
|
+
flush: flush.seq,
|
|
256
|
+
start: flush.start,
|
|
257
|
+
end: flush.end,
|
|
258
|
+
duration: Math.max(flush.end - flush.start, 0),
|
|
259
|
+
render: flush.render,
|
|
260
|
+
user: flush.user,
|
|
261
|
+
lanes,
|
|
262
|
+
spans
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
function findRun(model, predicate) {
|
|
268
|
+
for (let i = model.runs.size - 1; i >= 0; i -= 1) {
|
|
269
|
+
const run = model.runs.at(i);
|
|
270
|
+
if (run !== void 0 && predicate(run)) return run;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function findWrite(model, seq, id) {
|
|
274
|
+
for (let i = model.writes.size - 1; i >= 0; i -= 1) {
|
|
275
|
+
const write = model.writes.at(i);
|
|
276
|
+
if (write === void 0) continue;
|
|
277
|
+
if (write.seq === seq && write.id === id) return write;
|
|
278
|
+
if (write.seq < seq) return void 0;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function label(model, id) {
|
|
282
|
+
const node = model.node(id);
|
|
283
|
+
return node === void 0 ? `#${id}` : nodeLabel(node);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* "Why did this run?": follows the changed sources of a run back to the signal write that started
|
|
287
|
+
* it. A write made inside another effect continues with that effect's own run, so the chain can span
|
|
288
|
+
* several computations. The primary path follows the earliest changed source; the others are
|
|
289
|
+
* listed in `alsoCausedBy`. A chain longer than `maxDepth` is cut at that length.
|
|
290
|
+
*
|
|
291
|
+
* @param model The folded model.
|
|
292
|
+
* @param id The computation to explain.
|
|
293
|
+
* @param options `runSeq`: explain this run instead of the latest; `maxDepth`: chain length limit (default 64).
|
|
294
|
+
* @returns The explanation, or `null` when the computation has no recorded run.
|
|
295
|
+
* @example
|
|
296
|
+
* import { explainRun, foldEvents } from "@shadow-js/devtools";
|
|
297
|
+
*
|
|
298
|
+
* const why = explainRun(foldEvents([]), 3);
|
|
299
|
+
* const labels = why?.chain.map((step) => step.label); // undefined: no run of node 3 is recorded
|
|
300
|
+
* @see nodeLabel
|
|
301
|
+
* @since 1.0.0
|
|
302
|
+
*/
|
|
303
|
+
function explainRun(model, id, options = {}) {
|
|
304
|
+
const target = findRun(model, (run) => run.id === id && (options.runSeq === void 0 || run.seq === options.runSeq));
|
|
305
|
+
if (target === void 0) return null;
|
|
306
|
+
const maxDepth = options.maxDepth ?? 64;
|
|
307
|
+
const reverse = [{
|
|
308
|
+
kind: "run",
|
|
309
|
+
id: target.id,
|
|
310
|
+
label: label(model, target.id),
|
|
311
|
+
seq: target.seq,
|
|
312
|
+
changed: target.changed
|
|
313
|
+
}];
|
|
314
|
+
const alsoCausedBy = [];
|
|
315
|
+
const visited = /* @__PURE__ */ new Set([target.seq]);
|
|
316
|
+
let truncated = false;
|
|
317
|
+
let current = target;
|
|
318
|
+
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
319
|
+
const [primary, ...others] = current.causes;
|
|
320
|
+
if (primary === void 0) break;
|
|
321
|
+
if (current === target) for (const other of others) alsoCausedBy.push({
|
|
322
|
+
kind: other.kind,
|
|
323
|
+
id: other.id,
|
|
324
|
+
label: label(model, other.id),
|
|
325
|
+
seq: other.seq
|
|
326
|
+
});
|
|
327
|
+
if (primary.kind === "initial" || primary.kind === "unknown") {
|
|
328
|
+
reverse.push({
|
|
329
|
+
kind: primary.kind,
|
|
330
|
+
id: primary.id,
|
|
331
|
+
label: primary.kind,
|
|
332
|
+
seq: primary.seq
|
|
333
|
+
});
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (primary.kind === "write") {
|
|
337
|
+
const write = findWrite(model, primary.seq, primary.id);
|
|
338
|
+
if (write === void 0) {
|
|
339
|
+
reverse.push({
|
|
340
|
+
kind: "write",
|
|
341
|
+
id: primary.id,
|
|
342
|
+
label: label(model, primary.id),
|
|
343
|
+
seq: primary.seq
|
|
344
|
+
});
|
|
345
|
+
truncated = true;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
const step = {
|
|
349
|
+
kind: "write",
|
|
350
|
+
id: write.id,
|
|
351
|
+
label: label(model, write.id),
|
|
352
|
+
seq: write.seq,
|
|
353
|
+
preview: write.preview
|
|
354
|
+
};
|
|
355
|
+
if (write.writer !== 0) step.writer = label(model, write.writer);
|
|
356
|
+
reverse.push(step);
|
|
357
|
+
if (write.writer === 0) break;
|
|
358
|
+
const writerRun = findRunAfter(model, write.writer, write.seq);
|
|
359
|
+
if (writerRun === void 0 || visited.has(writerRun.seq)) {
|
|
360
|
+
truncated = writerRun === void 0;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
visited.add(writerRun.seq);
|
|
364
|
+
reverse.push({
|
|
365
|
+
kind: "run",
|
|
366
|
+
id: writerRun.id,
|
|
367
|
+
label: label(model, writerRun.id),
|
|
368
|
+
seq: writerRun.seq,
|
|
369
|
+
changed: writerRun.changed
|
|
370
|
+
});
|
|
371
|
+
current = writerRun;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const source = model.node(primary.id);
|
|
375
|
+
if (source !== void 0 && source.type !== "memo" && source.type !== "effect") {
|
|
376
|
+
reverse.push({
|
|
377
|
+
kind: "unknown",
|
|
378
|
+
id: primary.id,
|
|
379
|
+
label: label(model, primary.id),
|
|
380
|
+
seq: primary.seq
|
|
381
|
+
});
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
const upstream = findRun(model, (run) => run.seq === primary.seq);
|
|
385
|
+
if (upstream === void 0 || visited.has(upstream.seq)) {
|
|
386
|
+
reverse.push({
|
|
387
|
+
kind: "run",
|
|
388
|
+
id: primary.id,
|
|
389
|
+
label: label(model, primary.id),
|
|
390
|
+
seq: primary.seq
|
|
391
|
+
});
|
|
392
|
+
truncated = upstream === void 0;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
visited.add(upstream.seq);
|
|
396
|
+
reverse.push({
|
|
397
|
+
kind: "run",
|
|
398
|
+
id: upstream.id,
|
|
399
|
+
label: label(model, upstream.id),
|
|
400
|
+
seq: upstream.seq,
|
|
401
|
+
changed: upstream.changed
|
|
402
|
+
});
|
|
403
|
+
current = upstream;
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
target: {
|
|
407
|
+
id: target.id,
|
|
408
|
+
label: label(model, target.id),
|
|
409
|
+
seq: target.seq,
|
|
410
|
+
duration: target.duration
|
|
411
|
+
},
|
|
412
|
+
chain: reverse.reverse(),
|
|
413
|
+
alsoCausedBy,
|
|
414
|
+
truncated
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function findRunAfter(model, id, seq) {
|
|
418
|
+
let found;
|
|
419
|
+
for (let i = model.runs.size - 1; i >= 0; i -= 1) {
|
|
420
|
+
const run = model.runs.at(i);
|
|
421
|
+
if (run === void 0 || run.seq <= seq) break;
|
|
422
|
+
if (run.id === id) found = run;
|
|
423
|
+
}
|
|
424
|
+
return found;
|
|
425
|
+
}
|
|
426
|
+
//#endregion
|
|
427
|
+
export { componentStats, explainRun, flameTimeline, nodeLabel, ownerTree, snapshotOf };
|
|
428
|
+
|
|
429
|
+
//# sourceMappingURL=inspector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspector.js","names":[],"sources":["../../src/inspector.ts"],"sourcesContent":["import type { InspectorModel, NodeRecord, NodeType, RunRecord, WriteRecord } from \"./model\";\nimport { NO_ID } from \"./protocol\";\n\n/**\n * One node of the graph snapshot: an owner, signal, computation, resource or store, with its edges\n * and update counts. Plain data, safe to send to a remote panel as JSON. Built by {@link snapshotOf}.\n *\n * @since 1.0.0\n */\nexport interface SnapshotNode {\n /** Node id, as the runtime assigned it. */\n id: number;\n /** The kind of node. */\n type: NodeType;\n /** `root`, `owner`, `memo`, `effect`, `signal`, `resource`, `store`, or `component` after a mount. */\n label: string;\n /** The name given in the source, or `undefined`. */\n name: string | undefined;\n /** Owner id, or {@link NO_ID} for roots and detached nodes. */\n parent: number;\n /** Ids of the live nodes this node owns, in creation order. */\n children: number[];\n /** Ownership depth: 0 for roots. */\n depth: number;\n /** True when a component was mounted on this node. */\n isComponent: boolean;\n /** Nearest component that owns this node (the node itself for a component), or {@link NO_ID} for none. */\n component: number;\n /** Computations: ids read by the last run. */\n sources: number[];\n /** Ids of the computations that read this node. */\n observers: number[];\n /** Computations: how many times it ran, the first run included. */\n runs: number;\n /** Computations: runs after the first (the first run is the initial computation, not an update). */\n updates: number;\n /** Computations: duration of the last run, in milliseconds. */\n lastDuration: number;\n /** Computations: total duration of all runs, in milliseconds. */\n totalDuration: number;\n /** Signals and stores: a short text of the current value (empty for other kinds). */\n preview: string;\n /** Signals: number of writes. */\n writes: number;\n /** Resources: the last reported state (empty for other kinds). */\n state: string;\n /** Computations: created during the render phase. */\n render: boolean;\n}\n\n/**\n * Counts shown in the toolbar. The node counts are live nodes; `runs`, `writes`, `flushes`, `errors`\n * and `hydration` are the records the bounded buffers hold, not totals since the start.\n *\n * @since 1.0.0\n */\nexport interface SnapshotCounts {\n /** Live nodes of every kind. */\n nodes: number;\n /** Live owners, roots included. */\n owners: number;\n /** Live nodes a component was mounted on. */\n components: number;\n /** Live signals. */\n signals: number;\n /** Live memos. */\n memos: number;\n /** Live effects. */\n effects: number;\n /** Live resources. */\n resources: number;\n /** Live stores. */\n stores: number;\n /** Run records held (capacity: `ModelLimits.runs`). */\n runs: number;\n /** Write records held (capacity: `ModelLimits.writes`). */\n writes: number;\n /** Flush records held. */\n flushes: number;\n /** Error records held. */\n errors: number;\n /** Hydration mismatch records held. */\n hydration: number;\n}\n\n/**\n * The graph at one point in time: every live node with its edges, the root ids and the counts. Built\n * by {@link snapshotOf}.\n *\n * @since 1.0.0\n */\nexport interface GraphSnapshot {\n /** Sequence number of the last event folded into the model. */\n seq: number;\n /** Every live node, in creation order. */\n nodes: SnapshotNode[];\n /** Ids without a live parent, oldest first. */\n roots: number[];\n /** Counts for the toolbar. */\n counts: SnapshotCounts;\n /** Nodes the model ignored because `maxNodes` was reached. */\n overflow: number;\n}\n\n/**\n * Update statistics of one component, from {@link componentStats}.\n *\n * @since 1.0.0\n */\nexport interface ComponentStat {\n /** Id of the component's owner node. */\n id: number;\n /** The component's name, or `component#<id>` when it has none. */\n name: string;\n /** False once the component's owner has been disposed. */\n alive: boolean;\n /** Time the component was mounted, in milliseconds on the reporting clock. */\n mountedAt: number;\n /** Effects and memos whose nearest component is this one. */\n computations: number;\n /** Runs after the first, summed over those computations. */\n updates: number;\n /** Total run time of those computations, in milliseconds. */\n totalDuration: number;\n /** Sequence number of the latest update run, or 0 when there was none. */\n lastUpdateSeq: number;\n}\n\n/**\n * One bar of the flame timeline, from {@link flameTimeline}.\n *\n * @since 1.0.0\n */\nexport interface FlameSpan {\n /** Sequence number of the run event. */\n seq: number;\n /** The computation that ran. */\n id: number;\n /** Its display name (see {@link nodeLabel}), or `#id` when the node is unknown. */\n label: string;\n /** Its node type (`memo` or `effect`), or `unknown` when the node is unknown. */\n kind: string;\n /** Start time, in milliseconds on the reporting clock. */\n start: number;\n /** End time (`start` plus `duration`), in milliseconds. */\n end: number;\n /** Run time, in milliseconds. */\n duration: number;\n /** Lane: the number of runs of the same flush still running when this one started. */\n depth: number;\n /** Whether the run reported a changed value. */\n changed: boolean;\n /** The nearest component, or {@link NO_ID} for none. */\n component: number;\n}\n\n/**\n * The timeline of one flush, from {@link flameTimeline}.\n *\n * @since 1.0.0\n */\nexport interface FlameFlush {\n /** Flush sequence number, or `0` for runs that ran outside any flush. */\n flush: number;\n /** Start time in milliseconds (for flush `0`, the start of its first run). */\n start: number;\n /** End time in milliseconds (for flush `0`, the latest end of its runs). */\n end: number;\n /** `end - start` in milliseconds, never negative. */\n duration: number;\n /** Render queue size when the flush started (`0` for flush `0`). */\n render: number;\n /** User queue size when the flush started (`0` for flush `0`). */\n user: number;\n /** Number of lanes: the deepest span depth plus one, or `0` when there are no spans. */\n lanes: number;\n /** The runs of the flush, ordered by start time. */\n spans: FlameSpan[];\n}\n\n/**\n * A step of a causal chain, from {@link explainRun}.\n *\n * @since 1.0.0\n */\nexport interface ExplainStep {\n /**\n * `write`: a signal write. `run`: a computation run. `initial`: the first run of a computation.\n * `unknown`: a cause that is neither a write nor a run, such as a resource state change, or one that\n * was not recorded.\n */\n kind: \"write\" | \"run\" | \"initial\" | \"unknown\";\n /** The node involved, or {@link NO_ID} for an `initial` step. */\n id: number;\n /** The node's display name (see {@link nodeLabel}); for `initial` and `unknown` steps, the kind name. */\n label: string;\n /** Sequence number of the event that recorded the step. */\n seq: number;\n /** Writes: the new value (preview). */\n preview?: string;\n /** Writes: label of the computation that wrote, when known. */\n writer?: string;\n /** Runs: whether the value changed. */\n changed?: boolean;\n}\n\n/**\n * The answer to \"why did this run?\", from {@link explainRun}.\n *\n * @since 1.0.0\n */\nexport interface Explanation {\n /** The run that is explained: node id, label, event sequence number and duration in milliseconds. */\n target: { id: number; label: string; seq: number; duration: number };\n /** Root cause first, ending with the run itself. */\n chain: ExplainStep[];\n /** Other sources that also changed before the run. The chain follows only the earliest one. */\n alsoCausedBy: ExplainStep[];\n /** True when history ran out (the ring buffers dropped older records) before the chain reached its root. */\n truncated: boolean;\n}\n\n/**\n * A node of the nested owners tree, from {@link ownerTree}.\n *\n * @since 1.0.0\n */\nexport interface TreeNode {\n /** Node id. */\n id: number;\n /** Owned nodes, in creation order. */\n children: TreeNode[];\n}\n\n/**\n * The display name of a node: its given name, or `label#id` when it has none.\n *\n * @param node Any node record or snapshot node.\n * @returns The name, or `label#id`.\n * @example\n * import { nodeLabel } from \"@shadow-js/devtools\";\n *\n * nodeLabel({ name: \"count\", label: \"signal\", id: 3 }); // \"count\"\n * nodeLabel({ name: undefined, label: \"signal\", id: 3 }); // \"signal#3\"\n * @since 1.0.0\n */\nexport function nodeLabel(node: { name: string | undefined; label: string; id: number }): string {\n return node.name !== undefined && node.name !== \"\" ? node.name : `${node.label}#${node.id}`;\n}\n\nfunction toSnapshotNode(record: NodeRecord, model: InspectorModel): SnapshotNode {\n const computation = record.type === \"memo\" || record.type === \"effect\";\n return {\n id: record.id,\n type: record.type,\n label: record.label,\n name: record.name,\n parent: record.parent,\n children: [],\n depth: 0,\n isComponent: record.isComponent,\n component: model.componentOf(record.id),\n sources: record.sources.slice(),\n observers: record.observers === null ? [] : [...record.observers],\n runs: record.runs,\n updates: computation ? Math.max(record.runs - 1, 0) : 0,\n lastDuration: record.lastDuration,\n totalDuration: record.totalDuration,\n preview: record.preview,\n writes: record.writes,\n state: record.state,\n render: record.render,\n };\n}\n\n/**\n * Builds the graph snapshot: the owners tree, every live node with its sources and observers, and the\n * per-node run counts. Disposed nodes are not included. O(nodes); the model is not changed.\n *\n * @param model The folded model.\n * @returns A JSON-safe snapshot.\n * @example\n * import { foldEvents, snapshotOf } from \"@shadow-js/devtools\";\n *\n * const snap = snapshotOf(foldEvents([]));\n * const roots = snap.roots.map((id) => snap.nodes.find((node) => node.id === id));\n * @see ownerTree\n * @since 1.0.0\n */\nexport function snapshotOf(model: InspectorModel): GraphSnapshot {\n const nodes: SnapshotNode[] = [];\n const byId = new Map<number, SnapshotNode>();\n for (const record of model.nodes.values()) {\n const node = toSnapshotNode(record, model);\n nodes.push(node);\n byId.set(node.id, node);\n }\n const roots: number[] = [];\n for (const node of nodes) {\n const parent = node.parent === NO_ID ? undefined : byId.get(node.parent);\n if (parent === undefined) roots.push(node.id);\n else parent.children.push(node.id);\n }\n // Depth, iteratively: ownership depth is user controlled.\n const stack: number[] = [...roots].reverse();\n while (stack.length > 0) {\n const id = stack.pop();\n if (id === undefined) break;\n const node = byId.get(id);\n if (node === undefined) continue;\n for (const childId of node.children) {\n const child = byId.get(childId);\n if (child === undefined) continue;\n child.depth = node.depth + 1;\n stack.push(childId);\n }\n }\n const count = (type: NodeType): number => nodes.filter((node) => node.type === type).length;\n return {\n seq: model.lastSeq,\n nodes,\n roots,\n overflow: model.overflow,\n counts: {\n nodes: nodes.length,\n owners: count(\"owner\"),\n components: nodes.filter((node) => node.isComponent).length,\n signals: count(\"signal\"),\n memos: count(\"memo\"),\n effects: count(\"effect\"),\n resources: count(\"resource\"),\n stores: count(\"store\"),\n runs: model.runs.size,\n writes: model.writes.size,\n flushes: model.flushes.size,\n errors: model.errors.size,\n hydration: model.hydration.size,\n },\n };\n}\n\n/**\n * The owners as a nested tree, children in creation order. A node whose owner is no longer live\n * becomes a root. Built without recursion, so deep ownership cannot overflow the stack.\n *\n * @param snapshot A snapshot from {@link snapshotOf}.\n * @returns The roots, each with its children.\n * @example\n * import { foldEvents, ownerTree, snapshotOf } from \"@shadow-js/devtools\";\n *\n * const roots = ownerTree(snapshotOf(foldEvents([]))); // [] for an empty model\n * @see snapshotOf\n * @since 1.0.0\n */\nexport function ownerTree(snapshot: GraphSnapshot): TreeNode[] {\n const byId = new Map<number, TreeNode>();\n for (const node of snapshot.nodes) byId.set(node.id, { id: node.id, children: [] });\n const roots: TreeNode[] = [];\n for (const node of snapshot.nodes) {\n const tree = byId.get(node.id);\n if (tree === undefined) continue;\n const parent = node.parent === NO_ID ? undefined : byId.get(node.parent);\n if (parent === undefined) roots.push(tree);\n else parent.children.push(tree);\n }\n return roots;\n}\n\n/**\n * Per-component update counts: how many times the effects and memos under each component re-ran\n * after their first run. Components are the owners a `mount` event named. A component with no\n * computations still gets an entry.\n *\n * @param model The folded model.\n * @returns One entry per component, most updated first (ties by id).\n * @example\n * import { componentStats, foldEvents } from \"@shadow-js/devtools\";\n *\n * const top = componentStats(foldEvents([]))[0]; // undefined: no component is mounted yet\n * @since 1.0.0\n */\nexport function componentStats(model: InspectorModel): ComponentStat[] {\n const stats = new Map<number, ComponentStat>();\n const ensure = (record: NodeRecord): ComponentStat => {\n let stat = stats.get(record.id);\n if (stat === undefined) {\n stat = {\n id: record.id,\n name: record.name ?? `component#${record.id}`,\n alive: record.alive,\n mountedAt: record.mountedAt,\n computations: 0,\n updates: 0,\n totalDuration: 0,\n lastUpdateSeq: 0,\n };\n stats.set(record.id, stat);\n }\n return stat;\n };\n for (const record of model.nodes.values()) if (record.isComponent) ensure(record);\n for (const record of model.nodes.values()) {\n if (record.type !== \"memo\" && record.type !== \"effect\") continue;\n const componentId = model.componentOf(record.id);\n if (componentId === NO_ID) continue;\n const owner = model.node(componentId);\n if (owner === undefined) continue;\n const stat = ensure(owner);\n stat.computations += 1;\n stat.updates += Math.max(record.runs - 1, 0);\n stat.totalDuration += record.totalDuration;\n if (record.runs > 1 && record.lastRunSeq > stat.lastUpdateSeq)\n stat.lastUpdateSeq = record.lastRunSeq;\n }\n return [...stats.values()].sort((a, b) => b.updates - a.updates || a.id - b.id);\n}\n\n/**\n * Lays the runs of each flush out as flame-graph lanes: a run that started inside another run (a memo\n * pulled by an effect) sits one lane deeper. Runs outside any flush are grouped as flush `0`.\n *\n * @param model The folded model.\n * @param options `last`: only the newest N flushes (default: all kept). When `last` is set, flush `0`\n * is left out, and `last: 0` returns no flushes.\n * @returns One entry per flush, oldest first. Flush `0` comes first, when there are runs outside any\n * flush and `last` is not set.\n * @example\n * import { flameTimeline, foldEvents } from \"@shadow-js/devtools\";\n *\n * const lanes = flameTimeline(foldEvents([]), { last: 10 }).map((f) => [f.flush, f.lanes]); // []\n * @since 1.0.0\n */\nexport function flameTimeline(\n model: InspectorModel,\n options: { last?: number } = {},\n): FlameFlush[] {\n const byFlush = new Map<number, RunRecord[]>();\n for (const run of model.runs) {\n const bucket = byFlush.get(run.flush);\n if (bucket === undefined) byFlush.set(run.flush, [run]);\n else bucket.push(run);\n }\n const lay = (runs: RunRecord[]): { spans: FlameSpan[]; lanes: number } => {\n const sorted = runs\n .slice()\n .sort((a, b) => a.start - b.start || b.duration - a.duration || a.seq - b.seq);\n const open: number[] = [];\n const spans: FlameSpan[] = [];\n let lanes = 0;\n for (const run of sorted) {\n while (open.length > 0 && (open[open.length - 1] ?? 0) <= run.start) open.pop();\n const node = model.node(run.id);\n const end = run.start + run.duration;\n spans.push({\n seq: run.seq,\n id: run.id,\n label: node === undefined ? `#${run.id}` : nodeLabel(node),\n kind: node === undefined ? \"unknown\" : node.type,\n start: run.start,\n end,\n duration: run.duration,\n depth: open.length,\n changed: run.changed,\n component: model.componentOf(run.id),\n });\n open.push(end);\n if (open.length > lanes) lanes = open.length;\n }\n return { spans, lanes };\n };\n let flushes = model.flushes.toArray();\n if (options.last !== undefined && options.last >= 0)\n flushes = options.last === 0 ? [] : flushes.slice(-options.last);\n const out: FlameFlush[] = [];\n const loose = byFlush.get(0);\n if (loose !== undefined && loose.length > 0 && options.last === undefined) {\n const { spans, lanes } = lay(loose);\n const start = spans[0]?.start ?? 0;\n const end = spans.reduce((latest, span) => Math.max(latest, span.end), start);\n out.push({\n flush: 0,\n start,\n end,\n duration: end - start,\n render: 0,\n user: 0,\n lanes,\n spans,\n });\n }\n for (const flush of flushes) {\n const { spans, lanes } = lay(byFlush.get(flush.seq) ?? []);\n out.push({\n flush: flush.seq,\n start: flush.start,\n end: flush.end,\n duration: Math.max(flush.end - flush.start, 0),\n render: flush.render,\n user: flush.user,\n lanes,\n spans,\n });\n }\n return out;\n}\n\nfunction findRun(\n model: InspectorModel,\n predicate: (run: RunRecord) => boolean,\n): RunRecord | undefined {\n for (let i = model.runs.size - 1; i >= 0; i -= 1) {\n const run = model.runs.at(i);\n if (run !== undefined && predicate(run)) return run;\n }\n return undefined;\n}\n\nfunction findWrite(model: InspectorModel, seq: number, id: number): WriteRecord | undefined {\n for (let i = model.writes.size - 1; i >= 0; i -= 1) {\n const write = model.writes.at(i);\n if (write === undefined) continue;\n if (write.seq === seq && write.id === id) return write;\n if (write.seq < seq) return undefined;\n }\n return undefined;\n}\n\nfunction label(model: InspectorModel, id: number): string {\n const node = model.node(id);\n return node === undefined ? `#${id}` : nodeLabel(node);\n}\n\n/**\n * \"Why did this run?\": follows the changed sources of a run back to the signal write that started\n * it. A write made inside another effect continues with that effect's own run, so the chain can span\n * several computations. The primary path follows the earliest changed source; the others are\n * listed in `alsoCausedBy`. A chain longer than `maxDepth` is cut at that length.\n *\n * @param model The folded model.\n * @param id The computation to explain.\n * @param options `runSeq`: explain this run instead of the latest; `maxDepth`: chain length limit (default 64).\n * @returns The explanation, or `null` when the computation has no recorded run.\n * @example\n * import { explainRun, foldEvents } from \"@shadow-js/devtools\";\n *\n * const why = explainRun(foldEvents([]), 3);\n * const labels = why?.chain.map((step) => step.label); // undefined: no run of node 3 is recorded\n * @see nodeLabel\n * @since 1.0.0\n */\nexport function explainRun(\n model: InspectorModel,\n id: number,\n options: { runSeq?: number; maxDepth?: number } = {},\n): Explanation | null {\n const target = findRun(\n model,\n (run) => run.id === id && (options.runSeq === undefined || run.seq === options.runSeq),\n );\n if (target === undefined) return null;\n const maxDepth = options.maxDepth ?? 64;\n const reverse: ExplainStep[] = [\n {\n kind: \"run\",\n id: target.id,\n label: label(model, target.id),\n seq: target.seq,\n changed: target.changed,\n },\n ];\n const alsoCausedBy: ExplainStep[] = [];\n const visited = new Set<number>([target.seq]);\n let truncated = false;\n let current: RunRecord = target;\n for (let depth = 0; depth < maxDepth; depth += 1) {\n const [primary, ...others] = current.causes;\n if (primary === undefined) break;\n if (current === target) {\n for (const other of others) {\n alsoCausedBy.push({\n kind: other.kind,\n id: other.id,\n label: label(model, other.id),\n seq: other.seq,\n });\n }\n }\n if (primary.kind === \"initial\" || primary.kind === \"unknown\") {\n reverse.push({ kind: primary.kind, id: primary.id, label: primary.kind, seq: primary.seq });\n break;\n }\n if (primary.kind === \"write\") {\n const write = findWrite(model, primary.seq, primary.id);\n if (write === undefined) {\n reverse.push({\n kind: \"write\",\n id: primary.id,\n label: label(model, primary.id),\n seq: primary.seq,\n });\n truncated = true;\n break;\n }\n const step: ExplainStep = {\n kind: \"write\",\n id: write.id,\n label: label(model, write.id),\n seq: write.seq,\n preview: write.preview,\n };\n if (write.writer !== NO_ID) step.writer = label(model, write.writer);\n reverse.push(step);\n if (write.writer === NO_ID) break;\n // The writing computation's run ends after the write: take its first run recorded later.\n const writerRun = findRunAfter(model, write.writer, write.seq);\n if (writerRun === undefined || visited.has(writerRun.seq)) {\n truncated = writerRun === undefined;\n break;\n }\n visited.add(writerRun.seq);\n reverse.push({\n kind: \"run\",\n id: writerRun.id,\n label: label(model, writerRun.id),\n seq: writerRun.seq,\n changed: writerRun.changed,\n });\n current = writerRun;\n continue;\n }\n // A source that is not a computation (a resource changed state) has no run to step into.\n const source = model.node(primary.id);\n if (source !== undefined && source.type !== \"memo\" && source.type !== \"effect\") {\n reverse.push({\n kind: \"unknown\",\n id: primary.id,\n label: label(model, primary.id),\n seq: primary.seq,\n });\n break;\n }\n const upstream = findRun(model, (run) => run.seq === primary.seq);\n if (upstream === undefined || visited.has(upstream.seq)) {\n reverse.push({\n kind: \"run\",\n id: primary.id,\n label: label(model, primary.id),\n seq: primary.seq,\n });\n truncated = upstream === undefined;\n break;\n }\n visited.add(upstream.seq);\n reverse.push({\n kind: \"run\",\n id: upstream.id,\n label: label(model, upstream.id),\n seq: upstream.seq,\n changed: upstream.changed,\n });\n current = upstream;\n }\n return {\n target: {\n id: target.id,\n label: label(model, target.id),\n seq: target.seq,\n duration: target.duration,\n },\n chain: reverse.reverse(),\n alsoCausedBy,\n truncated,\n };\n}\n\nfunction findRunAfter(model: InspectorModel, id: number, seq: number): RunRecord | undefined {\n let found: RunRecord | undefined;\n for (let i = model.runs.size - 1; i >= 0; i -= 1) {\n const run = model.runs.at(i);\n if (run === undefined || run.seq <= seq) break;\n if (run.id === id) found = run;\n }\n return found;\n}\n"],"mappings":"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"}
|