Skip to content

perf(clock): skip per-test clock reads when the clock impl forks an interpreter #765

Description

@Chemaclass

Part of #761.

Problem

bashunit::runner::run_test reads the clock twice per test (src/runner.sh:1070 and src/runner.sh:1097). On shells without EPOCHREALTIME (bash < 5, i.e. macOS default 3.2) and without GNU date +%s%N (macOS/Alpine are explicitly excluded at src/clock.sh:22), bashunit::clock::now falls through to forking a perl interpreter per read (src/clock.sh:88-90). Measured: ~5-8ms per perl fork, so ~10-16ms per test, ~2400 perl processes and ~15-20s per full suite run on a stock Mac — plus the same tax inside every one of the 258 nested ./bashunit acceptance invocations.

The same two-reads-per-hook pattern exists for set_up_before_script/tear_down_after_script (src/runner.sh:1761,1778 and src/runner.sh:1900,1917) — per file, so much cheaper, but it rides the same code path.

Proposed behavior (needs ADR)

Per-test wall-clock timing becomes best-effort based on clock cost:

  1. Add bashunit::clock::is_expensive (or similar): true when the chosen impl forks an interpreter (perl, python, node, powershell); false for shell (EPOCHREALTIME) and date/date-seconds.
  2. Introduce an auto default for per-test timing: when the clock is expensive AND nothing needs per-test durations, skip BOTH clock::now reads in run_test. Per-test durations are consumed by:
    • execution-time display (bashunit::env::is_show_execution_time_enabled, src/env.sh:250, default true via _BASHUNIT_DEFAULT_SHOW_EXECUTION_TIME src/env.sh:103),
    • --profile (src/runner.sh:1101), --verbose (src/runner.sh:1105), and report writers (src/reports.sh).
      Suggested mechanics: change the default of BASHUNIT_SHOW_EXECUTION_TIME from "true" to "auto"; auto resolves to false only when the clock is expensive. An explicit BASHUNIT_SHOW_EXECUTION_TIME=true, --profile, --verbose, or an active report keeps precise timing (and its perl cost). Explicit false already skips display but currently NOT the clock reads — fix that too: when duration is not consumed, do not read the clock at all.
  3. When timing is skipped, the per-test duration renders as empty (verify print_successful_test / report writers handle an empty or 0 duration gracefully — decide and test one representation).
  4. Total suite runtime (bashunit::clock::total_runtime_in_milliseconds, 2 reads per run) stays as-is.
  5. Write an ADR in adrs/ documenting the decision and the alternative considered and rejected: a persistent co-process timestamp server over FIFOs (one fork per suite). Rejected because concurrent readers in --parallel mode would race on the FIFO without a portable lock (no flock on stock macOS), plus lifecycle/cleanup complexity.

If during implementation a materially better design emerges, propose it in the issue before building.

Tests first (TDD)

  • Unit tests for the impl-classification function (force _BASHUNIT_CLOCK_NOW_IMPL values).
  • Acceptance: on an expensive-clock system, default run shows no per-test times but correct totals; BASHUNIT_SHOW_EXECUTION_TIME=true restores them; --profile/report runs still produce durations. Use BASHUNIT_TEST_BASH_VERSION-style seams or env overrides rather than depending on the host — check how existing clock tests (tests/unit/clock_test.sh if present) fake impls.
  • Existing display/report tests must pass on Linux CI (bash 5, cheap clock: behavior unchanged there).

Constraints

  • Bash 3.0+ only: no declare -A, no [[ ]], no ${var,,}, no negative array indexing, no &>>.
  • No behavior change on fork-free clock systems (Linux CI, bash 5).
  • User-visible change (per-test times disappear by default on old-bash Macs): CHANGELOG.md entry + docs update required.

Acceptance criteria

  • ./bashunit tests/ and ./bashunit --parallel tests/ pass.
  • make sa, make lint pass; shfmt -w . produces no diff.
  • Fork census on macOS bash 3.2: perl count drops from ~2/test to ~2/run in a default run.
  • 100-noop benchmark before/after reported in PR (expect roughly 5s -> 3.5s or better).
  • ADR merged.

Line references valid at commit 4d80e7c.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions