Files
MultiforaDB/tests/e2e/compare-run.sh
Aleksey Shakhmatov 9dda943f26 db: documents live in the data file
Collection's slab of malloc'd 8 MiB segments becomes extents in the data file,
and a document's offset becomes an absolute file offset. That makes `doc_bytes`
base + off instead of a binary search over segment starts, and it removes the
hazard the segment list carried: the mapping's base never moves, so a slice into
it cannot be invalidated by growth. phase8 records a dangling-slab-pointer bug
of exactly the shape this deletes.

`slab_reserve` now runs before the log append and `slab_append` after it, and is
infallible. It had no reservation before because appending to an ArrayList could
only fail on OOM; a file-backed slab can also fail on growth, and failing after
the record is durable would report an error for a write the next open produces
anyway.

The data file is still recreated empty on every open and the log still replays in
full, so nothing durable depends on it yet and open/close semantics are
byte-for-byte what they were. The watermark that turns it into a checkpoint comes
next.

--

One bug, and it is worth reading because the milestone keeps producing this
shape. Collection holds a `*Pager`, and `Engine.open` builds an Engine on the
*stack* and returns it by value -- so every pointer taken during replay dangled
the moment it was moved. It surfaced as SIGBUS, then as a corrupt hashmap in the
*second* engine of an unrelated test: nothing resembling its cause. The pager is
heap-allocated now, as `Index` already was, for the same reason.

--

Measured on one harness, 512 MB / 16 KB docs, before and after:

  bulk insert throughput      742.6 MB/s -> 746.7 MB/s
  createIndex({k: 1})         26.8 ms    -> 16.2 ms
  countDocuments({})          2.1 ms     -> 1.1 ms
  findOne({k: 500}) indexed   0.75 ms    -> 0.53 ms
  find({p: range}).count()    6.6 ms     -> 4.1 ms
  aggregate $group by k       5.8 ms     -> 3.7 ms
  insertOne (sequential)      0.20 ms    -> 0.20 ms

Every row equal or faster. The regression this commit was scheduled early to
catch -- document bytes now reaching disk uncompressed on top of the LZ4 log --
did not appear at this size; bulk insert is flat and the reads gain from one
contiguous mapping instead of separately allocated segments.

What did *not* improve, stated plainly because it is the milestone's headline
claim: RSS is unchanged, 552 MB against 563 MB for 512 MB of data. It cannot
improve yet. Every open still replays the whole log and rewrites the whole slab,
so every page is touched and resident regardless of where it lives. "RSS =
working set" only becomes measurable once an open loads a checkpoint instead of
rebuilding, and the honest test for it is the multi-GB reopen in big.js, not this
microbenchmark.
2026-08-03 20:41:08 +03:00

130 lines
6.4 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Compare multiforadb against a real MongoDB with the same workload, same driver.
#
# bash tests/e2e/compare-run.sh [size] [doc-size]
# (defaults: 1g, 16k)
#
# Starts mongod on :27018 and multiforadb on :27019, runs compare.js against
# each (durable writes: multiforadb fsyncs per doc, mongod ack'd with j:true),
# measures kill -9 reopen time for both, and prints a side-by-side table.
set -u
cd "$(dirname "$0")/../.."
SIZE="${1:-1g}"
DOC="${2:-16k}"
echo "comparing multiforadb vs mongodb — dataset ${SIZE}, docs ~${DOC}"
CMPDIR=/tmp/mongo-cmp
mkdir -p "$CMPDIR/mongod"
MFDB_LOG="$CMPDIR/mfdb.log"
MFDB_OUT="$CMPDIR/mfdb-srv.out"
MD_OUT="$CMPDIR/md-srv.out"
MFDB_PORT=27019
MD_PORT=27018
rm -f "$MFDB_LOG" "$MFDB_LOG.data"
rm -rf "$CMPDIR/mongod" && mkdir -p "$CMPDIR/mongod"
# ---- MongoDB -------------------------------------------------------------
echo; echo "### mongod (MongoDB $(mongod --version | grep -oE 'v[0-9.]+' | head -1))"
mongod --dbpath "$CMPDIR/mongod" --port $MD_PORT --bind_ip 127.0.0.1 \
--quiet >"$MD_OUT" 2>&1 &
MD_PID=$!
# Poll until mongod answers; a fixed sleep is flaky right after other
# benchmark phases have warmed the machine.
wait_ready() { # $1 = url
for _ in $(seq 1 90); do
NODE_PATH="tests/e2e/node_modules" node -e "require('mongodb').MongoClient.connect(process.argv[1],{serverSelectionTimeoutMS:800}).then(c=>c.close().then(()=>process.exit(0))).catch(()=>process.exit(1))" "$1" 2>/dev/null \
&& return 0
sleep 1
done
return 1
}
wait_ready "mongodb://127.0.0.1:$MD_PORT" || { echo "mongod never became ready" >&2; exit 1; }
node tests/e2e/compare.js --url "mongodb://127.0.0.1:$MD_PORT" --label mongodb --size "$SIZE" --doc-size "$DOC" \
> "$CMPDIR/mongo-report.txt" 2>&1 || { echo "mongodb bench failed:"; tail -5 "$CMPDIR/mongo-report.txt"; }
MD_RSS=$(ps -o rss= -p $MD_PID | awk '{printf "%.0f", $1/1024}')
MD_DISK=$(du -sm "$CMPDIR/mongod" | awk '{print $1}')
echo; echo "### mongod kill -9 + reopen"
kill -9 $MD_PID; wait $MD_PID 2>/dev/null
MD_REOPEN=$(cd tests/e2e && node -e '
const { spawn } = require("child_process");
const { MongoClient } = require("mongodb");
const t0 = Date.now();
const p = spawn("mongod", ["--dbpath","/tmp/mongo-cmp/mongod","--port","27018","--bind_ip","127.0.0.1","--quiet"], {stdio:"ignore"});
const poll = async () => {
const c = new MongoClient("mongodb://127.0.0.1:27018", {serverSelectionTimeoutMS: 800});
try { await c.connect(); await c.db("admin").command({ping:1}); await c.close(); p.kill("SIGKILL"); console.log(((Date.now()-t0)/1000).toFixed(1)); }
catch { try { await c.close(); } catch {}; setTimeout(poll, 200); }
};
setTimeout(poll, 300);
')
kill -9 $MD_PID 2>/dev/null
# ---- multiforadb ----------------------------------------------------------
echo; echo "### multiforadb (recommended config: --compact-threshold 1g)"
# Debug is ~10-200x slower (see the README's perf section) — the comparison
# must use the optimized build.
zig build -Doptimize=ReleaseFast 2>&1 | grep -c "^error" | grep -q "^0" || { echo "build failed"; exit 1; }
./zig-out/bin/multiforadb --port $MFDB_PORT --db "$MFDB_LOG" --compact-threshold 1g >"$MFDB_OUT" 2>&1 &
MFDB_PID=$!
wait_ready "mongodb://127.0.0.1:$MFDB_PORT" || { echo "multiforadb never became ready" >&2; exit 1; }
node tests/e2e/compare.js --url "mongodb://127.0.0.1:$MFDB_PORT" --label multiforadb --size "$SIZE" --doc-size "$DOC" \
> "$CMPDIR/mfdb-report.txt" 2>&1 || { echo "multiforadb bench failed:"; tail -5 "$CMPDIR/mfdb-report.txt"; }
MFDB_RSS=$(ps -o rss= -p $MFDB_PID | awk '{printf "%.0f", $1/1024}')
MFDB_DISK=$(du -sm "$MFDB_LOG" | awk '{print $1}')
echo; echo "### multiforadb kill -9 + reopen (replay)"
kill -9 $MFDB_PID; wait $MFDB_PID 2>/dev/null
MFDB_REOPEN=$(cd tests/e2e && node -e '
const { spawn } = require("child_process");
const { MongoClient } = require("mongodb");
const t0 = Date.now();
const p = spawn("../../zig-out/bin/multiforadb", ["--port","27019","--db","/tmp/mongo-cmp/mfdb.log","--compact-threshold","1g"], {stdio:"ignore"});
const poll = async () => {
const c = new MongoClient("mongodb://127.0.0.1:27019", {serverSelectionTimeoutMS: 800});
try { await c.connect(); await c.db("admin").command({ping:1}); await c.close(); p.kill("SIGKILL"); console.log(((Date.now()-t0)/1000).toFixed(1)); }
catch { try { await c.close(); } catch {}; setTimeout(poll, 200); }
};
setTimeout(poll, 300);
')
kill -9 $MFDB_PID 2>/dev/null
# ---- side by side ----------------------------------------------------------
echo; echo "### side by side — ${SIZE} dataset, ~${DOC} docs"
cat > "$CMPDIR/meta.json" <<EOF
{"mfdb_rss_mb": "$MFDB_RSS", "md_rss_mb": "$MD_RSS", "mfdb_reopen": "${MFDB_REOPEN}s", "md_reopen": "${MD_REOPEN}s", "mfdb_disk_mb": "${MFDB_DISK}MB", "md_disk_mb": "${MD_DISK}MB"}
EOF
node -e '
const fs = require("fs");
const read = (p) => {
const m = {};
if (!fs.existsSync(p)) return m;
for (const line of fs.readFileSync(p, "utf8").split("\n")) {
const i = line.indexOf("\t");
if (i > 0) m[line.slice(0, i)] = line.slice(i + 1).replace(/\t.*$/, "");
}
return m;
};
const a = read("/tmp/mongo-cmp/mfdb-report.txt");
const b = read("/tmp/mongo-cmp/mongo-report.txt");
const meta = JSON.parse(fs.readFileSync("/tmp/mongo-cmp/meta.json", "utf8"));
const keys = ["insertOne (sequential) ×200","bulk insert throughput","docs loaded","createIndex({k: 1})",
"countDocuments({})","findOne({_id: <ObjectId>})","findOne({k: 500}) (indexed)","find({p: {$gte,$lt}}).count() (scan)",
"find({}).sort({_id:-1}).limit(20)","find({}, {proj}).limit(1000)","aggregate $group by k",
"updateOne({_id}) ×50","updateMany({k: 7}, {$inc})","deleteOne({_id}) + insertOne","node client RSS"];
const col = (v) => String(v).padEnd(22);
console.log(`${"benchmark".padEnd(42)} ${col("multiforadb")} ${col("mongodb")} ratio`);
for (const k of keys) {
const av = a[k] || "—", bv = b[k] || "—";
const ar = parseFloat(av), br = parseFloat(bv);
const ratio = isFinite(ar) && isFinite(br) && ar > 0 && br > 0 ? (ar / br).toFixed(1) + "x" : "";
console.log(`${k.padEnd(42)} ${col(av)} ${col(bv)} ${ratio}`);
}
console.log(`${`server RSS`.padEnd(42)} ${col(meta.mfdb_rss_mb + " MB")} ${col(meta.md_rss_mb + " MB")}`);
console.log(`${`kill -9 reopen`.padEnd(42)} ${col(meta.mfdb_reopen)} ${col(meta.md_reopen)}`);
console.log(`${`db on disk`.padEnd(42)} ${col(meta.mfdb_disk_mb)} ${col(meta.md_disk_mb)}`);
'
pkill -9 -f "multiforadb --port $MFDB_PORT" 2>/dev/null
echo; echo "done — reports: $CMPDIR/mfdb-report.txt, $CMPDIR/mongo-report.txt"