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

  1. Open your page, press F12, go to the Performance panel.
  2. 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.
  3. Right-click in the panel (or use the ↓ save button) → Save profile… — you get a trace .json.

In JetBrains WebStorm

  1. Run → Edit Configurations… and select (or create) your Node.js run configuration.
  2. In the Node parameters field, add:
    --cpu-prof --cpu-prof-dir=profiles
  3. Run as usual and exercise the code path; stop the process cleanly (the profile is written at exit).
  4. Pick up the CPU.*.cpuprofile file from the profiles folder 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:

  1. Start your app under the debugger (F5, any node launch configuration).
  2. 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.
  3. Choose CPU Profile, then how to stop it (manual, duration, or breakpoint-bounded). Exercise the slow path.
  4. Stop the profile — VS Code writes a .cpuprofile into your workspace and opens its own flame view of it.

Now analyze it. Take the .cpuprofile or the trace .json to the flamelens terminalauth <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