Skip to content

ci(viewer): run each shard serially, so a timing test does not race three others - #3061

Merged
louistrue merged 1 commit into
mainfrom
fix/viewer-shard-concurrency
Aug 21, 2026
Merged

ci(viewer): run each shard serially, so a timing test does not race three others#3061
louistrue merged 1 commit into
mainfrom
fix/viewer-shard-concurrency

Conversation

@louistrue

Copy link
Copy Markdown
Collaborator

Undoing the part of my own CI change that is causing flakes on other people's PRs.

The evidence

useSandbox.runSupersession.test.tsx failed on three unrelated PRs within an hour#3025, #3027, #3044 — always on Viewer tests (shard 1), always on a gate-timing assertion counting 0 or 2 where it wants 1.

None of those diffs can reach that test. Two touch only packages/extensions and packages/create, which never import it. On the same base, #3038's shard 1 ran the identical suite green. Locally shard 1 passes 3 of 3, at both concurrency settings.

So: same base, same suite, some red some green, no path from any of the diffs.

Why this is mine to undo

--test-concurrency=4 came from the first attempt at this lane — the one I measured and showed did not help CI at all. When sharding finally worked I carried it forward on the grounds that a real 2x locally was harmless.

It is not harmless. It makes a timing-sensitive test race three neighbours under CI load, and its speedup is now redundant: the parallelism comes from four concurrent shard jobs, not from concurrency inside one.

The cost, measured rather than assumed

shard 1, concurrency=4    19s local   (~2:15 on CI)
shard 1, concurrency=1    70s local   (~8:10 on CI)

Both against a 25 minute cap. 1386 tests pass either way.

Eight minutes per shard, four shards in parallel, is a budget that absorbs this without argument.

What I am not claiming

Not proof. I have a mechanism and a correlation, not a reproduction: it passes locally either way, and I have no clean pre-sharding CI baseline because the lane was broken all day.

What I do have is a variable I introduced whose benefit is now redundant. Removing it costs six well-affordable minutes and eliminates a candidate. If the flake survives this, it was never concurrency, and #3060 is where that investigation continues — including the more interesting possibility there, that a QuickJS runtime aborting during teardown in the preceding file perturbs the next one, in which case the fix is in the teardown and not in the assertion.

…hree others

`useSandbox.runSupersession.test.tsx` failed on THREE unrelated PRs within an
hour (#3025, #3027, #3044), always on `Viewer tests (shard 1)`, always with a
gate-timing assertion counting 0 or 2 where it wants 1. None of those diffs
can reach that test: two touch only packages/extensions and packages/create.
It passes locally 3 of 3 at both concurrency settings, and #3038's shard 1 on
the same base ran the identical suite green.

This is my change's consequence, so I am undoing the part that caused it
rather than asking three authors to re-run.

--test-concurrency=4 came from the FIRST attempt at this lane, which I
measured and showed did not help CI at all. I carried it forward once
sharding worked, on the grounds that a real 2x locally was harmless. It is
not harmless: it makes a timing-sensitive test race three neighbours under CI
load, and its speedup is now redundant, because the parallelism comes from
four concurrent shard JOBS.

Measured cost of going back to 1, rather than assumed: 70s versus 19s per
shard locally, so roughly 8 minutes versus 2 on CI, against a 25 minute cap.
That budget absorbs it without argument.

Not claiming proof. I have a mechanism and a correlation, not a reproduction
-- it passes locally either way, and I have no clean pre-sharding CI baseline
because the lane was broken. What I do have is a variable I introduced whose
benefit is now redundant, so removing it costs a well-affordable six minutes
and eliminates a candidate. If the flake survives this, it was never
concurrency and #3060 is where it goes.
@cursor

cursor Bot commented Aug 21, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_759b659c-82ee-4a21-b46b-4c9f85e9b45e)

@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in: 26 minutes

Limit details: You’ve used the included review currently available. Your 102 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0de56411-ce2d-45c7-a71f-c56dcb2bc746

📥 Commits

Reviewing files that changed from the base of the PR and between fe38b33 and 190e2f2.

📒 Files selected for processing (2)
  • .github/workflows/test.yml
  • apps/viewer/package.json

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 190e2f25c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/viewer/package.json
"typecheck": "tsc --noEmit",
"preview": "vite preview",
"test": "tsx --import ./src/test/vite-module-hooks.mjs --test --test-timeout=120000 --test-concurrency=4 $(find src -type f \\( -name '*.test.ts' -o -name '*.test.tsx' \\) | sort | awk -v s=\"${TEST_SHARD:-0}\" -v n=\"${TEST_SHARDS:-1}\" 'NR % n == s')",
"test": "tsx --import ./src/test/vite-module-hooks.mjs --test --test-timeout=120000 --test-concurrency=1 $(find src -type f \\( -name '*.test.ts' -o -name '*.test.tsx' \\) | sort | awk -v s=\"${TEST_SHARD:-0}\" -v n=\"${TEST_SHARDS:-1}\" 'NR % n == s')",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit serial execution to CI shards

When the canonical root pnpm test runs with TEST_SHARDS unset, this hardcoded value serializes all 521 viewer test files, not merely each CI shard. The four-way parallelism described in test.yml exists only in CI, while the preceding change measured the unsharded suite at 142s with concurrency 4 versus 295s with concurrency 1, so every developer/full Turbo run now incurs this regression. Keep the package default parallel and pass concurrency 1 specifically from the viewer-tests workflow.

AGENTS.md reference: AGENTS.md:L12-L13

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

Viewer benchmark

✅ No threshold regressions detected.

01_Snowdon_Towers_Sample_Structural(1).ifc

Baseline recorded 2026-07-01T20:31:05.538Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.

Metric Current Baseline Delta Threshold Status
firstBatchWaitMs 1812ms 2905ms -37.6% +50%
firstVisibleGeometryMs 2919ms 3652ms -20.1% +50%
streamCompleteMs 2880ms 3598ms -20.0% +50%
spatialReadyMs 1474ms 1032ms +42.8% +50%
metadataCompleteMs 1935ms 3063ms -36.8% +50%
totalWallClockMs 3000ms 3700ms -18.9% +50%

AC20-FZK-Haus.ifc

Baseline recorded 2026-07-01T20:30:59.972Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.

Metric Current Baseline Delta Threshold Status
firstBatchWaitMs 298ms 1075ms -72.3% +50%
firstVisibleGeometryMs 1723ms 1572ms +9.6% +50%
streamCompleteMs 1077ms 1980ms -45.6% +50%
spatialReadyMs 752ms 915ms -17.8% +50%
metadataCompleteMs 883ms 1392ms -36.6% +50%
totalWallClockMs 1800ms 3300ms -45.5% +50%

Refresh the baseline from a CI run: dispatch the Benchmark workflow with record_baseline, download the benchmark-baseline artifact, and commit baseline.json (see tests/benchmark/README.md).

@louistrue

Copy link
Copy Markdown
Collaborator Author

Result: all four shards green, including shard 1.

Viewer tests (shard 0)   4:03  success
Viewer tests (shard 1)   3:10  success   <- failed on five PRs at concurrency 4
Viewer tests (shard 2)   2:57  success
Viewer tests (shard 3)   2:43  success

And my cost estimate in the description above was wrong, in the good direction. I predicted roughly 8 minutes per shard, extrapolating from 70s locally against 19s at concurrency 4. Actual is 2:43 to 4:03, against 1:50 to 2:29 before. The CI cost is dominated by fixed setup rather than test execution, so the local ratio did not transfer.

That is the same mistake as my first attempt at this lane, where a real 2x locally bought exactly nothing on CI. A local ratio is a hypothesis about CI, not a measurement of it. It happened to land in my favour this time, which does not make the reasoning better.

What this does and does not establish. One green run cannot prove a flake is gone; a flaky test passes by luck sometimes. What it adds to the five failures under concurrency 4 is that the mechanism now has a matching observation on both sides, and the fix costs about a minute a shard rather than the six I was prepared to pay.

If it recurs after this lands, concurrency was not the cause and #3060 is where it continues — specifically the more interesting hypothesis there, that a QuickJS runtime aborting during teardown in the preceding file perturbs the next one, in which case the fix belongs in the teardown rather than the assertion.

@louistrue
louistrue merged commit 210961e into main Aug 21, 2026
25 checks passed
louistrue pushed a commit that referenced this pull request Aug 22, 2026
…#3066)

`useSandbox.runSupersession.test.tsx` failed on three unrelated PRs with
`0 !== 1, then 2 !== 1` on "the reproducer must be parked on its host gate",
always right after a `QuickJS aborted while freeing a runtime (#1922)`
cascade. #3061 dropped the shard's `--test-concurrency` from 4 to 1, which
mitigated it. This is the cause.

The fixture slept a flat 50ms and then asserted that a run had reached its
host gate. That is a guess about how long `execute()` needs to import
`@ifc-lite/sandbox` and stand up a QuickJS runtime, and it is wrong exactly
where this file makes it hardest: the #1922 tests at the bottom abort a real
WASM module, an aborted module is RETIRED, and the next `execute()` therefore
pays a full fresh `newQuickJSWASMModule()` rather than reusing the cached one.

So the maintainer's reading of the log was right in substance and off by one
boundary — a teardown cascade does perturb what follows it, but the boundary
is between TESTS in this file, not between files. Instrumenting the six wait
sites on a contended machine: the one site that runs after two module aborts
takes 46-72ms, every other site stays under ~20ms. The 50ms sleep straddled
the cost it was supposed to cover.

When it lost, the damage did not stop at one test. The assertion threw with
the run still un-awaited, that orphaned run parked a moment later into the
NEXT test's freshly emptied `gates`, and that test counted two — which is
precisely the reported `0 !== 1, then 2 !== 1` pair, and why it reads as a
flake: the trigger is machine load, not the diff under test.

Five further sites had the same guess in a different disguise. They started a
second run, awaited it, and treated the newer run settling as proof that the
older one had already REACHED its gate. Under load it has not.

Every site now waits for the gate to be registered rather than predicting when
it will be. That removes the guess instead of enlarging it: no assertion is
weakened, every `gates.length` count is unchanged, and the wait's 10s bound is
a hang bound, not a timing assumption — it is never approached on the passing
path. At the five settle-order sites the wait deliberately comes AFTER the
settle-order assertion, which is the load-bearing one; it cannot mask a
violation of that, because a run parked on a gate cannot settle until this
file opens the gate.

Reproduced and measured, `--test-concurrency=1` throughout, contention
supplied by background CPU load rather than by raising concurrency:

  Node 22.13.1, unloaded, before:  30/30 pass — it does NOT fail alone
  Node 22.23.2, unloaded, after:   30/30 pass
  Node 22.13.1, loaded,   before:  19/30 pass, then 17/20 on a re-run
  Node 22.13.1, loaded,   after:   30/30 pass, and 30/30 at 2x the load
  Node 22.23.2, loaded,   before:  17/20 pass, identical signature
  Node 22.23.2, loaded,   after:   30/30 pass

Both Node versions fail before and pass after, so this is not the
22.15.0 `module.registerHooks` loader change: it is load-dependent, not
version-dependent. Nothing here adds a delay to make a race less likely; the
sleep that WAS such a delay is what gets removed.

The `--test-concurrency=1` comment in test.yml is corrected to say that the
setting mitigated #3060 rather than fixing it, since a confident wrong comment
is worse than none. The setting itself is left alone.
@louistrue
louistrue deleted the fix/viewer-shard-concurrency branch August 22, 2026 07:42
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