#![forbid(unsafe_code)]
use std::collections::HashSet;
use std::path::Path;
use crate::core::extent::ChunkId;
use crate::store::segment::{self, ScanRecord};
use crate::store::{Store, StoreError};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SegmentPhysical {
pub seq: u64,
pub file_bytes: u64,
pub canonical_records: u64,
pub live_bytes: u64,
pub dead_indexed_bytes: u64,
pub index_hidden_bytes: u64,
pub unindexed_bytes: u64,
pub torn_bytes: u64,
pub zero_padding_bytes: u64,
pub format_overhead_bytes: u64,
}
impl SegmentPhysical {
pub fn reclaimable_bytes(&self) -> u64 {
self.dead_indexed_bytes
.saturating_add(self.index_hidden_bytes)
.saturating_add(self.unindexed_bytes)
.saturating_add(self.torn_bytes)
.saturating_add(self.zero_padding_bytes)
}
pub fn physical_live_ratio(&self) -> f64 {
let total = self
.live_bytes
.saturating_add(self.dead_indexed_bytes)
.saturating_add(self.index_hidden_bytes)
.saturating_add(self.unindexed_bytes);
if total == 0 {
return 1.0; }
self.live_bytes as f64 / total as f64
}
pub fn index_live_ratio(&self) -> f64 {
let total = self.live_bytes.saturating_add(self.dead_indexed_bytes);
if total == 0 {
return 1.0;
}
self.live_bytes as f64 / total as f64
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PhysicalReport {
pub segments: Vec<SegmentPhysical>,
pub file_bytes: u64,
pub live_bytes: u64,
pub dead_indexed_bytes: u64,
pub index_hidden_bytes: u64,
pub unindexed_bytes: u64,
pub torn_bytes: u64,
pub zero_padding_bytes: u64,
pub format_overhead_bytes: u64,
}
impl PhysicalReport {
pub fn index_accounted_bytes(&self) -> u64 {
self.live_bytes.saturating_add(self.dead_indexed_bytes)
}
pub fn unexplained(&self) -> u64 {
self.file_bytes.saturating_sub(
self.live_bytes
.saturating_add(self.dead_indexed_bytes)
.saturating_add(self.index_hidden_bytes)
.saturating_add(self.unindexed_bytes)
.saturating_add(self.torn_bytes)
.saturating_add(self.zero_padding_bytes)
.saturating_add(self.format_overhead_bytes),
)
}
}
fn classify(
rec: &ScanRecord,
seq: u64,
live: &HashSet<ChunkId>,
index: &crate::store::object::ObjectIndex,
) -> (bool, bool) {
match index.get(&rec.content_id) {
Some(loc) if loc.segment_seq == seq && loc.offset == rec.offset => {
(true, live.contains(&rec.content_id))
}
_ => (false, false),
}
}
fn scan_segment_classified(
path: &Path,
seq: u64,
limit_records: u64,
live: &HashSet<ChunkId>,
index: &crate::store::object::ObjectIndex,
) -> Result<SegmentPhysical, StoreError> {
let file_len = std::fs::metadata(path)
.map(|m| m.len())
.map_err(|e| StoreError::Io(e.to_string()))?;
let (records, clean_end) = segment::scan_segment(path, limit_records)?;
let mut out = SegmentPhysical {
seq,
file_bytes: file_len,
..Default::default()
};
out.canonical_records = 0;
let mut valid_bytes = 0u64;
for rec in &records {
let total = rec.total_size();
valid_bytes = valid_bytes.saturating_add(total);
let (canonical, is_live) = classify(rec, seq, live, index);
if canonical {
out.canonical_records += 1;
if is_live {
out.live_bytes = out.live_bytes.saturating_add(total);
} else {
out.dead_indexed_bytes = out.dead_indexed_bytes.saturating_add(total);
}
} else if index.get(&rec.content_id).is_some() {
out.index_hidden_bytes = out.index_hidden_bytes.saturating_add(total);
} else {
out.unindexed_bytes = out.unindexed_bytes.saturating_add(total);
}
}
let tail_bytes = file_len.saturating_sub(clean_end);
if tail_bytes > 0 {
let bytes = std::fs::read(path).map_err(|e| StoreError::Io(e.to_string()))?;
let tail = &bytes[clean_end as usize..];
if tail.iter().all(|&b| b == 0) {
out.zero_padding_bytes = tail_bytes;
} else {
out.torn_bytes = tail_bytes;
}
}
out.format_overhead_bytes = file_len
.saturating_sub(valid_bytes)
.saturating_sub(tail_bytes);
Ok(out)
}
pub fn scan_physical(store: &Store, live: &HashSet<ChunkId>) -> Result<PhysicalReport, StoreError> {
let index = store.object_index();
let mut report = PhysicalReport::default();
for seq in segment::list_segments(store.dir())? {
let seg = scan_segment_classified(
&segment::segment_path(store.dir(), seq),
seq,
store.config().max_records_per_segment,
live,
index,
)?;
report.file_bytes = report.file_bytes.saturating_add(seg.file_bytes);
report.live_bytes = report.live_bytes.saturating_add(seg.live_bytes);
report.dead_indexed_bytes = report
.dead_indexed_bytes
.saturating_add(seg.dead_indexed_bytes);
report.index_hidden_bytes = report
.index_hidden_bytes
.saturating_add(seg.index_hidden_bytes);
report.unindexed_bytes = report.unindexed_bytes.saturating_add(seg.unindexed_bytes);
report.torn_bytes = report.torn_bytes.saturating_add(seg.torn_bytes);
report.zero_padding_bytes = report
.zero_padding_bytes
.saturating_add(seg.zero_padding_bytes);
report.format_overhead_bytes = report
.format_overhead_bytes
.saturating_add(seg.format_overhead_bytes);
report.segments.push(seg);
}
Ok(report)
}
pub fn physical_report(store: &Store) -> Result<PhysicalReport, StoreError> {
let live = crate::store::gc::mark_live(store)?;
scan_physical(store, &live)
}
pub fn render(report: &PhysicalReport) -> String {
let mut out = String::new();
let pct = |b: u64| {
if report.file_bytes == 0 {
return 0.0;
}
100.0 * b as f64 / report.file_bytes as f64
};
out.push_str(&format!(
"physical reconciliation: {} B files\n",
report.file_bytes
));
out.push_str(&format!(
" live canonical {:>12} B ({:5.1}%)\n",
report.live_bytes,
pct(report.live_bytes)
));
out.push_str(&format!(
" dead indexed {:>12} B ({:5.1}%) [reclaimable]\n",
report.dead_indexed_bytes,
pct(report.dead_indexed_bytes)
));
out.push_str(&format!(
" index-hidden {:>12} B ({:5.1}%) [reclaimable]\n",
report.index_hidden_bytes,
pct(report.index_hidden_bytes)
));
out.push_str(&format!(
" unindexed {:>12} B ({:5.1}%) [reclaimable]\n",
report.unindexed_bytes,
pct(report.unindexed_bytes)
));
out.push_str(&format!(
" torn {:>12} B ({:5.1}%)\n",
report.torn_bytes,
pct(report.torn_bytes)
));
out.push_str(&format!(
" zero padding {:>12} B ({:5.1}%)\n",
report.zero_padding_bytes,
pct(report.zero_padding_bytes)
));
out.push_str(&format!(
" format overhead {:>12} B ({:5.1}%)\n",
report.format_overhead_bytes,
pct(report.format_overhead_bytes)
));
out.push_str(&format!(
" unexplained {:>12} B [must be 0]\n",
report.unexplained()
));
out
}