rust-perf · git:20260910.eee8645 · 2026-09-10 · sha256 3e49ec7ce4ea3881

rust-perf git:20260910.eee8645A

Immutable. This exact content is served forever at /api/v1/blob/3e49ec7ce4ea3881.

---
name: rust-perf
description: Rust performance and memory optimization for Skill Doctor L1. Use when making scans faster, reducing RSS or binary size, adding rayon/mmap, writing benchmarks, or discussing the §7.3 targets (2000 skills/min, 40MB, 12MB binary, 15s cold).
---

# Rust performance

Load this before optimizing. Do **not** sacrifice invariants (determinism, additive-only, no mandatory LLM) for speed.

## Pre-registered targets (§7.3) — not measured results

Treat these as the optimization budget. Write them into benches as `assert` ceilings once harness exists. Until then, design so they are reachable. Never publish a number as measured unless `sd-bench/RESULTS.md` was produced by the harness on a tagged release.

| Metric | Target |
|--------|--------|
| Throughput (offline L1, warm cache off) | **≥ 2,000 skills/min** |
| Peak RSS | **< 40 MB** |
| Install | **one binary, ~12 MB, zero runtime deps** |
| Cold install → first result | **< 15 s** |

Parity with Cisco/NVIDIA on synthetic TPR is the detection claim; **adoption** is this table. Competitors cannot move install footprint without leaving Python.

## What to optimize (in order)

1. **Do not compile YARA at scan time.** `build.rs` once. Scan = match.
2. **mmap** skill files with `memmap2` when size > ~64 KiB. Keep small files on the stack/heap.
3. **Zero extra alloc on the miss path.** A clean skill should not build a `Vec<Finding>` of zeros then throw it away — return empty slice / `SmallVec`.
4. **Rayon over files and engines**, not over lines inside one 2 KB SKILL.md.
5. **Sorted walk once.** Determinism and perf: `ignore`/`walkdir` → collect → `sort` → process.
6. **Canonical digest** over sorted `(path, content)` pairs; hash streaming, not a giant concatenated `String`.
7. **No tokio, no async, no reqwest** on the default path. Async startup cost kills cold-install.
8. **Binary size:** default features slim. `LTO=thin` in release profile. `strip = true`. Avoid `tokio`+`reqwest`+`openssl` in the default binary (use rustls only if L4 is on). Target ~12 MB **stripped musl**.
9. **RSS:** do not hold every file's bytes after L1 on that file finishes. Drop mmaps per file. Global caches are digest → findings, bounded.
10. **Ruleset** is embedded bytes, not a temp directory of `.yar` files at runtime.

## Forbidden “optimizations”

- Skipping Unicode/SD-11 work on the L2 path to go faster (L2 is optional; L1 unicode still runs).
- Unordered `HashMap` iteration in reports (breaks determinism). Use `BTreeMap` or sort before serialize.
- `parallel` + unsynchronized `Vec::push`.
- Adding an LLM “to filter false positives” on the hot path.
- `cargo build --release` numbers copied into the paper. Paper values stay **targets** until RESULTS.md.

## How to measure

```bash
cargo build -p skill-doctor --release
/usr/bin/time -v ./target/release/skill-doctor scan-all ./tests/fixtures --offline --deterministic
# RSS = Maximum resident set size
# Throughput = fixtures / wall time * 60
```

Prefer `criterion` or a fixed `benches/` once the scan path exists. Pin fixture counts. Do not use `std::time` in unit tests as a pass/fail speed gate (too noisy on CI). Put performance gates in `sd-bench` or a dedicated `perf.yml` nightly job, not in the every-commit clippy job.

## Release profile (root Cargo.toml)

```toml
[profile.release]
lto = "thin"
codegen-units = 1
opt-level = 3
strip = true
panic = "abort"   # binary only; libraries stay unwind if tests need it
```

Use `panic = "abort"` only on the binary package via `[profile.release.package.skill-doctor]` if tests need unwind.

## Mental model

Cold install < 15 s means: static musl binary on PATH, no `pip`, no model pull, no rule compile, first `scan` of a small skill is dominated by process start + mmap + YARA match. If you add a runtime that downloads models or compiles rules, you have missed the target on purpose.