Skip to content

Cut one root cause of smoke-all flakiness, and make the rest measurable#319

Merged
gordonwoodhull merged 2 commits into
mainfrom
deflake/smoke-all-e2e
Jun 19, 2026
Merged

Cut one root cause of smoke-all flakiness, and make the rest measurable#319
gordonwoodhull merged 2 commits into
mainfrom
deflake/smoke-all-e2e

Conversation

@gordonwoodhull

@gordonwoodhull gordonwoodhull commented Jun 19, 2026

Copy link
Copy Markdown
Member

Background

The nightly smoke-all E2E suite (78 in-browser WASM render tests) has been flaky for months. Every failure surfaces in CI as the same opaque line:

Timed out after 75000ms waiting for preview iframe to render.

Across the last 8 nightly logs, 114 / 115 smoke-all failures are that line (the 1 other is a content assertion). That single symptom hides several distinct causes — the project never loads, WASM never inits, the render itself hangs — which is why this suite has drawn a long series of "found the root cause" fixes that each only addressed one.

This PR does two things: fixes one of those causes, and makes the cause of every future failure observable so we stop guessing.

1. fix(sync-client) — bound repo.find() + load file docs concurrently

Traced via instrumentation: on a render-timeout, the app was stuck before the preview ever mounted — in connect() → loadFileDocuments(). That function loaded every project file doc serially, and findDoc()'s repo.find() had no deadline, so it waited out automerge-repo's own ~60s unavailable-doc timeout. One slow-to-serve doc → the whole project-open hung ~60s → the 75s render-wait expired. (The extension fixtures share one ~49-file project, so they hit a slow doc most often — which is exactly why they top the flaky list.)

  • repo.find() now gets a per-attempt AbortSignal.timeout(5s); a timed-out attempt degrades into the existing markFileUnavailable path instead of hanging.
  • File docs load concurrently (Promise.all), so the wait is bounded by the single slowest doc, not the sum.

This is a real user-facing fix too: opening a project no longer stalls for a minute when one file is slow to sync.

2. test(hub-e2e) — deflake + classify failures by pipeline stage

Harness hygiene (genuine but partial improvements): a best-effort VFS barrier, rendering once for assertions instead of 2–3× per test, and a longer element-wait for q2-preview decoration.

The durable part — failure classification. On a render-timeout, the helper now reads stable DOM markers + WASM hooks and emits a parseable line:

[smoke-diag] stage=CONNECT_STALL wasmReady=true vfs=3 projectSelector=1 …

stage localizes where the boot→render pipeline stalled: CONNECT_STALL / WASM_NOT_READY / PREVIEW_ROUTER_STALL / EDITOR_NO_PREVIEW / RENDER_STALL / …. Test-side only, fires only on the timeout path (passing tests unaffected). A companion change to the offline analyzer (e2e-failure-analysis, separate repo) parses this and reports a per-cause breakdown of nightly failures.

This already proved its worth: with the sync-client fix in place, the residual failures under load classify as EDITOR_NO_PREVIEW — a different stage than the CONNECT_STALL this PR fixes. Concrete proof that more causes remain, now measured rather than guessed.

Validation (local, induced CPU contention to mimic CI's 2-core load)

scenario before fix after fix
no contention (full 78) 78/78 (3.4m) 78/78 (2.4m)
heavy contention, extension subset (×3) ~all fail 2/15 fail
  • quarto-sync-client unit suite: 102 pass (no regression).
  • I could not make the suite 100% green under synthetic contention — see below.

What this does not fix

  • The residual EDITOR_NO_PREVIEW cause (preview never activates after the project loads) is untouched — that's the next investigation, and the new [smoke-diag] data is how we'll scope it.
  • The extension fixtures share one ~49-file project (extensions/_quarto.yml), inflating the slow-doc odds. Re-scoping that, eager retry-on-peer-arrival for unavailable docs, and loading non-active docs lazily are noted as follow-ups in the plan.

Notes for review

  • claude-notes/plans/2026-06-19-smoke-all-e2e-deflake.md carries the full investigation trail and measurements.
  • The companion analyzer change lives in e2e-failure-analysis — its value kicks in once this PR's [smoke-diag] line is on main and nightlies accumulate.

Opening a project hung for ~60s whenever a single file doc was slow to
serve. Two compounding causes in the sync client's connect():

1. findDoc()'s repo.find() had no deadline, so it waited out
   automerge-repo's own ~60s unavailable-doc timeout before rejecting.
2. loadFileDocuments() loaded every file doc SERIALLY, so that 60s stall
   (and any others) summed end-to-end across the whole project.

On a large project (the smoke-all extension fixtures share one ~49-file
project) or a CPU-starved sync server, one slow doc stalled the entire
project open past any caller's budget. In the hub-client this manifested
as the Editor/preview never mounting — the dominant cause of the
smoke-all E2E flakiness (traced: a file doc's repo.find hung exactly
60.2s while the test's 75s render-wait expired). It is also a real
user-facing hang on loaded machines.

Fixes:
- Bound each repo.find() attempt with AbortSignal.timeout (5s). A timed-out
  attempt is treated like the existing cold-start "unavailable" race:
  retried while a peer is connected, then surfaced as `unavailable` so the
  file degrades gracefully via the existing markFileUnavailable path
  instead of hanging the open. (A doc that loses the race re-loads when its
  index entry next changes; eager retry-on-peer-arrival remains the
  separate "plan D2" gap noted in syncWithFiles.)
- Load file docs CONCURRENTLY (Promise.all) so the wait is bounded by the
  single slowest doc, not the sum. Per-file unavailable handling and the
  rethrow of genuine (non-unavailable) errors are preserved.

Verified: quarto-sync-client suite 102 pass; smoke-all 78/78 with no
contention (and ~30% faster); under induced heavy CPU contention the
extension-fixture failures drop from ~all to ~2/15 (the rest gated by
the shared 49-file project + retries on CI).
The nightly smoke-all suite (78 in-browser WASM render tests) showed
failures concentrated on specific fixtures, all surfacing as the same
opaque "Timed out after 75000ms waiting for preview iframe to render".
This reworks the test harness to (a) reduce the timing races it can fix
itself and (b) make the *cause* of each failure observable.

Harness changes (hub-client/e2e):
- waitForVfsFiles — a best-effort VFS barrier (quiesce escape + 10s cap +
  3s grace) so the render/assertions observe a complete VFS without ever
  introducing a hard failure or stalling on over-broad project file sets.
- renderForAssertions — render ONCE for assertions and reuse the result
  for both HTML matching and diagnostics (was two extra full WASM renders
  per test on top of the live render).
- 30s element-wait for ensureHtmlElements — covers the q2-preview
  decoration / live-iframe catch-up that the 5s default was too short for.

Failure-stage classifier (the durable part):
- capturePreviewDiagnostics reads stable DOM markers (ProjectSelector
  .project-selector/.connecting/.error, Editor .editor-container,
  PreviewRouter "Loading preview...", the preview iframe + body) plus the
  WASM test hooks to classify WHERE the boot→render pipeline stalled, and
  the render-wait emits a single parseable "[smoke-diag] stage=... " line
  into the timeout error. Every smoke-all failure otherwise looks
  identical in CI (one symptom, several distinct causes); this lets the
  offline analyzer bucket nightly failures by cause instead of guessing
  one at a time. Test-side only, fires only on the timeout path, so
  passing tests are unaffected.

The investigation, measurements, and remaining-cause notes are in
claude-notes/plans/2026-06-19-smoke-all-e2e-deflake.md.
@gordonwoodhull gordonwoodhull force-pushed the deflake/smoke-all-e2e branch from e3b35a8 to 6f896c2 Compare June 19, 2026 18:21
@gordonwoodhull gordonwoodhull merged commit 81a9b47 into main Jun 19, 2026
5 checks passed
@gordonwoodhull gordonwoodhull deleted the deflake/smoke-all-e2e branch June 19, 2026 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant