Skip to content

Releases: jantimon/web-performance-debugger

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 13:30
c6549b3

Minor Changes

  • #215 85054d1 - Add the allocation gate. A GC-pressure regression that allocates hard but costs little CPU passed every
    gate before; now alloc-diff <baseline> <current> --fail-on-regression gates on net allocated bytes
    (mirroring cpu-diff), and assert --max-alloc-mb <mb> gates the total against a budget. Both read an
    --target node --alloc recording; 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), default max(1 MB, 25%), since sampled byte totals are ~15-20% directional. New view
    AllocDiffResult.

  • #213 d62f653 - assert gains --format json|toon, emitting a typed AssertView: a row per threshold (axis,
    budget, measured value or null, verdict pass/fail/n-a-fail, plus the routed member on a
    run-group), the overall passed, and the violations. 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 json gate 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 clear max(--noise-floor ms, --noise-pct% of the baseline), default max(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 carries noisePct and the effective gateFloorMs.

  • #219 c25e640 - Keep the timing series for repeated named measures in capture order and expose it
    through query 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 through query span --format json|toon.
    The exported SpanTiming type 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 remain null.

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 an IntersectionObserver and the near-empty-DOM signal through
    textContent, instead of getBoundingClientRect/innerText, and the on-ramp inspection runs outside
    the wpd:run window. 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 - Add WPD_INP_ENTRY_WAIT_MS to 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 stale latest now explains itself instead of surfacing a raw ENOENT on 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

Choose a tag to compare

@github-actions github-actions released this 31 Jul 13:38
74ec2c7

Patch Changes

  • #207 0ea5ee9 - Update eslint-plugin-no-comment-slop to 0.2.0, adding its two new rules at error: member comments
    use JSDoc (not //), and every member of a mostly-documented type carries a one-line doc. All
    findings fixed; internal only, no runtime or output change.

v1.3.2

Choose a tag to compare

@github-actions github-actions released this 30 Jul 16:48
af91fa9

Patch Changes

  • #195 62a4bac - query spans overview now carries the exact rendering counts a recording measured, on every row
    including the bar-bearing ones (chrome --breakdown, firefox measure). null keeps 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

Choose a tag to compare

@github-actions github-actions released this 30 Jul 14:21
8bb7c21

Patch Changes

  • #188 fa8f1b5 - AGENTS.md now ships in the npm package, so it lands at
    node_modules/@jantimon/web-performance-debugger/AGENTS.md for a consumer to read. wpd --help ends
    with the absolute paths to the installed AGENTS.md and README.md, so an agent can find and open them.

  • #192 9b79528 - Reorganized examples/: the demos a newcomer runs stay at the top level, and the measurement/
    calibration fixtures move to examples/probes/. No example was renamed; the top-level README.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 --out exists. The group's identity still comes from --group (meta.name), so latest
    and the group name resolve unchanged.

    README: the pnpm caveat now covers pnpm 11 blocking the bin. The ignored-build install error exits 1
    before wpd runs, stopping even the browserless --target node lane; unblock with pnpm approve-builds puppeteer.

  • #190 c9df5b3 - README: a plainer opening line, a reason for the Node 24+ requirement, and a gloss on the 5173
    port. The package description now matches the repo and covers rendering counts and allocation, so all
    three storefronts read the same. Adds an examples/ 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

Choose a tag to compare

@github-actions github-actions released this 30 Jul 12:24
44091a1

Minor Changes

  • #186 52ec7c7 - The react addon now names a hydration mismatch, not just its cost. React's default
    onRecoverableError dispatches a window error event; a mismatch fires one. The pre-app hook counts
    the hydration recoverable errors and stamps hydrationRecoverableErrors + the first message on the
    run span, shown in the React (addon) block of query span run and in --format json. Exact-count
    tier, build-independent (production fires it). An app that supplies its own onRecoverableError
    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 .stack trace) 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 --forced on --breakdown now 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 shows app.jsx:8, not dist/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-dev Performance-Track summary now reports real per-track ms, read from each entry's
      start/end (the instant TimeStamp events carry the span there, not on dur). Tracks nest, so the
      timing is per-track, no grand total.
  • #187 d991821 - README wording: the install line now says it installs the wpd command (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

Choose a tag to compare

@github-actions github-actions released this 30 Jul 10:19
73f1205

Minor Changes

  • #174 8a2497f - diff gains --format json|toon (and the hidden --json alias): the same field-by-field
    comparison, comparability warnings, gated regressions and per-span slice deltas as the human report,
    serialized as a DiffView (a GroupDiffView for a run-group diff). Human output and
    --fail-on-regression exit 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; --help and
    record --help gain quick-start epilogs; query events --kind lists the missing gc; query get
    accepts the shared --json alias; --breakdown/--deep help notes their mutual exclusivity and
    where to read the result.

  • #176 20e64af - Surface framework facts without drilling:

    • query spans overview rows now carry a compact addons.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 on query span.
    • Recordings now stamp meta.framework ("off" | "auto"), so a deliberate --framework off run is distinguishable from an auto run that detected no framework (both carry no Span.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 --breakdown forced-blame count is a sampling-frequency signal, never comparable to
    --deep's exact flush count. The built-in --url load flow stamps a meta.notes entry (and tags the
    query span run 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 include CaptureMode, 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 driver StepOpts/Until.

    Narrowed the closed-union output fields from string to their real unions: RecordingMeta.capture and GroupSpanMember.mode to the capture-mode union, and SpansResult/SpanAnatomy/GroupSpanStitch target to "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 trusted page.click carries ~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 spans now 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 frameFloor JSON field carries a basis discriminator: { 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 as FrameFloor (union) with WallMultipleFloor/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 past assert --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.

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 20:47
c939d68

Minor Changes

  • #166 6e84b40 - Clearer errors and a tidier query spans contract.

    • A module missing its run export 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 at query events.
    • JSON contract: query span's forced[] read-sites move from the at: "file:line:col" string to structured { source, line, column }, matching query blame --forced. query spans rows gain a compact frameFloor: { floorMs, multiple } when the wall is frame-floor dominated, so a consumer reads flooring off the overview instead of recomputing it.
  • #168 21b14bd - React framework addon, opt-out via --framework off|auto (default auto):

    • 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 --deep dev builds (Span.addons["react-dev"]), classified from the TimeStamp events wpd already stores. A production browser build emits none, so this is absent there.

    --framework off runs 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 span shows a labeled React (addon) block; --format json adds an addons object. New public types: ReactFacts, ReactDevFacts, SpanAddons, FrameworkMode.

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 19:08
b86c89e

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 summary object is gone. The run-level counts, wall, INP, longest-task duration and per-iteration stats now live on the run span (spans[] with kind: "run"); each driver step carries its own on its step span. Read counts from run.counts, timing from run.wallMs/run.inpMs/run.perIteration/run.stats.
    • Wall fields are named by clock: a span's page-clock headline stays wallMs (with wallClock: "page" | "trace" on every span whose wall is set), while the trace-clock window a reconciling bar tiles is breakdown.wallMs.
    • meta.passes: string[] is now meta.capture: string (the one capture mode).
    • meta.driver and meta.runtime are removed; derive them from meta.workload.lane ("driver"/"builtin-load" are driver mode, "node" is the node runtime).
    • summary.jsSelfMs moved to meta.jsSelfMs; summary.totalEvents moved to meta.totalEvents.

    query … --format json consumers: the step SpanEntry/SpanAnatomy field breakdownWallMs is renamed to windowMs; SpanCounts gains paintInvalidations.

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, the window._cf_chl_opt page global, and a __cf_chl_rt_tk document 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.browserVersion stamps the resolved engine build (chrome/firefox browser.version(), node process.version) as { raw, milestone }, and a new browser-version comparability axis WARNS (never blocks) when two recordings' milestones differ: exact counts survive a bump, directional numbers do not.
    • meta.botWall carries the detection verdict as structured data when --allow-bot-wall measured a challenge page anyway.
    • query span --format json gains softNavAgreement (the classifier-vs-engine soft-nav reconciliation, previously human-report-only); engineSoftNav is already emitted.
    • Site relation now tags a --url run'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.
  • #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 in step.softNav, keyed by the soft nav's navigationId and anchored to the route clock: routeLcp (tag/url/size, routeMs into the route), routeCls (the post-route shifts, spec session-window max), and routeInpMs/routeInteraction (the worst interaction after the route; the triggering click keeps the pre-nav id and stays in the step's main inp). 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, so softNav is absent, never a fabricated 0. No new flags; schema stays 5.

v0.23.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 15:43
e8da905

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:col string under at. Split at yourself only if you still need the joined form.

    Add bot-wall detection: when wpd's own navigation (the built-in --url load flow, a --url host 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-wall measures it anyway, with a
    loud note.

    Add siteRelation (same-origin | same-site | cross-site) on query cpu origin buckets of a
    --url run, 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 --url flow 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 carries engineSoftNav
    (count, navigationTypes, and the numeric ids) from an in-page soft-navigation observer. It is
    opportunistic: wpd never forces --enable-features, so an older Chrome or Firefox records nothing, and
    absence is never a fabricated 0.

    query span reconciles 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 --alloc runs V8's heap sampler (GC-inclusive) around your run() loop and attributes allocated bytes
    to source/package, answering "which dependency allocates". Read it with the new query 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 --alloc recording carries no CPU model; query cpu/cpu-diff on one
    point you at query alloc. Byte shares/ratios are trustworthy (~5%); the absolute total is directional
    (~10-20%).

  • 875c448: Boot LCP is now per-iteration sampled: under --iterations N the load step's lcp grows
    perIteration (the render-time series, null for an iteration that fired no entry, never 0) and
    stats (min/median/max), the same shape wall carries, so a run-to-run LCP swing is visible instead
    of hidden behind one number. query span prints 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, hadRecentInput shifts excluded), not a raw
    sum, with the top shifting elements attributed (tag#id, rect deltas). Scoped to the step's own
    window; Firefox has no layout-shift entry 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-gpu forcing lever now
    produces frames cleanly), so the --disable-gpu default stays as belt-and-braces; boot-LCP delivery
    still recovers within its bounded budget, with no missing-entry or 60 s-startTime anomaly; and a
    cross-origin LCP without Timing-Allow-Origin now reports a coarsened renderTime (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 sets meta.mainThread.split (so
    assert / diff --fail-on-regression refuse 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 left split false, so assert --max-layouts gated
    green on the first page's counts alone. Keep each run to one navigation for counts that cover all of it.

v0.22.0

Choose a tag to compare

@github-actions github-actions released this 28 Jul 16:30
ae5e1ed

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 forcedLayoutMs is now honestly not-measured (—), never the misleading number: the
      markers under-report the forced subset ~7x. Forced COUNTS are unchanged; read the bar's layout
      slice for total layout ms.
    • Cross-engine CPU self-time copy scoped to pure-JS / --target node work (reflow-heavy Firefox
      self-time carries a per-reflow marker tax, 1.5-3x).
    • query span now 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 a frameFloor {floorMs, multiple} field on the
      --format json view so consumers can detect flooring programmatically.
    • record failures now lead with the cause on every path, with the WPD_DEBUG hint trailing on the
      same line, so the last stderr line always names the actual error.

Patch Changes

  • 0d3899d: cpu-diff --fail-on-regression no longer fires the JS-self gate when BOTH recordings' jsSelfMs sit
    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.