| Q |
A |
| OS |
macOS (arm64) |
| Shell & version |
bash 3.2.57 |
| bashunit version |
0.50.1 |
Summary
Under --parallel on a shell without EPOCHREALTIME, every worker re-probes the clock, and the probe forks perl. The probe count scales with the number of tests. It happens even when per-test timing is switched off, because deciding that timing is off is what triggers the probe.
Measured
PATH-shim census over perl, one file of 200 trivial tests, macOS arm64, bash 3.2.57:
| Mode |
perl execs |
--no-parallel |
3 |
--parallel |
202 |
One exec per test. A second measurement on the same host counted 404 over 202 distinct test ids, so the exact multiplier depends on settings, but it scales per test either way.
A/B with the impl resolved in the main shell before dispatch, 200 tests, --parallel: 895 ms to 620 ms on macOS. On Linux the probe resolves to date or a plain subshell, so the effect there is noise (3.2: 784 to 771; 5.3: 414 to 394).
Current behavior
bashunit::clock::init (src/util/clock.sh:233-235), called from bashunit:99:
function bashunit::clock::init() {
_BASHUNIT_START_TIME=$(bashunit::clock::now)
}
bashunit::clock::now resolves the implementation through _choose_impl, which assigns _BASHUNIT_CLOCK_NOW_IMPL (src/util/clock.sh:3, :5-17). That assignment happens inside the command substitution, so it dies with the subshell. The main shell is left with _BASHUNIT_CLOCK_NOW_IMPL still empty.
In a sequential run the first real clock read in the main shell repopulates it once, hence 3 execs. Under --parallel each test runs in a fork that inherits the empty value, so each one re-probes: shell_time fails (no EPOCHREALTIME before 5.0), date +%s%N fails (BSD date has no %N), then perl runs.
The reason this fires with timing off is bashunit::clock::is_expensive (src/util/clock.sh:95). BASHUNIT_SHOW_EXECUTION_TIME=auto consults it (src/config/env.sh:524-529) from needs_test_duration (src/runner/context.sh:132-137), once per test. Answering "is the clock expensive" requires choosing the impl, which forks the interpreter it is trying to avoid.
.claude/rules/perf-fork-budget.md states the parallel budget as "~11 forks for a 10-test file". That figure does not include these, because the census fixture there is sequential.
Expected behavior
Resolve the clock implementation once in the main shell, before any worker is dispatched, and let the forks inherit the resolved value. _choose_impl already seeds the return slot as part of probing (#802), so the fix is to call it outside a command substitution rather than through $( ).
Two related cleanups in the same area:
The RED test is a fork-budget assertion in tests/acceptance/bashunit_run_forks_test.sh: the perl count for a --parallel run must not grow with the number of tests.
Summary
Under
--parallelon a shell withoutEPOCHREALTIME, every worker re-probes the clock, and the probe forksperl. The probe count scales with the number of tests. It happens even when per-test timing is switched off, because deciding that timing is off is what triggers the probe.Measured
PATH-shim census over
perl, one file of 200 trivial tests, macOS arm64, bash 3.2.57:perlexecs--no-parallel--parallelOne exec per test. A second measurement on the same host counted 404 over 202 distinct test ids, so the exact multiplier depends on settings, but it scales per test either way.
A/B with the impl resolved in the main shell before dispatch, 200 tests,
--parallel: 895 ms to 620 ms on macOS. On Linux the probe resolves todateor a plain subshell, so the effect there is noise (3.2: 784 to 771; 5.3: 414 to 394).Current behavior
bashunit::clock::init(src/util/clock.sh:233-235), called frombashunit:99:bashunit::clock::nowresolves the implementation through_choose_impl, which assigns_BASHUNIT_CLOCK_NOW_IMPL(src/util/clock.sh:3,:5-17). That assignment happens inside the command substitution, so it dies with the subshell. The main shell is left with_BASHUNIT_CLOCK_NOW_IMPLstill empty.In a sequential run the first real clock read in the main shell repopulates it once, hence 3 execs. Under
--paralleleach test runs in a fork that inherits the empty value, so each one re-probes:shell_timefails (noEPOCHREALTIMEbefore 5.0),date +%s%Nfails (BSDdatehas no%N), thenperlruns.The reason this fires with timing off is
bashunit::clock::is_expensive(src/util/clock.sh:95).BASHUNIT_SHOW_EXECUTION_TIME=autoconsults it (src/config/env.sh:524-529) fromneeds_test_duration(src/runner/context.sh:132-137), once per test. Answering "is the clock expensive" requires choosing the impl, which forks the interpreter it is trying to avoid..claude/rules/perf-fork-budget.mdstates the parallel budget as "~11 forks for a 10-test file". That figure does not include these, because the census fixture there is sequential.Expected behavior
Resolve the clock implementation once in the main shell, before any worker is dispatched, and let the forks inherit the resolved value.
_choose_implalready seeds the return slot as part of probing (#802), so the fix is to call it outside a command substitution rather than through$( ).Two related cleanups in the same area:
clock::initshould usebashunit::clock::now_to_slotand read the slot, which is the patternrun_testalready uses (src/runner/exec.sh:542,:599). See Per-testrpadand per-file clock reads still go through$( )instead of a return slot #1348.is_expensiveis a pure property of the resolved impl. Once the impl is resolved in the parent, the per-test call is a variable read.The RED test is a fork-budget assertion in
tests/acceptance/bashunit_run_forks_test.sh: theperlcount for a--parallelrun must not grow with the number of tests.