|
@@ -166,3 +166,32 @@ WAL and snapshot bytes on disk stay JSON-text in v1.11 — the on-disk format br
|
|
|
- **Decider:** Operator (fszontagh)
|
|
- **Decider:** Operator (fszontagh)
|
|
|
- **Discussion:** Single-session brainstorm; operator picked yyjson mut_doc on the recommended-option vote when presented with the three candidates.
|
|
- **Discussion:** Single-session brainstorm; operator picked yyjson mut_doc on the recommended-option vote when presented with the three candidates.
|
|
|
- **Re-open if:** yyjson 1.0 ships with a breaking API change OR Phase C settles on a storage engine that has its own native doc format (in which case Phase B's in-memory format may be replaced again).
|
|
- **Re-open if:** yyjson 1.0 ships with a breaking API change OR Phase C settles on a storage engine that has its own native doc format (in which case Phase B's in-memory format may be replaced again).
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Post-implementation amendment (2026-05-15, after T8 bench)
|
|
|
|
|
+
|
|
|
|
|
+The "memory per doc" row in the rationale table claimed ~100-200 bytes for yyjson mut_doc vs ~600-1200 for nlohmann::json — a projected 3-6× reduction. **The actual bench (`tests/bench_doc_memory.cpp`, 100k synthetic Zoe-shape docs, separate processes to avoid allocator carry-over) shows:**
|
|
|
|
|
+
|
|
|
|
|
+- nlohmann::json: **1618 bytes/doc**
|
|
|
|
|
+- yyjson mut_doc: **1594 bytes/doc**
|
|
|
|
|
+- **Reduction: ~1.5%**
|
|
|
|
|
+
|
|
|
|
|
+The projection was wrong: both representations are allocator-backed DOMs with similar node densities (yyjson_mut_val and nlohmann::json's discriminated union are each in the 32-64 byte range). The real per-doc footprint is dominated by the per-doc allocator overhead and string storage, neither of which differs materially between the two formats.
|
|
|
|
|
+
|
|
|
|
|
+**What Phase B actually delivers (correct framing for v1.11.0 release notes):**
|
|
|
|
|
+
|
|
|
|
|
+1. **Field-fast-path** — `Document::field(name)` walks immediate object children in O(field-count) without materialising the full tree. The dominant read pattern in `database_grpc_impl.cpp` and `memory_store.cpp` is single-key lookup; this is where the per-request CPU win lives.
|
|
|
|
|
+2. **Parser-buffer reuse path** — `Document::fromJson(j)` calls `set_data(j)` which encodes into yyjson. Phase A's `parse_to_nlohmann` still goes through nlohmann as the bridge; Phase C can short-circuit that by going yyjson_doc → yyjson_mut_doc directly, avoiding the intermediate nlohmann tree entirely. Phase B's accessor surface makes that future change a one-file rewrite.
|
|
|
|
|
+3. **Scaffolding for Phase C** — every `doc.data["X"]` callsite is now `doc.field("X")` / `doc.data()["X"]` / `doc.set_data(...)`. When Phase C swaps the storage substrate again (page-based pool, RocksDB blobs, whatever the spike picks), the callsite shape doesn't move.
|
|
|
|
|
+
|
|
|
|
|
+**What Phase B does NOT deliver:**
|
|
|
|
|
+
|
|
|
|
|
+- Materially smaller per-doc heap footprint. Both formats are tree-allocators.
|
|
|
|
|
+- Bounded total RSS independent of dataset size. That is the Phase C goal (page pool of fixed `buffer_pool_size_mb`, not per-doc memory).
|
|
|
|
|
+
|
|
|
|
|
+**Should Phase B still ship?** Operator confirmed yes, on 2026-05-15 after seeing the bench numbers: the field-fast-path win is real and the Phase C scaffolding burden is now retired. v1.11.0 release notes frame this honestly — no memory reduction is claimed.
|
|
|
|
|
+
|
|
|
|
|
+**Implementation deviations from this spec:**
|
|
|
|
|
+
|
|
|
|
|
+- `Doc` is **not move-only**. The `service/src/doc_binary.{hpp,cpp}` implementation added a copy constructor and copy-assignment that deep-copy via `yyjson_mut_val_mut_copy`. The original move-only design assumed every callsite could `std::move`, but `std::vector<Document>` return values from `find`/`getAllDocuments`/`bulkInsert` and replication conflict resolution force copy semantics. See commit `7eac3d6` for the change.
|