R2/R3 verification and R1 comparison
R2/R3 completed 36 real-server runs on 2026-09-06 using the R1 workload: Redis/Valkey ×
10,000/100,000/1,000,000 keywords × shared/diverse/Korean distributions × two repetitions.
The R1 measurements are unchanged in benchmarks/results/v3-20260906.json; new
measurements, source hashes, and safety results are in
benchmarks/results/r2-r3-20260906.json.
Environment: Apple M4, 10 logical CPUs, 16 GiB RAM, macOS 26.5.2, Go 1.26.7, Redis 8.10.1, source-built Valkey 9.1.2. Both standalone on localhost TCP with RDB/AOF disabled. Preset remains MemoryEfficient and polling remains 50 ms for measurement; R2 adds the default fixed 20 ms refresh debounce. Each run uses a fresh Go process. These are developer-workstation measurements with possible background host activity — not latency guarantees or confidence intervals.
Valkey uses prefetch-batch-max-size 0, as in the R1 baseline. The local default-prefetch
build previously crashed; this work neither establishes support for that configuration nor
diagnoses it. See the R1 report for the source checksum and
crash evidence.
What R2 changed
- The installed engine keeps verified immutable keyword slices and its manifest. Unchanged buckets share those slices; only changed nonempty buckets download chunk data. Candidate caches install with the engine, and only after success.
- MemoryEfficient builds consume the bucket sequence directly, avoiding the flattened dictionary slice and an extra keyword-set map. Nodes keep their only child inline and allocate a map only when they branch; BFS uses a compact integer queue; the first-rune Bloom filter sizes itself from distinct first runes rather than total keyword count.
- All three presets check cancellation during insertion, failure-link construction, and table filling. Cancellation discards the private candidate with no detached builder goroutine left running. Allocations, rune-count calls, and sorting finish before the next checkpoint.
- A fixed debounce window merges burst notifications and cannot be extended by new events,
so an in-flight build completes under ordinary writes rather than being starved by
repeated cancellation.
Closecancels its build context. Statusreports the last successful build’sDownloadedBuckets/ReusedBucketsand cumulativeCompletedBuilds. Redis schema, expected-version writes, receipt resolution, leases, and pruning semantics are unchanged.
Million-keyword comparison
Ranges are minimum–maximum of two runs. Ready combines write preparation, commit, refresh detection, and engine construction. RSS is the process high-water mark across the full run. Network counters are server-wide and include protocol, polling, and engine downloads.
| Server | Distribution | Add 1 ready: R1 → R2 (s) | Received bytes reduction for add 1 | Peak RSS: R1 → R2 (GiB) |
|---|---|---|---|---|
| redis | shared | 1.20–1.21 → 0.55–0.80 | 94.7% | 1.20–1.20 → 1.01–1.09 |
| redis | diverse | 2.28–2.30 → 1.11–1.47 | 91.9% | 3.14–3.55 → 2.05–2.05 |
| redis | korean | 2.96–3.17 → 1.22–1.32 | 96.2% | 3.02–3.74 → 2.33–2.45 |
| valkey | shared | 1.21–1.23 → 0.53–0.55 | 94.7% | 1.21–1.30 → 1.05–1.16 |
| valkey | diverse | 2.33–2.40 → 0.92–1.27 | 91.9% | 3.02–3.80 → 2.02–2.19 |
| valkey | korean | 2.90–3.40 → 1.21–1.60 | 96.2% | 3.37–3.69 → 2.33–2.50 |
All six million-keyword combinations reduced single-change received bytes and peak RSS in these runs. A full engine is still rebuilt after a change — this is not an incremental automaton or a constant-memory design, and larger changes touch more buckets and reuse less.
| Server | Distribution | Add 1,000 ready: R1 → R2 (s) | Add 1% ready: R1 → R2 (s) | Full replace ready: R1 → R2 (s) |
|---|---|---|---|---|
| redis | shared | 1.40–1.41 → 0.95–1.22 | 2.07–2.08 → 1.82–2.93 | 2.86–2.88 → 2.38–3.02 |
| redis | diverse | 2.70–2.71 → 1.32–1.46 | 3.01–3.38 → 2.11–4.08 | 3.98–4.34 → 3.06–3.96 |
| redis | korean | 3.55–4.48 → 1.38–1.90 | 4.63–6.11 → 2.64–2.82 | 7.43–7.71 → 3.24–3.92 |
| valkey | shared | 1.40–1.41 → 0.82–1.25 | 2.05–2.08 → 1.80–1.89 | 2.86–2.88 → 2.21–2.67 |
| valkey | diverse | 2.42–2.60 → 1.21–1.29 | 3.03–3.54 → 2.14–2.19 | 4.08–4.33 → 2.77–3.61 |
| valkey | korean | 4.03–4.38 → 1.45–1.60 | 3.98–4.49 → 2.64–2.70 | 6.78–7.31 → 4.17–4.92 |
Improvement is not uniform: some Redis large-change repetitions overlap or exceed the R1 range. Two repetitions do not establish a universal speedup.
The JSON also records 10,000/100,000-entry results, initial loading and startup, identical replacement, removals, search p50/p95/p99 while refreshing, Redis memory, and pruning, using the same seed 20260906 and generation functions. Prune measurements simulate the retention horizon by aging generation registry scores rather than waiting 24 hours. Separate million-entry safety tests passed against both servers after the matrix, with no concurrent workload on a measured endpoint.
R3 contracts and verification
Scan, MaskText, and ReplaceText are additive on AhoCorasick and
VersionedCollection; existing unlimited APIs are unchanged. Defaults and error behavior:
bounded text processing.
| Area | Evidence |
|---|---|
| Original positions | All presets match existing FindMatches for overlapping and leftmost-longest results; original byte slices survive Korean, emoji, İ case folding, and malformed UTF-8 |
| Result limits | At most MaxMatches retained; Truncated requires an additional eligible result |
| Work/input bounds | Oversize input and excess raw candidates fail explicitly, including candidates later filtered by word boundaries or overlap |
| Atomic rewrite | Match, output, and work exhaustion return no partial rewrite; empty literal replacement deletes; masks preserve original rune count, including NUL masks |
| Overlap | A bounded pending-start heap selects leftmost-longest matches without collecting all raw matches |
| Fuzz | FuzzScanLeftmostParity completed 30 seconds and 819,033 executions against the existing leftmost-longest implementation |
| Refresh | Bucket reuse, failed-candidate cache preservation, and burst coalescing tests pass |
| Cancellation | All three presets preserve the previous engine on cancellation during sequence ingestion and CPU construction; unrelated panics are not swallowed |
| Safety | Both real servers pass the million-entry conflict, pinned paging, and lease-safe pruning scenario |
Resource bounds are per call: scratch input indexing is proportional to the bounded input
size, and the pending window is bounded by input/longest-keyword length and candidate
count. A custom WordRune controls its own resource use. R3 claims no wall-clock bound on
callbacks or allocation.
Root and server race tests, root/server lint, all-module vet, benchmark-module tests, API snapshot/audit, and documentation compilation pass. Legacy Find/Add fuzz targets rerun for 30 seconds each. R2/R3 introduces no HTTP/gRPC surface and no storage schema migration.
Reproduce
Run the unchanged scripts/benchmark-v3.sh against each disposable endpoint with
ACOR_V3_SCALE_REPEATS=2 and separate output directories; the exact environment and
dictionary distributions are in the
R1 reproduction instructions. The R2/R3 defaults
select the new engine and refresh behavior automatically, and existing V1/V2 public
contracts remain covered by the regression suite.