benchforge 0.1.9 → 0.1.11
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 +40 -6
- package/dist/{BenchRunner-CSKN9zPy.d.mts → BenchRunner-BzyUfiyB.d.mts} +32 -8
- package/dist/{BrowserHeapSampler-DCeL42RE.mjs → BrowserHeapSampler-B6asLKWQ.mjs} +57 -57
- package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +1 -0
- package/dist/{GcStats-ByEovUi1.mjs → GcStats-wX7Xyblu.mjs} +15 -15
- package/dist/GcStats-wX7Xyblu.mjs.map +1 -0
- package/dist/HeapSampler-B8dtKHn1.mjs.map +1 -1
- package/dist/{TimingUtils-ClclVQ7E.mjs → TimingUtils-DwOwkc8G.mjs} +225 -225
- package/dist/TimingUtils-DwOwkc8G.mjs.map +1 -0
- package/dist/bin/benchforge.mjs +1 -1
- package/dist/browser/index.js +210 -210
- package/dist/index.d.mts +102 -46
- package/dist/index.mjs +3 -3
- package/dist/runners/WorkerScript.d.mts +1 -1
- package/dist/runners/WorkerScript.mjs +66 -66
- package/dist/runners/WorkerScript.mjs.map +1 -1
- package/dist/{src-Cf_LXwlp.mjs → src-B-DDaCa9.mjs} +1225 -990
- package/dist/src-B-DDaCa9.mjs.map +1 -0
- package/package.json +2 -1
- package/src/BenchMatrix.ts +125 -125
- package/src/BenchmarkReport.ts +50 -45
- package/src/HtmlDataPrep.ts +21 -21
- package/src/PermutationTest.ts +24 -24
- package/src/StandardSections.ts +45 -45
- package/src/StatisticalUtils.ts +60 -61
- package/src/browser/BrowserGcStats.ts +5 -5
- package/src/browser/BrowserHeapSampler.ts +63 -63
- package/src/cli/CliArgs.ts +6 -3
- package/src/cli/FilterBenchmarks.ts +5 -5
- package/src/cli/RunBenchCLI.ts +526 -498
- package/src/export/JsonExport.ts +10 -10
- package/src/export/PerfettoExport.ts +74 -74
- package/src/export/SpeedscopeExport.ts +202 -0
- package/src/heap-sample/HeapSampleReport.ts +143 -70
- package/src/heap-sample/HeapSampler.ts +55 -12
- package/src/heap-sample/ResolvedProfile.ts +89 -0
- package/src/html/HtmlReport.ts +33 -33
- package/src/html/HtmlTemplate.ts +67 -67
- package/src/html/browser/CIPlot.ts +50 -50
- package/src/html/browser/HistogramKde.ts +13 -13
- package/src/html/browser/LegendUtils.ts +48 -48
- package/src/html/browser/RenderPlots.ts +98 -98
- package/src/html/browser/SampleTimeSeries.ts +79 -79
- package/src/index.ts +6 -0
- package/src/matrix/MatrixFilter.ts +6 -6
- package/src/matrix/MatrixReport.ts +96 -96
- package/src/matrix/VariantLoader.ts +5 -5
- package/src/runners/AdaptiveWrapper.ts +151 -151
- package/src/runners/BasicRunner.ts +175 -175
- package/src/runners/BenchRunner.ts +8 -8
- package/src/runners/GcStats.ts +22 -22
- package/src/runners/RunnerOrchestrator.ts +168 -168
- package/src/runners/WorkerScript.ts +96 -96
- package/src/table-util/Formatters.ts +41 -36
- package/src/table-util/TableReport.ts +122 -122
- package/src/table-util/test/TableValueExtractor.ts +9 -9
- package/src/test/AdaptiveStatistics.integration.ts +7 -39
- package/src/test/HeapAttribution.test.ts +51 -0
- package/src/test/RunBenchCLI.test.ts +18 -18
- package/src/test/TestUtils.ts +24 -24
- package/src/tests/BenchMatrix.test.ts +12 -12
- package/src/tests/MatrixFilter.test.ts +15 -15
- package/dist/BrowserHeapSampler-DCeL42RE.mjs.map +0 -1
- package/dist/GcStats-ByEovUi1.mjs.map +0 -1
- package/dist/TimingUtils-ClclVQ7E.mjs.map +0 -1
- package/dist/src-Cf_LXwlp.mjs.map +0 -1
package/src/export/JsonExport.ts
CHANGED
|
@@ -47,6 +47,16 @@ function prepareJsonData(
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Clean CLI args for JSON export (remove undefined values) */
|
|
51
|
+
function cleanCliArgs(args: DefaultCliArgs): Record<string, any> {
|
|
52
|
+
const toCamel = (k: string) =>
|
|
53
|
+
k.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
|
|
54
|
+
const entries = Object.entries(args)
|
|
55
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
56
|
+
.map(([k, v]) => [toCamel(k), v]);
|
|
57
|
+
return Object.fromEntries(entries);
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
/** Convert a report group, mapping each report to the JSON result format */
|
|
51
61
|
function convertGroup(group: ReportGroup): BenchmarkGroup {
|
|
52
62
|
return {
|
|
@@ -91,13 +101,3 @@ function convertReport(report: any): BenchmarkResult {
|
|
|
91
101
|
},
|
|
92
102
|
};
|
|
93
103
|
}
|
|
94
|
-
|
|
95
|
-
/** Clean CLI args for JSON export (remove undefined values) */
|
|
96
|
-
function cleanCliArgs(args: DefaultCliArgs): Record<string, any> {
|
|
97
|
-
const toCamel = (k: string) =>
|
|
98
|
-
k.replace(/-([a-z])/g, (_, l) => l.toUpperCase());
|
|
99
|
-
const entries = Object.entries(args)
|
|
100
|
-
.filter(([, v]) => v !== undefined && v !== null)
|
|
101
|
-
.map(([k, v]) => [toCamel(k), v]);
|
|
102
|
-
return Object.fromEntries(entries);
|
|
103
|
-
}
|
|
@@ -73,54 +73,6 @@ function buildTraceEvents(
|
|
|
73
73
|
return events;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function instant(
|
|
77
|
-
ts: number,
|
|
78
|
-
name: string,
|
|
79
|
-
args: Record<string, unknown>,
|
|
80
|
-
): TraceEvent {
|
|
81
|
-
return { ph: "i", ts, pid, tid, cat: "bench", name, s: "t", args };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function counter(
|
|
85
|
-
ts: number,
|
|
86
|
-
name: string,
|
|
87
|
-
args: Record<string, unknown>,
|
|
88
|
-
): TraceEvent {
|
|
89
|
-
return { ph: "C", ts, pid, tid, cat: "bench", name, args };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Build events for a single benchmark run */
|
|
93
|
-
function buildBenchmarkEvents(results: MeasuredResults): TraceEvent[] {
|
|
94
|
-
const { samples, heapSamples, timestamps, pausePoints } = results;
|
|
95
|
-
if (!timestamps?.length) return [];
|
|
96
|
-
|
|
97
|
-
const events: TraceEvent[] = [];
|
|
98
|
-
for (let i = 0; i < samples.length; i++) {
|
|
99
|
-
const ts = timestamps[i];
|
|
100
|
-
const ms = Math.round(samples[i] * 100) / 100;
|
|
101
|
-
events.push(instant(ts, results.name, { n: i, ms }));
|
|
102
|
-
events.push(counter(ts, "duration", { ms }));
|
|
103
|
-
if (heapSamples?.[i] !== undefined) {
|
|
104
|
-
const MB = Math.round((heapSamples[i] / 1024 / 1024) * 10) / 10;
|
|
105
|
-
events.push(counter(ts, "heap", { MB }));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const pause of pausePoints ?? []) {
|
|
110
|
-
const ts = timestamps[pause.sampleIndex];
|
|
111
|
-
if (ts) events.push(instant(ts, "pause", { ms: pause.durationMs }));
|
|
112
|
-
}
|
|
113
|
-
return events;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Normalize timestamps so events start at 0 */
|
|
117
|
-
function normalizeTimestamps(events: TraceEvent[]): void {
|
|
118
|
-
const times = events.filter(e => e.ts > 0).map(e => e.ts);
|
|
119
|
-
if (times.length === 0) return;
|
|
120
|
-
const minTs = Math.min(...times);
|
|
121
|
-
for (const e of events) if (e.ts > 0) e.ts -= minTs;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
76
|
/** Merge V8 trace events from a previous run, aligning timestamps */
|
|
125
77
|
function mergeV8Trace(customEvents: TraceEvent[]): TraceEvent[] {
|
|
126
78
|
const traceFiles = readdirSync(".").filter(
|
|
@@ -135,38 +87,12 @@ function mergeV8Trace(customEvents: TraceEvent[]): TraceEvent[] {
|
|
|
135
87
|
return [...v8Events, ...customEvents];
|
|
136
88
|
}
|
|
137
89
|
|
|
138
|
-
/** Load V8 trace events from file, or undefined if unavailable */
|
|
139
|
-
function loadV8Events(
|
|
140
|
-
v8TracePath: string | undefined,
|
|
141
|
-
): TraceEvent[] | undefined {
|
|
142
|
-
if (!v8TracePath) return undefined;
|
|
143
|
-
try {
|
|
144
|
-
const v8Data = JSON.parse(readFileSync(v8TracePath, "utf-8")) as TraceFile;
|
|
145
|
-
console.log(
|
|
146
|
-
`Merged ${v8Data.traceEvents.length} V8 events from ${v8TracePath}`,
|
|
147
|
-
);
|
|
148
|
-
return v8Data.traceEvents;
|
|
149
|
-
} catch {
|
|
150
|
-
console.warn(`Could not parse V8 trace file: ${v8TracePath}`);
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
90
|
/** Write trace events to JSON file */
|
|
156
91
|
function writeTraceFile(outputPath: string, events: TraceEvent[]): void {
|
|
157
92
|
const traceFile: TraceFile = { traceEvents: events };
|
|
158
93
|
writeFileSync(outputPath, JSON.stringify(traceFile));
|
|
159
94
|
}
|
|
160
95
|
|
|
161
|
-
/** Clean CLI args for metadata */
|
|
162
|
-
function cleanArgs(args: DefaultCliArgs): Record<string, unknown> {
|
|
163
|
-
const skip = new Set(["_", "$0"]);
|
|
164
|
-
const entries = Object.entries(args).filter(
|
|
165
|
-
([k, v]) => v !== undefined && !skip.has(k),
|
|
166
|
-
);
|
|
167
|
-
return Object.fromEntries(entries);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
96
|
/** Spawn a detached child to merge V8 trace after process exit */
|
|
171
97
|
function scheduleDeferredMerge(outputPath: string): void {
|
|
172
98
|
const cwd = process.cwd();
|
|
@@ -201,3 +127,77 @@ function scheduleDeferredMerge(outputPath: string): void {
|
|
|
201
127
|
child.unref();
|
|
202
128
|
});
|
|
203
129
|
}
|
|
130
|
+
|
|
131
|
+
/** Clean CLI args for metadata */
|
|
132
|
+
function cleanArgs(args: DefaultCliArgs): Record<string, unknown> {
|
|
133
|
+
const skip = new Set(["_", "$0"]);
|
|
134
|
+
const entries = Object.entries(args).filter(
|
|
135
|
+
([k, v]) => v !== undefined && !skip.has(k),
|
|
136
|
+
);
|
|
137
|
+
return Object.fromEntries(entries);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Build events for a single benchmark run */
|
|
141
|
+
function buildBenchmarkEvents(results: MeasuredResults): TraceEvent[] {
|
|
142
|
+
const { samples, heapSamples, timestamps, pausePoints } = results;
|
|
143
|
+
if (!timestamps?.length) return [];
|
|
144
|
+
|
|
145
|
+
const events: TraceEvent[] = [];
|
|
146
|
+
for (let i = 0; i < samples.length; i++) {
|
|
147
|
+
const ts = timestamps[i];
|
|
148
|
+
const ms = Math.round(samples[i] * 100) / 100;
|
|
149
|
+
events.push(instant(ts, results.name, { n: i, ms }));
|
|
150
|
+
events.push(counter(ts, "duration", { ms }));
|
|
151
|
+
if (heapSamples?.[i] !== undefined) {
|
|
152
|
+
const MB = Math.round((heapSamples[i] / 1024 / 1024) * 10) / 10;
|
|
153
|
+
events.push(counter(ts, "heap", { MB }));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const pause of pausePoints ?? []) {
|
|
158
|
+
const ts = timestamps[pause.sampleIndex];
|
|
159
|
+
if (ts) events.push(instant(ts, "pause", { ms: pause.durationMs }));
|
|
160
|
+
}
|
|
161
|
+
return events;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Load V8 trace events from file, or undefined if unavailable */
|
|
165
|
+
function loadV8Events(
|
|
166
|
+
v8TracePath: string | undefined,
|
|
167
|
+
): TraceEvent[] | undefined {
|
|
168
|
+
if (!v8TracePath) return undefined;
|
|
169
|
+
try {
|
|
170
|
+
const v8Data = JSON.parse(readFileSync(v8TracePath, "utf-8")) as TraceFile;
|
|
171
|
+
console.log(
|
|
172
|
+
`Merged ${v8Data.traceEvents.length} V8 events from ${v8TracePath}`,
|
|
173
|
+
);
|
|
174
|
+
return v8Data.traceEvents;
|
|
175
|
+
} catch {
|
|
176
|
+
console.warn(`Could not parse V8 trace file: ${v8TracePath}`);
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Normalize timestamps so events start at 0 */
|
|
182
|
+
function normalizeTimestamps(events: TraceEvent[]): void {
|
|
183
|
+
const times = events.filter(e => e.ts > 0).map(e => e.ts);
|
|
184
|
+
if (times.length === 0) return;
|
|
185
|
+
const minTs = Math.min(...times);
|
|
186
|
+
for (const e of events) if (e.ts > 0) e.ts -= minTs;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function instant(
|
|
190
|
+
ts: number,
|
|
191
|
+
name: string,
|
|
192
|
+
args: Record<string, unknown>,
|
|
193
|
+
): TraceEvent {
|
|
194
|
+
return { ph: "i", ts, pid, tid, cat: "bench", name, s: "t", args };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function counter(
|
|
198
|
+
ts: number,
|
|
199
|
+
name: string,
|
|
200
|
+
args: Record<string, unknown>,
|
|
201
|
+
): TraceEvent {
|
|
202
|
+
return { ph: "C", ts, pid, tid, cat: "bench", name, args };
|
|
203
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { groupReports, type ReportGroup } from "../BenchmarkReport.ts";
|
|
7
|
+
import type { HeapProfile } from "../heap-sample/HeapSampler.ts";
|
|
8
|
+
import {
|
|
9
|
+
type ResolvedFrame,
|
|
10
|
+
type ResolvedProfile,
|
|
11
|
+
resolveProfile,
|
|
12
|
+
} from "../heap-sample/ResolvedProfile.ts";
|
|
13
|
+
|
|
14
|
+
/** speedscope file format (https://www.speedscope.app/file-format-schema.json) */
|
|
15
|
+
interface SpeedscopeFile {
|
|
16
|
+
$schema: "https://www.speedscope.app/file-format-schema.json";
|
|
17
|
+
shared: { frames: SpeedscopeFrame[] };
|
|
18
|
+
profiles: SpeedscopeProfile[];
|
|
19
|
+
name?: string;
|
|
20
|
+
exporter?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SpeedscopeFrame {
|
|
24
|
+
name: string;
|
|
25
|
+
file?: string;
|
|
26
|
+
line?: number;
|
|
27
|
+
col?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SpeedscopeProfile {
|
|
31
|
+
type: "sampled";
|
|
32
|
+
name: string;
|
|
33
|
+
unit: "bytes";
|
|
34
|
+
startValue: number;
|
|
35
|
+
endValue: number;
|
|
36
|
+
samples: number[][]; // each sample is stack of frame indices
|
|
37
|
+
weights: number[]; // bytes per sample
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Export heap profiles from benchmark results to speedscope JSON format.
|
|
41
|
+
* Creates one speedscope profile per benchmark that has a heapProfile.
|
|
42
|
+
* @returns resolved output path, or undefined if no profiles were found */
|
|
43
|
+
export function exportSpeedscope(
|
|
44
|
+
groups: ReportGroup[],
|
|
45
|
+
outputPath: string,
|
|
46
|
+
): string | undefined {
|
|
47
|
+
const frames: SpeedscopeFrame[] = [];
|
|
48
|
+
const frameIndex = new Map<string, number>();
|
|
49
|
+
const profiles: SpeedscopeProfile[] = [];
|
|
50
|
+
|
|
51
|
+
for (const group of groups) {
|
|
52
|
+
for (const report of groupReports(group)) {
|
|
53
|
+
const { heapProfile } = report.measuredResults;
|
|
54
|
+
if (!heapProfile) continue;
|
|
55
|
+
const resolved = resolveProfile(heapProfile);
|
|
56
|
+
profiles.push(buildProfile(report.name, resolved, frames, frameIndex));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (profiles.length === 0) {
|
|
61
|
+
console.log("No heap profiles to export.");
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const file: SpeedscopeFile = {
|
|
66
|
+
$schema: "https://www.speedscope.app/file-format-schema.json",
|
|
67
|
+
shared: { frames },
|
|
68
|
+
profiles,
|
|
69
|
+
exporter: "benchforge",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const absPath = resolve(outputPath);
|
|
73
|
+
writeFileSync(absPath, JSON.stringify(file));
|
|
74
|
+
console.log(`Speedscope profile exported to: ${outputPath}`);
|
|
75
|
+
return absPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Export to a temp file and open in speedscope via npx */
|
|
79
|
+
export function exportAndLaunchSpeedscope(groups: ReportGroup[]): void {
|
|
80
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
81
|
+
const outputPath = join(tmpdir(), `benchforge-${timestamp}.speedscope.json`);
|
|
82
|
+
const absPath = exportSpeedscope(groups, outputPath);
|
|
83
|
+
if (absPath) {
|
|
84
|
+
launchSpeedscope(absPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Launch speedscope viewer on a file via npx */
|
|
89
|
+
export function launchSpeedscope(filePath: string): void {
|
|
90
|
+
console.log("Opening speedscope...");
|
|
91
|
+
const child = spawn("npx", ["speedscope", filePath], {
|
|
92
|
+
detached: true,
|
|
93
|
+
stdio: "ignore",
|
|
94
|
+
});
|
|
95
|
+
child.unref();
|
|
96
|
+
child.on("error", () => {
|
|
97
|
+
console.error(
|
|
98
|
+
`Failed to launch speedscope. Run manually:\n npx speedscope ${filePath}`,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Convert a single HeapProfile to speedscope format (for standalone use) */
|
|
104
|
+
export function heapProfileToSpeedscope(
|
|
105
|
+
name: string,
|
|
106
|
+
profile: HeapProfile,
|
|
107
|
+
): SpeedscopeFile {
|
|
108
|
+
const frames: SpeedscopeFrame[] = [];
|
|
109
|
+
const frameIndex = new Map<string, number>();
|
|
110
|
+
const resolved = resolveProfile(profile);
|
|
111
|
+
const p = buildProfile(name, resolved, frames, frameIndex);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
$schema: "https://www.speedscope.app/file-format-schema.json",
|
|
115
|
+
shared: { frames },
|
|
116
|
+
profiles: [p],
|
|
117
|
+
exporter: "benchforge",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Build a single speedscope profile from a resolved heap profile */
|
|
122
|
+
function buildProfile(
|
|
123
|
+
name: string,
|
|
124
|
+
resolved: ResolvedProfile,
|
|
125
|
+
sharedFrames: SpeedscopeFrame[],
|
|
126
|
+
frameIndex: Map<string, number>,
|
|
127
|
+
): SpeedscopeProfile {
|
|
128
|
+
// Build nodeId -> stack of frame indices
|
|
129
|
+
const nodeStacks = new Map<number, number[]>();
|
|
130
|
+
for (const node of resolved.nodes) {
|
|
131
|
+
const stack = node.stack.map(f => internFrame(f, sharedFrames, frameIndex));
|
|
132
|
+
nodeStacks.set(node.nodeId, stack);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const samples: number[][] = [];
|
|
136
|
+
const weights: number[] = [];
|
|
137
|
+
|
|
138
|
+
if (!resolved.sortedSamples || resolved.sortedSamples.length === 0) {
|
|
139
|
+
console.error(
|
|
140
|
+
`Speedscope export: no samples in heap profile for "${name}", skipping`,
|
|
141
|
+
);
|
|
142
|
+
return {
|
|
143
|
+
type: "sampled",
|
|
144
|
+
name,
|
|
145
|
+
unit: "bytes",
|
|
146
|
+
startValue: 0,
|
|
147
|
+
endValue: 0,
|
|
148
|
+
samples,
|
|
149
|
+
weights,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const sample of resolved.sortedSamples) {
|
|
154
|
+
const stack = nodeStacks.get(sample.nodeId);
|
|
155
|
+
if (stack) {
|
|
156
|
+
samples.push(stack);
|
|
157
|
+
weights.push(sample.size);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const totalBytes = weights.reduce((sum, w) => sum + w, 0);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
type: "sampled",
|
|
165
|
+
name,
|
|
166
|
+
unit: "bytes",
|
|
167
|
+
startValue: 0,
|
|
168
|
+
endValue: totalBytes,
|
|
169
|
+
samples,
|
|
170
|
+
weights,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Intern a call frame, returning its index in the shared frames array */
|
|
175
|
+
function internFrame(
|
|
176
|
+
frame: ResolvedFrame,
|
|
177
|
+
sharedFrames: SpeedscopeFrame[],
|
|
178
|
+
frameIndex: Map<string, number>,
|
|
179
|
+
): number {
|
|
180
|
+
const { name, url, line, col } = frame;
|
|
181
|
+
const key = `${name}\0${url}\0${line}\0${col}`;
|
|
182
|
+
|
|
183
|
+
let idx = frameIndex.get(key);
|
|
184
|
+
if (idx === undefined) {
|
|
185
|
+
idx = sharedFrames.length;
|
|
186
|
+
// Match speedscope's convention: anonymous functions include location in name
|
|
187
|
+
const shortFile = url ? url.split("/").pop() : undefined;
|
|
188
|
+
const displayName =
|
|
189
|
+
name !== "(anonymous)"
|
|
190
|
+
? name
|
|
191
|
+
: shortFile
|
|
192
|
+
? `(anonymous ${shortFile}:${line})`
|
|
193
|
+
: "(anonymous)";
|
|
194
|
+
const entry: SpeedscopeFrame = { name: displayName };
|
|
195
|
+
if (url) entry.file = url;
|
|
196
|
+
if (line > 0) entry.line = line;
|
|
197
|
+
if (col != null) entry.col = col;
|
|
198
|
+
sharedFrames.push(entry);
|
|
199
|
+
frameIndex.set(key, idx);
|
|
200
|
+
}
|
|
201
|
+
return idx;
|
|
202
|
+
}
|