use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum ProgressPhase {
#[default]
Starting,
Loading,
Scanning,
Revalidating,
Indexing,
Analyzing,
Saving,
Summarizing,
}
impl ProgressPhase {
const ALL: [Self; 8] = [
Self::Starting,
Self::Loading,
Self::Scanning,
Self::Revalidating,
Self::Indexing,
Self::Analyzing,
Self::Saving,
Self::Summarizing,
];
const fn code(self) -> u8 {
match self {
Self::Starting => 0,
Self::Loading => 1,
Self::Scanning => 2,
Self::Revalidating => 3,
Self::Indexing => 4,
Self::Analyzing => 5,
Self::Saving => 6,
Self::Summarizing => 7,
}
}
fn from_code(code: u8) -> Self {
Self::ALL.get(usize::from(code)).copied().unwrap_or_default()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct ProgressSnapshot {
pub phase: ProgressPhase,
pub directories: u64,
pub files: u64,
pub bytes: u64,
pub allocated: u64,
pub analysis: Option<(u64, u64)>,
}
#[repr(align(128))]
#[derive(Default)]
struct WalkCells {
directories: AtomicU64,
files: AtomicU64,
bytes: AtomicU64,
allocated: AtomicU64,
}
#[repr(align(128))]
#[derive(Default)]
struct AnalysisCells {
known: AtomicBool,
done: AtomicU64,
total: AtomicU64,
}
#[repr(align(128))]
#[derive(Default)]
struct PhaseCell(AtomicU8);
#[derive(Default)]
struct Cells {
walk: WalkCells,
analysis: AnalysisCells,
phase: PhaseCell,
}
#[derive(Clone, Default)]
pub struct Progress {
cells: Arc<Cells>,
}
impl Progress {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn snapshot(&self) -> ProgressSnapshot {
let cells = &*self.cells;
let analysis = cells.analysis.known.load(Ordering::Acquire).then(|| {
(
cells.analysis.done.load(Ordering::Relaxed),
cells.analysis.total.load(Ordering::Relaxed),
)
});
ProgressSnapshot {
phase: ProgressPhase::from_code(cells.phase.0.load(Ordering::Acquire)),
directories: cells.walk.directories.load(Ordering::Relaxed),
files: cells.walk.files.load(Ordering::Relaxed),
bytes: cells.walk.bytes.load(Ordering::Relaxed),
allocated: cells.walk.allocated.load(Ordering::Relaxed),
analysis,
}
}
pub(crate) fn enter(&self, phase: ProgressPhase) {
self.cells.phase.0.store(phase.code(), Ordering::Relaxed);
}
#[cfg(feature = "watch")]
pub(crate) fn begin_pass(&self, phase: ProgressPhase) {
let walk = &self.cells.walk;
walk.directories.store(0, Ordering::Relaxed);
walk.files.store(0, Ordering::Relaxed);
walk.bytes.store(0, Ordering::Relaxed);
walk.allocated.store(0, Ordering::Relaxed);
self.cells.phase.0.store(phase.code(), Ordering::Release);
}
pub(crate) fn add_walked(&self, directories: u64, files: u64, bytes: u64, allocated: u64) {
let walk = &self.cells.walk;
walk.directories.fetch_add(directories, Ordering::Relaxed);
walk.files.fetch_add(files, Ordering::Relaxed);
walk.bytes.fetch_add(bytes, Ordering::Relaxed);
walk.allocated.fetch_add(allocated, Ordering::Relaxed);
}
pub(crate) fn begin_analysis(&self, total: u64) {
let analysis = &self.cells.analysis;
analysis.total.store(total, Ordering::Relaxed);
analysis.known.store(true, Ordering::Release);
}
pub(crate) fn add_analyzed(&self, files: u64) {
self.cells.analysis.done.fetch_add(files, Ordering::Relaxed);
}
}
impl fmt::Debug for Progress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let ProgressSnapshot { phase, directories, files, bytes, allocated, analysis } =
self.snapshot();
f.debug_struct("Progress")
.field("phase", &phase)
.field("directories", &directories)
.field("files", &files)
.field("bytes", &bytes)
.field("allocated", &allocated)
.field("analysis", &analysis)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fresh_handle_is_starting_with_nothing_counted() {
assert_eq!(
Progress::new().snapshot(),
ProgressSnapshot {
phase: ProgressPhase::Starting,
directories: 0,
files: 0,
bytes: 0,
allocated: 0,
analysis: None,
}
);
}
#[test]
fn clones_share_one_set_of_counters() {
let polled = Progress::new();
let handed_to_route = polled.clone();
handed_to_route.enter(ProgressPhase::Scanning);
handed_to_route.add_walked(2, 5, 700, 8_192);
handed_to_route.add_walked(1, 0, 0, 0);
assert_eq!(
polled.snapshot(),
ProgressSnapshot {
phase: ProgressPhase::Scanning,
directories: 3,
files: 5,
bytes: 700,
allocated: 8_192,
analysis: None,
}
);
}
#[test]
fn analysis_is_unknown_until_a_total_is_recorded() {
let progress = Progress::new();
progress.add_analyzed(1);
assert_eq!(progress.snapshot().analysis, None, "a count without a denominator");
progress.begin_analysis(4);
assert_eq!(progress.snapshot().analysis, Some((1, 4)));
progress.add_analyzed(3);
assert_eq!(progress.snapshot().analysis, Some((4, 4)));
}
#[test]
fn every_phase_survives_the_cell_round_trip() {
let progress = Progress::new();
for phase in ProgressPhase::ALL {
progress.enter(phase);
assert_eq!(progress.snapshot().phase, phase);
assert_eq!(ProgressPhase::from_code(phase.code()), phase);
}
assert_eq!(ProgressPhase::from_code(u8::MAX), ProgressPhase::Starting);
}
#[test]
fn debug_shows_the_snapshot_rather_than_the_cells() {
let progress = Progress::new();
progress.enter(ProgressPhase::Analyzing);
progress.begin_analysis(2);
assert_eq!(
format!("{progress:?}"),
"Progress { phase: Analyzing, directories: 0, files: 0, bytes: 0, allocated: 0, analysis: Some((0, 2)) }"
);
}
#[test]
fn the_shared_cells_keep_each_writer_on_its_own_line() {
assert_eq!(std::mem::align_of::<WalkCells>(), 128);
assert_eq!(std::mem::align_of::<AnalysisCells>(), 128);
assert_eq!(std::mem::align_of::<PhaseCell>(), 128);
assert!(std::mem::size_of::<WalkCells>() <= 128, "the four walk counters fit one line");
}
}