4.7 KiB
pg1-instrumented-metrics: Design
Architecture Overview
signal() ──→ per-type counter ──→ MetricsState.signal_writes_by_type
──→ user timestamp ──→ MetricsState.user_last_signal
signal_with_context() ──→ records user_id → (ts, entity_id)
retrieve()/search() ──→ reads user_last_signal ──→ staleness histogram
──→ after results ──→ feedback-loop histogram
/metrics ──→ render_prometheus() ──→ per-type counters + percentile gauges
/diagnostics ──→ render_diagnostics() ──→ JSON summary
Component Design
1. LatencyHistogram Extensions (histogram.rs)
Add three methods to the existing LatencyHistogram:
total_count() -> u64: Returns the total observation count (readsself.count).percentile(p: f64) -> Option<u64>: Computes approximate percentile via linear interpolation within cumulative buckets. ReturnsNoneif no observations. Clampspto[0.0, 1.0].render_percentile_gauges(name: &str) -> String: Renders p50, p95, p99 as Prometheus gauge lines.
Percentile algorithm: Given target = ceil(p * total_count), scan cumulative buckets. When a bucket's cumulative count >= target, interpolate linearly between the previous bucket's bound and this bucket's bound based on position within the bucket.
2. Per-Signal-Type Counters (metrics/mod.rs)
New field on MetricsState:
#[cfg(feature = "metrics")]
pub(crate) signal_writes_by_type: DashMap<String, AtomicU64>,
Capped at 256 entries. In signal(), after the existing metrics block, increment signal_writes_by_type[signal_type].
Rendered in Prometheus as:
tidaldb_signal_writes_by_type{signal_type="view"} 1234
tidaldb_signal_writes_by_type{signal_type="like"} 567
3. UserSignalTimestampMap (metrics/mod.rs)
#[cfg(feature = "metrics")]
pub(crate) struct UserSignalTimestampMap {
map: DashMap<u64, (u64, u64)>, // user_id -> (timestamp_ns, entity_id)
cap: usize,
}
Methods: new(cap), record(user_id, ts_ns, entity_id), get(user_id), evict_oldest().
Eviction: When map.len() >= cap, scan 64 random entries, evict the one with the oldest timestamp. This is O(1) amortized.
4. Personalization Staleness (query_ops.rs)
At the top of both retrieve() and search():
#[cfg(feature = "metrics")]
if let Some(uid) = query.for_user {
if let Some((last_signal_ns, _)) = self.metrics.user_last_signal.get(uid) {
let staleness_us = (now_ns - last_signal_ns) / 1_000;
self.metrics.personalization_staleness.observe(staleness_us);
}
}
New histogram field on MetricsState:
#[cfg(feature = "metrics")]
pub(crate) personalization_staleness: LatencyHistogram,
Bounds: [1_000, 10_000, 100_000, 500_000, 1_000_000, 5_000_000, 10_000_000, 30_000_000, 60_000_000] (1ms to 60s in microseconds).
5. Feedback-Loop Latency (query_ops.rs)
After query result assembly, if the user's last signal was within 60 seconds and the signaled entity appears in results:
#[cfg(feature = "metrics")]
if let Some((last_signal_ns, entity_id)) = user_signal_ctx {
let age_secs = (now_ns - last_signal_ns) / 1_000_000_000;
if age_secs <= 60 && result_contains(entity_id) {
let loop_us = (now_ns - last_signal_ns) / 1_000;
self.metrics.feedback_loop_latency.observe(loop_us);
}
}
6. /diagnostics Endpoint (http.rs)
Add /diagnostics route to handle_connection:
#[cfg(feature = "metrics")]
"/diagnostics" => ("200 OK", "application/json", state.render_diagnostics()),
render_diagnostics() produces JSON with: per-type signal counts, latency percentiles, staleness percentiles, feedback-loop percentiles.
File Change Matrix
| File | Change |
|---|---|
tidal/src/db/metrics/histogram.rs |
Add total_count(), percentile(), render_percentile_gauges(), 7 new tests |
tidal/src/db/metrics/mod.rs |
Add UserSignalTimestampMap, 4 new fields, render_diagnostics(), update render_prometheus() |
tidal/src/db/signals.rs |
Per-type counter increment, user timestamp recording |
tidal/src/db/query_ops.rs |
Staleness + feedback-loop recording in retrieve() and search() |
tidal/src/db/http.rs |
/diagnostics route |
Risk Analysis
- Performance: All new instrumentation is behind
#[cfg(feature = "metrics")]. DashMap operations are lock-free shard reads. No measurable impact on hot path. - Memory: UserSignalTimestampMap bounded at 10K entries (~240KB). Per-type counter map bounded at 256 entries (~8KB).
- Correctness: Percentile interpolation is approximate (standard for Prometheus-style histograms). Staleness/feedback-loop are best-effort -- missing entries are silently skipped.