Skip to content

Commit 3ff5963

Browse files
authored
Merge pull request #608 from TypedDevs/perf/runner-reduce-forks
perf(runner): reduce subprocess forks in test runner hot path
2 parents cf63216 + ad9b751 commit 3ff5963

7 files changed

Lines changed: 48 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
- Cache function discovery to avoid duplicate pipeline per test file
3232
- Reduce subshells in test execution hot path
3333
- Batch coverage recording with in-memory buffering
34+
- Cache `uname` result at source time to eliminate repeated subprocess forks in OS detection
35+
- Replace `bc` and `awk` subprocesses with native bash arithmetic in clock and duration formatting
36+
- Cache `base64 -w` flag support at load time instead of detecting per test
37+
- Use direct variable access for assertion state instead of getter subshells in runner hot path
3438

3539
### Fixed
3640
- Mocking `mktemp` no longer breaks spy creation (#602)

src/check_os.sh

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ _BASHUNIT_OS="Unknown"
55
_BASHUNIT_DISTRO="Unknown"
66

77
function bashunit::check_os::init() {
8+
_BASHUNIT_UNAME="$(uname)"
89
if bashunit::check_os::is_linux; then
910
_BASHUNIT_OS="Linux"
1011
if bashunit::check_os::is_ubuntu; then
@@ -39,16 +40,18 @@ function bashunit::check_os::is_nixos() {
3940
grep -q '^ID=nixos' /etc/os-release 2>/dev/null
4041
}
4142

43+
_BASHUNIT_UNAME="$(uname)"
44+
4245
function bashunit::check_os::is_linux() {
43-
[[ "$(uname)" == "Linux" ]]
46+
[[ "$_BASHUNIT_UNAME" == "Linux" ]]
4447
}
4548

4649
function bashunit::check_os::is_macos() {
47-
[[ "$(uname)" == "Darwin" ]]
50+
[[ "$_BASHUNIT_UNAME" == "Darwin" ]]
4851
}
4952

5053
function bashunit::check_os::is_windows() {
51-
case "$(uname)" in
54+
case "$_BASHUNIT_UNAME" in
5255
*MINGW* | *MSYS* | *CYGWIN*)
5356
return 0
5457
;;

src/clock.sh

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,19 @@ EOF
108108
date-seconds)
109109
local seconds
110110
seconds=$(date +%s)
111-
bashunit::math::calculate "$seconds * 1000000000"
111+
echo "$((seconds * 1000000000))"
112112
;;
113113
shell)
114114
# shellcheck disable=SC2155
115115
local shell_time="$(bashunit::clock::shell_time)"
116116
local seconds="${shell_time%%.*}"
117117
local microseconds="${shell_time#*.}"
118-
bashunit::math::calculate "($seconds * 1000000000) + ($microseconds * 1000)"
118+
# Pad to 6 digits and strip leading zeros for arithmetic
119+
microseconds="${microseconds}000000"
120+
microseconds="${microseconds:0:6}"
121+
microseconds="${microseconds#"${microseconds%%[!0]*}"}"
122+
microseconds="${microseconds:-0}"
123+
echo "$(( (seconds * 1000000000) + (microseconds * 1000) ))"
119124
;;
120125
*)
121126
bashunit::clock::_choose_impl || return 1

src/console_results.sh

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,11 @@ function bashunit::console_results::print_execution_time() {
126126
return
127127
fi
128128

129-
local time=$(bashunit::clock::total_runtime_in_milliseconds | awk '{printf "%.0f", $1}')
129+
local time
130+
time=$(bashunit::clock::total_runtime_in_milliseconds)
131+
# Strip decimal portion (integer truncation, Bash 3.0 compatible)
132+
time="${time%%.*}"
133+
time="${time:-0}"
130134

131135
if [[ "$time" -lt 1000 ]]; then
132136
printf "${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" \
@@ -144,8 +148,10 @@ function bashunit::console_results::print_execution_time() {
144148
return
145149
fi
146150

151+
local integer_part=$((time / 1000))
152+
local decimal_part=$(( (time % 1000) / 10 ))
147153
local formatted_seconds
148-
formatted_seconds=$(awk "BEGIN {printf \"%.2f\", $time / 1000}")
154+
formatted_seconds=$(printf "%d.%02d" "$integer_part" "$decimal_part")
149155

150156
printf "${_BASHUNIT_COLOR_BOLD}%s${_BASHUNIT_COLOR_DEFAULT}\n" \
151157
"Time taken: ${formatted_seconds}s"
@@ -160,8 +166,10 @@ function bashunit::console_results::format_duration() {
160166
local seconds=$((time_in_seconds % 60))
161167
echo "${minutes}m ${seconds}s"
162168
elif [[ "$duration_ms" -ge 1000 ]]; then
169+
local integer_part=$((duration_ms / 1000))
170+
local decimal_part=$(( (duration_ms % 1000) / 10 ))
163171
local formatted_seconds
164-
formatted_seconds=$(awk "BEGIN {printf \"%.2f\", $duration_ms / 1000}")
172+
formatted_seconds=$(printf "%d.%02d" "$integer_part" "$decimal_part")
165173
echo "${formatted_seconds}s"
166174
else
167175
echo "${duration_ms}ms"
@@ -234,8 +242,10 @@ function bashunit::console_results::print_successful_test() {
234242
local seconds=$((time_in_seconds % 60))
235243
time_display="${minutes}m ${seconds}s"
236244
elif [[ "$duration" -ge 1000 ]]; then
245+
local integer_part=$((duration / 1000))
246+
local decimal_part=$(( (duration % 1000) / 10 ))
237247
local formatted_seconds
238-
formatted_seconds=$(awk "BEGIN {printf \"%.2f\", $duration / 1000}")
248+
formatted_seconds=$(printf "%d.%02d" "$integer_part" "$decimal_part")
239249
time_display="${formatted_seconds}s"
240250
else
241251
time_display="${duration}ms"

src/helpers.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,10 @@ function bashunit::helper::encode_base64() {
120120
return
121121
fi
122122

123-
if command -v base64 >/dev/null; then
124-
printf '%s' "$value" | base64 -w 0 2>/dev/null || printf '%s' "$value" | base64 | tr -d '\n'
123+
if [[ "$_BASHUNIT_BASE64_WRAP_FLAG" == true ]]; then
124+
printf '%s' "$value" | base64 -w 0
125+
elif command -v base64 >/dev/null; then
126+
printf '%s' "$value" | base64 | tr -d '\n'
125127
else
126128
printf '%s' "$value" | openssl enc -base64 -A
127129
fi

src/runner.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ function bashunit::runner::run_test() {
762762
return
763763
fi
764764

765-
if [[ "$current_assertions_failed" != "$(bashunit::state::get_assertions_failed)" ]]; then
765+
if [[ "$current_assertions_failed" != "$_BASHUNIT_ASSERTIONS_FAILED" ]]; then
766766
bashunit::state::add_tests_failed
767767
bashunit::reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions" "$subshell_output"
768768
bashunit::runner::write_failure_result_output "$test_file" "$fn_name" "$subshell_output"
@@ -779,7 +779,7 @@ function bashunit::runner::run_test() {
779779
return
780780
fi
781781

782-
if [[ "$current_assertions_snapshot" != "$(bashunit::state::get_assertions_snapshot)" ]]; then
782+
if [[ "$current_assertions_snapshot" != "$_BASHUNIT_ASSERTIONS_SNAPSHOT" ]]; then
783783
bashunit::state::add_tests_snapshot
784784
# In failures-only mode, suppress snapshot test output
785785
if ! bashunit::env::is_failures_only_enabled; then
@@ -790,15 +790,15 @@ function bashunit::runner::run_test() {
790790
return
791791
fi
792792

793-
if [[ "$current_assertions_incomplete" != "$(bashunit::state::get_assertions_incomplete)" ]]; then
793+
if [[ "$current_assertions_incomplete" != "$_BASHUNIT_ASSERTIONS_INCOMPLETE" ]]; then
794794
bashunit::state::add_tests_incomplete
795795
bashunit::reports::add_test_incomplete "$test_file" "$label" "$duration" "$total_assertions"
796796
bashunit::runner::write_incomplete_result_output "$test_file" "$fn_name" "$subshell_output"
797797
bashunit::internal_log "Test incomplete" "$label"
798798
return
799799
fi
800800

801-
if [[ "$current_assertions_skipped" != "$(bashunit::state::get_assertions_skipped)" ]]; then
801+
if [[ "$current_assertions_skipped" != "$_BASHUNIT_ASSERTIONS_SKIPPED" ]]; then
802802
bashunit::state::add_tests_skipped
803803
bashunit::reports::add_test_skipped "$test_file" "$label" "$duration" "$total_assertions"
804804
bashunit::runner::write_skipped_result_output "$test_file" "$fn_name" "$subshell_output"
@@ -1228,7 +1228,7 @@ function bashunit::runner::record_test_hook_failure() {
12281228
local hook_message="$2"
12291229
local status="$3"
12301230

1231-
if [[ -n "$(bashunit::state::get_test_hook_failure)" ]]; then
1231+
if [[ -n "$_BASHUNIT_TEST_HOOK_FAILURE" ]]; then
12321232
return "$status"
12331233
fi
12341234

src/state.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
#!/usr/bin/env bash
22

3+
# Cache base64 -w flag support (Alpine needs -w 0, macOS does not support -w)
4+
if base64 --help 2>&1 | grep -q -- "-w"; then
5+
_BASHUNIT_BASE64_WRAP_FLAG=true
6+
else
7+
_BASHUNIT_BASE64_WRAP_FLAG=false
8+
fi
9+
310
_BASHUNIT_TESTS_PASSED=0
411
_BASHUNIT_TESTS_FAILED=0
512
_BASHUNIT_TESTS_SKIPPED=0
@@ -218,7 +225,7 @@ function bashunit::state::export_subshell_context() {
218225

219226
local encoded_test_hook_message
220227

221-
if base64 --help 2>&1 | grep -q -- "-w"; then
228+
if [[ "$_BASHUNIT_BASE64_WRAP_FLAG" == true ]]; then
222229
# Alpine requires the -w 0 option to avoid wrapping
223230
encoded_test_output=$(echo -n "$_BASHUNIT_TEST_OUTPUT" | base64 -w 0)
224231
encoded_test_title=$(echo -n "$_BASHUNIT_TEST_TITLE" | base64 -w 0)

0 commit comments

Comments
 (0)