Pārlūkot izejas kodu

test: add bench_doc_memory for phase B RSS verification

Manual micro-benchmark, NOT registered with CTest. Run as:
  ./build/tests/bench_doc_memory --mode=<nlohmann|binary> [N]

Two-process invocation pattern avoids the allocator-carry-over confound
where the second container in a single process reuses pages freed by
the first.

Measured at 100k synthetic Zoe-shape docs (separate processes):
  nlohmann::json:    1618 bytes/doc, 689 ms load time
  doc_binary::Doc:   1594 bytes/doc, 900 ms load time
  RSS reduction:     ~1.5%
  encode-time cost:  +30% vs raw nlohmann construction

HONEST NOTE: The format-decision doc predicted 3-6× per-doc RSS reduction.
That projection was based on a comparison against nlohmann's pointer-heavy
AST in the abstract, but both yyjson mut_doc AND nlohmann::json store
documents as allocator-tree-backed DOMs with similar density (each
node ~32-64 bytes, plus strings, plus pool overhead). The savings
from switching node representation are real but marginal.

Phase B's actual wins, which this bench does NOT capture, are:
  - parse-time: 2.80× (already measured in v1.10 bench_json_parse)
  - field-fast-path: O(field-count) reads without full materialise
  - reduced allocations under churn (one yyjson pool per doc vs
    many small nlohmann nodes)
  - hot WAL/snapshot path skips the parse→nlohmann materialise

Real in-memory bounded-RSS is Phase C work (page-based storage,
buffer pool). v1.11 ships as a parser-speed + field-access-speed
release, not a memory release. Decision doc to be amended in T9.
fszontagh 2 mēneši atpakaļ
vecāks
revīzija
9db468b83b
2 mainītis faili ar 161 papildinājumiem un 0 dzēšanām
  1. 20 0
      tests/CMakeLists.txt
  2. 141 0
      tests/bench_doc_memory.cpp

+ 20 - 0
tests/CMakeLists.txt

@@ -294,6 +294,26 @@ endif()
 
 target_link_libraries(bench_json_parse PRIVATE ${yyjson_LIBRARIES})
 
+# RSS bench (v1.11 Phase B T8) — manual bench, NOT registered with CTest.
+# Run as ./build/tests/bench_doc_memory [N].
+add_executable(bench_doc_memory
+    bench_doc_memory.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/../service/src/doc_binary.cpp
+)
+
+target_include_directories(bench_doc_memory PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}/../service/src
+    ${yyjson_INCLUDE_DIRS}
+)
+
+if(TARGET nlohmann_json::nlohmann_json)
+    target_link_libraries(bench_doc_memory PRIVATE nlohmann_json::nlohmann_json)
+else()
+    target_include_directories(bench_doc_memory PRIVATE ${NLOHMANN_JSON_INCLUDE_DIRS})
+endif()
+
+target_link_libraries(bench_doc_memory PRIVATE ${yyjson_LIBRARIES})
+
 # Register with CTest
 enable_testing()
 add_test(NAME vector_storage COMMAND test_vector_storage)

+ 141 - 0
tests/bench_doc_memory.cpp

@@ -0,0 +1,141 @@
+// RSS micro-benchmark — Phase B v1.11 binary doc storage.
+//
+// Loads N synthetic Zoe-shape JSON docs into ONE storage container (chosen by
+// --mode) and reports peak RSS. Run twice and diff: one with --mode=nlohmann
+// (v1.10 baseline), one with --mode=binary (v1.11). Comparing peak RSS across
+// two separate processes avoids the allocator-carry-over confound where the
+// second container in a single process reuses pages freed by the first.
+//
+// Not a regression gate — informational, run by hand to capture the
+// memory delta. Usage:
+//   ./bench_doc_memory --mode=nlohmann [N]
+//   ./bench_doc_memory --mode=binary   [N]
+//   ./bench_doc_memory --help
+
+#include <chrono>
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iostream>
+#include <random>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <nlohmann/json.hpp>
+
+#include "doc_binary.hpp"
+
+namespace bin = smartbotic::db::doc_binary;
+
+namespace {
+
+long rss_kb() {
+    std::ifstream f("/proc/self/status");
+    std::string line;
+    while (std::getline(f, line)) {
+        if (line.rfind("VmRSS:", 0) == 0) {
+            std::istringstream iss(line.substr(6));
+            long kb;
+            iss >> kb;
+            return kb;
+        }
+    }
+    return 0;
+}
+
+nlohmann::json synth_doc(size_t i, std::mt19937& rng) {
+    return nlohmann::json{
+        {"_id", "doc-" + std::to_string(i)},
+        {"_created_at", static_cast<long long>(1731628800000LL + static_cast<long long>(i))},
+        {"_updated_at", static_cast<long long>(1731628800000LL + static_cast<long long>(i))},
+        {"name", "name-" + std::to_string(i)},
+        {"tags", nlohmann::json::array({"hot", "pinned", "rec"})},
+        {"_vector", nlohmann::json::array({
+            std::uniform_real_distribution<double>(-1.0, 1.0)(rng),
+            std::uniform_real_distribution<double>(-1.0, 1.0)(rng),
+            std::uniform_real_distribution<double>(-1.0, 1.0)(rng),
+            std::uniform_real_distribution<double>(-1.0, 1.0)(rng),
+            std::uniform_real_distribution<double>(-1.0, 1.0)(rng),
+            std::uniform_real_distribution<double>(-1.0, 1.0)(rng),
+            std::uniform_real_distribution<double>(-1.0, 1.0)(rng),
+            std::uniform_real_distribution<double>(-1.0, 1.0)(rng),
+        })},
+        {"meta", {
+            {"flag", (i % 2) == 0},
+            {"score", std::uniform_real_distribution<double>(0.0, 1.0)(rng)},
+            {"counter", static_cast<int>(i % 1000)},
+        }},
+    };
+}
+
+void usage(const char* prog) {
+    std::cerr << "usage: " << prog << " --mode=<nlohmann|binary> [N]\n"
+              << "  N defaults to 100000\n";
+}
+
+}  // namespace
+
+int main(int argc, char** argv) {
+    std::string mode;
+    size_t n = 100000;
+    for (int i = 1; i < argc; ++i) {
+        const char* a = argv[i];
+        if (std::strncmp(a, "--mode=", 7) == 0) {
+            mode = a + 7;
+        } else if (std::strcmp(a, "--help") == 0) {
+            usage(argv[0]);
+            return 0;
+        } else {
+            n = static_cast<size_t>(std::atol(a));
+        }
+    }
+    if (mode != "nlohmann" && mode != "binary") {
+        usage(argv[0]);
+        return 1;
+    }
+
+    long rss_start = rss_kb();
+    std::cout << "mode:       " << mode << "\n"
+              << "docs:       " << n << "\n"
+              << "RSS start:  " << rss_start << " kB\n";
+
+    auto t0 = std::chrono::steady_clock::now();
+
+    if (mode == "nlohmann") {
+        std::vector<nlohmann::json> store;
+        store.reserve(n);
+        std::mt19937 rng(42);
+        for (size_t i = 0; i < n; ++i) {
+            store.push_back(synth_doc(i, rng));
+        }
+        auto t1 = std::chrono::steady_clock::now();
+        long rss_loaded = rss_kb();
+        std::cout << "load time:  "
+                  << std::chrono::duration<double, std::milli>(t1 - t0).count() << " ms\n"
+                  << "RSS loaded: " << rss_loaded << " kB\n"
+                  << "delta:      " << (rss_loaded - rss_start) << " kB\n"
+                  << "per-doc:    "
+                  << static_cast<double>(rss_loaded - rss_start) * 1024.0 / static_cast<double>(n)
+                  << " bytes\n"
+                  << "store size: " << store.size() << "\n";
+    } else {
+        std::vector<bin::Doc> store;
+        store.reserve(n);
+        std::mt19937 rng(42);
+        for (size_t i = 0; i < n; ++i) {
+            store.push_back(bin::encode(synth_doc(i, rng)));
+        }
+        auto t1 = std::chrono::steady_clock::now();
+        long rss_loaded = rss_kb();
+        std::cout << "load time:  "
+                  << std::chrono::duration<double, std::milli>(t1 - t0).count() << " ms\n"
+                  << "RSS loaded: " << rss_loaded << " kB\n"
+                  << "delta:      " << (rss_loaded - rss_start) << " kB\n"
+                  << "per-doc:    "
+                  << static_cast<double>(rss_loaded - rss_start) * 1024.0 / static_cast<double>(n)
+                  << " bytes\n"
+                  << "store size: " << store.size() << "\n";
+    }
+    return 0;
+}