1use std::collections::{HashMap, HashSet};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum LogOperation {
15 Read,
17 Write,
19 Delete,
21 Stat,
23 List,
25}
26
27#[derive(Clone, Debug)]
33pub struct AccessLogEntry {
34 pub entry_id: u64,
36 pub block_id: u64,
38 pub cid: String,
40 pub operation: LogOperation,
42 pub size_bytes: u64,
44 pub tick: u64,
46 pub peer_id: Option<String>,
48}
49
50#[derive(Clone, Debug, PartialEq)]
56pub enum AccessPattern {
57 Sequential,
59 Random,
61 Repeated {
63 block_id: u64,
65 },
66 BurstWrite,
68}
69
70#[derive(Clone, Debug)]
76pub struct LogConfig {
77 pub max_entries: usize,
79 pub burst_write_threshold: usize,
81 pub burst_window_ticks: u64,
83}
84
85impl Default for LogConfig {
86 fn default() -> Self {
87 Self {
88 max_entries: 10_000,
89 burst_write_threshold: 10,
90 burst_window_ticks: 10,
91 }
92 }
93}
94
95#[derive(Clone, Debug)]
101pub struct AccessLogStats {
102 pub total_entries: usize,
104 pub by_operation: HashMap<LogOperation, u64>,
106 pub total_bytes_read: u64,
108 pub total_bytes_written: u64,
110 pub unique_blocks_accessed: usize,
112}
113
114pub struct StorageAccessLog {
123 pub entries: Vec<AccessLogEntry>,
125 pub next_entry_id: u64,
127 pub config: LogConfig,
129 pub cumulative_ops: HashMap<LogOperation, u64>,
131 pub cumulative_read_bytes: u64,
133 pub cumulative_write_bytes: u64,
135 pub seen_blocks: HashSet<u64>,
137}
138
139impl StorageAccessLog {
140 pub fn new(config: LogConfig) -> Self {
142 Self {
143 entries: Vec::new(),
144 next_entry_id: 0,
145 config,
146 cumulative_ops: HashMap::new(),
147 cumulative_read_bytes: 0,
148 cumulative_write_bytes: 0,
149 seen_blocks: HashSet::new(),
150 }
151 }
152
153 pub fn log(
158 &mut self,
159 block_id: u64,
160 cid: String,
161 operation: LogOperation,
162 size_bytes: u64,
163 tick: u64,
164 peer_id: Option<String>,
165 ) {
166 if self.entries.len() >= self.config.max_entries {
168 self.entries.remove(0);
169 }
170
171 let entry = AccessLogEntry {
172 entry_id: self.next_entry_id,
173 block_id,
174 cid,
175 operation,
176 size_bytes,
177 tick,
178 peer_id,
179 };
180
181 self.next_entry_id += 1;
182
183 *self.cumulative_ops.entry(operation).or_insert(0) += 1;
185 match operation {
186 LogOperation::Read => self.cumulative_read_bytes += size_bytes,
187 LogOperation::Write => self.cumulative_write_bytes += size_bytes,
188 _ => {}
189 }
190 self.seen_blocks.insert(block_id);
191
192 self.entries.push(entry);
193 }
194
195 pub fn detect_pattern(&self, current_tick: u64) -> AccessPattern {
203 let window_start = current_tick.saturating_sub(self.config.burst_window_ticks);
205 let burst_writes = self
206 .entries
207 .iter()
208 .filter(|e| e.operation == LogOperation::Write && e.tick >= window_start)
209 .count();
210 if burst_writes >= self.config.burst_write_threshold {
211 return AccessPattern::BurstWrite;
212 }
213
214 let recent: Vec<&AccessLogEntry> = {
216 let start = self.entries.len().saturating_sub(10);
217 self.entries[start..].iter().collect()
218 };
219
220 let mut freq: HashMap<u64, usize> = HashMap::new();
223 for e in &recent {
224 *freq.entry(e.block_id).or_insert(0) += 1;
225 }
226 for e in &recent {
228 if freq.get(&e.block_id).copied().unwrap_or(0) >= 3 {
229 return AccessPattern::Repeated {
230 block_id: e.block_id,
231 };
232 }
233 }
234
235 let last_five_reads: Vec<u64> = self
238 .entries
239 .iter()
240 .filter(|e| e.operation == LogOperation::Read)
241 .rev()
242 .take(5)
243 .map(|e| e.block_id)
244 .collect::<Vec<_>>()
245 .into_iter()
246 .rev()
247 .collect();
248
249 if last_five_reads.len() == 5 && last_five_reads.windows(2).all(|w| w[0] < w[1]) {
250 return AccessPattern::Sequential;
251 }
252
253 AccessPattern::Random
254 }
255
256 pub fn entries_for_block(&self, block_id: u64) -> Vec<&AccessLogEntry> {
258 self.entries
259 .iter()
260 .filter(|e| e.block_id == block_id)
261 .collect()
262 }
263
264 pub fn entries_in_range(&self, from_tick: u64, to_tick: u64) -> Vec<&AccessLogEntry> {
266 self.entries
267 .iter()
268 .filter(|e| e.tick >= from_tick && e.tick <= to_tick)
269 .collect()
270 }
271
272 pub fn stats(&self) -> AccessLogStats {
274 AccessLogStats {
275 total_entries: self.next_entry_id as usize,
276 by_operation: self.cumulative_ops.clone(),
277 total_bytes_read: self.cumulative_read_bytes,
278 total_bytes_written: self.cumulative_write_bytes,
279 unique_blocks_accessed: self.seen_blocks.len(),
280 }
281 }
282
283 pub fn clear(&mut self) {
287 self.entries.clear();
288 }
289}
290
291#[cfg(test)]
296mod tests {
297 use super::*;
298
299 fn make_log(max: usize) -> StorageAccessLog {
300 let cfg = LogConfig {
301 max_entries: max,
302 burst_write_threshold: 10,
303 burst_window_ticks: 10,
304 };
305 StorageAccessLog::new(cfg)
306 }
307
308 fn log_op(log: &mut StorageAccessLog, block_id: u64, op: LogOperation, size: u64, tick: u64) {
309 log.log(block_id, format!("cid-{block_id}"), op, size, tick, None);
310 }
311
312 #[test]
314 fn test_log_appends_entry() {
315 let mut log = make_log(100);
316 log_op(&mut log, 1, LogOperation::Read, 512, 1);
317 assert_eq!(log.entries.len(), 1);
318 assert_eq!(log.entries[0].block_id, 1);
319 assert_eq!(log.entries[0].operation, LogOperation::Read);
320 assert_eq!(log.entries[0].size_bytes, 512);
321 assert_eq!(log.entries[0].tick, 1);
322 }
323
324 #[test]
326 fn test_entry_id_monotonic() {
327 let mut log = make_log(100);
328 for i in 0..5_u64 {
329 log_op(&mut log, i, LogOperation::Read, 1, i);
330 }
331 for (i, e) in log.entries.iter().enumerate() {
332 assert_eq!(e.entry_id, i as u64);
333 }
334 }
335
336 #[test]
338 fn test_fifo_eviction() {
339 let mut log = make_log(3);
340 for i in 0..5_u64 {
341 log_op(&mut log, i, LogOperation::Read, 1, i);
342 }
343 assert_eq!(log.entries.len(), 3);
345 assert_eq!(log.entries[0].block_id, 2);
346 assert_eq!(log.entries[2].block_id, 4);
347 }
348
349 #[test]
351 fn test_next_entry_id_not_decremented_on_eviction() {
352 let mut log = make_log(2);
353 for i in 0..5_u64 {
354 log_op(&mut log, i, LogOperation::Read, 1, i);
355 }
356 assert_eq!(log.next_entry_id, 5);
357 }
358
359 #[test]
361 fn test_cumulative_ops() {
362 let mut log = make_log(100);
363 log_op(&mut log, 1, LogOperation::Read, 100, 1);
364 log_op(&mut log, 2, LogOperation::Write, 200, 2);
365 log_op(&mut log, 3, LogOperation::Read, 300, 3);
366 let stats = log.stats();
367 assert_eq!(
368 *stats.by_operation.get(&LogOperation::Read).unwrap_or(&0),
369 2
370 );
371 assert_eq!(
372 *stats.by_operation.get(&LogOperation::Write).unwrap_or(&0),
373 1
374 );
375 assert_eq!(stats.total_bytes_read, 400);
376 assert_eq!(stats.total_bytes_written, 200);
377 }
378
379 #[test]
381 fn test_cumulative_stats_survive_eviction() {
382 let mut log = make_log(2);
383 log_op(&mut log, 1, LogOperation::Read, 100, 1);
384 log_op(&mut log, 2, LogOperation::Write, 200, 2);
385 log_op(&mut log, 3, LogOperation::Read, 300, 3);
386 assert_eq!(log.entries.len(), 2);
388 let stats = log.stats();
389 assert_eq!(stats.total_bytes_read, 400); assert_eq!(
391 *stats.by_operation.get(&LogOperation::Read).unwrap_or(&0),
392 2
393 );
394 }
395
396 #[test]
398 fn test_total_entries_equals_logged_count() {
399 let mut log = make_log(3);
400 for i in 0..7_u64 {
401 log_op(&mut log, i, LogOperation::Stat, 0, i);
402 }
403 assert_eq!(log.stats().total_entries, 7);
404 }
405
406 #[test]
408 fn test_seen_blocks_unique_count() {
409 let mut log = make_log(100);
410 for _ in 0..4 {
411 log_op(&mut log, 42, LogOperation::Read, 1, 1);
412 }
413 log_op(&mut log, 99, LogOperation::Write, 1, 2);
414 assert_eq!(log.stats().unique_blocks_accessed, 2);
415 }
416
417 #[test]
419 fn test_seen_blocks_survive_eviction() {
420 let mut log = make_log(2);
421 log_op(&mut log, 10, LogOperation::Read, 1, 1);
422 log_op(&mut log, 20, LogOperation::Read, 1, 2);
423 log_op(&mut log, 30, LogOperation::Read, 1, 3);
424 assert_eq!(log.stats().unique_blocks_accessed, 3);
426 }
427
428 #[test]
430 fn test_detect_pattern_burst_write() {
431 let _log = make_log(100);
432 let cfg = LogConfig {
433 max_entries: 100,
434 burst_write_threshold: 5,
435 burst_window_ticks: 10,
436 };
437 let mut log = StorageAccessLog::new(cfg);
438 for i in 0..5_u64 {
439 log_op(&mut log, i, LogOperation::Write, 1, 5 + i);
440 }
441 assert_eq!(log.detect_pattern(10), AccessPattern::BurstWrite);
442 }
443
444 #[test]
446 fn test_detect_pattern_no_burst_below_threshold() {
447 let cfg = LogConfig {
448 max_entries: 100,
449 burst_write_threshold: 5,
450 burst_window_ticks: 10,
451 };
452 let mut log = StorageAccessLog::new(cfg);
453 for i in 0..4_u64 {
454 log_op(&mut log, i, LogOperation::Write, 1, 5 + i);
455 }
456 assert_ne!(log.detect_pattern(10), AccessPattern::BurstWrite);
458 }
459
460 #[test]
462 fn test_detect_pattern_repeated() {
463 let mut log = make_log(100);
464 for _ in 0..3 {
465 log_op(&mut log, 77, LogOperation::Read, 1, 1);
466 }
467 log_op(&mut log, 99, LogOperation::Read, 1, 2);
468 assert_eq!(
469 log.detect_pattern(2),
470 AccessPattern::Repeated { block_id: 77 }
471 );
472 }
473
474 #[test]
476 fn test_detect_pattern_repeated_needs_three() {
477 let mut log = make_log(100);
478 log_op(&mut log, 77, LogOperation::Read, 1, 1);
479 log_op(&mut log, 77, LogOperation::Read, 1, 2);
480 log_op(&mut log, 10, LogOperation::Read, 1, 3);
481 log_op(&mut log, 20, LogOperation::Read, 1, 4);
482 log_op(&mut log, 30, LogOperation::Read, 1, 5);
483 let pat = log.detect_pattern(5);
485 assert!(
486 pat != AccessPattern::Repeated { block_id: 77 },
487 "Should not be Repeated with only 2 hits"
488 );
489 }
490
491 #[test]
493 fn test_detect_pattern_sequential() {
494 let mut log = make_log(100);
495 for i in 1..=5_u64 {
496 log_op(&mut log, i * 10, LogOperation::Read, 1, i);
497 }
498 assert_eq!(log.detect_pattern(5), AccessPattern::Sequential);
499 }
500
501 #[test]
503 fn test_detect_pattern_sequential_requires_strict_increase() {
504 let mut log = make_log(100);
505 for bid in [10_u64, 20, 20, 30, 40] {
507 log_op(&mut log, bid, LogOperation::Read, 1, 1);
508 }
509 assert_ne!(log.detect_pattern(1), AccessPattern::Sequential);
510 }
511
512 #[test]
514 fn test_detect_pattern_random() {
515 let mut log = make_log(100);
516 for bid in [5_u64, 2, 9, 1] {
517 log_op(&mut log, bid, LogOperation::Read, 1, 1);
518 }
519 assert_eq!(log.detect_pattern(1), AccessPattern::Random);
520 }
521
522 #[test]
524 fn test_detect_pattern_empty_is_random() {
525 let log = make_log(100);
526 assert_eq!(log.detect_pattern(0), AccessPattern::Random);
527 }
528
529 #[test]
531 fn test_entries_for_block() {
532 let mut log = make_log(100);
533 log_op(&mut log, 1, LogOperation::Read, 1, 1);
534 log_op(&mut log, 2, LogOperation::Write, 2, 2);
535 log_op(&mut log, 1, LogOperation::Stat, 3, 3);
536 log_op(&mut log, 3, LogOperation::Read, 4, 4);
537
538 let result = log.entries_for_block(1);
539 assert_eq!(result.len(), 2);
540 assert_eq!(result[0].tick, 1);
541 assert_eq!(result[1].tick, 3);
542 }
543
544 #[test]
546 fn test_entries_for_block_not_found() {
547 let mut log = make_log(100);
548 log_op(&mut log, 42, LogOperation::Read, 1, 1);
549 assert!(log.entries_for_block(99).is_empty());
550 }
551
552 #[test]
554 fn test_entries_in_range_inclusive() {
555 let mut log = make_log(100);
556 for tick in 1..=5_u64 {
557 log_op(&mut log, tick, LogOperation::Read, 1, tick);
558 }
559 let result = log.entries_in_range(2, 4);
560 assert_eq!(result.len(), 3);
561 assert!(result.iter().all(|e| e.tick >= 2 && e.tick <= 4));
562 }
563
564 #[test]
566 fn test_entries_in_range_outside() {
567 let mut log = make_log(100);
568 log_op(&mut log, 1, LogOperation::Read, 1, 5);
569 assert!(log.entries_in_range(10, 20).is_empty());
570 }
571
572 #[test]
574 fn test_entries_in_range_single_tick() {
575 let mut log = make_log(100);
576 log_op(&mut log, 1, LogOperation::Read, 1, 7);
577 log_op(&mut log, 2, LogOperation::Read, 1, 7);
578 log_op(&mut log, 3, LogOperation::Write, 1, 8);
579 let result = log.entries_in_range(7, 7);
580 assert_eq!(result.len(), 2);
581 }
582
583 #[test]
585 fn test_clear_keeps_cumulative_stats() {
586 let mut log = make_log(100);
587 log_op(&mut log, 1, LogOperation::Read, 100, 1);
588 log_op(&mut log, 2, LogOperation::Write, 200, 2);
589 log.clear();
590
591 assert!(log.entries.is_empty(), "entries should be cleared");
592 let stats = log.stats();
593 assert_eq!(stats.total_entries, 2); assert_eq!(stats.total_bytes_read, 100);
595 assert_eq!(stats.total_bytes_written, 200);
596 assert_eq!(stats.unique_blocks_accessed, 2);
597 }
598
599 #[test]
601 fn test_clear_does_not_reset_next_entry_id() {
602 let mut log = make_log(100);
603 log_op(&mut log, 1, LogOperation::Read, 1, 1);
604 log_op(&mut log, 2, LogOperation::Read, 1, 2);
605 log.clear();
606 log_op(&mut log, 3, LogOperation::Read, 1, 3);
608 assert_eq!(log.entries[0].entry_id, 2);
609 }
610
611 #[test]
613 fn test_peer_id_none() {
614 let mut log = make_log(100);
615 log.log(1, "cid-1".into(), LogOperation::Read, 1, 1, None);
616 assert!(log.entries[0].peer_id.is_none());
617 }
618
619 #[test]
621 fn test_peer_id_some() {
622 let mut log = make_log(100);
623 log.log(
624 1,
625 "cid-1".into(),
626 LogOperation::Read,
627 1,
628 1,
629 Some("peer-abc".into()),
630 );
631 assert_eq!(log.entries[0].peer_id.as_deref(), Some("peer-abc"));
632 }
633
634 #[test]
636 fn test_delete_stat_list_in_by_operation() {
637 let mut log = make_log(100);
638 log_op(&mut log, 1, LogOperation::Delete, 0, 1);
639 log_op(&mut log, 2, LogOperation::Stat, 0, 2);
640 log_op(&mut log, 3, LogOperation::List, 0, 3);
641 let stats = log.stats();
642 assert_eq!(
643 *stats.by_operation.get(&LogOperation::Delete).unwrap_or(&0),
644 1
645 );
646 assert_eq!(
647 *stats.by_operation.get(&LogOperation::Stat).unwrap_or(&0),
648 1
649 );
650 assert_eq!(
651 *stats.by_operation.get(&LogOperation::List).unwrap_or(&0),
652 1
653 );
654 }
655
656 #[test]
658 fn test_burst_write_ignores_old_writes() {
659 let cfg = LogConfig {
660 max_entries: 100,
661 burst_write_threshold: 3,
662 burst_window_ticks: 5,
663 };
664 let mut log = StorageAccessLog::new(cfg);
665 for i in 0..3_u64 {
667 log_op(&mut log, i, LogOperation::Write, 1, 1);
668 }
669 log_op(&mut log, 10, LogOperation::Write, 1, 96);
671 log_op(&mut log, 11, LogOperation::Write, 1, 97);
672 assert_ne!(log.detect_pattern(100), AccessPattern::BurstWrite);
674 }
675
676 #[test]
678 fn test_cid_stored_correctly() {
679 let mut log = make_log(100);
680 log.log(7, "bafyxyz".into(), LogOperation::Read, 1, 1, None);
681 assert_eq!(log.entries[0].cid, "bafyxyz");
682 }
683
684 #[test]
686 fn test_repeated_returns_first_found_block() {
687 let mut log = make_log(100);
688 for tick in 1..=3_u64 {
690 log_op(&mut log, 99, LogOperation::Read, 1, tick);
691 }
692 for tick in 4..=6_u64 {
693 log_op(&mut log, 77, LogOperation::Read, 1, tick);
694 }
695 let pat = log.detect_pattern(6);
697 assert_eq!(pat, AccessPattern::Repeated { block_id: 99 });
698 }
699}