Docs for the Rust version; benchmarks; build.sh fixes

README, CLAUDE.md, man pages, the user guide and its screenshot script
now describe the Rust build and the intentional behaviour changes, which
docs/MIGRATION.md lists. Go and Rust micro-benchmarks for the same
operations, and scripts/bench.sh for end-to-end comparison. build.sh
exports its tree root to the per-target jobs and keeps logs out of the
output directory; riscv64 musl builds use getrandom's raw syscall
backend, since zig's musl getrandom never reaches the kernel there.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andy Dixon
2026-10-08 11:50:55 +01:00
co-authored by Claude
parent cddde5ca52
commit 53968f8822
18 changed files with 659 additions and 109 deletions
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Compare the Go oracle and the Rust build on this machine, end to end.
#
# scripts/bench.sh [WORKDIR] (default: target/bench, on the repo's disk)
# RUNS=5 BIG_MB=2048 SMALL=5000 scripts/bench.sh
#
# For each implementation, in a fresh key and vault on the same filesystem:
# add one BIG_MB file, get it back, add SMALL small files (1-16 KiB) as a
# folder, list the catalogue (filler plus SMALL+1 records), and get one small
# file. Each step is timed RUNS times (median, min, max), with peak RSS and
# peak thread count. Every step includes unlocking the key (two Argon2id
# runs). Both implementations fsync every object and record, so `add`
# timings include durable writes. Page caches are warm: dropping them needs
# root, so cold-cache numbers are not measured here.
#
# Needs: go (to build the oracle), cargo, /usr/bin/time (GNU).
set -euo pipefail
cd "$(dirname "$0")/.."
root=$PWD
work=${1:-$root/target/bench}
runs=${RUNS:-3}
big_mb=${BIG_MB:-1024}
small=${SMALL:-2000}
mkdir -p -- "$work"
work=$(cd -- "$work" && pwd -P)
[[ $work == "$root" || $work == / ]] && { echo "refusing WORKDIR=$work" >&2; exit 2; }
cargo build --release --locked --quiet
(cd oracle/go && CGO_ENABLED=0 go build -trimpath -o "$work/vault-go" .)
cp target/release/vault "$work/vault-rust"
data=$work/data
if [[ ! -f $data/big || $(stat -c %s "$data/big") != $((big_mb << 20)) ]]; then
rm -rf -- "$data"
mkdir -p -- "$data/small"
head -c $((big_mb << 20)) /dev/urandom >"$data/big"
for i in $(seq -w 1 "$small"); do
head -c $((1024 + RANDOM % 15360)) /dev/urandom >"$data/small/f$i"
done
fi
export VAULT_PASSPHRASE=bench-passphrase
# measure LABEL CMD...: one timed run, printing "seconds rss_kib threads".
measure() {
local out=$work/time.$$ peak=0 pid t
/usr/bin/time -f '%e %M' -o "$out" "$@" >/dev/null 2>&1 &
pid=$!
while kill -0 $pid 2>/dev/null; do
for t in /proc/[0-9]*/status; do
if grep -q "^PPid:[[:space:]]*$pid$" "$t" 2>/dev/null; then
n=$(awk '/^Threads:/{print $2}' "$t" 2>/dev/null || echo 0)
((n > peak)) && peak=$n
fi
done
sleep 0.05
done
wait $pid
echo "$(cat "$out") $peak"
rm -f -- "$out"
}
summarise() { # lines of "secs rss threads" -> median min max, max rss, max threads
sort -n | awk '{t[NR]=$1; if($2>r)r=$2; if($3>th)th=$3} END {printf "%8.2f s (min %.2f, max %.2f) rss %6.0f MiB threads %d\n", t[int((NR+1)/2)], t[1], t[NR], r/1024, th}'
}
for impl in go rust; do
bin=$work/vault-$impl
echo "== $impl ($(stat -c %s "$bin") bytes)"
for step in add-big get-big add-small list get-small; do
results=""
for run in $(seq 1 "$runs"); do
v=$work/run-$impl
rm -rf -- "$v"
mkdir -p -- "$v"
export VAULT_KEY=$v/key VAULT_DIR=$v/store
"$bin" genkey 2>/dev/null
"$bin" init 2>/dev/null
case $step in
get-big | list | get-small) "$bin" add "$data/big" 2>/dev/null; "$bin" add "$data/small" 2>/dev/null ;;
esac
case $step in
add-big) r=$(measure "$bin" add "$data/big") ;;
get-big) r=$(measure "$bin" get big -o "$v/big.out") ;;
add-small) r=$(measure "$bin" add "$data/small") ;;
list) r=$(measure "$bin" list) ;;
get-small) r=$(measure "$bin" get "small/f$(printf "%0${#small}d" 1)" -o "$v/small.out") ;;
esac
results+="$r"$'\n'
done
printf ' %-10s ' "$step"
printf '%s' "$results" | grep . | summarise
done
rm -rf -- "$work/run-$impl"
done