Skip to content

feat: add parallelization - #54

Draft
brunoerg wants to merge 4 commits into
mainfrom
parallel
Draft

brunoerg wants to merge 4 commits into
mainfrom
parallel

Conversation

@brunoerg

Copy link
Copy Markdown
Owner

No description provided.

Comment thread src/parallel.rs Outdated
);
if !execution.success {
let setup_hint = if phase == "baseline" {
" A fresh parallel worktree has no existing build directory; use \

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.

Every baseline failure returns
this error

*** No errors detected
Traceback (most recent call last):
  File "/tmp/bcore-mutation-workers-KSJzCG/worker-0/./build/test/functional/test_runner.py", line 962, in <module>
    main()
  File "/tmp/bcore-mutation-workers-KSJzCG/worker-0/./build/test/functional/test_runner.py", line 464, in main
    os.makedirs(tmpdir)
  File "/home/ubuntu/.pyenv/versions/3.10.14/lib/python3.10/os.py", line 225, in makedirs
    mkdir(name, mode)
FileExistsError: [Errno 17] File exists: '/tmp/test_runner_₿_🏃_20260817_212053'

Error: InvalidInput("baseline command failed in worker 0. A fresh parallel worktree has no existing build directory; use --setup-command if the test command does not configure it.")

analysis command had --setup-command, the error message is misleading
bcore-mutation analyze --sqlite mutation.db --run-id=2 --parallel 2 --file-path="src/net_processing.cpp" --setup-command 'cmake -B build && cmake --build build -j4 --target bitcoind test_bitcoin' -c 'cmake --build build -j4 --target bitcoind test_bitcoin && ./build/bin/test_bitcoin --run_test=denialofservice_tests,peerman_tests,net_tests,txdownload_tests && ./build/test/functional/test_runner.py --failfast p2p_handshake.py p2p_leak.py p2p_sendtxrcncl.py p2p_tx_download.py p2p_blocksonly.py p2p_addrv2_relay.py p2p_node_network_limited.py'
the actual error was from test_runner , got the error after the analysis had run for about 50 minutes

@naiyoma

naiyoma commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Not sure if i am testing this right

But i generated mutants for verack message and then started analysis with this command

bcore-mutation analyze --sqlite mutation.db --run-id=2 --parallel 2 -t 3600 \                                                  
      --file-path="src/net_processing.cpp" \                                                                                                                                            
      --setup-command 'cmake -B build && cmake --build build -j4 --target bitcoind test_bitcoin' \                                                                                      
      -c 'cmake --build build -j4 --target bitcoind test_bitcoin && ./build/bin/test_bitcoin --run_test=denialofservice_tests,peerman_tests,net_tests,txdownload_tests &&               
    ./build/test/functional/p2p_handshake.py && ./build/test/functional/p2p_leak.py && ./build/test/functional/p2p_sendtxrcncl.py && ./build/test/functional/p2p_tx_download.py &&      
    ./build/test/functional/p2p_blocksonly.py && ./build/test/functional/p2p_addrv2_relay.py && ./build/test/functional/p2p_node_network_limited.py' \                                  
      2>&1 | tee parallel-run2.log

Observation

both workers were visibly running I could see them in the process list and the build directories growing. Then, roughly 45 minutes in,output stopped and never resumed. From the terminal I had no way to tell whether the build was still progressing, the baseline was running, a mutant was executing normally, or something had deadlocked. All four states look identical: no output.

While looking at /proc from a second shell

the two in-flight mutants had:

state = S wchan = futex_wait_queue utime+stime = 13 ticks
With -t 3600 each of those took a full hour before being killed

I think it makes sense to me now that i would experience more timeouts

I stopped the run at ~100 minutes with zero mutant results recorded.

Maybe we can add a periodic [worker N] mutant 53, 12m elapsed

i am still testing using verack I’ll see how long this it takes probably more than 4 hours

@naiyoma

naiyoma commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

bcore-mutation analyze --sqlite mutation.db --run-id=1 --parallel 2 -t 1200 --file-path="src/net_processing.cpp" --setup-command 'cmake -B build && cmake --build build -j4 --target bitcoind test_bitcoin' -c 'cmake --build build -j4 --target bitcoind test_bitcoin && ./build/test/functional/p2p_sendheaders.py' 2>&1 | tee run1.log

This time, I only had 2 mutants.
It took 44 minutes.
Both mutants were killed.
This one went well, and I got the same output I got when running without - -parallel

Parallel workers get an isolated Git worktree, but their commands inherited
the parent process environment, so every worker shared one system temporary
directory and one functional test port range.

Bitcoin Core's test_runner.py names its scratch directory after a timestamp
with second granularity and creates it without exist_ok. Workers start
together and do identical work, so they reach that line within the same
second and all but one fail with FileExistsError. Functional test ports are
derived from the test index rather than the process, so concurrent workers
running the same test list would then bind the same ports.

Give each worker a private temporary directory (TMPDIR, TMP, TEMP) and its
own TEST_RUNNER_PORT_MIN. A runner spans three port ranges, so only three
workers fit below the maximum port number; higher --parallel values now warn
instead of silently sharing ports.

Baseline failures also suggested --setup-command even when one was provided,
hiding the real error. The hint is now conditional, and the message quotes
the tail of the failing command's output.
@brunoerg

Copy link
Copy Markdown
Owner Author

Thanks, @naiyoma. I pushed two commits to fix some things you noticed. One of the issues is that parallel workers get an isolated Git worktree, but their commands inherited the parent process environment, so every worker shared one system temporary directory and one functional test port range. Also, changed it as commands now stream their output.

@brunoerg

Copy link
Copy Markdown
Owner Author

bcore-mutation analyze --sqlite mutation.db --run-id=1 --parallel 2 -t 1200 --file-path="src/net_processing.cpp" --setup-command 'cmake -B build && cmake --build build -j4 --target bitcoind test_bitcoin' -c 'cmake --build build -j4 --target bitcoind test_bitcoin && ./build/test/functional/p2p_sendheaders.py' 2>&1 | tee run1.log

This time, I only had 2 mutants. It took 44 minutes. Both mutants were killed. This one went well, and I got the same output I got when running without - -parallel

Any reason to use 1200 seconds as timeout?

@naiyoma

naiyoma commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Any reason to use 1200 seconds as timeout?

I was initially thinking that the default -t 300 was causing the 40 minutes of silence that I experienced, so I tried increasing it to see if it would improve anything. but, there was no observable difference.

@naiyoma

naiyoma commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Thanks, @naiyoma. I pushed two commits to fix some things you noticed. One of the issues is that parallel workers get an isolated Git worktree, but their commands inherited the parent process environment, so every worker shared one system temporary directory and one functional test port range. Also, changed it as commands now stream their output.

nice, will test again

@naiyoma

naiyoma commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

I’ve retested it with verack, exactly as I did here. #54 (comment), Got a timeout

Cross compiling ....................... FALSE
C++ compiler .......................... GNU 13.3.0, /usr/lib/ccache/c++
CMAKE_BUILD_TYPE ...................... RelWithDebInfo
Preprocessor defined macros ........... 
C++ compiler flags .................... -O2 -g -std=c++20 -fPIC -fno-extended-identifiers -fmacro-prefix-map=/tmp/bcore-mutation-workers-tJ9FHt/worker-0/src=. -fstack-reuse=none -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -Wstack-protector -fstack-protector-all -fcf-protection=full -fstack-clash-protection -Wall -Wextra -Wformat -Wformat-security -Wvla -Wredundant-decls -Wdate-time -Wduplicated-branches -Wduplicated-cond -Wlogical-op -Woverloaded-virtual -Wsuggest-override -Wimplicit-fallthrough -Wunreachable-code -Wbidi-chars=any -Wundef -Wno-unused-parameter
Linker flags .......................... -O2 -g -fstack-reuse=none -fstack-protector-all -fcf-protection=full -fstack-clash-protection -Wl,-z,relro -Wl,-z,now -Wl,-z,separate-code -fPIE -pie

NOTE: The summary above may not exactly match the final applied build flags
      if any additional CMAKE_* or environment variables have been modified.
      To see the exact flags applied, build with the --verbose option.

Treat compiler warnings as errors ..... OFF
Use ccache for compiling .............. ON


gmake[3]: *** [src/test/CMakeFiles/test_bitcoin.dir/build.make:1671: src/test/CMakeFiles/test_bitcoin.dir/transaction_tests.cpp.o] Terminated
gmake[2]: *** [CMakeFiles/Makefile2:1843: src/test/CMakeFiles/test_bitcoin.dir/all] Terminated
gmake[1]: *** [CMakeFiles/Makefile2:1850: src/test/CMakeFiles/test_bitcoin.dir/rule] Terminated
gmake: *** wait: No child processes.  Stop.
gmake: *** Waiting for unfinished jobs....
gmake: *** wait: No child processes.  Stop.

Error: InvalidInput("setup command failed in worker 0 (timed out after 3600s).")

i think the default timeout is too low trying now with 10800

@naiyoma

naiyoma commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Retested with this mutant #54 (comment), which is shorter. I got the same output, and it was quicker this time 30 minutes.

STDERR:
[node 1] Cleaning up leftover process
[node 0] Cleaning up leftover process

KILLED ✅
Executing command: git restore src/net_processing.cpp
Command exit code: 0

MUTATION SCORE: 100.00% (3 killed / 3 total)
Survived: 0

@naiyoma

naiyoma commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

@brunoerg My concern so far is that longer mutants seem to always time out, and I think this is because of the default.

/// Build and test commands for a single project.
pub trait ProjectCommands {
    /// Full clean build, run once before analyzing a folder when the user did
    /// not supply an explicit `--command`.
    fn build_command(&self) -> String;

    /// Timeout, in seconds, allowed for [`ProjectCommands::build_command`].
    fn build_timeout_secs(&self) -> u64 {
        3600 // 1 hour
    }

@naiyoma

naiyoma commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

I compared per-mutant analysis time between the normal (non-parallel) run and
--parallel, on the same mutant set, same machine, same test command.

68 mutants come from version and verack messages
(3833–4154), so I generated them as one range instead of two and got 68
mutants. Analyzing both together under --parallel took less time than
analyzing each separately and produced similar outcomes

My takeaway from this is that I could merge batches of mutants, and running them in parallel would save time

Mutants non-parallel parallel (N=2) saves winner
8 16 min 23 min −7 min non-parallel
16 30 min 30 min 0 min tie
68 119:43 min 76:06 min 43.6 min parallel

@naiyoma

naiyoma commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

still looking into this timeout #54 (comment), the issue might have been on my end, i didn't get this today

@naiyoma

naiyoma commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

I compared per-mutant analysis time between the normal (non-parallel) run and --parallel, on the same mutant set, same machine, same test command.

68 mutants come from version and verack messages (3833–4154), so I generated them as one range instead of two and got 68 mutants. Analyzing both together under --parallel took less time than analyzing each separately and produced similar outcomes

My takeaway from this is that I could merge batches of mutants, and running them in parallel would save time

Mutants non-parallel parallel (N=2) saves winner
8 16 min 23 min −7 min non-parallel
16 30 min 30 min 0 min tie
68 119:43 min 76:06 min 43.6 min parallel

Next, I’ll combine 4 messages. This will be around 100 mutants or more, and then I’ll compare the times. I expect parallel to have a better outcome.

@naiyoma

naiyoma commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

The more mutants, the more time parallel saves.

A worker printed nothing until its command finished, so a 45 minute build, a
running baseline, a working mutant and a stalled command all looked the same
from the terminal. Commands now stream their output instead of buffering it,
and report every 60 seconds how long they have been running along with their
most recent line of output.

Streaming also fixes a stall. Waiting for a buffered capture waits for the
output pipes to close, not just for the shell to exit, so any background
process that outlives the command kept the worker blocked with no CPU use and
nothing printed until the timeout expired. The pipes are now drained with a
grace period, after which the lingering processes are terminated.

Commands run in their own process group, and a timeout terminates the whole
group rather than only the shell. A leftover build or bitcoind node would
otherwise keep holding ports and datadirs, and turn one timeout into timeouts
for every mutant that followed.

The private process group also means the terminal no longer delivers Ctrl+C
to the workers' builds and tests, so cancellation has to reach them through
the tool. Every command of a run holds a cancellation signal; on Ctrl+C the
workers terminate their process trees with the same grace period as a
timeout, the orchestrator drains their results so interrupted mutants are
recorded as errors rather than left running, and only then are the
worktrees removed. Aborting the worker tasks instead would only have killed
the shell and orphaned everything it started.
@brunoerg

brunoerg commented Sep 7, 2026

Copy link
Copy Markdown
Owner Author

The more mutants, the more time parallel saves.

Perfect!

@naiyoma

naiyoma commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

@brunoerg

I generated 127 mutants for 9 messages covering VERSION, VERACK, SENDCMPCT, WTXIDRELAY, SENDADDRV2, FEATURE, SENDTXRCNCL, ADDR/ADDRV2, and INV.

I analyzed them using --parallel 2 and got 31 surviving mutants in total.

I then compared these results with the individual runs I did previously, where I analyzed each message separately. I remember it taking around 6–7 hours to get to the INV message, whereas the parallel run took ~2 hours, which is much faster.

If you look at the table below, you’ll see that the individual runs had more surviving mutants.

When I compared the two, there were two things that affected the results. One is that I was running on different head commits on master. I also think that some mutants that survived in the individual runs did not survive in the parallel run because the Combined (parallel) run ran every test against every mutant, whereas each individual run used only the tests for its own handler.

Run Mutants Killed Survived Kill Rate Time
Combined (parallel) 127 96 31 75.6% ~2 hours
Individual runs 123 74 49 60.2% ~6 hours

@brunoerg

brunoerg commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

@brunoerg

I generated 127 mutants for 9 messages covering VERSION, VERACK, SENDCMPCT, WTXIDRELAY, SENDADDRV2, FEATURE, SENDTXRCNCL, ADDR/ADDRV2, and INV.

I analyzed them using --parallel 2 and got 31 surviving mutants in total.

I then compared these results with the individual runs I did previously, where I analyzed each message separately. I remember it taking around 6–7 hours to get to the INV message, whereas the parallel run took ~2 hours, which is much faster.

If you look at the table below, you’ll see that the individual runs had more surviving mutants.

When I compared the two, there were two things that affected the results. One is that I was running on different head commits on master. I also think that some mutants that survived in the individual runs did not survive in the parallel run because the Combined (parallel) run ran every test against every mutant, whereas each individual run used only the tests for its own handler.

Run Mutants Killed Survived Kill Rate Time
Combined (parallel) 127 96 31 75.6% ~2 hours
Individual runs 123 74 49 60.2% ~6 hours

Great time results.

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