197
const { roots, events } = history;
198
if (events.length === 0) { return '(empty history)'; }
200
return events.map(e => `[+${e.time}ms] ${e.label}`).join('\n');
201
}
203
>
const n = events.length;
204
>
205
>
// Parent index per event (-1 = direct child of root).
206
>
const parentOf = new Array<number>(n).fill(-1);
207
>
const childrenOf: number[][] = Array.from({ length: n }, () => []);
208
>
const indexOfEvent = new Map<ExecutionEvent, number>();
209
>
for (let i = 0; i < n; i++) { indexOfEvent.set(events[i], i); }
210
>
for (let i = 0; i < n; i++) {
211
>
const p = events[i].parent;
212
>
if (p) {
213
>
const pi = indexOfEvent.get(p);
214
>
if (pi !== undefined) {
215
>
parentOf[i] = pi;
216
>
childrenOf[pi].push(i);
217
>
}
218
>
}
219
>
}
220
>
221
>
// Is this event the last child of its parent event?
222
>
const isLastChild = new Array<boolean>(n).fill(false);
223
>
for (let i = 0; i < n; i++) {
224
>
const p = parentOf[i];
225
>
if (p >= 0 && childrenOf[p][childrenOf[p].length - 1] === i) { isLastChild[i] = true; }
226
>
}
227
>
228
>
// Slot = visual column index for indentation. By default every child
229
>
// gets its own column (slot = parent.slot + 1) so pure last-child chains
230
>
// still show their depth structure. Once we pass the depth threshold,
231
>
// last-children collapse into their parent's slot to keep deeply nested
232
>
// traces from walking off the screen.
233
>
const COLLAPSE_DEPTH_THRESHOLD = 6;
234
>
const depthOf = new Array<number>(n).fill(0);
235
>
const slotOf = new Array<number>(n).fill(0);
236
>
for (let i = 0; i < n; i++) {
237
>
const p = parentOf[i];
238
>
if (p >= 0) {
239
>
depthOf[i] = depthOf[p] + 1;
240
>
const collapse = isLastChild[i] && depthOf[i] >= COLLAPSE_DEPTH_THRESHOLD;
241
>
slotOf[i] = slotOf[p] + (collapse ? 0 : 1);
242
>
}
243
>
}
244
>
245
>
// Display label = label plus the caller stack frame when present,
246
>
// e.g. `setTimeout · MyClass.foo (file.ts:42)`. Computed once so width
247
>
// math and the per-row render agree. `detailLines` holds any additional
248
>
// stack frames beyond the first; they are rendered as continuation rows.
249
>
const displayLabelOf = new Array<string>(n);
250
>
const detailLinesOf = new Array<readonly string[]>(n);
251
>
for (let i = 0; i < n; i++) {
252
>
const e = events[i];
253
>
const frames = e.detail ? e.detail.split('\n') : [];
254
>
displayLabelOf[i] = frames.length > 0 ? `${e.label} · ${frames[0]}` : e.label;
255
>
detailLinesOf[i] = frames.slice(1);
256
>
}
257
>
258
>
// Column width per root: indentation uses slots (last-children collapse
259
>
// into their parent's slot), so width must be slot-based to avoid
260
>
// reserving empty space for degenerate last-child chains.
261
>
const widthOf = new Map<ExecutionRoot, number>();
262
>
for (const r of roots) { widthOf.set(r, r.label.length); }
263
>
for (let i = 0; i < n; i++) {
264
>
const baseIndent = slotOf[i] * 3 + 3;
265
>
const maxLen = Math.max(displayLabelOf[i].length, ...detailLinesOf[i].map(l => l.length + 2));
266
>
const w = baseIndent + maxLen;
267
>
const cur = widthOf.get(events[i].root) ?? 0;
268
>
if (w > cur) { widthOf.set(events[i].root, w); }
269
>
}
270
>
271
>
// Compute time column width based on max time (rounded).
272
const maxTime = n > 0 ? Math.max(...events.map(e => Math.round(e.time))) : 0;
273
const timeColWidth = `+${maxTime}ms`.length;