The example app compares react-native-scc-storage with react-native-mmkv through each
library's public JS API, inside a Release build. Every claim in a results table should be
traceable to the raw samples saved with the report.
| Section | Question it answers | Unit |
|---|---|---|
| Throughput | What does one synchronous call cost when calls run back to back? | ns per operation |
| Latency after idle | What does a single call cost in a real UI handler, after the event loop sat idle? | ns, p50 and p99 |
| SCC-only diagnostics | Where does SCC time go (JS wrapper vs Nitro), how long do reopen and fsync take? | ns/op or ms |
Throughput runs on two stores: a small one (482 keys over strings of 16 B / 256 B / 4 KiB, numbers, booleans, ~1 KiB JSON documents) and a 20k-key one (10k strings + 10k numbers).
| Threat to validity | Control |
|---|---|
| Simulator or Debug timings | buildMode, platform and versions are recorded; the UI warns in Debug. Publish only Release numbers from physical devices. |
| One hot key for every call | Keys come from a precomputed random sequence over the whole family, so hashing and caches see a realistic spread. |
| Rewriting an identical value | Writes rotate through 8 payload variants that differ from the stored value at every byte. |
| The two libraries doing different work | Both run the same loop body; read checksums must match exactly, and after every write case the two stores are compared key by key. |
| Background work leaking into the other library's sample | SCC's WAL writer is drained with two untimed flush() calls after each SCC sample, so a compaction never overlaps the next sample. |
| Order effects and thermal drift | Per trial, case order is shuffled and each case flips a coin for which library goes first. The seed is stored in the report. |
| Tight loops hiding per-call fixed costs | The latency section runs one call per JS task after an idle event loop, alternating libraries call by call. |
| Means hiding stalls | Throughput keeps every 12-chunk timing (p95 chunk shown); latency reports p99. |
| No uncertainty estimate | 10 throughput trials and 4 latency trials per launch; speedups carry a 95% bootstrap CI and are only called when the CI excludes 1×. |
| Launch-to-launch variance | scripts/bench-ios.mjs runs several fresh launches and reports the min–max of the per-launch medians. |
| Asymmetric listener semantics | Both libraries deliver change events asynchronously through Nitro. Samples time the write calls; each library's deliveries are drained, untimed, before the other library runs. The 50-listener case subscribes the way each library's hooks do: SCC key listeners, MMKV one filtered listener per hook. |
- Durability differs. MMKV writes into an mmap, so a value survives a process kill once
setreturns. SCC's relaxed WAL appends on a background thread within ~8 ms;flush()is the barrier. Write rows compare API return latency, not the moment data is safe. - Batch rows compare one SCC
getMany/setManycall with 100 MMKV scalar calls, because MMKV has no batch API. They measure throughput, not atomicity. - Cold start is not compared. MMKV caches instances natively and cannot be reopened in process; SCC's reopen time is reported on its own.
- Not covered yet: memory footprint, Android automation (the in-app benchmark itself runs on Android), encrypted stores.
# in-app: Release build, then tap "Run benchmark" with the device idle and cool
npx expo run:ios --configuration Release
# automated: builds with autorun, 3 fresh launches, summary + raw JSON in bench-results/
npm run bench:ios -- --device "iPhone 17 Pro" --launches 3
npm run bench:ios -- --device <physical-device-udid> --cooldown 120
npm run bench:ios -- --profile quick --launches 1 # pipeline check, numbers not meaningful
# native core only, no bridge
cargo bench -p kv-core --bench scenario_benchThe runner starts the app with -sccBenchAutorun 1 -sccBenchProfile <profile> (iOS launch
arguments, read through NSUserDefaults), so one Release build serves every profile. It reads each
report from the app's own SCC store files (last_bench in the example store), because Release
builds drop console.log output.
To see where a call spends its time, launch one case in a loop and sample the JS thread
(simulator apps are host processes; xctrace attach hung on Xcode 26.6, /usr/bin/sample works):
xcrun simctl launch booted com.rustdd.scckv.example \
-sccProfileCase get_str16 -sccProfileLib scc -sccProfileSeconds 25
/usr/bin/sample <pid> 5 -mayDie -file get_str16.txtAndroid has no launch-argument path yet: build with EXPO_PUBLIC_SCC_AUTORUN_BENCHMARK=1 (and
optionally EXPO_PUBLIC_SCC_BENCH_PROFILE). These values are inlined and cached by Metro, so
clear the cache when changing them.