@abreen/tada 1.13.9 → 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 +2 -10
- package/build/types.d.ts +18 -6
- 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/compiler-types.d.ts +2 -2
- package/build/watch/compiler.ts +5 -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 -1
- 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/init/content/lectures/02/subdir/_foobar.html +0 -3
|
@@ -5,8 +5,14 @@ import { renderCodeSegment } from './code';
|
|
|
5
5
|
import { splitLines } from './literate-java';
|
|
6
6
|
import { normalizeOutputPath } from './paths';
|
|
7
7
|
import { computeLayout } from './trace-layout';
|
|
8
|
-
import { generateStepSvg } from './trace-svg';
|
|
9
|
-
import type {
|
|
8
|
+
import { filterStep, generateStepSvg } from './trace-svg';
|
|
9
|
+
import type {
|
|
10
|
+
TraceChunkEntry,
|
|
11
|
+
TraceHeapObject,
|
|
12
|
+
TraceManifest,
|
|
13
|
+
TraceOutputEvent,
|
|
14
|
+
TraceStep,
|
|
15
|
+
} from '../types';
|
|
10
16
|
|
|
11
17
|
export const DEFAULT_CHUNK_SIZE = 50;
|
|
12
18
|
|
|
@@ -21,6 +27,10 @@ export interface ChunkTraceOutputResult {
|
|
|
21
27
|
outputPaths: string[];
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
function stepOutputEvents(step: TraceStep): TraceOutputEvent[] {
|
|
31
|
+
return step.output ?? [];
|
|
32
|
+
}
|
|
33
|
+
|
|
24
34
|
function hashTraceFiles(files: { name: string; content: string }[]): string {
|
|
25
35
|
const hasher = createHash('sha256');
|
|
26
36
|
for (const file of files) {
|
|
@@ -43,13 +53,101 @@ function buildTraceOutputPath(
|
|
|
43
53
|
.join('/');
|
|
44
54
|
}
|
|
45
55
|
|
|
56
|
+
function escapeAttr(value: string): string {
|
|
57
|
+
return value
|
|
58
|
+
.replaceAll('&', '&')
|
|
59
|
+
.replaceAll('"', '"')
|
|
60
|
+
.replaceAll('<', '<')
|
|
61
|
+
.replaceAll('>', '>');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function framesMatchCaller(previous: TraceStep, current: TraceStep): boolean {
|
|
65
|
+
if (current.stack.length !== previous.stack.length - 1) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return current.stack.every((frame, index) => {
|
|
70
|
+
const previousFrame = previous.stack[index + 1];
|
|
71
|
+
return (
|
|
72
|
+
frame.method === previousFrame.method &&
|
|
73
|
+
frame.class === previousFrame.class
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addReachableHeapObjects(
|
|
79
|
+
id: string,
|
|
80
|
+
source: Record<string, TraceHeapObject>,
|
|
81
|
+
target: Record<string, TraceHeapObject>,
|
|
82
|
+
): void {
|
|
83
|
+
const queue = [id];
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
|
|
86
|
+
while (queue.length > 0) {
|
|
87
|
+
const currentId = queue.shift()!;
|
|
88
|
+
if (seen.has(currentId) || !source[currentId]) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
seen.add(currentId);
|
|
92
|
+
target[currentId] ??= source[currentId];
|
|
93
|
+
|
|
94
|
+
const obj = source[currentId];
|
|
95
|
+
if ('elements' in obj) {
|
|
96
|
+
for (const element of obj.elements) {
|
|
97
|
+
if (element.type === 'ref') {
|
|
98
|
+
queue.push(element.id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else if ('fields' in obj) {
|
|
102
|
+
for (const value of Object.values(obj.fields)) {
|
|
103
|
+
if (value.type === 'ref') {
|
|
104
|
+
queue.push(value.id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function bridgeConstructorReturnValues(steps: TraceStep[]): TraceStep[] {
|
|
112
|
+
const bridgedSteps = steps.map(step => ({
|
|
113
|
+
...step,
|
|
114
|
+
heap: { ...step.heap },
|
|
115
|
+
transientHeapRoots: step.transientHeapRoots
|
|
116
|
+
? [...step.transientHeapRoots]
|
|
117
|
+
: undefined,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
for (let i = 1; i < bridgedSteps.length; i++) {
|
|
121
|
+
const previous = bridgedSteps[i - 1];
|
|
122
|
+
const current = bridgedSteps[i];
|
|
123
|
+
const poppedFrame = previous.stack[0];
|
|
124
|
+
const thisValue = poppedFrame?.locals.this;
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
poppedFrame?.method !== '<init>' ||
|
|
128
|
+
thisValue?.type !== 'ref' ||
|
|
129
|
+
!framesMatchCaller(previous, current)
|
|
130
|
+
) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
addReachableHeapObjects(thisValue.id, previous.heap, current.heap);
|
|
135
|
+
current.transientHeapRoots ??= [];
|
|
136
|
+
if (!current.transientHeapRoots.includes(thisValue.id)) {
|
|
137
|
+
current.transientHeapRoots.push(thisValue.id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return bridgedSteps;
|
|
142
|
+
}
|
|
143
|
+
|
|
46
144
|
export function chunkTraceOutput(
|
|
47
145
|
output: string,
|
|
48
146
|
traceOutputDir: string,
|
|
49
147
|
relDir: string,
|
|
50
148
|
traceName: string,
|
|
51
|
-
|
|
52
|
-
source: string,
|
|
149
|
+
primaryFile: string,
|
|
150
|
+
sources: { file: string; source: string }[],
|
|
53
151
|
options: ChunkTraceOutputOptions = {},
|
|
54
152
|
): ChunkTraceOutputResult {
|
|
55
153
|
const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
@@ -60,18 +158,29 @@ export function chunkTraceOutput(
|
|
|
60
158
|
.split('\n')
|
|
61
159
|
.filter(l => l.length > 0);
|
|
62
160
|
|
|
63
|
-
const allSteps: TraceStep[] =
|
|
161
|
+
const allSteps: TraceStep[] = bridgeConstructorReturnValues(
|
|
162
|
+
lines.map(l => JSON.parse(l)),
|
|
163
|
+
);
|
|
64
164
|
|
|
65
|
-
const
|
|
165
|
+
const lineToStepsByFile = new Map<string, Record<number, number[]>>();
|
|
166
|
+
for (const source of sources) {
|
|
167
|
+
lineToStepsByFile.set(source.file, {});
|
|
168
|
+
}
|
|
66
169
|
for (let i = 0; i < allSteps.length; i++) {
|
|
67
170
|
const step = allSteps[i];
|
|
171
|
+
const file = path.basename(step.file);
|
|
172
|
+
const lineToSteps = lineToStepsByFile.get(file);
|
|
173
|
+
if (!lineToSteps) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
68
176
|
if (!lineToSteps[step.line]) {
|
|
69
177
|
lineToSteps[step.line] = [];
|
|
70
178
|
}
|
|
71
179
|
lineToSteps[step.line].push(i);
|
|
72
180
|
}
|
|
73
181
|
|
|
74
|
-
const
|
|
182
|
+
const layoutSteps = allSteps.map(step => ({ ...step, ...filterStep(step) }));
|
|
183
|
+
const layout = computeLayout(layoutSteps, ignoreFields);
|
|
75
184
|
|
|
76
185
|
let chunkEntries: TraceChunkEntry[] = [];
|
|
77
186
|
let chunkIndex = 0;
|
|
@@ -79,7 +188,12 @@ export function chunkTraceOutput(
|
|
|
79
188
|
|
|
80
189
|
for (const step of allSteps) {
|
|
81
190
|
const svg = generateStepSvg(step, layout);
|
|
82
|
-
chunkEntries.push({
|
|
191
|
+
chunkEntries.push({
|
|
192
|
+
file: path.basename(step.file),
|
|
193
|
+
line: step.line,
|
|
194
|
+
output: stepOutputEvents(step),
|
|
195
|
+
svg,
|
|
196
|
+
});
|
|
83
197
|
|
|
84
198
|
if (chunkEntries.length >= chunkSize) {
|
|
85
199
|
chunkFiles.push({
|
|
@@ -101,9 +215,11 @@ export function chunkTraceOutput(
|
|
|
101
215
|
const manifest: TraceManifest = {
|
|
102
216
|
totalSteps: allSteps.length,
|
|
103
217
|
chunkSize,
|
|
104
|
-
|
|
105
|
-
source
|
|
106
|
-
|
|
218
|
+
primaryFile,
|
|
219
|
+
sources: sources.map(source => ({
|
|
220
|
+
...source,
|
|
221
|
+
lineToSteps: lineToStepsByFile.get(source.file) ?? {},
|
|
222
|
+
})),
|
|
107
223
|
};
|
|
108
224
|
|
|
109
225
|
const manifestFile = {
|
|
@@ -218,11 +334,11 @@ export function buildManifestUrl({
|
|
|
218
334
|
}
|
|
219
335
|
|
|
220
336
|
export function renderTraceWidgetHtml({
|
|
221
|
-
|
|
337
|
+
highlightedSources,
|
|
222
338
|
manifestUrl,
|
|
223
339
|
totalSteps,
|
|
224
340
|
}: {
|
|
225
|
-
highlightedSource: string;
|
|
341
|
+
highlightedSources: { file: string; highlightedSource: string }[];
|
|
226
342
|
manifestUrl?: string;
|
|
227
343
|
totalSteps?: number;
|
|
228
344
|
}): string {
|
|
@@ -248,5 +364,11 @@ export function renderTraceWidgetHtml({
|
|
|
248
364
|
const wrapperClass = disabled
|
|
249
365
|
? 'trace-widget trace-disabled'
|
|
250
366
|
: 'trace-widget';
|
|
251
|
-
|
|
367
|
+
const sourcePanels = highlightedSources
|
|
368
|
+
.map((source, index) => {
|
|
369
|
+
const hidden = index === 0 ? '' : ' hidden';
|
|
370
|
+
return `<div class="trace-source" data-trace-source-file="${escapeAttr(source.file)}"${hidden}>${source.highlightedSource}</div>`;
|
|
371
|
+
})
|
|
372
|
+
.join('');
|
|
373
|
+
return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-source-wrapper">${sourcePanels}</div></div></div></div>`;
|
|
252
374
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TraceHeapObject } from '../types';
|
|
2
|
+
|
|
3
|
+
export type TraceHeapScalar = string | number | boolean;
|
|
4
|
+
|
|
5
|
+
const BOXED_PRIMITIVE_TYPES = new Set([
|
|
6
|
+
'java.lang.Boolean',
|
|
7
|
+
'java.lang.Byte',
|
|
8
|
+
'java.lang.Character',
|
|
9
|
+
'java.lang.Double',
|
|
10
|
+
'java.lang.Float',
|
|
11
|
+
'java.lang.Integer',
|
|
12
|
+
'java.lang.Long',
|
|
13
|
+
'java.lang.Short',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function isInlineHeapObject(obj: TraceHeapObject): boolean {
|
|
17
|
+
return (
|
|
18
|
+
'value' in obj &&
|
|
19
|
+
(obj.type === 'String' || BOXED_PRIMITIVE_TYPES.has(obj.type))
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatHeapObjectValue(
|
|
24
|
+
type: string,
|
|
25
|
+
value: TraceHeapScalar,
|
|
26
|
+
): string {
|
|
27
|
+
if (type === 'String') {
|
|
28
|
+
return `"${value}"`;
|
|
29
|
+
}
|
|
30
|
+
if (type === 'java.lang.Character') {
|
|
31
|
+
return `'${value}'`;
|
|
32
|
+
}
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
@@ -2,11 +2,17 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
|
+
import { parse as parseJava } from 'java-parser';
|
|
5
6
|
import { makeLogger } from '../log';
|
|
6
7
|
import { hasMainMethod, runJavac } from './literate-java';
|
|
7
8
|
|
|
8
9
|
const log = makeLogger(import.meta.url);
|
|
9
10
|
|
|
11
|
+
interface CstNode {
|
|
12
|
+
name?: string;
|
|
13
|
+
children?: Record<string, CstNode[]>;
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* Parse // @trace-ignore comments from Java source code.
|
|
12
18
|
* Returns a map of className -> fieldName[] for fields that should be
|
|
@@ -65,6 +71,42 @@ export function parseIgnoreFields(source: string): Record<string, string[]> {
|
|
|
65
71
|
return result;
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
const JAVA_TOP_LEVEL_TYPE_NODES = new Set([
|
|
75
|
+
'classDeclaration',
|
|
76
|
+
'interfaceDeclaration',
|
|
77
|
+
'enumDeclaration',
|
|
78
|
+
'recordDeclaration',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
export function hasExplicitTopLevelTypeDeclaration(source: string): boolean {
|
|
82
|
+
let cst: CstNode;
|
|
83
|
+
try {
|
|
84
|
+
cst = parseJava(source) as CstNode;
|
|
85
|
+
} catch (err: unknown) {
|
|
86
|
+
log.debug`Failed to parse Java source for unnamed class detection: ${(err as Error).message}`;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ordinaryCompilationUnit = cst.children?.ordinaryCompilationUnit?.[0];
|
|
91
|
+
const typeDeclarations =
|
|
92
|
+
ordinaryCompilationUnit?.children?.typeDeclaration ?? [];
|
|
93
|
+
|
|
94
|
+
return typeDeclarations.some(typeDeclaration =>
|
|
95
|
+
Object.keys(typeDeclaration.children ?? {}).some(childName =>
|
|
96
|
+
JAVA_TOP_LEVEL_TYPE_NODES.has(childName),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getUnnamedClassNames(javaFilePaths: string[]): string[] {
|
|
102
|
+
return javaFilePaths
|
|
103
|
+
.filter(javaFilePath => {
|
|
104
|
+
const source = fs.readFileSync(javaFilePath, 'utf-8');
|
|
105
|
+
return !hasExplicitTopLevelTypeDeclaration(source);
|
|
106
|
+
})
|
|
107
|
+
.map(javaFilePath => path.parse(javaFilePath).name);
|
|
108
|
+
}
|
|
109
|
+
|
|
68
110
|
let tracerClassDir: string | null = null;
|
|
69
111
|
|
|
70
112
|
function ensureTracerCompiled(): string {
|
|
@@ -87,7 +129,7 @@ function ensureTracerCompiled(): string {
|
|
|
87
129
|
return tempDir;
|
|
88
130
|
}
|
|
89
131
|
|
|
90
|
-
function
|
|
132
|
+
function compileTargetFiles(javaFilePaths: string[]): string {
|
|
91
133
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-trace-target-'));
|
|
92
134
|
|
|
93
135
|
runJavac(
|
|
@@ -96,10 +138,10 @@ function compileTargetFile(javaFilePath: string): string {
|
|
|
96
138
|
'-d',
|
|
97
139
|
tempDir,
|
|
98
140
|
'-sourcepath',
|
|
99
|
-
path.dirname(
|
|
100
|
-
|
|
141
|
+
path.dirname(javaFilePaths[0]),
|
|
142
|
+
...javaFilePaths,
|
|
101
143
|
],
|
|
102
|
-
{ tempDir, label: `Compilation failed for ${
|
|
144
|
+
{ tempDir, label: `Compilation failed for ${javaFilePaths[0]}` },
|
|
103
145
|
);
|
|
104
146
|
|
|
105
147
|
return tempDir;
|
|
@@ -116,14 +158,25 @@ export function validateJavaTraceTarget(
|
|
|
116
158
|
}
|
|
117
159
|
}
|
|
118
160
|
|
|
119
|
-
export function runJavaTrace(
|
|
120
|
-
|
|
161
|
+
export function runJavaTrace(
|
|
162
|
+
javaFilePaths: string[],
|
|
163
|
+
className: string,
|
|
164
|
+
): string {
|
|
165
|
+
const targetClassDir = compileTargetFiles(javaFilePaths);
|
|
121
166
|
|
|
122
167
|
try {
|
|
123
168
|
const tracerDir = ensureTracerCompiled();
|
|
169
|
+
const unnamedClassNames = getUnnamedClassNames(javaFilePaths);
|
|
124
170
|
return execFileSync(
|
|
125
171
|
'java',
|
|
126
|
-
[
|
|
172
|
+
[
|
|
173
|
+
'-cp',
|
|
174
|
+
tracerDir,
|
|
175
|
+
'TraceRunner',
|
|
176
|
+
className,
|
|
177
|
+
targetClassDir,
|
|
178
|
+
...unnamedClassNames,
|
|
179
|
+
],
|
|
127
180
|
{ timeout: 60000, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 },
|
|
128
181
|
);
|
|
129
182
|
} finally {
|
|
@@ -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
|
);
|