@abreen/tada 1.13.7 → 1.14.1
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/README.md +46 -0
- package/build/generate-content-assets.ts +39 -185
- package/build/pipeline.ts +1 -1
- package/build/source-model.ts +2 -14
- package/build/types.d.ts +18 -18
- package/build/utils/html-comments.ts +3 -0
- package/build/utils/jdi-runner/TraceRunner.java +227 -38
- package/build/utils/markdown-partials.ts +235 -0
- package/build/utils/markdown.ts +12 -1
- package/build/utils/python-runner/trace_runner.py +58 -13
- package/build/utils/render.ts +14 -12
- package/build/utils/trace-core.ts +136 -14
- package/build/utils/trace-heap.ts +34 -0
- package/build/utils/trace-java.ts +60 -7
- package/build/utils/trace-layout.ts +58 -13
- package/build/utils/trace-python.ts +6 -2
- package/build/utils/trace-svg.ts +54 -60
- package/build/utils/trace.ts +193 -78
- package/build/watch/build-full.ts +26 -52
- package/build/watch/build-helpers.ts +66 -0
- package/build/watch/build-incremental.ts +32 -60
- package/build/watch/build-result.ts +19 -0
- package/build/watch/compiler-types.d.ts +2 -2
- package/build/watch/compiler.ts +30 -62
- package/build/watch/config-paths.ts +56 -0
- package/build/watch/engine.ts +199 -0
- package/{watch → build/watch}/fs-commit.ts +1 -3
- package/build/watch/index.ts +1 -1
- package/build/watch/mutations.ts +23 -25
- package/build/watch/planner.ts +14 -78
- package/build/watch/runtime.ts +26 -38
- package/build/watch/snapshot.ts +26 -47
- package/build/watch/types.d.ts +84 -0
- package/build/watch/validation.ts +1 -1
- package/init/content/lectures/02/_pr1.md +1 -1
- package/init/content/lectures/02/index.md +2 -2
- package/init/content/lectures/02/subdir/_foobar.md +3 -0
- package/init/content/lectures/02/subdir/_pr2.md +3 -3
- package/package.json +1 -2
- package/src/slides/index.ts +6 -2
- package/src/trace/index.ts +49 -12
- package/src/trace/style.scss +10 -0
- package/src/trace/types.d.ts +16 -4
- package/build/utils/include.ts +0 -57
- package/build/watch-reachability-state.ts +0 -305
- package/init/content/lectures/02/subdir/_foobar.html +0 -3
- package/watch/engine.ts +0 -296
- package/watch/events.ts +0 -10
- package/watch/types.d.ts +0 -103
|
@@ -5,6 +5,11 @@ import type {
|
|
|
5
5
|
TraceLayout,
|
|
6
6
|
TraceObjectLayout,
|
|
7
7
|
} from '../types';
|
|
8
|
+
import {
|
|
9
|
+
formatHeapObjectValue,
|
|
10
|
+
isInlineHeapObject,
|
|
11
|
+
type TraceHeapScalar,
|
|
12
|
+
} from './trace-heap';
|
|
8
13
|
|
|
9
14
|
/** Maximum number of array cells to display. */
|
|
10
15
|
export const ARRAY_MAX_CELLS = 12;
|
|
@@ -24,6 +29,9 @@ const ROW_HEIGHT_RATIO = 2;
|
|
|
24
29
|
/** Minimum width for a value box in a field object. */
|
|
25
30
|
const MIN_VALUE_WIDTH = 40;
|
|
26
31
|
|
|
32
|
+
/** Minimum width for each displayed array cell. */
|
|
33
|
+
const MIN_ARRAY_CELL_WIDTH = 48;
|
|
34
|
+
|
|
27
35
|
/** Information collected about a heap object across all trace steps. */
|
|
28
36
|
export interface ObjectInfo {
|
|
29
37
|
id: string;
|
|
@@ -32,14 +40,18 @@ export interface ObjectInfo {
|
|
|
32
40
|
lastStep: number;
|
|
33
41
|
/** Set of object IDs this object ever references (across all steps). */
|
|
34
42
|
references: Set<string>;
|
|
43
|
+
/** Step indices where this object is actually present. */
|
|
44
|
+
activeSteps: Set<number>;
|
|
35
45
|
/** Per-field reference targets (field name -> set of referenced object IDs). */
|
|
36
46
|
fieldRefs: Map<string, Set<string>>;
|
|
37
47
|
/** Max number of fields seen on this object (for field objects). */
|
|
38
48
|
maxFields: number;
|
|
39
49
|
/** Max number of elements seen on this object (for arrays). */
|
|
40
50
|
maxElements: number;
|
|
41
|
-
/**
|
|
42
|
-
value:
|
|
51
|
+
/** Scalar value (for heap objects represented by a single value). */
|
|
52
|
+
value: TraceHeapScalar | null;
|
|
53
|
+
hasValue: boolean;
|
|
54
|
+
isInlineValue: boolean;
|
|
43
55
|
isArray: boolean;
|
|
44
56
|
isString: boolean;
|
|
45
57
|
/** All field names ever seen on this object. */
|
|
@@ -74,10 +86,13 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
|
|
|
74
86
|
firstStep: stepIndex,
|
|
75
87
|
lastStep: stepIndex,
|
|
76
88
|
references: new Set(),
|
|
89
|
+
activeSteps: new Set(),
|
|
77
90
|
fieldRefs: new Map(),
|
|
78
91
|
maxFields: 0,
|
|
79
92
|
maxElements: 0,
|
|
80
93
|
value: null,
|
|
94
|
+
hasValue: false,
|
|
95
|
+
isInlineValue: false,
|
|
81
96
|
isArray: false,
|
|
82
97
|
isString: false,
|
|
83
98
|
fieldNames: new Set(),
|
|
@@ -87,6 +102,7 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
|
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
info.lastStep = stepIndex;
|
|
105
|
+
info.activeSteps.add(stepIndex);
|
|
90
106
|
|
|
91
107
|
if ('elements' in obj) {
|
|
92
108
|
info.isArray = true;
|
|
@@ -125,7 +141,9 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
|
|
|
125
141
|
}
|
|
126
142
|
}
|
|
127
143
|
} else if ('value' in obj) {
|
|
128
|
-
info.
|
|
144
|
+
info.hasValue = true;
|
|
145
|
+
info.isString = obj.type === 'String';
|
|
146
|
+
info.isInlineValue ||= isInlineHeapObject(obj);
|
|
129
147
|
info.value = obj.value;
|
|
130
148
|
}
|
|
131
149
|
}
|
|
@@ -229,17 +247,24 @@ export function getObjectSize(
|
|
|
229
247
|
const charWidth = fontSize * CHAR_WIDTH_RATIO;
|
|
230
248
|
const rowHeight = fontSize * ROW_HEIGHT_RATIO;
|
|
231
249
|
|
|
232
|
-
if (info.
|
|
233
|
-
const text =
|
|
250
|
+
if (info.isInlineValue) {
|
|
251
|
+
const text = formatHeapObjectValue(info.type, info.value ?? '');
|
|
252
|
+
const textWidth = text.length * charWidth;
|
|
253
|
+
const minWidth = info.isString ? info.type.length * charWidth + 16 : 16;
|
|
254
|
+
const width = Math.max(minWidth, textWidth + 16);
|
|
255
|
+
return { width, height: rowHeight * 2 };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (info.hasValue) {
|
|
259
|
+
const text = String(info.value ?? '');
|
|
234
260
|
const textWidth = text.length * charWidth;
|
|
235
261
|
const width = Math.max(info.type.length * charWidth + 16, textWidth + 16);
|
|
236
|
-
|
|
237
|
-
return { width, height };
|
|
262
|
+
return { width, height: rowHeight * 2 };
|
|
238
263
|
}
|
|
239
264
|
|
|
240
265
|
if (info.isArray) {
|
|
241
266
|
const cellCount = Math.min(info.maxElements, ARRAY_MAX_CELLS);
|
|
242
|
-
const cellWidth = Math.max(charWidth * 3,
|
|
267
|
+
const cellWidth = Math.max(charWidth * 3, MIN_ARRAY_CELL_WIDTH);
|
|
243
268
|
const width = Math.max(
|
|
244
269
|
info.type.length * charWidth + 16,
|
|
245
270
|
cellCount * cellWidth,
|
|
@@ -754,6 +779,13 @@ function layoutArrays(
|
|
|
754
779
|
};
|
|
755
780
|
});
|
|
756
781
|
|
|
782
|
+
const minPlacementX = Math.min(...placements.map(p => p.x));
|
|
783
|
+
if (minPlacementX < 0) {
|
|
784
|
+
for (const p of placements) {
|
|
785
|
+
p.x -= minPlacementX;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
757
789
|
const gap = Math.round(OBJ_GAP * 0.5);
|
|
758
790
|
for (let j = 1; j < placements.length; j++) {
|
|
759
791
|
const prev = placements[j - 1];
|
|
@@ -767,7 +799,7 @@ function layoutArrays(
|
|
|
767
799
|
const refY = Math.round(yOffset + size.height + OBJ_GAP);
|
|
768
800
|
let maxRefHeight = 0;
|
|
769
801
|
for (const p of placements) {
|
|
770
|
-
result[p.id] = makeLayout(Math.round(
|
|
802
|
+
result[p.id] = makeLayout(Math.round(p.x), refY, p.size);
|
|
771
803
|
maxRefHeight = Math.max(maxRefHeight, p.height);
|
|
772
804
|
}
|
|
773
805
|
|
|
@@ -789,7 +821,11 @@ function layoutOrphans(
|
|
|
789
821
|
result: Record<string, TraceObjectLayout>,
|
|
790
822
|
yOffset: number,
|
|
791
823
|
) {
|
|
792
|
-
type OrphanSlot = {
|
|
824
|
+
type OrphanSlot = {
|
|
825
|
+
ids: string[];
|
|
826
|
+
activeSteps: Set<number>;
|
|
827
|
+
maxHeight: number;
|
|
828
|
+
};
|
|
793
829
|
|
|
794
830
|
const orphanIds = [...objects.keys()]
|
|
795
831
|
.filter(id => !(id in result))
|
|
@@ -806,14 +842,23 @@ function layoutOrphans(
|
|
|
806
842
|
const info = objects.get(id)!;
|
|
807
843
|
const size = sizes.get(id)!;
|
|
808
844
|
|
|
809
|
-
let slot = slots.find(existing =>
|
|
845
|
+
let slot = slots.find(existing => {
|
|
846
|
+
for (const stepIndex of info.activeSteps) {
|
|
847
|
+
if (existing.activeSteps.has(stepIndex)) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return true;
|
|
852
|
+
});
|
|
810
853
|
if (!slot) {
|
|
811
|
-
slot = { ids: [],
|
|
854
|
+
slot = { ids: [], activeSteps: new Set(), maxHeight: size.height };
|
|
812
855
|
slots.push(slot);
|
|
813
856
|
} else {
|
|
814
|
-
slot.lastStep = info.lastStep;
|
|
815
857
|
slot.maxHeight = Math.max(slot.maxHeight, size.height);
|
|
816
858
|
}
|
|
859
|
+
for (const stepIndex of info.activeSteps) {
|
|
860
|
+
slot.activeSteps.add(stepIndex);
|
|
861
|
+
}
|
|
817
862
|
slot.ids.push(id);
|
|
818
863
|
}
|
|
819
864
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { execFileSyncPython, resolvePythonCommand } from '../../python/command';
|
|
3
3
|
|
|
4
|
-
export function runPythonTrace(
|
|
4
|
+
export function runPythonTrace(
|
|
5
|
+
pythonFilePath: string,
|
|
6
|
+
tracedFilePaths: string[],
|
|
7
|
+
): string {
|
|
5
8
|
const pythonCommand = resolvePythonCommand();
|
|
6
9
|
if (!pythonCommand) {
|
|
7
10
|
throw new Error('Python is required to generate Python traces');
|
|
@@ -16,10 +19,11 @@ export function runPythonTrace(pythonFilePath: string): string {
|
|
|
16
19
|
timeout: 60000,
|
|
17
20
|
encoding: 'utf-8' as const,
|
|
18
21
|
maxBuffer: 50 * 1024 * 1024,
|
|
22
|
+
cwd: path.dirname(pythonFilePath),
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
return execFileSyncPython(
|
|
22
|
-
[runnerPath, pythonFilePath],
|
|
26
|
+
[runnerPath, pythonFilePath, ...tracedFilePaths],
|
|
23
27
|
options,
|
|
24
28
|
pythonCommand,
|
|
25
29
|
);
|
package/build/utils/trace-svg.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TraceObjectLayout,
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { ARRAY_MAX_CELLS, DEFAULT_FONT_SIZE } from './trace-layout';
|
|
10
|
+
import { formatHeapObjectValue, isInlineHeapObject } from './trace-heap';
|
|
10
11
|
|
|
11
12
|
/** Gap between stack area and heap area. */
|
|
12
13
|
const SECTION_GAP = 60;
|
|
@@ -17,6 +18,9 @@ const PADDING = 8;
|
|
|
17
18
|
/** Horizontal margin when routing stack-to-heap arrows. */
|
|
18
19
|
const ARROW_MARGIN = 20;
|
|
19
20
|
|
|
21
|
+
/** Approximate monospace character width for arrow targeting. */
|
|
22
|
+
const CHAR_WIDTH_RATIO = 0.6;
|
|
23
|
+
|
|
20
24
|
// ---------------------------------------------------------------------------
|
|
21
25
|
// Escape & format helpers
|
|
22
26
|
// ---------------------------------------------------------------------------
|
|
@@ -58,13 +62,9 @@ export function formatValue(val: TraceValue): string {
|
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
// ---------------------------------------------------------------------------
|
|
61
|
-
// filterStep -- remove args from main(), hide
|
|
65
|
+
// filterStep -- remove args from main(), hide unreachable heap objects
|
|
62
66
|
// ---------------------------------------------------------------------------
|
|
63
67
|
|
|
64
|
-
function isStringObject(obj: TraceHeapObject): boolean {
|
|
65
|
-
return 'value' in obj && obj.type === 'String';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
68
|
export function filterStep(step: TraceStep): {
|
|
69
69
|
stack: TraceStackFrame[];
|
|
70
70
|
heap: Record<string, TraceHeapObject>;
|
|
@@ -84,38 +84,6 @@ export function filterStep(step: TraceStep): {
|
|
|
84
84
|
return { ...frame, locals };
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
// Find objects reachable only through the hidden 'args' variable
|
|
88
|
-
const argsOnly = new Set<string>();
|
|
89
|
-
for (const frame of step.stack) {
|
|
90
|
-
if (frame.method === 'main') {
|
|
91
|
-
const argsVal = frame.locals.args;
|
|
92
|
-
if (argsVal?.type === 'ref') {
|
|
93
|
-
const q = [argsVal.id];
|
|
94
|
-
while (q.length > 0) {
|
|
95
|
-
const id = q.shift()!;
|
|
96
|
-
if (argsOnly.has(id) || !step.heap[id]) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
argsOnly.add(id);
|
|
100
|
-
const obj = step.heap[id];
|
|
101
|
-
if ('elements' in obj) {
|
|
102
|
-
for (const el of obj.elements) {
|
|
103
|
-
if (el.type === 'ref') {
|
|
104
|
-
q.push(el.id);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} else if ('fields' in obj) {
|
|
108
|
-
for (const v of Object.values(obj.fields)) {
|
|
109
|
-
if (v.type === 'ref') {
|
|
110
|
-
q.push(v.id);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
87
|
// Find objects reachable from visible (non-args) variables
|
|
120
88
|
const reachable = new Set<string>();
|
|
121
89
|
const queue: string[] = [];
|
|
@@ -126,6 +94,9 @@ export function filterStep(step: TraceStep): {
|
|
|
126
94
|
}
|
|
127
95
|
}
|
|
128
96
|
}
|
|
97
|
+
for (const id of step.transientHeapRoots ?? []) {
|
|
98
|
+
queue.push(id);
|
|
99
|
+
}
|
|
129
100
|
while (queue.length > 0) {
|
|
130
101
|
const id = queue.shift()!;
|
|
131
102
|
if (reachable.has(id) || !step.heap[id]) {
|
|
@@ -148,11 +119,10 @@ export function filterStep(step: TraceStep): {
|
|
|
148
119
|
}
|
|
149
120
|
}
|
|
150
121
|
|
|
151
|
-
// Keep heap objects
|
|
152
|
-
// aren't exclusively reachable from the hidden 'args'
|
|
122
|
+
// Keep only heap objects reachable from visible variables.
|
|
153
123
|
const heap: Record<string, TraceHeapObject> = {};
|
|
154
124
|
const sortedIds = Object.keys(step.heap)
|
|
155
|
-
.filter(id => reachable.has(id)
|
|
125
|
+
.filter(id => reachable.has(id))
|
|
156
126
|
.sort((a, b) => {
|
|
157
127
|
const numA = parseInt(a.split('_').pop()!, 10);
|
|
158
128
|
const numB = parseInt(b.split('_').pop()!, 10);
|
|
@@ -294,11 +264,12 @@ function svgHeapObject(
|
|
|
294
264
|
`<g class="trace-obj" data-id="${escapeXml(id)}" transform="translate(${absX},${absY})">`,
|
|
295
265
|
);
|
|
296
266
|
|
|
297
|
-
if ('value' in obj &&
|
|
298
|
-
// Strings
|
|
267
|
+
if ('value' in obj && isInlineHeapObject(obj)) {
|
|
268
|
+
// Strings and Java boxed primitives are heap values, but render inline.
|
|
299
269
|
parts.push(
|
|
300
|
-
`<text class="trace-val" x="
|
|
301
|
-
`dominant-baseline="central"
|
|
270
|
+
`<text class="trace-val" x="${width / 2}" y="${height / 2}" ` +
|
|
271
|
+
`text-anchor="middle" dominant-baseline="central">` +
|
|
272
|
+
`${escapeXml(formatHeapObjectValue(obj.type, obj.value))}</text>`,
|
|
302
273
|
);
|
|
303
274
|
parts.push('</g>');
|
|
304
275
|
return parts.join('');
|
|
@@ -505,6 +476,43 @@ function rectBoundaryPoint(
|
|
|
505
476
|
return { x: Math.round(cx + dx * scale), y: Math.round(cy + dy * scale) };
|
|
506
477
|
}
|
|
507
478
|
|
|
479
|
+
function heapTargetBoundaryPoint(
|
|
480
|
+
sx: number,
|
|
481
|
+
sy: number,
|
|
482
|
+
target: HeapObjInfo,
|
|
483
|
+
obj: TraceHeapObject | undefined,
|
|
484
|
+
dims: { fontSize: number },
|
|
485
|
+
): { x: number; y: number } {
|
|
486
|
+
if (!obj || !('value' in obj) || !isInlineHeapObject(obj)) {
|
|
487
|
+
return rectBoundaryPoint(
|
|
488
|
+
sx,
|
|
489
|
+
sy,
|
|
490
|
+
target.absX,
|
|
491
|
+
target.absY,
|
|
492
|
+
target.width,
|
|
493
|
+
target.height,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const text = formatHeapObjectValue(obj.type, obj.value);
|
|
498
|
+
const textWidth = Math.max(
|
|
499
|
+
dims.fontSize,
|
|
500
|
+
text.length * dims.fontSize * CHAR_WIDTH_RATIO,
|
|
501
|
+
);
|
|
502
|
+
const textHeight = dims.fontSize * 1.2;
|
|
503
|
+
const textPadding = 4;
|
|
504
|
+
const rx = target.absX + (target.width - textWidth) / 2 - textPadding;
|
|
505
|
+
const ry = target.absY + (target.height - textHeight) / 2 - textPadding;
|
|
506
|
+
return rectBoundaryPoint(
|
|
507
|
+
sx,
|
|
508
|
+
sy,
|
|
509
|
+
rx,
|
|
510
|
+
ry,
|
|
511
|
+
textWidth + textPadding * 2,
|
|
512
|
+
textHeight + textPadding * 2,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
508
516
|
/**
|
|
509
517
|
* Get the center of the child-slot box for a given field.
|
|
510
518
|
* The slot row has a small label at the top and a box below it.
|
|
@@ -547,14 +555,7 @@ function generateArrows(
|
|
|
547
555
|
target: HeapObjInfo,
|
|
548
556
|
style: 'fg' | 'dim' = 'fg',
|
|
549
557
|
): string {
|
|
550
|
-
const ep =
|
|
551
|
-
sx,
|
|
552
|
-
sy,
|
|
553
|
-
target.absX,
|
|
554
|
-
target.absY,
|
|
555
|
-
target.width,
|
|
556
|
-
target.height,
|
|
557
|
-
);
|
|
558
|
+
const ep = heapTargetBoundaryPoint(sx, sy, target, heap[target.id], dims);
|
|
558
559
|
const cpx = Math.round((sx + ep.x) / 2);
|
|
559
560
|
const cls = style === 'fg' ? 'trace-arrow' : 'trace-arrow-dim';
|
|
560
561
|
const marker = style === 'fg' ? 'trace-arrowhead' : 'trace-arrowhead-dim';
|
|
@@ -606,14 +607,7 @@ function generateArrows(
|
|
|
606
607
|
// Non-structural heap arrow: cubic Bezier with auto-detected direction
|
|
607
608
|
// (typically horizontal toward ref chains).
|
|
608
609
|
function heapArrow(sx: number, sy: number, target: HeapObjInfo): string {
|
|
609
|
-
const ep =
|
|
610
|
-
sx,
|
|
611
|
-
sy,
|
|
612
|
-
target.absX,
|
|
613
|
-
target.absY,
|
|
614
|
-
target.width,
|
|
615
|
-
target.height,
|
|
616
|
-
);
|
|
610
|
+
const ep = heapTargetBoundaryPoint(sx, sy, target, heap[target.id], dims);
|
|
617
611
|
const dx = ep.x - sx;
|
|
618
612
|
const dy = ep.y - sy;
|
|
619
613
|
const dist = Math.sqrt(dx * dx + dy * dy);
|