Add chain-DB benchmark harness (contrib/bench/)

bench-chaindb.sh times FastImportBlockFile() under each backend using a
user-supplied blk0001.dat. Wall time comes from the daemon's existing
StartupPerfLog line; peak RSS via ps sampling; datadir size via du.

Output is one CSV row per backend appended to ./bench-results.csv, plus
a stdout summary. Network is disabled during the run (-nolisten -connect=0)
so we measure only DB ingest cost.

Does not yet measure: reorg cost, network IBD speed, raw disk I/O.
LoadSnapshot path is still LevelDB-only; the harness intentionally exercises
the FastImportBlockFile rebuild instead, which works on both backends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 17:52:34 -07:00
parent 76579e3059
commit 674bdc7192
2 changed files with 301 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
# Chain DB benchmark harness
Measures `FastImportBlockFile()` speed under each chain-DB backend
(LevelDB vs RocksDB) using a user-supplied `blk0001.dat` block stream.
## Prerequisites
- A `trianglesd` binary built with both backends:
```
cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \
-DBUILD_DAEMON=ON \
-DBUILD_ROCKSDB=ON
cmake --build build
```
- An `blk0001.dat` file (old-style block stream). If you have a synced
node, copy `~/.triangles/blk0001.dat` (Linux) or `%APPDATA%\triangles\blk0001.dat` (Windows).
- Free disk space: ~3× the size of `blk0001.dat` per backend run
(raw blocks + chain DB index + working space).
## Usage
```bash
contrib/bench/bench-chaindb.sh \
--binary=$(pwd)/build/bin/trianglesd \
--bootstrap=/path/to/blk0001.dat
```
Runs each backend in turn, appends a CSV row to `./bench-results.csv`,
and prints a summary to stdout. Default `--dbcache=2048` (MB).
### Options
| Flag | Default | Notes |
| --- | --- | --- |
| `--binary=PATH` | (required) | Path to `trianglesd` |
| `--bootstrap=PATH` | (required) | Path to `blk0001.dat` |
| `--backends=LIST` | `leveldb,rocksdb` | Comma-separated subset |
| `--workdir=DIR` | `/tmp/triangles-bench-XXXXXX` | Per-backend datadirs go here |
| `--dbcache=MB` | `2048` | Chain DB cache size |
| `--results-csv=FILE` | `./bench-results.csv` | Appended to |
| `--keep-datadirs` | off | Preserve datadirs after run for inspection |
| `--rpc-port=BASE` | `19112` | Each backend uses `BASE+offset` |
## What it measures
| Column | Source |
| --- | --- |
| `wall_ms` | The daemon's own log line: `FastImportBlockFile: indexed N blocks in Mms` |
| `peak_rss_kb` | `ps -o rss=` sampled once per second |
| `datadir_bytes` | `du -sb` of the working datadir (includes `blk0001.dat`) |
| `blocks_indexed` | Parsed from the same log line |
## What it does not measure
- Network IBD (peer fetch, header sync) — this is pure DB ingest.
- UTXO snapshot load — `LoadSnapshot` is currently rocksdb-guarded
(see `src/utxosnapshot.cpp`); will be unblocked when LevelDB is retired.
- Reorg cost — separate test, not yet implemented.
- Disk I/O bytes (read/written) — could be added with `iostat` integration.
## Interpreting results
A meaningful comparison requires both rows to have run on the same machine
with the same `blk0001.dat`. The `host` column makes mixing runs across
machines visible in the CSV.
Backend-relevant size comparisons should subtract `bootstrap_size_bytes`
from `datadir_bytes` to isolate the chain DB tree.
## One-liners
```bash
# LevelDB only
./bench-chaindb.sh --binary=... --bootstrap=... --backends=leveldb
# Compare 2GB vs 4GB cache on RocksDB
./bench-chaindb.sh --binary=... --bootstrap=... --backends=rocksdb --dbcache=2048
./bench-chaindb.sh --binary=... --bootstrap=... --backends=rocksdb --dbcache=4096
# Keep the datadirs for poking around afterwards
./bench-chaindb.sh --binary=... --bootstrap=... --keep-datadirs
```
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env bash
# Benchmark FastImportBlockFile() speed across chain-DB backends.
#
# Reads a user-supplied blk0001.dat (old-style block stream) and times the
# full block-index rebuild under each backend. Output: a CSV row per backend
# with wall time, peak RSS, and resulting datadir size on disk.
#
# Usage:
# ./bench-chaindb.sh \
# --binary=/path/to/trianglesd \
# --bootstrap=/path/to/blk0001.dat \
# [--backends=leveldb,rocksdb] default: both
# [--workdir=/tmp/triangles-bench] parent dir for per-backend datadirs
# [--dbcache=2048] in MB
# [--results-csv=./bench-results.csv]
# [--keep-datadirs] preserve datadirs after run
# [--rpc-port=BASE] default 19112; each run uses BASE+offset
#
# Notes:
# - The binary must be built with BUILD_ROCKSDB=ON when --backends includes rocksdb.
# - This script does not assume Tor is configured. It launches with -nolisten
# and -connect=0 to keep the run network-isolated.
# - Wall time comes from the daemon's own perf log line:
# "FastImportBlockFile: indexed N blocks in Mms"
# - Peak RSS is sampled via `ps -o rss=` once a second.
set -euo pipefail
# ── Defaults ────────────────────────────────────────────────────────────────
BINARY=""
BOOTSTRAP=""
BACKENDS="leveldb,rocksdb"
WORKDIR=""
DBCACHE=2048
RESULTS_CSV="./bench-results.csv"
KEEP=0
RPC_BASE=19112
# ── Arg parsing ─────────────────────────────────────────────────────────────
for arg in "$@"; do
case "$arg" in
--binary=*) BINARY="${arg#*=}" ;;
--bootstrap=*) BOOTSTRAP="${arg#*=}" ;;
--backends=*) BACKENDS="${arg#*=}" ;;
--workdir=*) WORKDIR="${arg#*=}" ;;
--dbcache=*) DBCACHE="${arg#*=}" ;;
--results-csv=*) RESULTS_CSV="${arg#*=}" ;;
--keep-datadirs) KEEP=1 ;;
--rpc-port=*) RPC_BASE="${arg#*=}" ;;
-h|--help)
sed -n '2,28p' "$0" | sed 's/^# \?//'
exit 0 ;;
*)
echo "Unknown argument: $arg" >&2
exit 2 ;;
esac
done
[ -n "$BINARY" ] || { echo "--binary is required" >&2; exit 2; }
[ -n "$BOOTSTRAP" ] || { echo "--bootstrap is required" >&2; exit 2; }
[ -x "$BINARY" ] || { echo "Binary not executable: $BINARY" >&2; exit 2; }
[ -f "$BOOTSTRAP" ] || { echo "Bootstrap file not found: $BOOTSTRAP" >&2; exit 2; }
if [ -z "$WORKDIR" ]; then
WORKDIR="$(mktemp -d -t triangles-bench-XXXXXX)"
fi
mkdir -p "$WORKDIR"
echo "Workdir: $WORKDIR"
# ── CSV header (only if file is new) ───────────────────────────────────────
if [ ! -f "$RESULTS_CSV" ]; then
echo "timestamp,backend,bootstrap_size_bytes,dbcache_mb,blocks_indexed,wall_ms,peak_rss_kb,datadir_bytes,binary,host" > "$RESULTS_CSV"
fi
bootstrap_size="$(stat -c%s "$BOOTSTRAP" 2>/dev/null || stat -f%z "$BOOTSTRAP")"
host="$(hostname)"
ts_run="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# ── Per-backend run ─────────────────────────────────────────────────────────
run_backend() {
local backend="$1"
local idx="$2"
local datadir="$WORKDIR/$backend"
local rpc_port=$((RPC_BASE + idx))
local rss_log="$WORKDIR/$backend.rss.log"
echo
echo "════════════════════════════════════════════════════════════════════"
echo " Backend: $backend (datadir: $datadir, rpcport: $rpc_port)"
echo "════════════════════════════════════════════════════════════════════"
# Fresh datadir, copy bootstrap into place. FastImportBlockFile() picks
# this up automatically when the block index is empty.
rm -rf "$datadir"
mkdir -p "$datadir"
cp "$BOOTSTRAP" "$datadir/blk0001.dat"
# Minimal config — disable network so we measure only the import path.
cat > "$datadir/triangles.conf" <<EOF
chaindb=$backend
dbcache=$DBCACHE
nolisten=1
connect=0
rpcuser=bench
rpcpassword=bench
rpcport=$rpc_port
debug=1
printtoconsole=0
EOF
# Launch in background. -daemon would daemonize but we want to track the
# process tree; run in foreground and background it ourselves so we keep
# the PID for RSS sampling and clean shutdown.
local pid
"$BINARY" -datadir="$datadir" -conf="triangles.conf" >"$datadir/stdout.log" 2>&1 &
pid=$!
echo "Launched $backend (pid $pid)"
# RSS sampler: log peak every second to a file.
(
while kill -0 "$pid" 2>/dev/null; do
ps -o rss= -p "$pid" 2>/dev/null | tr -d ' ' >> "$rss_log" || true
sleep 1
done
) &
local sampler_pid=$!
# Watch for "FastImportBlockFile: indexed N blocks in Mms" in the daemon's
# debug.log, which is the deterministic completion signal.
local debug_log="$datadir/debug.log"
local wait_start
wait_start="$(date +%s)"
local timeout_s=86400 # 24 hours hard cap
local indexed_line=""
while :; do
if [ -f "$debug_log" ]; then
indexed_line="$(grep -E "FastImportBlockFile: indexed [0-9]+ blocks in [0-9]+ms" "$debug_log" | tail -1 || true)"
if [ -n "$indexed_line" ]; then
break
fi
fi
if ! kill -0 "$pid" 2>/dev/null; then
echo "Daemon exited before completion line appeared. Check $datadir/stdout.log" >&2
kill "$sampler_pid" 2>/dev/null || true
return 1
fi
local elapsed=$(( $(date +%s) - wait_start ))
if [ "$elapsed" -gt "$timeout_s" ]; then
echo "Timeout after ${timeout_s}s without completion line" >&2
kill "$pid" 2>/dev/null || true
kill "$sampler_pid" 2>/dev/null || true
return 1
fi
sleep 5
done
echo "Completion: $indexed_line"
# Parse blocks_indexed and wall_ms from the line.
local blocks_indexed wall_ms
blocks_indexed="$(echo "$indexed_line" | sed -E 's/.*indexed ([0-9]+) blocks.*/\1/')"
wall_ms="$(echo "$indexed_line" | sed -E 's/.*in ([0-9]+)ms.*/\1/')"
# Stop daemon cleanly via RPC, fall back to SIGTERM.
"$BINARY" -datadir="$datadir" -conf="triangles.conf" stop >/dev/null 2>&1 || \
kill -TERM "$pid" 2>/dev/null || true
# Wait up to 60s for clean exit.
local stop_wait=0
while kill -0 "$pid" 2>/dev/null && [ "$stop_wait" -lt 60 ]; do
sleep 1
stop_wait=$((stop_wait + 1))
done
kill -KILL "$pid" 2>/dev/null || true
wait "$sampler_pid" 2>/dev/null || true
# Peak RSS: max of the sampler's recorded values.
local peak_rss_kb=0
if [ -f "$rss_log" ] && [ -s "$rss_log" ]; then
peak_rss_kb="$(sort -nr "$rss_log" | head -1)"
fi
# Datadir size — separate the chain DB from blk0001.dat (which is ~constant
# across backends). We report the total datadir size; the consumer can
# subtract bootstrap_size_bytes if they want chain-DB-only.
local datadir_bytes
datadir_bytes="$(du -sb "$datadir" 2>/dev/null | awk '{print $1}' || du -sk "$datadir" | awk '{print $1*1024}')"
# Append CSV row.
echo "$ts_run,$backend,$bootstrap_size,$DBCACHE,$blocks_indexed,$wall_ms,$peak_rss_kb,$datadir_bytes,$BINARY,$host" >> "$RESULTS_CSV"
# Stdout summary.
printf " blocks indexed: %s\n" "$blocks_indexed"
printf " wall time: %s ms (%.1f min)\n" "$wall_ms" "$(awk "BEGIN{print $wall_ms/60000}")"
printf " peak RSS: %s KB (%.1f GB)\n" "$peak_rss_kb" "$(awk "BEGIN{print $peak_rss_kb/1024/1024}")"
printf " datadir size: %s bytes (%.1f GB)\n" "$datadir_bytes" "$(awk "BEGIN{print $datadir_bytes/1024/1024/1024}")"
# Cleanup unless --keep-datadirs.
if [ "$KEEP" -eq 0 ]; then
rm -rf "$datadir"
fi
}
# ── Main loop ──────────────────────────────────────────────────────────────
idx=0
IFS=',' read -r -a backends_arr <<< "$BACKENDS"
for backend in "${backends_arr[@]}"; do
case "$backend" in
leveldb|rocksdb) ;;
*) echo "Unknown backend: $backend" >&2; exit 2 ;;
esac
run_backend "$backend" "$idx"
idx=$((idx + 1))
done
echo
echo "Done. Results appended to $RESULTS_CSV"