Skip to content

speed up integration tests - #698

Merged
bit-aloo merged 15 commits into
stratum-mining:mainfrom
GitGab19:speed-up-integration-tests
Aug 11, 2026
Merged

speed up integration tests#698
bit-aloo merged 15 commits into
stratum-mining:mainfrom
GitGab19:speed-up-integration-tests

Conversation

@GitGab19

@GitGab19 GitGab19 commented Aug 7, 2026

Copy link
Copy Markdown
Member

This is a test PR with some improvements on the IT in order to speed up tests execution and improve flakiness.

@GitGab19
GitGab19 marked this pull request as draft August 7, 2026 10:54
@bit-aloo

bit-aloo commented Aug 7, 2026

Copy link
Copy Markdown
Member

We definitely got a nice speedup here, IT went from 30 mins to 20 mins! 🔥

@plebhash

plebhash commented Aug 8, 2026

Copy link
Copy Markdown
Member

thanks for the initiative @GitGab19

SRI community desperately needs to speed up our development process, since we're running on GitHub free tiers and Loupe reports are arriving at a pace in which our meatlayer processes are not being able to keep up with

we're all very grateful for Anthropic's FOSS support with out free-tier Claude Max accounts, but we've also been kinda disappointed at Fable and Opus performance (even when locked into the borderline mandatory Claude Code walled gardened harness, which sorta goes agaist FOSS spirit)

and Kimi K3 has been kicking ass across the entire Bitcoin Ecosystem lately, so we might as well try to leverage on our benefit too


so I'll try to leverage Kimi K3 for the creation of a plan, to be executed later by budget-friendly DeepSeek V4 Pro (both inside opencode harness which has been delivering great performance for my agentic workflows):

PROMPT to Kimi K3:

gitgab19 took some great initiative on #698, let's try to refine it further

context

SRI community is facing development workflow bottlenecks

SRI community desperately needs to speed up our development process, since we're running on GitHub free tiers and Loupe reports are arriving at a pace in which our meatlayer processes are not being able to keep up with

it's worth expanding on the implications of Github Runners free tiers:

  • concurrent PRs (which happen A LOT on SRI community workflow) tend to stack up
  • limited number of runners means whenever PR stack goes beyond a certain threshold, there's no further concurrent CI execution, and a stale queue starts forming

IUCC #698 is trimming some fat from Integration Tests CI global execution

I think the axis we can explore now is the great prior work from @bit-aloo, which made the genious suggestion of leveraging cargo nextest, which IMHO has been underexplored

up until now, we have been deliberately forcing test-threads = 1, while aiming for human-readable local logs (which can only happen if they're sequential)

let's allow ourselves to lift this restriction, aiming to optimize for concurrent execution, so that the stack is drained faster

as long as the final cargo nextest report has a list of which tests failed, if needed they can later be re-executed in isolation for log inspection

assuming VM resources of a free Github Runner, let's see how cargo nextest can accelerate execution of the global Integration Tests CI (aka ITFCI) stack, without sacrificing determinism or increasing flakiness

concurrent execution should also deliberately avoid flakiness by making sure concurrent CPU mining do not unintendly/blindly exhaust compute resources (which would reduce determinism)

in other words: let's optimize FOSS-friendly (aka free) CI compute resources, across the axis of:

  • cargo nextest-based optimization
  • global ITFCI execution concurrency

btw I'm sharing this prompt at #698 (comment)

parameters

methodology

try to maintain @GitGab19 prior work as much as possible

add new commits, progressively expanding speed-up-integration-tests branch, which we will push to plebhash fork, so they can be potentially cherry-picked into #698

I'll come back later with some commit suggestions to be cherry-picked

@plebhash

plebhash commented Aug 8, 2026

Copy link
Copy Markdown
Member
PLAN by Kimi K3:

Plan: concurrent ITFCI execution via cargo nextest (extending #698)

Branch: speed-up-integration-tests (currently at b19dfe97, on top of @GitGab19's 4 commits from #698).
Goal: lift test-threads = 1 so the Integration Tests CI drains faster on free-tier GitHub runners, without new flakiness sources.

Context / why this works now

  • nextest already runs each test in its own process. test-threads = 1 is the only thing forcing serialization today.
  • @GitGab19's speed up integration tests #698 commits already removed the big sequential-bottlenecks (fixed sleeps → readiness gates, 1s → 200ms polls), which also lowered per-test wall time variance — the precondition for safe concurrency.
  • GitHub free-tier runner specs (verified against official docs, public repos):
    • ubuntu-latest: 4 vCPU, 16 GB RAM, 14 GB SSD
    • macos-latest: 3 vCPU (M1), 7 GB RAM, 14 GB SSD
  • test-threads = "num-cpus" therefore means 4 on Linux, 3 on macOS — self-tuning, no matrix-specific config.

Hazards found while auditing the harness (must fix BEFORE lifting the limit)

H1. Cross-process port allocation race (determinism killer)

integration-tests/lib/utils.rs::get_available_port() probes with TcpListener::bind("127.0.0.1:0"), records the port in a static UNIQUE_PORTS: Mutex<HashSet>, then drops the socket. The static only dedupes within one process — under nextest each test is its own process, so with N>1 concurrent tests two processes can both probe the same free port in the window before either role binds it. Worse: bitcoind datadirs are keyed by port (.bitcoin-{port}), so a port collision means two bitcoinds sharing one datadir → corruption, not just a bind error.

H2. Cold-cache artifact download race (CI-first-run killer)

template_provider.rs downloads/unpacks Bitcoin Core, sv2-tp and high_diff_chain into the shared integration-tests/template-provider/ dir, guarded only by if !bin.exists(). On a fresh CI runner, N concurrent test processes all see "missing" and unpack simultaneously — and tarball::unpack writes a fixed-name temp.tar.gz into the destination. Corrupted archives / partial untars on essentially every cold CI run.

H3. CPU oversubscription by mining threads

mining_device auto mode = logical_cpus - 1 hashing threads per test process (integration-tests/lib/mining_device/mod.rs::worker_count()). On the 4-vCPU runner with 4 concurrent tests each spawning a mining device: ~12 busy hashing threads → contention, and timing-sensitive share-rate assertions (SHARES_PER_MINUTE = 120) go flaky. (sv1_minerd is already pinned to --threads 1.)

Non-hazards (verified, no action needed)

  • No hardcoded ports anywhere in integration-tests/ (all via get_available_address()).
  • static LOGGER: OnceCell — fine, per-process under nextest.
  • Stale-datadir cleanup from speed up integration tests #698 (efc4dca3) — compatible, keep as-is.

Commit plan (progressive, each commit leaves the branch green)

Commit 1 — test(integration): make port allocation race-free across test processes

Rewrite get_available_port() in integration-tests/lib/utils.rs to reserve ports with an advisory file lock held for the process lifetime:

  1. Create lock dir once: std::env::temp_dir().join("sv2-it-ports").
  2. Probe a free port with bind("127.0.0.1:0") as today, keep the probe socket open.
  3. Open/create {lockdir}/{port}.lock and try libc::flock(fd, LOCK_EX | LOCK_NB):
    • success → keep the File in a process-global Vec<File> (never dropped → lock held until process exit, kernel releases even on kill -9, so no stale-lock cleanup needed), drop probe socket, return port;
    • failure → another test process holds this port: drop everything, loop.
  4. Remove the now-obsolete UNIQUE_PORTS static (the lock dir supersedes it; within-process uniqueness is guaranteed because a held lock makes subsequent flock attempts fail).

Details for the executor:

  • Add libc = "0.2" to integration-tests/Cargo.toml (already in the transitive tree via corepc-node; zero new compile cost). flock exists on both Linux and macOS.
  • Keep the File handles in a static HELD_LOCKS: Lazy<Mutex<Vec<std::fs::File>>> — storing the File keeps the fd (and thus the lock) alive.
  • Exact order matters: probe → flock → drop probe. Never drop the probe before the flock succeeds, or another process can re-probe the same port.
  • Existing rustdoc on get_available_address/get_available_port must be updated to describe the locking scheme (per AGENTS.md: keep rust docs in sync).

Commit 2 — test(integration): serialize template-provider artifact downloads

In integration-tests/lib/template_provider.rs (BitcoinCore::start and TemplateProvider::start):

  • Guard each download+unpack block (bitcoin-core tarball, sv2-tp tarball, high_diff_chain) with a blocking exclusive libc::flock on a per-artifact lockfile, e.g. template-provider/.locks/bitcoin-31.0.lock.
  • Double-check inside the lock: re-test bin.exists() / high_diff_chain_dir.exists() after acquiring the lock — the first waiter must not re-download what the lock holder just unpacked.
  • While here: give tarball::unpack's temporary file a per-process-unique name (e.g. temp-{pid}.tar.gz) so even a bug elsewhere can't make two unpacks share the temp file. Cheap insurance, one line.

Note: this makes the first CI run slightly serialized at startup (one process downloads, others wait on the lock, then take the fast path). Do not add a CI pre-warm step speculatively — measure first.

Commit 3 — test(integration): pin harness mining devices to one worker thread

In integration-tests/lib/mod.rs::start_mining_device_sv2(), call crate::mining_device::set_cores(1) before spawning. One line; covers every test that mines through the harness helper. The standalone mining_device binary and benches keep auto mode (they're not part of ITFCI).

Expected effect: with test-threads = num-cpus, worst-case hashing load ≈ 1 thread per test slot + test runtime threads ≈ the machine's core count, not N×(cores−1).

Commit 4 — test(integration): run nextest at num-cpus

integration-tests/.config/nextest.toml:

# was: test-threads = 1 (human-readable sequential logs)
# Sequential logs are still available locally via:
#   cargo nextest run --test-threads=1 --no-capture
test-threads = "num-cpus"
  • Keep retries = 3 — it now also absorbs any residual rare flakes; a test that fails 4× in a row is still a deterministic failure signal.
  • slow-timeout/terminate-after: leave untouched initially; only relax (e.g. terminate-after = 3) if the validation runs below show CPU-contended tests getting killed at 120s. Don't tune speculatively.
  • Do not add threads-required overrides yet — same reason. It's the documented escape hatch if one heavy test (e.g. a multi-bitcoind JD test) measurably thrashes.

Commit 5 — ci(integration): drop --nocapture so concurrent output stays readable

.github/workflows/integration-tests.yaml: remove --nocapture from the cargo nextest run invocation. With concurrency, live stdout from N tests interleaves into garbage; nextest's default captures per-test output and prints it (with RUST_LOG=debug logs intact) only for failing/flaky tests at the end — which is exactly the "final report lists failures, re-run individually for logs" workflow we want. Debug-rerun instructions go in the commit message and as a comment above the step:

# failure triage: re-run a single test with captured logs shown live:
#   RUST_LOG=debug cargo nextest run --manifest-path=integration-tests/Cargo.toml \
#     -E 'test(<name>)' --no-capture

Validation protocol (run between/after commits)

Local (dev machine has more cores than CI, so also simulate the runner):

# build once
cargo nextest run --manifest-path=integration-tests/Cargo.toml --no-run

# baseline (current behavior) — record wall time
cargo nextest run --manifest-path=integration-tests/Cargo.toml --test-threads=1

# simulated ubuntu-latest runner
cargo nextest run --manifest-path=integration-tests/Cargo.toml --test-threads=4
# simulated macos-latest runner
cargo nextest run --manifest-path=integration-tests/Cargo.toml --test-threads=3

# flake check: 3 consecutive runs at 4 threads, all must pass with zero retries-fired diffs
for i in 1 2 3; do cargo nextest run --manifest-path=integration-tests/Cargo.toml --test-threads=4; done

Acceptance criteria:

  • Wall time at --test-threads=4 meaningfully below the --test-threads=1 baseline (expect roughly 2–3×, not linear — many tests are I/O-wait-bound on bitcoind).
  • No new FLAKY entries across the 3 repeat runs. If a specific test goes flaky: first try threads-required = 2 override for it; only as a last resort relax its timing assertion — and say so in the commit message.
  • cargo +1.88 clippy --manifest-path integration-tests/Cargo.toml --all-targets and cargo fmt --check clean (CI gates).

Then push to the fork and compare the GitHub Integration Tests job wall time against a recent main run, on both ubuntu-latest and macos-latest (macOS has 3 vCPU/7 GB — it's the constraint; if macOS regresses while Linux improves, consider test-threads = 2 override via a [profile.ci-macos]... only if data says so).


Estimated improvement (from real CI data)

Baseline measured on main, ubuntu-latest, run 31215815112 (2026-08-07):

Component Time
Compile (cargo nextest run build phase) 3m 27s
Test execution, test-threads = 1 25m 40s (1540s, 108 tests, 0 failed)
Total Integration Tests step ~29m 20s

Per-test distribution extracted from the same log: mean ≈ 14s, max = 41s
(translator_integration::non_aggregated_translator_correctly_deals_with_group_channels),
with a thick band of 30–40s tests (pool_solo_mining_*, *_group_*_channels). The suite is
dominated by I/O-wait (bitcoind/sv2-tp startup, readiness gates, message waits), not CPU —
exactly the workload that parallelizes well.

Model for test-threads = "num-cpus":

Factor Effect
Ideal 4-way parallelism (ubuntu) 1540s / 4 ≈ 385s
CPU contention (1 mining thread + tokio runtime + bitcoind bursts per test slot) ×1.2–1.4 per-test slowdown
Cold-start artifact downloads (bitcoin-core ×2, sv2-tp, high_diff_chain) overlapped with other tests under the new flock; ≈ net-neutral vs. today
Tail effect (108 tests, max 41s → ≤ ~1 idle slot at the end) +~30s
Retries under contention assumed flat (acceptance criterion blocks merge otherwise)
Runner Serial today Expected concurrent Speedup
ubuntu-latest (4 vCPU) 25m 40s ~8–11 min ~2.5–3×
macos-latest (3 vCPU M1) ~29m 30s ~11–14 min ~2–2.5×

Including the unchanged ~3.5min compile, the Integration Tests job lands at roughly
12–15 min instead of ~29–30 min per OS. Queue-level effect (the actual bottleneck this PR
targets): each PR's ITFCI occupancy halves, so a stacked PR queue drains ~2× faster even
before GitHub-side runner concurrency is considered.

Bounds: pessimistic case (contention ×1.5 + one retry wave) ≈ 13–15 min test time on ubuntu —
still ≥1.7× faster than today. Optimistic case (I/O-wait dominance confirmed) ≈ 7 min.

Longest-pole check: 41s max test × 1.5 worst-case contention ≈ 62s — comfortably under the
existing 120s terminate-after, so no test should hit the kill timeout purely from concurrency.

These numbers are estimates; the validation protocol above is what confirms them — record the
branch's first CI run and compare Summary [ ... ] against 1540s before declaring victory.

Explicitly out of scope (YAGNI for this PR)

  • Splitting the suite into nextest partitions across matrix jobs — bigger win but orthogonal; revisit only if concurrency alone doesn't drain the queue enough.
  • ubuntu-slim / job-level restructuring.
  • Caching template-provider/ binaries in CI (actions/cache) — compatible follow-up, independent of this change.
  • Changing SHARES_PER_MINUTE or any test semantics.

@plebhash

plebhash commented Aug 8, 2026

Copy link
Copy Markdown
Member
EXECUTION report by DeekSeek V4 Pro:

Execution report: concurrent ITFCI via cargo nextest

Branch: speed-up-integration-tests (plebhash fork).
Base: GitGab19/speed-up-integration-tests @ b19dfe97 (#698).

What was committed

8917834a style(integration): cargo fmt + Cargo.lock update for libc dep
1cd62cac ci(integration): drop --nocapture so concurrent output stays readable
76659cf8 test(integration): run nextest at num-cpus instead of serial
d37ff847 test(integration): pin harness mining devices to one worker thread
0e569385 test(integration): serialize template-provider artifact downloads across processes
66a17269 test(integration): make port allocation race-free across test processes
b19dfe97 test(integration): stop probing Bitcoin Core's IPC socket with a connect   ← #698 top
efc4dca3 test(integration): gate on readiness instead of fixed sleeps, drop datadirs
ca5fd5f9 test(integration): poll on a 200ms interval instead of 1s
5181a166 test(integration): fix latent ordering races in the test harness

1. Cross-process-safe port allocation (66a17269)

Files: integration-tests/lib/utils.rs, integration-tests/Cargo.toml

Replaced the in-process-only UNIQUE_PORTS dedup set with per-port flock lockfiles
under $TMPDIR/sv2-it-ports/ held for the process lifetime. Two concurrent nextest
processes can no longer race on the same ephemeral port. Lockfiles are kernel-released
even on kill -9, so stale files are harmless.

Added libc = "0.2" dep (already in the crate's transitive tree via corepc-node).

2. Serialized artifact downloads (0e569385)

Files: integration-tests/lib/template_provider.rs, integration-tests/lib/utils.rs

Added with_exclusive_lock() helper: blocking exclusive flock + inner exists()
re-check so only the first process actually downloads; the rest wait and skip.
Applied to three critical sections:

  • Bitcoin Core tarball (v30.2 / v31.0)
  • sv2-tp tarball (v1.1.0)
  • high_diff_chain unpack

Also made tarball::unpack write temp-{pid}.tar.gz instead of a static filename.

3. Mining device pinned to 1 thread (d37ff847)

File: integration-tests/lib/mod.rs

start_mining_device_sv2() now calls crate::mining_device::set_cores(1).
Every test that mines through the harness gets a single hashing thread, which
keeps CPU oversubscription under control on 3–4 vCPU runners. Standalone binary
and benches are untouched.

4. test-threads = "num-cpus" (76659cf8)

File: integration-tests/.config/nextest.toml

Replaced test-threads = 1 with test-threads = "num-cpus" (4 on ubuntu-latest,
3 on macos-latest per GitHub docs). Added comment documenting NEXTEST_TEST_THREADS=1
for local sequential debugging.

5. Dropped --nocapture in CI (1cd62cac)

File: .github/workflows/integration-tests.yaml

Removed --nocapture from the cargo nextest run step. nextest captures per-test
stdout/stderr and prints it only for failures — the exact "final report lists failures,
re-run individually" workflow requested. A YAML comment above the step documents the
triage rerun command.

Rebase note: the original commit had a YAML indentation bug (- name: at column 0)
that caused "workflow file issue" on the first CI push. Rebased to fix the indent
and move the triage comment out of the run: block into a YAML comment.

6. fmt + lockfile (8917834a)

Files: integration-tests/Cargo.lock, integration-tests/lib/template_provider.rs,
integration-tests/lib/utils.rs

Applied cargo fmt (indentation fixes for the new closures) and updated Cargo.lock
for the new libc dependency.

Validation

  • cargo check --manifest-path integration-tests/Cargo.toml — clean
  • cargo clippy --manifest-path integration-tests/Cargo.toml --all-targets — only
    pre-existing warnings, none from these changes
  • cargo fmt --manifest-path integration-tests/Cargo.toml -- --check — clean
  • integration-tests.yaml: validated with python3 -c "import yaml; ..." — parses correctly

CI observation plan

  1. Push to plebhash fork → trigger Integration Tests workflow.
  2. Compare the Summary [ ... ] line against the baseline of 1540s (from
    the main run at 31215815112):
    • Expected on ubuntu-latest: ~460–660s (8–11 min test time vs 25m40s).
    • Expected on macos-latest: ~660–840s (11–14 min vs ~29m30s).
  3. Check for green across both OS matrix entries; zero FLAKY lines.
  4. If a specific test goes flaky: note it, re-run that test serially to confirm
    it isn't inherently broken, then consider threads-required = 2 override.
  5. If the entire run is flaky: revert test-threads to 1 (only last-config commit)
    and investigate — but keep the port-lock and download-lock commits (commit 1+2)
    because they are correctness fixes regardless of concurrency.

Open for follow-up

  • Per-test threads-required overrides — only if measurements show specific
    heavy tests (multi-bitcoind JD tests, etc.) thrashing.
  • Artifact cache (bitcoin-core / sv2-tp binaries in CI via actions/cache) —
    independent optimization; the flock already prevents first-run download races.
  • Suite partitioning across matrix jobs — bigger architectural change;
    revisit only if per-job wall time is still unacceptable after this.

proposed commits for cherry-picking:

@plebhash

plebhash commented Aug 8, 2026

Copy link
Copy Markdown
Member

PERFORMANCE report by DeekSeek V4 Pro:

Performance report: concurrent ITFCI

Run: plebhash/sv2-apps#4 · Actions #31263476121
Branch: speed-up-integration-tests @ 8917834a
Baseline: stratum-mining/main @ 31215815112test-threads = 1, serial execution

🏗️ Full job time

Platform Total job
🐧 ubuntu-latest ~9m 38s
🍎 macos-latest ~9m 24s

Total Integration Tests job time went from ~29m 30s to ~9m 30s — roughly
3.15× faster end-to-end.

from ~30min to ~9min

tbh 9 min feels still a bit prohibitive IMO, but great improvements overall


a good next step would be to try to explore the following area, which has been left out of plan scope as YAGNI (and is also somewhat aligned to what was done on #600):

Splitting the suite into nextest partitions across matrix jobs — bigger win but orthogonal; revisit only if concurrency alone doesn't drain the queue enough.

not sure I want to push for it now though, feels like an area that deserves deeper reasoning before we get our hands dirty

for example, one important aspect worth of consideration: this will also affect the dynamics of how the global org-level Github Runner quota will be consumed, which impacts how concurrent PRs affect eachother (which the model called "queue" on the quoted sentence above)

@GitGab19
GitGab19 force-pushed the speed-up-integration-tests branch from b19dfe9 to 5701391 Compare August 10, 2026 11:31
@GitGab19

Copy link
Copy Markdown
Member Author

@plebhash I cherry-picked your commits and added a1896b9 and 5701391 on top of them.

I did that to address the flakiness exposed by parallel execution of your PR (plebhash#4).

The first makes minerd setup and teardown concurrency-safe, replaces a fixed startup delay with a readiness check, and properly reaps child processes to prevent nextest LEAK reports.
The second handles a legitimate vardiff UpdateChannel arriving before SubmitSharesExtended, without changing the sniffer’s general behavior.

The latest Ubuntu and macOS runs completed without flaky or leaky tests.

The results are:

  • ubuntu runner --> 8m 52s
  • macos runner --> 10m 49s

@GitGab19
GitGab19 force-pushed the speed-up-integration-tests branch from b38fe09 to 2ae3023 Compare August 10, 2026 15:25
@GitGab19

Copy link
Copy Markdown
Member Author

Update: with the cache commit (f334f5b), now we're running all the jobs in ~8min!

@GitGab19
GitGab19 marked this pull request as ready for review August 10, 2026 15:53
@GitGab19
GitGab19 force-pushed the speed-up-integration-tests branch from 2ae3023 to df8ecd7 Compare August 10, 2026 15:54

@bit-aloo bit-aloo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Super ACK

GitGab19 and others added 14 commits August 11, 2026 10:14
Four tests polled on one condition and then asserted on another that
settles later. Each was masked by a coarse 1s poll interval that happened
to return late enough for the second condition to hold, rather than by
any actual synchronisation:

- `SnifferSV1::wait_and_assert` waited using a fuzzy matcher (which also
  accepts an `OkResponse` whose serialized form merely *contains* the
  filter string) and then re-fetched with a strict predicate, panicking
  "Message disappeared after wait_for_message" when the two disagreed.
  It now polls on exactly the predicate it fetches with.

- `test_extension_negotiation_with_tlv_in_submit_shares` popped
  `RequestExtensions` off the queue without waiting for it. It is sent
  *after* `SetupConnectionSuccess`, so the pop could precede the send.

- `pool_api_endpoints_with_miner` and `jdc_api_endpoints_with_miner`
  polled until a client was registered, then asserted that client already
  had a channel. The channel opens after registration. Both now poll on
  the channel count; `poll_until` takes `&str` so it can address the
  dynamic `/clients/{id}/channels` route.

- `non_aggregated_translator_correctly_deals_with_group_channels`
  compared a `mining.notify` prevhash before and after a chain tip
  update without clearing the queue, so the stale pre-update notify
  could be matched and the prevhash appeared unchanged. Adds
  `SnifferSV1::clean_queue`, mirroring the Sv2 sniffer.

Each was reproduced deterministically before being fixed.
The suite spent most of its wall clock asleep. Bucketing every timestamp
gap across a full run showed ~613s of dead time in loops that checked a
condition, missed, and then slept a full second — so a message arriving
in 30ms still cost ~1000ms.

Introduces two intervals in `utils` rather than one:

- `POLL_INTERVAL` (200ms) for message-wait loops, which exit as soon as
  their message lands.
- `CONNECT_RETRY_INTERVAL` (1s) for the unbounded connect-retry loops,
  which have no timeout and spin for as long as a peer is absent —
  something several tests arrange deliberately. Every test runs on a bare
  `#[tokio::test]`, i.e. a single-threaded runtime, so those loops must
  not busy-poll against the test's own work.

200ms is not a tuning artefact. Measured across full runs: 1s leaves
~613s of dead time, 200ms leaves ~87s, and 50ms leaves ~61s. The residual
at 50ms is real message latency rather than poll delay, so 200ms already
captures ~96% of everything recoverable and anything in the 150-250ms
range lands in the same place.

50ms was also actively harmful: it starved the single-threaded runtime
and produced a 120s hang (nextest's slow-timeout terminate-after) in 5 of
6 runs. At 200ms: 0 hangs in 7 runs.
…tadirs

`TemplateProvider::start` slept a flat 2s after spawning Bitcoin Core and
a further 3s after spawning sv2-tp — 5s per template provider, ~79 times
per run, whether or not either process was ready.

A fixed sleep couples two things that should be independent: how long you
wait when the machine is slow (safety) and how long you wait when it is
fast (speed). Polling separates them, so the ceilings here are *more*
generous than the sleeps they replace (30s) while the common path returns
as soon as the process is actually serving. Measured: Bitcoin Core IPC
socket ready in ~110us, sv2-tp in ~151ms.

Both gates prove serviceability rather than existence. The IPC gate
connects to `node.sock` rather than checking that the path exists,
because datadirs are keyed by port and a stale socket file from an
earlier test can satisfy a `Path::exists` check while refusing traffic —
that failure mode caused `tdp_io_integration_v30x` to run for 108s
against a node that never answered, and `jdp_io_integration_v30x` to time
out entirely.

Note that a connect probe is only safe against a listener that ascribes
no meaning to a bare connection. The pool is not such a listener — it
accepts every connection as a protocol session — so `start_pool` keeps a
plain sleep. Probing it created phantom downstreams that failed setup and
hung `jds_isolates_state_for_colliding_request_ids_across_downstreams` in
4 of 10 runs.

Also removes each node's datadir on drop. Nothing cleaned them up, so a
full run left ~1.5GB of `.bitcoin-{port}` directories behind and
successive runs accumulated until the filesystem filled. Retention is
still available via SV2_KEEP_TEST_DATADIR for post-mortem inspection of
debug.log and chainstate, since that is plausibly why a persistent
`staticdir` was chosen in the first place.
The IPC readiness gate connected to node.sock and dropped the connection
immediately. Core's libmultiprocess layer sets TCP_NODELAY on every
accepted connection, so that setsockopt ran against a socket which was
already gone. Linux tolerates it; macOS returns EINVAL, which surfaced as

  mp/proxy.cpp:45: error: Uncaught exception in daemonized task.;
  exception = kj/async-io-unix.c++:1365: failed:
  setsocketopt(IPPROTO_TCP, TCP_NODELAY): Invalid argument

and took down Core's IPC listener. sv2-tp could then never connect, so it
never bound its Sv2 port, and the gate after it failed with

  timeout after 30s waiting for sv2-tp to listen on 127.0.0.1:49215

This is the rule already documented for start_pool in efc4dca — a connect
probe is only safe against a listener that ascribes no meaning to a bare
connection — applied to a capnp RPC endpoint, where it plainly does not
hold. It went unnoticed because every run validating this branch was on
Linux.

The gate now waits for the socket to appear rather than connecting to it.
Stat-ing was previously rejected because a stale node.sock from an earlier
test on the same port could satisfy it, so this also removes any leftover
datadir for the port before starting the node, which closes that window
directly instead of by probing around it.
Replace the per-process-only UNIQUE_PORTS in-process dedup set with
flock-based per-port lockfiles held for the process lifetime.  Two
concurrent nextest test processes probing bind(0) to find free ports
can now never pick the same port because the non-blocking exclusive
flock is forced to fail on the loser.

Bitcoin Core datadirs are keyed by port (.bitcoin-{port}), so a
port collision meant two nodes sharing one datadir, not just a bind
error.  The lockfiles live under $TMPDIR/sv2-it-ports/ and are
released by the kernel on exit (including kill -9), so stale files
are harmless and never need cleanup.
…oss processes

Concurrent nextest processes on a cold CI runner would all see the
bitcoin-core / sv2-tp / high_diff_chain directories as missing and
simultaneously download+unpack into the same shared tree.

Guard each artifact with a blocking exclusive flock so only the first
process actually downloads; the rest wait on the lock, then skip via
an inner exists() re-check.  Also give tarball::unpack a pid-unique
temp filename so two processes can never share the same staging file
even if the lock is bypassed.
The mining_device auto mode spawns (logical_cpus - 1) hashing threads per
test process.  With test-threads > 1 this oversubscribes the CI runner and
makes timing-sensitive share-rate assertions flaky (SHARES_PER_MINUTE).

Call set_cores(1) in the test-harness helper start_mining_device_sv2 so
every test that mines through the harness gets a single hashing thread.
The standalone binary and benches (not part of ITFCI) keep auto mode.
Replace test-threads = 1 with test-threads = "num-cpus" so the
Integration Tests CI drains the job queue faster on free-tier runners
(4 vCPU on ubuntu-latest, 3 on macos-latest per GitHub docs).

Comment documents the NEXTEST_TEST_THREADS=1 override for local
sequential debugging.
With test-threads > 1, live stdout from N interleaving tests is
unreadable.  nextest captures per-test output and prints it only for
failing/flaky tests at the end.  The RUST_LOG=debug logs remain
captured; re-run with --no-capture is documented in the step comment
for manual triage.
Use a common target directory for workspace commands and configure
rust-cache from the integration-tests workspace.

Replace ineffective caches and fix the MSRV cache ordering while
keeping the existing checks unchanged.
@GitGab19
GitGab19 force-pushed the speed-up-integration-tests branch from df8ecd7 to 1111a69 Compare August 11, 2026 08:15
Publish downloaded artifacts atomically after extraction and signing, retain
the minerd proxy listener until startup, and tolerate vardiff SetTarget
messages at the affected assertion sites.

Also fix Cargo cache invalidation across workspaces, remove the unused
readiness probe, and correct stale diagnostics and documentation.
@GitGab19
GitGab19 force-pushed the speed-up-integration-tests branch from 1e50034 to 30b7dfb Compare August 11, 2026 09:14
@bit-aloo
bit-aloo merged commit e96c81f into stratum-mining:main Aug 11, 2026
12 checks passed
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.

3 participants