Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ The managed-memory limit covers the index mapping, heat bits, L1, append buffers

C² owns one logical data path. Multi-device deployments stripe below the filesystem with RAID0 or an equivalent layer. Request routing, recovery identity, and descriptor count stay independent of device topology.

## Statistics

Existing health/resource snapshots and optional legacy activity counters retain their populations. Additional `RuntimeOptions::stats` recorders use cache-owned, preallocated atomic stripes whose allocation is validated and included in managed memory. Public operations contribute one exclusive terminal outcome; original L2 hits remain L2 even after promotion. L1 hit, L2 lookup and mutation timing are independently disabled, full or randomly sampled. L2 clocks start immediately after L1 miss, before index lookup and admission, so full L2 timing does not require clocks on L1 hits; full I/O timing reuses engine timestamps and preserves separate read/write/reclaim populations. Fixed scalar thread-local routing and sampling state cannot retain a cache instance or grow a per-cache registry. Recording adds no allocation, queue, recorder lock or worker.

Statistics snapshots load cumulative counters without resetting them or scanning metadata. Histogram bucket counts and duration sums may reflect slightly different instants during concurrent updates; quiescent snapshots are exact. Snapshot collection runs on the caller, with bounded output determined by the fixed operation/outcome set and histogram layout. Applications own all metric conversion and transport. Sampled observations remain explicitly distinct from full-population metrics, and invalid overflowed distributions are flagged for applications to omit. Additional recorder control objects fit the fixed runtime control reservation; variable stripe storage is separately charged.

## Recovery and failures

### Persistent artifacts
Expand Down
2 changes: 2 additions & 0 deletions BENCHMARK.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ The main controls are grouped below. See `benchmarks/cache/main.rs` for defaults
| Measurement | `CACHE_BENCH_READ_LATENCY_SAMPLE_INTERVAL` (default 16; zero disables), `CACHE_BENCH_STATS` (default false; true adds cache/I/O/resource records) |
| Gates | `CACHE_BENCH_MIN_PUT_OPS`, `CACHE_BENCH_MIN_RESIDENT_L1_OPS`, `CACHE_BENCH_MIN_L2_OPS`, `CACHE_BENCH_MAX_WARM_CLOSE_MS` |

The additional stats implementation is controlled independently by `CACHE_BENCH_REQUEST_STATS` (complete request counters, default false), `CACHE_BENCH_L1_LATENCY_SAMPLE_INTERVAL`, `CACHE_BENCH_L2_LATENCY_SAMPLE_INTERVAL`, and `CACHE_BENCH_MUTATION_LATENCY_SAMPLE_INTERVAL` (independent: 0 off, 1 full, greater values sample with that mean interval; default 0), `CACHE_BENCH_IO_LATENCY` (full engine latency, default false), and `CACHE_BENCH_STATS_SHARDS` (default 16). These instrument the library itself. `CACHE_BENCH_READ_LATENCY_SAMPLE_INTERVAL` remains the independent benchmark observer and should be held constant across comparisons. The effective additional settings are printed with each run. Compare disabled, counters-only, full and sampled modes on the same workload, keeping actual hit/overload populations in view.

`CACHE_BENCH_STATS=true` adds cache accounting on the measured request path. Use the same setting for baseline and candidate runs.

For device measurements, use a data set larger than host RAM and no larger than half of L2 capacity. Run baseline and candidate in alternating order at least five times, compare medians, and retain every sample. Throughput does not replace correctness, overload, memory, or latency checks.
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

### Bug Fixes

- Read I/O duration accounting now includes buffer preparation and scheduling between slot reservation and submission, matching the documented reservation-to-completion interval.

### Features

- Added `RuntimeOptions::stats` and `Cache::stats_snapshot()` for complete public request outcomes, independently configured full or sampled L1-hit, L2-lookup and mutation latency, and full read/write/reclaim I/O latency. L2 timing starts after L1 miss, allowing full L2 collection without clocks on L1 hits; structured durations identify their scope and collection mode. Recorders have bounded managed-memory accounting; snapshots preserve cumulative values and distinguish disabled families and sampled observations. The structured snapshots are independent of monitoring SDKs; applications own metric conversion and export. The existing `statistics` switch retains its behavior; fully specified runtime-option literals must add `stats` or use `..RuntimeOptions::default()`.

## v0.4.0 (2026-09-10)

### Breaking Changes
Expand Down
8 changes: 8 additions & 0 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,14 @@ IOPOLL is an additional explicit per-pool opt-in through `IoUringPoolConfig::wit

Health and managed-resource gauges are always available. Enable `RuntimeOptions::statistics` while tuning to obtain cumulative request, index, L1, and I/O counters. Enabled counters add relaxed atomic work on active paths, so measure the overhead before leaving all activity statistics enabled in a latency-critical deployment.

`RuntimeOptions::stats` adds independent controls: `request_counters` records all terminal public results; `l1_latency`, `l2_latency`, and `mutation_latency` independently select `LatencyMode::Off`, `Full`, or `Sampled { interval }`; `io_latency` records every engine completion by role; and `shards` selects a power of two from 1 through 64 (default 16). Defaults disable all additional recorders and preserve the legacy `statistics` switch. Fully specified `RuntimeOptions` literals need `stats: StatsOptions::default()` or a struct-update default.

Request counters distinguish original L1/L2 hits, misses, accepted mutations, overload, invalid input, unavailable mutations, other errors and cancelled gets. Never-polled futures contribute nothing; cancelled durations are partial lifetimes. L1 timing covers successful L1 lookups from the first poll. L2 timing begins immediately after L1 miss and covers index lookup, admission waiting, I/O and promotion, excluding the initial L1 lookup; early misses before L1 lookup contribute only to request counters. L1 miss discards the L1 timer and makes an independent L2 sampling decision. Put timing ends at acceptance, not publication or durability. I/O timing reuses engine timestamps and covers slot reservation through terminal completion, including engine queueing rather than just device service. An I/O histogram may complete after its caller has cancelled.

For low overhead start with counters and, if needed, full L2 lookup and I/O latency. Keep L1 latency off or sampled independently. Enabling only L2 timing starts no clock on L1 hits. Enable full timing for a selected population when every observed latency matters, or explicitly choose sampling after measuring overhead and per-series sample volume. Combine only matching timing scopes and sample intervals; each structured request row carries its own `latency_scope` and `latency_mode`. Applications choose metric names and attributes and own conversion, timestamps, scheduling and transport. Sampled bucket counts and sums are never inflated to full request volume. Use a full histogram with an aligned bucket boundary for exact observed SLO-threshold counts. No sampling setting guarantees detecting isolated long-tail events.

`stats_snapshot()` reports disabled families explicitly, preserves cumulative values across concurrent readers, and resets on reopen using the existing `metrics_epoch`. It does not scan L1/index/Region metadata or start background workers. Each histogram includes a validity flag; arithmetic overflow invalidates that series until reopen and applications must omit its distribution when exporting. Detailed occupancy remains an explicit `detailed_snapshot()` operation. Only enabled latency populations allocate histogram stripes. The fixed histogram boundaries are documented by `LATENCY_BUCKET_UPPER_BOUNDS_NS`; counts above the largest boundary remain in an unbounded bucket with their true duration sum.

## Goal-oriented profiles

### Balanced starting point
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ The on-disk format is versioned. During 0.x, deployments should expect cold star

`Cache::snapshot()` provides lock-free health and resource gauges. `RuntimeOptions { statistics: true, .. }` adds cumulative cache and I/O counters. `Cache::detailed_snapshot()` samples L1, index, write-buffer pressure, and Region metadata for periodic diagnostics.

`RuntimeOptions::stats` independently enables complete public request outcomes, L1-hit, L2-lookup and mutation latency (each `Off`, `Full`, or `Sampled`), and full I/O latency by read/write/reclaim role. `Cache::stats_snapshot()` combines these with the existing summary without metadata scans. Structured request rows include their timing scope and collection mode. Applications own metric conversion, timestamps, scheduling and transport. Run `cargo run --example stats -- <cache-data-path>` for an example. Full timing avoids sampling work; sampled histograms retain actual sample counts and cannot guarantee observation of rare tail events. Recorder storage is preallocated, bounded and charged to managed memory.

C² exposes snapshots for integration with the application's metrics SDK. An OpenTelemetry or Prometheus adapter can export:

- get outcomes from `l1_hits`, `l2_hits`, `l2_misses`, and `l2_read_overloads`;
Expand Down
21 changes: 21 additions & 0 deletions benchmarks/cache/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ struct BenchConfig {
io_mode: IoMode,
l1_eviction_policy: L1EvictionPolicy,
statistics_enabled: bool,
stats: cache2::StatsOptions,
directory: PathBuf,
}

Expand Down Expand Up @@ -154,6 +155,23 @@ impl BenchConfig {
}
};
let statistics_enabled = env_bool("CACHE_BENCH_STATS", false)?;
let latency = |name| -> io::Result<cache2::LatencyMode> {
Ok(match env_u32(name, 0)? {
0 => cache2::LatencyMode::Off,
1 => cache2::LatencyMode::Full,
interval => cache2::LatencyMode::Sampled {
interval: std::num::NonZeroU32::new(interval).expect("positive interval"),
},
})
};
let stats = cache2::StatsOptions {
request_counters: env_bool("CACHE_BENCH_REQUEST_STATS", false)?,
l1_latency: latency("CACHE_BENCH_L1_LATENCY_SAMPLE_INTERVAL")?,
l2_latency: latency("CACHE_BENCH_L2_LATENCY_SAMPLE_INTERVAL")?,
mutation_latency: latency("CACHE_BENCH_MUTATION_LATENCY_SAMPLE_INTERVAL")?,
io_latency: env_bool("CACHE_BENCH_IO_LATENCY", false)?,
shards: env_usize("CACHE_BENCH_STATS_SHARDS", 16)?,
};
let directory = env::var_os("CACHE_BENCH_DIR")
.map(PathBuf::from)
.unwrap_or_else(env::temp_dir);
Expand Down Expand Up @@ -264,6 +282,7 @@ impl BenchConfig {
io_mode,
l1_eviction_policy,
statistics_enabled,
stats,
directory,
})
}
Expand All @@ -285,6 +304,7 @@ impl BenchConfig {
l1_eviction_policy: self.l1_eviction_policy,
managed_memory_limit_bytes: self.managed_memory_limit_bytes,
statistics: self.statistics_enabled,
stats: self.stats,
read_admission: if self.read_io_wait_timeout.is_zero() {
ReadAdmission::Immediate
} else {
Expand Down Expand Up @@ -411,6 +431,7 @@ async fn run(config: BenchConfig) -> io::Result<()> {
};

println!("C² cache benchmark");
println!("additional_stats={:?}", config.stats);
println!(
"entries={} index_slots={} index_load={:.1}% resident_entries={} hot_entries={} hot_read_interval={} value={} B data={:.1} MiB memory={:.1} MiB initial_l1={:.1} MiB managed_memory_limit={:.1} MiB append_shards={} read_workers={} read_wait_capacity={} read_wait_timeout_us={} read_latency_sample_interval={} write_workers={} reclaim_workers={} write_clients={} read_clients={} l2_clients={} l1_entry_eligible={} l1_eviction={:?} engine={:?} mode={:?} statistics={}",
config.entries,
Expand Down
Loading