1use std::collections::HashMap;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum AccessType {
15 Read,
17 Write,
19 Prefetch,
21}
22
23#[derive(Clone, Debug)]
25pub struct AccessEvent {
26 pub block_id: u64,
28 pub tick: u64,
30 pub access_type: AccessType,
32}
33
34#[derive(Clone, Debug)]
36pub struct HotspotScore {
37 pub block_id: u64,
39 pub score: f64,
41 pub recent_accesses: u32,
43 pub is_hotspot: bool,
45}
46
47#[derive(Clone, Debug)]
49pub struct DetectorConfig {
50 pub window_ticks: u64,
52 pub hotspot_threshold: f64,
54 pub decay_factor: f64,
56 pub read_weight: f64,
58 pub write_weight: f64,
60 pub prefetch_weight: f64,
62}
63
64impl Default for DetectorConfig {
65 fn default() -> Self {
66 Self {
67 window_ticks: 100,
68 hotspot_threshold: 5.0,
69 decay_factor: 0.95,
70 read_weight: 1.0,
71 write_weight: 1.5,
72 prefetch_weight: 0.5,
73 }
74 }
75}
76
77#[derive(Clone, Debug)]
79pub struct HotspotDetectorStats {
80 pub total_blocks_tracked: usize,
82 pub hotspot_count: usize,
84 pub total_events_recorded: u64,
86}
87
88#[derive(Clone, Debug)]
90pub struct BlockAccessRecord {
91 pub block_id: u64,
93 pub score: f64,
95 pub events: Vec<AccessEvent>,
97 pub last_decay_tick: u64,
99}
100
101pub struct StorageHotspotDetector {
115 pub records: HashMap<u64, BlockAccessRecord>,
117 pub config: DetectorConfig,
119 pub total_events: u64,
121}
122
123impl StorageHotspotDetector {
124 pub fn new(config: DetectorConfig) -> Self {
126 Self {
127 records: HashMap::new(),
128 config,
129 total_events: 0,
130 }
131 }
132
133 pub fn record_access(&mut self, event: AccessEvent) {
140 let block_id = event.block_id;
141 let tick = event.tick;
142
143 let record = self
145 .records
146 .entry(block_id)
147 .or_insert_with(|| BlockAccessRecord {
148 block_id,
149 score: 0.0,
150 events: Vec::new(),
151 last_decay_tick: tick,
152 });
153
154 if tick > record.last_decay_tick {
156 let ticks_elapsed = tick - record.last_decay_tick;
157 let decay = pow_f64(self.config.decay_factor, ticks_elapsed);
159 record.score *= decay;
160 record.last_decay_tick = tick;
161 }
162
163 let window_start = tick.saturating_sub(self.config.window_ticks);
165 record.events.retain(|e| e.tick >= window_start);
166
167 let weight = match event.access_type {
169 AccessType::Read => self.config.read_weight,
170 AccessType::Write => self.config.write_weight,
171 AccessType::Prefetch => self.config.prefetch_weight,
172 };
173 record.score += weight;
174
175 record.events.push(event);
177
178 self.total_events += 1;
179 }
180
181 pub fn hotspot_score(&self, block_id: u64, current_tick: u64) -> Option<HotspotScore> {
185 let record = self.records.get(&block_id)?;
186
187 let decayed_score = if current_tick > record.last_decay_tick {
188 let ticks_elapsed = current_tick - record.last_decay_tick;
189 record.score * pow_f64(self.config.decay_factor, ticks_elapsed)
190 } else {
191 record.score
192 };
193
194 let window_start = current_tick.saturating_sub(self.config.window_ticks);
195 let recent_accesses = record
196 .events
197 .iter()
198 .filter(|e| e.tick >= window_start)
199 .count() as u32;
200
201 let is_hotspot = decayed_score >= self.config.hotspot_threshold;
202
203 Some(HotspotScore {
204 block_id,
205 score: decayed_score,
206 recent_accesses,
207 is_hotspot,
208 })
209 }
210
211 pub fn hotspots(&self, current_tick: u64) -> Vec<HotspotScore> {
213 let mut scores: Vec<HotspotScore> = self
214 .records
215 .keys()
216 .filter_map(|&id| self.hotspot_score(id, current_tick))
217 .filter(|s| s.is_hotspot)
218 .collect();
219
220 scores.sort_by(|a, b| {
221 b.score
222 .partial_cmp(&a.score)
223 .unwrap_or(std::cmp::Ordering::Equal)
224 });
225 scores
226 }
227
228 pub fn top_n(&self, n: usize, current_tick: u64) -> Vec<HotspotScore> {
233 let mut scores: Vec<HotspotScore> = self
234 .records
235 .keys()
236 .filter_map(|&id| self.hotspot_score(id, current_tick))
237 .collect();
238
239 scores.sort_by(|a, b| {
240 b.score
241 .partial_cmp(&a.score)
242 .unwrap_or(std::cmp::Ordering::Equal)
243 });
244 scores.truncate(n);
245 scores
246 }
247
248 pub fn evict_cold(&mut self, current_tick: u64) -> usize {
252 const COLD_THRESHOLD: f64 = 0.001;
253
254 let cold_ids: Vec<u64> = self
255 .records
256 .iter()
257 .filter_map(|(&id, record)| {
258 let decayed = if current_tick > record.last_decay_tick {
259 let elapsed = current_tick - record.last_decay_tick;
260 record.score * pow_f64(self.config.decay_factor, elapsed)
261 } else {
262 record.score
263 };
264 if decayed < COLD_THRESHOLD {
265 Some(id)
266 } else {
267 None
268 }
269 })
270 .collect();
271
272 let removed = cold_ids.len();
273 for id in cold_ids {
274 self.records.remove(&id);
275 }
276 removed
277 }
278
279 pub fn stats(&self, current_tick: u64) -> HotspotDetectorStats {
281 let total_blocks_tracked = self.records.len();
282 let hotspot_count = self
283 .records
284 .keys()
285 .filter_map(|&id| self.hotspot_score(id, current_tick))
286 .filter(|s| s.is_hotspot)
287 .count();
288
289 HotspotDetectorStats {
290 total_blocks_tracked,
291 hotspot_count,
292 total_events_recorded: self.total_events,
293 }
294 }
295}
296
297#[inline]
306fn pow_f64(base: f64, exp: u64) -> f64 {
307 match exp {
309 0 => 1.0,
310 1 => base,
311 _ => {
312 let mut result = 1.0_f64;
313 let mut b = base;
314 let mut e = exp;
315 while e > 0 {
317 if e & 1 == 1 {
318 result *= b;
319 }
320 b *= b;
321 e >>= 1;
322 }
323 result
324 }
325 }
326}
327
328#[cfg(test)]
333mod tests {
334 use super::*;
335
336 fn default_detector() -> StorageHotspotDetector {
337 StorageHotspotDetector::new(DetectorConfig::default())
338 }
339
340 fn make_event(block_id: u64, tick: u64, access_type: AccessType) -> AccessEvent {
341 AccessEvent {
342 block_id,
343 tick,
344 access_type,
345 }
346 }
347
348 #[test]
351 fn test_record_creates_entry() {
352 let mut det = default_detector();
353 assert!(det.records.is_empty());
354 det.record_access(make_event(42, 0, AccessType::Read));
355 assert_eq!(det.records.len(), 1);
356 assert!(det.records.contains_key(&42));
357 }
358
359 #[test]
362 fn test_score_increases_with_reads() {
363 let mut det = default_detector();
364 det.record_access(make_event(1, 0, AccessType::Read));
365 let s1 = det.hotspot_score(1, 0).unwrap().score;
366 det.record_access(make_event(1, 0, AccessType::Read));
367 let s2 = det.hotspot_score(1, 0).unwrap().score;
368 assert!(s2 > s1, "score should grow: {s2} > {s1}");
369 }
370
371 #[test]
372 fn test_write_weight_higher_than_read() {
373 let mut det = default_detector();
374 det.record_access(make_event(1, 0, AccessType::Read));
375 let read_score = det.hotspot_score(1, 0).unwrap().score;
376
377 let mut det2 = default_detector();
378 det2.record_access(make_event(2, 0, AccessType::Write));
379 let write_score = det2.hotspot_score(2, 0).unwrap().score;
380
381 assert!(
382 write_score > read_score,
383 "write ({write_score}) > read ({read_score})"
384 );
385 }
386
387 #[test]
388 fn test_prefetch_weight_lower_than_read() {
389 let mut det = default_detector();
390 det.record_access(make_event(1, 0, AccessType::Read));
391 let read_score = det.hotspot_score(1, 0).unwrap().score;
392
393 let mut det2 = default_detector();
394 det2.record_access(make_event(2, 0, AccessType::Prefetch));
395 let prefetch_score = det2.hotspot_score(2, 0).unwrap().score;
396
397 assert!(
398 prefetch_score < read_score,
399 "prefetch ({prefetch_score}) < read ({read_score})"
400 );
401 }
402
403 #[test]
406 fn test_read_weight_exact() {
407 let mut det = default_detector();
408 det.record_access(make_event(5, 10, AccessType::Read));
409 let score = det.hotspot_score(5, 10).unwrap().score;
410 assert!(
411 (score - 1.0).abs() < 1e-10,
412 "read weight should be 1.0, got {score}"
413 );
414 }
415
416 #[test]
417 fn test_write_weight_exact() {
418 let mut det = default_detector();
419 det.record_access(make_event(5, 10, AccessType::Write));
420 let score = det.hotspot_score(5, 10).unwrap().score;
421 assert!(
422 (score - 1.5).abs() < 1e-10,
423 "write weight should be 1.5, got {score}"
424 );
425 }
426
427 #[test]
428 fn test_prefetch_weight_exact() {
429 let mut det = default_detector();
430 det.record_access(make_event(5, 10, AccessType::Prefetch));
431 let score = det.hotspot_score(5, 10).unwrap().score;
432 assert!(
433 (score - 0.5).abs() < 1e-10,
434 "prefetch weight should be 0.5, got {score}"
435 );
436 }
437
438 #[test]
441 fn test_decay_reduces_score_over_time() {
442 let mut det = default_detector();
443 det.record_access(make_event(1, 0, AccessType::Read));
444 let score_at_0 = det.hotspot_score(1, 0).unwrap().score;
445 let score_at_10 = det.hotspot_score(1, 10).unwrap().score;
446 assert!(
447 score_at_10 < score_at_0,
448 "score should decay: {score_at_10} < {score_at_0}"
449 );
450 }
451
452 #[test]
453 fn test_decay_factor_applied_correctly() {
454 let config = DetectorConfig {
455 decay_factor: 0.5,
456 ..Default::default()
457 };
458 let mut det = StorageHotspotDetector::new(config);
459 det.record_access(make_event(1, 0, AccessType::Read)); let score = det.hotspot_score(1, 3).unwrap().score;
462 assert!((score - 0.125).abs() < 1e-10, "expected 0.125, got {score}");
463 }
464
465 #[test]
466 fn test_decay_applied_on_new_event() {
467 let config = DetectorConfig {
468 decay_factor: 0.5,
469 ..Default::default()
470 };
471 let mut det = StorageHotspotDetector::new(config);
472 det.record_access(make_event(1, 0, AccessType::Read)); det.record_access(make_event(1, 2, AccessType::Read));
475 let score = det.hotspot_score(1, 2).unwrap().score;
476 assert!((score - 1.25).abs() < 1e-10, "expected 1.25, got {score}");
477 }
478
479 #[test]
480 fn test_no_decay_when_tick_unchanged() {
481 let mut det = default_detector();
482 det.record_access(make_event(7, 5, AccessType::Read));
483 det.record_access(make_event(7, 5, AccessType::Read));
484 let score = det.hotspot_score(7, 5).unwrap().score;
485 assert!((score - 2.0).abs() < 1e-10, "expected 2.0, got {score}");
487 }
488
489 #[test]
492 fn test_window_eviction_removes_old_events() {
493 let config = DetectorConfig {
494 window_ticks: 10,
495 ..Default::default()
496 };
497 let mut det = StorageHotspotDetector::new(config);
498 det.record_access(make_event(1, 0, AccessType::Read));
499 det.record_access(make_event(1, 5, AccessType::Read));
500 det.record_access(make_event(1, 15, AccessType::Read));
502 let record = &det.records[&1];
503 assert_eq!(record.events.len(), 2, "events at tick 0 should be evicted");
505 }
506
507 #[test]
508 fn test_recent_accesses_counts_window_only() {
509 let config = DetectorConfig {
510 window_ticks: 10,
511 ..Default::default()
512 };
513 let mut det = StorageHotspotDetector::new(config);
514 det.record_access(make_event(1, 0, AccessType::Read));
515 det.record_access(make_event(1, 1, AccessType::Read));
516 det.record_access(make_event(1, 5, AccessType::Read));
517 let hs = det.hotspot_score(1, 15).unwrap();
519 assert_eq!(hs.recent_accesses, 1);
520 }
521
522 #[test]
525 fn test_hotspot_score_none_for_unknown() {
526 let det = default_detector();
527 assert!(det.hotspot_score(999, 0).is_none());
528 }
529
530 #[test]
533 fn test_is_hotspot_false_below_threshold() {
534 let mut det = default_detector(); det.record_access(make_event(1, 0, AccessType::Read)); let hs = det.hotspot_score(1, 0).unwrap();
537 assert!(!hs.is_hotspot);
538 }
539
540 #[test]
541 fn test_is_hotspot_true_above_threshold() {
542 let mut det = default_detector(); for _ in 0..6 {
544 det.record_access(make_event(1, 0, AccessType::Read)); }
546 let hs = det.hotspot_score(1, 0).unwrap();
547 assert!(hs.is_hotspot, "score {} should be >= 5.0", hs.score);
548 }
549
550 #[test]
551 fn test_is_hotspot_exactly_at_threshold() {
552 let config = DetectorConfig {
553 hotspot_threshold: 3.0,
554 read_weight: 3.0,
555 ..Default::default()
556 };
557 let mut det = StorageHotspotDetector::new(config);
558 det.record_access(make_event(1, 0, AccessType::Read)); let hs = det.hotspot_score(1, 0).unwrap();
560 assert!(hs.is_hotspot, "score == threshold should be hotspot");
561 }
562
563 #[test]
566 fn test_hotspots_sorted_descending() {
567 let mut det = default_detector(); for _ in 0..8 {
570 det.record_access(make_event(10, 0, AccessType::Read));
571 }
572 for _ in 0..6 {
574 det.record_access(make_event(20, 0, AccessType::Read));
575 }
576 for _ in 0..3 {
578 det.record_access(make_event(30, 0, AccessType::Read));
579 }
580 let hs = det.hotspots(0);
581 assert_eq!(hs.len(), 2, "only blocks 10 and 20 should be hotspots");
582 assert_eq!(hs[0].block_id, 10);
583 assert_eq!(hs[1].block_id, 20);
584 assert!(hs[0].score >= hs[1].score);
585 }
586
587 #[test]
588 fn test_hotspots_empty_when_none_qualify() {
589 let mut det = default_detector();
590 det.record_access(make_event(1, 0, AccessType::Read));
591 let hs = det.hotspots(0);
592 assert!(hs.is_empty());
593 }
594
595 #[test]
598 fn test_top_n_returns_correct_count() {
599 let mut det = default_detector();
600 for id in 0..10_u64 {
601 for _ in 0..((id + 1) as usize) {
602 det.record_access(make_event(id, 0, AccessType::Read));
603 }
604 }
605 let top = det.top_n(3, 0);
606 assert_eq!(top.len(), 3);
607 }
608
609 #[test]
610 fn test_top_n_sorted_descending() {
611 let mut det = default_detector();
612 for id in 0..5_u64 {
613 for _ in 0..((id + 1) as usize) {
614 det.record_access(make_event(id, 0, AccessType::Read));
615 }
616 }
617 let top = det.top_n(5, 0);
618 for w in top.windows(2) {
619 assert!(w[0].score >= w[1].score);
620 }
621 }
622
623 #[test]
624 fn test_top_n_fewer_than_n_blocks() {
625 let mut det = default_detector();
626 det.record_access(make_event(1, 0, AccessType::Read));
627 det.record_access(make_event(2, 0, AccessType::Read));
628 let top = det.top_n(10, 0);
629 assert_eq!(top.len(), 2);
630 }
631
632 #[test]
633 fn test_top_n_zero_returns_empty() {
634 let mut det = default_detector();
635 det.record_access(make_event(1, 0, AccessType::Read));
636 let top = det.top_n(0, 0);
637 assert!(top.is_empty());
638 }
639
640 #[test]
643 fn test_evict_cold_removes_stale_blocks() {
644 let config = DetectorConfig {
645 decay_factor: 0.5,
646 ..Default::default()
647 };
648 let mut det = StorageHotspotDetector::new(config);
649 det.record_access(make_event(1, 0, AccessType::Read)); let removed = det.evict_cold(20);
652 assert_eq!(removed, 1);
653 assert!(det.records.is_empty());
654 }
655
656 #[test]
657 fn test_evict_cold_keeps_warm_blocks() {
658 let mut det = default_detector();
659 for _ in 0..10 {
660 det.record_access(make_event(1, 0, AccessType::Read)); }
662 let removed = det.evict_cold(0);
663 assert_eq!(removed, 0);
664 assert_eq!(det.records.len(), 1);
665 }
666
667 #[test]
668 fn test_evict_cold_returns_count() {
669 let config = DetectorConfig {
670 decay_factor: 0.1,
671 ..Default::default()
672 };
673 let mut det = StorageHotspotDetector::new(config);
674 det.record_access(make_event(1, 0, AccessType::Read));
675 det.record_access(make_event(2, 0, AccessType::Read));
676 det.record_access(make_event(3, 0, AccessType::Read));
677 let removed = det.evict_cold(10);
679 assert_eq!(removed, 3);
680 }
681
682 #[test]
685 fn test_stats_total_events() {
686 let mut det = default_detector();
687 det.record_access(make_event(1, 0, AccessType::Read));
688 det.record_access(make_event(2, 0, AccessType::Write));
689 det.record_access(make_event(1, 1, AccessType::Read));
690 let s = det.stats(1);
691 assert_eq!(s.total_events_recorded, 3);
692 }
693
694 #[test]
695 fn test_stats_blocks_tracked() {
696 let mut det = default_detector();
697 det.record_access(make_event(1, 0, AccessType::Read));
698 det.record_access(make_event(2, 0, AccessType::Read));
699 det.record_access(make_event(1, 0, AccessType::Read)); let s = det.stats(0);
701 assert_eq!(s.total_blocks_tracked, 2);
702 }
703
704 #[test]
705 fn test_stats_hotspot_count() {
706 let mut det = default_detector(); for _ in 0..6 {
708 det.record_access(make_event(1, 0, AccessType::Read));
709 }
710 det.record_access(make_event(2, 0, AccessType::Read)); let s = det.stats(0);
712 assert_eq!(s.hotspot_count, 1);
713 }
714
715 #[test]
718 fn test_custom_weights_affect_score() {
719 let config = DetectorConfig {
720 read_weight: 2.0,
721 write_weight: 3.0,
722 prefetch_weight: 0.1,
723 ..Default::default()
724 };
725 let mut det = StorageHotspotDetector::new(config);
726 det.record_access(make_event(1, 0, AccessType::Read));
727 det.record_access(make_event(2, 0, AccessType::Write));
728 det.record_access(make_event(3, 0, AccessType::Prefetch));
729
730 let r = det.hotspot_score(1, 0).unwrap().score;
731 let w = det.hotspot_score(2, 0).unwrap().score;
732 let p = det.hotspot_score(3, 0).unwrap().score;
733
734 assert!((r - 2.0).abs() < 1e-10, "read: {r}");
735 assert!((w - 3.0).abs() < 1e-10, "write: {w}");
736 assert!((p - 0.1).abs() < 1e-10, "prefetch: {p}");
737 }
738
739 #[test]
742 fn test_pow_f64_zero_exponent() {
743 assert!((super::pow_f64(0.95, 0) - 1.0).abs() < 1e-12);
744 }
745
746 #[test]
747 fn test_pow_f64_one_exponent() {
748 assert!((super::pow_f64(0.95, 1) - 0.95).abs() < 1e-12);
749 }
750
751 #[test]
752 fn test_pow_f64_large_exponent() {
753 let result = super::pow_f64(0.5, 10);
754 assert!((result - (0.5_f64).powi(10)).abs() < 1e-12);
755 }
756}