Paths on this page use
~/.xum, the default Xum home. If you set XUM_ROOT, Xum uses that folder instead.
Run xum api commands
Most steps on this page use xum api <namespace> <procedure>. Namespace and procedure names are kebab-case, and input fields become kebab-case flags, for example xum api perf-captures capture-now --process backend.
xum api talks HTTP to a running Xum backend. It finds the backend in this order:
XUM_SERVER_URLandXUM_SERVER_AUTH_TOKEN, when set.server.lockin the Xum home. The desktop app writes it for its local API server (127.0.0.1, random port), unless you setXUM_NO_API_SERVER=1.xum serveralso writes it.http://localhost:3000.
xum server running while you use these commands.
Flight recorder
The flight recorder keeps the last 10 minutes of performance samples in memory: backend event-loop delay, garbage collection and heap, renderer long frames and slow interactions, and oRPC call timings. It works for the desktop app and forxum server.
Turn it on
Open Settings → Experiments and turn on Performance flight recorder. Or run:feature_flags.json in the Xum home.
Turning it off stops collection. The samples already recorded stay readable until they are 10 minutes old. A restart clears everything.
Read the samples
xum api output can stop early, so jq then reports incomplete JSON.
This command works while the recorder is off. Check state first: collecting means the recorder runs, off means it is off, and failed means it stopped with an error in failure.
Useful filters:
atMs, startMs, endMs, nowMs) are milliseconds on the performance clock (performance.timeOrigin + performance.now()). Compare them with nowMs to see how old an entry is.
What the snapshot contains
What the snapshot contains
Top-level keys:
version, state, nowMs, failure (only when state is failed), backend, renderer, trips, rpc.The renderer sends its entries to the backend every 5 s. It does not send entries from before you turned the recorder on.oRPC procedure timings cover every transport: HTTP (including
xum api), WebSocket, and the desktop app’s internal MessagePort. Flow-control waits come from WebSocket clients only.Trips
A trip is a detected problem. Xum records it intrips, and some trips start a CPU profile.
Privacy and cost
The snapshot contains no chat content and no event text. It does contain oRPC procedure names and error codes, script URLs, function names, invoker strings, event names and element tag names. Each window sends its renderer samples to the Xum backend it is connected to. Samples and profiles stay on the machine that runs that backend unless you share them yourself. For the desktop app, that is your machine. For a browser connected to a remotexum server, that is the server. Xum sends nothing to any other service.
When the recorder is off, Xum runs no observers or timers. When it is on, it costs about 1.4 ms of backend CPU per second, mostly for the 20 ms delay histogram. oRPC recording adds well under a microsecond per call. CPU profiles cost more (see below).
Triggered CPU profiles
When the flight recorder detects a stall, Xum records a short CPU profile of what runs next. The renderer sends long frames to the backend in 5 s batches, so a renderer capture can start several seconds after the frame. These captures are part of the Performance flight recorder experiment. There is no separate switch.
Rules:
- Each capture records 8 s at 1 ms sampling.
- Each trip kind has its own cooldown: 60 minutes for
loop-delay-p99(backend) captures and 10 minutes forlong-animation-frame(renderer) captures. The cooldown starts when Xum admits a capture, including captures that end up skipped. The 60-minute backend cooldown trades fewer backend pauses for fewer observations. - Only one capture runs at a time. Xum drops trips that arrive during a capture.
label says activity after trigger. Use it to see what work follows a stall, for example repeated renders or retries.
Capture a profile by hand
- Turn on the flight recorder.
-
Run:
--processisbackendorrenderer.--duration-msis 1000 to 30000 and defaults to 8000. A renderer capture profiles the desktop app’s main window. -
The command returns the capture’s metadata when it ends. Check it:
profileFilenames the written profile, andskippedReasonmeans Xum could not profile (see the skip reasons below).
PRECONDITION_FAILEDwith “perf captures need the perfFlightRecorder experiment”: turn on the flight recorder.CONFLICTwith “a perf capture is already in progress”: another capture is running. Wait and try again.CONFLICTwith “perf capture cancelled”: the flight recorder was turned off during the capture. Turn it back on and try again.
Find captures
{ dir, captures }, newest first. A capture with a profile is two files in ~/.xum/perf/captures/. A skipped capture (see the skip reasons below) has only the .json file.
<id>.cpuprofile: a V8 CPU profile in JSON. Chrome DevTools, speedscope and the offline analyzer can read it.<id>.json: the metadata. Xum writes it last, so a.jsonfile means the capture is complete.
id looks like 20261003T013338383Z-1a2b3c4d. The folder is private to your user (mode 0700, files 0600). For triggered captures, Xum logs [perfCaptures] captured (info) when the capture ends and [perfCaptures] capture failed (warn) when it fails. Manual captures do not write these lines.
There is no delete command. Delete the files by hand.
Metadata fields, skip reasons and retention
Metadata fields, skip reasons and retention
Metadata fields:
version, id, kind (loop-delay-p99, long-animation-frame or manual), process (backend or renderer), trigger (the trip, or null for a manual capture), startedAtMs, endedAtMs, samplingIntervalUs, durationMs, xumVersion, platform, label, and, when present, skippedReason, profileFile, profileBytes.When Xum cannot profile, it writes only the metadata file with a skippedReason:Retention: Xum keeps the newest 20 captures that have a profile, within 200 MiB in total (by file modification time), plus up to 20 skipped records. There is no age limit. Xum removes leftover temporary files older than 10 minutes.
Hang stacks (desktop)
When the desktop app’s main window stops responding, Xum logs where its JavaScript is stuck. This is always on and needs no experiment.- Xum logs
[diag] renderer unresponsive. - Xum asks the renderer for its JavaScript call stack, with a 2 s timeout, once per hang.
- Xum logs
[diag] renderer unresponsive JS stackwith{url, stack}, or[diag] renderer unresponsive JS stack unavailablewith the error.
~/.xum/logs/mux.log, or in the Output tab. The log file rotates at 10 MB and keeps mux.1.log to mux.3.log.
Pop-out windows and browser clients of xum server are not covered.
Offline analyzer
scripts/perf/analyzeProfiles.ts ranks the hottest functions in one or more CPU profiles, or compares two sets of profiles. It runs offline, uses no network, and never fetches remote source maps. Run it from a Xum source checkout:
*.cpuprofile files, and *.json files only when they look like a CPU profile, so it skips capture metadata. It reads V8 CPU profiles from Node, Bun, Chrome DevTools, Xum captures, and the perf E2E chrome-cpu-profile.json.
Example: capture and analyze a backend profile
-
Capture a profile while you reproduce the slow action:
-
List captures to see the folder:
-
Print the leaderboard:
The output starts with a
# CPU profile hotspotstitle and aProfiles:summary line. Then it shows time per category (app,node_modules,internal,extension,gc,program,idle) and aTop 25 by self timetable with the columns# | Function | Location | Category | Self ms | Self % | Total ms | Total % | Samples. Shares exclude idle time unless you pass--include-idle. -
Write folded stacks and open them in speedscope or
flamegraph.pl:Each line isframe;frame;frame <sampleCount>, root to leaf. Weights are sample counts, not time. The summary goes to stderr.
Compare two sets of profiles
--baseline (repeatable) turns on diff mode. The positional paths become the candidate set. The analyzer compares each function’s self time per second of wall time, in the columns Baseline ms/s | Candidate ms/s | Change ms/s. --min-change <ms/s> hides smaller changes (default 1). Rows match on file, function, line and column, so a function that moved shows as one removed and one added row. Folded output is not available in diff mode.
All options
All options
Exit status: 0 on success, 2 on usage errors, and 1 on any other failure, for example no valid profile on a required side, an
--out file that cannot be written, or a runtime error.Session tapes
A session tape records what one full chat subscription received from the backend, with its original timing. Tapes are for your own local performance analysis. The replay harness replays only synthetic tapes. Xum records full chat subscriptions from every client of the backend, not only the Xum UI: ACP sessions and other API clients that load a whole chat also create tapes. Turn it on in Settings → Experiments → Session tapes. It applies to chats you open after you turn it on. Xum writes tapes to~/.xum/perf/tapes/ (mode 0700, files 0600) as <startedAt>-<workspaceIdHash>-<tapeId>.jsonl.
What Xum records:
- Only full replays: the subscription that loads a whole chat, for example when you open it.
- Not reconnects, live-only subscriptions, or older history you load later with “load older”.
A crash, or a quit that needs more than 1 s to write, loses the open tapes. Xum never writes a partial file.
Save tapes and find the folder
While the Session tapes experiment is on, the command palette has two commands in its Help section. Open the palette in command mode withF4, or press Ctrl+Shift+P (⌘+Shift+P on macOS) and type >.
With the experiment off, the commands do not appear and the shortcuts do nothing. The shortcuts also do nothing while a dialog is open. See Keyboard shortcuts for all shortcuts.
Both commands show only a tape count and the folder path. They never show tape content.
Run Save open session tapes before you quit, or before you copy a tape for your own local analysis. The tape is then complete on disk, and you do not have to close the chat.
- Xum finalizes every open tape and writes it to disk. Each trailer has
end.reason: "stopped", orerrorif Xum had already failed to record an event in that tape. - The chat keeps running, but Xum does not record the rest of that subscription. Xum starts a new tape for the chat only at its next full replay, for example after you reload Xum.
- A message shows the number of saved tapes and the folder, for example
Saved 2 session tapes to <folder>. When Xum wrote no tape, it showsNo open session tapes to save. Folder: <folder>. In a chat view the message is a toast that stays for 15 s. Outside a chat view, for example in Analytics, it is a browser alert.
Could not save session tapes: and the error. If you run the command again while a save runs, Xum ignores it.
Reveal session tapes folder depends on where Xum runs:
- Desktop app: Xum opens
~/.xum/perf/tapes/in your file manager and showsOpened <folder>. If the folder does not exist yet, Xum creates it with mode 0700 first. xum serverin a browser: the backend cannot open a folder on your machine. The message showsSession tapes folder: <folder>instead, as a 15 s toast in a chat view.
Could not open the session tapes folder: and the error.
Format, limits and retention
Format, limits and retention
A tape is JSONL (format version 3):
- A header line with
tape,xumVersion,tapeId,workspaceIdHash,startedAt,masking: "none"andsubscription. - One line per event:
{t, bytes, event}, plusmetawhen the event holds values that plain JSON cannot represent. - A trailer:
{t, end: {reason, truncated, droppedEvents}}.
truncated: true and droppedEvents.Retention: Xum keeps the newest 20 tapes within 200 MiB. There is no age limit. Turning the experiment off does not delete tapes. Delete ~/.xum/perf/tapes/ yourself.Replay a synthetic tape (contributors)
make perf-tape-replay replays a synthetic tape through the real desktop app and profiles how fast the chat renders. It is for Xum contributors and runs from a Xum source checkout. It replays only the committed synthetic fixture tests/e2e/fixtures/sessionTapes/perf-tape-replay.jsonl. The make target takes no tape path.
-
Run:
The target runs
make build, then the Playwright Electron scenariotests/e2e/scenarios/perf.tapeReplay.spec.tswith one worker. It needs a display. On headless Linux, runxvfb-run -a make perf-tape-replay. Pass extra Playwright flags inPLAYWRIGHT_ARGS. -
Find the results in
artifacts/perf/electron/tape-replay-<timestamp>/:chrome-cpu-profile.json: a renderer CPU profile. The offline analyzer can read it.chrome-trace.json: a Chrome trace.react-profile.json: React render timings.perf-summary.json: the run summary. ItstapeReplayobject has the time to the first and last chat row (firstRowMs,lastRowMs), the tape’seventCountanddurationMs, andblockedRequests.
tests/e2e/fixtures/sessionTapes/tapeReplayFixture.ts and run bun scripts/perf/generateTapeReplayFixture.ts.
How replay stays offline
How replay stays offline
Xum serves a tape only inside the isolated test harness. The app replays only when both
XUM_E2E=1 and XUM_REPLAY_HARNESS=1 are set. The Electron test fixture (tests/e2e/electronTest.ts) sets XUM_E2E, and the scenario sets XUM_REPLAY_HARNESS and XUM_REPLAY_TAPES. The Makefile sets none of them. Anywhere else, the chat shows the refusal session tape replay runs only inside the perf harness.In replay mode, the desktop app does this:- It blocks renderer network requests. Only
data:,blob:,devtools:and localfile:URLs load, so recorded image URLs are never fetched. - It refuses model calls and external opens.
- It makes the chat read-only. A send shows a read-only error.
gh and Coder CLI probes. The scenario does that:- It removes variables that end in
_API_KEY,_AUTH_TOKENor_BASE_URLfrom the app’s environment. - It puts failing
ghandcoderstubs first onPATH. - It wraps
gitso that git can use only the local file transport.
XUM_REPLAY_TAPES is a JSON map of workspace ID to tape path. The scenario sets it. It is not a user setting. Each tape path must be an absolute local path: a POSIX path that starts with /, or a Windows drive path such as C:\. Xum refuses UNC paths, \\?\ prefixes, URLs and relative paths. This is a syntax check, not a folder restriction: .. segments and symlinks are accepted. The file name must end in .jsonl, and the file must be a regular file.Faster async context on Node 22 (xum server)
This section is for people who run xum server on Node 22.7 to 23. The Docker image already does this.
On Node 22, Node implements AsyncLocalStorage with async hooks, and those hooks run on every promise in the backend. Node 22.7 and later have a faster implementation, AsyncContextFrame, behind the --experimental-async-context-frame flag. In a synthetic workload on Node 22.19 (oRPC calls, mock-model streams, and history replays with 6,000 task records), the flag cut backend main-thread CPU by about 18.5%. A lighter workload saved about 9%. It is not a general speedup: the gain depends on how much async work your backend does.
Start the server with the flag (Linux and macOS):
xum server otherwise:
node:internal/async_local_storage/async_hooks and no promiseInitHook frames.
Report slowness
Report slowness saves one local report folder with the data a developer needs to study a slowdown: the flight recorder snapshot, a trace you can open in Perfetto, recent CPU profiles and system details. Xum uploads nothing. You review the folder and decide whether to share it. It needs the Performance flight recorder experiment (turn it on). Run it soon after the slowdown: the snapshot holds only the last 10 minutes. It works in the desktop app and inxum server.
Save a report
- Turn on the flight recorder.
- Reproduce the slowdown, if you can.
-
Run Report slowness from the command palette (Help section), or press its shortcut. See Keyboard shortcuts. From a terminal, you can run:
-
Xum writes the report folder and shows where it is:
- Desktop app: Xum opens the folder in your file manager and shows
Saved slowness report to <folder> and opened it in your file manager. xum serverin a browser: Xum showsSaved slowness report to <folder>.
- Desktop app: Xum opens the folder in your file manager and shows
Report slowness failed.
With the experiment off, the command does not appear and the shortcut does nothing. The shortcut also does nothing while a dialog is open. If you run the command again while Xum writes a report, Xum ignores it.
xum api perf-reports create takes no input. It returns dir (the folder), revealed (the desktop app opened it), includedCaptures, skippedCaptures and totalBytes. Errors:
PRECONDITION_FAILEDwith “Report slowness needs the Performance flight recorder experiment”: turn on the flight recorder.CONFLICTwith “a slowness report is already being written”: wait and try again.
What the report contains
Xum writes the report to~/.xum/perf/reports/<id>/. An id looks like 20261003T013338383Z-1a2b3c4d. The folder is private to your user (mode 0700, files 0600). Xum builds it in a hidden .<id>.partial folder and renames it when it is complete. Each report is at most 100 MiB.
The report never contains session tapes, chat content, prompts, tool payloads, session history or environment variables.
Xum does not delete old reports. Delete folders in
~/.xum/perf/reports/ yourself.
Why a capture was left out
Why a capture was left out
captures/manifest.json gives one reason for each left-out capture:A capture without a profile (a skipped capture) is copied as its
.json metadata only.Review before you share
The report also contains what every snapshot contains (see Privacy and cost): oRPC procedure names and error codes, script URLs, function names, invoker strings, event names and element tag names.environment.json adds the enabled experiments and system versions.
Report a performance problem
Collect these and attach them to the issue:- A Report slowness folder. Review it first.
- Numbers from the flight recorder. Paste excerpts of the snapshot, for example
jq '.trips'and the last samples, not the full file. - Analyzer output for your captures: the leaderboard, or a diff against a good run.
- Without a report: the
.cpuprofilefiles and their.jsonmetadata from~/.xum/perf/captures/. - The
[diag] renderer unresponsivelines from~/.xum/logs/mux.log, for hangs in the desktop app.