tidaldb/tidal/benches/session.rs
jordan cdbe9cb453 Merge remote-tracking branch 'origin/main' (m11/m12 cluster) into m9/m10
Reconciles two independently-developed lines from base 006d3d0:
  ours   — M9/M10 community layers, retroactive purge + re-materialization,
           signal revocation, agent capability boundaries, P1 feedback loop,
           reason labels, instrumented metrics
  theirs — M11/M12 cluster mode (tidal-net gRPC transport, tidal-server
           cluster/scatter-gather, tidal-stress), multi-vector preference,
           ANN candidate-gen, warm-tier day buckets, keyed signal snapshots

Notable semantic resolutions:

* storage::keys::Tag — both sides allocated 0x0E..0x11 for different
  records. Kept theirs' 0x0E..0x1A (shipped on-disk format) and renumbered
  ours to 0x1B..0x1E (CommunityMembership/Revocation/PurgeManifest/
  CommunityLeave); Tag::ALL grown to 30 so the contiguity drift guard holds.

* ranking executor — took theirs' rewrite (SignalReadPlan pre-pass, keyed
  SignalKey snapshots, Result-returning reads, finalize()) and re-applied
  ours' M10 read suppression at the chokepoints it introduced:
  single_signal_score, score_hot/trending/controversial,
  CreatorEngagementRate, and the Stage-4 boost loop.

* signals::warm — theirs' day-bucket/read-time-rotation rewrite, with ours'
  subtract_bucket and Clone extended to the new day tier; ours' test split
  kept (warm/tests.rs, warm/proptests.rs) carrying theirs' updated bodies.

* db::signals — kept ours' contribution-logging try_cohort_attribution in
  signal_dispatch.rs and theirs' event-time try_update_preference_vector;
  dropped the superseded duplicates.

* db::mod / from_parts — theirs' constructors, with ours' purge/
  re-materialization/revocation/community/skip-counter fields and restart
  rebuilds; from_parts kept in its own file per the 600-line guideline.

* schema::validation::builders — ours' module split with theirs' expanded
  tests; policy validation runs both sides' checks (read-signal lists +
  profile overrides, then the zero-duration limit guard).

* feedback Unhide no longer writes a -1.0 "hide" signal: theirs' engine
  rejects negative weights (spec §8). Reverses index state only, matching
  every other undo action.

* SessionState::new is now the single construction path (gains
  overrides_rejected/default_profile); AuditEntry gains kind on the
  deserialize path, inferred from the accepted flag as before.

* Removed tidal/src/replication/tcp_transport.rs and its test: never
  declared in replication/mod.rs on either branch, so it had never
  compiled and nothing referenced it. Superseded by tidal-net's
  GrpcTransport.

Verified: cargo clippy -p tidaldb (lib) clean; --all-targets compiles for
tidaldb/tidal-net/tidal-server/tidal-stress; 2094/2094 lib tests and the
integration suite pass except m8p3_reconcile_production's two CRDT-count
assertions, which fail identically on MERGE_HEAD (pre-existing).
tidalctl cannot build locally: its aws-sdk deps need rustc 1.91.1, local
toolchain is 1.91.0.
2026-08-03 02:16:04 -06:00

165 lines
4.8 KiB
Rust

#![allow(clippy::unwrap_used)]
//! Benchmarks for the session layer: signal writes, snapshot reads,
//! and retrieve queries with active session context.
use std::{collections::HashMap, time::Duration};
use criterion::{Criterion, black_box, criterion_group, criterion_main};
use tidaldb::{
TidalDb,
query::retrieve::{ProfileRef, RetrieveBuilder},
schema::{AgentPolicy, DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window},
};
fn session_schema() -> tidaldb::schema::Schema {
let mut builder = SchemaBuilder::new();
let _ = builder
.signal(
"reward",
EntityKind::Item,
DecaySpec::Exponential {
half_life: Duration::from_secs(7 * 24 * 3600),
},
)
.windows(&[Window::OneHour, Window::TwentyFourHours])
.velocity(false)
.add();
let _ = builder.session_policy(
"bench_policy",
AgentPolicy {
allowed_signals: vec!["reward".to_string()],
denied_signals: vec![],
max_session_duration: Duration::from_secs(3600),
max_signals_per_session: 1_000_000,
..AgentPolicy::default()
},
);
builder.build().unwrap()
}
/// Benchmark: `session_signal()` write throughput.
/// Target: < 200µs per call.
fn bench_session_signal(c: &mut Criterion) {
let db = TidalDb::builder()
.ephemeral()
.with_schema(session_schema())
.open()
.unwrap();
let mut meta = HashMap::new();
meta.insert("title".to_string(), "bench-item".to_string());
db.write_item_with_metadata(EntityId::new(1), &meta)
.unwrap();
let handle = db
.start_session(1, "bench-agent", "bench_policy", HashMap::new())
.unwrap();
let entity = EntityId::new(1);
let ts = Timestamp::now();
c.bench_function("session_signal", |b| {
b.iter(|| {
db.session_signal(
black_box(&handle),
black_box("reward"),
black_box(entity),
black_box(1.0_f64),
black_box(ts),
black_box(None),
)
.unwrap();
});
});
}
/// Benchmark: `session_snapshot()` on an active session with 100 signals.
/// Target: < 50µs per call.
fn bench_session_snapshot(c: &mut Criterion) {
let db = TidalDb::builder()
.ephemeral()
.with_schema(session_schema())
.open()
.unwrap();
let mut meta = HashMap::new();
meta.insert("title".to_string(), "bench-item".to_string());
db.write_item_with_metadata(EntityId::new(1), &meta)
.unwrap();
let handle = db
.start_session(2, "bench-agent", "bench_policy", HashMap::new())
.unwrap();
let session_id = handle.id;
let entity = EntityId::new(1);
let ts = Timestamp::now();
// Pre-load 100 signals.
for _ in 0..100 {
db.session_signal(&handle, "reward", entity, 1.0, ts, None)
.unwrap();
}
c.bench_function("session_snapshot_100_signals", |b| {
b.iter(|| db.session_snapshot(black_box(session_id)).unwrap());
});
}
/// Benchmark: `retrieve()` with FOR SESSION vs without, over 1K items.
/// Measures the overhead of session context in ranking.
/// Target: < 5ms overhead vs without session.
fn bench_retrieve_with_session(c: &mut Criterion) {
let db = TidalDb::builder()
.ephemeral()
.with_schema(session_schema())
.open()
.unwrap();
// Write 1K items.
for i in 1u64..=1000 {
let mut meta = HashMap::new();
meta.insert("title".to_string(), format!("item-{i}"));
db.write_item_with_metadata(EntityId::new(i), &meta)
.unwrap();
}
let handle = db
.start_session(3, "bench-agent", "bench_policy", HashMap::new())
.unwrap();
let session_id = handle.id;
let ts = Timestamp::now();
// Signal 10 entities to create a non-trivial session context.
for i in 1u64..=10 {
db.session_signal(&handle, "reward", EntityId::new(i), 1.0, ts, None)
.unwrap();
}
let query_with = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("hot"))
.limit(20)
.for_session(session_id)
.build()
.unwrap();
let query_without = RetrieveBuilder::new(EntityKind::Item, ProfileRef::new("hot"))
.limit(20)
.build()
.unwrap();
let mut group = c.benchmark_group("retrieve_1k_items");
group.bench_function("without_session", |b| {
b.iter(|| db.retrieve(black_box(&query_without)).unwrap());
});
group.bench_function("with_session", |b| {
b.iter(|| db.retrieve(black_box(&query_with)).unwrap());
});
group.finish();
}
criterion_group!(
benches,
bench_session_signal,
bench_session_snapshot,
bench_retrieve_with_session
);
criterion_main!(benches);