///|
/// Aggregated file activity for one directory bucket between two snapshots.
pub(all) struct DirectoryHotspot {
path : String
added_files : Int
removed_files : Int
changed_files : Int
added_bytes : Int64
removed_bytes : Int64
grown_bytes : Int64
shrunk_bytes : Int64
net_bytes : Int64
} derive(Eq, Debug, ToJson, FromJson)
///|
fn hotspot_bucket(path : String, depth : Int) -> String {
let parent = parent_path(path)
if parent == "" || depth <= 0 {
return ""
}
let parts = parent.split("/").to_array()
let count = if depth < parts.length() { depth } else { parts.length() }
parts[:count].join("/")
}
///|
fn hotspot_add_int(map : Map[String, Int], path : String, value : Int) -> Unit {
map[path] = map.get(path).unwrap_or(0) + value
}
///|
fn hotspot_add_bytes(
map : Map[String, Int64],
path : String,
value : Int64,
) -> Unit {
map[path] = map.get(path).unwrap_or(0L) + value
}
///|
fn changed_file_before(change : EntryChange) -> Entry? {
match change.before {
Some(entry) if entry.is_file() => Some(entry)
_ => None
}
}
///|
fn changed_file_after(change : EntryChange) -> Entry? {
match change.after {
Some(entry) if entry.is_file() => Some(entry)
_ => None
}
}
///|
/// Group changed files by the first `depth` directory segments.
/// A depth of one answers which top-level directory caused the most activity;
/// zero combines all root-relative changes into one bucket.
pub fn directory_hotspots(
diff : SnapshotDiff,
depth? : Int = 1,
) -> Array[DirectoryHotspot] {
let added_counts : Map[String, Int] = Map([])
let removed_counts : Map[String, Int] = Map([])
let changed_counts : Map[String, Int] = Map([])
let added_sizes : Map[String, Int64] = Map([])
let removed_sizes : Map[String, Int64] = Map([])
let grown_sizes : Map[String, Int64] = Map([])
let shrunk_sizes : Map[String, Int64] = Map([])
let paths : Map[String, Bool] = Map([])
for change in diff.changes {
let before = changed_file_before(change)
let after = changed_file_after(change)
if before is None && after is None {
continue
}
let path = hotspot_bucket(change.path, depth)
paths[path] = true
match (before, after) {
(None, Some(entry)) => {
hotspot_add_int(added_counts, path, 1)
hotspot_add_bytes(added_sizes, path, entry.size)
}
(Some(entry), None) => {
hotspot_add_int(removed_counts, path, 1)
hotspot_add_bytes(removed_sizes, path, entry.size)
}
(Some(old), Some(current)) => {
hotspot_add_int(changed_counts, path, 1)
if current.size > old.size {
hotspot_add_bytes(grown_sizes, path, current.size - old.size)
} else if old.size > current.size {
hotspot_add_bytes(shrunk_sizes, path, old.size - current.size)
}
}
(None, None) => ()
}
}
let result : Array[DirectoryHotspot] = []
for path, _ in paths {
let added = added_sizes.get(path).unwrap_or(0L)
let removed = removed_sizes.get(path).unwrap_or(0L)
let grown = grown_sizes.get(path).unwrap_or(0L)
let shrunk = shrunk_sizes.get(path).unwrap_or(0L)
result.push(DirectoryHotspot::{
path,
added_files: added_counts.get(path).unwrap_or(0),
removed_files: removed_counts.get(path).unwrap_or(0),
changed_files: changed_counts.get(path).unwrap_or(0),
added_bytes: added,
removed_bytes: removed,
grown_bytes: grown,
shrunk_bytes: shrunk,
net_bytes: added + grown - removed - shrunk,
})
}
result.sort_by(fn(a, b) {
let a_activity = a.added_bytes +
a.removed_bytes +
a.grown_bytes +
a.shrunk_bytes
let b_activity = b.added_bytes +
b.removed_bytes +
b.grown_bytes +
b.shrunk_bytes
if a_activity > b_activity {
-1
} else if a_activity < b_activity {
1
} else if a.path < b.path {
-1
} else if a.path > b.path {
1
} else {
0
}
})
result
}
///|
pub fn render_directory_hotspots(
hotspots : Array[DirectoryHotspot],
limit? : Int = 20,
) -> String {
let lines : Array[String] = [
"MoonDisk directory growth hotspots",
"Directories: " + hotspots.length().to_string(),
"",
"Net change Added Removed Changed Directory",
]
let count = if limit < 0 {
0
} else if limit < hotspots.length() {
limit
} else {
hotspots.length()
}
for index = 0; index < count; index = index + 1 {
let item = hotspots[index]
let display_path = if item.path == "" { "." } else { item.path }
lines.push(
pad_left(format_bytes(item.net_bytes), 12) +
" " +
pad_left(item.added_files.to_string(), 8) +
" " +
pad_left(item.removed_files.to_string(), 9) +
" " +
pad_left(item.changed_files.to_string(), 9) +
" " +
display_path,
)
}
lines.join("\n")
}
///|
pub fn directory_hotspots_to_json(
hotspots : Array[DirectoryHotspot],
pretty? : Bool = true,
) -> String {
let value = ToJson::to_json(hotspots)
if pretty {
value.stringify(indent=2)
} else {
value.stringify()
}
}