Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
.idea
node_modules/*
dist
tmp/

.yarn/install-state.gz
71 changes: 71 additions & 0 deletions scripts/analyze-cpuprofile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* eslint-env node */
//
// analyze-cpuprofile.js — print top frames by self-time from a .cpuprofile.
//
// Usage: node scripts/analyze-cpuprofile.js path/to/file.cpuprofile [topN]
//
// .cpuprofile format (Chrome DevTools):
// { nodes: [{id, callFrame:{functionName,url,lineNumber}, hitCount?, children?}],
// samples: [nodeId,...], timeDeltas: [µs,...] }
// Self-time per node = sum of timeDeltas for samples whose node === id.
// We bucket by (functionName, url) so anonymous closures inside the same file
// merge sensibly.

'use strict';

const fs = require('fs');
const path = require('path');

const file = process.argv[2];
const topN = Number(process.argv[3] || 20);
if (!file) {
console.error('usage: analyze-cpuprofile.js <file.cpuprofile> [topN]');
process.exit(2);
}

const profile = JSON.parse(fs.readFileSync(file, 'utf8'));
const { nodes, samples, timeDeltas } = profile;

const nodeById = new Map();
for (const n of nodes) nodeById.set(n.id, n);

const selfByNode = new Map();
for (let i = 0; i < samples.length; i++) {
const id = samples[i];
const dt = timeDeltas[i] || 0;
selfByNode.set(id, (selfByNode.get(id) || 0) + dt);
}

const totalUs = timeDeltas.reduce((a, b) => a + b, 0);

const buckets = new Map();
for (const [id, us] of selfByNode) {
const node = nodeById.get(id);
const cf = node.callFrame || {};
const fn = cf.functionName || '(anonymous)';
const url = cf.url || '';
const key = `${fn}\t${url}`;
buckets.set(key, (buckets.get(key) || 0) + us);
}

const ranked = [...buckets.entries()]
.map(([k, us]) => {
const [fn, url] = k.split('\t');
return { fn, url, us };
})
.sort((a, b) => b.us - a.us);

console.log(`# ${path.basename(file)}`);
console.log(`# total profile time: ${(totalUs / 1000).toFixed(1)} ms across ${samples.length} samples`);
console.log('');
console.log(' self% self_ms function (file)');
console.log(' ------ ------- -----------------------------------------------------');
for (const row of ranked.slice(0, topN)) {
const pct = (row.us / totalUs) * 100;
const ms = row.us / 1000;
const tag = row.url
? path.basename(row.url.replace(/^file:\/\//, ''))
: '(no-file)';
const fn = row.fn || '(anonymous)';
console.log(` ${pct.toFixed(1).padStart(5)}% ${ms.toFixed(1).padStart(6)} ${fn} (${tag})`);
}
59 changes: 59 additions & 0 deletions scripts/analyze-heapprofile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* eslint-env node */
//
// analyze-heapprofile.js — top frames by self-allocation bytes from
// node --heap-prof output. Format: { head: { id, callFrame, selfSize,
// children: [...] } }, recursive. selfSize is sampled bytes attributed to
// allocations done directly in that frame.
//
// Usage: node scripts/analyze-heapprofile.js path/to/file.heapprofile [topN]

'use strict';

const fs = require('fs');
const path = require('path');

const file = process.argv[2];
const topN = Number(process.argv[3] || 20);
if (!file) {
console.error('usage: analyze-heapprofile.js <file.heapprofile> [topN]');
process.exit(2);
}

const profile = JSON.parse(fs.readFileSync(file, 'utf8'));

const buckets = new Map();
let total = 0;

(function walk(node) {
if (node.selfSize > 0) {
const cf = node.callFrame || {};
const fn = cf.functionName || '(anonymous)';
const url = cf.url || '';
const key = `${fn}\t${url}`;
buckets.set(key, (buckets.get(key) || 0) + node.selfSize);
total += node.selfSize;
}
if (node.children) for (const c of node.children) walk(c);
})(profile.head);

const ranked = [...buckets.entries()]
.map(([k, b]) => {
const [fn, url] = k.split('\t');
return { fn, url, bytes: b };
})
.sort((a, b) => b.bytes - a.bytes);

console.log(`# ${path.basename(file)}`);
console.log(`# total sampled allocations: ${(total / 1024 / 1024).toFixed(1)} MB`);
console.log('');
console.log(' alloc% bytes(KB) function (file)');
console.log(' ------ --------- -----------------------------------------------------');
for (const row of ranked.slice(0, topN)) {
const pct = (row.bytes / total) * 100;
const kb = row.bytes / 1024;
const tag = row.url
? path.basename(row.url.replace(/^file:\/\//, ''))
: '(no-file)';
const fn = row.fn || '(anonymous)';
console.log(` ${pct.toFixed(1).padStart(5)}% ${kb.toFixed(1).padStart(8)} ${fn} (${tag})`);
}
171 changes: 171 additions & 0 deletions scripts/profile-driver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/* eslint-env node */
//
// profile-driver.js — runs ONE scenario against ONE fixture, sized so a
// single node invocation under --cpu-prof / --heap-prof / --prof produces a
// useful profile. Pair with scripts/profile.sh, which wraps node with the
// right flags.
//
// Env vars:
// SCENARIO init | opf | gpf | eachmap-gen | eachmap-orig (default: opf)
// FIXTURE path under benchmarks/jridgewell/fixtures (default: babel.min.js.map)
// ITERS override per-scenario iteration count (optional)
// WARMUP warmup iters before the measured loop (default: scenario-specific)
//
// The driver intentionally does *only* the scenario inside the measured loop.
// Fixture read + JSON.parse happen up front so disk/JSON cost doesn't pollute
// the profile. The Consumer is also re-used across iterations for opf / gpf /
// eachmap scenarios so we measure the steady-state hot path, not init.

'use strict';

const fs = require('fs');
const path = require('path');
const { performance } = require('perf_hooks');

const { SourceMapConsumer } = require('../source-map.js');

const SCENARIO = process.env.SCENARIO || 'opf';
const FIXTURE_NAME = process.env.FIXTURE || 'babel.min.js.map';
const FIXTURE_PATH = path.isAbsolute(FIXTURE_NAME)
? FIXTURE_NAME
: path.join(__dirname, '..', 'benchmarks', 'jridgewell', 'fixtures', FIXTURE_NAME);

const raw = fs.readFileSync(FIXTURE_PATH, 'utf8');
const json = JSON.parse(raw);

// Build a deterministic set of (line, column) probes drawn from the decoded
// mapping table itself. Random probes that miss every mapping line would skew
// the profile toward "binary-search-the-empty-line" code paths.
function buildOpfProbes(consumer, count) {
const probes = [];
consumer.eachMapping((m) => {
probes.push({ line: m.generatedLine, column: m.generatedColumn });
});
if (probes.length === 0) return probes;
const stride = Math.max(1, Math.floor(probes.length / count));
const out = [];
for (let i = 0; i < probes.length && out.length < count; i += stride) {
out.push(probes[i]);
}
return out;
}

function buildGpfProbes(consumer, count) {
const probes = [];
consumer.eachMapping((m) => {
if (m.source) {
probes.push({ source: m.source, line: m.originalLine, column: m.originalColumn });
}
});
if (probes.length === 0) return probes;
const stride = Math.max(1, Math.floor(probes.length / count));
const out = [];
for (let i = 0; i < probes.length && out.length < count; i += stride) {
out.push(probes[i]);
}
return out;
}

const scenarios = {
init({ iters }) {
iters = iters || 30;
// No warmup: every iteration is a fresh init, that's the whole point.
let sink = 0;
const t0 = performance.now();
for (let i = 0; i < iters; i++) {
const c = new SourceMapConsumer(json);
// Touch one position so the lazy parts that the constructor schedules
// actually run (e.g. the originalPositionFor warmup that triggers
// _buildOriginalMappings on first call is excluded — see gpf scenario).
const r = c.originalPositionFor({ line: 1, column: 0 });
sink += r.line || 0;
}
return { iters, ms: performance.now() - t0, sink };
},

opf({ iters, warmup }) {
iters = iters || 5_000_000;
warmup = warmup == null ? 5000 : warmup;
const c = new SourceMapConsumer(json);
const probes = buildOpfProbes(c, 5000);
if (probes.length === 0) throw new Error('no probes built');
// Warmup: triggers JIT tier-up so the profile reflects optimized code.
for (let i = 0; i < warmup; i++) {
const p = probes[i % probes.length];
c.originalPositionFor(p);
}
let sink = 0;
const t0 = performance.now();
for (let i = 0; i < iters; i++) {
const p = probes[i % probes.length];
const r = c.originalPositionFor(p);
sink += r.line || 0;
}
return { iters, ms: performance.now() - t0, sink };
},

gpf({ iters, warmup }) {
iters = iters || 3_000_000;
warmup = warmup == null ? 5000 : warmup;
const c = new SourceMapConsumer(json);
const probes = buildGpfProbes(c, 5000);
if (probes.length === 0) throw new Error('no probes built');
for (let i = 0; i < warmup; i++) {
const p = probes[i % probes.length];
c.generatedPositionFor(p);
}
let sink = 0;
const t0 = performance.now();
for (let i = 0; i < iters; i++) {
const p = probes[i % probes.length];
const r = c.generatedPositionFor(p);
sink += r.line || 0;
}
return { iters, ms: performance.now() - t0, sink };
},

'eachmap-gen': function ({ iters, warmup }) {
iters = iters || 300;
warmup = warmup == null ? 3 : warmup;
const c = new SourceMapConsumer(json);
let sink = 0;
for (let i = 0; i < warmup; i++) c.eachMapping(() => {});
const t0 = performance.now();
for (let i = 0; i < iters; i++) {
c.eachMapping((m) => {
sink += m.generatedLine | 0;
});
}
return { iters, ms: performance.now() - t0, sink };
},

'eachmap-orig': function ({ iters, warmup }) {
iters = iters || 300;
warmup = warmup == null ? 3 : warmup;
const c = new SourceMapConsumer(json);
const ORIG = SourceMapConsumer.ORIGINAL_ORDER;
let sink = 0;
for (let i = 0; i < warmup; i++) c.eachMapping(() => {}, null, ORIG);
const t0 = performance.now();
for (let i = 0; i < iters; i++) {
c.eachMapping((m) => {
sink += m.generatedLine | 0;
}, null, ORIG);
}
return { iters, ms: performance.now() - t0, sink };
},
};

if (!scenarios[SCENARIO]) {
console.error(`unknown SCENARIO=${SCENARIO}; expected one of: ${Object.keys(scenarios).join(', ')}`);
process.exit(2);
}

const iters = process.env.ITERS ? Number(process.env.ITERS) : undefined;
const warmup = process.env.WARMUP != null ? Number(process.env.WARMUP) : undefined;

console.log(`scenario=${SCENARIO} fixture=${path.basename(FIXTURE_PATH)} node=${process.version}`);
const result = scenarios[SCENARIO]({ iters, warmup });
console.log(
` iters=${result.iters} total=${result.ms.toFixed(1)}ms per-iter=${(result.ms / result.iters).toFixed(4)}ms sink=${result.sink}`,
);
81 changes: 81 additions & 0 deletions scripts/profile.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
#
# profile.sh — run scripts/profile-driver.js under V8 profilers and emit:
# tmp/profiles/$SCENARIO.cpuprofile load in Chrome DevTools → Performance
# tmp/profiles/$SCENARIO.heapprofile load in Chrome DevTools → Memory → Allocation profile
# tmp/profiles/$SCENARIO.prof.txt node --prof text summary (V8 ticks)
#
# Usage:
# scripts/profile.sh # SCENARIO=opf, FIXTURE=babel.min.js.map
# SCENARIO=init scripts/profile.sh
# SCENARIO=eachmap-gen FIXTURE=vscode.map scripts/profile.sh
# SCENARIO=opf MODE=cpu scripts/profile.sh # cpu profile only (skip heap + prof)
#
# MODE values: all (default), cpu, heap, prof

set -euo pipefail

SCENARIO="${SCENARIO:-opf}"
FIXTURE="${FIXTURE:-babel.min.js.map}"
MODE="${MODE:-all}"

ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"

OUT_DIR="$ROOT/tmp/profiles"
mkdir -p "$OUT_DIR"

# Skip the per-call sandbox/permission overhead; --no-warnings keeps the
# output clean. --max-old-space-size matches the bench scripts so vscode.map
# doesn't OOM.
NODE_FLAGS=(--no-warnings --max-old-space-size=8192)

DRIVER="$ROOT/scripts/profile-driver.js"

export SCENARIO FIXTURE

run_cpu() {
echo "--- CPU profile (SCENARIO=$SCENARIO FIXTURE=$FIXTURE) ---"
rm -f "$OUT_DIR/$SCENARIO.cpuprofile"
node "${NODE_FLAGS[@]}" \
--cpu-prof \
--cpu-prof-dir="$OUT_DIR" \
--cpu-prof-name="$SCENARIO.cpuprofile" \
--cpu-prof-interval=100 \
"$DRIVER"
echo " → $OUT_DIR/$SCENARIO.cpuprofile"
}

run_heap() {
echo "--- Heap allocation profile (SCENARIO=$SCENARIO FIXTURE=$FIXTURE) ---"
rm -f "$OUT_DIR/$SCENARIO.heapprofile"
node "${NODE_FLAGS[@]}" \
--heap-prof \
--heap-prof-dir="$OUT_DIR" \
--heap-prof-name="$SCENARIO.heapprofile" \
"$DRIVER"
echo " → $OUT_DIR/$SCENARIO.heapprofile"
}

run_prof() {
echo "--- V8 tick profile (SCENARIO=$SCENARIO FIXTURE=$FIXTURE) ---"
local work
work="$(mktemp -d)"
(
cd "$work"
node "${NODE_FLAGS[@]}" --prof "$DRIVER"
local log
log="$(ls isolate-*.log | head -n 1)"
node --prof-process "$log" > "$OUT_DIR/$SCENARIO.prof.txt"
)
rm -rf "$work"
echo " → $OUT_DIR/$SCENARIO.prof.txt"
}

case "$MODE" in
cpu) run_cpu ;;
heap) run_heap ;;
prof) run_prof ;;
all) run_cpu; run_heap; run_prof ;;
*) echo "ERROR: unknown MODE='$MODE' (expected: all, cpu, heap, prof)" >&2; exit 1 ;;
esac
Loading