@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,17 +5,22 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
import pdb
|
|
7
7
|
import sys
|
|
8
|
+
import traceback
|
|
8
9
|
import types
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class CaptureStream(io.TextIOBase):
|
|
12
|
-
def __init__(self, sink):
|
|
13
|
+
def __init__(self, sink, stream):
|
|
13
14
|
self._sink = sink
|
|
15
|
+
self._stream = stream
|
|
14
16
|
|
|
15
17
|
def write(self, s):
|
|
16
18
|
if not isinstance(s, str):
|
|
17
19
|
s = str(s)
|
|
18
|
-
self._sink.
|
|
20
|
+
if self._sink and self._sink[-1]["stream"] == self._stream:
|
|
21
|
+
self._sink[-1]["text"] += s
|
|
22
|
+
else:
|
|
23
|
+
self._sink.append({"stream": self._stream, "text": s})
|
|
19
24
|
return len(s)
|
|
20
25
|
|
|
21
26
|
def flush(self):
|
|
@@ -28,9 +33,10 @@ class CaptureStream(io.TextIOBase):
|
|
|
28
33
|
class TracePdb(pdb.Pdb):
|
|
29
34
|
SYNTHETIC_FRAME_NAMES = {"<listcomp>", "<dictcomp>", "<setcomp>"}
|
|
30
35
|
|
|
31
|
-
def __init__(self, target_path, trace_writer, output_chunks):
|
|
36
|
+
def __init__(self, target_path, traced_paths, trace_writer, output_chunks):
|
|
32
37
|
super().__init__(nosigint=True, readrc=False)
|
|
33
38
|
self.target_path = os.path.abspath(target_path)
|
|
39
|
+
self.traced_paths = set(os.path.abspath(path) for path in traced_paths)
|
|
34
40
|
self.trace_writer = trace_writer
|
|
35
41
|
self.output_chunks = output_chunks
|
|
36
42
|
self.pending_snapshot = None
|
|
@@ -69,18 +75,24 @@ class TracePdb(pdb.Pdb):
|
|
|
69
75
|
if self.pending_snapshot is None:
|
|
70
76
|
return
|
|
71
77
|
payload = dict(self.pending_snapshot)
|
|
72
|
-
payload["
|
|
78
|
+
payload["output"] = self._drain_output()
|
|
73
79
|
self.trace_writer.write(json.dumps(payload) + "\n")
|
|
74
80
|
self.trace_writer.flush()
|
|
75
81
|
self.pending_snapshot = None
|
|
76
82
|
|
|
77
83
|
def _drain_output(self):
|
|
78
84
|
if not self.output_chunks:
|
|
79
|
-
return
|
|
80
|
-
output =
|
|
85
|
+
return []
|
|
86
|
+
output = list(self.output_chunks)
|
|
81
87
|
self.output_chunks.clear()
|
|
82
88
|
return output
|
|
83
89
|
|
|
90
|
+
def emit_crash(self, frame):
|
|
91
|
+
payload = self._capture_snapshot(frame)
|
|
92
|
+
payload["output"] = self._drain_output()
|
|
93
|
+
self.trace_writer.write(json.dumps(payload) + "\n")
|
|
94
|
+
self.trace_writer.flush()
|
|
95
|
+
|
|
84
96
|
def _capture_snapshot(self, frame):
|
|
85
97
|
reachable = {}
|
|
86
98
|
return {
|
|
@@ -206,7 +218,7 @@ class TracePdb(pdb.Pdb):
|
|
|
206
218
|
|
|
207
219
|
def _is_traced_frame(self, frame):
|
|
208
220
|
return (
|
|
209
|
-
os.path.abspath(frame.f_code.co_filename)
|
|
221
|
+
os.path.abspath(frame.f_code.co_filename) in self.traced_paths
|
|
210
222
|
and frame.f_code.co_name not in self.SYNTHETIC_FRAME_NAMES
|
|
211
223
|
)
|
|
212
224
|
|
|
@@ -221,11 +233,14 @@ class TracePdb(pdb.Pdb):
|
|
|
221
233
|
|
|
222
234
|
|
|
223
235
|
def main():
|
|
224
|
-
if len(sys.argv)
|
|
225
|
-
print("Usage: trace_runner.py <python_file>", file=sys.stderr)
|
|
236
|
+
if len(sys.argv) < 2:
|
|
237
|
+
print("Usage: trace_runner.py <python_file> [traced_file ...]", file=sys.stderr)
|
|
226
238
|
sys.exit(1)
|
|
227
239
|
|
|
228
240
|
target_path = os.path.abspath(sys.argv[1])
|
|
241
|
+
traced_paths = [os.path.abspath(path) for path in sys.argv[2:]]
|
|
242
|
+
if target_path not in traced_paths:
|
|
243
|
+
traced_paths.insert(0, target_path)
|
|
229
244
|
target_dir = os.path.dirname(target_path)
|
|
230
245
|
target_name = os.path.basename(target_path)
|
|
231
246
|
|
|
@@ -234,8 +249,9 @@ def main():
|
|
|
234
249
|
|
|
235
250
|
trace_writer = sys.stdout
|
|
236
251
|
output_chunks = []
|
|
237
|
-
|
|
238
|
-
|
|
252
|
+
stdout_capture = CaptureStream(output_chunks, "stdout")
|
|
253
|
+
stderr_capture = CaptureStream(output_chunks, "stderr")
|
|
254
|
+
debugger = TracePdb(target_path, traced_paths, trace_writer, output_chunks)
|
|
239
255
|
|
|
240
256
|
code_globals = {
|
|
241
257
|
"__name__": "__main__",
|
|
@@ -253,14 +269,21 @@ def main():
|
|
|
253
269
|
source = f.read()
|
|
254
270
|
compiled = compile(source, target_path, "exec")
|
|
255
271
|
|
|
256
|
-
sys.stdout =
|
|
257
|
-
sys.stderr =
|
|
272
|
+
sys.stdout = stdout_capture
|
|
273
|
+
sys.stderr = stderr_capture
|
|
258
274
|
|
|
259
275
|
try:
|
|
260
276
|
debugger.set_step()
|
|
261
277
|
debugger.runcall(exec, compiled, code_globals, code_globals)
|
|
262
278
|
except SystemExit:
|
|
263
279
|
pass
|
|
280
|
+
except BaseException as exc:
|
|
281
|
+
debugger.flush_pending()
|
|
282
|
+
sys.stderr.write(format_target_traceback(exc, set(traced_paths)))
|
|
283
|
+
frame = find_traced_exception_frame(debugger, exc.__traceback__)
|
|
284
|
+
if frame is None:
|
|
285
|
+
raise
|
|
286
|
+
debugger.emit_crash(frame)
|
|
264
287
|
finally:
|
|
265
288
|
debugger.flush_pending()
|
|
266
289
|
finally:
|
|
@@ -268,5 +291,27 @@ def main():
|
|
|
268
291
|
sys.stderr = original_stderr
|
|
269
292
|
|
|
270
293
|
|
|
294
|
+
def find_traced_exception_frame(debugger, tb):
|
|
295
|
+
traced = None
|
|
296
|
+
while tb is not None:
|
|
297
|
+
if debugger._is_traced_frame(tb.tb_frame):
|
|
298
|
+
traced = tb.tb_frame
|
|
299
|
+
tb = tb.tb_next
|
|
300
|
+
return traced
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def format_target_traceback(exc, traced_paths):
|
|
304
|
+
frames = [
|
|
305
|
+
frame
|
|
306
|
+
for frame in traceback.extract_tb(exc.__traceback__)
|
|
307
|
+
if os.path.abspath(frame.filename) in traced_paths
|
|
308
|
+
]
|
|
309
|
+
return (
|
|
310
|
+
"Traceback (most recent call last):\n"
|
|
311
|
+
+ "".join(traceback.format_list(frames))
|
|
312
|
+
+ "".join(traceback.format_exception_only(type(exc), exc))
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
271
316
|
if __name__ == "__main__":
|
|
272
317
|
main()
|
package/build/utils/render.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from './code';
|
|
26
26
|
import { extensionIsMarkdown } from './file-types';
|
|
27
27
|
import { createTraceHelpers } from './trace';
|
|
28
|
-
import {
|
|
28
|
+
import { stripHtmlComments } from './html-comments';
|
|
29
29
|
import { finalizeHtmlPage } from './final-html';
|
|
30
30
|
import {
|
|
31
31
|
createApplyBasePath,
|
|
@@ -60,6 +60,8 @@ const log = makeLogger(import.meta.url);
|
|
|
60
60
|
|
|
61
61
|
const tadaVersion: string = pkg.version;
|
|
62
62
|
|
|
63
|
+
export { stripHtmlComments } from './html-comments';
|
|
64
|
+
|
|
63
65
|
const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
|
|
64
66
|
|
|
65
67
|
function isWatchMode(assetFiles: string[]): boolean {
|
|
@@ -433,9 +435,9 @@ function renderPlainTextContent(
|
|
|
433
435
|
{
|
|
434
436
|
manifestUrl: string;
|
|
435
437
|
artifactId: string;
|
|
436
|
-
highlightedSource: string;
|
|
438
|
+
highlightedSources: { file: string; highlightedSource: string }[];
|
|
437
439
|
totalSteps: number;
|
|
438
|
-
|
|
440
|
+
sourceMtims: Record<string, number>;
|
|
439
441
|
}
|
|
440
442
|
>;
|
|
441
443
|
contentDir?: string;
|
|
@@ -459,7 +461,7 @@ function renderPlainTextContent(
|
|
|
459
461
|
`${filePath}: slides mode is only supported on Markdown pages`,
|
|
460
462
|
);
|
|
461
463
|
}
|
|
462
|
-
const
|
|
464
|
+
const frontMatterMd = createMarkdown(siteVariables, {
|
|
463
465
|
filePath,
|
|
464
466
|
slides: pageVariables.slides === true,
|
|
465
467
|
});
|
|
@@ -480,8 +482,8 @@ function renderPlainTextContent(
|
|
|
480
482
|
);
|
|
481
483
|
|
|
482
484
|
// Render title and description as inline Markdown
|
|
483
|
-
renderInlineField(
|
|
484
|
-
renderInlineField(
|
|
485
|
+
renderInlineField(frontMatterMd, pageVariablesProcessed, 'title');
|
|
486
|
+
renderInlineField(frontMatterMd, pageVariablesProcessed, 'description');
|
|
485
487
|
|
|
486
488
|
resolveAuthor(pageVariablesProcessed, filePath, dependencyCollector);
|
|
487
489
|
|
|
@@ -526,8 +528,6 @@ function renderPlainTextContent(
|
|
|
526
528
|
params.renderTrace = helpers.renderTrace;
|
|
527
529
|
}
|
|
528
530
|
|
|
529
|
-
params.include = createIncludeFunction(filePath, params, dependencyCollector);
|
|
530
|
-
|
|
531
531
|
let html: string;
|
|
532
532
|
try {
|
|
533
533
|
html = _.template(strippedContent)(params);
|
|
@@ -541,6 +541,12 @@ function renderPlainTextContent(
|
|
|
541
541
|
let tocItems: unknown[] | null = null;
|
|
542
542
|
let alertIds: string[] = [];
|
|
543
543
|
if (extensionIsMarkdown(ext)) {
|
|
544
|
+
const md = createMarkdown(siteVariables, {
|
|
545
|
+
filePath,
|
|
546
|
+
slides: pageVariables.slides === true,
|
|
547
|
+
templateParams: params,
|
|
548
|
+
dependencyCollector,
|
|
549
|
+
});
|
|
544
550
|
const env: Record<string, unknown> = { alertIds: [] as string[] };
|
|
545
551
|
html = md.render(html!, env);
|
|
546
552
|
tocItems = (env.tocItems as unknown[] | undefined) || null;
|
|
@@ -555,10 +561,6 @@ function renderPlainTextContent(
|
|
|
555
561
|
};
|
|
556
562
|
}
|
|
557
563
|
|
|
558
|
-
export function stripHtmlComments(str: string): string {
|
|
559
|
-
return str.replace(/<!---[\s\S]*?-->/g, '');
|
|
560
|
-
}
|
|
561
|
-
|
|
562
564
|
export function renderLiterateJavaPageAsset({
|
|
563
565
|
filePath,
|
|
564
566
|
contentDir,
|
|
@@ -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 {
|