1use std::cell::RefCell;
10use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
11use std::sync::{Arc, Mutex, OnceLock};
12use std::time::{Duration, Instant};
13
14use serde::Serialize;
15
16const MAX_REGISTERED_THREADS: usize = 128;
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20#[repr(usize)]
21pub enum PerformanceStageV2 {
22 SearchParse = 0,
23 SearchAdmission = 1,
24 SearchMask = 2,
25 SearchExecute = 3,
26 SearchMaterialize = 4,
27 SearchEncode = 5,
28 IngestParseValidate = 6,
29 IngestWalEncode = 7,
30 ObjectFileSync = 8,
31 ObjectDirectorySync = 9,
32 ObjectPutReadback = 10,
33 RaftAppend = 11,
34 CommittedIndexPersistence = 12,
35 TantivyCommit = 13,
36 TantivyReload = 14,
37 ProjectionRebuild = 15,
38 VisibilityProof = 16,
39 IngestResponseMaterialize = 17,
40}
41
42impl PerformanceStageV2 {
43 pub const ALL: [Self; 18] = [
45 Self::SearchParse,
46 Self::SearchAdmission,
47 Self::SearchMask,
48 Self::SearchExecute,
49 Self::SearchMaterialize,
50 Self::SearchEncode,
51 Self::IngestParseValidate,
52 Self::IngestWalEncode,
53 Self::ObjectFileSync,
54 Self::ObjectDirectorySync,
55 Self::ObjectPutReadback,
56 Self::RaftAppend,
57 Self::CommittedIndexPersistence,
58 Self::TantivyCommit,
59 Self::TantivyReload,
60 Self::ProjectionRebuild,
61 Self::VisibilityProof,
62 Self::IngestResponseMaterialize,
63 ];
64
65 #[must_use]
67 pub const fn as_str(self) -> &'static str {
68 match self {
69 Self::SearchParse => "search_parse",
70 Self::SearchAdmission => "search_admission",
71 Self::SearchMask => "search_mask",
72 Self::SearchExecute => "search_execute",
73 Self::SearchMaterialize => "search_materialize",
74 Self::SearchEncode => "search_encode",
75 Self::IngestParseValidate => "ingest_parse_validate",
76 Self::IngestWalEncode => "ingest_wal_encode",
77 Self::ObjectFileSync => "object_file_sync",
78 Self::ObjectDirectorySync => "object_directory_sync",
79 Self::ObjectPutReadback => "object_put_readback",
80 Self::RaftAppend => "raft_append",
81 Self::CommittedIndexPersistence => "committed_index_persistence",
82 Self::TantivyCommit => "tantivy_commit",
83 Self::TantivyReload => "tantivy_reload",
84 Self::ProjectionRebuild => "projection_rebuild",
85 Self::VisibilityProof => "visibility_proof",
86 Self::IngestResponseMaterialize => "ingest_response_materialize",
87 }
88 }
89
90 const fn index(self) -> usize {
91 self as usize
92 }
93}
94
95#[derive(Debug)]
96struct ThreadCounters {
97 observations: [AtomicU64; PerformanceStageV2::ALL.len()],
98 elapsed_nanos: [AtomicU64; PerformanceStageV2::ALL.len()],
99}
100
101impl ThreadCounters {
102 fn new() -> Self {
103 Self {
104 observations: std::array::from_fn(|_| AtomicU64::new(0)),
105 elapsed_nanos: std::array::from_fn(|_| AtomicU64::new(0)),
106 }
107 }
108
109 fn record(&self, stage: PerformanceStageV2, elapsed: Duration) {
110 let index = stage.index();
111 self.observations[index].fetch_add(1, Ordering::Relaxed);
112 let elapsed = u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX);
113 let _ = self.elapsed_nanos[index].fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
114 Some(current.saturating_add(elapsed))
115 });
116 }
117}
118
119#[derive(Debug)]
120struct Registry {
121 threads: Mutex<Vec<Arc<ThreadCounters>>>,
122 overflow: Arc<ThreadCounters>,
123 overflow_assignments: AtomicU64,
124 observation_enabled: AtomicBool,
125}
126
127impl Registry {
128 fn new() -> Self {
129 Self {
130 threads: Mutex::new(Vec::new()),
131 overflow: Arc::new(ThreadCounters::new()),
132 overflow_assignments: AtomicU64::new(0),
133 observation_enabled: AtomicBool::new(true),
134 }
135 }
136
137 fn register_thread(&self) -> Arc<ThreadCounters> {
138 let mut threads = match self.threads.lock() {
139 Ok(threads) => threads,
140 Err(poisoned) => poisoned.into_inner(),
141 };
142 if threads.len() >= MAX_REGISTERED_THREADS {
143 self.overflow_assignments.fetch_add(1, Ordering::Relaxed);
144 return Arc::clone(&self.overflow);
145 }
146 let counters = Arc::new(ThreadCounters::new());
147 threads.push(Arc::clone(&counters));
148 counters
149 }
150
151 fn registered_threads(&self) -> Vec<Arc<ThreadCounters>> {
152 let threads = match self.threads.lock() {
153 Ok(threads) => threads,
154 Err(poisoned) => poisoned.into_inner(),
155 };
156 threads.clone()
157 }
158}
159
160static REGISTRY: OnceLock<Registry> = OnceLock::new();
161
162fn registry() -> &'static Registry {
163 REGISTRY.get_or_init(Registry::new)
164}
165
166thread_local! {
167 static THREAD_COUNTERS: RefCell<Option<Arc<ThreadCounters>>> = const { RefCell::new(None) };
168}
169
170fn thread_counters() -> Arc<ThreadCounters> {
171 THREAD_COUNTERS.with(|slot| {
172 if let Ok(slot) = slot.try_borrow()
173 && let Some(counters) = slot.as_ref()
174 {
175 return Arc::clone(counters);
176 }
177 let counters = registry().register_thread();
178 if let Ok(mut slot) = slot.try_borrow_mut() {
179 *slot = Some(Arc::clone(&counters));
180 }
181 counters
182 })
183}
184
185#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
187pub struct PerformanceStageReadingV2 {
188 pub stage: &'static str,
189 pub observations: u64,
190 pub elapsed_nanos: u64,
191}
192
193#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
195pub struct BoundedStageSnapshotV2 {
196 pub format_version: u32,
197 pub readings: Vec<PerformanceStageReadingV2>,
198 pub registered_threads: u64,
199 pub overflow_assignments: u64,
200}
201
202impl BoundedStageSnapshotV2 {
203 #[must_use]
205 pub fn since(&self, earlier: &Self) -> Self {
206 let readings = self
207 .readings
208 .iter()
209 .zip(&earlier.readings)
210 .map(|(current, prior)| PerformanceStageReadingV2 {
211 stage: current.stage,
212 observations: current.observations.saturating_sub(prior.observations),
213 elapsed_nanos: current.elapsed_nanos.saturating_sub(prior.elapsed_nanos),
214 })
215 .collect();
216 Self {
217 format_version: 2,
218 readings,
219 registered_threads: self.registered_threads,
220 overflow_assignments: self.overflow_assignments.saturating_sub(earlier.overflow_assignments),
221 }
222 }
223
224 #[must_use]
226 pub fn reading(&self, stage: PerformanceStageV2) -> Option<&PerformanceStageReadingV2> {
227 self.readings.get(stage.index())
228 }
229}
230
231#[must_use]
234pub fn bounded_stage_snapshot_v2() -> BoundedStageSnapshotV2 {
235 let registry = registry();
236 let threads = registry.registered_threads();
237 let mut observations = [0_u64; PerformanceStageV2::ALL.len()];
238 let mut elapsed_nanos = [0_u64; PerformanceStageV2::ALL.len()];
239 for counters in threads.iter().chain(std::iter::once(®istry.overflow)) {
240 for stage in PerformanceStageV2::ALL {
241 let index = stage.index();
242 observations[index] =
243 observations[index].saturating_add(counters.observations[index].load(Ordering::Relaxed));
244 elapsed_nanos[index] =
245 elapsed_nanos[index].saturating_add(counters.elapsed_nanos[index].load(Ordering::Relaxed));
246 }
247 }
248 BoundedStageSnapshotV2 {
249 format_version: 2,
250 readings: PerformanceStageV2::ALL
251 .into_iter()
252 .map(|stage| {
253 let index = stage.index();
254 PerformanceStageReadingV2 {
255 stage: stage.as_str(),
256 observations: observations[index],
257 elapsed_nanos: elapsed_nanos[index],
258 }
259 })
260 .collect(),
261 registered_threads: u64::try_from(threads.len()).unwrap_or(u64::MAX),
262 overflow_assignments: registry.overflow_assignments.load(Ordering::Relaxed),
263 }
264}
265
266pub fn record_performance_stage_v2(stage: PerformanceStageV2, elapsed: Duration) {
268 if registry().observation_enabled.load(Ordering::Relaxed) {
269 thread_counters().record(stage, elapsed);
270 }
271}
272
273pub struct PerformanceObservationV2 {
275 stage: PerformanceStageV2,
276 started: Option<Instant>,
277}
278
279impl PerformanceObservationV2 {
280 #[must_use]
281 pub fn start(stage: PerformanceStageV2) -> Self {
282 let started = registry()
283 .observation_enabled
284 .load(Ordering::Relaxed)
285 .then(Instant::now);
286 Self { stage, started }
287 }
288}
289
290impl Drop for PerformanceObservationV2 {
291 fn drop(&mut self) {
292 if let Some(started) = self.started {
293 record_performance_stage_v2(self.stage, started.elapsed());
294 }
295 }
296}
297
298#[cfg(test)]
299fn set_observation_enabled_for_test(enabled: bool) {
300 registry().observation_enabled.store(enabled, Ordering::SeqCst);
301}
302
303#[cfg(test)]
304mod tests {
305 use std::hint::black_box;
306 use std::sync::MutexGuard;
307
308 use super::*;
309
310 static PERFORMANCE_TEST_LOCK: Mutex<()> = Mutex::new(());
311
312 struct RestoreObservation;
313
314 impl Drop for RestoreObservation {
315 fn drop(&mut self) {
316 set_observation_enabled_for_test(true);
317 }
318 }
319
320 fn request_sized_work() {
321 let mut value = 0x9e37_79b9_7f4a_7c15_u64;
322 for iteration in 0..250_000_u64 {
323 value = value.rotate_left(7) ^ iteration.wrapping_mul(0xd6e8_feb8_6659_fd93);
324 black_box(value);
325 }
326 }
327
328 fn test_lock() -> MutexGuard<'static, ()> {
329 match PERFORMANCE_TEST_LOCK.lock() {
330 Ok(guard) => guard,
331 Err(poisoned) => poisoned.into_inner(),
332 }
333 }
334
335 fn measure_request_window(observed: bool) -> u64 {
336 set_observation_enabled_for_test(observed);
337 let started = Instant::now();
338 for _ in 0..64 {
339 let _stage = PerformanceObservationV2::start(PerformanceStageV2::SearchExecute);
340 request_sized_work();
341 }
342 u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX)
343 }
344
345 fn percentile(values: &mut [u64], numerator: usize, denominator: usize) -> u64 {
346 values.sort_unstable();
347 let index = values
348 .len()
349 .saturating_sub(1)
350 .saturating_mul(numerator)
351 .div_ceil(denominator);
352 values[index.min(values.len().saturating_sub(1))]
353 }
354
355 #[test]
356 fn performance_v2_snapshot_is_bounded_monotonic_and_stage_exact() {
357 let _guard = test_lock();
358 let before = bounded_stage_snapshot_v2();
359 record_performance_stage_v2(PerformanceStageV2::ObjectDirectorySync, Duration::from_nanos(17));
360 let after = bounded_stage_snapshot_v2();
361 let delta = after.since(&before);
362 let reading = delta
363 .reading(PerformanceStageV2::ObjectDirectorySync)
364 .map(|reading| (reading.observations, reading.elapsed_nanos));
365 assert_eq!(reading, Some((1, 17)));
366 assert_eq!(delta.readings.len(), PerformanceStageV2::ALL.len());
367 assert!(after.registered_threads <= MAX_REGISTERED_THREADS as u64);
368 }
369
370 #[test]
371 fn performance_v2_snapshot_retains_completed_worker_observations() {
372 let _guard = test_lock();
373 let before = bounded_stage_snapshot_v2();
374 let worker = std::thread::spawn(|| {
375 record_performance_stage_v2(PerformanceStageV2::VisibilityProof, Duration::from_nanos(31));
376 });
377 assert!(worker.join().is_ok());
378
379 let delta = bounded_stage_snapshot_v2().since(&before);
380 let reading = delta
381 .reading(PerformanceStageV2::VisibilityProof)
382 .map(|reading| (reading.observations, reading.elapsed_nanos));
383 assert_eq!(reading, Some((1, 31)));
384 }
385
386 #[test]
387 fn performance_v2_no_observation_control_stays_within_three_percent() {
388 let _guard = test_lock();
389 let _restore = RestoreObservation;
390 let mut observed = Vec::with_capacity(41);
391 let mut unobserved = Vec::with_capacity(41);
392 request_sized_work();
393 for trial in 0..41 {
394 if trial % 2 == 0 {
395 observed.push(measure_request_window(true));
396 unobserved.push(measure_request_window(false));
397 } else {
398 unobserved.push(measure_request_window(false));
399 observed.push(measure_request_window(true));
400 }
401 }
402 let observed_p50 = percentile(&mut observed.clone(), 50, 100);
403 let observed_p95 = percentile(&mut observed, 95, 100);
404 let unobserved_p50 = percentile(&mut unobserved.clone(), 50, 100);
405 let unobserved_p95 = percentile(&mut unobserved, 95, 100);
406 assert!(
407 observed_p50 <= unobserved_p50.saturating_mul(103).div_ceil(100),
408 "observed_p50={observed_p50} unobserved_p50={unobserved_p50}"
409 );
410 assert!(
411 observed_p95 <= unobserved_p95.saturating_mul(103).div_ceil(100),
412 "observed_p95={observed_p95} unobserved_p95={unobserved_p95}"
413 );
414 }
415}