Ctrl K
ComunidadGuías…

benchmarks/prototype-sqlite-records.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Throwaway prototype: SQLite-authoritative Records, Markdown as a projection.

NOT product code and not wired into anything. It answers one question with numbers:
if a collection's rows lived in a per-vault SQLite table (same natural key, an
O(1) guard, a per-row audit event) and the Markdown item files were only a rendered
read-only projection, what would a single guarded append and a guard refresh cost at
1,000 and 10,000 items?

Two projection modes are timed:

* ``sync``  -- the item file is rendered and atomically written inside the append,
  before the transaction commits (a failed write rolls the row back).
* ``async`` -- the transaction commits the row and an outbox entry; a worker thread
  renders and writes the file afterwards. Foreground latency excludes the render;
  the worker's lag is reported separately.

What is deliberately NOT here, so the numbers are storage-layer only: request
validation, the mutation-boundary lease, the idempotency ledger, governance and
withheld-is-absent filtering, index fan-out and the response. The findings doc adds
the flat, size-independent cost of those (measured on the real path) back on.

    uv run python benchmarks/prototype-sqlite-records.py --sizes 1000,10000 --appends 30
"""

from __future__ import annotations

import argparse
import datetime as dt
import hashlib
import json
import os
import queue
import shutil
import sqlite3
import sys
import tempfile
import threading
import time
import uuid
from pathlib import Path

REPO = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO / "src"))

SCHEMA = """
CREATE TABLE collections (
  collection_id TEXT PRIMARY KEY, generation INTEGER NOT NULL, manifest_hash TEXT NOT NULL);
CREATE TABLE records (
  collection_id TEXT NOT NULL, item_key TEXT NOT NULL, natural_key TEXT NOT NULL,
  values_json TEXT NOT NULL, body TEXT NOT NULL, item_hash TEXT NOT NULL,
  version INTEGER NOT NULL,
  PRIMARY KEY (collection_id, item_key),
  UNIQUE (collection_id, natural_key));
CREATE TABLE audit (
  seq INTEGER PRIMARY KEY AUTOINCREMENT, collection_id TEXT NOT NULL,
  transition_id TEXT NOT NULL, parent_id TEXT, operation TEXT NOT NULL,
  item_key TEXT NOT NULL, before_hash TEXT, after_hash TEXT NOT NULL,
  payload_hash TEXT NOT NULL, why TEXT NOT NULL, at TEXT NOT NULL);
CREATE TABLE outbox (
  seq INTEGER PRIMARY KEY AUTOINCREMENT, collection_id TEXT NOT NULL,
  item_key TEXT NOT NULL, enqueued_ns INTEGER NOT NULL, done_ns INTEGER);
"""


def _load_harness():
    """The latency harness owns the synthetic manifest and item generator; reuse both."""
    import importlib.util

    spec = importlib.util.spec_from_file_location(
        "records_append_latency_harness", REPO / "scripts" / "measure-records-append-latency.py"
    )
    assert spec is not None and spec.loader is not None
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def _pct(values: list[float], q: float) -> float:
    ordered = sorted(values)
    rank = (len(ordered) - 1) * q
    lo, hi = int(rank), min(int(rank) + 1, len(ordered) - 1)
    return ordered[lo] + (ordered[hi] - ordered[lo]) * (rank - lo)


def _summary(values: list[float]) -> dict[str, float]:
    return {
        "p50_ms": round(1000 * _pct(values, 0.5), 2),
        "p95_ms": round(1000 * _pct(values, 0.95), 2),
    }


class Store:
    """The prototype: rows are authoritative, files are rendered from them."""

    def __init__(self, root: Path, manifest, sync_mode: str, synchronous: str) -> None:
        from exomem import record_formats
        from exomem import structured_collections as collections

        self.record_formats = record_formats
        self.collections = collections
        self.root = root
        self.manifest = manifest
        self.cid = manifest.collection_id
        self.mode = sync_mode
        self.db = sqlite3.connect(root / "records.sqlite", isolation_level=None)
        self.db.execute("PRAGMA journal_mode=WAL")
        self.db.execute(f"PRAGMA synchronous={synchronous}")
        self.db.executescript(SCHEMA)
        self.db.execute(
            "INSERT INTO collections VALUES (?,?,?)",
            (self.cid, 0, manifest.manifest_version.hash),
        )
        self.items_dir = root / manifest.storage.source
        self.items_dir.mkdir(parents=True, exist_ok=True)
        self.lag_seconds: list[float] = []
        self._queue: queue.Queue[tuple[str, dict, str] | None] = queue.Queue()
        self._worker: threading.Thread | None = None
        if sync_mode == "async":
            self._worker = threading.Thread(target=self._drain, daemon=True)
            self._worker.start()

    # -- guard ------------------------------------------------------------------
    def refresh_guard(self) -> int:
        """The client's container-version read: one indexed row."""
        row = self.db.execute(
            "SELECT generation FROM collections WHERE collection_id=?", (self.cid,)
        ).fetchone()
        return int(row[0])

    # -- projection ---------------------------------------------------------------
    def _render(self, key: str, values: dict, correlation: str) -> str:
        return self.record_formats.render_markdown_item(
            self.manifest, values, key, "", correlation, resolve_relationship=None
        )

    def _write_projection(self, key: str, text: str) -> None:
        target = self.items_dir / f"{key}.md"
        temp = target.with_suffix(".tmp")
        with open(temp, "w", encoding="utf-8") as handle:
            handle.write(text)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temp, target)

    def _drain(self) -> None:
        while (job := self._queue.get()) is not None:
            key, values, correlation, enqueued = job  # type: ignore[misc]
            self._write_projection(key, self._render(key, values, correlation))
            self.lag_seconds.append((time.perf_counter_ns() - enqueued) / 1e9)
            self._queue.task_done()

    # -- append -------------------------------------------------------------------
    def append(self, values: dict, why: str, expected_generation: int | None) -> None:
        natural = json.dumps(
            [values["observed_on"], values["subject"]], separators=(",", ":")
        )
        key = str(uuid.uuid5(uuid.NAMESPACE_URL, f"{self.cid}/{natural}"))
        correlation = uuid.uuid4().hex[:24]
        payload = json.dumps(values, sort_keys=True, separators=(",", ":"))
        payload_hash = hashlib.sha256(payload.encode()).hexdigest()
        db = self.db
        db.execute("BEGIN IMMEDIATE")
        try:
            generation, manifest_hash = db.execute(
                "SELECT generation, manifest_hash FROM collections WHERE collection_id=?",
                (self.cid,),
            ).fetchone()
            if expected_generation is not None and expected_generation != generation:
                raise RuntimeError("STALE_RECORD")
            twin = db.execute(
                "SELECT item_key FROM records WHERE collection_id=? AND natural_key=?",
                (self.cid, natural),
            ).fetchone()
            if twin is not None:
                raise RuntimeError("RECORD_ID_CONFLICT")
            item_hash = payload_hash
            db.execute(
                "INSERT INTO records VALUES (?,?,?,?,?,?,?)",
                (self.cid, key, natural, payload, "", item_hash, 1),
            )
            parent = db.execute(
                "SELECT transition_id FROM audit WHERE collection_id=? ORDER BY seq DESC LIMIT 1",
                (self.cid,),
            ).fetchone()
            db.execute(
                "INSERT INTO audit (collection_id, transition_id, parent_id, operation,"
                " item_key, before_hash, after_hash, payload_hash, why, at)"
                " VALUES (?,?,?,?,?,?,?,?,?,?)",
                (
                    self.cid,
                    correlation,
                    parent[0] if parent else None,
                    "append",
                    key,
                    None,
                    item_hash,
                    payload_hash,
                    why,
                    dt.datetime.now(dt.timezone.utc).isoformat(),
                ),
            )
            db.execute(
                "UPDATE collections SET generation=generation+1 WHERE collection_id=?",
                (self.cid,),
            )
            if self.mode == "sync":
                self._write_projection(key, self._render(key, values, correlation))
            else:
                db.execute(
                    "INSERT INTO outbox (collection_id, item_key, enqueued_ns) VALUES (?,?,?)",
                    (self.cid, key, time.perf_counter_ns()),
                )
            db.execute("COMMIT")
        except BaseException:
            db.execute("ROLLBACK")
            raise
        if self.mode == "async":
            self._queue.put((key, values, correlation, time.perf_counter_ns()))  # type: ignore[arg-type]

    def flush(self) -> None:
        if self._worker is not None:
            self._queue.join()
            self._queue.put(None)
            self._worker.join()

    def seed(self, count: int, item, project: bool) -> float:
        started = time.perf_counter()
        db = self.db
        db.execute("BEGIN")
        for index in range(count):
            values = item(index)
            natural = json.dumps(
                [values["observed_on"], values["subject"]], separators=(",", ":")
            )
            key = str(uuid.uuid5(uuid.NAMESPACE_URL, f"{self.cid}/{natural}"))
            payload = json.dumps(values, sort_keys=True, separators=(",", ":"))
            digest = hashlib.sha256(payload.encode()).hexdigest()
            db.execute(
                "INSERT INTO records VALUES (?,?,?,?,?,?,?)",
                (self.cid, key, natural, payload, "", digest, 1),
            )
            if project:
                self._write_projection_fast(key, values)
        db.execute("UPDATE collections SET generation=?", (count,))
        db.execute("COMMIT")
        return time.perf_counter() - started

    def _write_projection_fast(self, key: str, values: dict) -> None:
        text = self._render(key, values, "seed")
        (self.items_dir / f"{key}.md").write_text(text, encoding="utf-8")


def run(size: int, appends: int, mode: str, synchronous: str) -> dict[str, object]:
    shared = _load_harness()

    tmp = Path(tempfile.mkdtemp(prefix="exomem-sqlite-proto-"))
    try:
        os.environ.setdefault("EXOMEM_STATE_ROOT", str(tmp / "state"))
        manifest_file = tmp / shared.COLLECTION
        manifest_file.parent.mkdir(parents=True)
        manifest_file.write_text(shared.MANIFEST, encoding="utf-8")
        from exomem import structured_collections as collections

        manifest = collections.load_manifest(tmp, manifest_file)
        store = Store(manifest_file.parent, manifest, mode, synchronous)
        seeded = store.seed(size, shared._item, project=True)
        refresh: list[float] = []
        wall: list[float] = []
        for index in range(appends):
            start = time.perf_counter()
            generation = store.refresh_guard()
            refresh.append(time.perf_counter() - start)
            start = time.perf_counter()
            store.append(shared._item(size + index), "prototype", generation)
            wall.append(time.perf_counter() - start)
        store.flush()
        return {
            "size": size,
            "mode": mode,
            "synchronous": synchronous,
            "seed_seconds": round(seeded, 1),
            "append": _summary(wall),
            "guard_refresh": _summary(refresh),
            "projection_lag_ms": _summary(store.lag_seconds) if store.lag_seconds else None,
        }
    finally:
        shutil.rmtree(tmp, ignore_errors=True)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--sizes", default="1000,10000")
    parser.add_argument("--appends", type=int, default=30)
    parser.add_argument("--synchronous", default="FULL", choices=["FULL", "NORMAL"])
    parser.add_argument("--out", default="sqlite-prototype-results.json")
    args = parser.parse_args()
    results = []
    for size in [int(part) for part in args.sizes.split(",")]:
        for mode in ("sync", "async"):
            print(f"== size {size} mode {mode} ==", file=sys.stderr, flush=True)
            results.append(run(size, args.appends, mode, args.synchronous))
    Path(args.out).write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8")
    print(json.dumps(results, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())