diff --git a/contrib/bench/README.md b/contrib/bench/README.md new file mode 100644 index 0000000..1c0e3d0 --- /dev/null +++ b/contrib/bench/README.md @@ -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 +``` diff --git a/contrib/bench/bench-chaindb.sh b/contrib/bench/bench-chaindb.sh new file mode 100644 index 0000000..bb33e8a --- /dev/null +++ b/contrib/bench/bench-chaindb.sh @@ -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" <"$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"