diff --git a/.changeset/span-timing-samples.md b/.changeset/span-timing-samples.md new file mode 100644 index 0000000..4c135b8 --- /dev/null +++ b/.changeset/span-timing-samples.md @@ -0,0 +1,8 @@ +--- +"@jantimon/web-performance-debugger": minor +--- + +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`. diff --git a/README.md b/README.md index a75b242..1c99c4a 100644 --- a/README.md +++ b/README.md @@ -1148,6 +1148,28 @@ split lives at `byPackage[]` (`key`/`selfMs`/`selfPct`, plus `siteRelation` on a For per-span numbers, `query spans --format json` gives one `UnifiedSlices` shape (`js.byPackage`, `style`, `layout`, …) across chrome/firefox/node — read that, never the multi-MB recording. +Read a run or driver step's timing samples through `query span`: + +```bash +wpd query span latest run --format json | jq '.timing' +``` + +`SpanAnatomy.timing` is a `SpanTiming` block with `samplesMs` in capture order, +`stats` (min/median/mean/max), `sampleUnit: "iteration"`, a `boundary` of `run-call` +or `driver-step`, and a `clock` of `page`, `trace`, or `null` when unspecified. +The `page` clock uses `performance.now()` in a browser page or Node runtime. +Unmeasured step repetitions are omitted, so the sample count can differ from the +requested iteration count. Statistics use the stored samples and are `null` below +two samples. The whole block +is `null` when no valid sample series is stored; WPD does not infer samples from a +profile window or an aggregate wall. Capture overhead still applies to these timings. + +These samples measure each call or step, independently of the window that the +profile bar covers. Keep `timing.stats.medianMs` distinct from `wallMs` and +`windowMs`. For a run-group, each `members[]` entry carries its own `timing` block; +WPD does not combine samples from different captures. `query spans` stays a compact +overview; drill into `query span` for the sample series. + Recordings are self-describing: `meta.schemaVersion` stamps the on-disk schema epoch (currently `"5"`), and a reader **rejects** any artifact from another epoch with a "recorded by an older wpd; re-record" message rather than mis-parsing it into silent nulls. Numbers are rounded to 4 decimals on diff --git a/scripts/pack-smoke.mjs b/scripts/pack-smoke.mjs index 98a56eb..e02890e 100644 --- a/scripts/pack-smoke.mjs +++ b/scripts/pack-smoke.mjs @@ -110,6 +110,7 @@ try { "SpanEntry", "UnifiedSlices", "SpanAnatomy", + "SpanTiming", "CpuOverview", "FrameQueryResult", "BlameEntry", diff --git a/src/commands/query.ts b/src/commands/query.ts index 0e12574..9a18499 100644 --- a/src/commands/query.ts +++ b/src/commands/query.ts @@ -39,6 +39,7 @@ import { blameRowLowConfidence, } from "../model/capture-mode.js"; import { runSpan } from "../model/span.js"; +import { spanTiming } from "../model/span-timing.js"; import { dim } from "../output/color.js"; import { num, table, middleEllipsis, SOURCE_COL_MAX } from "../output/ascii.js"; import { analyzeThrash } from "../trace/thrash.js"; @@ -382,6 +383,7 @@ function buildSpanAnatomy( return { recording: recordingPath, + timing: spanTiming(span), target, label: span.label, kind: span.kind, @@ -619,6 +621,7 @@ async function buildGroupSpanStitch( */ mode: member.mode as CaptureMode, ...(member.variant ? { variant: member.variant } : {}), + timing: anatomy.timing, wallMs: anatomy.wallMs, aggregation: anatomy.aggregation, iterations: anatomy.iterations, diff --git a/src/index.ts b/src/index.ts index ec07d14..c4b00c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,6 +106,7 @@ export type { SpanForced, SpanHotFunctions, SpanAnatomy, + SpanTiming, GroupSpanMember, GroupSpanSources, GroupSpanStitch, diff --git a/src/model/query.ts b/src/model/query.ts index ea5dd58..7bfbc4b 100644 --- a/src/model/query.ts +++ b/src/model/query.ts @@ -3,6 +3,7 @@ // call sites can be annotated and the JSON contract cannot silently drift import type { + BenchStats, CpuBreakdown, CpuFunction, CpuGroupStat, @@ -554,6 +555,20 @@ export interface SpanHotFunctions { functions?: (Omit & { totalMs?: number })[]; } +/** Recorded timing samples and the scope each sample measures */ +export interface SpanTiming { + /** Recorded call or step durations in capture order; unmeasured steps are omitted */ + sampleUnit: "iteration"; + /** Start/end scope of each sample; excludes work outside these calls or step marks */ + boundary: "run-call" | "driver-step"; + /** `page` denotes performance.now() in the page or Node runtime; null means unspecified */ + clock: "page" | "trace" | null; + /** Recorded durations in milliseconds; independent of the profiled bar window */ + samplesMs: number[]; + /** Statistics over these samples; null for fewer than two samples */ + stats: BenchStats | null; +} + /** * `query span