1use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29use std::time::{Duration, SystemTime};
30
31#[derive(Debug, Clone)]
33pub struct HeatmapConfig {
34 pub time_bucket_size: Duration,
36 pub performance_buckets: Vec<u64>,
38 pub max_data_points: usize,
40 pub percentiles: Vec<u8>,
42}
43
44impl HeatmapConfig {
45 pub fn new() -> Self {
47 Self {
48 time_bucket_size: Duration::from_secs(3600), performance_buckets: vec![10, 50, 100, 250, 500, 1000, 2500, 5000],
50 max_data_points: 10000,
51 percentiles: vec![50, 95, 99],
52 }
53 }
54
55 pub fn with_time_bucket_size(mut self, duration: Duration) -> Self {
57 self.time_bucket_size = duration;
58 self
59 }
60
61 pub fn with_performance_buckets(mut self, buckets: Vec<u64>) -> Self {
63 self.performance_buckets = buckets;
64 self
65 }
66
67 pub fn with_max_data_points(mut self, max: usize) -> Self {
69 self.max_data_points = max;
70 self
71 }
72}
73
74impl Default for HeatmapConfig {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct PerformanceDataPoint {
83 pub timestamp: SystemTime,
85 pub operation: String,
87 pub duration_ms: u64,
89 pub field: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct HeatmapCell {
96 pub time_bucket: u64,
98 pub performance_bucket: usize,
100 pub count: usize,
102 pub avg_duration_ms: f64,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Heatmap {
109 pub cells: Vec<HeatmapCell>,
111 pub time_bucket_size: u64,
113 pub performance_buckets: Vec<u64>,
115 pub start_time: SystemTime,
117 pub end_time: SystemTime,
119}
120
121impl Heatmap {
122 pub fn max_count(&self) -> usize {
124 self.cells.iter().map(|c| c.count).max().unwrap_or(0)
125 }
126
127 pub fn min_count(&self) -> usize {
129 self.cells.iter().map(|c| c.count).min().unwrap_or(0)
130 }
131
132 pub fn to_json(&self) -> Result<String, String> {
134 serde_json::to_string_pretty(self).map_err(|e| format!("JSON serialization failed: {}", e))
135 }
136
137 pub fn to_html(&self) -> String {
139 let mut html = String::from("<html><head><style>\n");
140 html.push_str(".heatmap { display: grid; gap: 2px; }\n");
141 html.push_str(".cell { padding: 10px; text-align: center; border: 1px solid #ddd; }\n");
142 html.push_str(".hot { background-color: #ff0000; color: white; }\n");
143 html.push_str(".warm { background-color: #ffaa00; }\n");
144 html.push_str(".cool { background-color: #00ff00; }\n");
145 html.push_str(".cold { background-color: #e0e0e0; }\n");
146 html.push_str("</style></head><body>\n");
147 html.push_str("<h1>Performance Heatmap</h1>\n");
148 html.push_str("<div class='heatmap'>\n");
149
150 let max_count = self.max_count() as f64;
151
152 for cell in &self.cells {
153 let intensity = if max_count > 0.0 {
154 cell.count as f64 / max_count
155 } else {
156 0.0
157 };
158
159 let class = if intensity > 0.75 {
160 "hot"
161 } else if intensity > 0.5 {
162 "warm"
163 } else if intensity > 0.25 {
164 "cool"
165 } else {
166 "cold"
167 };
168
169 html.push_str(&format!(
170 "<div class='cell {}'>Count: {}<br>Avg: {:.1}ms</div>\n",
171 class, cell.count, cell.avg_duration_ms
172 ));
173 }
174
175 html.push_str("</div></body></html>");
176 html
177 }
178
179 pub fn to_ascii(&self) -> String {
181 let max_count = self.max_count();
182 let mut output = String::from("Performance Heatmap:\n");
183
184 for cell in &self.cells {
185 let intensity = (cell.count * 10)
186 .checked_div(max_count)
187 .unwrap_or(0)
188 .min(10);
189
190 let bar = "█".repeat(intensity);
191 output.push_str(&format!(
192 "Bucket {}: {} ({} queries, avg {:.1}ms)\n",
193 cell.performance_bucket, bar, cell.count, cell.avg_duration_ms
194 ));
195 }
196
197 output
198 }
199
200 pub fn to_csv(&self) -> String {
202 let mut csv = String::from("time_bucket,performance_bucket,count,avg_duration_ms\n");
203
204 for cell in &self.cells {
205 csv.push_str(&format!(
206 "{},{},{},{:.2}\n",
207 cell.time_bucket, cell.performance_bucket, cell.count, cell.avg_duration_ms
208 ));
209 }
210
211 csv
212 }
213}
214
215pub struct HeatmapGenerator {
217 config: HeatmapConfig,
218 data_points: Vec<PerformanceDataPoint>,
219}
220
221impl HeatmapGenerator {
222 pub fn new(config: HeatmapConfig) -> Self {
224 Self {
225 config,
226 data_points: Vec::new(),
227 }
228 }
229
230 pub fn record_query(&mut self, operation: impl Into<String>, duration_ms: u64) {
232 self.record_query_at(operation, duration_ms, SystemTime::now());
233 }
234
235 pub fn record_query_at(
237 &mut self,
238 operation: impl Into<String>,
239 duration_ms: u64,
240 timestamp: SystemTime,
241 ) {
242 self.data_points.push(PerformanceDataPoint {
243 timestamp,
244 operation: operation.into(),
245 duration_ms,
246 field: None,
247 });
248
249 if self.data_points.len() > self.config.max_data_points {
251 let excess = self.data_points.len() - self.config.max_data_points;
252 self.data_points.drain(0..excess);
253 }
254 }
255
256 pub fn record_field(
258 &mut self,
259 operation: impl Into<String>,
260 field: impl Into<String>,
261 duration_ms: u64,
262 ) {
263 self.data_points.push(PerformanceDataPoint {
264 timestamp: SystemTime::now(),
265 operation: operation.into(),
266 duration_ms,
267 field: Some(field.into()),
268 });
269
270 if self.data_points.len() > self.config.max_data_points {
271 let excess = self.data_points.len() - self.config.max_data_points;
272 self.data_points.drain(0..excess);
273 }
274 }
275
276 pub fn generate_time_heatmap(&self) -> Result<Heatmap, String> {
278 if self.data_points.is_empty() {
279 return Err("No data points available".to_string());
280 }
281
282 let start_time = self
283 .data_points
284 .iter()
285 .map(|p| p.timestamp)
286 .min()
287 .expect("collection should not be empty");
288 let end_time = self
289 .data_points
290 .iter()
291 .map(|p| p.timestamp)
292 .max()
293 .expect("collection should not be empty");
294
295 let mut cells_map: HashMap<(u64, usize), Vec<u64>> = HashMap::new();
296
297 for point in &self.data_points {
298 let time_bucket = self.get_time_bucket(point.timestamp, start_time);
299 let perf_bucket = self.get_performance_bucket(point.duration_ms);
300
301 cells_map
302 .entry((time_bucket, perf_bucket))
303 .or_default()
304 .push(point.duration_ms);
305 }
306
307 let mut cells: Vec<HeatmapCell> = cells_map
308 .into_iter()
309 .map(|((time_bucket, perf_bucket), durations)| {
310 let count = durations.len();
311 let avg_duration_ms = durations.iter().sum::<u64>() as f64 / count as f64;
312
313 HeatmapCell {
314 time_bucket,
315 performance_bucket: perf_bucket,
316 count,
317 avg_duration_ms,
318 }
319 })
320 .collect();
321
322 cells.sort_by_key(|c| (c.time_bucket, c.performance_bucket));
323
324 Ok(Heatmap {
325 cells,
326 time_bucket_size: self.config.time_bucket_size.as_secs(),
327 performance_buckets: self.config.performance_buckets.clone(),
328 start_time,
329 end_time,
330 })
331 }
332
333 pub fn generate_operation_heatmap(&self) -> HashMap<String, Vec<HeatmapCell>> {
335 let mut operation_maps: HashMap<String, HashMap<usize, Vec<u64>>> = HashMap::new();
336
337 for point in &self.data_points {
338 let perf_bucket = self.get_performance_bucket(point.duration_ms);
339
340 operation_maps
341 .entry(point.operation.clone())
342 .or_default()
343 .entry(perf_bucket)
344 .or_default()
345 .push(point.duration_ms);
346 }
347
348 operation_maps
349 .into_iter()
350 .map(|(operation, buckets)| {
351 let cells: Vec<HeatmapCell> = buckets
352 .into_iter()
353 .map(|(perf_bucket, durations)| {
354 let count = durations.len();
355 let avg_duration_ms = durations.iter().sum::<u64>() as f64 / count as f64;
356
357 HeatmapCell {
358 time_bucket: 0,
359 performance_bucket: perf_bucket,
360 count,
361 avg_duration_ms,
362 }
363 })
364 .collect();
365
366 (operation, cells)
367 })
368 .collect()
369 }
370
371 fn get_time_bucket(&self, timestamp: SystemTime, start_time: SystemTime) -> u64 {
373 let elapsed = timestamp
374 .duration_since(start_time)
375 .unwrap_or_default()
376 .as_secs();
377 elapsed / self.config.time_bucket_size.as_secs()
378 }
379
380 fn get_performance_bucket(&self, duration_ms: u64) -> usize {
382 self.config
383 .performance_buckets
384 .iter()
385 .position(|&threshold| duration_ms <= threshold)
386 .unwrap_or(self.config.performance_buckets.len())
387 }
388
389 pub fn get_percentile_stats(&self) -> HashMap<u8, u64> {
391 if self.data_points.is_empty() {
392 return HashMap::new();
393 }
394
395 let mut durations: Vec<u64> = self.data_points.iter().map(|p| p.duration_ms).collect();
396 durations.sort_unstable();
397
398 let mut stats = HashMap::new();
399 for &percentile in &self.config.percentiles {
400 let index = (durations.len() * percentile as usize / 100).min(durations.len() - 1);
401 stats.insert(percentile, durations[index]);
402 }
403
404 stats
405 }
406
407 pub fn clear(&mut self) {
409 self.data_points.clear();
410 }
411
412 pub fn data_point_count(&self) -> usize {
414 self.data_points.len()
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use std::time::UNIX_EPOCH;
422
423 #[test]
424 fn test_heatmap_config_creation() {
425 let config = HeatmapConfig::new()
426 .with_time_bucket_size(Duration::from_secs(1800))
427 .with_performance_buckets(vec![100, 500, 1000])
428 .with_max_data_points(5000);
429
430 assert_eq!(config.time_bucket_size.as_secs(), 1800);
431 assert_eq!(config.performance_buckets.len(), 3);
432 assert_eq!(config.max_data_points, 5000);
433 }
434
435 #[test]
436 fn test_heatmap_config_default() {
437 let config = HeatmapConfig::default();
438
439 assert_eq!(config.time_bucket_size.as_secs(), 3600);
440 assert!(!config.performance_buckets.is_empty());
441 assert_eq!(config.max_data_points, 10000);
442 }
443
444 #[test]
445 fn test_performance_data_point() {
446 let point = PerformanceDataPoint {
447 timestamp: SystemTime::now(),
448 operation: "GetUser".to_string(),
449 duration_ms: 150,
450 field: Some("user".to_string()),
451 };
452
453 assert_eq!(point.operation, "GetUser");
454 assert_eq!(point.duration_ms, 150);
455 assert_eq!(point.field, Some("user".to_string()));
456 }
457
458 #[test]
459 fn test_heatmap_generator_creation() {
460 let config = HeatmapConfig::default();
461 let generator = HeatmapGenerator::new(config);
462
463 assert_eq!(generator.data_point_count(), 0);
464 }
465
466 #[test]
467 fn test_record_query() {
468 let mut generator = HeatmapGenerator::new(HeatmapConfig::default());
469
470 generator.record_query("GetUser", 100);
471 generator.record_query("GetPosts", 250);
472
473 assert_eq!(generator.data_point_count(), 2);
474 }
475
476 #[test]
477 fn test_record_field() {
478 let mut generator = HeatmapGenerator::new(HeatmapConfig::default());
479
480 generator.record_field("GetUser", "profile", 50);
481
482 assert_eq!(generator.data_point_count(), 1);
483 assert!(generator.data_points[0].field.is_some());
484 }
485
486 #[test]
487 fn test_max_data_points() {
488 let config = HeatmapConfig::default().with_max_data_points(5);
489 let mut generator = HeatmapGenerator::new(config);
490
491 for i in 0..10 {
492 generator.record_query(format!("Query{}", i), 100);
493 }
494
495 assert_eq!(generator.data_point_count(), 5);
496 }
497
498 #[test]
499 fn test_generate_time_heatmap() {
500 let mut generator = HeatmapGenerator::new(HeatmapConfig::default());
501
502 let now = SystemTime::now();
503 generator.record_query_at("Query1", 50, now);
504 generator.record_query_at("Query2", 150, now);
505 generator.record_query_at("Query3", 500, now);
506
507 let heatmap = generator.generate_time_heatmap().expect("should succeed");
508
509 assert!(!heatmap.cells.is_empty());
510 assert_eq!(heatmap.start_time, now);
511 }
512
513 #[test]
514 fn test_generate_time_heatmap_empty() {
515 let generator = HeatmapGenerator::new(HeatmapConfig::default());
516
517 let result = generator.generate_time_heatmap();
518
519 assert!(result.is_err());
520 }
521
522 #[test]
523 fn test_generate_operation_heatmap() {
524 let mut generator = HeatmapGenerator::new(HeatmapConfig::default());
525
526 generator.record_query("GetUser", 100);
527 generator.record_query("GetUser", 150);
528 generator.record_query("GetPosts", 500);
529
530 let op_heatmap = generator.generate_operation_heatmap();
531
532 assert!(op_heatmap.contains_key("GetUser"));
533 assert!(op_heatmap.contains_key("GetPosts"));
534 }
535
536 #[test]
537 fn test_performance_bucket() {
538 let config = HeatmapConfig::default().with_performance_buckets(vec![100, 500, 1000]);
539 let generator = HeatmapGenerator::new(config);
540
541 assert_eq!(generator.get_performance_bucket(50), 0);
542 assert_eq!(generator.get_performance_bucket(250), 1);
543 assert_eq!(generator.get_performance_bucket(750), 2);
544 assert_eq!(generator.get_performance_bucket(2000), 3);
545 }
546
547 #[test]
548 fn test_get_percentile_stats() {
549 let mut generator = HeatmapGenerator::new(HeatmapConfig::default());
550
551 for i in 1..=100 {
552 generator.record_query("Query", i * 10);
553 }
554
555 let stats = generator.get_percentile_stats();
556
557 assert!(stats.contains_key(&50));
558 assert!(stats.contains_key(&95));
559 assert!(stats.contains_key(&99));
560
561 let p50 = stats.get(&50).expect("should succeed");
562 assert!(*p50 >= 400 && *p50 <= 600);
563 }
564
565 #[test]
566 fn test_percentile_stats_empty() {
567 let generator = HeatmapGenerator::new(HeatmapConfig::default());
568
569 let stats = generator.get_percentile_stats();
570
571 assert!(stats.is_empty());
572 }
573
574 #[test]
575 fn test_clear() {
576 let mut generator = HeatmapGenerator::new(HeatmapConfig::default());
577
578 generator.record_query("Query1", 100);
579 generator.record_query("Query2", 200);
580
581 assert_eq!(generator.data_point_count(), 2);
582
583 generator.clear();
584
585 assert_eq!(generator.data_point_count(), 0);
586 }
587
588 #[test]
589 fn test_heatmap_max_min_count() {
590 let cells = vec![
591 HeatmapCell {
592 time_bucket: 0,
593 performance_bucket: 0,
594 count: 10,
595 avg_duration_ms: 100.0,
596 },
597 HeatmapCell {
598 time_bucket: 1,
599 performance_bucket: 1,
600 count: 50,
601 avg_duration_ms: 200.0,
602 },
603 HeatmapCell {
604 time_bucket: 2,
605 performance_bucket: 2,
606 count: 25,
607 avg_duration_ms: 300.0,
608 },
609 ];
610
611 let heatmap = Heatmap {
612 cells,
613 time_bucket_size: 3600,
614 performance_buckets: vec![100, 500, 1000],
615 start_time: SystemTime::now(),
616 end_time: SystemTime::now(),
617 };
618
619 assert_eq!(heatmap.max_count(), 50);
620 assert_eq!(heatmap.min_count(), 10);
621 }
622
623 #[test]
624 fn test_heatmap_to_json() {
625 let heatmap = Heatmap {
626 cells: vec![HeatmapCell {
627 time_bucket: 0,
628 performance_bucket: 0,
629 count: 10,
630 avg_duration_ms: 100.0,
631 }],
632 time_bucket_size: 3600,
633 performance_buckets: vec![100, 500],
634 start_time: SystemTime::UNIX_EPOCH,
635 end_time: SystemTime::UNIX_EPOCH,
636 };
637
638 let json = heatmap.to_json().expect("should succeed");
639
640 assert!(json.contains("cells"));
641 assert!(json.contains("time_bucket_size"));
642 }
643
644 #[test]
645 fn test_heatmap_to_html() {
646 let heatmap = Heatmap {
647 cells: vec![HeatmapCell {
648 time_bucket: 0,
649 performance_bucket: 0,
650 count: 10,
651 avg_duration_ms: 100.0,
652 }],
653 time_bucket_size: 3600,
654 performance_buckets: vec![100, 500],
655 start_time: SystemTime::UNIX_EPOCH,
656 end_time: SystemTime::UNIX_EPOCH,
657 };
658
659 let html = heatmap.to_html();
660
661 assert!(html.contains("<html>"));
662 assert!(html.contains("heatmap"));
663 assert!(html.contains("Count: 10"));
664 }
665
666 #[test]
667 fn test_heatmap_to_ascii() {
668 let heatmap = Heatmap {
669 cells: vec![HeatmapCell {
670 time_bucket: 0,
671 performance_bucket: 0,
672 count: 10,
673 avg_duration_ms: 100.0,
674 }],
675 time_bucket_size: 3600,
676 performance_buckets: vec![100, 500],
677 start_time: SystemTime::UNIX_EPOCH,
678 end_time: SystemTime::UNIX_EPOCH,
679 };
680
681 let ascii = heatmap.to_ascii();
682
683 assert!(ascii.contains("Performance Heatmap"));
684 assert!(ascii.contains("Bucket"));
685 }
686
687 #[test]
688 fn test_heatmap_to_csv() {
689 let heatmap = Heatmap {
690 cells: vec![HeatmapCell {
691 time_bucket: 0,
692 performance_bucket: 0,
693 count: 10,
694 avg_duration_ms: 100.0,
695 }],
696 time_bucket_size: 3600,
697 performance_buckets: vec![100, 500],
698 start_time: SystemTime::UNIX_EPOCH,
699 end_time: SystemTime::UNIX_EPOCH,
700 };
701
702 let csv = heatmap.to_csv();
703
704 assert!(csv.contains("time_bucket,performance_bucket,count,avg_duration_ms"));
705 assert!(csv.contains("0,0,10,100"));
706 }
707
708 #[test]
709 fn test_time_bucket_calculation() {
710 let config = HeatmapConfig::default().with_time_bucket_size(Duration::from_secs(60));
711 let generator = HeatmapGenerator::new(config);
712
713 let start = UNIX_EPOCH;
714 let t1 = start + Duration::from_secs(30);
715 let t2 = start + Duration::from_secs(90);
716 let t3 = start + Duration::from_secs(150);
717
718 assert_eq!(generator.get_time_bucket(t1, start), 0);
719 assert_eq!(generator.get_time_bucket(t2, start), 1);
720 assert_eq!(generator.get_time_bucket(t3, start), 2);
721 }
722}