434
*/
435
export function renderLaneGraph(history: ExecutionHistory): string {
437
>
if (events.length === 0) { return ''; }
438
>
439
>
interface Node {
440
>
readonly label: string;
441
>
readonly parent: Node | undefined;
442
>
readonly isSynthetic: boolean;
443
>
}
444
>
445
>
// Insert synthetic root nodes before their first child.
446
>
const nodes: Node[] = [];
447
>
const syntheticForRoot = new Map<ExecutionRoot, Node>();
448
>
const nodeByEvent = new Map<ExecutionEvent, Node>();
449
>
450
>
// Which roots have at least one direct child event?
451
>
const rootsWithChildren = new Set<ExecutionRoot>();
452
>
for (const e of events) { if (!e.parent) { rootsWithChildren.add(e.root); } }
453
>
454
>
for (const e of events) {
455
>
if (rootsWithChildren.has(e.root) && !syntheticForRoot.has(e.root)) {
456
>
const syn: Node = { label: `+${e.root.label}`, parent: undefined, isSynthetic: true };
457
>
syntheticForRoot.set(e.root, syn);
458
>
nodes.push(syn);
459
>
}
460
>
const timeStr = `+${e.time}ms`.padStart(7);
461
>
const parent = e.parent ? nodeByEvent.get(e.parent)! : syntheticForRoot.get(e.root);
462
>
const node: Node = { label: `[${timeStr}] ${e.label}`, parent, isSynthetic: false };
463
>
nodeByEvent.set(e, node);
464
>
nodes.push(node);
465
>
}
466
>
467
>
const n = nodes.length;
468
>
const parentOf = new Array<number>(n).fill(-1);
469
>
const childrenOf: number[][] = Array.from({ length: n }, () => []);
470
>
const indexOfNode = new Map<Node, number>();
471
>
for (let i = 0; i < n; i++) { indexOfNode.set(nodes[i], i); }
472
>
for (let i = 0; i < n; i++) {
473
>
const p = nodes[i].parent;
474
>
if (p) {
475
>
const pi = indexOfNode.get(p);
476
>
if (pi !== undefined) { parentOf[i] = pi; childrenOf[pi].push(i); }
477
>
}
478
>
}
479
>
480
>
// Assign columns: every node with children gets its own column.
481
>
const colOf = new Array<number>(n).fill(-1);
482
>
let totalCols = 0;
483
>
for (let i = 0; i < n; i++) {
484
>
if (childrenOf[i].length > 0) { colOf[i] = totalCols++; }
485
>
}
486
>
487
>
if (totalCols === 0) {
488
return events.map(e => `[+${`${e.time}ms`.padStart(5)}] ${e.label}`).join('\n');
489
}
491
>
const active = new Array<number>(totalCols).fill(-1);
492
>
const lines: string[] = [];
493
>
494
>
for (let i = 0; i < n; i++) {
495
>
const node = nodes[i];
496
>
const pIdx = parentOf[i];
497
>
const connectCol = pIdx >= 0 ? colOf[pIdx] : -1;
498
>
const last = pIdx >= 0 && childrenOf[pIdx][childrenOf[pIdx].length - 1] === i;
499
>
const opensCol = childrenOf[i].length > 0 ? colOf[i] : -1;
500
>
const horizEnd = pIdx >= 0 ? (opensCol >= 0 ? opensCol : totalCols) : -1;
501
>
502
>
const chars: string[] = [];
503
>
for (let c = 0; c < totalCols; c++) {
504
>
const isActive = active[c] >= 0;
505
>
const isConnect = c === connectCol;
506
>
const isOpen = c === opensCol && !isConnect;
507
>
const inHoriz = connectCol >= 0 && c > connectCol && c < horizEnd;
508
>
509
>
let g: string, s: string;
510
>
if (isConnect) {
511
>
g = last ? '└' : '├';
512
>
s = '─';
513
>
} else if (isOpen && node.isSynthetic) {
514
>
g = '+';
515
>
s = node.label.slice(1, 2) || '?';
516
>
} else if (isOpen && connectCol >= 0) {
517
>
g = '╷'; s = '─';
518
>
} else if (isOpen) {
519
g = '╷'; s = ' ';
521
g = '┼'; s = '─';
523
g = '─'; s = '─';
525
g = '│'; s = ' ';
527
>
g = ' '; s = ' ';
528
>
}
529
>
chars.push(g, s);
530
>
}
531
>
532
>
if (last) { active[colOf[pIdx]] = -1; }
533
>
if (opensCol >= 0) { active[opensCol] = i; }
534
>
535
>
if (node.isSynthetic) {
536
>
lines.push(chars.join('').trimEnd());
537
>
} else {
538
>
lines.push(`${chars.join('')}${node.label}`);
539
>
}
540
>
}
541
>
542
>
return lines.join('\n');
543
>
}