docs java javascript python c / c++ c# reading reports
How to profile JavaScript
JavaScript profiling splits into two worlds that produce different files:
Node.js server code (V8 CPU profiles, .cpuprofile) and
the browser (Chrome DevTools performance traces). flamelens analyzes
both — a trace additionally carries the main-thread story: long tasks, and the
scripting / layout / paint / GC split behind the jank. Capture needs nothing but what
you already have installed.
Node.js from the command line
V8 has a sampling CPU profiler built in — one flag, no dependencies:
# profile the whole run; writes CPU.<timestamp>.cpuprofile in the working dir
node --cpu-prof app.js
# choose where the files land
node --cpu-prof --cpu-prof-dir=./profiles app.js
For a long-running server you don't want to profile end-to-end, attach the inspector instead:
node --inspect app.js
# then: open chrome://inspect in Chrome → your target → "inspect"
# DevTools → Performance panel (or the legacy Profiler tab) → record → stop → save
Browser code: Chrome DevTools
- Open your page, press F12, go to the Performance panel.
- Press record, do the slow thing (a route change, a heavy render, the janky scroll), press stop. Keep recordings short and focused — 5–15 seconds of the actual problem.
- Right-click in the panel (or use the ↓ save button) → Save profile…
— you get a trace
.json.
In JetBrains WebStorm
- Run → Edit Configurations… and select (or create) your Node.js run configuration.
- In the Node parameters field, add:
--cpu-prof --cpu-prof-dir=profiles - Run as usual and exercise the code path; stop the process cleanly (the profile is written at exit).
- Pick up the
CPU.*.cpuprofilefile from theprofilesfolder in your project.
This works identically for an npm-script configuration — put the flags in
NODE_OPTIONS in the configuration's environment variables instead:
NODE_OPTIONS=--cpu-prof.
In VS Code
VS Code's JavaScript debugger has a profiler built in — no flags needed:
- Start your app under the debugger (F5, any
nodelaunch configuration). - Open the Command Palette (Ctrl+Shift+P) and run Debug: Take Performance Profile — or click the record ⏺ button in the Call Stack view's header.
- Choose CPU Profile, then how to stop it (manual, duration, or breakpoint-bounded). Exercise the slow path.
- Stop the profile — VS Code writes a
.cpuprofileinto your workspace and opens its own flame view of it.
Now analyze it. Take the .cpuprofile or the trace
.json to the flamelens terminal —
auth <email> → verify <code> →
scan init. Traces get the long-task and rendering-pipeline breakdown
on top of hot functions. Java service in the same system?
JFR works too.
Scanning a running Node process PRO
Everything above is a file you capture and upload. If the slow thing only happens in a deployed environment, flamelens can attach to the process itself. Node's equivalent of a JVM's JMX port is the inspector — the same protocol Chrome DevTools speaks:
node --inspect=0.0.0.0:9229 app.js
Then, in the terminal:
scan live app.example.com:9229 60 --node
We open the inspector's WebSocket, run Profiler.start, wait, then
Profiler.stop and analyze the .cpuprofile that comes back —
exactly the file you'd have saved by hand from DevTools, minus the hand.
Read this before opening an inspector port. The inspector is not a
read-only metrics port like JMX-with-a-password. It is a full debugger channel, and
it has no authentication of any kind. Anyone who can reach
9229 can call Runtime.evaluate and execute arbitrary code
in your process, read every secret in memory, and open a shell.
An open inspector port is remote code execution as a service.
It's worse than it looks, too: the protocol's only origin defense is a host-header
check, so a browser on any machine inside your network can be used to reach a
localhost inspector via DNS rebinding. Node's own docs say to never
bind it to a public interface, and we agree — we'll refuse private and loopback
targets anyway, which is why the direct path only works for a genuinely
internet-reachable process.
So use --inspect directly only when: the exposure is
short-lived (turn it on, scan, turn it off — kill -USR1
<pid> enables it on a running process without a restart), the port is
firewalled to our egress address alone, and the process holds nothing you'd mind
losing. Otherwise bind --inspect=127.0.0.1:9229 and use the agent
below, which needs no exposed port at all. That is the honest recommendation for
production.
Continuous sampling with an agent ENTERPRISE
The agent runs beside your process and connects out to us over HTTPS, so the
inspector can stay bound to 127.0.0.1 where it belongs, and nothing
inbound is ever opened. It polls for due samples, records locally, and posts the
file — the same loop as the Java
agent, with the recorder swapped:
agent new prod-api-1
# → prints a token, ONCE. store it like a password.
schedule new api localhost:9229 node 30 60
# → sample localhost:9229 for 60s, every 30 minutes
The recorder is a small Node script the agent shells out to. It speaks the same inspector protocol, but over the loopback interface only:
// profile-self.js — require() this in your app; it profiles the process it lives in.
const { Session } = require('node:inspector/promises');
const fs = require('node:fs/promises');
async function record(seconds, out) {
const session = new Session();
session.connect(); // no port, no listener — in-process
await session.post('Profiler.enable');
await session.post('Profiler.start');
await new Promise(r => setTimeout(r, seconds * 1000));
const { profile } = await session.post('Profiler.stop');
session.disconnect();
await fs.writeFile(out, JSON.stringify(profile));
}
// Sample on demand without an open port: kill -USR2 <pid>
process.on('SIGUSR2', () => record(60, '/tmp/flamelens.cpuprofile'));
That form needs no port at all — the cleanest option if you can add a few lines to your
app. To sample a separate local process instead, run it with
--inspect=127.0.0.1:9229 and point any CDP client
(chrome-remote-interface is the usual one) at
http://127.0.0.1:9229/json/list, then drive the same three commands.
Loopback-bound is the important part: the agent is on the same host, so the port never
needs to leave it.
The agent's upload step is identical to the Java one — post the file to
/upload with the schedule id and a .cpuprofile extension. The
work item tells the agent which recorder to use:
work=$(curl -sf -X POST "$API/work" -H "X-Flamelens-Agent-Token: $TOKEN")
echo "$work" | jq -c '.work[]' | while read -r item; do
id=$(echo "$item" | jq -r .scheduleUuid)
secs=$(echo "$item" | jq -r .durationSeconds)
case $(echo "$item" | jq -r .runtime) in
JAVA) out=/tmp/s.jfr; jcmd "$PID" JFR.start name=fl duration="${secs}s" >/dev/null
sleep "$((secs+2))"
jcmd "$PID" JFR.dump name=fl filename="$out" >/dev/null ;;
NODE) out=/tmp/s.cpuprofile; node record.js "$secs" "$out" ;;
PYTHON) out=/tmp/s.speedscope; py-spy record --format speedscope -o "$out" \
--pid "$PID" --duration "$secs" >/dev/null ;;
NATIVE) out=/tmp/s.perf; perf record -F 99 -p "$PID" -g -o /tmp/perf.data \
-- sleep "$secs" >/dev/null 2>&1
perf script -i /tmp/perf.data | c++filt > "$out" ;;
esac
curl -sf -X POST "$API/upload" -H "X-Flamelens-Agent-Token: $TOKEN" \
-F "scheduleUuid=$id" -F "profile=@$out" >/dev/null
rm -f "$out"
done
Samples land in your current tuning session and get diffed against the previous one, so a schedule becomes a timeline rather than a pile of files.
What makes a good profile
- Profile the symptom. A 10-second recording of the slow endpoint beats a 10-minute recording of everything.
- Production mode, not dev mode.
NODE_ENV=production, minified client builds off — dev-server overhead (HMR, source-map work) will dominate an unbuilt profile and lie to you. - Warm up first. JIT-cold code profiles differently; hit the path once before recording it.