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.
130 lines
6.4 KiB
Bash
130 lines
6.4 KiB
Bash
#!/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"
|