use crate::db_file_summary::DbFileSummary;
use crate::tracked_file_summary::TrackedFileSummary;
use hashbrown::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObsoleteKind {
Exact,
Inexact,
DupsAllowed,
}
impl ObsoleteKind {
fn track_offset(self) -> bool {
matches!(self, ObsoleteKind::Exact | ObsoleteKind::DupsAllowed)
}
fn check_dup_offsets(self) -> bool {
matches!(self, ObsoleteKind::Exact)
}
}
#[derive(Debug)]
pub struct UtilizationTracker {
tracked_files: HashMap<u32, TrackedFileSummary>,
db_file_summaries: HashMap<u32, HashMap<u32, DbFileSummary>>,
tracked_bytes: i64,
track_detail: bool,
}
impl UtilizationTracker {
pub fn new(track_detail: bool) -> Self {
Self {
tracked_files: HashMap::new(),
db_file_summaries: HashMap::new(),
tracked_bytes: 0,
track_detail,
}
}
pub fn count_obsolete_node(
&mut self,
file_number: u32,
offset: u32,
size: i32,
count_as_ln: bool,
db_id: Option<u32>,
) {
self.count_obsolete(
file_number,
offset,
size,
count_as_ln,
db_id,
ObsoleteKind::Exact,
);
}
pub fn count_obsolete_node_inexact(
&mut self,
file_number: u32,
offset: u32,
size: i32,
count_as_ln: bool,
db_id: Option<u32>,
) {
self.count_obsolete(
file_number,
offset,
size,
count_as_ln,
db_id,
ObsoleteKind::Inexact,
);
}
pub fn count_obsolete_node_dups_allowed(
&mut self,
file_number: u32,
offset: u32,
size: i32,
count_as_ln: bool,
db_id: Option<u32>,
) {
self.count_obsolete(
file_number,
offset,
size,
count_as_ln,
db_id,
ObsoleteKind::DupsAllowed,
);
}
fn count_obsolete(
&mut self,
file_number: u32,
offset: u32,
size: i32,
count_as_ln: bool,
db_id: Option<u32>,
kind: ObsoleteKind,
) {
let track_detail = self.track_detail;
let tracked =
self.tracked_files.entry(file_number).or_insert_with(|| {
TrackedFileSummary::new(file_number, track_detail)
});
let summary = tracked.get_summary_mut();
if count_as_ln {
summary.obsolete_ln_count += 1;
if size > 0 {
summary.obsolete_ln_size += size;
summary.obsolete_ln_size_counted += 1;
}
} else {
summary.obsolete_in_count += 1;
debug_assert_eq!(
size, 0,
"obsolete IN size must be 0 (JE BaseUtilizationTracker.countObsolete)"
);
}
if kind.track_offset() && offset != 0 {
if kind.check_dup_offsets() {
debug_assert!(
!tracked.get_obsolete_offsets().contains(&offset),
"checkDupOffsets: offset {offset} in file {file_number} already counted obsolete (JE invariant)"
);
}
tracked.add_obsolete_offset(offset);
}
if let Some(db) = db_id {
let db_summary = self
.db_file_summaries
.entry(db)
.or_default()
.entry(file_number)
.or_default();
if count_as_ln {
db_summary.obsolete_ln_count += 1;
if size > 0 {
db_summary.obsolete_ln_size += size;
db_summary.obsolete_ln_size_counted += 1;
}
} else {
db_summary.obsolete_in_count += 1;
}
}
self.update_tracked_bytes();
}
pub fn track_obsolete(
&mut self,
file_number: u32,
offset: u32,
size: i32,
count_as_ln: bool,
) {
self.count_obsolete_node_dups_allowed(
file_number,
offset,
size,
count_as_ln,
None,
);
}
pub fn count_obsolete_db(&mut self, db_id: u32) {
let Some(per_file) = self.db_file_summaries.remove(&db_id) else {
return;
};
let track_detail = self.track_detail;
for (file_num, db_summary) in per_file {
let tracked =
self.tracked_files.entry(file_num).or_insert_with(|| {
TrackedFileSummary::new(file_num, track_detail)
});
let summary = tracked.get_summary_mut();
let ln_obsolete_count =
db_summary.total_ln_count - db_summary.obsolete_ln_count;
let ln_obsolete_size =
db_summary.total_ln_size - db_summary.obsolete_ln_size;
let in_obsolete_count =
db_summary.total_in_count - db_summary.obsolete_in_count;
summary.obsolete_ln_count += ln_obsolete_count;
summary.obsolete_ln_size += ln_obsolete_size;
summary.obsolete_in_count += in_obsolete_count;
let ln_obsolete_size_counted = ln_obsolete_count
+ (db_summary.obsolete_ln_count
- db_summary.obsolete_ln_size_counted);
summary.obsolete_ln_size_counted += ln_obsolete_size_counted;
}
self.update_tracked_bytes();
}
pub fn count_new_log_entry(
&mut self,
file_number: u32,
size: i32,
is_ln: bool,
is_in: bool,
) {
self.count_new_log_entry_db(file_number, size, is_ln, is_in, None);
}
pub fn count_new_log_entry_db(
&mut self,
file_number: u32,
size: i32,
is_ln: bool,
is_in: bool,
db_id: Option<u32>,
) {
let track_detail = self.track_detail;
let tracked =
self.tracked_files.entry(file_number).or_insert_with(|| {
TrackedFileSummary::new(file_number, track_detail)
});
let summary = tracked.get_summary_mut();
summary.total_count += 1;
summary.total_size += size;
if is_ln {
summary.total_ln_count += 1;
summary.total_ln_size += size;
if size > summary.max_ln_size {
summary.max_ln_size = size;
}
}
if is_in {
summary.total_in_count += 1;
summary.total_in_size += size;
}
if let Some(db) = db_id {
let db_summary = self
.db_file_summaries
.entry(db)
.or_default()
.entry(file_number)
.or_default();
if is_ln {
db_summary.total_ln_count += 1;
db_summary.total_ln_size += size;
}
if is_in {
db_summary.total_in_count += 1;
db_summary.total_in_size += size;
}
}
self.update_tracked_bytes();
}
pub fn get_db_file_summary(
&self,
db_id: u32,
file_number: u32,
) -> Option<&DbFileSummary> {
self.db_file_summaries.get(&db_id)?.get(&file_number)
}
pub fn get_tracked_summary(
&self,
file_number: u32,
) -> Option<&TrackedFileSummary> {
self.tracked_files.get(&file_number)
}
pub fn get_tracked_summary_mut(
&mut self,
file_number: u32,
) -> Option<&mut TrackedFileSummary> {
self.tracked_files.get_mut(&file_number)
}
pub fn get_tracked_files(&self) -> &HashMap<u32, TrackedFileSummary> {
&self.tracked_files
}
pub fn get_tracked_files_mut(
&mut self,
) -> &mut HashMap<u32, TrackedFileSummary> {
&mut self.tracked_files
}
pub fn remove_all_tracked_files(
&mut self,
) -> HashMap<u32, TrackedFileSummary> {
self.tracked_bytes = 0;
std::mem::take(&mut self.tracked_files)
}
pub fn get_bytes_tracked(&self) -> i64 {
self.tracked_bytes
}
pub fn get_tracked_file_count(&self) -> usize {
self.tracked_files.len()
}
pub fn clear(&mut self) {
self.tracked_files.clear();
self.db_file_summaries.clear();
self.tracked_bytes = 0;
}
fn update_tracked_bytes(&mut self) {
self.tracked_bytes =
self.tracked_files.values().map(|t| t.memory_size() as i64).sum();
}
}
impl Default for UtilizationTracker {
fn default() -> Self {
Self::new(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let tracker = UtilizationTracker::new(true);
assert_eq!(tracker.get_tracked_file_count(), 0);
assert_eq!(tracker.get_bytes_tracked(), 0);
}
#[test]
fn test_track_obsolete_ln() {
let mut tracker = UtilizationTracker::new(true);
tracker.track_obsolete(1, 100, 50, true);
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().obsolete_ln_count, 1);
assert_eq!(tracked.get_summary().obsolete_ln_size, 50);
assert_eq!(tracked.get_summary().obsolete_ln_size_counted, 1);
assert_eq!(tracked.obsolete_offset_count(), 1);
}
#[test]
fn test_track_obsolete_ln_size_zero_does_not_count_size() {
let mut tracker = UtilizationTracker::new(true);
tracker.track_obsolete(1, 100, 0, true);
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(
tracked.get_summary().obsolete_ln_count,
1,
"count still increments for a size-0 obsolete LN"
);
assert_eq!(
tracked.get_summary().obsolete_ln_size,
0,
"CLN-F3: size must NOT accumulate when size <= 0"
);
assert_eq!(
tracked.get_summary().obsolete_ln_size_counted,
0,
"CLN-F3: counted must NOT increment when size <= 0"
);
}
#[test]
fn test_track_obsolete_in() {
let mut tracker = UtilizationTracker::new(true);
tracker.track_obsolete(1, 100, 0, false);
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().obsolete_in_count, 1);
assert_eq!(tracked.get_summary().obsolete_ln_count, 0);
}
#[test]
fn test_count_new_ln_entry() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_new_log_entry(1, 100, true, false);
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().total_count, 1);
assert_eq!(tracked.get_summary().total_size, 100);
assert_eq!(tracked.get_summary().total_ln_count, 1);
assert_eq!(tracked.get_summary().total_ln_size, 100);
assert_eq!(tracked.get_summary().max_ln_size, 100);
}
#[test]
fn test_count_new_in_entry() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_new_log_entry(1, 200, false, true);
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().total_count, 1);
assert_eq!(tracked.get_summary().total_size, 200);
assert_eq!(tracked.get_summary().total_in_count, 1);
assert_eq!(tracked.get_summary().total_in_size, 200);
}
#[test]
fn test_max_ln_size_tracking() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_new_log_entry(1, 50, true, false);
tracker.count_new_log_entry(1, 100, true, false);
tracker.count_new_log_entry(1, 75, true, false);
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().max_ln_size, 100);
}
#[test]
fn test_multiple_files() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_new_log_entry(1, 100, true, false);
tracker.count_new_log_entry(2, 200, true, false);
tracker.count_new_log_entry(3, 300, true, false);
assert_eq!(tracker.get_tracked_file_count(), 3);
assert!(tracker.get_tracked_summary(1).is_some());
assert!(tracker.get_tracked_summary(2).is_some());
assert!(tracker.get_tracked_summary(3).is_some());
}
#[test]
fn test_track_detail_disabled() {
let mut tracker = UtilizationTracker::new(false);
tracker.track_obsolete(1, 100, 50, true);
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().obsolete_ln_count, 1);
assert_eq!(tracked.obsolete_offset_count(), 0);
}
#[test]
fn test_remove_all_tracked_files() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_new_log_entry(1, 100, true, false);
tracker.count_new_log_entry(2, 200, true, false);
let tracked_files = tracker.remove_all_tracked_files();
assert_eq!(tracked_files.len(), 2);
assert_eq!(tracker.get_tracked_file_count(), 0);
assert_eq!(tracker.get_bytes_tracked(), 0);
}
#[test]
fn test_clear() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_new_log_entry(1, 100, true, false);
tracker.track_obsolete(1, 100, 50, true);
tracker.clear();
assert_eq!(tracker.get_tracked_file_count(), 0);
assert_eq!(tracker.get_bytes_tracked(), 0);
}
#[test]
fn test_get_tracked_files_mut() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_new_log_entry(1, 100, true, false);
{
let files = tracker.get_tracked_files_mut();
if let Some(tracked) = files.get_mut(&1) {
tracked.get_summary_mut().total_count += 10;
}
}
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().total_count, 11);
}
#[test]
fn test_bytes_tracked_increases() {
let mut tracker = UtilizationTracker::new(true);
let initial_bytes = tracker.get_bytes_tracked();
tracker.count_new_log_entry(1, 100, true, false);
let after_entry = tracker.get_bytes_tracked();
assert!(after_entry > initial_bytes);
tracker.track_obsolete(1, 100, 50, true);
let after_obsolete = tracker.get_bytes_tracked();
assert!(after_obsolete >= after_entry);
}
#[test]
fn test_accumulate_entries_same_file() {
let mut tracker = UtilizationTracker::new(true);
for i in 0..10 {
tracker.count_new_log_entry(1, 100, true, false);
tracker.track_obsolete(1, (i + 1) * 100, 50, true);
}
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().total_count, 10);
assert_eq!(tracked.get_summary().obsolete_ln_count, 10);
assert_eq!(tracked.obsolete_offset_count(), 10);
}
#[test]
fn test_count_obsolete_node_exact_with_db() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_new_log_entry_db(1, 80, true, false, Some(7));
tracker.count_obsolete_node(1, 200, 80, true, Some(7));
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().obsolete_ln_count, 1);
assert_eq!(tracked.obsolete_offset_count(), 1, "exact tracks offset");
let db = tracker.get_db_file_summary(7, 1).unwrap();
assert_eq!(db.total_ln_count, 1);
assert_eq!(db.obsolete_ln_count, 1);
assert_eq!(db.obsolete_ln_size, 80);
}
#[test]
fn test_count_obsolete_node_inexact_skips_offset() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_obsolete_node_inexact(1, 200, 80, true, Some(7));
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(tracked.get_summary().obsolete_ln_count, 1);
assert_eq!(
tracked.obsolete_offset_count(),
0,
"inexact must NOT track the (approximate) offset"
);
}
#[test]
fn test_count_obsolete_node_dups_allowed() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_obsolete_node_dups_allowed(1, 200, 80, true, Some(7));
tracker.count_obsolete_node_dups_allowed(1, 200, 80, true, Some(7));
let tracked = tracker.get_tracked_summary(1).unwrap();
assert_eq!(
tracked.get_summary().obsolete_ln_count,
2,
"both counts land (double-count is legitimate here)"
);
assert_eq!(
tracked.obsolete_offset_count(),
2,
"dups-allowed tracks the offset each time"
);
}
#[test]
fn test_count_obsolete_db() {
let mut tracker = UtilizationTracker::new(true);
tracker.count_new_log_entry_db(1, 100, true, false, Some(7));
tracker.count_new_log_entry_db(1, 100, true, false, Some(7));
tracker.count_new_log_entry_db(1, 100, true, false, Some(7));
tracker.count_obsolete_node(1, 300, 100, true, Some(7));
let before = tracker.get_tracked_summary(1).unwrap();
assert_eq!(before.get_summary().obsolete_ln_count, 1);
tracker.count_obsolete_db(7);
let after = tracker.get_tracked_summary(1).unwrap();
assert_eq!(after.get_summary().obsolete_ln_count, 3);
assert_eq!(after.get_summary().obsolete_ln_size, 300);
assert_eq!(after.get_summary().obsolete_ln_size_counted, 3);
assert!(tracker.get_db_file_summary(7, 1).is_none());
}
#[test]
fn test_default() {
let tracker = UtilizationTracker::default();
assert_eq!(tracker.get_tracked_file_count(), 0);
}
}