#!/usr/bin/env bash # # std.sh — shared "standard library" for the fastwc benchmark scripts. # # Every benchmark script lives in its own directory (benchmarks/files/words, # benchmarks/files/lines, benchmarks/stdin/piping) and sets two variables # before sourcing this file: # SCRIPT_DIR — the benchmark script's own directory (test data lives here) # REPO_DIR — the repository root (release binary and tools live here) # # Provides the helpers every benchmark script needs: # checkfastwc() verify a release build of fastwc exists # checkwc() locate the coreutils wc implementation # createtxt() create (or reuse) a text file with N such lines # time_ms() run a command once, print elapsed wall time in ms # capture_count() print the first whitespace-separated field of output # run_case() race fastwc against the reference on a file argument # run_stdin_case() same, but feeding the file through standard input # run_solo_case() time fastwc alone (no reference) and print throughput # run_cases() run run_case for a list of sizes, fail-fast # run_stdin_cases() run run_stdin_case for a list of sizes, fail-fast # write_failed_report() write the human readable failure report # # The suites fail fast: the moment fastwc is slower than (or disagrees # with) the reference wc, a human readable report is written to # FAILED-benchmark.txt next to the suite and it returns non-zero. set -u # Pin the C locale: GNU wc -w silently switches to multibyte decoding under a # UTF-8 locale, which would slow the oracle down and mask the documented # byte-semantics divergence. Both sides count bytes here. export LC_ALL=C export LC_CTYPE=C FASTWC="$REPO_DIR/bin/release/fastwc" DATA_DIR="$SCRIPT_DIR/.data" GENFILE="$REPO_DIR/benchmarks/tools/genfile" # optional C helper, built by test-all.sh BENCH_NAME="${BENCH_NAME:-wc}" # set by the caller: coreutils BENCH_REPS="${BENCH_REPS:-3}" # interleaved runs per case; minimum is kept RESULT_ROWS="" # accumulated results table WC_CMD=() # filled by checkwc() TEXT_FILE="" # filled by createtxt() if [[ -z "$BENCH_REPS" || "$BENCH_REPS" -lt 1 ]]; then BENCH_REPS=1 fi # checkfastwc — make sure the release build exists and is executable. checkfastwc() { printf 'checking for release build fastwc... ' if [[ -x "$FASTWC" ]]; then printf 'yes\n' return 0 fi printf 'no\n' printf 'configure: error: no release build of fastwc found at %s\n' "$FASTWC" >&2 printf 'configure: error: run "make release" first to generate one\n' >&2 exit 1 } # checkwc — locate the coreutils wc implementation and store its invocation # in $WC_CMD. Exits on failure. checkwc() { printf 'checking for coreutils wc... ' if command -v wc >/dev/null 2>&1 \ && wc --version 2>/dev/null | head -n1 | grep -qi 'GNU coreutils'; then printf 'yes\n' WC_CMD=(wc) else printf 'no\n' printf 'configure: error: GNU Coreutils wc not found in PATH\n' >&2 exit 1 fi } # createrandstr — print one random 10-character alphanumeric string. createrandstr() { local chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' local out='' i for ((i = 0; i < 10; i++)); do out+="${chars:$((RANDOM % ${#chars})):1}" done printf '%s\n' "$out" } # createtxt — make sure a text file with rows of random # 10-character alphanumeric strings exists in this suite's .data directory. # A copy generated by a previous run is reused (checked by exact byte size: # 10 chars + '\n' per line), so repeated benchmark runs are cheap. # Prints the path and sets $TEXT_FILE; returns non-zero if generation fails. createtxt() { local lines="$1" local expect=$((lines * 11)) local have=0 TEXT_FILE="$DATA_DIR/words-$lines.txt" if [[ -f "$TEXT_FILE" ]]; then have=$(stat -c '%s' "$TEXT_FILE" 2>/dev/null || printf '0') fi if [[ "$have" -ne "$expect" ]]; then mkdir -p "$DATA_DIR" if [[ -x "$GENFILE" ]]; then "$GENFILE" "$lines" > "$TEXT_FILE" || { printf 'createtxt: error: failed to generate %s\n' "$TEXT_FILE" >&2 return 1 } else printf 'createtxt: warning: %s not built, using slow shell fallback\n' "$GENFILE" >&2 printf 'createtxt: warning: run ./test-all.sh to build the helper tools\n' >&2 : > "$TEXT_FILE" for ((i = 0; i < lines; i++)); do createrandstr >> "$TEXT_FILE" done fi fi printf '%s\n' "$TEXT_FILE" } # time_ms — run a command once and print elapsed wall time in ms. time_ms() { local s e s=$(date +%s%N) "$@" >/dev/null 2>&1 e=$(date +%s%N) printf '%s\n' "$(( (e - s) / 1000000 ))" } # time_us — run a command once and print elapsed wall time in µs. # More precise than time_ms: sub-millisecond runs come out as e.g. 812, # not 0. The benchmark keeps the µs reading for fastwc and derives the ms. time_us() { local s e s=$(date +%s%N) "$@" >/dev/null 2>&1 e=$(date +%s%N) printf '%s\n' "$(( (e - s) / 1000 ))" } # capture_count — print the first whitespace-separated field of a # command's output, i.e. the count reported by `wc -w/-l` or `fastwc -w/-l`. capture_count() { "$@" 2>/dev/null | awk 'NR == 1 { print $1 }' } # write_failed_report