Performance Budget
In this section
This document is a contract, and it has two halves: what a read costs, and what a write costs. Both targets below are CI-enforced regression gates, not aspirations. Code that fails a gate does not ship.
Why a budget matters
Brainy + brainy is the only database that answers a single query combining:
Metadata filter — structured
whereover indexed fields (LSM metadata index with Roaring bitmap evaluation)Graph traversal — multi-hop relationship walks with verb-type selectivity (graph adjacency LSM with verb endpoint lookups)
Vector similarity — semantic search over the entire corpus (Adaptive DiskANN: Mode 1 for in-memory recall, hybrid for PQ-compressed billion-scale)
No pure vector database (Pinecone, Weaviate, Qdrant), pure graph database (Neo4j, Memgraph), or document database (MongoDB, Elasticsearch) does this. PostgreSQL with pgvector gets two of three with a multi-second join cost. The combined query is the differentiator and it is only differentiating if it is sub- millisecond at billion scale.
The budget locks in that property as a release criterion.
The budget
All measurements are p50 / p99 over 100 queries on a single 32- core box (workstation tier: i9-14900HX / 64 GB / NVMe Gen 4; billion-tier: bxl9000 / Ryzen 9 7950X3D / 184 GB / NVMe). Queries are drawn from a representative workload mix described in Methodology below.
Read path budget
Query shape | 1 M entities | 10 M | 100 M |
|---|---|---|---|
Filter-only | ≤ 0.5 ms | ≤ 1 ms | ≤ 3 ms |
Filter-only p99 | ≤ 2 ms | ≤ 5 ms | ≤ 15 ms |
Vector-only | ≤ 1 ms | ≤ 2 ms | ≤ 5 ms |
Vector-only p99 | ≤ 3 ms | ≤ 6 ms | ≤ 15 ms |
Hybrid | ≤ 2 ms | ≤ 4 ms | ≤ 10 ms |
Hybrid p99 | ≤ 8 ms | ≤ 15 ms | ≤ 30 ms |
1-hop | ≤ 1.5 ms | ≤ 3 ms | ≤ 8 ms |
2-hop | ≤ 3 ms | ≤ 6 ms | ≤ 15 ms |
2-hop p99 | ≤ 12 ms | ≤ 25 ms | ≤ 50 ms |
Hybrid + 2-hop p50 | ≤ 5 ms | ≤ 10 ms | ≤ 25 ms |
Cross-subsystem ID resolution budget
find() returns UUID strings to JS, so every result row pays the int→UUID resolution. Batched APIs are mandatory; per-call APIs are not part of the hot path.
Operation | Budget |
|---|---|
| ≤ 100 μs |
| ≤ 80 μs |
| ≤ 500 μs |
| ≤ 150 μs |
| ≤ 120 μs |
Write path budget
Operation | 1 M | 10 M | 100 M |
|---|---|---|---|
Single | ≤ 0.3 ms | ≤ 0.3 ms | ≤ 0.5 ms |
Single | ≤ 0.2 ms | ≤ 0.2 ms | ≤ 0.3 ms |
Batched | ≤ 20 ms | ≤ 20 ms | ≤ 30 ms |
Sustained insert throughput (32-core box) | ≥ 50 K/s | ≥ 30 K/s | ≥ 20 K/s |
Recall floor
Recall is a correctness property, but it has a budget too: a query that returns the wrong answer in 0.5 ms is worse than one that takes 1 ms and returns the right one.
Workload | Mode | Recall@10 floor |
|---|---|---|
Production embeddings (dim ≥ 384, clustered) | Mode 1 in-memory | ≥ 0.97 |
Production embeddings | Mode 2 hybrid (PQ16) | ≥ 0.95 |
SIFT canonical (dim 128, BIGANN ground truth) | Mode 1 in-memory | ≥ 0.98 |
SIFT canonical | Mode 2 hybrid | ≥ 0.55 (PQ16 is adversarial here — operators tune via |
How the budget is enforced
Every gate in the table corresponds to a regression test in src/native/findPerfGates.test.ts (to be added; tracked under the Brainy 3.0 release plan). The test pattern follows the existing lsmPerfGates.test.ts:
Build a corpus at the target scale with a representative field distribution (50 fields, mixed cardinality, ~10 verbs per noun, dim 384 vectors with clustered structure).
Warm the page cache with one pass over the query set.
Run 100 queries per shape, record p50 and p99.
Assert p50 ≤ budget × 1.0 and p99 ≤ budget × 1.2 (20 % headroom on p99 absorbs CI variance).
A red gate is a release blocker. The test prints the measured distribution so the regression is bisectable to a commit.
The write-side budget: maintenance cost
Read latency is only half a contract. The other half is what a write costs, and it carries a law of its own:
A write cycle costs O(the work that cycle does) — never O(the store's size, its value space, or its flush history).
This is not a nice-to-have. An index maintenance term that scales with the store is invisible on a small brain and fatal on a real one, and it fails in the direction that looks safest: every test passes, and production stalls. brainy 4.2.1 shipped exactly that — an update() probed every known value of every known field, once per update — through a fully green suite.
MEASURED (scripts/flush-cost-repro.mjs, 12,000 rows × 90 fields with several near-unique columns, 200 updates + flush per cycle, brainy 10.3.1, 32-core box). Two flush depths, because a store's history is an axis of its own — the same data, written in 12 segments per field or in 240:
build | per cycle, 12 seg/field | per cycle, 240 seg/field | probes per update |
|---|---|---|---|
4.1.0 | 655 – 721 ms | 662 – 738 ms | 0 |
4.2.1 | 59,121 – 67,397 ms | 340,363 – 350,092 ms | ~97,300 |
4.2.2 | 655 – 752 ms | 698 – 791 ms | 2 – 4 |
Read the 4.2.1 row across, not down: the probe count barely moves between the two depths, yet the cycle costs 5.7× more. That is a second term — one that scaled with how long the process had been running rather than with the data — and it is the reason the field reports described a stall that got worse hour by hour. Both terms are gone in 4.2.2, which is flat across the two depths.
Two lanes enforce it, and they are deliberately different instruments:
src/native/maintenanceCostBudget.test.ts— always on, part of the ordinary suite. It asserts the shape of the cost from the engine's own counters: probes per update stay bounded by the entity's own pairs, do not grow when the store grows 4×, and reverse-index heals never repeat for an entity already healed. A counter-based assertion cannot flake into a false green the way a stopwatch can../scripts/ci-on-bxl9000.sh cost— the at-scale lane, run before every release. It builds the aged, many-field, update-heavy shape the rest of the suite lacks and enforces a per-cycle wall budget.
Methodology
Workload mix
The representative workload is a 70 / 20 / 10 split:
70 % filter-driven queries (
wherewith 1-3 predicates, selectivity from 0.1 % to 10 %)20 % vector-driven queries (vector search with optional metadata pre-filter)
10 % graph-driven queries (1- or 2-hop walks with verb-type selectivity)
The mix tracks the access pattern Soulcraft Memory observes in production: most queries are structured browse / filter; vector search is the discovery path; graph hops are the relationship expansion.
Hardware floor
The budget is anchored on the brainy 3.0 design floor — a 32- thread Intel Core i9-14900HX with 64 GB DDR5 and NVMe Gen 4 SSD. That's roughly a $2K commodity workstation; production targets sit on top of it. The bxl9000 measurements (Ryzen 9 7950X3D, 184 GB) are the billion-tier ceiling. Numbers from cloud VMs are typically 1.3–1.8× slower for the same work because of memory bandwidth and NVMe queue depth.
What "p50" and "p99" mean here
p50 is the median measurement over 100 query repetitions with a warm page cache. It represents the steady-state cost.
p99 is the 99th percentile and represents the worst typical query — page-cache miss, garbage collection pause, or a background msync.
We do not publish a p99.9 budget. p99.9 measurements are dominated by external noise (kernel scheduling, NVMe firmware latency spikes) and are not reproducible across runs.
The hot paths inside find()
A unified find({where, vector, hop, orderBy, limit, offset}) query executes against three brainy subsystems in sequence. Each subsystem has a sub-budget that rolls into the totals above.
Filter pushdown —
metadataIndex.getIdsForFilter(field, value)returns a Roaring bitmap of matching entity ints. AND / OR / NOT combinators are bitmap operations. Sub-budget: ≤ 0.3 ms for a 3-predicate AND at 100 M.Vector search —
NativeDiskAnnWrapper.searchWithFilterwalks the Vamana graph, prefilters candidates against the metadata bitmap, scores by distance. Sub-budget: ≤ 1 ms at 1 M (Mode 1), ≤ 4 ms at 100 M (hybrid).Graph hop expansion —
graphIndex.getNeighbors(int_id, verb_type)returns a Roaring bitmap of neighbor ints. Sub- budget: ≤ 0.3 ms per hop at 100 M.Cross-subsystem ID resolution —
intsToUuidsBatchover the final result set. Sub-budget: ≤ 0.1 ms per 1 K results.
The sub-budgets sum to within the top-level budget when the query plan is correct. They blow the budget when the plan is wrong (e.g. vector search runs over the whole corpus when a 1 %-selective filter could prune it first). The query planner described in Investments below is what keeps the plan correct.
The brainy 3.0 performance investments
The shadow-page rework (G4.1, G4.4, G4.2 redo per the snapshot safety contract) sets the substrate. The following investments build on it to reach the budget at billion scale:
Cross-subsystem query planner. Today
find()runs filter → vector → graph in a fixed order. The planner usesmetadataIndex.getCountForCriteriafor cardinality estimates and picks the lowest-cost order per query (e.g. a 0.01 %- selective filter goes first, prunes 99.99 % of vector work). PostgreSQL does this. Pure vector DBs don't.Batched cross-subsystem APIs. Every napi boundary call pays ~5 μs of overhead. Resolving 10 K UUIDs one-at-a-time costs 50 ms in napi alone, before any real work. Batched variants (
intsToUuidsBatch,getNeighborsBatch,getVerbsBatchCached) amortise that to ~0.5 μs per item. The hot path uses batches exclusively.Lock-free delta cache reads. The LSM delta cache (added in G4.3, applied in G4.1 + G4.4) currently sits behind a
Mutex. A shardedDashMaporArcSwap<Arc<HashMap>>MVCC pattern lifts the read-side lock entirely, lets concurrentfind()calls scale linearly with cores.SIMD bitmap operations. CRoaring uses portable intersect/union; modern AVX-512 popcnt and
vpternlogqkernels are ~3–5× faster on bitmap-heavy filter combinations.DiskANN predicate pushdown. The Vamana beam search currently scores every neighbor of every visited node. With a metadata-bitmap predicate passed into the search engine, pruned nodes are skipped during the walk, not after. Massive win for high-selectivity hybrid queries.
Pipelined result resolution. DiskANN currently returns all top-K candidates before resolution starts. Pipelining the resolution (
intsToUuidsBatchover the first 10 candidates while the walk continues) hides the resolution latency entirely.Zero-allocation hot path.
find()allocates intermediateVec<u64>andHashSet<String>per query. A bumpalo-style arena allocator (reset per query, no per-element free) removes the allocation pressure and the GC pause.
Each investment ships with the gates above as the proof point.
Forbidden patterns
The performance contract has anti-patterns the way the snapshot safety contract does:
Per-row napi calls on the hot path. Anything in
find()that doesfor (id of ids) { resolve(id) }over napi is incorrect. Use a batch.Lock acquisition on every read. A
Mutexon the read path costs at minimum a CAS on every call. At 100 K QPS with contention, this is a bottleneck before the first real operation runs. UseArcSwapor per-shard locking.Synchronous compactions on the hot write path. Compaction is a background operation. A write that blocks for the duration of a compaction is a p99 cliff; we currently run compaction synchronously in G4.3's
ShadowPageEndpointsStore(acceptable at 3.0 launch scale) but the pattern is explicitly recognised as a future fix.String allocation in the inner loop. UUIDs are converted to strings for the JS boundary, but the conversion has to happen once per result row, not per intermediate set. Carry integer ids through the planner; convert at the egress.
Quadratic scans where indexed lookups exist. Filter evaluation that walks all entities checking each against a predicate, instead of consulting the metadata index, is the hidden cliff at scale. Every
whereclause is index- accelerated or it's a release blocker.
See also
docs/snapshot-safety.md— the snapshot safety contract this performance work is co-designed with.docs/verification-report.md— measured baseline numbers (SIFT1M / SIFT10M / 1M synthetic, AVX-512 baseline) that the budget is anchored on.docs/comparison.md— comparison of brainy + brainy against pure vector / pure graph / pure document databases on the triple-intelligence query.