diff --git a/CHANGELOG.md b/CHANGELOG.md index ad3f0a9b..c39f90b1 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: 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) - `assert_equals`/`assert_same` failures with multiline values now render a git word-diff below the header (requires git, opt out with `BASHUNIT_NO_DIFF=true`, respects `--no-color`); machine reports keep the raw values (#777) diff --git a/src/env.sh b/src/env.sh index 426af435..a1a9c22a 100644 --- a/src/env.sh +++ b/src/env.sh @@ -486,14 +486,23 @@ TERMINAL_WIDTH="$(bashunit::env::find_terminal_width)" CAT="$(command -v cat)" GREP="$(command -v grep)" MKTEMP="$(command -v mktemp)" -FAILURES_OUTPUT_PATH=$("$MKTEMP") -SKIPPED_OUTPUT_PATH=$("$MKTEMP") -INCOMPLETE_OUTPUT_PATH=$("$MKTEMP") -RISKY_OUTPUT_PATH=$("$MKTEMP") -PROFILE_OUTPUT_PATH=$("$MKTEMP") +# Deferred-output scratch files. Each used to be its own `mktemp` fork; at ~258 +# nested cold starts in the acceptance suite that is ~1.5k forks and dominates +# cold-start cost (#798). Derive them from one run-unique directory instead: +# `bashunit::random_str` is fork-free and every consumer appends with `>>` (which +# creates the file lazily) or guards reads with `[ -s ... ]`, so the files need +# not be pre-created. The random suffix keeps the directory unique across +# recursive and parallel invocations, matching TEMP_DIR_PARALLEL_TEST_SUITE. +_BASHUNIT_RUN_OUTPUT_DIR="${TMPDIR:-/tmp}/bashunit/run/${_BASHUNIT_OS:-Unknown}/$(bashunit::random_str 8)" +mkdir -p "$_BASHUNIT_RUN_OUTPUT_DIR" 2>/dev/null || true +FAILURES_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/failures" +SKIPPED_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/skipped" +INCOMPLETE_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/incomplete" +RISKY_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/risky" +PROFILE_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/profile" # Collects ":" for every failing test in a run so the # next --rerun-failed can replay just those. Shared across parallel subshells. -RERUN_FAILED_OUTPUT_PATH=$("$MKTEMP") +RERUN_FAILED_OUTPUT_PATH="$_BASHUNIT_RUN_OUTPUT_DIR/rerun-failed" # Initialize temp directory once at startup for performance BASHUNIT_TEMP_DIR="${TMPDIR:-/tmp}/bashunit/tmp" diff --git a/tests/acceptance/bashunit_coldstart_forks_test.sh b/tests/acceptance/bashunit_coldstart_forks_test.sh new file mode 100644 index 00000000..470442e8 --- /dev/null +++ b/tests/acceptance/bashunit_coldstart_forks_test.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Regression guard for #798. bashunit's cold start historically forked `mktemp` +# once per deferred-output scratch file (failures, skipped, incomplete, risky, +# profile, rerun). Across the acceptance suite's ~258 nested runs that is ~1.5k +# forks and dominates wall-clock. Those scratch files must now be derived from a +# single run directory (no per-file fork), so a cold start forks `mktemp` zero +# times for them. +function test_coldstart_does_not_fork_mktemp_per_scratch_file() { + 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 `mktemp` process executions: a traced line whose command token is + # an absolute path ending in `mktemp` with no arguments. This excludes the + # `command -v mktemp` probe and the `MKTEMP=` variable assignment. + local mktemp_forks + mktemp_forks="$(printf '%s\n' "$trace" | grep -cE '^\++ +/[^ ]*mktemp$' || true)" + + assert_less_than 1 "$mktemp_forks" +}