Releases: jantimon/web-performance-debugger
Release list
v1.5.0
Minor Changes
-
#215
85054d1- Add the allocation gate. A GC-pressure regression that allocates hard but costs little CPU passed every
gate before; nowalloc-diff <baseline> <current> --fail-on-regressiongates on net allocated bytes
(mirroringcpu-diff), andassert --max-alloc-mb <mb>gates the total against a budget. Both read an
--target node --allocrecording; a recording with no allocation model is a loud n/a-FAIL, never a
silent pass, and an alloc-diff refuses across an incompatible workload/lane/capture.The gate floor scales with the workload, the same as
cpu-diff:max(--noise-floor MB, --noise-pct% of the baseline), defaultmax(1 MB, 25%), since sampled byte totals are ~15-20% directional. New view
AllocDiffResult. -
#213
d62f653-assertgains--format json|toon, emitting a typedAssertView: a row per threshold (axis,
budget, measuredvalueornull,verdictpass/fail/n-a-fail, plus the routedmemberon a
run-group), the overallpassed, and theviolations. A CI PR-comment script now consumes the gate
verdict structurally instead of scraping the ASCII table. The exit code is unchanged (0 = passed, 1 =
any failed), so a--format jsongate fails the build exactly as the human report does. -
#212
1af338b-cpu-diff --fail-on-regression: the noise floor now scales with the workload. The net JS self-time
must clearmax(--noise-floor ms, --noise-pct% of the baseline), defaultmax(0.5 ms, 15%). The old
fixed 0.5 ms floor false-reds byte-identical code more as the workload grows (measured ~2% at a 5 ms
workload, ~40% at 220 ms /--iterations 20), because summed self-time grows while the floor stays
absolute; the percentage term tracks it, so identical code stays green at any iteration count while a
30%+ regression on a small workload still fails.New flags
--noise-floor <ms>and--noise-pct <n>widen (or tighten) the two terms;CpuDiffResult
JSON now carriesnoisePctand the effectivegateFloorMs. -
#219
c25e640- Keep the timing series for repeated named measures in capture order and expose it
throughquery span --format json|toon. The timing block uses
sampleUnit: "occurrence", since one iteration can produce several measures with the same name.
The profile bar keeps its actual lower-median occurrence; its slices are not averaged. -
#218
f230801- Expose run and driver-step timing samples throughquery span --format json|toon.
The exportedSpanTimingtype names each sample's clock and boundary and keeps
measured statistics separate from the profiled bar window. Run-group members carry
their own timing blocks. Missing samples remainnull.
Patch Changes
-
#209
ec6a66e- Bot-wall detection no longer forces a layout flush while it inspects a page. The collector reads
iframe viewport coverage through anIntersectionObserverand the near-empty-DOM signal through
textContent, instead ofgetBoundingClientRect/innerText, and the on-ramp inspection runs outside
thewpd:runwindow. On a page that keeps layout dirty this drops a few spurious layout/style counts
that the inspection previously added to the run span. Detection results are unchanged. -
#211
2b6eaef- AddWPD_INP_ENTRY_WAIT_MSto raise the bounded in-page drain that a step waits for a trusted
interaction's Event Timing entry. The default stays 250ms; a genuinely slow host, where the entry's
task slips later, can extend it (whole ms) so per-step INP still lands. No change to default behaviour
or output. -
#217
f541c48- A stalelatestnow explains itself instead of surfacing a rawENOENTon an internal path. When the
artifact the pointer names has been deleted, every consumer verb says which one is gone (recording,
CPU/allocation profile, or run-group manifest), shows its path the way the reports do, and names the
fix: record again, or pass an explicit path. A deleted run-group member names the member.
v1.4.1
v1.3.2
Patch Changes
- #195
62a4bac-query spansoverview now carries the exact rendering counts a recording measured, on every row
including the bar-bearing ones (chrome--breakdown, firefox measure).nullkeeps meaning
not-measured, never not-projected, so the overview no longer reads as "not-measured" for a count the
drill (query span) shows measured.
v1.3.1
Patch Changes
-
#188
fa8f1b5- AGENTS.md now ships in the npm package, so it lands at
node_modules/@jantimon/web-performance-debugger/AGENTS.mdfor a consumer to read.wpd --helpends
with the absolute paths to the installed AGENTS.md and README.md, so an agent can find and open them. -
#192
9b79528- Reorganizedexamples/: the demos a newcomer runs stay at the top level, and the measurement/
calibration fixtures move toexamples/probes/. No example was renamed; the top-levelREADME.md
now maps each demo to the question it answers. Docs and tests point at the new probe paths. -
#193
49636ad-record --members ... --out <path>now names the group's manifest and member recordings from--out's
basename (before, only its directory was used and the basename was silently dropped), so a path a caller
derives from--outexists. The group's identity still comes from--group(meta.name), solatest
and the group name resolve unchanged.README: the pnpm caveat now covers pnpm 11 blocking the bin. The ignored-build install error exits 1
beforewpdruns, stopping even the browserless--target nodelane; unblock withpnpm approve-builds puppeteer. -
#190
c9df5b3- README: a plainer opening line, a reason for the Node 24+ requirement, and a gloss on the5173
port. The package description now matches the repo and covers rendering counts and allocation, so all
three storefronts read the same. Adds anexamples/map pointing newcomers at the three main demos. -
#191
2af855f- README/docs wording: plain words replace "provenance"; the Node and install lines lose their
explanations.
v1.3.0
Minor Changes
- #186
52ec7c7- Thereactaddon now names a hydration mismatch, not just its cost. React's default
onRecoverableErrordispatches a windowerrorevent; a mismatch fires one. The pre-app hook counts
the hydration recoverable errors and stampshydrationRecoverableErrors+ the first message on the
run span, shown in theReact (addon)block ofquery span runand in--format json. Exact-count
tier, build-independent (production fires it). An app that supplies its ownonRecoverableError
suppresses the event, so an absent count is not proof of clean hydration.
Patch Changes
-
#185
b0f5861- Docs: AGENTS.md tightened -- same rules, half the words. The counterintuitive-field reference is
delegated to the README's Consuming-the-JSON section, and the shape is now a documented decision in
docs/dev/agents-md.md. -
#184
4044c14- Docs: restructure the README for a first-time reader. Install command and requirements now sit at the
top, a Troubleshooting index maps each common failure to the section that fixes it, and cold terms
(CLS, TOON,base::TimeTicks, the.stacktrace) are glossed on first use. -
#181
8b2b478- Release job no longer fails when the current version is already on npm. A push to main with no pending
changesets re-attempts the current version; the publish step now treats "already published at this
version" as a no-op success and fails only on a genuine publish error. -
#182
912538e- Three React-debugging fixes:query blame --forcedon--breakdownnow resolves a forcing read to source. A sampled read-site
whose executing line falls on a minified bundle line falls back to the leaf function's own column
(the frame the CPU model resolves), so a bundled app showsapp.jsx:8, notdist/app.js:9.query span <step>no longer prints "React (addon): not detected" on a step span. Detection is a
run-level fact; a step shows its commit count alone. JSON stays honest (detection absent, not fake).- The
react-devPerformance-Track summary now reports real per-track ms, read from each entry's
start/end(the instant TimeStamp events carry the span there, not ondur). Tracks nest, so the
timing is per-track, no grand total.
-
#187
d991821- README wording: the install line now says it installs thewpdcommand (long form
web-performance-debugger) instead of "bins", and the "at a glance" contrast names a page score like
Lighthouse instead of an unexplained "scorecard".
v1.2.0
Minor Changes
-
#174
8a2497f-diffgains--format json|toon(and the hidden--jsonalias): the same field-by-field
comparison, comparability warnings, gated regressions and per-span slice deltas as the human report,
serialized as aDiffView(aGroupDiffViewfor a run-group diff). Human output and
--fail-on-regressionexit codes are unchanged. New exported types:DiffView,DiffMetricRow,
GroupDiffView,DiffOutput.CLI help polish: the top-level tagline names all three targets and CPU self-time;
--helpand
record --helpgain quick-start epilogs;query events --kindlists the missinggc;query get
accepts the shared--jsonalias;--breakdown/--deephelp notes their mutual exclusivity and
where to read the result. -
#176
20e64af- Surface framework facts without drilling:query spansoverview rows now carry a compactaddons.react(version+build), so a bulk consumer reads framework identity off the overview instead of opening each span. The full per-span facts (commit counts, server phases) stay onquery span.- Recordings now stamp
meta.framework("off" | "auto"), so a deliberate--framework offrun is distinguishable from anautorun that detected no framework (both carry noSpan.addons).
-
#177
1bb07a4- Three polish fixes: README links to non-shipped repo files (AGENTS.md, docs/, ...) are now absolute
GitHub URLs, so they resolve on the npm page and in an installed package. The blame docs now state
that a sampled--breakdownforced-blamecountis a sampling-frequency signal, never comparable to
--deep's exact flush count. The built-in--urlload flow stamps ameta.notesentry (and tags the
query spanrun line) when the boot did near-zero work, the tell of a consent/region shell measured
in place of the app; note-tier only, never a gate. -
#172
dc97fda- Complete the public type surface so every type named inside an exported type is itself importable (no more hand-rolling a shape the package already describes). New root exports includeCaptureMode,TargetLane,WorkloadLane,Measured,CpuBreakdown/CpuSlice/CpuJsSlice,LayoutShift(+Source/Rect),EngineSoftNav,SoftNavRoute(+Lcp)/SoftNavVerdict/SoftNavAgreement,ThrashReport/ThrashStep/DirtiedByWrite/DirtiedByWriteRollup/FirefoxDirtiedByReport,WorkloadIdentity,SourceMapDiagnostics/SourceMapFailure,FrameFloor/WallMultipleFloor/WorkSignalFloor,SpanCountsEntry,RawProfileNode/RawCallFrame/GeckoSlice, and the driverStepOpts/Until.Narrowed the closed-union output fields from
stringto their real unions:RecordingMeta.captureandGroupSpanMember.modeto the capture-mode union, andSpansResult/SpanAnatomy/GroupSpanStitchtargetto"chrome" | "firefox" | "node". Reading these fields now yields the exact literal type instead of a bare string. -
#175
11173d5- Label floored trusted-click driver steps on--breakdown. A trustedpage.clickcarries ~8ms of
input dispatch inside the step window, so a floored cheap step's wall (~41ms) lands off any exact
frame multiple and the old wall-multiple check missed it.query span/query spansnow read a
step's flooring off its reconciling bar (sub-frame real work in an idle-dominated window), so a
sub-frame interaction is no longer read as real work.The
frameFloorJSON field carries abasisdiscriminator:{ basis: "wall-multiple", floorMs, multiple }for a bench/in-page/measure wall and INP, or{ basis: "work-signal", floorMs, workMs }
for a driver step. Exported asFrameFloor(union) withWallMultipleFloor/WorkSignalFloor.
Patch Changes
-
#170
8ab7390- Driver reliability fixes:- INP no longer under-reports on a slow environment. The end-of-step flush now drains every in-page observer's
takeRecords()(INP, LoAF, layout-shift, LCP, soft-nav) before reading, and waits (bounded) for the Event Timing entry on a step that dispatched a trusted interaction. An entry queued-but-undispatched at the read instant was silently lost, reading INP lower and letting a real regression slip pastassert --max-inp. - A mid-step hard navigation no longer hard-fails the record. When a step's action triggers a navigation that commits during the default settle, the settle re-attaches to the new document instead of dying with "Execution context was destroyed".
- SIGINT/SIGTERM/SIGHUP now clean up. A killed run SIGKILLs its Chrome process and unlinks its temp files instead of orphaning them, then re-raises the signal.
- INP no longer under-reports on a slow environment. The end-of-step flush now drains every in-page observer's
v1.1.0
Minor Changes
-
#166
6e84b40- Clearer errors and a tidierquery spanscontract.- A module missing its
runexport now gets a message naming the module and the one-line fix (export async function run(ctx) { ... }), instead of the placeholder'run' / 'run' export. - Every verb honors
WPD_DEBUG=1: the message always prints, and the full stack follows when the env is set. The one-line message trails the(set WPD_DEBUG=1 ...)hint so a caller reading only the last stderr line still sees the real error. - A trace wpd cannot parse now says so as a capture fault ("re-record; file an issue"), keeping the mechanical detail.
query get <id>on a missing id points atquery events.- JSON contract:
query span'sforced[]read-sites move from theat: "file:line:col"string to structured{ source, line, column }, matchingquery blame --forced.query spansrows gain a compactframeFloor: { floorMs, multiple }when the wall is frame-floor dominated, so a consumer reads flooring off the overview instead of recomputing it.
- A module missing its
-
#168
21b14bd- React framework addon, opt-out via--framework off|auto(defaultauto):- Detection + commit counts on the browser lanes (dev and production alike): React present/version/renderer/build from a pre-load hook, plus an exact per-step commit count. Ride the run span and each step span under
Span.addons.react. - Node-lane server phases (
--target node): react-dom self-time rolled onto the stable server-phase anchors. React 19 production resolves them; React 18 production is mangled, so the fact is honestly absent. - React Performance Tracks on chrome
--deepdev builds (Span.addons["react-dev"]), classified from theTimeStampevents wpd already stores. A production browser build emits none, so this is absent there.
--framework offruns zero addon code and leaves the recording unchanged. All React logic lives behind one registry interface the core never imports through; addons only read what the capture recorded.query spanshows a labeledReact (addon)block;--format jsonadds anaddonsobject. New public types:ReactFacts,ReactDevFacts,SpanAddons,FrameworkMode. - Detection + commit counts on the browser lanes (dev and production alike): React present/version/renderer/build from a pre-load hook, plus an exact per-step commit count. Ride the run span and each step span under
v1.0.0
Major Changes
-
#160
b358b32- Breaking: schema 5. Recordings from earlier versions refuse to open with a re-record message. Re-record any stored baselines after upgrading. CLI commands and flags are unchanged, and measurement semantics are unchanged.Raw-JSON (recording) consumers:
- The
summaryobject is gone. The run-level counts, wall, INP, longest-task duration and per-iteration stats now live on the run span (spans[]withkind: "run"); each driver step carries its own on its step span. Read counts fromrun.counts, timing fromrun.wallMs/run.inpMs/run.perIteration/run.stats. - Wall fields are named by clock: a span's page-clock headline stays
wallMs(withwallClock: "page" | "trace"on every span whose wall is set), while the trace-clock window a reconciling bar tiles isbreakdown.wallMs. meta.passes: string[]is nowmeta.capture: string(the one capture mode).meta.driverandmeta.runtimeare removed; derive them frommeta.workload.lane("driver"/"builtin-load"are driver mode,"node"is the node runtime).summary.jsSelfMsmoved tometa.jsSelfMs;summary.totalEventsmoved tometa.totalEvents.
query … --format jsonconsumers: the stepSpanEntry/SpanAnatomyfieldbreakdownWallMsis renamed towindowMs;SpanCountsgainspaintInvalidations. - The
Minor Changes
-
#164
5c7857b- Dogfood remediation and a CI-gap close:- Cloudflare inline managed challenge is now detected: its same-origin
/cdn-cgi/challenge-platform/script, thewindow._cf_chl_optpage global, and a__cf_chl_rt_tkdocument token are strong signals, so a "Just a moment" interstitial no longer measures as the site. An embedded cross-origin Turnstile widget still passes. meta.browserVersionstamps the resolved engine build (chrome/firefoxbrowser.version(), nodeprocess.version) as{ raw, milestone }, and a newbrowser-versioncomparability axis WARNS (never blocks) when two recordings' milestones differ: exact counts survive a bump, directional numbers do not.meta.botWallcarries the detection verdict as structured data when--allow-bot-wallmeasured a challenge page anyway.query span --format jsongainssoftNavAgreement(the classifier-vs-engine soft-nav reconciliation, previously human-report-only);engineSoftNavis already emitted.- Site relation now tags a
--urlrun's resolved remote packages/files (not just unmapped origin buckets), from the script origin they resolved from; a mixed-origin bucket stays untagged. - wpd's own bot-wall probe frame no longer buckets in
query cpu. - New README "Running wpd in CI" section (cache the pinned browser; when a preinstalled browser is safe). Puppeteer is pinned exactly.
- Cloudflare inline managed challenge is now detected: its same-origin
-
#163
6667b93- Per-soft-step route web vitals (Chrome 151+). When a driver step soft-navigates and Chrome's heuristic fires, the step now carries the route transition's own LCP-equivalent, CLS, and INP instep.softNav, keyed by the soft nav'snavigationIdand anchored to the route clock:routeLcp(tag/url/size,routeMsinto the route),routeCls(the post-route shifts, spec session-window max), androuteInpMs/routeInteraction(the worst interaction after the route; the triggering click keeps the pre-nav id and stays in the step's maininp).query span <step>prints them under the step. Opportunistic and additive: a programmatic or untrusted-click route, older Chrome, and Firefox/node fire no engine entry, sosoftNavis absent, never a fabricated 0. No new flags; schema stays 5.
v0.23.0
Minor Changes
-
32df428: Breaking (
query blame --format json): each read-site row is now a structured location
({ source, line?, column?, ... }) matching the human table's columns, instead of a single
file:line:colstring underat. Splitatyourself only if you still need the joined form.Add bot-wall detection: when wpd's own navigation (the built-in
--urlload flow, a--urlhost page)
lands on a bot-challenge interstitial (Cloudflare, DataDome, hCaptcha, PerimeterX, Arkose),record
refuses before measuring — non-zero exit, an evidence-listed error, and a<recording>.wall.png
screenshot — rather than reporting the challenge page as the site. Detection is conservative (rendered
interstitial only, never a captcha script a form embeds).--allow-bot-wallmeasures it anyway, with a
loud note.Add
siteRelation(same-origin|same-site|cross-site) onquery cpuorigin buckets of a
--urlrun, via the public-suffix list. It is a URL-mechanical fact, never an ownership or
"third-party" claim (a cross-site CDN can be first-party-owned).The built-in
--urlflow now names the failure class (navigation timeout, HTTP/2 reset,
context-destroyed) and points at the driver-module escape hatch: wpd retries its own machinery's races,
never the site's refusals. -
ac00852: Record Chrome's own soft-navigation verdict on a driver step, beside the url+timeOrigin classifier. On
a Chrome that ships the Soft Navigations API (151+, default-on) a step carriesengineSoftNav
(count,navigationTypes, and the numeric ids) from an in-pagesoft-navigationobserver. It is
opportunistic: wpd never forces--enable-features, so an older Chrome or Firefox records nothing, and
absence is never a fabricated 0.query spanreconciles the two verdicts: where the classifier reads a step "soft" but the engine fired
no entry (a programmatic history change, an untrusted click, or no qualifying paint), it notes both and
picks no winner, so a route the engine's metrics miss is visible rather than hidden. -
bcb0a9b: Add
--alloc: a node-lane allocation-attribution capture mode.wpd record <module> --target node --allocruns V8's heap sampler (GC-inclusive) around yourrun()loop and attributes allocated bytes
to source/package, answering "which dependency allocates". Read it with the newquery alloc --by package|file|function. It is a dedicated mode with the CPU sampler OFF (a co-riding heap sampler
inflates CPU self-time), so an--allocrecording carries no CPU model;query cpu/cpu-diffon one
point you atquery alloc. Byte shares/ratios are trustworthy (~5%); the absolute total is directional
(~10-20%). -
875c448: Boot LCP is now per-iteration sampled: under
--iterations Ntheloadstep'slcpgrows
perIteration(the render-time series,nullfor an iteration that fired no entry, never 0) and
stats(min/median/max), the same shapewallcarries, so a run-to-run LCP swing is visible instead
of hidden behind one number.query spanprints the spread; the identity fields stay a real sample.Add per-step CLS: a driver step carries
layoutShift(Chrome only) — the spec session-window maximum
(session windows gap-capped at 1s / window-capped at 5s,hadRecentInputshifts excluded), not a raw
sum, with the top shifting elements attributed (tag#id, rect deltas). Scoped to the step's own
window; Firefox has nolayout-shiftentry type, so it is absent there, never a fake 0. Both are
additive fields (schema stays 4).
Patch Changes
- e745844: Move the bundled browser to Chrome 151 (Puppeteer 25.4.0), so CI, e2e, and a fresh install all run
one browser. Re-probed the load-bearing headless facts on 151: the one-frame floor (16.7 ms / 60 Hz)
is unchanged; the GPU frame-sink stall no longer reproduces (its--in-process-gpuforcing lever now
produces frames cleanly), so the--disable-gpudefault stays as belt-and-braces; boot-LCP delivery
still recovers within its bounded budget, with no missing-entry or 60 s-startTimeanomaly; and a
cross-origin LCP withoutTiming-Allow-Originnow reports a coarsenedrenderTime(more data, not
wrong data). Soft-navigation entry types (soft-navigation,interaction-contentful-paint) are
present by default in the measured browser. - db12e7a: Lower the cross-process split-detection floor so a lighter second navigation is not silently
uncounted. A run that navigates across renderer processes now setsmeta.mainThread.split(so
assert/diff --fail-on-regressionrefuse count gates) once the second navigation renders at least
5% of the busiest thread's layout/paint -- the same husk share the re-anchor uses. A second navigation
doing 5-24% of the first page's work previously leftsplitfalse, soassert --max-layoutsgated
green on the first page's counts alone. Keep each run to one navigation for counts that cover all of it.
v0.22.0
Minor Changes
-
126cc29: Firefox honesty pass, plus a frame-floor fix:
- Firefox frame floor corrected to the measured 16.6 ms (was 8.3 ms): the 8.3 ms / 120 Hz reading
is display-contingent, so on CI and idle-panel hosts Firefox sits on the same ~60 Hz floor as Chrome. - Firefox
forcedLayoutMsis now honestly not-measured (—), never the misleading number: the
markers under-report the forced subset ~7x. Forced COUNTS are unchanged; read the bar'slayout
slice for total layout ms. - Cross-engine CPU self-time copy scoped to pure-JS /
--target nodework (reflow-heavy Firefox
self-time carries a per-reflow marker tax, 1.5-3x). query spannow annotates a wall/INP sitting on n× the frame floor (n up to 4) when the window
is wait-dominated, and exposes the match as aframeFloor {floorMs, multiple}field on the
--format jsonview so consumers can detect flooring programmatically.recordfailures now lead with the cause on every path, with theWPD_DEBUGhint trailing on the
same line, so the last stderr line always names the actual error.
- Firefox frame floor corrected to the measured 16.6 ms (was 8.3 ms): the 8.3 ms / 120 Hz reading
Patch Changes
- 0d3899d:
cpu-diff --fail-on-regressionno longer fires the JS-self gate when BOTH recordings'jsSelfMssit
below the sampler's resolving floor (~10 samples, ~2ms at the 200us interval, derived from each side's
recorded interval so the larger wins). Below resolving power a net delta is sampler quantization, not a
code change, so two identical near-zero runs now gate green; the output (human and JSON) carries a
disclosure note and the exit stays 0 unless another gated axis fires.