# Carto at scale

> Empirical scale data for the bitmap engine — synthetic stress sweep + every real-world repo in the Carto corpus. Reproducer commands at the bottom.

The bitmap-backed graph engine is designed to handle 100K-1M files cleanly. This page is the empirical receipt. It captures both the **dense-synth stress sweep** (deterministic worst-case fan-out distribution) and the **real-world corpus** (4 of the 7 larger repos, the largest open-source codebases under MIT/permissive licenses we routinely test against).

## What's measured

For each run we capture:

- **Init time** — full `runSync` from a wiped `.carto/` directory.
- **Sync time** — second `runSync` immediately after, no files changed (mtime+hash skip path).
- **DB size** — `.carto/carto.db` bytes on disk.
- **bitmap.bin size** — the sidecar bytes on disk.
- **Sidecar (RAM)** — sum of all bitmap word-array byte lengths held in memory.
- **Peak RSS** — process resident memory after init and during query workloads.
- **MCP query p50 / p99** — 1000 calls each (50-call warmup) for the 5 production bitmap tools (`blastRadius`, `crossDomain`, `highImpactFiles`, `similarPatterns`) plus `simulate_change_impact`. Random query inputs are seeded so identical `--seed` produces identical workloads.

The query stage opens SQLite **read-only** — invariant honored. The bitmap-as-derived-disposable invariant is also honored: the runner only reads from `bitmap.bin`, never writes.

All numbers below were captured on the maintainer dev box: Apple silicon, Node 20.20, macOS, NVMe SSD. Recapture via the commands at the bottom.

## Synthetic stress sweep (dense fan-out)

Source: `bench/scale-test/`. Generates a deterministic-for-seed TypeScript repo with a worst-case-shaped import graph: 5 domain-like top-level dirs (`auth`/`payments`/`database`/`events`/`core`), Pareto(α=2) fan-out clipped to [1,8], 75% same-domain / 25% cross-domain, hotspot-biased targets so the first ~10% of file ids accumulate most in-edges. Imports always reference an earlier file id (acyclic).

Edge density ≈ 1.5 per file. **This is denser than every real-world repo in the corpus** — real codebases have many leaf files with zero imports, so the synth deliberately exercises the worst case for the bitmap layer.

| Files | Edges | Init | Sync | DB | bitmap.bin | Sidecar (RAM) | Peak RSS |
|------:|------:|-----:|-----:|---:|-----------:|--------------:|---------:|
| 1,000 | 1,392 | 533ms | 70ms | 756 KB | 222 KB | 186 KB | 149 MB |
| 10,000 | 14,876 | 3.28s | 627ms | 6.5 MB | 17.0 MB | 18.1 MB | 350 MB |
| 50,000 | 76,145 | 1.1m | 5.14s | 36.9 MB | 414.9 MB | 488.4 MB | 1.35 GB |

| Files | blastRadius p50/p99 | crossDomain p50/p99 | highImpactFiles p50/p99 | similarPatterns p50/p99 | simulate_change_impact p50/p99 |
|------:|---------------------|---------------------|------------------------|------------------------|--------------------------------|
| 1,000 | 1.6µs / 24.4µs | 19.5µs / 25.0µs | 709ns / 4.3µs | 276µs / 484µs | 7.0µs / 78.8µs |
| 10,000 | 6.6µs / 67.7µs | 705µs / 914µs | 750ns / 1.4µs | 17.0ms / 19.3ms | 11.6µs / 113µs |
| 50,000 | 22.4µs / 472µs | 28.1ms / 28.9ms | 750ns / 1.3µs | 462ms / 798ms | 49.9µs / 455µs |

### What 50K reveals

Three distinct failure modes show up at the dense-synth 50K row, **all of them addressable by a Roaring-bitmap upgrade on the roadmap**:

1. **Storage scales linearly with N** — every file gets its own forward + reverse bitmap of size ~`N/8` bytes. At N=50K bitmap.bin is 415 MB; at N=100K linear extrapolation puts it at ~1.7 GB. The current pure-Node `Uint32Array` bitset (`src/bitmap/bitset.js`) trades storage efficiency for raw query speed and zero native deps. Roaring would cut this 5-50× depending on density.
2. **`crossDomain` p50 hits 28 ms** — linear sweep over the `crossForward` Map. Fixable with a precomputed cross-domain edge bitmap that's OR-aggregated once at build time, not iterated per query.
3. **`similarPatterns` p50 hits 462 ms** — Jaccard over import sets at scale costs O(candidates × set-intersection). The MCP layer calls this with `top_k=5` so user-facing latency is well under the raw bench number, but the absolute cost is real.

Three things stay healthy at 50K: `blastRadius` p50 22µs, `highImpactFiles` p50 750ns (popcount index is O(1) array slice), `simulate_change_impact` p50 50µs.

### Pending (not yet captured on this box)

| Files | Notes |
|------:|-------|
| 100,000 | Stress-test for the dense-bitset implementation. ~3-5 min walltime expected. Worth running before any Roaring work to lock in the regression baseline. |
| 500,000 | Practical ceiling for the dense-bitset; expect bitmap.bin > 4 GB, possibly hitting OOM on 16 GB-RAM dev boxes. |
| 1,000,000 | Roaring-bitmap upgrade target. Expect dense-bitset OOM. |

Reproducible via `npm run bench:scale -- --size 100000 --keep --out /tmp/carto-100k`.

## Real-world corpus

These are the 4 largest repos in `~/carto-test-repos`, all already indexed by `~/carto-test-repos/run-bench.sh` (the bench harness baseline). The query latency below was measured against those existing indexes via `--queries-only` so this measurement does not perturb the indexes used by `node test/accuracy-corpus.js`.

| Repo | Indexed files | Edges | DB | bitmap.bin | Sidecar (RAM) |
|------|--------------:|------:|---:|-----------:|--------------:|
| [cal.com](https://github.com/calcom/cal.com) | 4,351 | 3,478 | 3.1 MB | 1.9 MB | 2.0 MB |
| [nextjs](https://github.com/vercel/next.js) | 6,193 | 7,930 | 15.0 MB | 4.4 MB | 4.3 MB |
| [supabase](https://github.com/supabase/supabase) | 6,330 | 5,189 | 4.8 MB | 4.4 MB | 4.4 MB |
| [vscode](https://github.com/microsoft/vscode) | 7,567 | 13,335 | 14.3 MB | 4.1 MB | 4.5 MB |

| Repo | blastRadius p50/p99 | crossDomain p50/p99 | highImpactFiles p50/p99 | similarPatterns p50/p99 | simulate_change_impact p50/p99 |
|------|---------------------|---------------------|------------------------|------------------------|--------------------------------|
| cal.com | 4.3µs / 51µs | 148µs / 279µs | 667ns / 1.0µs | 1.0µs / 1.99ms | 10.3µs / 71.0µs |
| nextjs | 5.0µs / 155µs | 196µs / 284µs | 750ns / 1.3µs | 1.2µs / 3.01ms | 28.9µs / 211µs |
| supabase | 5.5µs / 47.9µs | 201µs / 260µs | 708ns / 1.0µs | 1.5µs / 3.19ms | 10.8µs / 65.9µs |
| vscode | 2.7µs / 428µs | 1.23ms / 1.47ms | 750ns / 1.7µs | 834ns / 4.03ms | 19.3µs / 637µs |

### Real-world vs dense-synth

The 7,567-file vscode row vs the 10,000-file synth row tells the most important story:

- vscode `similarPatterns` p50 is **834ns**; synth-10K is **17ms**. Real codebases have far sparser per-file import sets — most files import 0-3 things, a handful import many. Synth is a deliberate worst case (Pareto fan-out always ≥1, often 4-8). Real Jaccard-over-import-sets fits in cache; synth doesn't.
- vscode `crossDomain` p50 is **1.23ms**; synth-10K is **704µs**. Cross-domain edges scale with edge density; synth is denser. Both are sub-2ms at any sensible scale.
- vscode `bitmap.bin` is **4.1 MB** at 7,567 files; synth-10K is **17 MB** at 10,000 files. ~4× density difference, reflected directly in storage.

The takeaway: **dense-synth identifies the failure modes; real-world data sets the actual user expectations.** Both are needed.

## Linux kernel and Chromium

Block 1.B in PEAK §10 explicitly calls out the Linux kernel and Chromium as scale targets. Neither is in `~/carto-test-repos` today (multi-GB clones, gated on the maintainer's disk budget). The `bench/scale-test/real-world.js` driver lands here so capturing those rows is one command once a clone exists:

```bash
git clone --depth=1 https://github.com/torvalds/linux ~/clones/linux
node bench/scale-test/real-world.js --repo ~/clones/linux --name linux-kernel
```

```bash
# Chromium follows the official `fetch` flow; expect a 30+ GB clone.
node bench/scale-test/real-world.js --repo ~/clones/chromium/src --name chromium
```

These rows are intentionally not yet captured. The 50K dense-synth + 7,567-file vscode + 6,330-file supabase rows above already establish the scaling profile of the current implementation; whether kernel/Chromium clear or fail is *known* in the bitmap-storage sense (linear in N, hits the GB scale at 100K+ files in the worst case). Capturing them on the maintainer box is a confirm-the-known activity, not a discover-the-unknown one.

## Reproducing this page

Every number above is mechanically reproducible. The exact commands:

```bash
# Synth sweep (deterministic, dense-fan-out worst case)
npm run bench:scale -- --size 1000
npm run bench:scale -- --size 10000
npm run bench:scale -- --size 50000

# Real-world corpus (uses existing indexes; doesn't re-index)
node bench/scale-test/real-world.js --repo ~/carto-test-repos/tier-b/vscode --name vscode --queries-only
node bench/scale-test/real-world.js --repo ~/carto-test-repos/tier-b/supabase --name supabase --queries-only
node bench/scale-test/real-world.js --repo ~/carto-test-repos/tier-b/nextjs --name nextjs --queries-only
node bench/scale-test/real-world.js --repo ~/carto-test-repos/tier-b/cal.com --name cal.com --queries-only

# Larger (maintainer-machine work)
npm run bench:scale -- --size 100000 --keep --out /tmp/carto-100k
```

Each run writes `bench/scale-test/results/<tag>-<ISO-timestamp>.json` and refreshes `bench/scale-test/REPORT.md`. The aggregator keeps the latest run per (kind, label) so re-running a row replaces the old number.

---

_See `bench/scale-test/README.md` for full harness documentation._
