diff --git a/CHANGELOG.md b/CHANGELOG.md index c39f90b1..08995cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - `--jobs auto` / `-j auto` caps parallel concurrency at the CPU core count (portable across Linux/macOS/BSD); the default stays unlimited (#766) ### Changed +- Faster cold start: selecting the `perl` clock reads the time once instead of an empty `Time::HiRes` probe followed by a separate first read (2 -> 1 `perl` fork per cold start on bash 3.2 / macOS). No behaviour change - Faster cold start: the deferred-output scratch files (failures/skipped/incomplete/risky/profile/rerun) now share one run-unique directory instead of forking `mktemp` once each (6 -> 0 `mktemp` forks per cold start; ~62ms -> ~50ms per nested run on bash 3.2). No behaviour change (#798) - Faster cold start: `check_os` detects the OS with a single `uname` fork at load instead of two (matters across the acceptance suite's nested `bashunit` runs). No behaviour change (#798) - Faster test execution: removed the remaining round-2 fork leftovers — `wait_for_job_slot` counts running background jobs without forking `wc` on every poll (145 -> 0 `wc` forks across a small parallel run), and `check_duplicate_functions` folds its per-file grep+awk+sort+uniq scan into a single awk pass. No behaviour change (#761) diff --git a/src/clock.sh b/src/clock.sh index f6501b40..a81cf888 100644 --- a/src/clock.sh +++ b/src/clock.sh @@ -33,12 +33,23 @@ function bashunit::clock::_choose_impl() { esac fi - # 3. Try Perl with Time::HiRes + # 3. Try Perl with Time::HiRes. Probe by reading the actual time (not an empty + # `-e ""`) so the pending first read reuses this fork instead of paying a + # second one; a non-digit/empty result means perl or Time::HiRes is missing, so + # fall through. The value is seeded into the return slot for now_to_slot. attempts[attempts_count]="Perl" attempts_count=$((attempts_count + 1)) - if bashunit::dependencies::has_perl && perl -MTime::HiRes -e "" &>/dev/null; then - _BASHUNIT_CLOCK_NOW_IMPL="perl" - return 0 + if bashunit::dependencies::has_perl; then + local perl_now + perl_now="$(perl -MTime::HiRes -e 'printf("%.0f\n", Time::HiRes::time() * 1000000000)' 2>/dev/null)" + case "$perl_now" in + '' | *[!0-9]*) ;; + *) + _BASHUNIT_CLOCK_NOW_IMPL="perl" + _BASHUNIT_CLOCK_NOW_OUT="$perl_now" + return 0 + ;; + esac fi # 4. Try Python 3 with time module @@ -99,7 +110,14 @@ _BASHUNIT_CLOCK_NOW_OUT="" # Returns: 0 on success, 1 when no clock implementation is available. function bashunit::clock::now_to_slot() { if [ -z "$_BASHUNIT_CLOCK_NOW_IMPL" ]; then + _BASHUNIT_CLOCK_NOW_OUT="" bashunit::clock::_choose_impl || return 1 + # _choose_impl may have already read the current time while selecting an + # interpreter impl (e.g. perl); reuse that value for this first read instead + # of forking a second interpreter. + if [ -n "$_BASHUNIT_CLOCK_NOW_OUT" ]; then + return 0 + fi fi case "$_BASHUNIT_CLOCK_NOW_IMPL" in diff --git a/tests/acceptance/bashunit_coldstart_forks_test.sh b/tests/acceptance/bashunit_coldstart_forks_test.sh index 470442e8..3dec865c 100644 --- a/tests/acceptance/bashunit_coldstart_forks_test.sh +++ b/tests/acceptance/bashunit_coldstart_forks_test.sh @@ -23,3 +23,25 @@ function test_coldstart_does_not_fork_mktemp_per_scratch_file() { assert_less_than 1 "$mktemp_forks" } + +# Regression guard for the clock cold start. Selecting the `perl` clock impl used +# to fork perl twice: once for an empty `Time::HiRes` probe and once for the +# first real time read. Choosing an interpreter impl must cost at most one fork +# now (the probe reads the actual time and seeds it for the first read). On +# platforms that pick a fork-free/`date` clock (e.g. Linux) this is zero. +function test_coldstart_selects_clock_impl_without_a_probe_fork() { + if bashunit::check_os::is_windows; then + bashunit::skip "process tracing is unreliable under Git Bash" && return + fi + + local trace + trace="$(PS4='+ ' bash -x ./bashunit --version 2>&1 >/dev/null)" + + # Count real interpreter executions used by the clock (perl/python/node) at + # cold start. `command -v ` is a shell builtin and never appears as an + # executed command line, so it is not counted. + local interp_forks + interp_forks="$(printf '%s\n' "$trace" | grep -cE '^\++ +(perl|python|node) ' || true)" + + assert_less_or_equal_than 1 "$interp_forks" +}