oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

Recorded measurements

A review of draft 2 observed that every performance figure in §7 was an assertion with no benchmark, command, machine or sample behind it, and that the FTS size could not be reconciled with the stated schema. Both were fair. This file is the answer: tools/bench produces it, and §7 cites it rather than restating numbers.

Machine and command

linux/amd64, 16 cpu, go1.26.5
go build -o bench ./tools/bench
./bench --corpus /home/paulca/amadan.net/oficina --chunks 90000

Run 2026-09-03. The corpus is every .md in the oficina tree, chunked by memoria's own chunker (internal/memoria/chunk), then cycled to reach the target count. Cycling repeats vocabulary, so the FTS figures are optimistic: genuinely varied English produces a larger postings list.

What a memoria chunk actually is

650 chunks from the real corpus, mean 202 words / 1325 bytes.

This corrects draft 2, which assumed 400 words and ~1.6 KB. The chunker targets 400 words but folds runts and never straddles a heading, and real documents have headings every few paragraphs — so the mean lands at about half the target. Every figure below follows from the measured size, not the intended one.

At 90,000 chunks that is 113.8 MB of text.

FTS5

| schema | index size | bytes/chunk | build | one BM25 query | |---|---|---|---|---| | fts5(body, tokenize='porter unicode61') | 200.0 MB | 2329 | 6.8 s | 1.09 ms | | fts5(body, content='', tokenize=…) | 49.3 MB | 574 | 6.2 s | 1.09 ms |

Draft 2 claimed 104 MB. That was wrong by about half. A default FTS5 table keeps its own copy of the inserted text, which the earlier measurement did not account for; the review caught this before the arithmetic downstream of it was trusted.

The contentless table is 4× smaller and just as fast to query, but it cannot return column values, so snippet() does not work on it and memoria would have to build its own highlight from the sealed chunk. That is a real trade and §7 records it as a decision rather than an accident.

Brute-force cosine, one goroutine, pure Go, no SIMD

Best of three runs. Draft 2 asserted 3.2 GFLOP/s; measured, it is 3.2 GFLOP/s, and every latency in draft 2's table was right to within a millisecond.

| chunks × dims | measured | GFLOP/s | vectors f32 | vectors int8 | |---|---|---|---|---| | 10,000 × 512 | 3.18 ms | 3.2 | 20 MB | 5 MB | | 90,000 × 512 | 28.9 ms | 3.2 | 176 MB | 44 MB | | 100,000 × 768 | 49.2 ms | 3.1 | 293 MB | 73 MB | | 250,000 × 512 | 80.0 ms | 3.2 | 488 MB | 122 MB |

The wake footprint, recomputed

At 90,000 chunks:

| design | FTS | vectors | separate text | total | |---|---|---|---|---| | default FTS (text lives in the index) | 200 MB | 44 MB | — | 244 MB | | contentless FTS + text for snippets | 49 MB | 44 MB | 114 MB | 207 MB |

So the ceiling under a 250 MB / 5-second budget is about 90,000–110,000 chunks, not the 150,000 draft 2 derived. Draft 1's ~100,000 was closer, and draft 2's revision moved the number the wrong way while claiming to have fixed the arithmetic.

Still not measured

  • Bucket read bandwidth and AES-GCM throughput on the platform. The 50–100 MB/s and ~1 GB/s in §7.3 remain assumptions, and they are the inputs the ceiling is most sensitive to.
  • The rest of a cold start: opening SQLite, allocating the vector matrix, parsing the snapshot, building id maps. The 5-second budget is spent on transfer and decryption alone, which flatters it.
  • A corpus of genuinely varied English rather than a cycled one.