Skip to content

Run e2e test buckets in parallel with ctest -j - #8253

Closed
Amaury Chamayou (achamayou) wants to merge 3 commits into
mainfrom
achamayou-ci-runtime-under-15-min
Closed

Run e2e test buckets in parallel with ctest -j#8253
Amaury Chamayou (achamayou) wants to merge 3 commits into
mainfrom
achamayou-ci-runtime-under-15-min

Conversation

@achamayou

@achamayou Amaury Chamayou (achamayou) commented Sep 1, 2026

Copy link
Copy Markdown
Member

Summary

CI wall-clock is dominated by the virtual e2e jobs. On run 33500039436 (main, post-#8236/#8240), VMSS Virtual B and C were at 15.2 and 15.5 min against a 15 min target.

This PR takes the slowest job from 15.5 min to 12.4 min, and leaves the whole workflow paced by ACI SNP Milan, which it does not touch.

Job Before After
VMSS Virtual A 13.1 12.4
VMSS Virtual B 15.2 11.3
VMSS Virtual C 15.5 9.8
ACI SNP Milan 12.7 13.1 unchanged
ACI SNP Genoa 11.1 11.0 unchanged

Validated on run 33515162772, all green.

Root cause

ctest ran the buckets strictly serially (-j was only ever used for unit tests), while individual e2e tests use wildly different amounts of the 16-core runner. Most e2e tests already drive several CCF networks at once via infra.runner.ConcurrentRunner, so their load varies by more than an order of magnitude.

Measuring node process lifetimes from the uploaded logs (sum of node lifetimes / test duration = average concurrently live nodes):

bucket_b test wall avg live nodes
nodes_test 162s 15.1
schema_test 105s 10.9
recovery_test 167s 8.6
e2e_logging_http2 71s 8.0
recovery_stale_snapshot_join_test 50s 2.7
recovery_intermediate_snapshot_join_test 26s 2.6
recovery_snapshot_endorsements_test 9s 0.8

The bucket_b timeline contained a contiguous 85s stretch running one small three-node network at a time on a 16-core box. partitions was the worst case: a whole 282s step at an average of 4.2 live nodes.

Change

1. Weighted parallelism. Each e2e test declares a PROCESSORS weight in units of concurrently live CCF node processes, and the buckets run with ctest -j. ctest keeps the sum of the weights of running tests within the budget, packing light tests alongside heavy ones without overcommitting the runner. The budget is 1.5 nodes per core (scripts/ci-test-jobs.sh, overridable via CCF_CI_TEST_JOBS), reflecting that CCF nodes in e2e tests mostly wait on timers and sockets rather than burning CPU.

partitions is long but light, so it is labelled bucket_c and its configuration is selected in the same ctest invocation, letting it overlap the rest of the bucket instead of occupying a serial step of its own. It stays gated on the partitions configuration, so ctest -L bucket_c without -C partitions (ci-al4, the bucket snapshot check, coverage) is unaffected, and its iptables DROP rules only match its own nodes' ports.

2. Resource lock for the shared npm tree. The first parallel run failed with modules_test and auth racing in js/ccf-app: one test's npm install replaced node_modules while the other's triggered the package's prepare script, so tsc was missing. modules_test, auth and programmability_and_jwt all build that tree, so they now share a ctest RESOURCE_LOCK. They remain free to overlap everything else, so the lock costs almost nothing.

3. bucket_a moved to job C. With B and C fixed, job A became the critical path. It is paced entirely by its clang-tidy build (7.9 of 14.1 min), so the only meaningful work that can leave is the e2e bucket. ctest -L takes a regex, so one invocation covers both buckets.

The per-test timeout goes from 360s to 600s, since tests now share the runner.

scripts/e2e-test-load.py recomputes the weights from a CI run so they can be refreshed rather than guessed when a test's set of sub-tests changes. It also flags nodes that outlive their test.

Coverage and testing

  • The same 23 e2e tests run before and after (7 on B; 14 bucket_c + partitions and 2 bucket_a on C). No test was dropped or skipped.
  • Test buckets match snapshot passes, so tests/ci-buckets.txt is unchanged.
  • Contention is mild: per-test times rose ~3-4% (recovery_test 166.9 to 173.0s, e2e_logging 84.0 to 87.5s, partitions 281.6 to 283.8s), confirming the runner is not being starved.
  • Verified in a CMake harness that add_e2e_test applies explicit weights, defaults unmeasured tests to 4, applies resource locks only when given, and preserves labels.
  • Verified ctest -L bucket_c without -C still excludes partitions, ctest -C partitions -L bucket_c includes it, and -L "bucket_a|bucket_c" selects both buckets without touching bucket_b.
  • Verified ctest honours PROCESSORS (four weight-8 tests take 13.6s at -j 8, 7.5s at -j 16, 4.5s at -j 32, and a test heavier than the budget still runs on its own) and RESOURCE_LOCK (three 3s tests sharing a lock take 9.2s at -j 32).
  • Verified scripts/e2e-test-load.py reproduces the measured weights.
  • gersemi, black, shellcheck and prettier clean; full ci-checks.sh green in CI.

Follow-ups (not in this PR)

  • Leaked nodes. Two governance_test nodes outlive their test by ~8.5 min, logging ~660 lines/min on a retry loop through the rest of the bucket. Cause: gov() rebinds network to the object returned by test_all_members, which stops the original network and recovers into a new one; the enclosing context manager only owns the original, so the recovered network, including the deliberately-untrusted nodes added after it, is never torn down.
  • Serial tails. reconfiguration.run_all runs run_join_old_snapshot (22s) and run_join_no_snapshot_against_original_primary (7s) serially after its main network, and runs the whole ~25-test reconfiguration suite twice (IPv4 and IPv6). partitions_test.run runs run_ledger_chunk_bytes_check (44s) and run_in_place_restart_uncommittable_ledger_check (13s) serially after its main network; overlapping those needs per-instance iptables chain names, since Partitioner shares a global CCF-TEST chain and flushes it on construction.

CI wall-clock time is dominated by the two virtual e2e jobs, which sit at
15.2 min (bucket_b) and 15.5 min (bucket_c + partitions) against a 15 min
target. Both jobs pay ~5 min of fixed overhead (checkout, dependencies,
build), so the tests themselves have to fit in well under 10 min.

ctest ran the buckets strictly serially, while individual tests use wildly
different amounts of the 16-core runner. Measuring node process lifetimes
from the logs uploaded by run 33500039436 shows bucket_b spending 50s, 26s
and 9s stretches running a single three-node network, and `partitions`
occupying a whole 282s step at an average of 4.2 live nodes.

Declare each e2e test's load as a PROCESSORS weight, in units of
concurrently live CCF node processes, and run the buckets with `ctest -j`.
ctest then keeps the sum of the weights of running tests within the budget,
packing light tests alongside heavy ones without overcommitting the runner.
The budget is 1.5 nodes per core, reflecting that CCF nodes in e2e tests
mostly wait on timers and sockets rather than burning CPU.

`partitions` is long but light, so label it bucket_c and select its
configuration in the same ctest invocation. It now overlaps the rest of the
bucket instead of running in a serial step of its own. It stays gated on the
`partitions` configuration, so runs that do not ask for it are unaffected,
and its iptables DROP rules only match its own nodes' ports.

Raise the per-test timeout to 600s, since tests now share the runner.

Add scripts/e2e-test-load.py to recompute the weights from a CI run, so they
can be refreshed rather than guessed when a test's sub-tests change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 24ad6978-a80f-45bc-b73b-268aeaf2b30e
Running bucket_c in parallel made `modules_test` and `auth` fail together
with:

  npm error path /__w/CCF/CCF/js/ccf-app
  npm error command sh -c npm run build
  npm error > tsc
  npm error sh: line 1: tsc: command not found

Three tests build the same npm trees outside their own workspaces:
`modules_test` and `programmability_and_jwt` both call
`npm_tests.build_npm_app`, which installs `js/ccf-app` and then builds
`tests/npm-app`, and `auth`'s `run_interpreter_reuse` installs
`tests/js-interpreter-reuse`, whose `file:../../js/ccf-app` dependency
rebuilds the same package. Concurrently, one test's install replaces
`js/ccf-app/node_modules` while another's triggers the package's `prepare`
script, so `tsc` is missing when it runs.

Give those three tests a shared ctest RESOURCE_LOCK, which guarantees ctest
never schedules them concurrently. They stay free to overlap the rest of the
bucket, including the long `partitions` test, so the lock costs little: the
three total 79s against a bucket that is now paced by `partitions` at 282s.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 24ad6978-a80f-45bc-b73b-268aeaf2b30e
With the buckets running in parallel, VMSS Virtual B drops to 11.0 min and
VMSS Virtual C to 9.9 min, which leaves VMSS Virtual A as the critical path
at 14.1 min - under the 15 min target, but not by enough to call it
reliable.

Job A is paced by one thing: the Debug build with clang-tidy takes 7.9 min
of its 14.1 (13:22:30 to 13:30:27 in run 33512624021). `ci-checks.sh`, the
Python package tests and the unit tests together account for under a minute,
so the only meaningful work that can leave the job is the e2e bucket.

Run bucket_a on job C instead, which has the most headroom. ctest's `-L`
takes a regex, so one invocation covers both buckets. bucket_a is small
(lts_compatibility at 76s and tls_groups_test at 3s) and packs into the gaps
alongside `partitions`.

This leaves ACI SNP Milan at ~12.8 min as the slowest job, so there is no
value in trimming job A further.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 24ad6978-a80f-45bc-b73b-268aeaf2b30e
@achamayou
Amaury Chamayou (achamayou) marked this pull request as ready for review September 1, 2026 14:05
Copilot AI lite review requested due to automatic review settings September 1, 2026 14:05
@achamayou
Amaury Chamayou (achamayou) requested a review from a team as a code owner September 1, 2026 14:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR reduces CI wall-clock time for virtual end-to-end jobs by enabling parallel scheduling of e2e test buckets via ctest -j, using per-test PROCESSORS weights (and RESOURCE_LOCK where needed) to avoid overcommitting shared resources.

Changes:

  • Add per-test PROCESSORS weights (and a shared RESOURCE_LOCK for npm build contention) to enable weighted parallel execution of e2e buckets with ctest -j.
  • Introduce a CI helper (scripts/ci-test-jobs.sh) to compute the ctest -j budget, with an override via CCF_CI_TEST_JOBS.
  • Update CI workflows to run bucket tests in parallel and rebalance where bucket labels run (moving bucket_a execution to VMSS Virtual C and overlapping partitions via configuration gating).

Custom instructions used

  • None (no files from .github/copilot-instructions.md or .github/instructions/ were loaded during this review).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
scripts/e2e-test-load.py Adds a utility to recompute PROCESSORS weights from CI node logs and ctest summaries.
scripts/ci-test-jobs.sh Computes the ctest -j budget for CI e2e buckets, with an environment override.
CMakeLists.txt Assigns PROCESSORS weights and adds a shared RESOURCE_LOCK for tests that build shared npm trees; relabels partitions into bucket_c while keeping it gated by configuration.
cmake/gersemi_definitions.cmake Extends add_e2e_test argument definitions to include PROCESSORS and RESOURCE_LOCK.
cmake/common.cmake Implements PROCESSORS (with a default) and optional RESOURCE_LOCK as CTest properties for add_e2e_test.
.github/workflows/ci.yml Switches bucket execution to ctest -j with higher timeout, and runs `bucket_a
.github/workflows/ci-al4.yml Switches bucket execution to ctest -j with higher timeout for the AL4 workflow.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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.

2 participants