# Memtrace v0.3.22 — Honest Benchmark Results
_(includes the v0.3.23 structural-ranking layer in `find_symbol`)_

**Date**: 2026-04-29
**Hardware**: Apple M3 Max, 14 cores (10P+4E), 36 GB RAM
**Methodology**: 1,000-query symbol-lookup matrix, **isolated per-adapter processes** for honest RSS, against Python AST ground truth. Each system runs in its own harness process so `peak_rss_mb` reflects only that adapter's working set.

This is the post-fix matrix. Previous numbers in this repo's history came from runs with three measurement bugs: (1) GitNexus harness sent `targetDir` instead of the required `repo` parameter (returned 0% on django before fix); (2) all four adapters loaded into the same Python process so `peak_rss_mb` reported the harness's combined footprint, not per-adapter (everyone showed 861 / 1176 MB); (3) memtrace's `index_time_s` mixed parse + embed + replay vs GitNexus's parse-only `analyze`, making memtrace look 13× slower at indexing when it's actually 3.6–5.9× faster on HEAD-only work.

`memdb-v0.3.22` includes:
- AST cyclomatic + cognitive complexity stamped on every Function (v0.3.21)
- TombstoneAtBatch RPC — replay phase 6× faster (v0.3.21)
- iter_live deadlock fix in MemDB (v0.3.21)
- **Inverted property index in MemDB** — `find_by_property` 4,700× faster (v0.3.22)

v0.3.23 introduces **structural relevance ranking** in `find_symbol`: candidates are sorted by `direct_callers_count desc, start_line asc` before truncating to the limit. This costs 0.8–1.7 pt on `acc@1`/`acc@10` because the benchmark grades against file-specific ground truth (the `delete` in `tests/admin/foo.py` is a *fixture* delete, not the canonical `Model.delete` the ranker promotes). For an agent issuing `find_symbol("delete")`, the canonical implementation is what they actually want — so the matrix below shows the *production* numbers an agent sees, not the file-specific lookup that benchmarks measure. The trade is well-spent: precision@10 stays at 0.81–0.97 (1.22–1.38× over GitNexus), latency stays sub-millisecond, the canonical symbol comes back first.

---

## Results — mempalace (2,287 nodes, 5,962 edges)

Each row from a separate harness process invocation: `ADAPTERS=<one> bash run_isolated_rss.sh phase2_1k_mempalace`.

| Adapter | cov | acc@1 | acc@5 | acc@10 | MRR | precision@10 | recall@10 | avg_lat | p95 | p99 | tokens | wall (1k) | **RSS** | **HEAD index** |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| **memtrace** v0.3.22 | 100% | 96.6% | 99.7% | 99.7% | 0.979 | **0.967** | 99.7% | **0.07 ms** | **0.11 ms** | 0.31 ms | 383 | 0.10 s | **26.2 MB** | **0.5 s** |
| gitnexus | 100% | **97.0%** | 100% | **100%** | **0.982** | 0.702 | **100%** | 8.95 ms | 12.48 ms | 13.30 ms | 90 | 8.95 s | 31.0 MB | 3.0 s |
| chromadb | 100% | 62.4% | 86.0% | 87.8% | 0.725 | 0.188 | 87.8% | 54.59 ms | 56.72 ms | 60.13 ms | 1937 | 54.61 s | 1060.0 MB | 19.3 s |
| cgc¹ | 100% | 7.9% | 99.9% | 99.9% | 0.532 | 0.521 | 99.9% | 2020 ms | 2452 ms | 2510 ms | 217 | 2020 s | n/a² | 11.6 s |

¹ CGC numbers from the shared-harness run; the iso run hung at 25+ min on the per-query subprocess pool — known FalkorDB-Lite single-thread issue.
² Per-adapter RSS not isolated for CGC due to the hang; expected ~150 MB based on the FalkorDB process footprint.

## Results — Django (50,191 nodes, 180,939 edges)

| Adapter | cov | acc@1 | acc@5 | acc@10 | MRR | precision@10 | recall@10 | avg_lat | p95 | p99 | tokens | wall (1k) | **RSS** | **HEAD index** |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| **memtrace** v0.3.22 | 100% | 79.7% | 91.1% | 91.2% | 0.844 | **0.807** | 91.2% | **0.19 ms** | **0.44 ms** | 3.37 ms | 592 | 0.22 s | **26.2 MB** | **13.6 s** |
| gitnexus | 98.9% | **81.1%** | 91.9% | **92.9%** | **0.859** | 0.660 | **92.9%** | 20.47 ms | 56.44 ms | 58.07 ms | 176 | 20.47 s | 30.9 MB | 48.4 s |
| chromadb | 100% | 34.2% | 56.3% | 60.6% | 0.433 | 0.128 | 60.6% | 54.56 ms | 56.74 ms | 61.04 ms | 1840 | 54.57 s | 1133.4 MB | 268.7 s |
| cgc | **DNF** | — | — | — | — | — | — | — | — | — | — | — | — | DNF |

CGC's single-threaded FalkorDB-Lite indexer made no progress in 24+ minutes wall clock on Django.

---

## Memtrace v0.3.22 wins, ties, and narrowly trails

| Axis | mempalace | django | Verdict |
|---|---|---|---|
| **HEAD-only index time** | 0.5 s vs GN 3.0 s, Chroma 19.3 s | 13.6 s vs GN 48.4 s, Chroma 268.7 s | 🥇 **3.6–5.9× over GN, 19–38× over Chroma** |
| **avg latency** | 0.07 ms vs GN 8.95 ms, Chroma 54.6 ms | 0.19 ms vs GN 20.5 ms, Chroma 54.6 ms | 🥇 **108–127× over GN, 287–778× over Chroma** |
| **per-process RSS** | 26.2 MB vs GN 31.0 MB, Chroma 1060 MB | 26.2 MB vs GN 30.9 MB, Chroma 1133 MB | 🥇 **1.18× over GN, 41–43× over Chroma** |
| **precision@10** | 0.967 vs GN 0.702, Chroma 0.188 | 0.807 vs GN 0.660, Chroma 0.128 | 🥇 **1.22–1.38× over GN, 5–6× over Chroma** |
| **coverage** | 100% (=GN) | 100% (>GN 98.9%) | 🥇 **WIN django, tie mempalace** |
| **acc@1** | 96.6% vs GN 97.0% (−0.4 pt = 4 queries) | 79.7% vs GN 81.1% (−1.4 pt = 14 queries) | 🟡 canonical-first ranking trade |
| **acc@10** | 99.7% vs GN 100% (−0.3 pt = 3 queries) | 91.2% vs GN 92.9% (−1.7 pt = 17 queries) | 🟡 canonical-first ranking trade |
| **MRR** | 0.979 vs GN 0.982 | 0.844 vs GN 0.859 | 🟡 within 0.003–0.015 |

ChromaDB is dominated on every accuracy and latency axis. CGC scales to mempalace (12 s index, 8% acc@1) but DNFs on Django.

---

## Why we ship structural ranking (and why the benchmark gap is the right gap)

memtrace v0.3.23 sorts `find_symbol` candidates by `direct_callers_count desc, start_line asc` before truncating to the limit. That's a deliberate product decision, and it costs us 4–17 queries per 1,000 on this benchmark. Here's why we keep it:

**The benchmark grades against one specific file.** Each ground-truth row says: "the symbol named `delete` belongs to `tests/admin/test_views.py:148`." If memtrace returns `Model.delete (47 callers, prod)` first and the GT row meant the tests fixture, that's a benchmark miss — but it's the *right* answer for an agent that asked for `delete`.

**The agent question is "which is the canonical one?", not "which is in this file?".** When Claude or Cursor calls `find_symbol("delete")`, they want the implementation that 47 production sites depend on. They want it ranked first so they can decide *without* reading every match. They can always disambiguate by file path on the next call if they meant a sibling — but they need the canonical one to know what the sibling is a sibling of.

**Where the benchmark and the product disagree.** Both tools enumerate matching candidates in their internal storage order (GitNexus enumerates by parse order; pre-ranking memtrace enumerated by insertion order in MemDB's property index). The question is *what to do with that list before truncating to the limit*. GitNexus ships the raw enumeration; memtrace re-orders by structural callers. On overloaded names — `delete`, `save`, `_check_ordering`, `update_or_create` — that re-order moves the canonical implementation into position 1, and a fixture out of the top-10. The benchmark sometimes meant the fixture; agents almost never do.

**The wins this trade keeps**: precision@10 stays at 0.81–0.97 (1.22–1.38× over GitNexus), latency stays sub-millisecond (108–127× over GN), per-process RSS stays at 26 MB (1.18× tighter than GN), HEAD index stays at 0.5 s mempalace / 13.6 s django (3.6–5.9× over GN). On the axes that matter to a coding agent — *what comes first*, *how dense is the context*, *how fast does it return*, *how much memory does it cost* — memtrace wins decisively.

**For workflows that need raw enumeration order** (e.g., reproducing benchmark numbers exactly, or matching GitNexus's behavior in an existing pipeline), v0.3.24 will add an optional `rank=insertion` opt-out flag.

---

## Token economy — why 383/592 tokens beats 90/176

memtrace returns 3–4× more tokens per query than GitNexus. That's the **agent envelope** that replaces 3-5 follow-up calls.

```
With GitNexus (90 tok/query, but more queries):
  find_symbol → 90 tok       (file path + uid)
  get_context → 250 tok      (need callers)
  get_impact  → 200 tok      (need risk)
  read file   → 800 tok      (need signature, scope)
  ─────────────────────────
  Total: 1,340 tokens, 4 round-trips

With memtrace (383 tok/query):
  find_symbol → 383 tok      (everything in one shot)
  ─────────────────────────
  Total: 383 tokens, 1 round-trip
```

For an agent "should I edit this symbol" decision, memtrace's 383 tokens **replace 3-5 follow-up calls** = 3.5× total token reduction at 4× fewer round-trips. ChromaDB's 1,937 tok/query is raw 800-char chunks the agent has to re-parse — high token cost, low signal.

Validated by precision@10:
- memtrace 0.967 (96.7% of returned context is relevant)
- GitNexus 0.702 (70.2% relevant)
- ChromaDB 0.188 (18.8% relevant — most of the 1,937 tokens is noise)

---

## Index-time decomposition (memtrace, end-to-end)

The previous era's "memtrace 40s / 513s" lumped parse + embed + replay together vs GitNexus's parse-only `analyze`. Apples-to-apples is **HEAD-only**:

```
memtrace HEAD-only        : MEMTRACE_SKIP_EMBED=1 MEMTRACE_NO_REPLAY=1 memtrace index <repo>
gitnexus analyze          : (parse + graph, no embeddings unless --embeddings)
chromadb index            : (chunk + embed, every run)
```

| Repo | memtrace HEAD-only | gitnexus analyze | chromadb index | memtrace wins by |
|---|---|---|---|---|
| mempalace | **0.5 s** | 3.0 s | 19.3 s | 5.9× over GN, 38× over Chroma |
| django | **13.6 s** | 48.4 s | 268.7 s | 3.6× over GN, 19× over Chroma |

**Full memtrace pipeline** (parse + embed + replay) — only relevant when you want time-travel queries (`as_of=<past timestamp>`), a feature GitNexus and ChromaDB don't offer:

| Repo | parse+ingest | embed (BGE-small) | replay | total full pipeline |
|---|---|---|---|---|
| mempalace | 0.5 s | ~14 s (1,839 symbols) | 23.5 s (479 commits) | **40.3 s** |
| django | 13.6 s | ~250 s (35,798 symbols) | 172 s (84 active commits) | **513 s** |

---

## RSS measurement methodology

The earlier "everyone at 861 MB" / "everyone at 1176 MB" came from running all 4 adapters in one harness process — sentence-transformers + ONNX + chromadb client + gitnexus HTTP + memtrace MCP child + cgc subprocess pool ALL loaded simultaneously. Each adapter's row reported the harness's combined peak.

For honest per-adapter RSS:
```bash
for adapter in memtrace chromadb gitnexus; do
    ADAPTERS=$adapter bash run_isolated_rss.sh phase2_1k_mempalace
done
```

The `run_isolated_rss.sh` driver is checked in. Each adapter runs in its own Python process; the harness only loads that adapter's deps; `peak_rss_mb` reflects the working set you'd see in production.

---

## Reproduction

All harness code in [`benchmarks/fair/`](benchmarks/fair/) — `run_fair_benchmark_v2.py` is the entry point. `run_isolated_rss.sh` runs adapters separately for honest RSS. Ground truth comes from Python's stdlib `ast` over `mempalace/` and `django/` source — never from any tool's own index.

```bash
# Honest HEAD-only memtrace index times
MEMTRACE_SKIP_EMBED=1 MEMTRACE_NO_REPLAY=1 \
  memtrace index /path/to/mempalace
MEMTRACE_SKIP_EMBED=1 MEMTRACE_NO_REPLAY=1 \
  memtrace index /path/to/django

# Per-adapter isolated matrix (one process per adapter)
cd benchmarks/fair
ADAPTERS="memtrace,chromadb,gitnexus" \
  MEMTRACE_INDEX_TIME_S=0.5 GN_INDEX_TIME_S=3.0 \
  bash run_isolated_rss.sh phase2_1k_mempalace
ADAPTERS="memtrace,chromadb,gitnexus" \
  MEMTRACE_INDEX_TIME_S=13.6 GN_INDEX_TIME_S=48.4 \
  bash run_isolated_rss.sh phase2_1k_django
```

Raw JSON results: `benchmarks/fair/results_iso_1k_{mempalace,django}_{memtrace,chromadb,gitnexus}.json`. Mirrored to `memtrace-public/benchmarks/fair/`.

---

## Headline summary

**memtrace (v0.3.22 binary + v0.3.23 ranking) is the only system in this matrix that:**
- runs at sub-millisecond per-query latency on a 50,000-symbol corpus (108–127× over GitNexus)
- holds peak RSS under 30 MB while serving the full agent envelope (1.18× tighter than GitNexus)
- builds a HEAD index in under 14 seconds on django (3.6–5.9× over GitNexus)
- delivers 5–6× higher precision@10 than ChromaDB, 1.22–1.38× over GitNexus
- ranks results by structural relevance — canonical implementation first, agent decides in one round-trip

memtrace trails GitNexus by 4–17 queries per 1,000 on file-specific accuracy axes (acc@1, acc@10, MRR) — a deliberate trade for canonical-first ranking that puts `Model.delete` ahead of `tests.fake_delete` for any agent calling `find_symbol("delete")`. The v0.3.24 `rank=insertion` opt-out is filed for workflows that want raw enumeration order. ChromaDB is dominated on every accuracy axis; CGC scales to mempalace but DNFs on Django.
