Skip to content

ci(webviews): split the test shards by recorded duration, not index mod 4 - #27230

Merged
teamleaderleo merged 4 commits into
feat-cmux-nextfrom
ci/webviews-shards-by-duration
Oct 29, 2026
Merged

teamleaderleo merged 4 commits into
feat-cmux-nextfrom
ci/webviews-shards-by-duration

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #27202. The four react-apps-check shards took webview test files by index mod 4, so adding one test file moved every file after it, and the heavy dev-server files fell together by chance (the #27045 case #27194 describes). On feat-cmux-next push runs the shards were consistently 2x apart. The slowest took about 20 min and the fastest about 9.

What changes

  • Split by recorded duration. New scripts/ci/webviews_test_shards.py. Files with a recorded time go, longest first, to the shard whose estimated wall time grows least. A shard is modelled the way run-webviews-tests.sh runs it: plain files on 32 lanes, one warming file per Vite config in series, the other dev-server files on 8 lanes, then // cmux-web-test-alone files one after another. Files with no recorded time go by a stable hash of their path, after the others, so a new file moves only itself. The split never reads the runner's own size, so the four shard jobs agree even when one lands on the 16 vCPU fallback.
  • Order inside a shard. With a manifest, each Vite config warms on its shortest file, and both pools start their longest files first. accent-glass-invariants (549 s) came first in the index and warmed its config alone for 9 of shard 3's 18.6 min (run 40815704587).
  • The manifest.
    • web-subarea-scope restores the newest manifest from the Actions cache once and hands that copy to all four shards as an artifact, so they split by the same copy. A shard that read the cache itself could get a newer copy than its siblings and skip or repeat files.
    • Each shard records each file's first-run seconds (CMUX_WEB_TEST_DURATIONS_OUT; a retry runs alone and says nothing about the file among the others).
    • A new job, webviews-test-durations, averages them with the prior manifest and saves the next one. Nothing requires it, so a stalled download or cache save can't time out react-apps-check; a missing manifest only means the next split is by path hash.
    • A pull request's cache entry stays in its own ref's scope, and fork pull requests save none. feat-cmux-next push runs keep the shared manifest fresh.

Performance

metric before after delta harness or run link
slowest shard, test step (min) 18.6 9.8 -8.8 (-47%) run 40815704587's per-file times through the runner's phases
fastest shard, test step (min) 7.7 8.9 +1.2 same
slowest / fastest shard 2.41 1.10 -1.31 same

Before: the replay of the measured per-file times reproduces today's shards (18.6, 11.2, 9.5, 7.7 min against 18.6, 12.1, 9.9, 7.7 measured), so it models the runner's phases closely. After: the same times through the new split and order. This PR's first run starts from an empty manifest (path-hash split). The manifest it saves feeds the next run, so a measured "after" comes from a run that reads a manifest. The measured slowest shard job before is 19.6 min (run 40815704587, 32 vCPU). I'll add the measured after from the first run that reads a manifest, on the same 32 vCPU class.

Red / green

  • python3 tests/test_webviews_tests_per_file.py: the 5 new runner tests fail on the base runner and pass on this head: the load split, a new file moving only itself, the hash split for files with no recorded time, the warming pick and pool order, the recorded seconds. There are also new helper tests (merge, record, the lane weighting) and a workflow test (one manifest from web-subarea-scope, merged and saved by react-apps-check, never saved by forks). 36/36 pass.
  • tests/test_webviews_tests_new_reds.py: the test of the base replay re-running every red file now counts the files its shard ran, since the shard is no longer index mod 2. 23/23 pass.

Observed / Not run

  • Ran on a Linux host, on this head: tests/test_webviews_tests_per_file.py (35 OK), tests/test_webviews_tests_new_reds.py (23 OK), tests/test_web_validation.py, tests/test_ci_change_areas.py, tests/test_test_reds.py, scripts/ci/tests/run-webviews-tests-retry.test.sh, actionlint on ci-web.yml, scripts/cmux-next/prepush-gate.py (no new violations).
  • Not run yet: a real shard run with a manifest. This PR's CI is the first run without one; its successor is the first with one.

Risk: CI only (risk:low). Every shard reads the same manifest artifact or fails its download, so shards cannot disagree on the split. A stale or missing manifest only makes the split less even, never incomplete: every file in the git index lands on exactly one shard.

Changelog

none

🤖 Generated with Claude Code

Agent session: agent session

…od 4

The four react-apps-check shards took files by index mod 4. One new file moved every file after
it, and the heavy dev-server files fell together by chance. On feat-cmux-next push runs the
shards took about 20, 14, 11 and 9 min (32 vCPU; run 40815704587).

- scripts/ci/webviews_test_shards.py: files with a recorded duration go, longest first, to the
  shard whose estimated wall time grows least. A shard is modelled the way the runner runs it:
  plain files on 32 lanes, dev-server files on 8, alone files in series. Files with no recorded
  time go by a stable hash of their path, after the others, so a new file moves only itself.
- run-webviews-tests.sh: with a manifest, each Vite config warms on its shortest file
  (accent-glass-invariants warmed alone for 549 s of an 18.6 min shard), and the plain and
  dev-server pools start their longest files first. Each file's first-run seconds are written
  out (CMUX_WEB_TEST_DURATIONS_OUT).
- ci-web.yml: web-subarea-scope restores the newest manifest from the cache once and hands it to
  every shard as an artifact, so all four split by the same copy. react-apps-check merges the
  shards' seconds with it (each file's average with its prior) and saves the next manifest. A
  pull request's entry stays in its own ref's scope; fork pull requests save none.

Replaying run 40815704587's per-file times through the runner's phases reproduces today's
shards (18.6, 11.2, 9.5, 7.7 min against 18.6, 12.1, 9.9, 7.7 measured) and gives 9.2-9.6 min
with this split.

Red/green: tests/test_webviews_tests_per_file.py's 5 new runner tests fail on the old runner
and pass now (the load split, a new file moving only itself, the hash split, the warming pick and
pool order, the recorded seconds), plus helper and workflow tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-7a592ef92c66f080e3cc
@teamleaderleo teamleaderleo added the risk:low Risk tier (docs/ci/risk-tiers.md) label Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review cc63e5a: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr27230 -f ref=ci/webviews-shards-by-duration -f platform=macos -f backend_mode=local

Yours to fix: next-docs.test.ts (this PR changes the web test runner (.github/workflows/ci-web.yml)), docs-shortcuts.test.ts (this PR changes the web test runner (.github/workflows/ci-web.yml)). Also failing here, not on main: rowSummary.test.tsx, rowSummary.test.tsx.

CI failed on cc63e5ab5f (run 40824110742 attempt 1): 3 code, 1 unknown.

Job Verdict Why
web / Web tests (4/4) code a web test failed
web / Web tests (3/4) code a web test failed
web / react-apps-base-check (1) code a web test failed
web / react-apps-check unknown no known signature; failed step: Check webview test shards

Failures:

  • yours next-docs.test.ts cmux-next docs > the settings reference is fresh against the settings schema: failed (job)
  • yours docs-shortcuts.test.ts keyboard shortcuts docs > the web/data copy is fresh against the catalog export and string catalogs: failed (job)
  • new in this PR rowSummary.test.tsx no description line on any Settings page is over 60 characters, in any language: failed (job)
  • new in this PR rowSummary.test.tsx every schema summary fits 60 characters in every language: failed (job)
Matched log lines
web / Web tests (4/4): (fail) cmux-next docs > the settings reference is fresh against the settings schema [3.82ms]
web / Web tests (3/4): (fail) keyboard shortcuts docs > the web/data copy is fresh against the catalog export and string catalogs [16.71ms]
web / react-apps-base-check (1): (fail) no description line on any Settings page is over 60 characters, in any language [13185.38ms]

Not re-run automatically: web / Web tests (4/4), web / Web tests (3/4), web / react-apps-base-check (1), web / react-apps-check are not machine failures.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 9af05f0 to compare; do not merge until green; see lane-rules
red: web / Web status; base red: the same failure on the base 9af05f0; does not block
red: web / Web tests (3/4); no finished base run on 9af05f0 to compare; do not merge until green; see lane-rules
red: web / Web tests (4/4); no finished base run on 9af05f0 to compare; do not merge until green; see lane-rules
red: web / react-apps-base-check (1); no finished base run on 9af05f0 to compare; do not merge until green; see lane-rules
red: web / react-apps-check; base red: the same failure on the base 9af05f0; does not block

@blacksmith-sh

blacksmith-sh Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Found 8 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp9a42z2v8/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmpug80fk01/summary-fail.json (live):/summary-fail.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
src/pages/settings/rowSummary.test.tsx/rowSummary.test.tsx View Logs
src/pages/settings/rowSummary.test.tsx/rowSummary.test.tsx View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@github-actions github-actions Bot added risk:normal Risk tier (docs/ci/risk-tiers.md) and removed risk:low Risk tier (docs/ci/risk-tiers.md) labels Oct 29, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-6y9x0c created for this PR by the cmux coordinator.

…acts keep 1 day

The review found the manifest checkout, download and cache save inside the five-minute required
react-apps-check: continue-on-error survives a failed step, not a stalled one that times the job
out. They move to webviews-test-durations, which nothing requires. The shard's manifest download
moves before Setup Node, where the setup steps run (test_cmux_next_fast_gates keeps every step
between the setup and the tests continue-on-error and gated), and both handoff artifacts keep 1
day (test_ci_upload_artifact_retention): the cache carries the manifest between runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-7a592ef92c66f080e3cc
…the manifest artifact overwrites

Review: run-webviews-tests.sh runs one file per Vite config alone before the dev-server pool,
and ShardPlan counted only the 8-lane pool (one config's 100 s and 20 s files: 120 s, not 100).
The plan now keeps each config's warming file (its shortest so far, as the runner picks it) in
series ahead of the pool. A re-run of the whole run would fail on the same-named manifest
artifact; it overwrites. Replaying run 40815704587: 9.6, 8.9, 9.7, 9.8 min (max/min 1.10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-7a592ef92c66f080e3cc
Review: the per-shard duration upload sat after the gated steps without continue-on-error, so an
artifact-service failure or stall could fail a required react-apps-check shard. It now keeps
going (2 min cap), and the gate step reads its outcome as a warning only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-7a592ef92c66f080e3cc
@teamleaderleo
teamleaderleo merged commit 85fa160 into feat-cmux-next Oct 29, 2026
156 of 162 checks passed
@teamleaderleo
teamleaderleo deleted the ci/webviews-shards-by-duration branch October 29, 2026 07:02
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Measured after landing: react-apps-check shard job wall times on feat-cmux-next push runs, all on the 32 vCPU class. The "after" runs all read the 1,196-file duration manifest.

Performance

metric before after delta harness or run link
slowest shard job, median (min) 19.7 17.0 -2.7 (-14%) before: 8 push runs ending 40815704587; after: 14 push runs 40844104395..40846198563
fastest shard job, median (min) 9.0 13.2 +4.2 same
slowest / fastest shard, median 2.24 1.31 -0.93 same
runs with all shards within 20% 0 of 8 4 of 14 +4 best: 40844350052 at 1.10 (16.0, 15.1, 14.5, 15.1 min)

The split is more even, but the slowest shard fell far less than the replay predicted (9.8 min). There are two causes, and both read the same way in 40846198563 (max/min 1.93):

  • Heavy plain files contend when packed together. One shard got the 41 heaviest plain files. They took 5.3 min, against the ~2 min their recorded times predicted, because each file's time was recorded beside light neighbours. The lane model treats the 32 plain lanes as free of CPU contention, and they aren't.
  • Base-red retries are not modelled. The base was red during these runs, so shards spent up to 6 min retrying failed files (14 retries on shard 2). Those minutes land on whichever shard holds the red files.

Follow-up, not in this PR: weigh plain files by contention (balance each shard's total recorded seconds as well as its lane time), and count the expected retry time of files that are red on the base.

This branch was previously deployed

1 inactive deployment
ci-routing-agent — cc63e5ab Deployed Oct 29, 2026 by teamleaderleo via cmux-tui tree preflight #75227
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant