Skip to content

Perf profile

STANDARDIZED PERFORMANCE PROFILE: drives scripted segments through the main gameplay regimes in headless Chrome or Edge (real GPU) and writes one metrics file per segment, so two runs are comparable number for number.

Start the dev server first (npm run dev, Vite’s default port 5173), then:

node scripts/perf-profile.mjs --out <dir> [--label baseline]
[--url http://localhost:5173/]
[--segments home-town-walk,body-swap]
[--profileJson scripts/perf-profiles/<file>.json]
[--size 3840x2160] [--shots] [--audio]
[--warm [--profile <chrome-user-data-dir>]]
--out output root (default: <os temp>/perf-profile)
--label run name; files go to <out>/<label>/ (default: run)
--url game URL; `diag&noenter&nointro` is always appended
--segments comma-separated subset of segment names
--profileJson segment file (default scripts/perf-profiles/default.json:
home-idle, home-town-walk, home-surface-flight,
space-approach, arrival-settle, luna-approach,
luna-arrival-settle, body-swap, luna-surface-flight)
--size window size WxH (default 1920x1080; render targets scale
with it, so a 4K question needs a 4K run)
--shots PNG screenshot at the end of each segment
--audio allow autoplay so the AudioContext (and voice) can run
--warm reuse a shared Chrome profile (warm shader cache AND the
previous run's autosave); --profile names that directory.
The default is a fresh profile per run, because the game
autosaves and a reused profile starts where the last run
ended. With --warm, read <segment>.where.json to see where
each segment actually ran.

WHY A SCRIPTED PROFILE. A hand-flown capture measures the pilot as much as the build: the scene evolves on its own (weather, orbits, streaming), so captures taken minutes apart differ by more than the change under test. Every segment starts from a deterministic pose set by console hooks, replays a fixed key script for a fixed wall time, and reports its own profiler window, so the same segment in two runs saw the same work.

SEGMENT FIELDS (JSON array of objects):

name, seconds, settleSec timed window length and untimed settle
setup: [expr...] console expressions: teleports, body swaps,
flag flips
expectBody, arriveTimeoutSec wait until __where() reports this body with
no swap pending (warns if it never arrives)
setupAfterArrive: [expr...] run once the swap has landed; anything that
poses the player belongs here
preKeys, keys: [[atSec, "down"|"up", "KeyW"]...]
preKeys fire before the settle, keys inside
the timed window
sampleEverySec, watch periodic samples into <segment>.series.json
(the profiler ring holds only 240 frames)
spin: [startSec, endSec, pxPerSec] right-button camera drag
teardown: [expr...] state probes run after the dump
ab: { arm: [expr...] }, repeats, windowSec, armSettleSec, windowKeys,
windowSpin: [[startSec, endSec, pxPerSec]...]
interleaved A/B inside one page life, with
the arm order rotated every repeat; windowKeys
and windowSpin (a right-button camera drag, a
leg may be negative) replay inside each window

The profiler is reset after the settle, so the window measures the steady regime (a segment with settleSec 0 measures the transition itself).

OUTPUT in /

<segment>.json npcProfiler dump: CPU spans (mean/p95/p99/max),
counters, gauges, gpu_times_ms, the cpu.frame and
cpu.frameWall histograms, and the spike ring
<segment>.where.json pose at the start and end of the window
<segment>.series.json periodic samples (sampleEverySec only)
<segment>.teardown.json full teardown probe values (teardown only)
<segment>.ab.json paired A/B windows (ab segments only)
<segment>.png screenshot (--shots only)
boot.json boot timing and phase attribution
summary.json the cross-segment table
console.log the whole session, boot marks included

Never add __cfg('timeSpeed', 0) to a setup list. Freezing the clock stops the weather simulation too, which flattens the sea and reads as a speedup that is really a frozen scene. To hold the sun for comparable shots, pin the time of day per window instead.

Written by release:site-sync from scripts/perf-profile.mjs at v2026.09.24. Edit the engine document, not this page.