# M0 gate results — mmap + WAL storage foundation (PLAN D7)
#
# Machine: Apple Silicon, macOS 25.5.0, 16 KiB system pages, APFS.
# Server:  MultiforaDB at the commit named below, ReleaseFast.
# Driver:  mongodb@7.5.0 (pinned in tests/e2e/package-lock.json).
# mongod:  8.3.7, for the parity rows.
#
# Read this alongside PLAN.md §5. Every number here is reproducible with the
# command printed under it; where a gate was not met, the number is recorded as
# measured and the reason is stated rather than the workload being tuned until
# it passed.

[D7.1] unit tests, ReleaseFast and ReleaseSafe
  zig build test -Doptimize=ReleaseFast    122/122 pass
  zig build test -Doptimize=ReleaseSafe    122/122 pass
  zig build fuzz                           fuzz_split, spill, spill2, stress clean
  Both modes matter: `protect_stable` (the mprotect belt over the published
  image) is comptime-off in ReleaseFast, and `std.posix.mprotect` not existing
  in Zig 0.16 was a ReleaseSafe-only compile error.

[D7.2] end-to-end suites — all green
  reproduce: zig-out/bin/multiforadb --port 27020 --db /tmp/mfdb-e2e.log \
               --ttl-sweep-secs 1 &   then node tests/e2e/<suite>
  e2e.js    CRUD, operators, aggregate, errors        35/35
  e2e3.js   secondary indexes: unique/sparse/compound 16/16
  e2e4.js   TTL indexes: expiry and rejected specs    17/17
  e2e5.js   miscellaneous command surface              3/3
  e2e2.js   concurrent, 4 writers + 4 readers          2/2
  e2e2.js   crash-a / crash-b (kill -9, then verify)   1/1 then 3/3
  e2e6.js   compaction and log growth (own server)    72/72

[D7.3] large-collection smoke — big.js
  reproduce: node tests/e2e/big.js --size 20g --doc-size 16k
                                  20g run          4g run
  documents                       1,310,720        262,144   (x 16 KiB)
  live data                       21.47 GB         4.29 GB
  data file                       21.75 GB         4.35 GB   (+1.3%, +1.4%)
  log file, min -> final          16 B -> 2.5 MB   16 B -> 7.7 MB
  bulk insert                     411.0 MB/s       560.3 MB/s
  peak server RSS during load     18,492 MB        3,318 MB
  kill -9 then reopen             0.5 s            0.5 s
  RSS after reopen                237 MB (1.1%)    130 MB (3.0%)
  count after restart             1,310,720 OK     262,144 OK
  last doc intact after restart   payload 16254B   OK
  kill -9 after 200 acked writes  200/200          200/200
  This is the milestone's central claim, measured: an open costs the working
  set, not the size of the database, and it no longer replays the whole log.
  Reopen is flat at 0.5 s from 4 GB to 21.5 GB, and RSS after reopen falls as a
  *share* of the data as the database grows — which is what "RSS = working set"
  has to mean to be worth anything.
  Before M0 the same shape of run reported 523 MB resident after reopen for a
  512 MB database, because recovering each document's `_id` meant reading every
  document at open. That scan is what the mmap foundation deleted.
  Two other things the 20g run shows, honestly. Peak RSS during the *load* is
  18.5 GB: writing 21 GB touches 21 GB of pages and the kernel keeps them until
  it needs the memory, so a bulk load is not where the mmap win shows up. And a
  cold full scan of 21 GB costs ~55 s — countDocuments, the range scan and the
  unindexed updateMany all land there, ~390 MB/s off the disk — which is correct
  and is the reason M1's cursors and index-only paths matter.

[D7.4] churn gate — doc/slab garbage (PLAN amendment A2 retargets D6.2 here)
  40,000 x 16 KiB documents (655 MB live), one secondary index, steady state
  measured as data-file size / live data after the ratio stopped moving.
                                        before the fixes   after
  delete half and refill, 6 rounds      4.10x, climbing    1.65x, flat
  random $set over 5x the collection    3.58x, climbing    2.47x, flat
  "Climbing" is the whole finding: nothing was being reclaimed at all. Three
  bugs, all fixed in `db/pager: reclaim what churn abandons` — a compaction
  trigger that had read `log.data_bytes` since before the log was truncated at
  every checkpoint, a numeric stable mark that made recycled pages look
  published, and a first-fit free list that let one-page requests dismantle
  the extents. A fourth fix (a rebuild publishes twice, so the space it frees
  is reusable immediately) took the interleaved case from 3.58x to 2.47x.
  The gate hoped for ~1.3x and this is above it, structurally: a rebuild needs
  a whole second copy of the live data before the first can be freed, and
  two-generation retention holds the old copy through two more publishes. The
  gate's purpose was to decide whether doc-level free lists are needed after
  M0. They are; that is an M1 item. What M0 owes is a bound, and there is one.

[D7.5] benchmark parity vs the pre-mmap engine
  reproduce: bash tests/e2e/bench-run.sh 1g 16k "1 4 8 16 32"
  baseline:  tests/e2e/results/phase8.txt (c8d547f, all-in-RAM engine)
  M0:        three runs, tests/e2e/results/bench-2026080{3-223947,3-225128,3-225322}.txt
  Read the range, not a single delta. Two consecutive runs of the *same* binary
  moved the sub-10 ms rows by 27-51% on this machine, so any one comparison
  reads whatever the noise did that minute. Best-of-three against the baseline:
  row                                    pre-mmap        M0 min..max    best
  insertOne (sequential) x200             0.20 ms       0.20..0.22 ms     +0%
  bulk insert throughput                732.4 MB/s   463.4..555.5 MB/s   -24%
  createIndex({k: 1})                     74.4 ms      27.9..28.9 ms     -62%
  countDocuments({})                       3.1 ms        2.6..3.6 ms     -16%
  findOne({_id: <ObjectId>})              0.59 ms       0.60..0.80 ms     +2%
  findOne({k: 500}) (indexed)             0.56 ms       0.54..0.73 ms      -4%
  find({p: {$gte,$lt}}).count() (scan)    13.0 ms      11.2..14.5 ms     -14%
  find({}).sort({_id:-1}).limit(20)        2.2 ms        1.5..1.9 ms     -32%
  find({}, {proj}).limit(1000)             3.5 ms        3.6..3.8 ms      +3%
  aggregate $group by k                    8.3 ms       7.4..11.2 ms     -11%
  updateOne({_id}) x50                    0.14 ms       0.15..0.18 ms     +7%
  updateMany({k: 7}, {$inc})               1.9 ms        1.9..2.0 ms      +0%
  deleteOne({_id}) + insertOne            0.58 ms       0.63..0.69 ms     +9%
  node client RSS                          158 MB        150..163 MB      -5%
  concurrent durable insertOne, docs/s:
  clients  1                               8,435     7,709..8,373        -1%
  clients  4                              22,480    20,122..20,880        -7%
  clients  8                              25,920    26,210..26,349        +2%
  clients 16                              31,079    27,794..29,594        -5%
  clients 32                              34,041    32,235..32,817        -4%
  One reproducible regression: bulk insert, -24%, steady across all three runs.
  This is PLAN risk 1 exactly as written -- document bytes now reach the disk
  uncompressed in the data file on top of the LZ4 log, and that writeback
  bandwidth is new. Every other row is inside the run-to-run spread. The gate
  as stated was "no phase8 row regressed"; it is met for the read and latency
  rows and not for bulk load, which is the trade the milestone makes and which
  risk 1 anticipated. Untried mitigations, in the order worth trying:
  MADV_HUGEPAGE / a larger growth chunk, and not writing doc bytes twice
  (log and slab) for a bulk path that could log an extent reference instead.
  createIndex at -62% is the other reproducible number, and it comes from the
  same change: a bulk build now packs pages in the mapping instead of growing
  an ArrayList.

[D7.6] MongoDB spec-test scorecard
  reproduce: bash tests/spec/fetch.sh && node tests/spec/run.js --scorecard
  total   131 pass   161 fail   195 skip   175 files   0 errored
  Byte-identical to the scorecard recorded before the storage rewrite. That is
  the intended result and it is worth stating plainly: M0 replaced the document
  store, the node arena and the overflow slab, added copy-on-write, a watermark
  and a checkpoint, and moved every index leaf's payload -- and changed no
  observable CRUD or aggregate semantics.

# WHAT THE GATES FOUND
# Five bugs, none of which any unit test or e2e suite had reached:
#   1. The compaction trigger had been dead since commit 14 (log truncation
#      zeroes the counter it gated on). Nothing reclaimed doc-slab garbage.
#   2. `page_mut_cow` asked `p >= stable_pages`, which is false for a recycled
#      page, so every write to one copied and freed it again.
#   3. First fit let copy-on-write's one-page requests shave the extents the
#      doc slab needs, so the free list drained every generation and the file
#      grew anyway.
#   4. A rebuild's freed space took two unrelated checkpoints to become
#      reusable, so the next rebuild grew the file instead of reusing it.
#   5. `reserve_pages`' promise was a single counter on the pager. Two upserts
#      on different collections release it independently, so the first to
#      finish revoked the second's promise mid-write -- aborting the server at
#      four concurrent clients, on the first concurrent benchmark since the
#      data file landed. PLAN risk 3, whose mitigation was never built.
# Four of the five are invisible without a workload that runs long enough to
# reach a steady state, or wide enough to have two writers. That is the
# argument for keeping both the churn gate and the concurrent benchmark in the
# gate list rather than treating them as optional.
#
# ONE COMPATIBILITY GAP FOUND, NOT FIXED (out of M0's scope, storage-only)
# `update.apply` (src/update.zig:18) rejects any update document whose first
# key is not `$`, so replacement-style writes -- replaceOne, findOneAndReplace,
# bulkWrite's replaceOne -- fail with "bad update". Found while building the
# churn workload. It is a CRUD feature, not storage, and it is already part of
# what the 161 spec failures cover.
