1use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct LatencyHistogram {
13 buckets: Vec<u64>,
15 counts: Vec<u64>,
17 total_samples: u64,
19 min_latency_us: u64,
21 max_latency_us: u64,
23 sum_latency_us: u64,
25}
26
27impl LatencyHistogram {
28 pub fn new() -> Self {
32 Self::with_buckets(vec![10, 50, 100, 500, 1000, 5000, 10000, 50000])
33 }
34
35 pub fn with_buckets(mut buckets: Vec<u64>) -> Self {
37 buckets.sort_unstable();
38 let counts = vec![0; buckets.len() + 1];
39
40 Self {
41 buckets,
42 counts,
43 total_samples: 0,
44 min_latency_us: u64::MAX,
45 max_latency_us: 0,
46 sum_latency_us: 0,
47 }
48 }
49
50 pub fn record(&mut self, latency: Duration) {
52 let latency_us = latency.as_micros() as u64;
53
54 self.min_latency_us = self.min_latency_us.min(latency_us);
56 self.max_latency_us = self.max_latency_us.max(latency_us);
57
58 self.sum_latency_us += latency_us;
60 self.total_samples += 1;
61
62 let bucket_idx = self
64 .buckets
65 .iter()
66 .position(|&b| latency_us < b)
67 .unwrap_or(self.buckets.len());
68 self.counts[bucket_idx] += 1;
69 }
70
71 pub fn avg(&self) -> Duration {
73 if self.total_samples == 0 {
74 Duration::from_micros(0)
75 } else {
76 Duration::from_micros(self.sum_latency_us / self.total_samples)
77 }
78 }
79
80 pub fn min(&self) -> Duration {
82 if self.min_latency_us == u64::MAX {
83 Duration::from_micros(0)
84 } else {
85 Duration::from_micros(self.min_latency_us)
86 }
87 }
88
89 pub fn max(&self) -> Duration {
91 Duration::from_micros(self.max_latency_us)
92 }
93
94 pub fn percentile(&self, p: f64) -> Duration {
98 if self.total_samples == 0 {
99 return Duration::from_micros(0);
100 }
101
102 let target_count = (self.total_samples as f64 * p) as u64;
103 let mut cumulative = 0u64;
104
105 for (idx, &count) in self.counts.iter().enumerate() {
106 cumulative += count;
107 if cumulative >= target_count {
108 let latency_us = if idx < self.buckets.len() {
110 self.buckets[idx]
111 } else {
112 self.max_latency_us
113 };
114 return Duration::from_micros(latency_us);
115 }
116 }
117
118 Duration::from_micros(self.max_latency_us)
119 }
120
121 pub fn p50(&self) -> Duration {
123 self.percentile(0.50)
124 }
125
126 pub fn p90(&self) -> Duration {
128 self.percentile(0.90)
129 }
130
131 pub fn p95(&self) -> Duration {
133 self.percentile(0.95)
134 }
135
136 pub fn p99(&self) -> Duration {
138 self.percentile(0.99)
139 }
140
141 pub fn p999(&self) -> Duration {
143 self.percentile(0.999)
144 }
145
146 pub fn count(&self) -> u64 {
148 self.total_samples
149 }
150
151 pub fn summary(&self) -> String {
153 format!(
154 "Samples: {}, Min: {:?}, Max: {:?}, Avg: {:?}, P50: {:?}, P90: {:?}, P95: {:?}, P99: {:?}",
155 self.total_samples,
156 self.min(),
157 self.max(),
158 self.avg(),
159 self.p50(),
160 self.p90(),
161 self.p95(),
162 self.p99()
163 )
164 }
165}
166
167impl Default for LatencyHistogram {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173#[derive(Debug, Clone, Default)]
175pub struct PerformanceProfiler {
176 histograms: BTreeMap<String, LatencyHistogram>,
178}
179
180impl PerformanceProfiler {
181 pub fn new() -> Self {
183 Self {
184 histograms: BTreeMap::new(),
185 }
186 }
187
188 pub fn record(&mut self, operation: &str, latency: Duration) {
190 self.histograms
191 .entry(operation.to_string())
192 .or_default()
193 .record(latency);
194 }
195
196 pub fn get_histogram(&self, operation: &str) -> Option<&LatencyHistogram> {
198 self.histograms.get(operation)
199 }
200
201 pub fn histograms(&self) -> &BTreeMap<String, LatencyHistogram> {
203 &self.histograms
204 }
205
206 pub fn report(&self) -> String {
208 let mut report = String::from("=== Performance Profile ===\n\n");
209
210 for (operation, histogram) in &self.histograms {
211 report.push_str(&format!("Operation: {operation}\n"));
212 report.push_str(&format!(" {}\n\n", histogram.summary()));
213 }
214
215 report
216 }
217
218 pub fn reset(&mut self) {
220 self.histograms.clear();
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct ThroughputTracker {
227 operation: String,
229 total_ops: u64,
231 total_bytes: u64,
233 start_time: std::time::Instant,
235}
236
237impl ThroughputTracker {
238 pub fn new(operation: String) -> Self {
240 Self {
241 operation,
242 total_ops: 0,
243 total_bytes: 0,
244 start_time: std::time::Instant::now(),
245 }
246 }
247
248 pub fn record_op(&mut self) {
250 self.total_ops += 1;
251 }
252
253 pub fn record_bytes(&mut self, bytes: u64) {
255 self.total_bytes += bytes;
256 }
257
258 pub fn ops_per_second(&self) -> f64 {
260 let elapsed = self.start_time.elapsed().as_secs_f64();
261 if elapsed > 0.0 {
262 self.total_ops as f64 / elapsed
263 } else {
264 0.0
265 }
266 }
267
268 pub fn bytes_per_second(&self) -> f64 {
270 let elapsed = self.start_time.elapsed().as_secs_f64();
271 if elapsed > 0.0 {
272 self.total_bytes as f64 / elapsed
273 } else {
274 0.0
275 }
276 }
277
278 pub fn megabytes_per_second(&self) -> f64 {
280 self.bytes_per_second() / (1024.0 * 1024.0)
281 }
282
283 pub fn elapsed(&self) -> Duration {
285 self.start_time.elapsed()
286 }
287
288 pub fn summary(&self) -> String {
290 format!(
291 "{}: {} ops in {:?} ({:.2} ops/s, {:.2} MB/s)",
292 self.operation,
293 self.total_ops,
294 self.elapsed(),
295 self.ops_per_second(),
296 self.megabytes_per_second()
297 )
298 }
299}
300
301#[derive(Debug, Clone, Default)]
303pub struct BatchProfiler {
304 total_batches: u64,
306 total_items: u64,
308 batch_sizes: LatencyHistogram,
310 batch_latencies: LatencyHistogram,
312}
313
314impl BatchProfiler {
315 pub fn new() -> Self {
317 Self {
318 total_batches: 0,
319 total_items: 0,
320 batch_sizes: LatencyHistogram::with_buckets(vec![1, 10, 50, 100, 500, 1000]),
321 batch_latencies: LatencyHistogram::new(),
322 }
323 }
324
325 pub fn record_batch(&mut self, batch_size: usize, latency: Duration) {
327 self.total_batches += 1;
328 self.total_items += batch_size as u64;
329
330 self.batch_sizes
332 .record(Duration::from_micros(batch_size as u64));
333 self.batch_latencies.record(latency);
334 }
335
336 pub fn avg_batch_size(&self) -> f64 {
338 if self.total_batches == 0 {
339 0.0
340 } else {
341 self.total_items as f64 / self.total_batches as f64
342 }
343 }
344
345 pub fn avg_latency_per_item(&self) -> Duration {
347 if self.total_items == 0 {
348 Duration::from_micros(0)
349 } else {
350 let total_latency_us = self.batch_latencies.sum_latency_us;
351 Duration::from_micros(total_latency_us / self.total_items)
352 }
353 }
354
355 pub fn summary(&self) -> String {
357 format!(
358 "Batches: {}, Items: {}, Avg Batch Size: {:.2}, Avg Latency: {:?}, Avg per Item: {:?}",
359 self.total_batches,
360 self.total_items,
361 self.avg_batch_size(),
362 self.batch_latencies.avg(),
363 self.avg_latency_per_item()
364 )
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_latency_histogram_basic() {
374 let mut hist = LatencyHistogram::new();
375
376 hist.record(Duration::from_micros(50));
377 hist.record(Duration::from_micros(100));
378 hist.record(Duration::from_micros(150));
379
380 assert_eq!(hist.count(), 3);
381 assert!(hist.min() <= Duration::from_micros(50));
382 assert!(hist.max() >= Duration::from_micros(150));
383 }
384
385 #[test]
386 fn test_latency_histogram_percentiles() {
387 let mut hist = LatencyHistogram::new();
388
389 for i in 1..=100 {
391 hist.record(Duration::from_micros(i * 100));
392 }
393
394 assert_eq!(hist.count(), 100);
395
396 let p50 = hist.p50();
397 let p90 = hist.p90();
398 let p99 = hist.p99();
399
400 assert!(p50 <= p90);
402 assert!(p90 <= p99);
403 }
404
405 #[test]
406 fn test_performance_profiler() {
407 let mut profiler = PerformanceProfiler::new();
408
409 profiler.record("put", Duration::from_micros(100));
410 profiler.record("put", Duration::from_micros(150));
411 profiler.record("get", Duration::from_micros(50));
412
413 assert!(profiler.get_histogram("put").is_some());
414 assert!(profiler.get_histogram("get").is_some());
415 assert!(profiler.get_histogram("delete").is_none());
416
417 let put_hist = profiler.get_histogram("put").unwrap();
418 assert_eq!(put_hist.count(), 2);
419
420 let report = profiler.report();
421 assert!(report.contains("put"));
422 assert!(report.contains("get"));
423 }
424
425 #[test]
426 fn test_throughput_tracker() {
427 let mut tracker = ThroughputTracker::new("test".to_string());
428
429 for _ in 0..100 {
430 tracker.record_op();
431 tracker.record_bytes(1024);
432 }
433
434 assert_eq!(tracker.total_ops, 100);
435 assert_eq!(tracker.total_bytes, 102400);
436 assert!(tracker.ops_per_second() > 0.0);
437
438 let summary = tracker.summary();
439 assert!(summary.contains("test"));
440 assert!(summary.contains("100 ops"));
441 }
442
443 #[test]
444 fn test_batch_profiler() {
445 let mut profiler = BatchProfiler::new();
446
447 profiler.record_batch(10, Duration::from_micros(1000));
448 profiler.record_batch(20, Duration::from_micros(2000));
449 profiler.record_batch(30, Duration::from_micros(3000));
450
451 assert_eq!(profiler.total_batches, 3);
452 assert_eq!(profiler.total_items, 60);
453 assert_eq!(profiler.avg_batch_size(), 20.0);
454
455 let summary = profiler.summary();
456 assert!(summary.contains("Batches: 3"));
457 assert!(summary.contains("Items: 60"));
458 }
459}