diff --git a/doc/plans/2026-11-03-native-event-ingestion.md b/doc/plans/2026-11-03-native-event-ingestion.md index 6eabf190ff6..c829f49b1cc 100644 --- a/doc/plans/2026-11-03-native-event-ingestion.md +++ b/doc/plans/2026-11-03-native-event-ingestion.md @@ -350,7 +350,7 @@ Registered v2 browser protocol SHA256: `5498ed8dfc7139285aeda582c540eb572f07a1ad Comparator SHA256: `d8cf6f55339ad811ca1743f4bc73b74afa3a321b93e17ef913fc0339b47b4d7d`. Individual source hashes and H's exact committed revision are retained with each qualification/execution receipt. -### V2 admission qualification correction, before fresh A/A +### V2 admission qualification correction, before fresh browser A/A H `96b1a5f1a4497d1389c21b0cc52d55b86641690d` passed the compiled single-run smoke and completed paced 128, burst 16, and ten runs of 128 events. @@ -372,7 +372,7 @@ pg_stat_activity capture exists. The product admission hazard remains separate from this ingestion change; increasing pool size or request timeouts is not part of the experiment. -Before any fresh A/A, H now schedules each background using the same normal +Before any fresh browser A/A, H now schedules each background using the same normal PATCH, then requires that exact participant's provider GET and original-clock hello before scheduling the next. All 100 providers remain live behind the closed common emission gate. A **shared 180-second readiness budget starts @@ -385,8 +385,19 @@ rejects reports without it. Original exceptions, exact activation progress and separate cleanup errors are retained without accepting a cleanup failure. Registered corrected browser protocol SHA256: -`b955ff1f84d82655e5e1d331b2494cb811f459f44aee0e29205a70636c07dc84`. +`5430314c028c9e05ddf4e27e6e22a0191ee0d4a61ffc61649627fd93777acebc`. Comparator SHA256: -`55eab8950528a6ec6a30728fb7614b5a9fb7aa210526cb8b7d3237a82db65672`. +`c1b7249022a95ff2186e72784247ca83125550c02910d8ad2bf52e1389cf9afc`. The corrected H commit and source hashes are recorded before its full baseline qualification and new browser A/A. Earlier H96b samples never enter that series. + +The original sparse-stream acceptance also requires no regression in gaps between +rendered chunks. After terminal drainage, H derives consecutive gaps between +distinct observed paint timestamps from the existing retained `visibleMs` values. +Reports include paint count, gap count, median gap and maximum observed gap; +no tail quantile is inferred from a small gap sample. The comparator requires +observed paced-stream gaps and gates the maximum observed gap against the same +five-pair A/A noise envelope. This adds the previously missing acceptance check +before browser A/A; it changes neither the streaming observer nor emitted work. +The independent source-owner A/A may run during local H work because its workload, +measurement code and gates remain unchanged. diff --git a/scripts/compare-native-ingestion-browser.ts b/scripts/compare-native-ingestion-browser.ts index 71ba68c25d7..c22c88b3c65 100644 --- a/scripts/compare-native-ingestion-browser.ts +++ b/scripts/compare-native-ingestion-browser.ts @@ -47,6 +47,10 @@ for (const pair of pairs) for (const arm of ["baseline", "candidate"] as const) assert.equal(result.persistedAfterReload, true); assert.equal(result.matchedVisibleClocks, result.visible?.length, "Every observed marker needs its original provider clock"); assert.ok(result.matchedVisibleClocks >= (result.stop ? 3 : result.events), "Missing foreground render/clock observations"); + if (name === "paced") { + assert.ok(result.interChunkGaps?.count >= 1, "Paced streaming needs observed inter-chunk paint gaps"); + assert.equal(result.interChunkGaps.paintCount, result.interChunkGaps.count + 1); + } if (result.prelude) { assert.equal(result.preludeProof?.individualDeltas, result.prelude, "Existing history requires independently stored prelude deltas"); assert.equal(result.preludeProof?.uniqueSourceIdentities, result.prelude); @@ -66,6 +70,7 @@ const metric = (item: any, key: string): number => key.split(".").reduce((value, const measurements = names.flatMap(name => { const observed = first.baseline.cases.find((item: any) => item.name === name); const keys = ["firstVisibleAfterEmissionMs", "emissionToVisible.medianMs", "drainThroughTerminalMs", + ...(name === "paced" ? ["interChunkGaps.maxMs"] : []), ...(observed.emissionToVisible.p95Ms === undefined ? [] : ["emissionToVisible.p95Ms", "intendedToVisible.p95Ms"]), ...(observed.stop ? ["stopThroughTerminalMs", "stopThroughAcknowledgementMs"] : [])]; return keys.map(key => { diff --git a/scripts/native-ingestion-performance-gates.test.mjs b/scripts/native-ingestion-performance-gates.test.mjs index 86a809859d5..5ecb86f0d3c 100644 --- a/scripts/native-ingestion-performance-gates.test.mjs +++ b/scripts/native-ingestion-performance-gates.test.mjs @@ -82,6 +82,7 @@ function browserReport(revision, latency) { epochMs: 1000, preludeProof: workload.prelude ? { individualDeltas: workload.prelude, uniqueSourceIdentities: workload.prelude, verifiedAtMs: 999 } : undefined, visible: Array(workload.stop ? 3 : workload.events).fill({}), matchedVisibleClocks: workload.stop ? 3 : workload.events, firstVisibleAfterEmissionMs: latency, emissionToVisible: { medianMs: latency, p95Ms: latency }, + interChunkGaps: { paintCount: workload.events, count: workload.events - 1, medianMs: latency, maxMs: latency }, intendedToVisible: { p95Ms: latency }, drainThroughTerminalMs: latency, stopThroughTerminalMs: latency, stopThroughAcknowledgementMs: latency, durable: Array.from({ length: workload.runs }, (_, index) => ({ token: String(index), ordered: true, @@ -126,6 +127,7 @@ test("browser CLI rejects the three-pairs-above-noise counterexample and ineligi ["unqualified-admission", report => { delete report.admissionProtocol; }, /admission protocol/], ["unfinished-cleanup", report => { report.cleanup.complete = false; }, /completed fixture cleanup/], ["cleanup-error", report => { report.cleanup.errors.push({ step: "restore", message: "timeout" }); }, /cleanup errors/], + ["missing-paced-gaps", report => { delete report.cases[0].interChunkGaps; }, /inter-chunk paint gaps/], ["unmatched-visible-clock", report => { report.cases[0].matchedVisibleClocks--; }, /original provider clock/], ["unmatched-durable-clock", report => { report.cases[0].durable[0].matchedSourceClocks--; }, /original provider clock/], ["browser-trace", report => { report.browserRecording.trace = "on"; }, /trace capture off/], diff --git a/tests/e2e/native-event-ingestion.journey.ts b/tests/e2e/native-event-ingestion.journey.ts index 38dc7f34ad1..69d04dd28ae 100644 --- a/tests/e2e/native-event-ingestion.journey.ts +++ b/tests/e2e/native-event-ingestion.journey.ts @@ -331,8 +331,13 @@ test("native ingestion reaches the shipped UI promptly and survives reload", asy return { ...original!, ...marker }; // Keep the original paint timestamp; never restamp after clock arrival. }); if (!stop) expect(new Set(visible.map(item => item.index)).size).toBe(workload.events); + // Terminal drainage has finished. Derive paint gaps from the retained + // observer timestamps without adding work to the streaming observer. + const paintTimes = [...new Set(visible.map(item => item.visibleMs))].sort((a, b) => a - b); + const { count, medianMs, maxMs } = stats(paintTimes.slice(1).map((at, index) => at - paintTimes[index])); + const interChunkGaps = { paintCount: paintTimes.length, count, medianMs, maxMs }; const clockEvidence: Array> = []; - const result = { ...workload, stop, nonce, markerCode, epochMs, preludeProof, clockEvidence, runIds: runs.map(run => run.id), participants: participants.length, visible, matchedVisibleClocks: visible.length, + const result = { ...workload, stop, nonce, markerCode, epochMs, preludeProof, clockEvidence, interChunkGaps, runIds: runs.map(run => run.id), participants: participants.length, visible, matchedVisibleClocks: visible.length, firstVisibleAfterEmissionMs: visible[0].visibleMs - visible[0].emittedMs, emissionToVisible: stats(visible.map(item => item.visibleMs - item.emittedMs)), intendedToVisible: stats(visible.map(item => item.visibleMs - item.intendedMs)),