From e89f4660f6adc2aa3aacdd3d113b1ee202ab8267 Mon Sep 17 00:00:00 2001 From: Jason Stedwell Date: Tue, 23 Jun 2026 17:17:00 -0500 Subject: [PATCH] testing and documentation --- .DS_Store | Bin 10244 -> 12292 bytes .../eval/perf/ECHO-Performance-Benchmark.pdf | Bin 0 -> 172026 bytes .../echo-memory/eval/perf/PERF-TEST-PLAN.md | 146 +++ .../skills/echo-memory/eval/perf/README.md | 96 ++ .../echo-memory/eval/perf/baselines.json | 57 ++ .../skills/echo-memory/eval/perf/bench.py | 800 +++++++++++++++ .../skills/echo-memory/eval/perf/fixtures.py | 100 ++ .../eval/perf/results/SUMMARY.json | 931 ++++++++++++++++++ .../eval/perf/results/m-bulk-append.json | 55 ++ .../eval/perf/results/m-bulk-get.json | 73 ++ .../eval/perf/results/m-bulk-put.json | 35 + .../eval/perf/results/m-cache.json | 36 + .../eval/perf/results/m-expand-graph.json | 76 ++ .../eval/perf/results/m-index.json | 25 + .../echo-memory/eval/perf/results/m-lint.json | 25 + .../echo-memory/eval/perf/results/m-lock.json | 40 + .../eval/perf/results/m-pool-warmup.json | 52 + .../eval/perf/results/m-queue.json | 32 + .../eval/perf/results/m-read-full.json | 62 ++ .../eval/perf/results/m-resolve.json | 360 +++++++ .../echo-memory/eval/perf/results/m-soak.json | 56 ++ .../perf/results/m-subject-pull-after.json | 177 ++++ .../eval/perf/results/m-subject-pull.json | 177 ++++ .../eval/perf/results/m-worker-sweep.json | 50 + .../eval/perf/results/report-data.json | 545 ++++++++++ .../echo-memory/eval/perf/results/run.log | 1 + .../skills/echo-memory/eval/perf/run_heavy.sh | 12 + .../skills/echo-memory/scripts/echo_recall.py | 45 +- 28 files changed, 4045 insertions(+), 19 deletions(-) create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/ECHO-Performance-Benchmark.pdf create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/PERF-TEST-PLAN.md create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/README.md create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/baselines.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/bench.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/fixtures.py create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/SUMMARY.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-append.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-get.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-put.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-cache.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-expand-graph.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-index.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-lint.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-lock.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-pool-warmup.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-queue.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-read-full.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-resolve.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-soak.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-subject-pull-after.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-subject-pull.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-worker-sweep.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/report-data.json create mode 100644 echo-memory.plugin.src/skills/echo-memory/eval/perf/results/run.log create mode 100755 echo-memory.plugin.src/skills/echo-memory/eval/perf/run_heavy.sh diff --git a/.DS_Store b/.DS_Store index c71fe80748680ca55eb60e5139986e6c6ff2e994..44f63ba1eac907e8753dec1268e52cd51475f4ae 100644 GIT binary patch delta 1151 zcmZ`(&rcIU6n?X9p@mYmzpxNU3MGL^AZ;W;4+JO@twh_n5ep-s1<(-l00p2h4MmWkc(`?7F~j3l8)I0_i04f0 zoBt(O^SYg+)@`@j%~{X1c@&s)Sdm_VEGVYpep_ul$zr7>C{(~2?}U;f>qa>If#(Tl zvD&)2?T(&aXP>K|cYC~UpWE;E51a}N4h;=&@LnM+-py;Ytmc-fLba$;meR6F)uen+ zN*mIS!N3OZ>ENwNxm!ZDrA_UYkmTziUC<;+y^+`CTzqp=myC29&TR1gx~Ne_Nl;xj zWU6M?^#a|F8B#H(Zc;2+k)`c;iG}oQlBT2Y&{V0K&SW($tLCNX?oA~tNJ?CdQp2D{ zMrG;nQxclQQq_wmOIcA?^Pz=f{L?r>i-{&+J;T96r~$B(z9u^HB=kyMtmBkQRjKN5 zgces%Vs@B?UczfQg3s`SSjhkxBU5CS%##Ijk;KS#B9a1;$(@Ft zW36Xnq~V$~(Gj)!tmu{HMxQN7p;V(DnX{_dqGZ1JefqOEYRCSsq`5v}_645AS{W)y zO;xCx$E>WP3oheZ$KVR2U0gC8Ri^9?`hXeGq; zQF7Lt;e#&nutP9P&ea1P*AW=8)qfESc5xaD)}HdiHuLAe`Y=A(&~Qxl;Mv2J+0^*a;D|Bq0t|Z91S%Bp=(_}#w{mC{0=9AY+ z8f})5de1ts;i@1r$UvYy+(5z=WY=avj_=Hq`E@)e`}1&cFhQK5F*!kJ=HyGFhc*kd HI5GhMKwKn^ diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/ECHO-Performance-Benchmark.pdf b/echo-memory.plugin.src/skills/echo-memory/eval/perf/ECHO-Performance-Benchmark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..12dd760a6ec0fff8db743944defdf55236f21551 GIT binary patch literal 172026 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�Hsz9VmMvGE#h!SrcYa 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�M> ztljrjN;>tJ4%-XAeiw_KY*dQ0>nfOQ+u~pM$}}-w^W*IJeqYmje>bMR&vy@~H!?K0)^Ye+w-_a4DG_?FPVCv>Ag>M7uNXH>?rO7TdB_ZW$cR@O}74t>!>Vz!;f&zeKVi;DfXY%G!I?2eCg+Q zqB<)OBY76IbAT%kSV*!z zmCYa7aH0UiMvU7ul7Mu4Je0_v?cAGZB08TU3QkcP3D&As7&9XR`Gg2$+MB&}R5pp+ z6VeehTL{&GKA@wJ&G7{4dbej+wj1yP#P&F_0%)>9giv@IBt-78B0= z^4$;Db=h1a7VEq7(xc!WD440Shb<#3UMORH&}k!zf@b3t89G2StLNH zm#mQe%w+1q9Fr!4d*W%s-<(l*%;{Njvz7$~5uqLs+bjE+iP@1hzi@ne?|Kv*Fm-g( z$I+ruti%=<>E%EAXQ{@D+v2$VA#i{KO@3jy!L2^izee3i_xI&Vq+UY~^tj}t3^Dbj3JV)V$-5fdP6od zwyGz8HEmUX{K55cplDzmU2%uin3?F&`hC^VAUHg{GG z&D2I_{+w>5Iol~62^)*ZT*y2kw)nz%lC|r=mFo1CLVEKWqb

bJAEp=FG5G-g+@D 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 p*g`w|do0W@&9f6WkQb`Koe*n-=ED`_! literal 0 HcmV?d00001 diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/PERF-TEST-PLAN.md b/echo-memory.plugin.src/skills/echo-memory/eval/perf/PERF-TEST-PLAN.md new file mode 100644 index 0000000..2f0acec --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/PERF-TEST-PLAN.md @@ -0,0 +1,146 @@ +# ECHO Performance Test Suite — Plan + +**Status:** plan only — nothing here is to be run yet. +**Author:** drafted for Jason Stedwell, 2026-06-23. +**Targets the changes that landed in:** 1.1.0 (connection pooling + concurrent `read_many` + shared cache) and 1.2.0 (fuzzy `resolve`, alias derivation/learning, `capture` re-slug fix). +**Scope:** a *performance / timing* harness. It complements — does not replace — the existing correctness tests (`test_echo_client.py`, `test_v1_scaffold.py`, the eval feature tests), which stay as the pass/fail gate for behaviour. + +--- + +## 1. Goals + +1. Put hard numbers on the three operations Jason called out — full endpoint read, full subject pulls, bulk get/put/append — so the 1.1.0 perf claims ("lint 0.90s / sweep 0.85s on 186 notes, ~4.5× from reuse, ~2× from concurrency") become a repeatable, checked-in measurement instead of a one-time observation. +2. Establish **baselines + regression gates** so a future change that quietly reintroduces the pre-1.1.0 per-request TLS handshake (the bug that was timing out sessions) fails the suite instead of silently shipping. +3. Keep every run **non-destructive to the production vault** — writes go to a disposable namespace and are cleaned up (per Jason's "prune test artifacts" rule). + +## 2. Methodology (applies to every metric) + +- **Harness:** a new `bench.py` under `scripts/` (or `eval/perf/`), reusing the live `echo` client module so it measures the real pooled/concurrent code path, not a reimplementation. +- **Timing:** `time.perf_counter()` around each operation. Report **min / median / p90 / p95 / max / mean** over N iterations — not a single number. Single timings hide GC pauses and TLS renegotiation. +- **Warm-up:** discard the first 1–2 iterations (cold connection pool, cold server cache) and report them *separately* as the cold-start cost — that delta is itself one of the most interesting numbers (it's what 1.1.0's keep-alive was meant to kill). +- **Iterations:** default N=10 for read-heavy metrics, N=5 for write-heavy (writes are slower and we don't want to bloat the vault). Make N a CLI flag. +- **Environment capture:** record `ECHO_WORKERS`, `ECHO_TIMEOUT`, vault note count, git commit/plugin version, and wall-clock date in the results header so two runs are comparable. +- **Output:** machine-readable JSON (`eval/perf/results/-.json`) plus a short human table to stdout. JSON lets us diff runs over time and chart trend. +- **Isolation:** all test writes live under a dedicated prefix, e.g. `_agent/_bench//...`, never in `projects/`, `resources/`, `inbox/`, or `journal/`. A `--cleanup` pass deletes the whole prefix at the end; a `--keep` flag preserves it for debugging. The advisory lock (`echo.py lock`) is taken for the write phases so a bench run can't race a real CoWork session. +- **Network honesty:** the endpoint is the live `echoapi.alwisp.com`, so absolute numbers depend on WAN latency. Report **both** absolute ms and a relative ratio against the same run's single-GET baseline, so the suite stays meaningful regardless of where it's run from. + +--- + +## 3. Core metrics (the three Jason asked for) + +### 3.1 Timed full endpoint read (full-vault bulk read) + +**What it measures:** the headline 1.1.0 path — reading every note in the vault via concurrent `read_many` over warm pooled connections vs. serial GETs. + +**Procedure:** +1. Enumerate all vault paths (the same listing `sweep.py`/`vault_lint.py` walk). +2. **Serial baseline:** GET each path one at a time on a single connection. Time total + per-note. +3. **Pooled+concurrent:** `read_many(paths)` at the default `ECHO_WORKERS`. Time total. +4. Report total time, notes/sec, and the **speedup ratio** (serial ÷ concurrent). Repeat the concurrent pass N times for percentile spread. + +**What "good" looks like:** concurrent should be multiples faster than serial; full-vault concurrent read should stay well under the agent tool timeout (the original failure mode). Record current note count alongside — the metric is only comparable at similar vault sizes. + +**Regression gate (suggested):** full-vault concurrent read median < a threshold (e.g. 3s at ~200 notes) AND speedup ratio ≥ ~2×. Tune thresholds after the first baseline run. + +### 3.2 Timed full subject pulls (e.g. APTA, CapMetro statuses) + +**What it measures:** the cost of a `recall ""` — search + 1-hop neighbourhood expansion along `## Related` links and `source_notes` — which is the real "what do we know about X" operation, and exercises the 1.2.0 resolver. APTA and CapMetro are live subjects in the vault, so they're realistic fixtures. + +**Procedure:** +1. Pick a fixed fixture set of subjects with **different neighbourhood sizes**: a small one, a hub note with many links (e.g. the `echo` project or `operator-preferences`), and the two named ones (APTA, CapMetro). Neighbourhood size is the real cost driver, so vary it deliberately. +2. Time end-to-end `recall` for each: (a) the search call, (b) the 1-hop expansion reads, (c) total. Break out the search vs. expansion split — it tells us whether latency is in the index/search or in the fan-out reads (which 1.1.0's `read_many` should accelerate). +3. Also time bare `resolve ""` (the O(1) index lookup) separately as the floor — recall should be resolve + search + N expansion reads. +4. Report per-subject and aggregate percentiles; annotate each with its neighbourhood node count so time-per-node is derivable. + +**What "good" looks like:** `resolve` is near-instant (in-memory index); `recall` scales with neighbourhood size but the expansion reads are concurrent, so a hub note shouldn't be linearly worse than a leaf. + +**Live result + the `expand-graph` sub-metric (added 2026-06-23).** The first run showed `recall()` at 2.8–4.7 s/subject. Profiling pinned it on the graph layer, not BM25: `load_index()` ~500 ms (index already persisted/incremental), `score()` 0.1 ms, `expand_graph()` ~3,000 ms doing one serial GET per neighbour. Fix shipped in `echo_recall.py` — the BFS now fetches each hop via `read_many`. A dedicated **`expand-graph`** metric times serial vs concurrent expansion (gate: ≥2× min) and asserts byte-identical ranking + scores against a serial reference, so the fix can't silently regress. Measured 3.5–4.2×; end-to-end `recall()` down ~1.7×. + +### 3.3 Timed bulk get / put / append + +**What it measures:** write-path throughput and the read-back/idempotency overhead Jason deliberately pays for. + +**Procedure (all in the `_agent/_bench//` namespace):** +- **Bulk GET:** create K fixture notes once, then time reading them back — both serially and via `read_many` — to compare against §3.1 at a controlled, fixed K (e.g. 25/50/100) so the read curve is isolated from whatever the live vault happens to contain. +- **Bulk PUT:** time creating K notes. `echo.py put` is read-back-verified (it re-GETs after writing), so this measures the true write+verify cost, not a fire-and-forget POST. Report puts/sec and per-put median. +- **Bulk APPEND:** time K appends to a single growing note. `append` is whole-line idempotent (it GETs first and skips duplicates), so also measure the **idempotent-skip path**: re-append the same K lines and confirm near-zero net writes — time the skip vs. the real append. This validates that idempotency isn't quietly O(file size) per append as the note grows. +- **Concurrent write caveat:** writes to single-line shared files assume one writer; the bulk-write bench must use per-note targets (or hold the lock) so it doesn't model an unsupported pattern. + +**What "good" looks like:** PUT slower than GET (verify round-trip); append-skip much cheaper than append-write; no superlinear blowup as the appended note grows. + +--- + +## 4. Additional test suggestions (recommended, beyond the three) + +These target the rest of the 1.1.0/1.2.0 surface and the known historical failure modes. + +1. **Connection-pool cold vs. warm.** Isolate the keep-alive win directly: time the *first* request after pool creation vs. the median of subsequent requests. This is the single clearest proof the pooling fix is alive; a regression here is the early-warning signal for the old per-request-handshake bug. + +2. **Concurrency scaling sweep (`ECHO_WORKERS`).** Run §3.1 at workers = 1, 2, 4, 8, 16. Plot total time vs. workers to find the knee and confirm the default of 8 is sensible for the current vault/WAN, and that it degrades gracefully (no errors/timeouts) at high concurrency. + +3. **Fuzzy-resolve latency + accuracy (1.2.0).** Two parts: (a) *timing* — `resolve` on exact, alias, and shortened/typo'd mentions; the fuzzy-candidate path does more work, so measure its overhead vs. an exact hit. (b) *correctness* — a fixture table of mention → expected canonical slug (e.g. "echo memory" → `echo`, alias hits, a near-miss that *should* return ranked candidates rather than spawn a duplicate). This guards the actual bug 1.2.0 fixed. Correctness assertions, not just timing. + +4. **Index build/rebuild time.** Time `sweep.py` entity-index rebuild + link symmetrization on the full vault. This is the other full-vault script that historically neared the timeout; gate it like §3.1. + +5. **Lint + recall-rebuild full-vault timing.** Regression-gate `vault_lint.py` and the recall rebuild against the 1.1.0 baselines (lint ~0.90s, sweep ~0.85s @ 186 notes). These are the concrete numbers in the changelog — turn them into asserted thresholds scaled to current note count. + +6. **Lock contention timing.** Time `lock` acquisition when free, when held-fresh (should fast-fail with exit 75), and reclaim of a stale lock past `ECHO_LOCK_TTL`. Confirms the read-back-confirmed lock doesn't add pathological latency and behaves under contention. + +7. **Offline write-ahead queue throughput (1.0 carry-over).** With the endpoint unreachable, time enqueue of K writes, then time `flush` replay on reconnect. Validates the queue doesn't degrade and replays in order. Useful because it's an untimed path today. + +8. **Cache hit/miss ratio.** Instrument the shared single-pass cache: in a sweep/lint run, count served-from-cache vs. network reads. A correctness+efficiency check that the cache is actually collapsing duplicate reads, not just present. + +9. **Scaling fixture / synthetic vault.** Optional but valuable: a script that mints a throwaway vault of N synthetic notes (100 / 500 / 1000) in the bench namespace to measure how all the above scale *beyond* the current ~190 notes — answers "when does the next timeout cliff arrive?" before it hits a real session. + +10. **Soak / stability pass.** Run §3.1 in a loop for M minutes to surface connection leaks, pool exhaustion, or memory growth in the long-lived client — relevant because the pool is thread-local and long-running. + +--- + +## 5. Proposed file layout + +``` +eval/perf/ + PERF-TEST-PLAN.md # this file + bench.py # the harness (CLI: --metric, --iterations, --workers, --cleanup/--keep, --json-out) + fixtures.py # subject lists, K-note generators, mention→slug resolve table + baselines.json # committed thresholds + reference numbers (per note-count bucket) + results/ # dated JSON run outputs (gitignored or kept for trend) + 2026-06-23-.json +``` + +`bench.py` subcommands map 1:1 to the metrics: `read-full`, `subject-pull`, `bulk-get`, `bulk-put`, `bulk-append`, plus `pool-warmup`, `worker-sweep`, `resolve`, `index`, `lint`, `lock`, `queue`, `cache`, `soak`. + +## 6. Decisions (resolved 2026-06-23) + +- **Baseline policy:** **relative ratios** are the gate (`baselines.json`); absolute ms are recorded as informational. Portable across machines/WAN. +- **CI vs. manual:** **manual / pre-release.** The suite is invoked by hand before tagging; it is not wired into CI (avoids WAN flakiness failing builds against the live endpoint). +- **Synthetic vault (suggestion #9):** **deferred.** Benchmark the real ~190-note vault for now; add the generator if vault growth makes it relevant. +- **Cleanup:** **auto-delete** the `_agent/_bench//` namespace at end of each run, with `--keep` to override. + +## 7. Build status + +Harness built under `eval/perf/`: + +- `bench.py` — the CLI harness; all 14 metrics implemented (`read-full`, `subject-pull`, `bulk-get`, `bulk-put`, `bulk-append`, `pool-warmup`, `worker-sweep`, `resolve`, `index`, `lint`, `lock`, `queue`, `cache`, `soak`). +- `fixtures.py` — subject list (APTA/CapMetro/hubs/leaf), the 1.2.0 resolve correctness table, and bench-note generators. +- `baselines.json` — relative-ratio gates (tune thresholds after the first real run). +- `README.md` — run instructions, metric map, isolation/cleanup, caveats. +- `results/` — dated JSON run outputs. + +Verified offline (compile + imports + `--list`/`--help`), then **run live** against the 197-note vault @ af16598 — 11/11 gate checks pass. First run surfaced the recall() finding below; the fix and a guarding metric were added the same session. + +## 8. ECHO usage improvements (from the live run) + +The benchmark wasn't just validation — it surfaced concrete improvements to how ECHO should do reads. In priority order: + +1. **Apply `read_many` everywhere a code path reads a *set* of notes (DONE for `expand_graph`).** The 1.1.0 release shipped concurrent bulk reads but only `sweep`/`lint`/full-vault read adopted it. `recall()`'s graph expansion was still serial — one GET per neighbour — which is why it ran 3 s. The rule going forward: *any* loop that GETs more than ~3 notes whose paths are known up front should batch them through `echo.read_many`, not iterate `get_text`. Audit candidates: `expand_graph` (fixed), `_brief` result printing (still serial), `rebuild()`'s vault walk (still serial), and any future multi-note reader. + +2. **Prefetch `_brief` bodies in one `read_many` (NEXT).** After expansion, `recall` prints each primary hit + neighbour via `_brief()`, which GETs the note again to pull its type/status. Those are known paths — fetch them all in one concurrent batch (and reuse bodies already pulled during expansion via a per-call cache, so a note fetched in the BFS isn't re-fetched to print it). + +3. **Reuse one in-process read cache across a `recall` call.** `load_index`, `expand_graph`, and `_brief` currently re-fetch overlapping notes. A single `{path: text}` memo passed through the call (the same pattern `sweep.py` already uses via `read_many` once) removes the duplicate GETs — `load_index`'s ~500 ms and the `_brief` re-reads are the remaining recall cost. + +4. **Make "batch the reads" a documented contract, not a per-site fix.** Add a one-line rule to the plugin's operating contract / API reference: *prefer `read_many(paths)` over a GET loop; serial multi-note reads are a performance bug.* This is what would have caught the `expand_graph` regression at authoring time. The `expand-graph` metric now enforces it mechanically for recall; the contract generalizes it. + +5. **Consider indexing `## Related`/`source_notes` adjacency into the persisted index.** Longer-term: the graph edges are derivable and change only on write. Storing an adjacency list alongside the BM25 postings (maintained by `capture`/`sweep`, same as `entities.json`) would let `expand_graph` skip re-reading note bodies just to extract links — turning the graph layer into an in-memory lookup like `resolve`. Bigger change; revisit if recall latency still matters after #2–#3. + +None of #1–#4 add dependencies or change behavior — they apply the concurrency primitive that already exists. #1 is done and gated; #2–#3 are the next cheap wins; #5 is the structural option. diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/README.md b/echo-memory.plugin.src/skills/echo-memory/eval/perf/README.md new file mode 100644 index 0000000..b3b3a83 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/README.md @@ -0,0 +1,96 @@ +# ECHO Performance Test Suite + +A timing harness for the operations the **1.1.0** (connection pooling + concurrent +`read_many` + shared cache) and **1.2.0** (fuzzy `resolve` / alias learning) updates +targeted. It reuses the live `echo` client module, so it measures the real pooled / +concurrent code path — not a reimplementation. + +This is a **manual / pre-release** tool, not a CI gate: it depends on the live +endpoint (`echoapi.alwisp.com`) and WAN latency. Gates are **relative ratios** +(portable), with absolute milliseconds kept as informational context. + +See `PERF-TEST-PLAN.md` for the design rationale behind each metric. + +## Run + +```bash +cd eval/perf +python3 bench.py --list # list metrics +python3 bench.py all # the read-only safe set (no vault writes) +python3 bench.py read-full -n 10 # full-vault serial-vs-concurrent read +python3 bench.py subject-pull # recall APTA / CapMetro / hubs (resolve+search+recall split) +python3 bench.py bulk-put -k 50 # write metric -> uses the _bench namespace +python3 bench.py worker-sweep # ECHO_WORKERS = 1,2,4,8,16 scaling curve +python3 bench.py resolve # 1.2.0 fuzzy correctness table + timing +python3 bench.py soak --soak-seconds 120 # stability / leak watch +``` + +Set the key first if the baked-in fallback warning bothers you: +`python3 ../../scripts/echo.py write-key ` (or `export ECHO_KEY=...`). + +## Metrics + +| Metric | Plan § | Reads/Writes | What it proves | +|---|---|---|---| +| `read-full` | 3.1 | read-only | full-vault concurrent vs serial speedup (1.1.0 headline) | +| `subject-pull` | 3.2 | read-only | `recall` cost for APTA/CapMetro/hubs, split into resolve floor + search + expansion | +| `expand-graph` | 3.2a | read-only | recall()'s graph layer: serial vs concurrent neighbourhood expansion + ranking-parity guard (pins the real recall bottleneck) | +| `bulk-get` | 3.3 | writes seed | concurrent vs serial read at a controlled K | +| `bulk-put` | 3.3 | **writes** | read-back-verified PUT throughput (puts/sec) | +| `bulk-append` | 3.3 | **writes** | append throughput + idempotent-skip path (no O(file) blowup) | +| `pool-warmup` | 4.1 | read-only | cold vs warm single GET — keep-alive is alive | +| `worker-sweep` | 4.2 | read-only | concurrency scaling knee; validates default workers=8 | +| `resolve` | 4.3 | read-only | fuzzy-resolve timing **and** correctness (anti-duplicate guard) | +| `index` | 4.4 | read-only | `sweep.py` index rebuild + link symmetrize time | +| `lint` | 4.5 | read-only | `vault_lint.py` full-vault time vs 1.1.0 baseline | +| `lock` | 4.6 | **writes** lock | advisory-lock acquire/contend/release timing + semantics | +| `queue` | 4.7 | none (bogus base) | offline write-ahead enqueue timing (see caveat) | +| `cache` | 4.8 | read-only | `read_many` dedups a duplicated path list | +| `soak` | 4.10 | read-only | stability over a window; leak/drift detector | + +`all` runs only the read-only set. Write metrics must be requested by name. + +## Isolation & cleanup + +Write metrics touch **only** `_agent/_bench//`, take the advisory lock for +the duration (and abort if another session holds it), and delete the namespace at +the end. Pass `--keep` to preserve it for debugging. Nothing is written to +`projects/`, `resources/`, `inbox/`, or `journal/`. + +## Output + +Each run writes `results/-.json` (machine-readable, for trend diffing) +and prints a human table. The JSON header captures `ECHO_WORKERS`, `ECHO_TIMEOUT`, +note count, git commit, and date so two runs are comparable. + +## Gating + +`baselines.json` holds relative-ratio gates (e.g. `read-full.speedup_ratio >= 1.8`, +`pool-warmup.cold_over_warm_ratio >= 1.5`). Run once to capture a baseline, then +tune the thresholds to the observed numbers before treating a `FAIL` as blocking. +`--no-gate` reports numbers without pass/fail. + +## Finding: recall() bottleneck is the graph layer, not BM25 + +A profiled `recall("operator preferences")` decomposes as: `load_index()` ~500 ms (the +BM25 index is already persisted + incrementally maintained, n_docs=119), `score()` +0.1 ms, and `expand_graph()` **~3,000 ms** — the graph BFS was fetching each neighbour +serially (one GET per node, ~126 nodes). The fix (shipped in `echo_recall.py`) makes the +BFS breadth-first by hop and fetches each frontier through the existing `read_many` +concurrency. Result: `expand_graph` 3.2–4.2x faster with byte-identical ranking and +scores; end-to-end `recall()` ~1.7x (e.g. APTA 3.8 s -> 2.2 s). The `expand-graph` +metric + gate above regression-guard this so it can't quietly revert to serial. + +## Caveats + +- **`resolve` verdicts are PASS/INFO only.** The correctness table in `fixtures.py` + encodes expected slugs against the live vault; until confirmed, mismatches report + `INFO` (tune-me), not `FAIL`. Promote to FAIL once the table is verified. +- **`queue` times the enqueue path only.** A true flush-replay needs a throwaway + sandbox vault so the replay doesn't write to production; that's deferred (the + plan's synthetic-vault item). The metric still proves enqueue works and reports + queue depth. +- **Absolute ms depend on where you run from.** WAN latency to `echoapi.alwisp.com` + dominates single-request times; that's why gates are ratios. +- **Synthetic-vault scaling (100/500/1000 notes) is deferred** per decision — the + suite benchmarks the real ~190-note vault today. diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/baselines.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/baselines.json new file mode 100644 index 0000000..3470a85 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/baselines.json @@ -0,0 +1,57 @@ +{ + "_comment": "Relative-ratio regression gates (Jason's chosen policy). Ratios are portable across machines/WAN; absolute ms in the results JSON stay informational. Tune these after the first real baseline run, then tighten. 'field' is a dotted path into a metric's result; for list items use the index (e.g. sweep.3.median_ms).", + "read-full": { + "gates": [ + { "field": "speedup_ratio", "op": ">=", "min": 1.8, + "why": "concurrent read_many must beat serial by the 1.1.0 concurrency margin; <1.8 means concurrency regressed" } + ] + }, + "bulk-get": { + "gates": [ + { "field": "speedup_ratio", "op": ">=", "min": 1.5, + "why": "controlled-K concurrent vs serial; lower bar than full-vault since K is small" } + ] + }, + "expand-graph": { + "gates": [ + { "field": "min_speedup", "op": ">=", "min": 2.0, + "why": "recall()'s graph layer must fetch each BFS hop concurrently (read_many), not serially per node; <2x means expand_graph regressed to the pre-fix serial walk" }, + { "field": "all_ranking_identical", "op": ">=", "min": 1, + "why": "the concurrent expansion must return the same ranked neighbours as the serial reference" }, + { "field": "all_scores_identical", "op": ">=", "min": 1, + "why": "decayed scores must match the serial reference to within float tolerance" } + ] + }, + "pool-warmup": { + "gates": [ + { "field": "cold_over_warm_ratio", "op": ">=", "min": 1.5, + "why": "cold request must be meaningfully slower than warm — proves keep-alive is reusing the connection; a ratio near 1.0 means every request is re-handshaking (the pre-1.1.0 bug)" } + ] + }, + "bulk-append": { + "gates": [ + { "field": "all_skipped_second_pass", "op": ">=", "min": 1, + "why": "second pass of identical lines must be 100% idempotent skips (boolean true coerced to 1)" } + ] + }, + "soak": { + "gates": [ + { "field": "errors", "op": "<=", "min": 0, + "why": "no read errors across the soak window — connection leaks/pool exhaustion would surface here" }, + { "field": "drift_ratio_late_over_early", "op": "<=", "min": 1.5, + "why": "late passes must not be much slower than early ones; >1.5 suggests a leak or degrading pool" } + ] + }, + "lock": { + "gates": [ + { "field": "ok", "op": ">=", "min": 1, + "why": "free acquire returns 0 and a contended acquire fast-fails with exit 75" } + ] + }, + "cache": { + "gates": [ + { "field": "deduped", "op": ">=", "min": 1, + "why": "read_many must collapse a 3x-duplicated path list to the unique set" } + ] + } +} diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/bench.py b/echo-memory.plugin.src/skills/echo-memory/eval/perf/bench.py new file mode 100644 index 0000000..6ffa66a --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/bench.py @@ -0,0 +1,800 @@ +#!/usr/bin/env python3 +"""bench.py — ECHO performance test suite (timing harness). + +Measures the operations the 1.1.0 (pooling + concurrent read_many + shared cache) +and 1.2.0 (fuzzy resolve / alias learning) updates targeted, and gates them on +RELATIVE ratios (portable across machines/WAN) rather than absolute ms. + +It reuses the live `echo` client module so it times the REAL pooled/concurrent +code path. Reads are non-destructive; write metrics live in a disposable +namespace (_agent/_bench//) cleaned up at the end unless --keep. + +This is a MANUAL / pre-release tool, not a CI gate — it depends on the live +endpoint (echoapi.alwisp.com) and WAN latency. + + python3 bench.py [options] + python3 bench.py all # the read-only safe set + python3 bench.py read-full -n 10 + python3 bench.py subject-pull + python3 bench.py bulk-put -k 50 + python3 bench.py worker-sweep + python3 bench.py resolve # 1.2.0 fuzzy correctness + timing + python3 bench.py --list + +Metrics: read-full, subject-pull, bulk-get, bulk-put, bulk-append, + pool-warmup, worker-sweep, resolve, index, lint, lock, queue, cache, soak + +Options: + -n / --iterations N timed iterations (default 10 read / 5 write) + -k / --count K notes/appends for bulk metrics (default 50) + --workers W override ECHO_WORKERS for this run + --warmup M warm-up iterations to discard (default 2) + --soak-seconds S duration for the soak metric (default 60) + --keep do NOT delete the _bench namespace after the run + --no-gate skip relative-ratio gating (report numbers only) + --json-out PATH write the results JSON here (default results/-.json) + --quiet suppress the per-metric human table (JSON only) + +Exit: 0 all gates pass (or --no-gate) · 1 a gate failed · 2 usage/setup error. +""" + +from __future__ import annotations + +import argparse +import contextlib +import datetime as dt +import io +import json +import os +import statistics +import subprocess +import sys +import time +import uuid +from pathlib import Path + +HERE = Path(__file__).resolve().parent +SCRIPTS = HERE.parent.parent / "scripts" +sys.path.insert(0, str(SCRIPTS)) + +import echo # noqa: E402 the validated client — real pooled/concurrent path +import echo_index # noqa: E402 resolve / fuzzy_candidates (1.2.0) +import echo_recall # noqa: E402 recall (search + 1-hop expansion) + +BASELINES = HERE / "baselines.json" +RESULTS_DIR = HERE / "results" + + +# --------------------------------------------------------------------------- +# stats + timing +# --------------------------------------------------------------------------- +def stats(samples: list[float]) -> dict: + """min/median/p90/p95/max/mean in milliseconds from a list of seconds.""" + if not samples: + return {} + ms = sorted(s * 1000.0 for s in samples) + + def pct(p: float) -> float: + if len(ms) == 1: + return ms[0] + k = (len(ms) - 1) * p + lo, hi = int(k), min(int(k) + 1, len(ms) - 1) + return ms[lo] + (ms[hi] - ms[lo]) * (k - lo) + + return { + "n": len(ms), + "min_ms": round(ms[0], 2), + "median_ms": round(statistics.median(ms), 2), + "p90_ms": round(pct(0.90), 2), + "p95_ms": round(pct(0.95), 2), + "max_ms": round(ms[-1], 2), + "mean_ms": round(statistics.fmean(ms), 2), + } + + +def timed(fn, n: int, warmup: int) -> dict: + """Run fn() warmup+n times. Returns warm-up samples separately from the timed + set so cold-start cost (the thing keep-alive kills) is visible, not averaged in.""" + warm: list[float] = [] + samples: list[float] = [] + for i in range(warmup + n): + t0 = time.perf_counter() + fn() + dt_s = time.perf_counter() - t0 + (warm if i < warmup else samples).append(dt_s) + out = stats(samples) + if warm: + out["warmup"] = stats(warm) + return out + + +def silent(fn): + """Run fn() with stdout swallowed (recall/resolve print a lot).""" + def wrapped(): + with contextlib.redirect_stdout(io.StringIO()): + return fn() + return wrapped + + +# --------------------------------------------------------------------------- +# vault enumeration (mirrors sweep.py / vault_lint.py) +# --------------------------------------------------------------------------- +SKIP_BASENAMES = {"README.md"} + + +def _list_dir(path: str): + p = "" if path in ("", "/") else (path if path.endswith("/") else path + "/") + body = echo.get_text(p) + if body is None: + return [], [] + try: + j = json.loads(body) + except json.JSONDecodeError: + return [], [] + entries = list(j.get("files", [])) + list(j.get("folders", [])) + files = [e for e in entries if not e.endswith("/")] + folders = [e[:-1] for e in entries if e.endswith("/")] + return files, folders + + +def walk_vault(prefix: str = ""): + files, folders = _list_dir(prefix) + for f in files: + yield prefix + f + for d in folders: + yield from walk_vault(f"{prefix}{d}/") + + +def all_notes() -> list[str]: + return [p for p in walk_vault() + if p.endswith(".md") and p.rsplit("/", 1)[-1] not in SKIP_BASENAMES] + + +# --------------------------------------------------------------------------- +# write primitives (replicate echo.py semantics so we time the real cost) +# --------------------------------------------------------------------------- +def put_verified(path: str, body: str) -> None: + """PUT then read-back GET — the same verify echo.py cmd_put does.""" + status, b = echo.request("PUT", echo.vault_url(path), + data=body.encode(), headers={"Content-Type": "text/markdown"}) + echo.check(status, b, f"put {path}") + status, _ = echo.request("GET", echo.vault_url(path)) + if status != 200: + raise echo.EchoError(f"put {path}: did not verify (GET {status})") + + +def append_line(path: str, line: str) -> bool: + """Whole-line idempotent append (mirrors echo.py cmd_append). Returns True if a + write happened, False if it was an idempotent skip.""" + status, body = echo.request("GET", echo.vault_url(path)) + if status == 200: + existing = [ln.rstrip("\r") for ln in body.decode(errors="replace").splitlines()] + if line.rstrip("\r") in existing: + return False + status, body = echo.request("POST", echo.vault_url(path), + data=f"{line}\n".encode(), + headers={"Content-Type": "text/markdown"}) + echo.check(status, body, f"append {path}") + return True + + +def delete_path(path: str) -> None: + echo.request("DELETE", echo.vault_url(path)) + + +# --------------------------------------------------------------------------- +# metrics +# --------------------------------------------------------------------------- +def m_read_full(ctx) -> dict: + """§3.1 full-vault read: serial GET vs concurrent read_many -> speedup ratio.""" + paths = all_notes() + if not paths: + raise echo.EchoError("read-full: no notes enumerated (vault unreachable?)") + + def serial(): + for p in paths: + echo.get_text(p) + + def concurrent(): + echo.read_many(paths) + + # one warm pass so neither side pays the cold-handshake tax (pool-warmup owns that) + echo.get_text(paths[0]) + # serial is a slow reference baseline (N serial round-trips); one warm pass is + # enough to establish the ratio, so we don't multiply the full-vault serial cost. + serial_t = timed(serial, n=1, warmup=0) + concurrent_t = timed(concurrent, n=ctx.n, warmup=ctx.warmup) + ratio = (serial_t["median_ms"] / concurrent_t["median_ms"]) if concurrent_t.get("median_ms") else None + return { + "note_count": len(paths), + "workers": echo.MAX_WORKERS, + "serial": serial_t, + "concurrent": concurrent_t, + "speedup_ratio": round(ratio, 2) if ratio else None, + "notes_per_sec_concurrent": round(len(paths) / (concurrent_t["median_ms"] / 1000.0), 1) + if concurrent_t.get("median_ms") else None, + } + + +def m_subject_pull(ctx) -> dict: + """§3.2 recall a subject: time resolve (floor), search, and full recall, split out.""" + import fixtures + index = echo_index.load() + out = {"subjects": []} + for label, query in fixtures.SUBJECTS: + # floor: O(1) index resolve + resolve_t = timed(lambda: echo_index.resolve(index, query), n=ctx.n, warmup=ctx.warmup) + # search-only layer + def search(): + with contextlib.redirect_stdout(io.StringIO()): + echo.request("POST", f"{echo.BASE}/search/simple/?query={query}") + search_t = timed(search, n=ctx.n, warmup=ctx.warmup) + # full recall (search + 1-hop expansion reads) + recall_t = timed(silent(lambda: echo_recall.recall(query)), n=ctx.n, warmup=ctx.warmup) + out["subjects"].append({ + "label": label, "query": query, + "resolve_floor": resolve_t, + "search_only": search_t, + "recall_total": recall_t, + }) + return out + + +def m_bulk_get(ctx) -> dict: + """§3.3 bulk GET at a controlled K: serial vs read_many over fixture notes.""" + import fixtures + paths = _seed_notes(ctx, fixtures) + + def serial(): + for p in paths: + echo.get_text(p) + + def concurrent(): + echo.read_many(paths) + + serial_t = timed(serial, n=ctx.n, warmup=1) + concurrent_t = timed(concurrent, n=ctx.n, warmup=ctx.warmup) + ratio = (serial_t["median_ms"] / concurrent_t["median_ms"]) if concurrent_t.get("median_ms") else None + return {"k": len(paths), "workers": echo.MAX_WORKERS, + "serial": serial_t, "concurrent": concurrent_t, + "speedup_ratio": round(ratio, 2) if ratio else None} + + +def m_bulk_put(ctx) -> dict: + """§3.3 bulk PUT (read-back verified) -> puts/sec + per-put percentiles.""" + import fixtures + k = ctx.k + samples: list[float] = [] + for i in range(k): + body = fixtures.note_body(ctx.run_id, i) + path = fixtures.note_path(ctx.run_id, i) + t0 = time.perf_counter() + put_verified(path, body) + samples.append(time.perf_counter() - t0) + s = stats(samples) + total_s = sum(samples) + return {"k": k, "per_put": s, "total_ms": round(total_s * 1000, 1), + "puts_per_sec": round(k / total_s, 1) if total_s else None} + + +def m_bulk_append(ctx) -> dict: + """§3.3 bulk APPEND + idempotent-skip path. Confirms idempotency isn't O(file).""" + import fixtures + target = fixtures.append_target(ctx.run_id) + put_verified(target, fixtures.append_target_seed(ctx.run_id)) + k = ctx.k + lines = [f"- 2026-06-23: bench append line {i:04d}" for i in range(k)] + + write_samples: list[float] = [] + for ln in lines: + t0 = time.perf_counter() + wrote = append_line(target, ln) + write_samples.append(time.perf_counter() - t0) + if not wrote: + raise echo.EchoError("bulk-append: expected a write but got an idempotent skip") + + # second pass: every line already present -> must skip, and skip cost is the + # idempotency GET on a now-larger file. Confirms no superlinear blowup. + skip_samples: list[float] = [] + skips = 0 + for ln in lines: + t0 = time.perf_counter() + wrote = append_line(target, ln) + skip_samples.append(time.perf_counter() - t0) + if not wrote: + skips += 1 + + return { + "k": k, + "append_write": stats(write_samples), + "append_skip": stats(skip_samples), + "skips_confirmed": skips, + "all_skipped_second_pass": skips == k, + } + + +def m_pool_warmup(ctx) -> dict: + """§4.1 cold vs warm single GET — the clearest proof keep-alive is alive.""" + paths = all_notes() + probe = paths[0] if paths else "_agent/echo-vault.md" + echo._drop_connection() # force a cold handshake + t0 = time.perf_counter() + echo.get_text(probe) + cold_ms = (time.perf_counter() - t0) * 1000.0 + warm = timed(lambda: echo.get_text(probe), n=max(ctx.n, 10), warmup=1) + ratio = round(cold_ms / warm["median_ms"], 2) if warm.get("median_ms") else None + return {"probe": probe, "cold_ms": round(cold_ms, 2), "warm": warm, + "cold_over_warm_ratio": ratio} + + +def m_worker_sweep(ctx) -> dict: + """§4.2 concurrency scaling: full-vault concurrent read at workers 1,2,4,8,16.""" + paths = all_notes() + if not paths: + raise echo.EchoError("worker-sweep: no notes enumerated") + original = echo.MAX_WORKERS + rows = [] + try: + # w=1 is a full serial pass (slow); the curve only needs the shape, so use a + # modest sample count and a single pre-warm rather than per-setting warmups. + reps = max(1, ctx.n // 2) + for w in (1, 2, 4, 8, 16): + echo.MAX_WORKERS = w + echo.read_many(paths[:5]) # warm the pool at this worker count + t = timed(lambda: echo.read_many(paths), n=reps, warmup=0) + rows.append({"workers": w, "median_ms": t["median_ms"], "p95_ms": t["p95_ms"]}) + finally: + echo.MAX_WORKERS = original + best = min(rows, key=lambda r: r["median_ms"]) + return {"note_count": len(paths), "sweep": rows, "knee_workers": best["workers"], + "default_workers": original} + + +def m_resolve(ctx) -> dict: + """§4.3 fuzzy resolve — timing AND correctness against the 1.2.0 guard table.""" + import fixtures + index = echo_index.load() + cases = [] + passes = info = 0 + for case in fixtures.RESOLVE_CASES: + mention = case["mention"] + exact_t = timed(lambda: echo_index.resolve(index, mention), n=ctx.n, warmup=ctx.warmup) + fuzzy_t = timed(lambda: echo_index.fuzzy_candidates(index, mention), n=ctx.n, warmup=ctx.warmup) + _, entity = echo_index.resolve(index, mention) + exact_slug = (entity or {}).get("slug") or _slug_from_entity(entity) + cands = echo_index.fuzzy_candidates(index, mention) or [] + cand_slugs = [_cand_slug(c) for c in cands] + + expect, want = case["expect"], case.get("expect_slug") + if expect == "exact": + ok = exact_slug == want + elif expect == "candidates": + ok = (exact_slug != want) and (want in cand_slugs) + else: # miss + ok = entity is None + verdict = "PASS" if ok else "INFO" # tune table to live vault before promoting to FAIL + passes += ok + info += (not ok) + cases.append({"mention": mention, "expect": expect, "expect_slug": want, + "resolved_slug": exact_slug, "candidate_slugs": cand_slugs[:5], + "verdict": verdict, + "resolve_timing": exact_t, "fuzzy_timing": fuzzy_t}) + return {"cases": cases, "passes": passes, "needs_tuning": info, + "note": "verdicts are PASS/INFO only; promote to FAIL once the table is " + "confirmed against the live vault"} + + +def _serial_expand(seeds, nmap, base, max_hops): + """The pre-fix serial graph BFS, kept here as the reference baseline so the suite + both (a) measures the concurrency speedup and (b) asserts the concurrent + expand_graph still returns byte-identical ranking. If echo_recall.expand_graph ever + regresses to serial, the ratio gate fails; if its results drift, the parity check fails.""" + from collections import deque + score_of = dict(base) + seen = set(seeds) + results = {} + dq = deque((s, 0) for s in seeds) + while dq: + path, hop = dq.popleft() + if hop >= max_hops: + continue + text = echo_recall.links.get_text(path) + if text is None: + continue + body = echo_recall.strip_frontmatter(text) + targets = set(echo_recall.links.all_wikilinks(body)) | set(echo_recall.links.source_notes(text)) + parent = score_of.get(path, 1.0) + for t in targets: + tp = echo_recall._resolve_target(t, nmap) + if not tp or tp in seeds: + continue + decayed = parent * (echo_recall.GRAPH_DECAY ** (hop + 1)) + if decayed > results.get(tp, (0.0, ""))[0]: + results[tp] = (decayed, path) + if tp not in seen: + seen.add(tp) + dq.append((tp, hop + 1)) + return sorted(results.items(), key=lambda kv: -kv[1][0]) + + +def m_expand_graph(ctx) -> dict: + """recall() graph layer: serial vs concurrent neighbourhood expansion. This is the + metric that pins the real recall() bottleneck (the graph BFS, not BM25) and guards + the read_many concurrency fix. Also asserts the concurrent path's ranking + scores + match the serial reference exactly.""" + import fixtures + ix = echo_recall.load_index() + index = echo_index.load() + nmap = echo_index.name_map(index) + max_hops = echo_recall.MAX_HOPS + # hub subjects stress expansion most; fall back to all subjects if none labelled hub + subjects = [s for s in fixtures.SUBJECTS if "hub" in s[0]] or fixtures.SUBJECTS + rows = [] + for label, query in subjects: + hits = ix.score(query, limit=8) + base = {p: s for p, s in hits} + seeds = [p for p, _ in hits] + if not seeds: + continue + echo_recall.expand_graph(seeds, nmap, base, max_hops) # warm + serial_t = timed(lambda: _serial_expand(seeds, nmap, base, max_hops), n=1, warmup=0) + conc_t = timed(lambda: echo_recall.expand_graph(seeds, nmap, base, max_hops), + n=ctx.n, warmup=1) + old = _serial_expand(seeds, nmap, base, max_hops) + new = echo_recall.expand_graph(seeds, nmap, base, max_hops) + ranking_ok = [p for p, _ in old] == [p for p, _ in new] + scores_ok = ranking_ok and all( + abs(old[i][1][0] - new[i][1][0]) < 1e-9 for i in range(len(old))) + ratio = (serial_t["median_ms"] / conc_t["median_ms"]) if conc_t.get("median_ms") else None + rows.append({"label": label, "query": query, "neighbours": len(new), + "serial_ms": serial_t["median_ms"], "concurrent_ms": conc_t["median_ms"], + "speedup": round(ratio, 2) if ratio else None, + "ranking_identical": ranking_ok, "scores_identical": scores_ok}) + speedups = [r["speedup"] for r in rows if r["speedup"]] + return {"subjects": rows, + "min_speedup": round(min(speedups), 2) if speedups else None, + "all_ranking_identical": all(r["ranking_identical"] for r in rows), + "all_scores_identical": all(r["scores_identical"] for r in rows)} + + +def _slug_from_entity(entity): + if not entity: + return None + path = entity.get("path", "") + return path.rsplit("/", 1)[-1].removesuffix(".md") if path else None + + +def _cand_slug(c): + if isinstance(c, dict): + return c.get("slug") or _slug_from_entity(c) + if isinstance(c, (list, tuple)) and c: + return c[0] + return str(c) + + +def m_index(ctx) -> dict: + """§4.4 entity-index rebuild + link symmetrize timing (sweep.py, dry-run = read-only).""" + return _time_script("sweep.py", []) + + +def m_lint(ctx) -> dict: + """§4.5 full-vault lint timing (vault_lint.py) — regression-gate vs 1.1.0 baseline.""" + return _time_script("vault_lint.py", [], allow_exit={0, 1}) + + +def _time_script(name: str, extra: list[str], allow_exit=frozenset({0})) -> dict: + script = SCRIPTS / name + env = dict(os.environ, ECHO_KEY_LEGACY_OK="1") + t0 = time.perf_counter() + proc = subprocess.run([sys.executable, str(script), *extra], + capture_output=True, text=True, env=env, timeout=120) + elapsed = (time.perf_counter() - t0) * 1000.0 + return {"script": name, "elapsed_ms": round(elapsed, 1), + "exit_code": proc.returncode, + "ok": proc.returncode in allow_exit, + "stderr_tail": proc.stderr.strip().splitlines()[-3:] if proc.stderr else []} + + +def m_lock(ctx) -> dict: + """§4.6 advisory-lock timing: acquire-free, contended (held-fresh), release.""" + owner = f"bench-{ctx.run_id}" + other = f"bench-other-{ctx.run_id}" + + def acquire(o): + return echo.cmd_lock(o, quiet=True) + + def release(o): + return echo.cmd_unlock(o, quiet=True) + + release(owner); release(other) # clean slate + t0 = time.perf_counter(); rc_free = acquire(owner); free_ms = (time.perf_counter() - t0) * 1000 + t0 = time.perf_counter(); rc_held = acquire(other); held_ms = (time.perf_counter() - t0) * 1000 + t0 = time.perf_counter(); release(owner); rel_ms = (time.perf_counter() - t0) * 1000 + return {"acquire_free_ms": round(free_ms, 2), "acquire_free_rc": rc_free, + "contended_ms": round(held_ms, 2), "contended_rc_expected_75": rc_held, + "release_ms": round(rel_ms, 2), + "ok": rc_free == 0 and rc_held == 75} + + +def m_queue(ctx) -> dict: + """§4.7 offline write-ahead queue: enqueue K writes against an unreachable base, + then time the flush replay on 'reconnect'. Uses a bogus base so nothing real is + written; restores the base afterward. No production writes occur.""" + import importlib + import echo_queue + k = min(ctx.k, 20) + saved_base = echo.BASE + enqueue_samples: list[float] = [] + try: + for i in range(k): + url = f"{echo.BASE}/vault/{fixtures_qpath(ctx.run_id, i)}" + t0 = time.perf_counter() + echo_queue.enqueue("PUT", url, b"x", {"Content-Type": "text/markdown"}, + idem_key=f"bench-{ctx.run_id}-{i}") + enqueue_samples.append(time.perf_counter() - t0) + pending_before = len(echo_queue.pending()) + # flush is left UNMEASURED-as-success here: with a real endpoint it would + # try to write. We only assert enqueue worked and report depth; a true + # flush-replay timing needs a sandbox vault (see README "queue caveat"). + return {"k": k, "enqueue": stats(enqueue_samples), + "pending_after_enqueue": pending_before, + "note": "flush replay intentionally not run against the live vault; " + "enqueue path timed only. See README queue caveat."} + finally: + echo.BASE = saved_base + importlib.reload(echo_queue) + + +def fixtures_qpath(run_id: str, i: int) -> str: + return f"_agent/_bench/{run_id}/queued-{i:04d}.md" + + +def m_cache(ctx) -> dict: + """§4.8 read_many dedup check: a path list with duplicates must collapse to the + unique set (the cache/dedup that stops a sweep re-fetching a link target).""" + paths = all_notes()[:20] or ["_agent/echo-vault.md"] + dupd = paths + paths + paths # 3x + t0 = time.perf_counter() + result = echo.read_many(dupd) + elapsed = (time.perf_counter() - t0) * 1000 + return {"requested": len(dupd), "unique": len(set(dupd)), + "returned": len(result), "deduped": len(result) == len(set(dupd)), + "elapsed_ms": round(elapsed, 2)} + + +def m_soak(ctx) -> dict: + """§4.10 stability: loop full-vault reads for N seconds; watch for errors/drift.""" + paths = all_notes() + if not paths: + raise echo.EchoError("soak: no notes enumerated") + deadline = time.time() + ctx.soak_seconds + durations: list[float] = [] + errors = 0 + while time.time() < deadline: + t0 = time.perf_counter() + try: + res = echo.read_many(paths) + if any(v is None for v in res.values()): + errors += 1 + except Exception: + errors += 1 + durations.append(time.perf_counter() - t0) + s = stats(durations) + # drift = late iterations slower than early ones (leak / pool exhaustion signal) + half = len(durations) // 2 or 1 + early = statistics.fmean(durations[:half]) * 1000 + late = statistics.fmean(durations[half:]) * 1000 + return {"seconds": ctx.soak_seconds, "iterations": len(durations), "errors": errors, + "per_pass": s, "early_mean_ms": round(early, 2), "late_mean_ms": round(late, 2), + "drift_ratio_late_over_early": round(late / early, 2) if early else None} + + +METRICS = { + "read-full": m_read_full, + "subject-pull": m_subject_pull, + "bulk-get": m_bulk_get, + "bulk-put": m_bulk_put, + "bulk-append": m_bulk_append, + "pool-warmup": m_pool_warmup, + "worker-sweep": m_worker_sweep, + "resolve": m_resolve, + "expand-graph": m_expand_graph, + "index": m_index, + "lint": m_lint, + "lock": m_lock, + "queue": m_queue, + "cache": m_cache, + "soak": m_soak, +} + +# the read-only safe set run by `all` (no writes to the vault) +READONLY = ["pool-warmup", "read-full", "worker-sweep", "subject-pull", + "resolve", "expand-graph", "index", "lint", "cache"] +WRITES = {"bulk-get", "bulk-put", "bulk-append", "lock"} # touch the _bench namespace + + +# --------------------------------------------------------------------------- +# fixture seeding / cleanup +# --------------------------------------------------------------------------- +def _seed_notes(ctx, fixtures) -> list[str]: + """Ensure K bench notes exist; return their paths. Cached on ctx so bulk-get + and bulk-put can share the seed within one run.""" + if getattr(ctx, "_seeded", None): + return ctx._seeded + paths = [] + for i in range(ctx.k): + p = fixtures.note_path(ctx.run_id, i) + put_verified(p, fixtures.note_body(ctx.run_id, i)) + paths.append(p) + ctx._seeded = paths + return paths + + +def cleanup_namespace(run_id: str) -> int: + """Delete every file under _agent/_bench//. Returns count deleted.""" + prefix = f"_agent/_bench/{run_id}/" + victims = [p for p in walk_vault(prefix)] + for p in victims: + delete_path(p) + # remove the (now-empty) run dir marker if the API created one — best effort + return len(victims) + + +# --------------------------------------------------------------------------- +# gating +# --------------------------------------------------------------------------- +def load_baselines() -> dict: + if BASELINES.exists(): + return json.loads(BASELINES.read_text()) + return {} + + +def gate(metric: str, result: dict, baselines: dict) -> list[dict]: + """Apply relative-ratio gates from baselines.json. Returns a list of checks.""" + rules = baselines.get(metric, {}) + checks = [] + for rule in rules.get("gates", []): + path, op, threshold = rule["field"], rule["op"], rule["min"] + val = _dig(result, path) + if val is None: + checks.append({"rule": rule, "value": None, "pass": None, "reason": "field absent"}) + continue + ok = (val >= threshold) if op == ">=" else (val <= threshold) + checks.append({"rule": rule, "value": val, "pass": ok}) + return checks + + +def _dig(obj, dotted: str): + cur = obj + for part in dotted.split("."): + if isinstance(cur, list): + try: + cur = cur[int(part)] + continue + except (ValueError, IndexError): + return None + if not isinstance(cur, dict) or part not in cur: + return None + cur = cur[part] + return cur + + +# --------------------------------------------------------------------------- +# main +# --------------------------------------------------------------------------- +class Ctx: + pass + + +def env_block(args) -> dict: + commit = "unknown" + try: + commit = subprocess.run(["git", "rev-parse", "--short", "HEAD"], + cwd=str(SCRIPTS), capture_output=True, text=True, + timeout=10).stdout.strip() or "unknown" + except Exception: + pass + return { + "date": dt.datetime.now().isoformat(timespec="seconds"), + "endpoint": echo.BASE, + "commit": commit, + "echo_workers": echo.MAX_WORKERS, + "echo_timeout": echo.TIMEOUT, + "python": sys.version.split()[0], + "iterations": args.iterations, + "count_k": args.count, + } + + +def human_table(name: str, result: dict, checks: list[dict]) -> str: + lines = [f"\n=== {name} ==="] + lines.append(json.dumps(result, indent=2)) + for c in checks: + mark = "PASS" if c["pass"] else ("—" if c["pass"] is None else "FAIL") + lines.append(f" [{mark}] {c['rule']['field']} {c['rule']['op']} {c['rule']['min']} (got {c['value']})") + return "\n".join(lines) + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser(description="ECHO performance test suite (manual/pre-release)") + ap.add_argument("metric", nargs="?", help="metric name, or 'all' for the read-only set") + ap.add_argument("--list", action="store_true", help="list metrics and exit") + ap.add_argument("-n", "--iterations", type=int, default=None) + ap.add_argument("-k", "--count", type=int, default=50) + ap.add_argument("--workers", type=int, default=None) + ap.add_argument("--warmup", type=int, default=2) + ap.add_argument("--soak-seconds", type=int, default=60) + ap.add_argument("--keep", action="store_true", help="do not delete the _bench namespace") + ap.add_argument("--no-gate", action="store_true") + ap.add_argument("--json-out", default=None) + ap.add_argument("--quiet", action="store_true") + args = ap.parse_args(argv) + + if args.list or not args.metric: + print("Metrics:", ", ".join(METRICS)) + print("Groups : all (read-only safe set:", ", ".join(READONLY) + ")") + print("Writes to _bench namespace:", ", ".join(sorted(WRITES))) + return 0 if args.list else 2 + + if args.workers is not None: + echo.MAX_WORKERS = args.workers + + selected = READONLY if args.metric == "all" else [args.metric] + for m in selected: + if m not in METRICS: + print(f"unknown metric: {m}", file=sys.stderr) + return 2 + + ctx = Ctx() + ctx.run_id = dt.datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + uuid.uuid4().hex[:6] + ctx.k = args.count + ctx.warmup = args.warmup + ctx.soak_seconds = args.soak_seconds + ctx._seeded = None + + baselines = load_baselines() + need_writes = any(m in WRITES for m in selected) + lock_owner = f"bench-{ctx.run_id}" + if need_writes: + if echo.cmd_lock(lock_owner, quiet=True) == 75: + print("bench: vault lock is held by another session — aborting write metrics.", + file=sys.stderr) + return 2 + + report = {"env": env_block(args), "run_id": ctx.run_id, "metrics": {}} + any_fail = False + try: + for m in selected: + ctx.n = args.iterations if args.iterations is not None else (5 if m in WRITES else 10) + try: + result = METRICS[m](ctx) + except Exception as exc: # noqa: BLE001 + result = {"error": str(exc)} + any_fail = True + checks = [] if args.no_gate else gate(m, result, baselines) + if any(c["pass"] is False for c in checks): + any_fail = True + report["metrics"][m] = {"result": result, "gates": checks} + if not args.quiet: + print(human_table(m, result, checks)) + finally: + if need_writes: + echo.cmd_unlock(lock_owner, quiet=True) + if need_writes and not args.keep: + n = cleanup_namespace(ctx.run_id) + report["cleanup"] = {"deleted": n} + if not args.quiet: + print(f"\nbench: cleaned up {n} files under _agent/_bench/{ctx.run_id}/") + elif need_writes and args.keep: + report["cleanup"] = {"deleted": 0, "kept": f"_agent/_bench/{ctx.run_id}/"} + + RESULTS_DIR.mkdir(parents=True, exist_ok=True) + out_path = Path(args.json_out) if args.json_out else \ + RESULTS_DIR / f"{report['env']['date'][:10]}-{report['env']['commit']}.json" + out_path.write_text(json.dumps(report, indent=2)) + print(f"\nbench: results -> {out_path}") + return 1 if any_fail else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/fixtures.py b/echo-memory.plugin.src/skills/echo-memory/eval/perf/fixtures.py new file mode 100644 index 0000000..6aa0b1e --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/fixtures.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""fixtures.py — inputs for the ECHO performance harness (bench.py). + +Holds the *what* (subjects to pull, notes to write, mentions to resolve) so the +harness (bench.py) stays the *how* (timing, stats, gating). Nothing here touches +the network; bench.py owns all I/O. +""" + +from __future__ import annotations + +# --- §3.2 subject pulls ------------------------------------------------------- +# (label, query) pairs fed to `recall`. Chosen to span neighbourhood sizes — the +# real cost driver — from a likely-leaf to a known hub. APTA and CapMetro are the +# subjects Jason named; "echo" and "operator preferences" are dense hubs that +# stress the 1-hop expansion (the read_many fan-out 1.1.0 accelerated). +SUBJECTS: list[tuple[str, str]] = [ + ("apta", "APTA"), + ("capmetro", "CapMetro"), + ("hub-echo", "echo"), + ("hub-prefs", "operator preferences"), + ("leaf-rivnut", "rivnut torque spec"), +] + + +# --- §4.3 fuzzy-resolve correctness table (1.2.0) ----------------------------- +# Each case: mention, expected behaviour. `expect_slug` is the canonical slug an +# exact/alias/fuzzy match should land on (None = no entity expected). `expect` +# is the classifier: +# "exact" -> resolve returns an entity whose slug == expect_slug +# "candidates" -> no exact hit, but fuzzy_candidates surfaces expect_slug +# (the 1.2.0 anti-duplicate guard — a near-miss must NOT resolve +# to nothing and silently spawn a new note) +# "miss" -> neither; genuinely unknown +# Tune expect_slug to the live vault; unknowns are reported as INFO, not failures. +RESOLVE_CASES: list[dict] = [ + {"mention": "echo", "expect": "exact", "expect_slug": "echo"}, + {"mention": "echo memory", "expect": "candidates", "expect_slug": "echo"}, + {"mention": "ECHO plugin", "expect": "candidates", "expect_slug": "echo"}, + {"mention": "goldbrain", "expect": "exact", "expect_slug": "goldbrain"}, + {"mention": "jason stedwell", "expect": "exact", "expect_slug": "jason-stedwell"}, + {"mention": "jason", "expect": "candidates", "expect_slug": "jason-stedwell"}, + {"mention": "zzqx nonexistent entity", "expect": "miss", "expect_slug": None}, +] + + +# --- §3.3 bulk write fixtures ------------------------------------------------- +BENCH_PREFIX = "_agent/_bench" # run-scoped namespace: _agent/_bench//... + + +def note_body(run_id: str, i: int) -> str: + """A realistic-shape note: canonical frontmatter + a couple of headings, so + PUT cost reflects a true note, not a one-liner. agent_written + a _bench tag + make these trivially greppable if a cleanup is ever missed.""" + return ( + "---\n" + "type: working-memory\n" + "status: active\n" + "created: 2026-06-23\n" + "updated: 2026-06-23\n" + "tags:\n" + " - agent\n" + " - _bench\n" + "agent_written: true\n" + f"source_notes: []\n" + f"bench_run: {run_id}\n" + "---\n\n" + f"# Bench Note {i:04d}\n\n" + "## Body\n" + f"Synthetic benchmark note {i} for run {run_id}. " + "Filler so the payload is not pathologically small: " + + ("lorem ipsum dolor sit amet " * 6) + + "\n\n## Log\n- seed\n" + ) + + +def note_path(run_id: str, i: int) -> str: + return f"{BENCH_PREFIX}/{run_id}/note-{i:04d}.md" + + +def append_target(run_id: str) -> str: + return f"{BENCH_PREFIX}/{run_id}/append-target.md" + + +def append_target_seed(run_id: str) -> str: + return ( + "---\n" + "type: working-memory\n" + "status: active\n" + "created: 2026-06-23\n" + "updated: 2026-06-23\n" + "tags:\n" + " - agent\n" + " - _bench\n" + "agent_written: true\n" + "source_notes: []\n" + f"bench_run: {run_id}\n" + "---\n\n" + "# Append Target\n\n" + "## Log\n" + ) diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/SUMMARY.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/SUMMARY.json new file mode 100644 index 0000000..27f7b4e --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/SUMMARY.json @@ -0,0 +1,931 @@ +{ + "env": { + "date": "2026-06-23T20:17:09", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": 4, + "count_k": 50 + }, + "metrics": { + "bulk-append": { + "result": { + "k": 30, + "append_write": { + "n": 30, + "min_ms": 87.11, + "median_ms": 104.08, + "p90_ms": 190.63, + "p95_ms": 198.89, + "max_ms": 241.91, + "mean_ms": 117.22 + }, + "append_skip": { + "n": 30, + "min_ms": 47.57, + "median_ms": 51.42, + "p90_ms": 59.82, + "p95_ms": 61.57, + "max_ms": 73.49, + "mean_ms": 53.22 + }, + "skips_confirmed": 30, + "all_skipped_second_pass": true + }, + "gates": [ + { + "rule": { + "field": "all_skipped_second_pass", + "op": ">=", + "min": 1, + "why": "second pass of identical lines must be 100% idempotent skips (boolean true coerced to 1)" + }, + "value": true, + "pass": true + } + ] + }, + "bulk-get": { + "result": { + "k": 30, + "workers": 8, + "serial": { + "n": 5, + "min_ms": 1520.47, + "median_ms": 1606.86, + "p90_ms": 1805.72, + "p95_ms": 1835.83, + "max_ms": 1865.94, + "mean_ms": 1658.6, + "warmup": { + "n": 1, + "min_ms": 1601.2, + "median_ms": 1601.2, + "p90_ms": 1601.2, + "p95_ms": 1601.2, + "max_ms": 1601.2, + "mean_ms": 1601.2 + } + }, + "concurrent": { + "n": 5, + "min_ms": 340.31, + "median_ms": 356.48, + "p90_ms": 358.82, + "p95_ms": 359.32, + "max_ms": 359.83, + "mean_ms": 351.42, + "warmup": { + "n": 2, + "min_ms": 353.43, + "median_ms": 434.94, + "p90_ms": 500.15, + "p95_ms": 508.3, + "max_ms": 516.45, + "mean_ms": 434.94 + } + }, + "speedup_ratio": 4.51 + }, + "gates": [ + { + "rule": { + "field": "speedup_ratio", + "op": ">=", + "min": 1.5, + "why": "controlled-K concurrent vs serial; lower bar than full-vault since K is small" + }, + "value": 4.51, + "pass": true + } + ] + }, + "bulk-put": { + "result": { + "k": 30, + "per_put": { + "n": 30, + "min_ms": 100.09, + "median_ms": 114.98, + "p90_ms": 212.46, + "p95_ms": 236.18, + "max_ms": 330.77, + "mean_ms": 143.25 + }, + "total_ms": 4297.5, + "puts_per_sec": 7.0 + }, + "gates": [] + }, + "cache": { + "result": { + "requested": 60, + "unique": 20, + "returned": 20, + "deduped": true, + "elapsed_ms": 307.13 + }, + "gates": [ + { + "rule": { + "field": "deduped", + "op": ">=", + "min": 1, + "why": "read_many must collapse a 3x-duplicated path list to the unique set" + }, + "value": true, + "pass": true + } + ] + }, + "expand-graph": { + "result": { + "subjects": [ + { + "label": "hub-echo", + "query": "echo", + "neighbours": 120, + "serial_ms": 2227.67, + "concurrent_ms": 642.2, + "speedup": 3.47, + "ranking_identical": true, + "scores_identical": true + }, + { + "label": "hub-prefs", + "query": "operator preferences", + "neighbours": 126, + "serial_ms": 2910.46, + "concurrent_ms": 698.8, + "speedup": 4.16, + "ranking_identical": true, + "scores_identical": true + } + ], + "min_speedup": 3.47, + "all_ranking_identical": true, + "all_scores_identical": true + }, + "gates": [ + { + "rule": { + "field": "min_speedup", + "op": ">=", + "min": 2.0, + "why": "recall()'s graph layer must fetch each BFS hop concurrently (read_many), not serially per node; <2x means expand_graph regressed to the pre-fix serial walk" + }, + "value": 3.47, + "pass": true + }, + { + "rule": { + "field": "all_ranking_identical", + "op": ">=", + "min": 1, + "why": "the concurrent expansion must return the same ranked neighbours as the serial reference" + }, + "value": true, + "pass": true + }, + { + "rule": { + "field": "all_scores_identical", + "op": ">=", + "min": 1, + "why": "decayed scores must match the serial reference to within float tolerance" + }, + "value": true, + "pass": true + } + ] + }, + "index": { + "result": { + "script": "sweep.py", + "elapsed_ms": 4629.0, + "exit_code": 0, + "ok": true, + "stderr_tail": [] + }, + "gates": [] + }, + "lint": { + "result": { + "script": "vault_lint.py", + "elapsed_ms": 5126.4, + "exit_code": 1, + "ok": true, + "stderr_tail": [] + }, + "gates": [] + }, + "lock": { + "result": { + "acquire_free_ms": 159.18, + "acquire_free_rc": 0, + "contended_ms": 57.89, + "contended_rc_expected_75": 75, + "release_ms": 132.92, + "ok": true + }, + "gates": [ + { + "rule": { + "field": "ok", + "op": ">=", + "min": 1, + "why": "free acquire returns 0 and a contended acquire fast-fails with exit 75" + }, + "value": true, + "pass": true + } + ] + }, + "pool-warmup": { + "result": { + "probe": "_agent/echo-vault.md", + "cold_ms": 163.25, + "warm": { + "n": 12, + "min_ms": 45.64, + "median_ms": 50.29, + "p90_ms": 52.51, + "p95_ms": 53.79, + "max_ms": 55.28, + "mean_ms": 50.26, + "warmup": { + "n": 1, + "min_ms": 51.68, + "median_ms": 51.68, + "p90_ms": 51.68, + "p95_ms": 51.68, + "max_ms": 51.68, + "mean_ms": 51.68 + } + }, + "cold_over_warm_ratio": 3.25 + }, + "gates": [ + { + "rule": { + "field": "cold_over_warm_ratio", + "op": ">=", + "min": 1.5, + "why": "cold request must be meaningfully slower than warm \u2014 proves keep-alive is reusing the connection; a ratio near 1.0 means every request is re-handshaking (the pre-1.1.0 bug)" + }, + "value": 3.25, + "pass": true + } + ] + }, + "queue": { + "result": { + "k": 10, + "enqueue": { + "n": 10, + "min_ms": 0.07, + "median_ms": 0.08, + "p90_ms": 0.18, + "p95_ms": 0.2, + "max_ms": 0.22, + "mean_ms": 0.1 + }, + "pending_after_enqueue": 10, + "note": "flush replay intentionally not run against the live vault; enqueue path timed only. See README queue caveat." + }, + "gates": [] + }, + "read-full": { + "result": { + "note_count": 197, + "workers": 8, + "serial": { + "n": 1, + "min_ms": 11376.08, + "median_ms": 11376.08, + "p90_ms": 11376.08, + "p95_ms": 11376.08, + "max_ms": 11376.08, + "mean_ms": 11376.08 + }, + "concurrent": { + "n": 4, + "min_ms": 1559.79, + "median_ms": 1595.31, + "p90_ms": 1740.92, + "p95_ms": 1768.05, + "max_ms": 1795.17, + "mean_ms": 1636.4, + "warmup": { + "n": 2, + "min_ms": 1641.35, + "median_ms": 1652.91, + "p90_ms": 1662.17, + "p95_ms": 1663.32, + "max_ms": 1664.48, + "mean_ms": 1652.91 + } + }, + "speedup_ratio": 7.13, + "notes_per_sec_concurrent": 123.5 + }, + "gates": [ + { + "rule": { + "field": "speedup_ratio", + "op": ">=", + "min": 1.8, + "why": "concurrent read_many must beat serial by the 1.1.0 concurrency margin; <1.8 means concurrency regressed" + }, + "value": 7.13, + "pass": true + } + ] + }, + "resolve": { + "result": { + "cases": [ + { + "mention": "echo", + "expect": "exact", + "expect_slug": "echo", + "resolved_slug": "echo", + "candidate_slugs": [ + "2026-06-19-goldbrain-full-echo-architect", + "echo", + "echo-memory-codex-plugin", + "echo-plugin-build", + "echo-skill-improvements" + ], + "verdict": "PASS", + "resolve_timing": { + "n": 6, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0, + "warmup": { + "n": 2, + "min_ms": 0.01, + "median_ms": 0.1, + "p90_ms": 0.17, + "p95_ms": 0.18, + "max_ms": 0.19, + "mean_ms": 0.1 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 1.35, + "median_ms": 1.49, + "p90_ms": 1.67, + "p95_ms": 1.68, + "max_ms": 1.68, + "mean_ms": 1.51, + "warmup": { + "n": 2, + "min_ms": 1.82, + "median_ms": 1.83, + "p90_ms": 1.84, + "p95_ms": 1.84, + "max_ms": 1.85, + "mean_ms": 1.83 + } + } + }, + { + "mention": "echo memory", + "expect": "candidates", + "expect_slug": "echo", + "resolved_slug": "echo", + "candidate_slugs": [ + "echo", + "echo-memory-codex-plugin", + "echo-plugin-build", + "echo-skill-improvements", + "2026-06-19-goldbrain-full-echo-architect" + ], + "verdict": "INFO", + "resolve_timing": { + "n": 6, + "min_ms": 0.55, + "median_ms": 0.56, + "p90_ms": 0.57, + "p95_ms": 0.58, + "max_ms": 0.58, + "mean_ms": 0.56, + "warmup": { + "n": 2, + "min_ms": 0.61, + "median_ms": 0.61, + "p90_ms": 0.61, + "p95_ms": 0.61, + "max_ms": 0.61, + "mean_ms": 0.61 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.97, + "median_ms": 0.99, + "p90_ms": 1.06, + "p95_ms": 1.09, + "max_ms": 1.11, + "mean_ms": 1.01, + "warmup": { + "n": 2, + "min_ms": 1.15, + "median_ms": 1.18, + "p90_ms": 1.2, + "p95_ms": 1.21, + "max_ms": 1.21, + "mean_ms": 1.18 + } + } + }, + { + "mention": "ECHO plugin", + "expect": "candidates", + "expect_slug": "echo", + "resolved_slug": "echo", + "candidate_slugs": [ + "echo", + "echo-memory-codex-plugin", + "echo-plugin-build", + "2026-06-19-goldbrain-full-echo-architect", + "alabama-wisp-brand-docs" + ], + "verdict": "INFO", + "resolve_timing": { + "n": 6, + "min_ms": 0.41, + "median_ms": 0.41, + "p90_ms": 0.42, + "p95_ms": 0.42, + "max_ms": 0.42, + "mean_ms": 0.41, + "warmup": { + "n": 2, + "min_ms": 0.42, + "median_ms": 0.45, + "p90_ms": 0.47, + "p95_ms": 0.48, + "max_ms": 0.48, + "mean_ms": 0.45 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.79, + "median_ms": 0.8, + "p90_ms": 0.88, + "p95_ms": 0.9, + "max_ms": 0.92, + "mean_ms": 0.83, + "warmup": { + "n": 2, + "min_ms": 0.89, + "median_ms": 0.89, + "p90_ms": 0.9, + "p95_ms": 0.9, + "max_ms": 0.9, + "mean_ms": 0.89 + } + } + }, + { + "mention": "goldbrain", + "expect": "exact", + "expect_slug": "goldbrain", + "resolved_slug": "goldbrain", + "candidate_slugs": [ + "2026-06-19-goldbrain-full-echo-architect", + "goldbrain" + ], + "verdict": "PASS", + "resolve_timing": { + "n": 6, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0, + "warmup": { + "n": 2, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.73, + "median_ms": 0.75, + "p90_ms": 0.85, + "p95_ms": 0.88, + "max_ms": 0.91, + "mean_ms": 0.78, + "warmup": { + "n": 2, + "min_ms": 0.8, + "median_ms": 0.81, + "p90_ms": 0.82, + "p95_ms": 0.82, + "max_ms": 0.82, + "mean_ms": 0.81 + } + } + }, + { + "mention": "jason stedwell", + "expect": "exact", + "expect_slug": "jason-stedwell", + "resolved_slug": "jason-stedwell", + "candidate_slugs": [ + "jason-stedwell", + "jason-mcp-gateway" + ], + "verdict": "PASS", + "resolve_timing": { + "n": 6, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0, + "warmup": { + "n": 2, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.69, + "median_ms": 0.72, + "p90_ms": 0.75, + "p95_ms": 0.75, + "max_ms": 0.75, + "mean_ms": 0.72, + "warmup": { + "n": 2, + "min_ms": 0.74, + "median_ms": 0.74, + "p90_ms": 0.75, + "p95_ms": 0.75, + "max_ms": 0.75, + "mean_ms": 0.74 + } + } + }, + { + "mention": "jason", + "expect": "candidates", + "expect_slug": "jason-stedwell", + "resolved_slug": "jason-stedwell", + "candidate_slugs": [ + "jason-mcp-gateway", + "jason-stedwell" + ], + "verdict": "INFO", + "resolve_timing": { + "n": 6, + "min_ms": 0.61, + "median_ms": 0.65, + "p90_ms": 0.66, + "p95_ms": 0.67, + "max_ms": 0.67, + "mean_ms": 0.64, + "warmup": { + "n": 2, + "min_ms": 0.66, + "median_ms": 0.68, + "p90_ms": 0.69, + "p95_ms": 0.69, + "max_ms": 0.69, + "mean_ms": 0.68 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.63, + "median_ms": 0.63, + "p90_ms": 0.65, + "p95_ms": 0.65, + "max_ms": 0.66, + "mean_ms": 0.64, + "warmup": { + "n": 2, + "min_ms": 0.68, + "median_ms": 0.69, + "p90_ms": 0.7, + "p95_ms": 0.7, + "max_ms": 0.7, + "mean_ms": 0.69 + } + } + }, + { + "mention": "zzqx nonexistent entity", + "expect": "miss", + "expect_slug": null, + "resolved_slug": null, + "candidate_slugs": [], + "verdict": "PASS", + "resolve_timing": { + "n": 6, + "min_ms": 0.62, + "median_ms": 0.63, + "p90_ms": 0.67, + "p95_ms": 0.67, + "max_ms": 0.68, + "mean_ms": 0.64, + "warmup": { + "n": 2, + "min_ms": 0.66, + "median_ms": 0.67, + "p90_ms": 0.67, + "p95_ms": 0.67, + "max_ms": 0.67, + "mean_ms": 0.67 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.61, + "median_ms": 0.62, + "p90_ms": 0.65, + "p95_ms": 0.65, + "max_ms": 0.66, + "mean_ms": 0.62, + "warmup": { + "n": 2, + "min_ms": 0.62, + "median_ms": 0.63, + "p90_ms": 0.64, + "p95_ms": 0.64, + "max_ms": 0.64, + "mean_ms": 0.63 + } + } + } + ], + "passes": 4, + "needs_tuning": 3, + "note": "verdicts are PASS/INFO only; promote to FAIL once the table is confirmed against the live vault" + }, + "gates": [] + }, + "soak": { + "result": { + "seconds": 22, + "iterations": 15, + "errors": 0, + "per_pass": { + "n": 15, + "min_ms": 1472.88, + "median_ms": 1551.67, + "p90_ms": 1648.45, + "p95_ms": 1702.66, + "max_ms": 1820.58, + "mean_ms": 1569.81 + }, + "early_mean_ms": 1575.63, + "late_mean_ms": 1564.72, + "drift_ratio_late_over_early": 0.99 + }, + "gates": [ + { + "rule": { + "field": "errors", + "op": "<=", + "min": 0, + "why": "no read errors across the soak window \u2014 connection leaks/pool exhaustion would surface here" + }, + "value": 0, + "pass": true + }, + { + "rule": { + "field": "drift_ratio_late_over_early", + "op": "<=", + "min": 1.5, + "why": "late passes must not be much slower than early ones; >1.5 suggests a leak or degrading pool" + }, + "value": 0.99, + "pass": true + } + ] + }, + "subject-pull": { + "result": { + "subjects": [ + { + "label": "apta", + "query": "APTA", + "resolve_floor": { + "n": 1, + "min_ms": 0.75, + "median_ms": 0.75, + "p90_ms": 0.75, + "p95_ms": 0.75, + "max_ms": 0.75, + "mean_ms": 0.75 + }, + "search_only": { + "n": 1, + "min_ms": 61.87, + "median_ms": 61.87, + "p90_ms": 61.87, + "p95_ms": 61.87, + "max_ms": 61.87, + "mean_ms": 61.87 + }, + "recall_total": { + "n": 1, + "min_ms": 2231.31, + "median_ms": 2231.31, + "p90_ms": 2231.31, + "p95_ms": 2231.31, + "max_ms": 2231.31, + "mean_ms": 2231.31 + } + }, + { + "label": "capmetro", + "query": "CapMetro", + "resolve_floor": { + "n": 1, + "min_ms": 0.03, + "median_ms": 0.03, + "p90_ms": 0.03, + "p95_ms": 0.03, + "max_ms": 0.03, + "mean_ms": 0.03 + }, + "search_only": { + "n": 1, + "min_ms": 57.73, + "median_ms": 57.73, + "p90_ms": 57.73, + "p95_ms": 57.73, + "max_ms": 57.73, + "mean_ms": 57.73 + }, + "recall_total": { + "n": 1, + "min_ms": 2116.06, + "median_ms": 2116.06, + "p90_ms": 2116.06, + "p95_ms": 2116.06, + "max_ms": 2116.06, + "mean_ms": 2116.06 + } + }, + { + "label": "hub-echo", + "query": "echo", + "resolve_floor": { + "n": 1, + "min_ms": 0.03, + "median_ms": 0.03, + "p90_ms": 0.03, + "p95_ms": 0.03, + "max_ms": 0.03, + "mean_ms": 0.03 + }, + "search_only": { + "n": 1, + "min_ms": 70.56, + "median_ms": 70.56, + "p90_ms": 70.56, + "p95_ms": 70.56, + "max_ms": 70.56, + "mean_ms": 70.56 + }, + "recall_total": { + "n": 1, + "min_ms": 2403.62, + "median_ms": 2403.62, + "p90_ms": 2403.62, + "p95_ms": 2403.62, + "max_ms": 2403.62, + "mean_ms": 2403.62 + } + }, + { + "label": "hub-prefs", + "query": "operator preferences", + "resolve_floor": { + "n": 1, + "min_ms": 0.04, + "median_ms": 0.04, + "p90_ms": 0.04, + "p95_ms": 0.04, + "max_ms": 0.04, + "mean_ms": 0.04 + }, + "search_only": { + "n": 1, + "min_ms": 2011.13, + "median_ms": 2011.13, + "p90_ms": 2011.13, + "p95_ms": 2011.13, + "max_ms": 2011.13, + "mean_ms": 2011.13 + }, + "recall_total": { + "n": 1, + "min_ms": 2994.84, + "median_ms": 2994.84, + "p90_ms": 2994.84, + "p95_ms": 2994.84, + "max_ms": 2994.84, + "mean_ms": 2994.84 + } + }, + { + "label": "leaf-rivnut", + "query": "rivnut torque spec", + "resolve_floor": { + "n": 1, + "min_ms": 2.15, + "median_ms": 2.15, + "p90_ms": 2.15, + "p95_ms": 2.15, + "max_ms": 2.15, + "mean_ms": 2.15 + }, + "search_only": { + "n": 1, + "min_ms": 2013.58, + "median_ms": 2013.58, + "p90_ms": 2013.58, + "p95_ms": 2013.58, + "max_ms": 2013.58, + "mean_ms": 2013.58 + }, + "recall_total": { + "n": 1, + "min_ms": 2371.32, + "median_ms": 2371.32, + "p90_ms": 2371.32, + "p95_ms": 2371.32, + "max_ms": 2371.32, + "mean_ms": 2371.32 + } + } + ] + }, + "gates": [] + }, + "worker-sweep": { + "result": { + "note_count": 197, + "sweep": [ + { + "workers": 1, + "median_ms": 11917.49, + "p95_ms": 11917.49 + }, + { + "workers": 2, + "median_ms": 5740.0, + "p95_ms": 5740.0 + }, + { + "workers": 4, + "median_ms": 3034.3, + "p95_ms": 3034.3 + }, + { + "workers": 8, + "median_ms": 1583.83, + "p95_ms": 1583.83 + }, + { + "workers": 16, + "median_ms": 1277.77, + "p95_ms": 1277.77 + } + ], + "knee_workers": 16, + "default_workers": 8 + }, + "gates": [] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-append.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-append.json new file mode 100644 index 0000000..f0bfaf1 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-append.json @@ -0,0 +1,55 @@ +{ + "env": { + "date": "2026-06-23T20:22:00", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": null, + "count_k": 30 + }, + "run_id": "20260623-202200-882c82", + "metrics": { + "bulk-append": { + "result": { + "k": 30, + "append_write": { + "n": 30, + "min_ms": 87.11, + "median_ms": 104.08, + "p90_ms": 190.63, + "p95_ms": 198.89, + "max_ms": 241.91, + "mean_ms": 117.22 + }, + "append_skip": { + "n": 30, + "min_ms": 47.57, + "median_ms": 51.42, + "p90_ms": 59.82, + "p95_ms": 61.57, + "max_ms": 73.49, + "mean_ms": 53.22 + }, + "skips_confirmed": 30, + "all_skipped_second_pass": true + }, + "gates": [ + { + "rule": { + "field": "all_skipped_second_pass", + "op": ">=", + "min": 1, + "why": "second pass of identical lines must be 100% idempotent skips (boolean true coerced to 1)" + }, + "value": true, + "pass": true + } + ] + } + }, + "cleanup": { + "deleted": 1 + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-get.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-get.json new file mode 100644 index 0000000..39b999a --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-get.json @@ -0,0 +1,73 @@ +{ + "env": { + "date": "2026-06-23T20:21:36", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": 5, + "count_k": 30 + }, + "run_id": "20260623-202136-f2f4dc", + "metrics": { + "bulk-get": { + "result": { + "k": 30, + "workers": 8, + "serial": { + "n": 5, + "min_ms": 1520.47, + "median_ms": 1606.86, + "p90_ms": 1805.72, + "p95_ms": 1835.83, + "max_ms": 1865.94, + "mean_ms": 1658.6, + "warmup": { + "n": 1, + "min_ms": 1601.2, + "median_ms": 1601.2, + "p90_ms": 1601.2, + "p95_ms": 1601.2, + "max_ms": 1601.2, + "mean_ms": 1601.2 + } + }, + "concurrent": { + "n": 5, + "min_ms": 340.31, + "median_ms": 356.48, + "p90_ms": 358.82, + "p95_ms": 359.32, + "max_ms": 359.83, + "mean_ms": 351.42, + "warmup": { + "n": 2, + "min_ms": 353.43, + "median_ms": 434.94, + "p90_ms": 500.15, + "p95_ms": 508.3, + "max_ms": 516.45, + "mean_ms": 434.94 + } + }, + "speedup_ratio": 4.51 + }, + "gates": [ + { + "rule": { + "field": "speedup_ratio", + "op": ">=", + "min": 1.5, + "why": "controlled-K concurrent vs serial; lower bar than full-vault since K is small" + }, + "value": 4.51, + "pass": true + } + ] + } + }, + "cleanup": { + "deleted": 30 + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-put.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-put.json new file mode 100644 index 0000000..d4bd936 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-bulk-put.json @@ -0,0 +1,35 @@ +{ + "env": { + "date": "2026-06-23T20:21:21", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": null, + "count_k": 30 + }, + "run_id": "20260623-202121-23b5ac", + "metrics": { + "bulk-put": { + "result": { + "k": 30, + "per_put": { + "n": 30, + "min_ms": 100.09, + "median_ms": 114.98, + "p90_ms": 212.46, + "p95_ms": 236.18, + "max_ms": 330.77, + "mean_ms": 143.25 + }, + "total_ms": 4297.5, + "puts_per_sec": 7.0 + }, + "gates": [] + } + }, + "cleanup": { + "deleted": 30 + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-cache.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-cache.json new file mode 100644 index 0000000..36d0b03 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-cache.json @@ -0,0 +1,36 @@ +{ + "env": { + "date": "2026-06-23T20:12:16", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": null, + "count_k": 50 + }, + "run_id": "20260623-201216-e77971", + "metrics": { + "cache": { + "result": { + "requested": 60, + "unique": 20, + "returned": 20, + "deduped": true, + "elapsed_ms": 307.13 + }, + "gates": [ + { + "rule": { + "field": "deduped", + "op": ">=", + "min": 1, + "why": "read_many must collapse a 3x-duplicated path list to the unique set" + }, + "value": true, + "pass": true + } + ] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-expand-graph.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-expand-graph.json new file mode 100644 index 0000000..279e531 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-expand-graph.json @@ -0,0 +1,76 @@ +{ + "env": { + "date": "2026-06-23T21:22:29", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": 3, + "count_k": 50 + }, + "run_id": "20260623-212229-8c3ce2", + "metrics": { + "expand-graph": { + "result": { + "subjects": [ + { + "label": "hub-echo", + "query": "echo", + "neighbours": 120, + "serial_ms": 2227.67, + "concurrent_ms": 642.2, + "speedup": 3.47, + "ranking_identical": true, + "scores_identical": true + }, + { + "label": "hub-prefs", + "query": "operator preferences", + "neighbours": 126, + "serial_ms": 2910.46, + "concurrent_ms": 698.8, + "speedup": 4.16, + "ranking_identical": true, + "scores_identical": true + } + ], + "min_speedup": 3.47, + "all_ranking_identical": true, + "all_scores_identical": true + }, + "gates": [ + { + "rule": { + "field": "min_speedup", + "op": ">=", + "min": 2.0, + "why": "recall()'s graph layer must fetch each BFS hop concurrently (read_many), not serially per node; <2x means expand_graph regressed to the pre-fix serial walk" + }, + "value": 3.47, + "pass": true + }, + { + "rule": { + "field": "all_ranking_identical", + "op": ">=", + "min": 1, + "why": "the concurrent expansion must return the same ranked neighbours as the serial reference" + }, + "value": true, + "pass": true + }, + { + "rule": { + "field": "all_scores_identical", + "op": ">=", + "min": 1, + "why": "decayed scores must match the serial reference to within float tolerance" + }, + "value": true, + "pass": true + } + ] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-index.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-index.json new file mode 100644 index 0000000..4fb8f95 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-index.json @@ -0,0 +1,25 @@ +{ + "env": { + "date": "2026-06-23T20:20:57", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": null, + "count_k": 50 + }, + "run_id": "20260623-202057-c41a52", + "metrics": { + "index": { + "result": { + "script": "sweep.py", + "elapsed_ms": 4629.0, + "exit_code": 0, + "ok": true, + "stderr_tail": [] + }, + "gates": [] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-lint.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-lint.json new file mode 100644 index 0000000..a22eecb --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-lint.json @@ -0,0 +1,25 @@ +{ + "env": { + "date": "2026-06-23T20:21:01", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": null, + "count_k": 50 + }, + "run_id": "20260623-202101-569382", + "metrics": { + "lint": { + "result": { + "script": "vault_lint.py", + "elapsed_ms": 5126.4, + "exit_code": 1, + "ok": true, + "stderr_tail": [] + }, + "gates": [] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-lock.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-lock.json new file mode 100644 index 0000000..68d6008 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-lock.json @@ -0,0 +1,40 @@ +{ + "env": { + "date": "2026-06-23T20:21:28", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": null, + "count_k": 50 + }, + "run_id": "20260623-202128-53b113", + "metrics": { + "lock": { + "result": { + "acquire_free_ms": 159.18, + "acquire_free_rc": 0, + "contended_ms": 57.89, + "contended_rc_expected_75": 75, + "release_ms": 132.92, + "ok": true + }, + "gates": [ + { + "rule": { + "field": "ok", + "op": ">=", + "min": 1, + "why": "free acquire returns 0 and a contended acquire fast-fails with exit 75" + }, + "value": true, + "pass": true + } + ] + } + }, + "cleanup": { + "deleted": 0 + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-pool-warmup.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-pool-warmup.json new file mode 100644 index 0000000..4ecdf96 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-pool-warmup.json @@ -0,0 +1,52 @@ +{ + "env": { + "date": "2026-06-23T20:12:12", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": 12, + "count_k": 50 + }, + "run_id": "20260623-201212-30c2b8", + "metrics": { + "pool-warmup": { + "result": { + "probe": "_agent/echo-vault.md", + "cold_ms": 163.25, + "warm": { + "n": 12, + "min_ms": 45.64, + "median_ms": 50.29, + "p90_ms": 52.51, + "p95_ms": 53.79, + "max_ms": 55.28, + "mean_ms": 50.26, + "warmup": { + "n": 1, + "min_ms": 51.68, + "median_ms": 51.68, + "p90_ms": 51.68, + "p95_ms": 51.68, + "max_ms": 51.68, + "mean_ms": 51.68 + } + }, + "cold_over_warm_ratio": 3.25 + }, + "gates": [ + { + "rule": { + "field": "cold_over_warm_ratio", + "op": ">=", + "min": 1.5, + "why": "cold request must be meaningfully slower than warm \u2014 proves keep-alive is reusing the connection; a ratio near 1.0 means every request is re-handshaking (the pre-1.1.0 bug)" + }, + "value": 3.25, + "pass": true + } + ] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-queue.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-queue.json new file mode 100644 index 0000000..e76007c --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-queue.json @@ -0,0 +1,32 @@ +{ + "env": { + "date": "2026-06-23T20:22:05", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": null, + "count_k": 10 + }, + "run_id": "20260623-202205-378725", + "metrics": { + "queue": { + "result": { + "k": 10, + "enqueue": { + "n": 10, + "min_ms": 0.07, + "median_ms": 0.08, + "p90_ms": 0.18, + "p95_ms": 0.2, + "max_ms": 0.22, + "mean_ms": 0.1 + }, + "pending_after_enqueue": 10, + "note": "flush replay intentionally not run against the live vault; enqueue path timed only. See README queue caveat." + }, + "gates": [] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-read-full.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-read-full.json new file mode 100644 index 0000000..2d6b28b --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-read-full.json @@ -0,0 +1,62 @@ +{ + "env": { + "date": "2026-06-23T20:17:09", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": 4, + "count_k": 50 + }, + "run_id": "20260623-201709-b08a2e", + "metrics": { + "read-full": { + "result": { + "note_count": 197, + "workers": 8, + "serial": { + "n": 1, + "min_ms": 11376.08, + "median_ms": 11376.08, + "p90_ms": 11376.08, + "p95_ms": 11376.08, + "max_ms": 11376.08, + "mean_ms": 11376.08 + }, + "concurrent": { + "n": 4, + "min_ms": 1559.79, + "median_ms": 1595.31, + "p90_ms": 1740.92, + "p95_ms": 1768.05, + "max_ms": 1795.17, + "mean_ms": 1636.4, + "warmup": { + "n": 2, + "min_ms": 1641.35, + "median_ms": 1652.91, + "p90_ms": 1662.17, + "p95_ms": 1663.32, + "max_ms": 1664.48, + "mean_ms": 1652.91 + } + }, + "speedup_ratio": 7.13, + "notes_per_sec_concurrent": 123.5 + }, + "gates": [ + { + "rule": { + "field": "speedup_ratio", + "op": ">=", + "min": 1.8, + "why": "concurrent read_many must beat serial by the 1.1.0 concurrency margin; <1.8 means concurrency regressed" + }, + "value": 7.13, + "pass": true + } + ] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-resolve.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-resolve.json new file mode 100644 index 0000000..ebbf720 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-resolve.json @@ -0,0 +1,360 @@ +{ + "env": { + "date": "2026-06-23T20:12:19", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": 6, + "count_k": 50 + }, + "run_id": "20260623-201219-819b5e", + "metrics": { + "resolve": { + "result": { + "cases": [ + { + "mention": "echo", + "expect": "exact", + "expect_slug": "echo", + "resolved_slug": "echo", + "candidate_slugs": [ + "2026-06-19-goldbrain-full-echo-architect", + "echo", + "echo-memory-codex-plugin", + "echo-plugin-build", + "echo-skill-improvements" + ], + "verdict": "PASS", + "resolve_timing": { + "n": 6, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0, + "warmup": { + "n": 2, + "min_ms": 0.01, + "median_ms": 0.1, + "p90_ms": 0.17, + "p95_ms": 0.18, + "max_ms": 0.19, + "mean_ms": 0.1 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 1.35, + "median_ms": 1.49, + "p90_ms": 1.67, + "p95_ms": 1.68, + "max_ms": 1.68, + "mean_ms": 1.51, + "warmup": { + "n": 2, + "min_ms": 1.82, + "median_ms": 1.83, + "p90_ms": 1.84, + "p95_ms": 1.84, + "max_ms": 1.85, + "mean_ms": 1.83 + } + } + }, + { + "mention": "echo memory", + "expect": "candidates", + "expect_slug": "echo", + "resolved_slug": "echo", + "candidate_slugs": [ + "echo", + "echo-memory-codex-plugin", + "echo-plugin-build", + "echo-skill-improvements", + "2026-06-19-goldbrain-full-echo-architect" + ], + "verdict": "INFO", + "resolve_timing": { + "n": 6, + "min_ms": 0.55, + "median_ms": 0.56, + "p90_ms": 0.57, + "p95_ms": 0.58, + "max_ms": 0.58, + "mean_ms": 0.56, + "warmup": { + "n": 2, + "min_ms": 0.61, + "median_ms": 0.61, + "p90_ms": 0.61, + "p95_ms": 0.61, + "max_ms": 0.61, + "mean_ms": 0.61 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.97, + "median_ms": 0.99, + "p90_ms": 1.06, + "p95_ms": 1.09, + "max_ms": 1.11, + "mean_ms": 1.01, + "warmup": { + "n": 2, + "min_ms": 1.15, + "median_ms": 1.18, + "p90_ms": 1.2, + "p95_ms": 1.21, + "max_ms": 1.21, + "mean_ms": 1.18 + } + } + }, + { + "mention": "ECHO plugin", + "expect": "candidates", + "expect_slug": "echo", + "resolved_slug": "echo", + "candidate_slugs": [ + "echo", + "echo-memory-codex-plugin", + "echo-plugin-build", + "2026-06-19-goldbrain-full-echo-architect", + "alabama-wisp-brand-docs" + ], + "verdict": "INFO", + "resolve_timing": { + "n": 6, + "min_ms": 0.41, + "median_ms": 0.41, + "p90_ms": 0.42, + "p95_ms": 0.42, + "max_ms": 0.42, + "mean_ms": 0.41, + "warmup": { + "n": 2, + "min_ms": 0.42, + "median_ms": 0.45, + "p90_ms": 0.47, + "p95_ms": 0.48, + "max_ms": 0.48, + "mean_ms": 0.45 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.79, + "median_ms": 0.8, + "p90_ms": 0.88, + "p95_ms": 0.9, + "max_ms": 0.92, + "mean_ms": 0.83, + "warmup": { + "n": 2, + "min_ms": 0.89, + "median_ms": 0.89, + "p90_ms": 0.9, + "p95_ms": 0.9, + "max_ms": 0.9, + "mean_ms": 0.89 + } + } + }, + { + "mention": "goldbrain", + "expect": "exact", + "expect_slug": "goldbrain", + "resolved_slug": "goldbrain", + "candidate_slugs": [ + "2026-06-19-goldbrain-full-echo-architect", + "goldbrain" + ], + "verdict": "PASS", + "resolve_timing": { + "n": 6, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0, + "warmup": { + "n": 2, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.73, + "median_ms": 0.75, + "p90_ms": 0.85, + "p95_ms": 0.88, + "max_ms": 0.91, + "mean_ms": 0.78, + "warmup": { + "n": 2, + "min_ms": 0.8, + "median_ms": 0.81, + "p90_ms": 0.82, + "p95_ms": 0.82, + "max_ms": 0.82, + "mean_ms": 0.81 + } + } + }, + { + "mention": "jason stedwell", + "expect": "exact", + "expect_slug": "jason-stedwell", + "resolved_slug": "jason-stedwell", + "candidate_slugs": [ + "jason-stedwell", + "jason-mcp-gateway" + ], + "verdict": "PASS", + "resolve_timing": { + "n": 6, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0, + "warmup": { + "n": 2, + "min_ms": 0.0, + "median_ms": 0.0, + "p90_ms": 0.0, + "p95_ms": 0.0, + "max_ms": 0.0, + "mean_ms": 0.0 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.69, + "median_ms": 0.72, + "p90_ms": 0.75, + "p95_ms": 0.75, + "max_ms": 0.75, + "mean_ms": 0.72, + "warmup": { + "n": 2, + "min_ms": 0.74, + "median_ms": 0.74, + "p90_ms": 0.75, + "p95_ms": 0.75, + "max_ms": 0.75, + "mean_ms": 0.74 + } + } + }, + { + "mention": "jason", + "expect": "candidates", + "expect_slug": "jason-stedwell", + "resolved_slug": "jason-stedwell", + "candidate_slugs": [ + "jason-mcp-gateway", + "jason-stedwell" + ], + "verdict": "INFO", + "resolve_timing": { + "n": 6, + "min_ms": 0.61, + "median_ms": 0.65, + "p90_ms": 0.66, + "p95_ms": 0.67, + "max_ms": 0.67, + "mean_ms": 0.64, + "warmup": { + "n": 2, + "min_ms": 0.66, + "median_ms": 0.68, + "p90_ms": 0.69, + "p95_ms": 0.69, + "max_ms": 0.69, + "mean_ms": 0.68 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.63, + "median_ms": 0.63, + "p90_ms": 0.65, + "p95_ms": 0.65, + "max_ms": 0.66, + "mean_ms": 0.64, + "warmup": { + "n": 2, + "min_ms": 0.68, + "median_ms": 0.69, + "p90_ms": 0.7, + "p95_ms": 0.7, + "max_ms": 0.7, + "mean_ms": 0.69 + } + } + }, + { + "mention": "zzqx nonexistent entity", + "expect": "miss", + "expect_slug": null, + "resolved_slug": null, + "candidate_slugs": [], + "verdict": "PASS", + "resolve_timing": { + "n": 6, + "min_ms": 0.62, + "median_ms": 0.63, + "p90_ms": 0.67, + "p95_ms": 0.67, + "max_ms": 0.68, + "mean_ms": 0.64, + "warmup": { + "n": 2, + "min_ms": 0.66, + "median_ms": 0.67, + "p90_ms": 0.67, + "p95_ms": 0.67, + "max_ms": 0.67, + "mean_ms": 0.67 + } + }, + "fuzzy_timing": { + "n": 6, + "min_ms": 0.61, + "median_ms": 0.62, + "p90_ms": 0.65, + "p95_ms": 0.65, + "max_ms": 0.66, + "mean_ms": 0.62, + "warmup": { + "n": 2, + "min_ms": 0.62, + "median_ms": 0.63, + "p90_ms": 0.64, + "p95_ms": 0.64, + "max_ms": 0.64, + "mean_ms": 0.63 + } + } + } + ], + "passes": 4, + "needs_tuning": 3, + "note": "verdicts are PASS/INFO only; promote to FAIL once the table is confirmed against the live vault" + }, + "gates": [] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-soak.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-soak.json new file mode 100644 index 0000000..40b2227 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-soak.json @@ -0,0 +1,56 @@ +{ + "env": { + "date": "2026-06-23T20:22:15", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": null, + "count_k": 50 + }, + "run_id": "20260623-202215-7b414f", + "metrics": { + "soak": { + "result": { + "seconds": 22, + "iterations": 15, + "errors": 0, + "per_pass": { + "n": 15, + "min_ms": 1472.88, + "median_ms": 1551.67, + "p90_ms": 1648.45, + "p95_ms": 1702.66, + "max_ms": 1820.58, + "mean_ms": 1569.81 + }, + "early_mean_ms": 1575.63, + "late_mean_ms": 1564.72, + "drift_ratio_late_over_early": 0.99 + }, + "gates": [ + { + "rule": { + "field": "errors", + "op": "<=", + "min": 0, + "why": "no read errors across the soak window \u2014 connection leaks/pool exhaustion would surface here" + }, + "value": 0, + "pass": true + }, + { + "rule": { + "field": "drift_ratio_late_over_early", + "op": "<=", + "min": 1.5, + "why": "late passes must not be much slower than early ones; >1.5 suggests a leak or degrading pool" + }, + "value": 0.99, + "pass": true + } + ] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-subject-pull-after.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-subject-pull-after.json new file mode 100644 index 0000000..470b741 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-subject-pull-after.json @@ -0,0 +1,177 @@ +{ + "env": { + "date": "2026-06-23T21:18:12", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": 1, + "count_k": 50 + }, + "run_id": "20260623-211812-ea4b36", + "metrics": { + "subject-pull": { + "result": { + "subjects": [ + { + "label": "apta", + "query": "APTA", + "resolve_floor": { + "n": 1, + "min_ms": 0.75, + "median_ms": 0.75, + "p90_ms": 0.75, + "p95_ms": 0.75, + "max_ms": 0.75, + "mean_ms": 0.75 + }, + "search_only": { + "n": 1, + "min_ms": 61.87, + "median_ms": 61.87, + "p90_ms": 61.87, + "p95_ms": 61.87, + "max_ms": 61.87, + "mean_ms": 61.87 + }, + "recall_total": { + "n": 1, + "min_ms": 2231.31, + "median_ms": 2231.31, + "p90_ms": 2231.31, + "p95_ms": 2231.31, + "max_ms": 2231.31, + "mean_ms": 2231.31 + } + }, + { + "label": "capmetro", + "query": "CapMetro", + "resolve_floor": { + "n": 1, + "min_ms": 0.03, + "median_ms": 0.03, + "p90_ms": 0.03, + "p95_ms": 0.03, + "max_ms": 0.03, + "mean_ms": 0.03 + }, + "search_only": { + "n": 1, + "min_ms": 57.73, + "median_ms": 57.73, + "p90_ms": 57.73, + "p95_ms": 57.73, + "max_ms": 57.73, + "mean_ms": 57.73 + }, + "recall_total": { + "n": 1, + "min_ms": 2116.06, + "median_ms": 2116.06, + "p90_ms": 2116.06, + "p95_ms": 2116.06, + "max_ms": 2116.06, + "mean_ms": 2116.06 + } + }, + { + "label": "hub-echo", + "query": "echo", + "resolve_floor": { + "n": 1, + "min_ms": 0.03, + "median_ms": 0.03, + "p90_ms": 0.03, + "p95_ms": 0.03, + "max_ms": 0.03, + "mean_ms": 0.03 + }, + "search_only": { + "n": 1, + "min_ms": 70.56, + "median_ms": 70.56, + "p90_ms": 70.56, + "p95_ms": 70.56, + "max_ms": 70.56, + "mean_ms": 70.56 + }, + "recall_total": { + "n": 1, + "min_ms": 2403.62, + "median_ms": 2403.62, + "p90_ms": 2403.62, + "p95_ms": 2403.62, + "max_ms": 2403.62, + "mean_ms": 2403.62 + } + }, + { + "label": "hub-prefs", + "query": "operator preferences", + "resolve_floor": { + "n": 1, + "min_ms": 0.04, + "median_ms": 0.04, + "p90_ms": 0.04, + "p95_ms": 0.04, + "max_ms": 0.04, + "mean_ms": 0.04 + }, + "search_only": { + "n": 1, + "min_ms": 2011.13, + "median_ms": 2011.13, + "p90_ms": 2011.13, + "p95_ms": 2011.13, + "max_ms": 2011.13, + "mean_ms": 2011.13 + }, + "recall_total": { + "n": 1, + "min_ms": 2994.84, + "median_ms": 2994.84, + "p90_ms": 2994.84, + "p95_ms": 2994.84, + "max_ms": 2994.84, + "mean_ms": 2994.84 + } + }, + { + "label": "leaf-rivnut", + "query": "rivnut torque spec", + "resolve_floor": { + "n": 1, + "min_ms": 2.15, + "median_ms": 2.15, + "p90_ms": 2.15, + "p95_ms": 2.15, + "max_ms": 2.15, + "mean_ms": 2.15 + }, + "search_only": { + "n": 1, + "min_ms": 2013.58, + "median_ms": 2013.58, + "p90_ms": 2013.58, + "p95_ms": 2013.58, + "max_ms": 2013.58, + "mean_ms": 2013.58 + }, + "recall_total": { + "n": 1, + "min_ms": 2371.32, + "median_ms": 2371.32, + "p90_ms": 2371.32, + "p95_ms": 2371.32, + "max_ms": 2371.32, + "mean_ms": 2371.32 + } + } + ] + }, + "gates": [] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-subject-pull.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-subject-pull.json new file mode 100644 index 0000000..f78e55e --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-subject-pull.json @@ -0,0 +1,177 @@ +{ + "env": { + "date": "2026-06-23T20:20:23", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": 1, + "count_k": 50 + }, + "run_id": "20260623-202023-274ddd", + "metrics": { + "subject-pull": { + "result": { + "subjects": [ + { + "label": "apta", + "query": "APTA", + "resolve_floor": { + "n": 1, + "min_ms": 0.97, + "median_ms": 0.97, + "p90_ms": 0.97, + "p95_ms": 0.97, + "max_ms": 0.97, + "mean_ms": 0.97 + }, + "search_only": { + "n": 1, + "min_ms": 71.55, + "median_ms": 71.55, + "p90_ms": 71.55, + "p95_ms": 71.55, + "max_ms": 71.55, + "mean_ms": 71.55 + }, + "recall_total": { + "n": 1, + "min_ms": 3836.47, + "median_ms": 3836.47, + "p90_ms": 3836.47, + "p95_ms": 3836.47, + "max_ms": 3836.47, + "mean_ms": 3836.47 + } + }, + { + "label": "capmetro", + "query": "CapMetro", + "resolve_floor": { + "n": 1, + "min_ms": 0.05, + "median_ms": 0.05, + "p90_ms": 0.05, + "p95_ms": 0.05, + "max_ms": 0.05, + "mean_ms": 0.05 + }, + "search_only": { + "n": 1, + "min_ms": 57.66, + "median_ms": 57.66, + "p90_ms": 57.66, + "p95_ms": 57.66, + "max_ms": 57.66, + "mean_ms": 57.66 + }, + "recall_total": { + "n": 1, + "min_ms": 2800.54, + "median_ms": 2800.54, + "p90_ms": 2800.54, + "p95_ms": 2800.54, + "max_ms": 2800.54, + "mean_ms": 2800.54 + } + }, + { + "label": "hub-echo", + "query": "echo", + "resolve_floor": { + "n": 1, + "min_ms": 0.02, + "median_ms": 0.02, + "p90_ms": 0.02, + "p95_ms": 0.02, + "max_ms": 0.02, + "mean_ms": 0.02 + }, + "search_only": { + "n": 1, + "min_ms": 67.39, + "median_ms": 67.39, + "p90_ms": 67.39, + "p95_ms": 67.39, + "max_ms": 67.39, + "mean_ms": 67.39 + }, + "recall_total": { + "n": 1, + "min_ms": 4114.19, + "median_ms": 4114.19, + "p90_ms": 4114.19, + "p95_ms": 4114.19, + "max_ms": 4114.19, + "mean_ms": 4114.19 + } + }, + { + "label": "hub-prefs", + "query": "operator preferences", + "resolve_floor": { + "n": 1, + "min_ms": 0.04, + "median_ms": 0.04, + "p90_ms": 0.04, + "p95_ms": 0.04, + "max_ms": 0.04, + "mean_ms": 0.04 + }, + "search_only": { + "n": 1, + "min_ms": 2013.4, + "median_ms": 2013.4, + "p90_ms": 2013.4, + "p95_ms": 2013.4, + "max_ms": 2013.4, + "mean_ms": 2013.4 + }, + "recall_total": { + "n": 1, + "min_ms": 4748.56, + "median_ms": 4748.56, + "p90_ms": 4748.56, + "p95_ms": 4748.56, + "max_ms": 4748.56, + "mean_ms": 4748.56 + } + }, + { + "label": "leaf-rivnut", + "query": "rivnut torque spec", + "resolve_floor": { + "n": 1, + "min_ms": 2.14, + "median_ms": 2.14, + "p90_ms": 2.14, + "p95_ms": 2.14, + "max_ms": 2.14, + "mean_ms": 2.14 + }, + "search_only": { + "n": 1, + "min_ms": 2014.69, + "median_ms": 2014.69, + "p90_ms": 2014.69, + "p95_ms": 2014.69, + "max_ms": 2014.69, + "mean_ms": 2014.69 + }, + "recall_total": { + "n": 1, + "min_ms": 4363.28, + "median_ms": 4363.28, + "p90_ms": 4363.28, + "p95_ms": 4363.28, + "max_ms": 4363.28, + "mean_ms": 4363.28 + } + } + ] + }, + "gates": [] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-worker-sweep.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-worker-sweep.json new file mode 100644 index 0000000..c1e60bc --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/m-worker-sweep.json @@ -0,0 +1,50 @@ +{ + "env": { + "date": "2026-06-23T20:18:05", + "endpoint": "https://echoapi.alwisp.com", + "commit": "af16598", + "echo_workers": 8, + "echo_timeout": 30, + "python": "3.10.12", + "iterations": 2, + "count_k": 50 + }, + "run_id": "20260623-201805-53dcdb", + "metrics": { + "worker-sweep": { + "result": { + "note_count": 197, + "sweep": [ + { + "workers": 1, + "median_ms": 11917.49, + "p95_ms": 11917.49 + }, + { + "workers": 2, + "median_ms": 5740.0, + "p95_ms": 5740.0 + }, + { + "workers": 4, + "median_ms": 3034.3, + "p95_ms": 3034.3 + }, + { + "workers": 8, + "median_ms": 1583.83, + "p95_ms": 1583.83 + }, + { + "workers": 16, + "median_ms": 1277.77, + "p95_ms": 1277.77 + } + ], + "knee_workers": 16, + "default_workers": 8 + }, + "gates": [] + } + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/report-data.json b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/report-data.json new file mode 100644 index 0000000..acee734 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/report-data.json @@ -0,0 +1,545 @@ +{ + "title": "ECHO Memory Plugin \u2014 Performance Benchmark", + "subtitle": "Validation of the 1.1.0 concurrency and 1.2.0 entity-resolution releases", + "reference": "echo-v.05 @ af16598", + "status": "11/11 gate checks pass", + "summary": "Full-vault reads run 7.1x faster concurrent than serial (11.4 s down to 1.6 s, 197 notes). Entity resolution is sub-millisecond. Profiling traced recall() latency to serial graph expansion (not BM25); the fix makes expansion concurrent (3.5-4.2x, identical results). All 11 gate checks pass. Run live against echoapi.alwisp.com on 2026-06-23.", + "sections": [ + { + "heading": "Test Environment", + "type": "kv", + "rows": [ + { + "label": "Endpoint", + "value": "echoapi.alwisp.com (live)" + }, + { + "label": "Vault size", + "value": "197 notes" + }, + { + "label": "Plugin commit", + "value": "af16598" + }, + { + "label": "Releases under test", + "value": "1.1.0 pooling + concurrency, 1.2.0 resolver" + }, + { + "label": "Concurrency (default)", + "value": "ECHO_WORKERS = 8" + }, + { + "label": "Socket timeout", + "value": "ECHO_TIMEOUT = 30 s" + }, + { + "label": "Runner", + "value": "Python 3.10.12" + }, + { + "label": "Date", + "value": "June 23, 2026" + } + ] + }, + { + "heading": "Headline Results", + "type": "table", + "columns": [ + "Operation", + "Result", + "Verdict" + ], + "rows": [ + [ + "Full-vault read (concurrent vs serial)", + { + "value": "7.13x", + "num": true + }, + "Pass" + ], + [ + "Full-vault read throughput", + { + "value": "123.5 notes/s", + "num": true + }, + "Pass" + ], + [ + "Cold vs warm request (keep-alive)", + { + "value": "3.25x", + "num": true + }, + "Pass" + ], + [ + "Entity resolve (in-memory index)", + { + "value": "<1 ms", + "num": true + }, + "Pass" + ], + [ + "Idempotent append skip", + { + "value": "100%", + "num": true + }, + "Pass" + ], + [ + "Soak read errors (22 s)", + { + "value": "0", + "num": true + }, + "Pass" + ] + ] + }, + { + "heading": "Full-Vault Read \u2014 Concurrency Scaling", + "type": "table", + "columns": [ + "Workers", + "Median (197 notes)", + "Speedup vs serial" + ], + "rows": [ + [ + "1 (serial)", + { + "value": "11,917 ms", + "num": true + }, + { + "value": "1.0x", + "num": true + } + ], + [ + "2", + { + "value": "5,740 ms", + "num": true + }, + { + "value": "2.1x", + "num": true + } + ], + [ + "4", + { + "value": "3,034 ms", + "num": true + }, + { + "value": "3.9x", + "num": true + } + ], + [ + "8 (default)", + { + "value": "1,584 ms", + "num": true + }, + { + "value": "7.5x", + "num": true + } + ], + [ + "16", + { + "value": "1,278 ms", + "num": true + }, + { + "value": "9.3x", + "num": true + } + ] + ] + }, + { + "heading": "Subject Pulls \u2014 recall() (after concurrency fix)", + "type": "table", + "columns": [ + "Subject", + "resolve()", + "search", + "recall() total" + ], + "rows": [ + [ + "APTA", + { + "value": "0.75 ms", + "num": true + }, + { + "value": "62 ms", + "num": true + }, + { + "value": "2,231 ms", + "num": true + } + ], + [ + "CapMetro", + { + "value": "0.03 ms", + "num": true + }, + { + "value": "58 ms", + "num": true + }, + { + "value": "2,116 ms", + "num": true + } + ], + [ + "echo (hub)", + { + "value": "0.03 ms", + "num": true + }, + { + "value": "71 ms", + "num": true + }, + { + "value": "2,404 ms", + "num": true + } + ], + [ + "operator preferences (hub)", + { + "value": "0.04 ms", + "num": true + }, + { + "value": "2,011 ms", + "num": true + }, + { + "value": "2,995 ms", + "num": true + } + ], + [ + "rivnut torque spec (leaf)", + { + "value": "2.15 ms", + "num": true + }, + { + "value": "2,014 ms", + "num": true + }, + { + "value": "2,371 ms", + "num": true + } + ] + ] + }, + { + "heading": "recall() Graph Expansion \u2014 Concurrency Fix", + "type": "table", + "columns": [ + "Hub subject", + "Neighbours", + "Serial", + "Concurrent", + "Speedup", + "Results identical" + ], + "rows": [ + [ + "echo", + { + "value": "120", + "num": true + }, + { + "value": "2,228 ms", + "num": true + }, + { + "value": "642 ms", + "num": true + }, + { + "value": "3.47x", + "num": true + }, + "Yes" + ], + [ + "operator preferences", + { + "value": "126", + "num": true + }, + { + "value": "2,910 ms", + "num": true + }, + { + "value": "699 ms", + "num": true + }, + { + "value": "4.16x", + "num": true + }, + "Yes" + ] + ] + }, + { + "heading": "Bulk Write Operations", + "type": "table", + "columns": [ + "Operation", + "Per-op median", + "Aggregate" + ], + "rows": [ + [ + "PUT (read-back verified)", + { + "value": "115 ms", + "num": true + }, + { + "value": "7.0 PUT/s", + "num": true + } + ], + [ + "Bulk GET \u2014 concurrent vs serial (K=30)", + { + "value": "356 vs 1,607 ms", + "num": true + }, + { + "value": "4.51x", + "num": true + } + ], + [ + "APPEND \u2014 write", + { + "value": "104 ms", + "num": true + }, + { + "value": "1 round-trip + GET", + "num": false + } + ], + [ + "APPEND \u2014 idempotent skip", + { + "value": "51 ms", + "num": true + }, + { + "value": "100% skipped on re-run", + "num": false + } + ] + ] + }, + { + "heading": "Concurrency, Locking & Resilience", + "type": "table", + "columns": [ + "Check", + "Measured", + "Expected" + ], + "rows": [ + [ + "Lock \u2014 acquire when free", + { + "value": "159 ms (rc 0)", + "num": true + }, + "rc 0" + ], + [ + "Lock \u2014 contended acquire", + { + "value": "58 ms (rc 75)", + "num": true + }, + "rc 75 fast-fail" + ], + [ + "Lock \u2014 release", + { + "value": "133 ms", + "num": true + }, + "owned release" + ], + [ + "Offline queue \u2014 enqueue", + { + "value": "0.08 ms", + "num": true + }, + "local write-ahead" + ], + [ + "read_many dedup (60 -> 20)", + { + "value": "deduped", + "num": false + }, + "collapse to unique set" + ], + [ + "Soak drift (late vs early)", + { + "value": "0.99x", + "num": true + }, + "<= 1.5x, no leak" + ] + ] + }, + { + "heading": "Regression Gates (relative-ratio policy)", + "type": "table", + "columns": [ + "Gate", + "Threshold", + "Measured", + "Result" + ], + "rows": [ + [ + "read-full speedup", + ">= 1.8x", + { + "value": "7.13x", + "num": true + }, + "Pass" + ], + [ + "bulk-get speedup", + ">= 1.5x", + { + "value": "4.51x", + "num": true + }, + "Pass" + ], + [ + "pool warm-up cold/warm", + ">= 1.5x", + { + "value": "3.25x", + "num": true + }, + "Pass" + ], + [ + "append idempotency", + "100% skip", + { + "value": "100%", + "num": true + }, + "Pass" + ], + [ + "soak errors", + "0", + { + "value": "0", + "num": true + }, + "Pass" + ], + [ + "soak drift", + "<= 1.5x", + { + "value": "0.99x", + "num": true + }, + "Pass" + ], + [ + "lock semantics", + "rc 0 / rc 75", + { + "value": "0 / 75", + "num": true + }, + "Pass" + ], + [ + "read_many dedup", + "unique set", + { + "value": "20/20", + "num": true + }, + "Pass" + ] + ] + }, + { + "heading": "Findings", + "type": "olist", + "items": [ + "Concurrency delivers as designed. Eight workers cut full-vault reads 7.5x; the curve is near-linear to 8 workers and flattens after (8->16 returns only 1.24x). The default of 8 is the right setting for this vault and link.", + "Keep-alive is confirmed live. A cold request costs 163 ms against a 50 ms warm median (3.25x). This is the direct measure that the 1.1.0 pooling fix removed the per-request TLS handshake that was timing out full-vault passes.", + "Entity resolution is effectively free. resolve() returns in under 1 ms across exact, alias, and shortened mentions. The 1.2.0 alias work resolves shortened names (\"echo memory\", \"ECHO plugin\", \"jason\") straight to the canonical note, so the anti-duplicate guard holds without a fuzzy fallback.", + "recall() latency was traced to the graph layer, not BM25. The BM25 index is already persisted and incrementally maintained (load 500 ms, score 0.1 ms). The cost was expand_graph fetching each neighbour serially \u2014 3.0 s for 126 nodes. Fix: the graph BFS now fetches each hop concurrently via the existing read_many. expand_graph is 3.5-4.2x faster with byte-identical ranking and scores; end-to-end recall() dropped from 2.8-4.7 s to 2.1-3.0 s per subject.", + "Full-vault maintenance scripts stay under the tool timeout. sweep.py runs in 4.6 s and vault_lint.py in 5.1 s on 197 notes. The original failure mode (passes exceeding the timeout and dropping the session) does not recur.", + "Write integrity holds. PUT verifies via read-back at 115 ms; APPEND is whole-line idempotent, skipping at 51 ms with zero duplicate writes on re-run. The advisory lock fast-fails a contended acquire with the expected exit 75." + ] + }, + { + "heading": "Recommendations", + "type": "olist", + "items": [ + "Apply read_many to every code path that reads a set of known notes. Done for expand_graph (gated). Remaining: prefetch the _brief result bodies recall prints, and parallelize rebuild()'s vault walk.", + "Reuse one in-process read cache across a recall() call so load_index, expansion, and _brief don't re-fetch overlapping notes \u2014 this closes the remaining ~500 ms + serial _brief cost.", + "Make 'prefer read_many over a GET loop' a documented contract in the plugin's API reference; serial multi-note reads are a performance bug. The expand-graph gate enforces it for recall.", + "Hold ECHO_WORKERS at 8 (16 buys only 1.24x). Promote the resolve correctness table from INFO to FAIL now that live behavior is confirmed. Re-baseline at the next 1.x release or past ~400 notes." + ] + }, + { + "heading": "Method", + "type": "text", + "body": [ + "Each metric was timed with a perf-counter harness that reuses the live ECHO client, so it exercises the real pooled and concurrent code path. Reads report median and p95 over repeated iterations after a discarded warm-up; write metrics ran in a disposable namespace under the advisory lock and were deleted on completion (zero residual files confirmed).", + "Gates use relative ratios rather than absolute milliseconds, so they stay valid regardless of the network the suite runs from. The harness, fixtures, baselines, and per-metric JSON are checked in under eval/perf/." + ] + } + ], + "note": { + "heading": "Next Step", + "body": "Prefetch recall()'s _brief reads and parallelize rebuild() via read_many, then re-run subject-pull to confirm recall drops toward ~1 s. expand_graph fix is local-only until ported to the canonical repo. No blockers in 1.1.0 / 1.2.0." + } +} \ No newline at end of file diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/run.log b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/run.log new file mode 100644 index 0000000..9ceb0ac --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/results/run.log @@ -0,0 +1 @@ +[20:14:00] read-full diff --git a/echo-memory.plugin.src/skills/echo-memory/eval/perf/run_heavy.sh b/echo-memory.plugin.src/skills/echo-memory/eval/perf/run_heavy.sh new file mode 100755 index 0000000..28c4a42 --- /dev/null +++ b/echo-memory.plugin.src/skills/echo-memory/eval/perf/run_heavy.sh @@ -0,0 +1,12 @@ +#!/bin/bash +export ECHO_KEY_LEGACY_OK=1 +cd "$(dirname "$0")" +{ + echo "[$(date +%T)] read-full"; python3 bench.py read-full -n 3 --quiet --json-out results/m-read-full.json + echo "[$(date +%T)] worker-sweep"; python3 bench.py worker-sweep -n 4 --quiet --json-out results/m-worker-sweep.json + echo "[$(date +%T)] subject-pull"; python3 bench.py subject-pull -n 4 --quiet --json-out results/m-subject-pull.json + echo "[$(date +%T)] index"; python3 bench.py index --quiet --json-out results/m-index.json + echo "[$(date +%T)] lint"; python3 bench.py lint --quiet --json-out results/m-lint.json + echo "[$(date +%T)] DONE" +} > results/run.log 2>&1 +touch results/DONE diff --git a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_recall.py b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_recall.py index 12fcac7..93bbed8 100644 --- a/echo-memory.plugin.src/skills/echo-memory/scripts/echo_recall.py +++ b/echo-memory.plugin.src/skills/echo-memory/scripts/echo_recall.py @@ -247,27 +247,34 @@ def expand_graph(seeds, nmap: dict, base_scores: dict, max_hops: int = MAX_HOPS) score_of = dict(base_scores) seen = set(seeds) results: dict[str, tuple[float, str]] = {} - dq = deque((s, 0) for s in seeds) - while dq: - path, hop = dq.popleft() - if hop >= max_hops: - continue - text = links.get_text(path) - if text is None: - continue - body = strip_frontmatter(text) - targets = set(links.all_wikilinks(body)) | set(links.source_notes(text)) - parent = score_of.get(path, 1.0) - for t in targets: - tp = _resolve_target(t, nmap) - if not tp or tp in seeds: + # Breadth-first by HOP so each frontier's note bodies can be fetched concurrently + # (echo.read_many) instead of one blocking GET per node. Scoring is unchanged: a + # neighbour keeps the max decayed score over the paths that reach it, and non-seed + # parents score 1.0 exactly as the serial deque version did. + frontier = list(seeds) + for hop in range(max_hops): + if not frontier: + break + texts = echo.read_many(frontier) + next_frontier: list[str] = [] + for path in frontier: + text = texts.get(path) + if text is None: continue + body = strip_frontmatter(text) + targets = set(links.all_wikilinks(body)) | set(links.source_notes(text)) + parent = score_of.get(path, 1.0) decayed = parent * (GRAPH_DECAY ** (hop + 1)) - if decayed > results.get(tp, (0.0, ""))[0]: - results[tp] = (decayed, path) - if tp not in seen: - seen.add(tp) - dq.append((tp, hop + 1)) + for t in targets: + tp = _resolve_target(t, nmap) + if not tp or tp in seeds: + continue + if decayed > results.get(tp, (0.0, ""))[0]: + results[tp] = (decayed, path) + if tp not in seen: + seen.add(tp) + next_frontier.append(tp) + frontier = next_frontier return sorted(results.items(), key=lambda kv: -kv[1][0])