84 lines
4.5 KiB
Markdown
84 lines
4.5 KiB
Markdown
|
|
---
|
|||
|
|
title: Performance Envelopes
|
|||
|
|
slug: guides/performance-envelopes
|
|||
|
|
public: true
|
|||
|
|
category: guides
|
|||
|
|
template: guide
|
|||
|
|
order: 40
|
|||
|
|
description: Measured per-operation latency envelopes at stated scales — what to expect, on what hardware, and exactly how each number was produced.
|
|||
|
|
next:
|
|||
|
|
- guides/find-limits
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# Performance Envelopes
|
|||
|
|
|
|||
|
|
Every number on this page is **measured, never projected** — produced by the script
|
|||
|
|
cited at the bottom, against the built package (the artifact you install), on the stated
|
|||
|
|
hardware. Each entry says what was measured, at what scale, on which storage backend.
|
|||
|
|
When a release touches a measured path, that operation is re-measured and this page
|
|||
|
|
updates in the same release.
|
|||
|
|
|
|||
|
|
Two scopes to keep straight:
|
|||
|
|
|
|||
|
|
- **These envelopes are the pure-JS engine** (no native accelerator registered) on
|
|||
|
|
filesystem storage. This is the floor every deployment gets from `npm install` alone.
|
|||
|
|
- **Accelerated deployments** (the optional native provider) publish their own numbers —
|
|||
|
|
this page never claims them.
|
|||
|
|
|
|||
|
|
## Read operations
|
|||
|
|
|
|||
|
|
Reads are where the architecture pays off: after the write path has done its indexing
|
|||
|
|
work, queries answer from purpose-built indexes without scanning.
|
|||
|
|
|
|||
|
|
| Operation | 1,000 entities | 10,000 entities | Notes |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `get(id)` (warm) | p50 < 0.1ms | p50 < 0.1ms | served from cache/metadata index |
|
|||
|
|
| `find` (metadata: indexed equality + range, limit 100) | p50 1.0ms · p95 1.8ms | p50 7.0ms · p95 8.9ms | column-store bitmap paths |
|
|||
|
|
| `related(id)` (per-node adjacency) | p50 < 0.1ms · p95 0.2ms | p50 < 0.1ms | LSM adjacency index — O(degree), scale-independent |
|
|||
|
|
| `find` (semantic: embed + HNSW, 1k docs) | p50 178ms · p95 393ms | — | dominated by WASM query embedding (measured on a machine under concurrent load — treat the p95 as an upper bound); the vector search itself is single-digit ms |
|
|||
|
|
|
|||
|
|
## Write operations
|
|||
|
|
|
|||
|
|
Under Model-B **every write is its own durable generation** — a single-op `add` pays
|
|||
|
|
serialization, before-image staging, and fsync before it acks. That durability is priced
|
|||
|
|
into the write path visibly, by design:
|
|||
|
|
|
|||
|
|
| Operation | 1,000 entities | 10,000 entities | Notes |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `add` (single-op) | p50 167ms · p95 171ms | p50 165ms · p95 172ms | full durable generation per write — flat across scale |
|
|||
|
|
| `addMany` (bulk) | ~163ms/entity | ~187ms/entity | **currently per-item commits** — see the honest note below |
|
|||
|
|
| `relateMany` | ~0.8ms/edge | ~0.9ms/edge | edges batch efficiently today |
|
|||
|
|
| `flush` (steady-state, 1 pending write) | p50 8ms · p95 10ms | p50 45ms · p95 52ms | durability-only since 8.9.0 — cost no longer depends on history backlog or retention mode |
|
|||
|
|
|
|||
|
|
**The honest note on bulk writes:** `addMany` today commits each item as its own
|
|||
|
|
generation (the same durability as single-op `add`, serialized by the single-writer
|
|||
|
|
lock), so bulk-load cost is N × single-op cost. Batched chunk commits (one generation
|
|||
|
|
and one fsync window per chunk, as `removeMany` already does) are designed into the
|
|||
|
|
unified-commit work on the current roadmap. Until that ships, size bulk imports
|
|||
|
|
accordingly — 10k entities is minutes, not seconds, on filesystem storage.
|
|||
|
|
|
|||
|
|
## Open / close
|
|||
|
|
|
|||
|
|
| Operation | 1,000 entities | 10,000 entities | Notes |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `open` (empty store) | ~560ms | ~190ms | includes embedder initialization |
|
|||
|
|
| `open` (warm, populated, clean shutdown) | 763ms | 4.9s | pure-JS vector index load dominates and grows with entity count; the native accelerator exists precisely to remove this |
|
|||
|
|
| `close` | bounded | bounded | auto-compaction pass is time-bounded (~5s max) since 8.9.0 |
|
|||
|
|
|
|||
|
|
A store that was NOT cleanly closed pays index rebuilds on top of the warm-open
|
|||
|
|
number (tens of seconds at 10k) — clean shutdown is worth engineering for.
|
|||
|
|
|
|||
|
|
## How these were produced
|
|||
|
|
|
|||
|
|
- **Hardware**: Intel Core i9-14900HX (32 threads), 62GB RAM, NVMe, Linux, Node v22.
|
|||
|
|
- **Backend**: `storage: { type: 'filesystem' }`, pure JS (no native providers).
|
|||
|
|
- **Embeddings**: deterministic stub for non-semantic ops (isolates engine cost);
|
|||
|
|
the real WASM embedder for the semantic row (that's what you'll run).
|
|||
|
|
- **Method**: p50/p95 over 50–200 samples per op against the built `dist/`;
|
|||
|
|
the measuring script ships in the repo history and re-runs per release.
|
|||
|
|
|
|||
|
|
Numbers on different hardware will differ; the *shape* (sub-2ms indexed reads,
|
|||
|
|
~160ms embedding-bound semantic queries, durability-priced writes) is the envelope
|
|||
|
|
you should hold your deployment against. If your measurements diverge from these
|
|||
|
|
shapes by an order of magnitude, something is wrong — file it.
|