3 # Copyright © Tavian Barnes <tavianator@tavianator.com>
4 # SPDX-License-Identifier: 0BSD
7 [chromium]="https://chromium.googlesource.com/chromium/src.git"
8 [linux]="https://github.com/torvalds/linux.git"
9 [rust]="https://github.com/rust-lang/rust.git"
13 [chromium]=119.0.6036.2
18 COMPLETE_DEFAULT=(linux rust chromium)
19 EARLY_QUIT_DEFAULT=(chromium)
21 STRATEGIES_DEFAULT=(rust)
25 printf 'Usage: tailfin run %s [--default]\n' "${BASH_SOURCE[0]}"
26 printf ' [--complete] [--early-quit] [--print] [--strategies]\n'
27 printf ' [--build=...] [--bfs] [--find] [--fd]\n'
28 printf ' [--no-clean] [--help]\n\n'
31 printf ' Run the default set of benchmarks\n\n'
33 printf ' --complete[=CORPUS]\n'
34 printf ' Complete traversal benchmark. \n'
35 printf ' Default corpus is --complete="%s"\n\n' "${COMPLETE_DEFAULT[*]}"
37 printf ' --early-quit[=CORPUS]\n'
38 printf ' Early quitting benchmark. \n'
39 printf ' Default corpus is --early-quit=%s\n\n' "${EARLY_QUIT_DEFAULT[*]}"
41 printf ' --print[=CORPUS]\n'
42 printf ' Path printing benchmark. \n'
43 printf ' Default corpus is --print=%s\n\n' "${PRINT_DEFAULT[*]}"
45 printf ' --strategies[=CORPUS]\n'
46 printf ' Search strategy benchmark.\n'
47 printf ' Default corpus is --strategies=%s\n\n' "${STRATEGIES_DEFAULT[*]}"
49 printf ' --jobs[=CORPUS]\n'
50 printf ' Parallelism benchmark.\n'
51 printf ' Default corpus is --jobs=%s\n\n' "${JOBS_DEFAULT[*]}"
53 printf ' --build=COMMIT\n'
54 printf ' Build this bfs commit and benchmark it. Specify multiple times to\n'
55 printf ' compare, e.g. --build=3.0.1 --build=3.0.2\n\n'
57 printf ' --bfs[=COMMAND]\n'
58 printf ' Benchmark an existing build of bfs\n\n'
60 printf ' --find[=COMMAND]\n'
61 printf ' Compare against find\n\n'
63 printf ' --fd[=COMMAND]\n'
64 printf ' Compare against fd\n\n'
66 printf ' --no-clean\n'
67 printf ' Use any existing corpora as-is\n\n'
70 printf ' This message\n\n'
73 # Hack to export an array
75 local str=$(declare -p "$1" | sed 's/ -a / -ga /')
80 # Hack to import an array
87 # Set up the benchmarks
89 ROOT=$(realpath -- "$(dirname -- "${BASH_SOURCE[0]}")/..")
90 if ! [ "$PWD" -ef "$ROOT" ]; then
91 printf 'error: Please run this script from %s\n\n' "$ROOT" >&2
119 # bfs commits/tags to benchmark
122 BFS+=("bfs-${arg#*=}")
124 # Utilities to benchmark against
145 COMPLETE=("${COMPLETE_DEFAULT[@]}")
148 read -ra COMPLETE <<<"${arg#*=}"
151 EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}")
154 read -ra EARLY_QUIT <<<"${arg#*=}"
157 PRINT=("${PRINT_DEFAULT[@]}")
160 read -ra PRINT <<<"${arg#*=}"
163 STRATEGIES=("${STRATEGIES_DEFAULT[@]}")
166 read -ra STRATEGIES <<<"${arg#*=}"
169 JOBS=("${JOBS_DEFAULT[@]}")
172 read -ra JOBS <<<"${arg#*=}"
175 COMPLETE=("${COMPLETE_DEFAULT[@]}")
176 EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}")
177 PRINT=("${PRINT_DEFAULT[@]}")
178 STRATEGIES=("${STRATEGIES_DEFAULT[@]}")
179 JOBS=("${JOBS_DEFAULT[@]}")
186 printf 'error: Unknown option %q\n\n' "$arg" >&2
193 if ((UID == 0)); then
197 echo "Building bfs ..."
198 as-user make -s -j"$nproc" release all
200 as-user mkdir -p bench/corpus
203 for corpus in "${COMPLETE[@]}" "${EARLY_QUIT[@]}" "${PRINT[@]}" "${STRATEGIES[@]}" "${JOBS[@]}"; do
204 if ((cloned["$corpus"])); then
209 dir="bench/corpus/$corpus"
210 if ((CLEAN)) || ! [ -e "$dir" ]; then
211 as-user ./bench/clone-tree.sh "${URLS[$corpus]}" "${TAGS[$corpus]}" "$dir"{,.git}
215 if ((${#BUILD[@]} > 0)); then
216 echo "Creating bfs worktree ..."
218 worktree="bench/worktree"
219 as-user git worktree add -qd "$worktree"
220 defer as-user git worktree remove "$worktree"
222 bin="$(realpath -- "$SETUP_DIR")/bin"
225 for commit in "${BUILD[@]}"; do
227 echo "Building bfs $commit ..."
229 as-user git checkout -qd "$commit" --
230 as-user make -s -j"$nproc" release
231 if [ -e ./bin/bfs ]; then
232 as-user cp ./bin/bfs "$bin/bfs-$commit"
234 as-user cp ./bfs "$bin/bfs-$commit"
236 as-user make -s clean
240 # $SETUP_DIR contains `:` so it won't work in $PATH
241 # Work around this with a symlink
242 tmp=$(as-user mktemp)
243 as-user ln -sf "$bin" "$tmp"
245 export PATH="$tmp:$PATH"
252 export_array COMPLETE
253 export_array EARLY_QUIT
255 export_array STRATEGIES
258 if ((UID == 0)); then
265 # Runs hyperfine and saves the output
267 local tmp_md="$BENCH_DIR/.bench.md"
268 local md="$BENCH_DIR/bench.md"
269 local tmp_json="$BENCH_DIR/.bench.json"
270 local json="$BENCH_DIR/bench.json"
273 printf 'Nothing to do\n\n' | tee -a "$md"
277 hyperfine -w2 -M20 --export-markdown="$tmp_md" --export-json="$tmp_json" "$@" &>/dev/tty
278 cat "$tmp_md" >>"$md"
279 cat "$tmp_json" >>"$json"
280 rm "$tmp_md" "$tmp_json"
282 printf '\n' | tee -a "$md"
285 # Print the header for a benchmark group
287 printf "## $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md"
290 # Print the header for a benchmark subgroup
292 printf "### $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md"
295 # Print the header for a benchmark sub-subgroup
297 printf "#### $1\\n\\n" "${@:2}" | tee -a "$BENCH_DIR/bench.md"
300 # Benchmark the complete traversal of a directory tree
301 # (without printing anything)
302 bench-complete-corpus() {
303 total=$(./bin/bfs "$2" -printf '.' | wc -c)
305 subgroup "%s (%'d files)" "$1" "$total"
308 for bfs in "${BFS[@]}"; do
309 cmds+=("$bfs $2 -false")
312 for find in "${FIND[@]}"; do
313 cmds+=("$find $2 -false")
316 for fd in "${FD[@]}"; do
317 cmds+=("$fd -u '^$' $2")
320 do-hyperfine "${cmds[@]}"
323 # All complete traversal benchmarks
326 group "Complete traversal"
329 bench-complete-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
334 # Benchmark quiting as soon as a file is seen
335 bench-early-quit-corpus() {
337 max_depth=$(./bin/bfs "$dir" -printf '%d\n' | sort -rn | head -n1)
339 subgroup '%s (depth %d)' "$1" "$max_depth"
341 # Save the list of unique filenames, along with their depth
342 UNIQ="$BENCH_DIR/uniq"
343 ./bin/bfs "$dir" -printf '%d %f\n' | sort -k2 | uniq -uf1 >"$UNIQ"
345 for ((i = 2; i <= max_depth; i *= 2)); do
346 subsubgroup 'Depth %d' "$i"
348 # Sample random uniquely-named files at depth $i
349 export FILES="$BENCH_DIR/uniq-$i"
350 sed -n "s/^$i //p" "$UNIQ" | shuf -n20 >"$FILES"
351 if ! [ -s "$FILES" ]; then
356 for bfs in "${BFS[@]}"; do
357 cmds+=("$bfs $dir -name \$(shuf -n1 \$FILES) -print -quit")
360 for find in "${FIND[@]}"; do
361 cmds+=("$find $dir -name \$(shuf -n1 \$FILES) -print -quit")
364 for fd in "${FD[@]}"; do
365 cmds+=("$fd -usg1 \$(shuf -n1 \$FILES) $dir")
368 do-hyperfine "${cmds[@]}"
372 # All early-quitting benchmarks
375 group "Early termination"
378 bench-early-quit-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
383 # Benchmark printing paths without colors
384 bench-print-nocolor() {
385 subsubgroup '%s' "$1"
388 for bfs in "${BFS[@]}"; do
392 for find in "${FIND[@]}"; do
396 for fd in "${FD[@]}"; do
397 cmds+=("$fd -u --search-path $2")
400 do-hyperfine "${cmds[@]}"
403 # Benchmark printing paths with colors
404 bench-print-color() {
405 subsubgroup '%s' "$1"
408 for bfs in "${BFS[@]}"; do
409 cmds+=("$bfs $2 -color")
412 for fd in "${FD[@]}"; do
413 cmds+=("$fd -u --search-path $2 --color=always")
416 do-hyperfine "${cmds[@]}"
419 # All printing benchmarks
422 group "Printing paths"
424 subgroup "Without colors"
426 bench-print-nocolor "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
429 subgroup "With colors"
431 bench-print-color "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
436 # Benchmark search strategies
437 bench-strategies-corpus() {
440 if ((${#BFS[@]} == 1)); then
441 cmds=("$BFS -S "{bfs,dfs,ids,eds}" $2 -false")
442 do-hyperfine "${cmds[@]}"
444 for S in bfs dfs ids eds; do
445 subsubgroup '`-S %s`' "$S"
448 for bfs in "${BFS[@]}"; do
449 cmds+=("$bfs -S $S $2 -false")
451 do-hyperfine "${cmds[@]}"
456 # All search strategy benchmarks
459 group "Search strategies"
462 bench-strategies-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
467 # Benchmark parallelism
468 bench-jobs-corpus() {
471 if ((${#BFS[@]} + ${#FD[@]} == 1)); then
473 for bfs in "${BFS[@]}"; do
474 if "$bfs" -j1 -quit &>/dev/null; then
475 cmds+=("$bfs -j"{1,2,3,4,6,8,12,16}" $2 -false")
477 cmds+=("$bfs $2 -false")
481 for fd in "${FD[@]}"; do
482 cmds+=("$fd -j"{1,2,3,4,6,8,12,16}" -u '^$' $2")
485 do-hyperfine "${cmds[@]}"
487 for j in 1 2 3 4 6 8 12 16; do
488 subsubgroup '`-j%d`' $j
491 for bfs in "${BFS[@]}"; do
492 if "$bfs" -j1 -quit &>/dev/null; then
493 cmds+=("$bfs -j$j $2 -false")
494 elif ((j == 1)); then
495 cmds+=("$bfs $2 -false")
499 for fd in "${FD[@]}"; do
500 cmds+=("$fd -j$j -u '^$' $2")
503 if ((${#cmds[@]})); then
504 do-hyperfine "${cmds[@]}"
510 # All parallelism benchmarks
516 bench-jobs-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
521 # Print benchmarked versions
525 local md="$BENCH_DIR/bench.md"
527 printf '```console\n' >>"$md"
530 for bfs in "${BFS[@]}"; do
531 printf '$ %s --version | head -n1\n' "$bfs"
532 "$bfs" --version | head -n1
535 for find in "${FIND[@]}"; do
536 printf '$ %s --version | head -n1\n' "$find"
537 "$find" --version | head -n1
540 for fd in "${FD[@]}"; do
541 printf '$ %s --version\n' "$fd"
549 # Print benchmark details
556 # Run all the benchmarks
562 import_array COMPLETE
563 import_array EARLY_QUIT
565 import_array STRATEGIES
568 bench-complete "${COMPLETE[@]}"
569 bench-early-quit "${EARLY_QUIT[@]}"
570 bench-print "${PRINT[@]}"
571 bench-strategies "${STRATEGIES[@]}"
572 bench-jobs "${JOBS[@]}"