Performance
The profile viewer shows where a running program spent its time or its memory. Every call path is a block as wide as its share, in a flame graph with the callers below or an icicle with them above; click a block for its figures, double-click to zoom in. The functions table gives each function's own time and its total with what it called, sorted and searchable, and draws what calls the chosen one and what it calls. A timeline lays the samples and any traced spans out over time, so you can pick out one slow request and look only at that stretch.
It reads V8 .cpuprofile and .heapprofile files, trace event JSON with the CPU samples of a recording, pprof (gzipped or not) and collapsed stacks. Give it a profile from before a change and it compares the two, colouring each block by how much it grew or shrank and listing the functions that changed most. The flame graph goes out as SVG, the profile as collapsed stacks, pprof or JSON, and the functions as CSV - for a support ticket, a performance page in your application or a check on every pull request.
Try it with your own profiles in confidence: they are opened by your browser, on your device. Nothing is uploaded, no copy reaches us or anyone else, and nothing is kept once you close this tab.
A slow quote, found in the flame graph
The order service of Brackenlow Garden Centres, a made-up chain, prices a click-and-collect basket before it is paid for. Customers say quotes are slow. Here is an invented V8 CPU profile of a load test that sent 30 quotes one after another, from 5 to 84 lines each. Find the function that takes the time, see what calls it, pick out the slowest quote on the timeline, and rule out the database.
// Node 20 or later, from the Gold package: after a load test, list the ten functions with the most
// CPU time of their own, write the flame graph, and fail when one function takes more than a fifth.
import { readFileSync, writeFileSync } from 'node:fs';
import { readProfile, functionStats, profileTotal, buildTree, flameSvg, formatValue } from './dist/node/bmx-profile-viewer.mjs';
const profile = await readProfile(readFileSync('load-test/orders-quote.cpuprofile'), 'orders-quote.cpuprofile');
const all = profileTotal(profile);
const top = functionStats(profile).filter(f => !profile.frames[f.frame].kind).slice(0, 10);
for (const f of top) {
const { name, file = '', line = '' } = profile.frames[f.frame];
console.log(`${formatValue(f.self, 'microseconds').padStart(10)} ${((100 * f.self) / all).toFixed(1).padStart(5)}% ${name} ${file}:${line}`);
}
writeFileSync('load-test/flame.svg', flameSvg(buildTree(profile), { frames: profile.frames, unit: 'microseconds', title: profile.name }));
process.exitCode = top[0] && top[0].self / all > 0.2 ? 1 : 0;
Before and after the fix
The fix keeps each compiled promotion rule in a map instead of building it again for every line of every basket. The same 30 quotes were sent again, this time recorded as trace event JSON, so each quote and its stages are spans on the timeline beside the CPU samples. The earlier recording is the baseline: see what got faster, what the fix added, and how long the 84-line basket takes now.
Get the code
<script src="/assets/bmx-components.min.js"></script> <bmx-profile-viewer id="pv-fix" label="Order service before and after the fix" file-name="brackenlow-orders-fix" style="inline-size: 100%; --bmx-profile-viewer-height: 34rem"></bmx-profile-viewer>