results: the M0 gates, measured
PLAN D7's six items, with the numbers and the command that reproduces each in
tests/e2e/results/m0-gates.txt. Unit tests green in both optimize modes, the
whole e2e matrix green, the spec scorecard byte-identical at 131/161/195, and
the large smoke run at the scale D7.3 asked for:
21.47 GB collection (1,310,720 x 16 KiB)
data file 21.75 GB (+1.3% over the documents)
log after the load 2.5 MB (checkpoints reclaim it)
kill -9 then reopen 0.5 s (0.5 s at 4 GB too -- flat)
RSS after reopen 237 MB (1.1% of the data)
count after restart 1,310,720 last document byte-intact
acked writes after kill 200/200
That is the milestone's claim, measured: an open costs the working set rather
than the size of the database. Before M0 the same measurement was 523 MB
resident for a 512 MB database, because recovering each document's `_id` meant
reading every document at open.
Two gates need reading rather than a tick, and m0-gates.txt says so where a
reader would otherwise take a tick for granted.
The churn gate settles at 1.65x live data (delete-heavy) to 2.47x
(update-heavy), flat, above the ~1.3x amendment A2 hoped for. Rebuild-only
reclamation cannot reach that: it needs a whole second copy of the live data
before the first can be freed. The gate existed to decide whether doc-level
free lists are needed after M0, and that is the answer.
Benchmark parity holds for every read and latency row inside the run-to-run
spread, and bulk insert regresses 24% (732 -> 555 MB/s), reproducibly across
three runs. Risk 1 as written: document bytes now reach the disk uncompressed
on top of the LZ4 log. createIndex improves 62% from the same change.
Three measurement bugs fixed while running the gates, because each would have
put a false number in the README:
- `compare-run.sh` measured "db on disk" as `du` of the log alone against
`du` of mongod's whole dbpath. It reported 20 MB for a 1 GB collection --
the documents had moved to <db>.data. Honest figure, measured: 914 MB of
allocated blocks against mongod's compressed 85 MB.
- `big.js` counted "compaction events" as "the log shrank", which is a
*checkpoint* now. It claimed 12 compaction rewrites during a pure insert
load, which has no garbage to compact.
- `big.js` labelled peak RSS "in-memory engine: docs live in RAM" and its
summary said the collection was held "fully in RAM". Both were true of the
engine this milestone replaced.
README: the storage section described an all-in-RAM engine; the comparison
table mixed one old run's body with three new rows; and `findOne({_id})` was
documented as a full scan for integer ids, which the ordered `_id_` index made
false (2 ms against 55 s for a scan of the same 21.5 GB collection). The table
is now best-of-three for both servers, with the measured variance stated, since
two runs of the same binary moved the sub-10 ms rows by 27-51%.
This commit is contained in:
80
PLAN.md
80
PLAN.md
@@ -532,13 +532,79 @@ discipline lives, and `ls`/`du` stay honest for D6.6's backup story.
|
||||
| 16 | drop the docs hashmap | original step 7 |
|
||||
| 17 | gates | D7's six items; churn gate per amended D6.2 |
|
||||
|
||||
M0 is done when D7's six items pass. Two harness fixes are prerequisites for
|
||||
the gate rather than the work: `bench-run.sh` copies its report over
|
||||
`bench-latest.txt` unconditionally, including after a run that only warned,
|
||||
so the baseline being defended can be clobbered; and the four B+tree dev
|
||||
harnesses (`spill.zig`, `spill2.zig`, `stress.zig`, `fuzz_split.zig`) are in
|
||||
no build step, so `zig build test` will not notice an API break in the one
|
||||
place that fuzzes splits and >1 KB keys.
|
||||
All seventeen commits are landed. Both harness fixes that were prerequisites
|
||||
for the gate rather than the work are in: `bench-run.sh` no longer copies its
|
||||
report over `bench-latest.txt` after a degraded run (it used to, so the
|
||||
baseline being defended could be clobbered), and the four B+tree dev harnesses
|
||||
(`spill.zig`, `spill2.zig`, `stress.zig`, `fuzz_split.zig`) now have a
|
||||
`zig build fuzz` step — they were in no build step, so `zig build test` could
|
||||
not notice an API break in the one place that fuzzes splits and >1 KB keys.
|
||||
|
||||
### The gate result
|
||||
|
||||
Measured numbers, and the command that reproduces each, are in
|
||||
`tests/e2e/results/m0-gates.txt`. In short:
|
||||
|
||||
| D7 | gate | outcome |
|
||||
|---|---|---|
|
||||
| 1 | unit tests RF + RS | pass, 122/122 both |
|
||||
| 2 | e2e matrix unchanged | pass, every suite |
|
||||
| 3 | 20-30 GB smoke, RSS ≈ working set, fast reopen | pass at 21.5 GB |
|
||||
| 4 | churn gate | measured, bounded, above the hoped-for 1.3x |
|
||||
| 5 | benchmark parity | pass except bulk insert, -24% |
|
||||
| 6 | spec runner + scorecard | pass, scorecard unchanged |
|
||||
|
||||
Two of those need reading rather than a tick.
|
||||
|
||||
**D7.4** hoped for ~1.3x of live data and settles at 1.65x (delete-heavy) to
|
||||
2.47x (update-heavy), flat in both cases. Before the gate's own findings were
|
||||
fixed it was 4.1x and *climbing linearly* — nothing was being reclaimed at
|
||||
all. The gate's stated purpose (amendment A2) was to decide whether doc-level
|
||||
free lists are needed after M0, and the answer is yes, in M1: a rebuild needs
|
||||
a whole second copy of the live data before the first can be freed, so
|
||||
rebuild-only reclamation cannot reach 1.3x however it is tuned. What M0 owed
|
||||
was a bound, and there is one.
|
||||
|
||||
**D7.5** says "no phase8 row regresses". Bulk insert throughput regresses
|
||||
24%, reproducibly (732 → 555 MB/s). That is risk 1 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 this machine's
|
||||
run-to-run spread, which two consecutive runs of the same binary showed to be
|
||||
27-51% on the sub-10 ms rows — so the gate is met for the read and latency
|
||||
rows and not for bulk load. `createIndex` improves 62%, also reproducibly,
|
||||
from the same change.
|
||||
|
||||
### What the gates found
|
||||
|
||||
Five bugs, none of which any unit test or e2e suite had reached, and four of
|
||||
which need either a long-running workload or a second concurrent writer to
|
||||
appear at all:
|
||||
|
||||
1. The compaction trigger had been dead since commit 14 — it gated on
|
||||
`log.data_bytes`, and truncating the log at every checkpoint zeroes that.
|
||||
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.
|
||||
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, and two upserts
|
||||
on different collections release it independently — so the first to finish
|
||||
revoked the second's promise mid-write. This aborted the server at four
|
||||
concurrent clients on the first concurrent benchmark since the data file
|
||||
landed. Risk 3, whose mitigation (private pre-allocated runs) was listed in
|
||||
this document and never built.
|
||||
|
||||
The lesson worth carrying into M1 is the shape of 1-4: every one is a
|
||||
*reclamation* bug, invisible to any test that does not run long enough to
|
||||
reach a steady state. The unit suite proved each mechanism works once. Only
|
||||
the churn gate showed that none of them worked twice.
|
||||
|
||||
A compatibility gap also surfaced, out of M0's scope and left alone:
|
||||
`update.apply` (`src/update.zig:18`) rejects any update document whose first
|
||||
key is not `$`, so `replaceOne`, `findOneAndReplace` and `bulkWrite`'s
|
||||
`replaceOne` all fail with "bad update". It is a CRUD feature rather than
|
||||
storage, and it is already inside what the 161 spec failures cover.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user