Repository navigation
Investigate CI test speedups with baseline evidence and cost/fidelity gates before adoption #894
Description
Objective
Reduce the elapsed time of Netsuke's CI test gate without increasing Ubicloud expenditure or weakening test fidelity. Establish a current baseline, run controlled experiments, and publish the CI evidence before adopting any optimization.
This is an evidence-led investigation, not authorization to implement every proposed change. A candidate may be rejected or deferred. Do not create additional paid benchmark jobs, enlarge runners, or enable an optimization by default merely because its isolated microbenchmark looks promising.
Verified starting point, not a performance baseline
Source inspection on 10 October 2026 used main at 24293a1b3616872100d379e277540bb1df89203c. Revalidate these details against the implementation base before beginning.
- The Linux merge gate uses
ubicloud-standard-4-ubuntu-2404for eligible runs, with four Cargo jobs and four Nextest workers. Fork PRs use a separate GitHub-hosted fallback. The PR lane combines test execution and coverage, passesall-features,all-targetsanddoctests, and pinsgenerate-coverageto7d751eded578e48dacf43ab173d5820e6437c474. - The cache action hashes the complete
ci.ymland cache-action file intopins_hash. Both installed-tool and Whitaker cache keys depend on it. Only trusted trunk pushes publish these archives. The action deliberately does not archivetarget/; compiler output belongs to sccache. - The public-API UI harness has eight direct-rustc fixture tests which each call
NetsukeRlib::build(). Each call invokes Cargo and discovers the rlib and dependency search paths through Cargo's JSON messages. The verbose-timing fixture also checks that compilation fails without--extern netsuke. The separatecli_configuration_fixture_compilestest exercises an actual Cargo consumer project. - Nextest configuration already serializes build-capable child-Cargo tests in
nested-cargo-builds, with anchored filters. Its comments record a historical 452.4-second PR Nextest run and a further 274 seconds for profile merging/report generation on run 34914144521.
Those September figures are historical observations recorded in the repository, not measurements of today's gate and not an expected saving. Do not reuse the earlier unvalidated 8m07 figure as a baseline. Repeated Cargo calls do not imply eight cold builds: identify freshness checks, real recompilation and lock waits separately.
Related work: #732 is closed/completed and addressed earlier nested-build inefficiencies. Preserve its feature-alignment improvements rather than reopening the same fix. #693 covers a distinct split-build-dir/Windows fidelity question. This investigation does not authorize reducing that test's scope or replacing real consumers with weaker stand-ins.
Non-negotiable constraints
- Keep the existing Ubicloud runner shape, job topology, build-worker limits and test-worker limits. No extra sharding, matrix expansion, scheduled benchmark workflow, repeated manual dispatches, or reruns solely to grow the sample. Do not move work to another paid lane and call that a saving.
- Use existing logs first. Collect additional evidence through normal, already-required CI executions. Before enabling experimental work, record its overhead and how it fits the existing budget. If convincing evidence cannot be collected within that envelope, leave the candidate unadopted and document the limitation.
- Preserve the complete required test inventory and its selection: unit, integration, behavioural, public-API compilation, real-Ninja, packaging, workflow-contract and other existing gates, plus the separate doctest pass. Preserve feature/target/workspace scope, assertions, negative controls, property-test budgets, isolation, and failure propagation. Keep mutation/Kani and other independent workflows unchanged.
- Keep the existing coverage instrumentation, full detailed report, exclusions, denominator and ratchet semantics. No summary-only substitution, silently ignored malformed profiles, discarded subprocess coverage, new exclusions, or removal of uncovered files to improve either time or percentage.
- Preserve toolchain pins, warnings-as-errors, coverage-specific compiler/linker settings, trusted single-writer cache policy, and existing timeout ordering. No blanket retries, skipped slow tests, lowered test counts, or timeout increases as a performance fix.
- Do not pursue a restore-and-delete optimization for
target/: that archive does not exist in this cache design. Do not add one as part of this work.
Phase 1: establish the baseline and evidence format
Reproducible provenance
For every observation, retain the run URL, job URL/ID, run attempt, event type, source and tested merge SHA, experiment variant, relevant diff, runner label/image/architecture and CPU/memory capacity. Record the Rust/Cargo/LLVM/Nextest/cargo-llvm-cov versions, shared-action SHAs, feature/target selections, effective compiler flags, profile and concurrency limits.
Record the effective cache keys and exact/fallback/miss results separately for Cargo downloads, installed tools, Whitaker and sccache. Capture sccache hit/miss and compilation statistics rather than treating all restored runs as equally warm.
Keep PR coverage, trunk coverage, trunk cache-writer jobs and GitHub-hosted fork runs in separate cohorts. Trace the pinned shared coverage action and its scripts to confirm actual commands and phase boundaries. Record missing evidence explicitly instead of reconstructing timings from assumptions.
Measure the whole paid job and the components
Use the existing resource sampler and cache observations where possible. Add only bounded, tested telemetry needed to distinguish:
| Component | Required evidence |
|---|---|
| Queue and provisioning | Record separately from execution; do not count reduced queue time as reduced paid compute without billing evidence |
| Cache restore and tool installation | Per-step elapsed time, hit/miss state, keys, archive sizes where available, and whether installers actually did work |
| Compilation | Instrumented build time, real compiler invocations/freshness, compiler-cache behaviour, and nested Cargo preparation time |
| Test execution | Nextest execution time, complete selected/executed test identity sets, per-test durations, failures/skips/ignored tests/retries and slow-test tail |
| Nested-build scheduling | Number and duration of Cargo invocations, build-capable group occupancy, lock-wait evidence where available, and the exclusive tail on the critical path |
| Coverage processing | Raw-profile count/bytes, merge time, export time, report parsing/ratchet time and profile diagnostics |
| Doctests | Separate duration, selection, counts and outcome |
| Whole job | Start-to-finish execution including setup, telemetry, uploads, cleanup and post-job cache work; peak memory, CPU use and available I/O indicators |
Do not add together overlapping per-test durations and describe the result as wall-clock savings. Include shared preparation outside Nextest's own timer in the primary end-to-end measurement.
Prefer native timings or lightweight instrumentation of the pinned action. Splitting coverage into execution and reporting merely to obtain timings must not execute the tests twice, clean profiles prematurely, omit objects, or change failure semantics. Apply equivalent instrumentation to control and candidate.
Important confound: editing ci.yml to add telemetry currently changes the broad installed-tool keys. Distinguish that instrumentation-induced miss and any subsequent cache warm-up from both baseline performance and the optimization's effect. Do not purge production caches or provoke cold builds on Ubicloud for benchmarking.
Sampling and analysis plan
Before evaluating candidates, commit an analysis plan with a primary endpoint, minimum useful improvement, non-regression checks and sample/exclusion rules. Use whole-job duration and total chargeable consumption as primary outcomes; phase timings explain the mechanism.
Aim initially for at least five comparable control observations and five candidate observations per evaluated condition, accumulated from ordinary runs. This is a collection floor, not automatic proof. Continue observing naturally when noise or sample size prevents a reliable conclusion; do not buy extra runs to meet it.
Keep application source, dependency lockfile, toolchain, test inventory and relevant runner/cache conditions equivalent, apart from the candidate and identical telemetry. Where normal development prevents matching, label the comparison observational and explain the confound. A neighbouring PR with different tests or a different toolchain is not a controlled baseline.
Publish individual observations, medians and spread, absolute and percentage differences, and uncertainty appropriate to the sample. Do not present a p95 from a handful of runs as a reliable tail estimate. Separate naturally warm and cold/miss observations; never compare a cold control with a warm candidate as proof of improvement. Retain failed, cancelled, timed-out and retried attempts in the cost/reliability accounting even when they cannot enter successful-run timing comparisons.
Phase 2: test the candidates independently
Experiment A: installation-input cache fingerprints
Hypothesis: unrelated workflow edits unnecessarily invalidate expensive installed-tool caches. Narrowing the fingerprints may reduce setup time on CI-changing PRs without changing any executed checks.
- Inventory every actual installation input, including values sourced indirectly through Makefiles/configuration, tool versions, action/installer revisions, checksums, Python environments, relevant toolchain/ABI settings and runner compatibility.
- Derive explicit compatibility fingerprints, separately for general tools and Whitaker where appropriate. Preserve version checks and the existing trusted writer/restore boundaries. Do not merely remove
ci.ymlfrom the hash and hope the remaining inputs suffice. - Add mutation/contract tests proving that a relevant pin, installer, ABI, runner-image or suite input invalidates the correct cache; an unrelated comment/test-step edit does not; and restore/save render identical keys. Retain separate Cargo-download keying.
- Measure cache restore plus installation time and whole-job time, not cache-hit percentage alone. Evaluate the source-only warm path and naturally occurring CI-edit cases separately.
- Account for one-off key-schema migration/cold-start cost and how an authorized trunk run populates the candidate generation. Do not let a permanently cold PR-only key masquerade as a fair trial, weaken the single-writer policy, or perform redundant paid population runs.
This is a setup-time candidate, not a claim that the test executable itself will become faster.
Experiment B: prepare public-API compilation artifacts once per run
Hypothesis: the eight direct-rustc fixtures can retain independent compilation/assertions while sharing one Cargo build/artifact-discovery preparation.
- Prototype a run-scoped manifest containing the exact rlib and dependency directories resolved from Cargo JSON. Prepare it once under the same toolchain, manifest, feature selection, target/build layout and effective instrumentation environment as the existing helper.
- Preserve eight separately identifiable fixture tests and their individual rustc invocations, the verbose-timing negative control, temporary-output isolation, complete dependency search paths and response-file handling. Preserve the real Cargo-project boundary in
cli_configuration_fixture_compiles. - Nextest's process-per-test model means a process-local
OnceLockis not sufficient. Evaluate a scoped setup script or equivalent run-scoped preparation mechanism. Nextest setup scripts currently require experimental opt-in; using them needs an explicit compatibility/maintenance decision rather than an incidental dependency. - Scope data to the current invocation. Never reuse another run's manifest or guess the newest rlib. Detect stale/mismatched/missing/malformed artifacts, propagate preparation failures, and avoid overwritten uplifted rlibs/version skew when other nested builds run. Keep a correct fallback for individual local
cargo testinvocations and existing supported platforms. - Keep current serialization for the first experiment. Any subsequent change to group membership is a separate, measured variant justified by the remaining resource usage and race analysis; preserve anchored filter contracts.
- Add focused tests for one preparation serving all relevant tests, zero unnecessary preparation when none are selected, error propagation, invalid manifests, configuration mismatch, split layouts and long response-file paths.
- Measure the number of preparations, Cargo freshness/rebuild work, total preparation-plus-fixture duration, serialized tail, whole-job time and peak memory. Reject an approach that merely moves the same cost outside Nextest's reported duration or adds another build configuration.
Experiment C: reduce coverage merge/export overhead, only if the baseline confirms it
Hypothesis: profile-file proliferation or reporting work may explain a substantial post-test tail. The historical 274 seconds identifies a phase to investigate, not a proven fix.
- First measure the actual
LLVM_PROFILE_FILEpattern, raw-profile inventory, binary/object inventory and separate merge/export timings. Establish whether reporting is still material before changing it. - If raw-profile proliferation is the cause, test a small, predeclared, sequential set of bounded LLVM merge-pool configurations on the existing runner. LLVM documents
%Nmpools, but validate support and behaviour in the repository's pinned runtime/tooling. Inspect the entire filename pattern: a per-process component can defeat cross-process pooling. - Preserve run isolation and binary identity; validate collision avoidance, profile collection paths, subprocess contributions, locking and crash/error handling. Do not assume
%bis effective without the necessary build-ID support, or silently change the coverage linker settings to obtain it. - Measure any work shifted into test-process exit and any lock contention, as well as reduced report time. A faster merge with equally slower tests is not a win.
- Retain full LCOV records and identical report scope. Compare normalized source/file/function/line/branch records where currently emitted, including uncovered entries. Use deterministic controls to verify exact counter accumulation across repeated/concurrent processes. Investigate application-suite counter differences against documented natural nondeterminism; equal headline percentages alone do not establish fidelity.
- Do not assume
llvm-profdata -j 4introduces parallelism: LLVM already supports automatic thread selection. Record the actual invocation before proposing a thread override. Keep aggregate work within the same resource limits.
Do not sweep a large parameter matrix. Rule out incompatible or unpromising configurations with cheap local checks first, then evaluate only justified candidates through normal CI.
Phase 3: evidence review and adoption decision
Keep each candidate independently reversible and opt-in during its evaluation. Instrumentation may land separately, but default behaviour must remain unchanged until the relevant evidence review accepts the candidate. Experimental activation must select one implementation for an existing run, not execute both full suites in the same paid job.
For each candidate, publish this decision record in the issue and link it from the adoption PR:
| Field | Required content |
|---|---|
| Hypothesis and variant | Exact change, base/candidate SHAs and predeclared success criterion |
| Cohort | Run/job links, attempts, cache states, versions, source/test comparability and sample size |
| Effect | Whole-job and target-phase baseline/candidate timings, absolute/relative change and uncertainty |
| Cost | Total chargeable consumption across affected jobs, including failed/cancelled attempts, setup/reporting and telemetry; migration cost separately |
| Fidelity | Test-identity diff, individual outcomes and doctest evidence, negative controls, detailed coverage comparison and relevant contract checks |
| Resources/reliability | Peak-memory and CPU/I/O observations, timeout/flake/retry changes and confounders |
| Decision | Adopt, reject or inconclusive, with rationale and an exact rollback path |
Use billing data when available. Otherwise label runner-seconds on the unchanged shape as a cost proxy, not verified invoice savings, and document the limitation. Include any new storage/transfer footprint. A statistically inconclusive increase is not evidence that cost remained unchanged.
Adoption requires a repeatable, useful improvement beyond observed noise in the intended cohort, no end-to-end regression elsewhere, no increased Ubicloud consumption, and no loss of test or coverage fidelity. A cold-cache setup win must not hide a warm-path regression. Reject candidates whose complexity, uncertainty or overhead outweighs the demonstrated benefit.
If several candidates pass independently, check their combination through normal CI before enabling the combination by default. Validate affected shared harnesses on the existing Windows/macOS lanes without adding jobs. Any necessary shared-actions change must retain backwards-compatible defaults for other consumers and use a reviewed immutable pin.
After adoption, keep lightweight regression telemetry for a documented window of ordinary runs, publish the observed follow-through here, and revert on fidelity loss, resource/cost regression or disappearance of the claimed benefit. Do not close the issue on implementation alone.
Acceptance checklist
- Current source/runner/action state and related fixes revalidated; no stale historical number promoted to a current baseline.
- Baseline dataset, phase attribution, test inventory and resource/cache evidence published with run/job links.
- Analysis plan and cost envelope recorded before candidate evaluation.
- Candidates A, B and C evaluated separately or explicitly rejected/deferred with evidence.
- No additional paid CI jobs/reruns, runner enlargement, matrix expansion or deliberate production cache purges.
- Whole-job and aggregate-consumption comparisons include setup, reporting, post-job work and experimental overhead.
- Complete test selection, assertions, negative controls, doctests, isolation and detailed coverage remain intact.
- Relevant workflow/cache/harness regression contracts pass; affected existing platform lanes remain green.
- Per-candidate evidence and an explicit adoption decision precede default enablement.
- Adopted combination, rollback procedure and post-adoption observations documented; inconclusive candidates remain disabled.
No CI runs were dispatched and no implementation or workflow changes were made when filing this issue.
Activity
leynos commentedon Oct 11, 2026
@CodeRabbit plan
coderabbitai commentedon Oct 11, 2026
Coding Plan
Summary
NEXTEST_RUN_ID, and C only if the baseline confirms a material merge and export tail.Design Choices
Design Choice 1: Where to keep the analysis plan, observations and decision records
Options Considered:
docs/execplans/, a normalized observation dataset indocs/ci-performance/, and mirror each decision record in the issue.Chosen Option: 2
Rationale: This follows the ExecPlan convention, keeps provenance under review, and still publishes each decision in the issue. Use ADRs only for adopted architectural changes.
Design Choice 2: How to separate coverage phases inside the single `cargo llvm-cov nextest` command
Options Considered:
Runninglines if they can be enabled, the doctest start and the step end. Combine these with job and step timestamps from the API.Chosen Option: 3
Rationale: This adds no execution, keeps failure semantics, and applies the same way to control and candidate. If verbose
Runninglines cannot be enabled without a shared-action change, report merge and export as one post-test phase and record that limitation.Design Choice 3: How to evaluate candidate A without a cold PR-only key or extra trunk population runs
Options Considered:
Chosen Option: 3
Rationale: This adds no paid work or storage and keeps the single-writer policy. It gives hit-rate evidence. Confirm the effect after adoption through the follow-through window.
Design Choice 4: Source of the candidate A fingerprint inputs
Options Considered:
with:blocks ofci.yml. This duplicates it across the restore and save calls.ci.ymlat key-render time. This needs YAML tooling before any installation.ci.yml,Makefileand the installer files. Make it fail on any drift or omission.Chosen Option: 3
Rationale: Restore and save then read one file by construction. The contract test also proves the manifest is complete. This extends the existing
python_toolchain_sync_test.pysync pattern.Design Choice 5: Run-scoped preparation mechanism for candidate B
Options Considered:
experimental = ["setup-scripts"]and a cross-platform helper. It also runs serially before tests, which blocks all four workers. It must also work on both 0.9.133 and the coverage action's 0.9.145.OnceLock. This is ineffective under process-per-test.NEXTEST_RUN_ID, guard it with a file lock in a run-scoped directory, and validate it with a provenance manifest. Fall back to today's per-test build whenNEXTEST_RUN_IDor the opt-in flag is absent.Chosen Option: 3
Rationale: This needs no experimental Nextest feature, does no preparation when no consumer is selected, keeps preparation inside the one-slot group, and works on all platforms. Record the setup-script compatibility decision as "rejected for the first variant" in the decision record.
Design Choice 6: Opt-in activation during evaluation
Options Considered:
ci.ymlper trial.ci-experiment:public-api-prepandci-experiment:profile-pool. Read them only in the same-repository PR coverage step. Set one environment flag for each label.Chosen Option: 3
Rationale: Labels select one implementation for an existing ordinary run. They do not trigger runs under the current
pull_requestevent types, and they never reach trunk orcoverage-main.yml.💡 User Tips
Regenerate the plan with different choices with
@coderabbitai <feedback>.Implementation Steps
Phase 1: Revalidation, analysis plan and evidence tooling
Fix the reference state and the rules for evidence before any change to a workflow. Keep all work in this phase to documentation and offline tooling. Add no CI cost.
Preserve the planning context: AGENTS.md requires living ExecPlans and ADRs. The ticket requires publication in the issue. The pinned coverage action has no separate merge or export step. cargo-llvm-cov 0.9.0 has no timers.
Task 1: Task 1.1: Publish the revalidation record
Record the verified current state against the ticket's reference SHA
24293a1b….docs/execplans/ci-test-speedups-investigation.md. Use the status and section format indocs/documentation-style-guide.md.build-test, trunkcoverage-main, trunk cache writer), worker limits, timeout hierarchy and action pins from the exploration.%p.cache-matched-key.test_support/dylint.tomlis not hashed.pyproject.tomlinputs are absent from the keys.docs/developers-guide.mdlines on coverage without Nextest and on an old action pin are stale.#732is complete and that#693closed via#736as a Windows split-build-dir question. Mark both as out of scope for change.Task 2: Task 1.2: Commit the analysis plan and cost envelope
Predeclare endpoints and rules before any candidate data exists.
started_attocompleted_at, including post-job steps) for the same-repository PRbuild-testjob.Task 3: Task 1.3: Build the offline evidence collector
Turn GitHub API exports, logs and artefacts into normalized observations. Keep missing values explicit.
scripts/ci_test_evidence.py. Follow the pattern ofscripts/release_operation_observability.py: fixed labels,Nonefor missing or backwards timestamps, and no secrets or raw errors.gh api …/runs/{id}/attempts/{n}/jobsand the job logs. Also ingest the telemetry artefacts from Phase 2 when they are present.tests/workflow_contracts/ci_test_evidence_test.pyfor parsing, missing-evidence handling, attempt separation and cohort classification.docs/ci-performance/as JSONL with a short README. Do not commit raw logs.Task 4: Task 1.4: Harvest the pre-telemetry baseline from existing logs
Use the evidence that already exists before any workflow edit.
ci.ymlandcoverage-main.ymlon the current base, within log retention. Classify them by cohort.ci.ymloraction.ymlchanged. Use this frequency to define how large candidate A's opportunity can be.🤖 Prompt for AI agents
Phase 2: Bounded baseline telemetry
Land one telemetry PR so that the instrumentation-induced key rotation happens once. Apply identical telemetry to control and candidate runs. Do not change default execution.
Task 1: Task 2.1: Emit structured Nextest results
Capture executed test identities, per-test durations, outcomes and retries natively.
.config/nextest.toml, enable JUnit output for profileciwith no stored success output. Do not change timeouts, groups or selection.tests/workflow_contracts/whole_run_profile_test.pyandtimeout_ordering_test.pyso that they accept the JUnit stanza and still assert unchanged budgets.Task 2: Task 2.2: Capture coverage inventories and upload a telemetry bundle
Record profile, object and version evidence before cleanup deletes it.
always()step to.github/workflows/ci.yml(PR coverage) and.github/workflows/coverage-main.yml. Place it after the coverage step and before therm … target/llvm-cov-target target/llvm-covcleanup..github/scripts/. Make the script write JSON with the.profrawcount, total bytes and size distribution, the.profdatasize, the instrumented object count, therustc -vVoutput (including the LLVM version) and the copied JUnit file.upload-artifactaction. Follow the pattern in.github/workflows/release.yml:always(), missing-file tolerance, and a fixed short retention. Include the inventory JSON, JUnit, the sccache JSON and text, and the timestamped resource log.Runninglines for merge and export. If one exists, set it identically in both coverage steps. If none exists, record the combined post-test phase as a limitation.Task 3: Task 2.3: Extend resource and cache observations
Close the attribution gaps in the existing telemetry.
.github/actions/memory-sampler/action.ymlso that each sample line has an epoch timestamp. Keep the aggregates unchanged..github/actions/linux-gate-cache/action.ymlso that it reportscache-matched-keyfor the Cargo-download restore. Separate exact hit, prefix restore and miss states.Task 4: Task 2.4: Add nested Cargo telemetry to the existing harness
Make freshness, real rebuilds and lock waits visible for the control path.
NetsukeRlib::buildintests/command_env_ui_tests.rsand the analogous preparation intests/support/test_support_rlib.rsto print one fixed-format line per Cargo invocation. Include the test name, elapsed time, the counts offreshand non-fresh compiler-artefact messages, and whether stderr contains Cargo's "Blocking waiting for file lock" text.tests/support/cargo_artifacts.rs. Do not change the existing selection API.Task 5: Task 2.5: Contract-test the telemetry and accumulate the baseline
Keep the telemetry bounded. Ensure that it cannot change gate semantics.
tests/workflow_contracts/. Require the inventory and upload steps to bealways()and to sit before cleanup. Require upload to tolerate missing files. Ensure that telemetry does not alter the coverage inputs,RUSTFLAGS, the watchdog or the job ceiling. Require the sampler to log timestamps. Require the Linux observation to report the matched key.make test-workflow-contractsand the affected Rust tests locally.🤖 Prompt for AI agents
Phase 3: Experiment A – installation-input cache fingerprints
Derive explicit compatibility fingerprints and evaluate them as shadow keys. Keep live keys unchanged until adoption.
Preserve the fingerprint-input constraint: installer
uses:SHAs cannot be referenced as expressions. Make the restore call and save call render identical keys.Task 1: Task 3.1: Inventory inputs and commit fingerprint manifests
Make every real installation input explicit.
toolsandwhitakerfingerprint manifests under.github/actions/linux-gate-cache/.NEXTEST_VERSION, Ruff, ty, Python baseline, mdtablefix, yamllint, and actionlint (version, SHA256 and installer commit).setup-rust,install-action,setup-uv, the mdtablefix installer and the Nixie installer.tools/mold/VERSION,tools/mold/SHA256SUMS,scripts/install-build-tools.shandscripts/build-tools-common.sh.installer-version, and hashes of the rootdylint.tomlandtest_support/dylint.toml.pylint, the unverified Whitaker suite revision and the upstream installer behaviour. Keep the installers' own version and checksum checks unchanged.Task 2: Task 3.2: Extract a tested key renderer with a shadow v2 key
Prove restore and save symmetry by construction. Keep live behaviour unchanged.
.github/actions/linux-gate-cache/action.ymlinto a script next to the action. Keep the v1 keys byte-identical and keep Cargo-download keying separate.netsuke-tools-v2-…andnetsuke-whitaker-v2-…from the manifests. Write them to the step summary and the telemetry bundle only. Do not restore or save them.github.event_name == 'push' && github.ref == 'refs/heads/main'and the per-save hit gating unchanged.Task 3: Task 3.3: Add mutation and completeness contracts
Show what invalidates each cache and what does not.
tests/workflow_contracts/linux_cache_fingerprint_test.pywith these tests:dylint.toml) changes the correct v2 key and only that key.ci.ymlfixture leaves the v2 keys unchanged.ci.ymland every Makefile tool pin matches a manifest value, so adding a new installer without a fingerprint fails.cache_ownership_test.pyandcache_write_policy_test.pypassing without loosening them.Task 4: Task 3.4: Evaluate counterfactual hits and setup cost
Measure whole-job effects, not hit rate alone.
🤖 Prompt for AI agents
Phase 4: Experiment B – run-scoped public-API artefact preparation
Share one Cargo preparation across the eight direct-rustc fixtures. Keep every test, invocation and control.
Task 1: Task 4.1: Implement opt-in run-scoped preparation with a provenance manifest
Prepare once per Nextest run. Validate before every consumer uses the preparation.
tests/support/thatNetsukeRlib::buildcalls. Activate it only whenNETSUKE_PUBLIC_API_PREP=run-scopedandNEXTEST_RUN_IDare both present. Otherwise run the current per-test build unchanged, including for localcargo test.NEXTEST_RUN_ID, make the first consumer run the existing Cargo command withGATE_FEATURE_ARGUMENTS. Parse the JSON with the existing parser.RUSTFLAGS,CARGO_ENCODED_RUSTFLAGSset or unset and its value, wrappers, target root and build root.cli_configuration_fixture_compilesand thenested-cargo-buildsmembership unchanged.Task 2: Task 4.2: Add focused tests
Cover the required behaviour of the preparation mechanism.
tests/ui/split_build_dir_cargo_messages.jsonl.tests/support/rustc_response_file.rsthat goes beyond the Windows command-line limit.nested-cargo-builds. Keep the anchored selector contracts innextest_child_cargo_group_test.pypassing.Task 3: Task 4.3: Wire label-gated activation and evaluate
Measure the real end-to-end effect.
.github/workflows/ci.yml, setNETSUKE_PUBLIC_API_PREPonly when the labelci-experiment:public-api-prepis present. Add a contract that forbids the variable in fork, trunk andcoverage-main.ymlpaths.🤖 Prompt for AI agents
Phase 5: Experiment C – coverage profile merge overhead (conditional)
Proceed only if the baseline shows a material post-test tail. Otherwise defer with evidence.
Task 1: Task 5.1: Gate the experiment on baseline evidence and local checks
Confirm the mechanism before any CI trial.
%Nmpooling works for process-per-test runs. The default pattern has%pbefore%Nm, so the default creates per-process files.llvm-profdata show.%m, profile collection paths, subprocess contributions and crash behaviour. Do not rely on%b. Do not change linker settings.Task 2: Task 5.2: Add an LCOV fidelity comparator
Compare full detailed reports, not headline percentages.
scripts/that normalizes LCOVSF,FN,FNDA,DAandBRDArecords, including uncovered entries. Make it report set differences and count differences.tests/workflow_contracts/.Task 3: Task 5.3: Run the predeclared pool configuration through ordinary CI
Test one small, sequential set of configurations.
LLVM_PROFILE_FILE_NAMEto a pooled pattern ending in.profrawonly when the labelci-experiment:profile-poolis present. Start with a pool size equal to the existing four test threads. Evaluate a second predeclared size only if lock contention explains an inconclusive result.llvm-profdata. It already selects threads automatically.🤖 Prompt for AI agents
Phase 6: Evidence review, adoption and follow-through
Decide on each candidate separately. Adopt only with evidence. Keep a working rollback.
Task 1: Task 6.1: Publish per-candidate decision records
Fill in the ticket's decision-record fields for candidates A, B and C.
Task 2: Task 6.2: Adopt accepted candidates and check combinations
Enable only the candidates that pass, through ordinary CI.
docs/, updatesdocs/developers-guide.md, and removes the experiment label path.Task 3: Task 6.3: Run post-adoption follow-through
Confirm that the benefit lasts.
🤖 Prompt for AI agents
Research
Ticket Summary
Run an evidence-led investigation into faster Netsuke CI test gates. Revalidate the current state and publish a provenance-rich baseline. Add only bounded telemetry. Evaluate candidates A (cache fingerprints), B (run-scoped public-API artefact preparation) and C (coverage profile merge) independently and opt-in. Adopt a candidate only after a published decision record shows a real gain, no extra Ubicloud consumption and no loss of test or coverage fidelity.
Codebase Summary
The Linux merge gate in
.github/workflows/ci.ymlruns onubicloud-standard-4-ubuntu-2404for same-repository runs and onubuntu-latestfor fork PRs. It usesCARGO_BUILD_JOBS=4,NEXTEST_TEST_THREADS=4and Nextest profileci.PR coverage calls
leynos/shared-actions/.github/actions/generate-coverage@7d751ede…. That action runs onecargo llvm-cov nextest --workspacecommand, which builds, tests, merges and exports. It then runs an uninstrumentedcargo test --doc. Each Cargo process has a 1800-second watchdog. At this SHA, the action installs and enforces cargo-nextest 0.9.145 and cargo-llvm-cov 0.9.0. Coverage therefore does not use the workflow's 0.9.133 pin.cargo-llvm-cov 0.9.0 sets
LLVM_PROFILE_FILEto<workspace>-%p-%<N>m.profrawunder Nextest unlessLLVM_PROFILE_FILE_NAMEis set. It merges withllvm-profdata merge -sparse -f <list>and exports withllvm-cov export -format=lcov. It prints no phase timings.Trunk coverage runs in
.github/workflows/coverage-main.yml. That workflow owns ratchet and CodeScene publication..github/actions/linux-gate-cache/action.ymlrenders Cargo-download, tools and Whitaker keys.pins_hashhashes the whole ofci.ymlandaction.yml. Only a trunk push saves an archive.target/is never archived. The Linux observation step does not reportcache-matched-key..github/actions/memory-sampler/action.ymlwrites 15-second samples without timestamps and does not upload them.In
tests/command_env_ui_tests.rs, eight direct-rustc fixtures callNetsukeRlib::buildwith--all-featuresand parse Cargo JSON throughtests/support/cargo_artifacts.rs. They pass arguments throughtests/support/rustc_response_file.rs. These fixtures run serially in the one-slotnested-cargo-buildsgroup in.config/nextest.toml.Python workflow contracts in
tests/workflow_contracts/enforce cache ownership, writer policy, timeout ordering, profile and selector rules.scripts/release_operation_observability.pyis the tested pattern for "missing means no value, not zero" telemetry.File-Level Change Summary and Follow-Up Agent Prompts
File-level changes
docs/execplans/ci-test-speedups-investigation.md(new): revalidation, analysis plan, cost envelope, progress and decision records.docs/ci-performance/(new): normalized observation JSONL and README.scripts/ci_test_evidence.py(new), withtests/workflow_contracts/ci_test_evidence_test.py: offline collector..config/nextest.toml: JUnit output for profileci. Do not change budgets or groups..github/workflows/ci.ymland.github/workflows/coverage-main.yml:always()inventory step before cleanup, telemetry upload, sampler stop moved later, and label-gated variables for B and C (PR coverage only)..github/scripts/(new script): profile, object and version inventory..github/actions/memory-sampler/action.yml: timestamped samples..github/actions/linux-gate-cache/action.yml, a key-render script and fingerprint manifests (new): matched-key reporting, extracted renderer, shadow v2 keys.tests/workflow_contracts/: telemetry contracts,linux_cache_fingerprint_test.py, experiment-gating contracts, and updates to the profile and timeout contracts.tests/command_env_ui_tests.rs,tests/support/test_support_rlib.rs,tests/support/cargo_artifacts.rs, a newtests/support/run-scoped preparation module, andtests/support/rustc_response_file.rs: telemetry, opt-in preparation and focused tests.scripts/(new): LCOV comparator. Use it only if Phase 5 proceeds.docs/adr-0NN-*.mdanddocs/developers-guide.md: change only at adoption.Prompt: evidence collector (Phase 1)
Prompt: baseline telemetry PR (Phase 2)
Prompt: Experiment A
Prompt: Experiment B
Prompt: Experiment C
🚀 Next Steps
🤖 All AI agent prompts combined
💡 Iterate on the plan with:
@coderabbitai <feedback>Start Coding →
leynos commentedon Oct 15, 2026
Offline evidence checkpoint for #894; no optimization is adopted.
The investigation plan fixes the endpoints, cohorts, useful-improvement threshold, cost envelope and fidelity gates. The evidence report links individual observations and explains missing fields.
The original harvest contains 21 ordinary attempts and 81 job records. A dated supplement retains all five jobs from ordinary attempt 38750711833, attempt 1, the latest ordinary PR attempt returned by the 15 October query without an outcome filter. These are 22 observational attempts and 86 records, not a controlled baseline. No CI execution or rerun was requested for collection.
The supplemental Linux job took 1,369 seconds end to end, with 495 seconds in the combined coverage step and 344.029 seconds of native Nextest execution. It reported 3,933 passes and six skips; three native doctest batches total 129 passes and 32 ignored tests. Its source and CI stages differ: the executed identity sets have 25 additions and four removals compared with the earlier retained job. Keep it separate from the earlier summaries; it is not a fifth matched control.
All three archive caches hit exactly in that job, while sccache recorded 2,934 hits and 559 misses. Peak system-used memory was 2,591 MiB; process RSS, separate merge/export timings and raw-profile inventory remain missing. Archive warmth does not establish compiler-cache equivalence.
The supplement's complete attempt accounts for 4,230 runner-seconds: 1,369 on the four-vCPU Ubicloud shape, 787 on the two-vCPU shape and 2,074 on GitHub-hosted jobs. The original harvest separately retains failed and cancelled attempts. These are consumption proxies for existing ordinary runs, not invoice savings or new benchmark expenditure. The two JSONL files add approximately 10.3 MiB and 0.75 MiB before Git compression; storage/transfer billing is unavailable. Queue/provisioning is not counted as a saving.
Each candidate has no evaluated variant SHA, candidate cohort or estimated effect yet. Existing runner shape, topology, worker limits, toolchain/action pins, Rust harness, selection, assertions, doctests, detailed coverage, ratchet, cache ownership and independent workflows remain unchanged by this checkpoint. There is no optimization to roll back. Future variants remain opt-in and independently reversible.
Adoption still requires at least five comparable ordinary controls and five candidate observations per condition, useful improvement beyond the larger of 60 seconds and control IQR, no cost increase and preserved fidelity. Source/test differences prevent causal comparisons here. Do not close the issue on this checkpoint.
Validation/publication status: the reviewed documentation and saved evidence are published in draft PR #918. Formatting, Markdown, spelling and Mermaid checks pass; the latest documentation increment has a complete CodeRabbit review with no findings. Earlier documentation review reported one unnamed unreviewed file, retained as a review-coverage limitation.
Six offline-collector Python files remain staged locally and are not part of that published head. Their linting and typing pass; 1,311 workflow contracts pass with three skips, followed by 27 passing script checks. Separate doctests pass with 129 passes and 32 ignored cases. Full Rust validation remains blocked by the unchanged packaging timeout; the single diagnostic awaits its predeclared shared-queue condition. Collector code commit/review and addition to the draft remain pending the required gate. No complete Rust-gate pass is claimed.
leynos commentedon Oct 15, 2026
Documentation checkpoint update: draft PR #918, head
55a66ca91d1989a6caa5fc8fe9a0e5e5cff8e130, now has all required checks green. This includes ordinary CI run 38762276230, attempt 1, Windows gates and the Release Dry Run across Linux, macOS and Windows. No rerun or dispatch was requested.Native checkout identifies tested merge
0ced8474e03ba6ec22d45abf8d81145e90d41a35, with newer main486b876c7244c81b4fa9787a990448beebafea26and the PR head as parents. Linux job 116516379338 passed 3,928 Nextest tests with six skips and the separate doctests (129 passes, 32 ignored). The original packaging test passed in 161.125 seconds under CI's existing configuration.The tested merge differs from the local implementation tree across 78 paths, including Rust tests and Nextest configuration. Newer main also raises the packaging-specific allowance from 300 to 600 seconds; this branch retains 300 seconds. It therefore cannot replace the local failed Rust gate or establish comparable performance: the local suite selected 3,912 tests. Its packaging timeout and the other local failures remain recorded. H4's quiet-queue condition produced no qualifying pair in 424 observations before a temporary hold; no diagnostic test ran. Monitoring has resumed with the same launch condition and original timeout.
The six added Python collector files remain staged locally and are not part of this published head. Their Python gates pass, but code commit/review remain pending the required Rust validation. Candidates A, B and C remain deferred and disabled. This update is integration/validation evidence, not a controlled timing baseline, an adoption decision or a cost-saving claim. The issue remains open.
Record validation and recovery evidence (#894)
Clarify the diagnostic date range (#894)
leynos commentedon Oct 16, 2026
Baseline attribution checkpoint: direct-rustc fixture durations from retained ordinary logs.
All eight direct-rustc fixtures have recorded passing outcomes in six trunk-coverage jobs and seven PR-coverage jobs. Each recorded fixture body took less than 0.6 seconds. The tables show within-job fixture medians and ranges, with cohorts kept separate. These are descriptive native timings: source, runner and cache conditions vary, and no candidate was executed. No CI run or rerun was requested for this analysis.
Trunk coverage
3029d496/3029d496a75420e6/a75420e6b73f8d62/b73f8d627539e32e/7539e32e557966c8/557966c89e1bf27e/9e1bf27ePR coverage
dc07efdd/210e963a7faf1f1d/96901a7870b8b0b7/02256c19c89535d0/8f1fdd7d3317c8ce/78a62891ec0afc1c/1d4bbde62b01a3e3/820bfdaaFull source/tested SHAs, event, job provenance, versions, cache evidence, individual test identities/outcomes/durations and missing fields remain in the immutable original JSONL and dated supplement. Jobs without these fixture outcomes remain in cost/reliability accounting; this component table is not an exclusion from those datasets.
No overlapping durations were added into a wall-clock saving. Cargo freshness/rebuild counts, lock waits, shared preparation and the group's exclusive critical-path tail remain missing. The observed short fixture bodies do not establish those quantities. Candidate B remains deferred; there is no measured candidate effect, cost-saving claim or fidelity/adoption decision. The matched-control/candidate sample floors and whole-job/cost/fidelity gates remain unmet.
Record CI evidence and adoption criteria (#894)
Punctuate the coverage experiment condition (#894)
Clarify the CI evidence validation state (#894)
Retain current CI observations and parser evidence (#894)
Record validation and recovery evidence (#894)
166 remaining items
Fix CI evidence review regressions (#894)
Record current CI validation state (#894)
Record current CI validation state (#894)
Add CI evidence output contract tests (#894)
Split CI evidence output contract test (#894)
Bound CI evidence log parsing (#894)
Record parser review gate results (#894)
Record post-rebase validation state (#894)
Record current PR readiness and review state (#894)
Address latest CI evidence review findings (#894)
Complete CI evidence review repairs (#894)
Record published CI validation status (#894)
Validate CI evidence inputs and cache misses (#894)
Use structural matching for CI evidence job fields (#894)
Plan: Record CI test-speed evidence and adoption gates (#894) (#918)