mirror of https://github.com/sveltejs/svelte
chore: compiler profiling + parser bench (#18767)
Used these to speed up parsing/find compilation bottlenecks. Useful for later analysis therefore committing it.pull/18732/merge
parent
4bf15ae6f3
commit
6f7857af59
@ -0,0 +1,346 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.resolve(fileURLToPath(import.meta.url), '../..');
|
||||||
|
const compiler_path = 'packages/svelte/src/compiler/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge V8 profiles from a Vitest run into a compiler-only flame graph and hotspot summary.
|
||||||
|
* Samples retain callees outside the compiler once execution has entered compiler code.
|
||||||
|
* @param {string} run_dir
|
||||||
|
*/
|
||||||
|
export function analyze_compiler_profiles(run_dir) {
|
||||||
|
const raw_dir = path.join(run_dir, 'raw');
|
||||||
|
const profile_files = fs
|
||||||
|
.readdirSync(raw_dir)
|
||||||
|
.filter((file) => file.endsWith('.cpuprofile'))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
if (profile_files.length === 0) {
|
||||||
|
throw new Error(`No CPU profiles found in ${raw_dir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Array<{ name: string, file?: string, line?: number, col?: number }>} */
|
||||||
|
const frames = [];
|
||||||
|
/** @type {Map<string, number>} */
|
||||||
|
const frame_indices = new Map();
|
||||||
|
/** @type {Map<string, { stack: number[], weight: number }>} */
|
||||||
|
const stacks = new Map();
|
||||||
|
/** @type {Map<string, Hotspot>} */
|
||||||
|
const hotspots = new Map();
|
||||||
|
/** @type {Map<string, Hotspot>} */
|
||||||
|
const files = new Map();
|
||||||
|
let total_time = 0;
|
||||||
|
let compiler_time = 0;
|
||||||
|
let garbage_collection_time = 0;
|
||||||
|
let total_samples = 0;
|
||||||
|
let compiler_samples = 0;
|
||||||
|
|
||||||
|
for (const profile_file of profile_files) {
|
||||||
|
let profile;
|
||||||
|
try {
|
||||||
|
profile = JSON.parse(fs.readFileSync(path.join(raw_dir, profile_file), 'utf8'));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Could not parse CPU profile ${profile_file}`, { cause: error });
|
||||||
|
}
|
||||||
|
const nodes = Array.isArray(profile.nodes) ? profile.nodes : [];
|
||||||
|
const samples = Array.isArray(profile.samples) ? profile.samples : [];
|
||||||
|
const time_deltas = Array.isArray(profile.timeDeltas) ? profile.timeDeltas : [];
|
||||||
|
const nodes_by_id = new Map(nodes.map((node) => [node.id, node]));
|
||||||
|
const parents = new Map();
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
for (const child of node.children || []) {
|
||||||
|
parents.set(child, node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < samples.length; i += 1) {
|
||||||
|
const weight = typeof time_deltas[i] === 'number' ? time_deltas[i] : 1;
|
||||||
|
const stack = get_stack(samples[i], nodes_by_id, parents);
|
||||||
|
total_time += weight;
|
||||||
|
total_samples += 1;
|
||||||
|
|
||||||
|
if (stack.at(-1)?.function_name === '(garbage collector)') {
|
||||||
|
garbage_collection_time += weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compiler_index = stack.findIndex((frame) => frame.url.includes(compiler_path));
|
||||||
|
if (compiler_index === -1) continue;
|
||||||
|
|
||||||
|
const compiler_stack = stack.slice(compiler_index);
|
||||||
|
const stack_indices = compiler_stack.map(get_frame_index);
|
||||||
|
const stack_key = stack_indices.join(',');
|
||||||
|
const existing_stack = stacks.get(stack_key);
|
||||||
|
|
||||||
|
if (existing_stack) {
|
||||||
|
existing_stack.weight += weight;
|
||||||
|
} else {
|
||||||
|
stacks.set(stack_key, { stack: stack_indices, weight });
|
||||||
|
}
|
||||||
|
|
||||||
|
compiler_time += weight;
|
||||||
|
compiler_samples += 1;
|
||||||
|
|
||||||
|
const leaf = compiler_stack.at(-1);
|
||||||
|
if (leaf) {
|
||||||
|
get_hotspot(hotspots, frame_key(leaf), leaf).self += weight;
|
||||||
|
get_hotspot(files, leaf.url || '(native)', {
|
||||||
|
...leaf,
|
||||||
|
function_name: leaf.url || '(native)'
|
||||||
|
}).self += weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen_frames = new Set();
|
||||||
|
const seen_files = new Set();
|
||||||
|
|
||||||
|
for (const frame of compiler_stack) {
|
||||||
|
const key = frame_key(frame);
|
||||||
|
if (!seen_frames.has(key)) {
|
||||||
|
get_hotspot(hotspots, key, frame).inclusive += weight;
|
||||||
|
seen_frames.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = frame.url || '(native)';
|
||||||
|
if (!seen_files.has(file)) {
|
||||||
|
get_hotspot(files, file, { ...frame, function_name: file }).inclusive += weight;
|
||||||
|
seen_files.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compiler_time === 0) {
|
||||||
|
throw new Error('The CPU profiles contain no samples from packages/svelte/src/compiler');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hotspot_rows = rank(hotspots, compiler_time);
|
||||||
|
const file_rows = rank(files, compiler_time);
|
||||||
|
const compact_stacks = [...stacks.values()];
|
||||||
|
const summary = {
|
||||||
|
profile_files: profile_files.length,
|
||||||
|
total_samples,
|
||||||
|
compiler_samples,
|
||||||
|
total_time_microseconds: total_time,
|
||||||
|
compiler_time_microseconds: compiler_time,
|
||||||
|
garbage_collection_time_microseconds: garbage_collection_time,
|
||||||
|
compiler_share_percent: (compiler_time * 100) / total_time,
|
||||||
|
hotspots: hotspot_rows,
|
||||||
|
files: file_rows
|
||||||
|
};
|
||||||
|
const speedscope = {
|
||||||
|
$schema: 'https://www.speedscope.app/file-format-schema.json',
|
||||||
|
name: `Svelte compiler: ${path.basename(run_dir)}`,
|
||||||
|
exporter: 'Svelte compiler test profiler',
|
||||||
|
activeProfileIndex: 0,
|
||||||
|
shared: { frames },
|
||||||
|
profiles: [
|
||||||
|
{
|
||||||
|
type: 'sampled',
|
||||||
|
name: 'Compiler-active samples from pnpm test',
|
||||||
|
unit: 'microseconds',
|
||||||
|
startValue: 0,
|
||||||
|
endValue: compiler_time,
|
||||||
|
samples: compact_stacks.map((entry) => entry.stack),
|
||||||
|
weights: compact_stacks.map((entry) => entry.weight)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(run_dir, 'flamegraph.speedscope.json'),
|
||||||
|
`${JSON.stringify(speedscope)}\n`
|
||||||
|
);
|
||||||
|
fs.writeFileSync(path.join(run_dir, 'summary.json'), `${JSON.stringify(summary, null, '\t')}\n`);
|
||||||
|
fs.writeFileSync(path.join(run_dir, 'summary.md'), render_markdown(summary));
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
|
||||||
|
/** @param {Frame} frame */
|
||||||
|
function get_frame_index(frame) {
|
||||||
|
const key = frame_key(frame);
|
||||||
|
const existing = frame_indices.get(key);
|
||||||
|
if (existing !== undefined) return existing;
|
||||||
|
|
||||||
|
const index = frames.length;
|
||||||
|
const location = frame.url ? ` (${frame.url}:${frame.line})` : '';
|
||||||
|
frames.push({
|
||||||
|
name: `${frame.function_name}${location}`,
|
||||||
|
...(frame.url ? { file: frame.url, line: frame.line, col: frame.column } : {})
|
||||||
|
});
|
||||||
|
frame_indices.set(key, index);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} leaf_id
|
||||||
|
* @param {Map<number, any>} nodes_by_id
|
||||||
|
* @param {Map<number, number>} parents
|
||||||
|
* @returns {Frame[]}
|
||||||
|
*/
|
||||||
|
function get_stack(leaf_id, nodes_by_id, parents) {
|
||||||
|
/** @type {Frame[]} */
|
||||||
|
const stack = [];
|
||||||
|
const seen = new Set();
|
||||||
|
let id = leaf_id;
|
||||||
|
|
||||||
|
while (typeof id === 'number' && !seen.has(id)) {
|
||||||
|
seen.add(id);
|
||||||
|
const node = nodes_by_id.get(id);
|
||||||
|
if (!node) break;
|
||||||
|
|
||||||
|
const call_frame = node.callFrame || {};
|
||||||
|
stack.push({
|
||||||
|
function_name: call_frame.functionName || '(anonymous)',
|
||||||
|
url: normalize_url(call_frame.url || ''),
|
||||||
|
line: typeof call_frame.lineNumber === 'number' ? call_frame.lineNumber + 1 : 0,
|
||||||
|
column: typeof call_frame.columnNumber === 'number' ? call_frame.columnNumber + 1 : 0
|
||||||
|
});
|
||||||
|
id = parents.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} url */
|
||||||
|
function normalize_url(url) {
|
||||||
|
if (!url) return '';
|
||||||
|
|
||||||
|
let pathname = url.replace(/^\/\@fs\//, '/').replace(/[?#].*$/, '');
|
||||||
|
if (pathname.startsWith('file://')) {
|
||||||
|
try {
|
||||||
|
pathname = fileURLToPath(pathname);
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.isAbsolute(pathname)) {
|
||||||
|
const relative = path.relative(root, pathname);
|
||||||
|
if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
|
||||||
|
pathname = relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathname.replaceAll(path.sep, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Frame} frame */
|
||||||
|
function frame_key(frame) {
|
||||||
|
return `${frame.function_name}\0${frame.url}\0${frame.line}\0${frame.column}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Map<string, Hotspot>} collection
|
||||||
|
* @param {string} key
|
||||||
|
* @param {Frame} frame
|
||||||
|
*/
|
||||||
|
function get_hotspot(collection, key, frame) {
|
||||||
|
let hotspot = collection.get(key);
|
||||||
|
if (!hotspot) {
|
||||||
|
hotspot = { ...frame, self: 0, inclusive: 0 };
|
||||||
|
collection.set(key, hotspot);
|
||||||
|
}
|
||||||
|
return hotspot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Map<string, Hotspot>} collection
|
||||||
|
* @param {number} total
|
||||||
|
*/
|
||||||
|
function rank(collection, total) {
|
||||||
|
return [...collection.values()]
|
||||||
|
.map((hotspot) => ({
|
||||||
|
function: hotspot.function_name,
|
||||||
|
url: hotspot.url,
|
||||||
|
line: hotspot.line,
|
||||||
|
column: hotspot.column,
|
||||||
|
self_microseconds: hotspot.self,
|
||||||
|
self_percent: (hotspot.self * 100) / total,
|
||||||
|
inclusive_microseconds: hotspot.inclusive,
|
||||||
|
inclusive_percent: (hotspot.inclusive * 100) / total
|
||||||
|
}))
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
b.self_microseconds - a.self_microseconds ||
|
||||||
|
b.inclusive_microseconds - a.inclusive_microseconds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {ReturnType<typeof analyze_compiler_profiles>} summary */
|
||||||
|
function render_markdown(summary) {
|
||||||
|
const lines = [
|
||||||
|
'# Svelte compiler CPU profile',
|
||||||
|
'',
|
||||||
|
'Open `flamegraph.speedscope.json` in [Speedscope](https://www.speedscope.app/) for the interactive flame graph.',
|
||||||
|
'',
|
||||||
|
'## Coverage',
|
||||||
|
'',
|
||||||
|
'| Metric | Value |',
|
||||||
|
'| --- | ---: |',
|
||||||
|
`| Raw profiles | ${summary.profile_files} |`,
|
||||||
|
`| All profiled CPU time | ${format_time(summary.total_time_microseconds)} |`,
|
||||||
|
`| Compiler-active CPU time | ${format_time(summary.compiler_time_microseconds)} |`,
|
||||||
|
`| Compiler share | ${summary.compiler_share_percent.toFixed(2)}% |`,
|
||||||
|
`| All-process garbage collection | ${format_time(summary.garbage_collection_time_microseconds)} |`,
|
||||||
|
'',
|
||||||
|
'Compiler-active time includes external callees while a compiler frame is on the stack. Garbage collection is reported for context but cannot be attributed to compiler stacks by V8.',
|
||||||
|
'',
|
||||||
|
'## Top Self Hotspots',
|
||||||
|
'',
|
||||||
|
'| Rank | Function | Location | Self | Self % | Inclusive | Inclusive % |',
|
||||||
|
'| ---: | --- | --- | ---: | ---: | ---: | ---: |'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [index, row] of summary.hotspots.slice(0, 50).entries()) {
|
||||||
|
lines.push(render_row(index, row));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
'',
|
||||||
|
'## Top Files',
|
||||||
|
'',
|
||||||
|
'| Rank | File | Self | Self % | Inclusive | Inclusive % |',
|
||||||
|
'| ---: | --- | ---: | ---: | ---: | ---: |'
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [index, row] of summary.files.slice(0, 50).entries()) {
|
||||||
|
lines.push(
|
||||||
|
`| ${index + 1} | ${escape_cell(row.function)} | ${format_time(row.self_microseconds)} | ${row.self_percent.toFixed(2)}% | ${format_time(row.inclusive_microseconds)} | ${row.inclusive_percent.toFixed(2)}% |`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${lines.join('\n')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} index @param {ReturnType<typeof rank>[number]} row */
|
||||||
|
function render_row(index, row) {
|
||||||
|
const location = row.url ? `${row.url}:${row.line}:${row.column}` : '(native)';
|
||||||
|
return `| ${index + 1} | ${escape_cell(row.function)} | ${escape_cell(location)} | ${format_time(row.self_microseconds)} | ${row.self_percent.toFixed(2)}% | ${format_time(row.inclusive_microseconds)} | ${row.inclusive_percent.toFixed(2)}% |`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} microseconds */
|
||||||
|
function format_time(microseconds) {
|
||||||
|
return `${(microseconds / 1000).toFixed(1)} ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} value */
|
||||||
|
function escape_cell(value) {
|
||||||
|
return value.replaceAll('|', '\\|');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
const run_dir = process.argv[2];
|
||||||
|
if (!run_dir) {
|
||||||
|
console.error('Usage: node benchmarking/analyze-compiler-profile.js <profile-run-directory>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
analyze_compiler_profiles(path.resolve(run_dir));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{ function_name: string, url: string, line: number, column: number }} Frame
|
||||||
|
* @typedef {Frame & { self: number, inclusive: number }} Hotspot
|
||||||
|
*/
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { parser_benchmarks } from './parser.bench.js';
|
||||||
|
|
||||||
|
export const compiler_benchmarks = parser_benchmarks;
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { parse } from '../../../packages/svelte/src/compiler/phases/1-parse/index.js';
|
||||||
|
import { fastest_test } from '../../utils.js';
|
||||||
|
|
||||||
|
const elements_source = Array.from(
|
||||||
|
{ length: 1000 },
|
||||||
|
(_, i) =>
|
||||||
|
`<!-- item ${i} --><section data-index="${i}" class="card card-${i}" title="plain text ${i}"><h2>Item ${i} has a reasonably long entity-free text value</h2><input disabled value="prefix {value} suffix"><textarea>plain {value} text</textarea></section>`
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
const script_style_source = `<script>
|
||||||
|
// line comment
|
||||||
|
/* block comment */
|
||||||
|
const value = 42;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
${Array.from(
|
||||||
|
{ length: 1000 },
|
||||||
|
(_, i) => `.item-${i} { /* property ${i} */ color: red; margin-inline: ${i}px; }`
|
||||||
|
).join('\n')}
|
||||||
|
</style>`;
|
||||||
|
|
||||||
|
const typescript_source = `${Array.from(
|
||||||
|
{ length: 1000 },
|
||||||
|
(_, i) => `<div data-index="${i}">Item ${i}</div>`
|
||||||
|
).join('\n')}
|
||||||
|
{value as number}
|
||||||
|
<script lang="ts">
|
||||||
|
let value: number = 42;
|
||||||
|
</script>${' \n'.repeat(1000)}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} label
|
||||||
|
* @param {string} source
|
||||||
|
* @param {number} iterations
|
||||||
|
*/
|
||||||
|
function create_parser_benchmark(label, source, iterations) {
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
fn: async () => {
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
parse(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fastest_test(10, () => {
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
parse(source);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parser_benchmarks = [
|
||||||
|
create_parser_benchmark('parser_elements', elements_source, 5),
|
||||||
|
create_parser_benchmark('parser_script_style', script_style_source, 50),
|
||||||
|
create_parser_benchmark('parser_typescript', typescript_source, 25)
|
||||||
|
];
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
# Compiler CPU profiling
|
||||||
|
|
||||||
|
Run the full test suite with V8 CPU profiling enabled in every Vitest worker:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm profile:compiler
|
||||||
|
```
|
||||||
|
|
||||||
|
Arguments are passed to `pnpm test`, so a smaller smoke run can target a suite:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm profile:compiler snapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Each run is stored under `benchmarking/.profiles/compiler-tests/<timestamp>`. Set
|
||||||
|
`SVELTE_PROFILE_NAME` to give a run a stable name; existing runs are never overwritten.
|
||||||
|
`benchmarking/.profiles/compiler-tests/latest.txt` contains the latest successful run name.
|
||||||
|
|
||||||
|
Each run contains:
|
||||||
|
|
||||||
|
- `raw/*.cpuprofile`: the original V8 profile from every Vitest worker
|
||||||
|
- `flamegraph.speedscope.json`: compiler-active stacks merged for use in
|
||||||
|
[Speedscope](https://www.speedscope.app/)
|
||||||
|
- `summary.md`: ranked self/inclusive hotspots and files
|
||||||
|
- `summary.json`: the same measurements for further analysis
|
||||||
|
- `manifest.json`: the command, revision, timestamps, and test result
|
||||||
|
|
||||||
|
The merged profile includes downstream callees while a compiler source frame is on the stack.
|
||||||
|
This captures parser, walker, and printer dependencies without including unrelated test runtime.
|
||||||
|
V8 reports garbage collection outside the originating stack, so the summary reports process-wide
|
||||||
|
GC separately rather than attributing it to the compiler.
|
||||||
|
|
||||||
|
The default sampling interval is 5 ms. It can be changed for shorter, focused runs:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
SVELTE_CPU_PROF_INTERVAL=1000 pnpm profile:compiler snapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing raw profiles can be analyzed again without rerunning tests:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node benchmarking/analyze-compiler-profile.js benchmarking/.profiles/compiler-tests/<run>
|
||||||
|
```
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { analyze_compiler_profiles } from './analyze-compiler-profile.js';
|
||||||
|
|
||||||
|
const root = path.resolve(fileURLToPath(import.meta.url), '../..');
|
||||||
|
const profiles_root = path.join(root, 'benchmarking/.profiles/compiler-tests');
|
||||||
|
const requested_name = process.env.SVELTE_PROFILE_NAME;
|
||||||
|
const run_name = safe(requested_name || new Date().toISOString().replaceAll(':', '-'));
|
||||||
|
const run_dir = path.join(profiles_root, run_name);
|
||||||
|
const raw_dir = path.join(run_dir, 'raw');
|
||||||
|
const test_args = process.argv.slice(2);
|
||||||
|
const sampling_interval = process.env.SVELTE_CPU_PROF_INTERVAL || '5000';
|
||||||
|
|
||||||
|
if (fs.existsSync(run_dir)) {
|
||||||
|
console.error(`Profile run already exists: ${path.relative(root, run_dir)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(raw_dir, { recursive: true });
|
||||||
|
|
||||||
|
const revision = spawnSync('git', ['rev-parse', 'HEAD'], {
|
||||||
|
cwd: root,
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
const started_at = new Date().toISOString();
|
||||||
|
const manifest_file = path.join(run_dir, 'manifest.json');
|
||||||
|
const command = [
|
||||||
|
'pnpm',
|
||||||
|
'test',
|
||||||
|
'--execArgv=--cpu-prof',
|
||||||
|
`--execArgv=--cpu-prof-dir=${raw_dir}`,
|
||||||
|
`--execArgv=--cpu-prof-interval=${sampling_interval}`,
|
||||||
|
...test_args
|
||||||
|
];
|
||||||
|
|
||||||
|
write_manifest({
|
||||||
|
command,
|
||||||
|
revision: revision.status === 0 ? revision.stdout.trim() : null,
|
||||||
|
started_at,
|
||||||
|
status: 'running'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Compiler profiles will be written to ${path.relative(root, run_dir)}`);
|
||||||
|
|
||||||
|
const result = spawnSync(command[0], command.slice(1), {
|
||||||
|
cwd: root,
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
|
||||||
|
let analysis_error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
analyze_compiler_profiles(run_dir);
|
||||||
|
fs.writeFileSync(path.join(profiles_root, 'latest.txt'), `${run_name}\n`);
|
||||||
|
} catch (error) {
|
||||||
|
analysis_error = error instanceof Error ? error.stack || error.message : String(error);
|
||||||
|
console.error(analysis_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
write_manifest({
|
||||||
|
command,
|
||||||
|
revision: revision.status === 0 ? revision.stdout.trim() : null,
|
||||||
|
started_at,
|
||||||
|
finished_at: new Date().toISOString(),
|
||||||
|
status: result.status === 0 && analysis_error === null ? 'completed' : 'failed',
|
||||||
|
test_exit_code: result.status,
|
||||||
|
test_signal: result.signal,
|
||||||
|
analysis_error
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 0 && analysis_error === null) {
|
||||||
|
console.log(
|
||||||
|
`Compiler flame graph: ${path.relative(root, path.join(run_dir, 'flamegraph.speedscope.json'))}`
|
||||||
|
);
|
||||||
|
console.log(`Compiler hotspot summary: ${path.relative(root, path.join(run_dir, 'summary.md'))}`);
|
||||||
|
} else {
|
||||||
|
process.exitCode = result.status || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} value */
|
||||||
|
function safe(value) {
|
||||||
|
return value.replace(/[^a-z0-9._-]+/gi, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Record<string, unknown>} manifest */
|
||||||
|
function write_manifest(manifest) {
|
||||||
|
fs.writeFileSync(manifest_file, `${JSON.stringify(manifest, null, '\t')}\n`);
|
||||||
|
}
|
||||||
Loading…
Reference in new issue