///|
pub(all) struct SnapshotHistory {
format_version : Int
root : String
snapshots : Array[Snapshot]
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct HistoryPoint {
created_at : Int64
total_size : Int64
files : Int
directories : Int
scan_errors : Int
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct HistorySummary {
observations : Int
first_at : Int64
last_at : Int64
first_size : Int64
last_size : Int64
minimum_size : Int64
maximum_size : Int64
net_change : Int64
average_change : Int64
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct PathTrend {
path : String
first_size : Int64
last_size : Int64
minimum_size : Int64
maximum_size : Int64
net_change : Int64
observations : Int
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn SnapshotHistory::new(root : String) -> SnapshotHistory {
SnapshotHistory::{ format_version: 1, root, snapshots: [], }
}
///|
pub fn history_from_snapshot(snapshot : Snapshot) -> SnapshotHistory {
SnapshotHistory::{
format_version: 1,
root: snapshot.root,
snapshots: [snapshot],
}
}
///|
pub fn append_history_snapshot(
history : SnapshotHistory,
snapshot : Snapshot,
) -> SnapshotHistory raise {
if history.root != snapshot.root {
raise Failure("Snapshot root does not match history root: " + snapshot.root)
}
if !history.snapshots.is_empty() &&
snapshot.created_at <=
history.snapshots[history.snapshots.length() - 1].created_at {
raise Failure("History snapshots must have increasing creation times")
}
let snapshots = history.snapshots.copy()
snapshots.push(snapshot)
{ ..history, snapshots, }
}
///|
pub fn history_points(history : SnapshotHistory) -> Array[HistoryPoint] {
history.snapshots.map(fn(snapshot) {
HistoryPoint::{
created_at: snapshot.created_at,
total_size: snapshot.total_size(),
files: snapshot.file_count(),
directories: snapshot.directory_count(),
scan_errors: snapshot.errors.length(),
}
})
}
///|
pub fn summarize_history(history : SnapshotHistory) -> HistorySummary? {
if history.snapshots.is_empty() {
return None
}
let points = history_points(history)
let first = points[0]
let last = points[points.length() - 1]
let mut minimum = first.total_size
let mut maximum = first.total_size
for point in points {
if point.total_size < minimum {
minimum = point.total_size
}
if point.total_size > maximum {
maximum = point.total_size
}
}
let net = last.total_size - first.total_size
HistorySummary::{
observations: points.length(),
first_at: first.created_at,
last_at: last.created_at,
first_size: first.total_size,
last_size: last.total_size,
minimum_size: minimum,
maximum_size: maximum,
net_change: net,
average_change: if points.length() < 2 {
0L
} else {
net / (points.length() - 1).to_int64()
},
}
|> Some
}
///|
fn file_size_map(snapshot : Snapshot) -> Map[String, Int64] {
let sizes : Map[String, Int64] = Map([])
for entry in snapshot.entries {
if entry.is_file() {
sizes[entry.path] = entry.size
}
}
sizes
}
///|
pub fn path_trends(history : SnapshotHistory) -> Array[PathTrend] {
if history.snapshots.is_empty() {
return []
}
let paths : Map[String, Bool] = Map([])
let maps : Array[Map[String, Int64]] = []
for snapshot in history.snapshots {
let sizes = file_size_map(snapshot)
for path, _ in sizes {
paths[path] = true
}
maps.push(sizes)
}
let result : Array[PathTrend] = []
for path, _ in paths {
let first_size = maps[0].get(path).unwrap_or(0L)
let last_size = maps[maps.length() - 1].get(path).unwrap_or(0L)
let mut minimum = first_size
let mut maximum = first_size
let mut observations = 0
for sizes in maps {
let value = sizes.get(path).unwrap_or(0L)
if sizes.get(path) is Some(_) {
observations += 1
}
if value < minimum {
minimum = value
}
if value > maximum {
maximum = value
}
}
result.push(PathTrend::{
path,
first_size,
last_size,
minimum_size: minimum,
maximum_size: maximum,
net_change: last_size - first_size,
observations,
})
}
result.sort_by(fn(a, b) {
if a.net_change > b.net_change {
-1
} else if a.net_change < b.net_change {
1
} else if a.path < b.path {
-1
} else {
1
}
})
result
}
///|
pub fn fastest_growing_paths(
history : SnapshotHistory,
limit : Int,
) -> Array[PathTrend] {
let growing = path_trends(history).filter(fn(trend) { trend.net_change > 0L })
if growing.length() > limit {
growing[:limit].to_owned()
} else {
growing
}
}
///|
pub fn render_history(history : SnapshotHistory, limit? : Int = 10) -> String {
let lines : Array[String] = [
"MoonDisk snapshot history",
"Root: " + history.root,
"Observations: " + history.snapshots.length().to_string(),
]
match summarize_history(history) {
None => lines.push("No snapshots have been recorded.")
Some(summary) => {
lines.push("First size: " + format_bytes(summary.first_size))
lines.push("Latest size: " + format_bytes(summary.last_size))
lines.push("Net change: " + format_bytes(summary.net_change))
lines.push("Peak size: " + format_bytes(summary.maximum_size))
lines.push("")
lines.push("Fastest-growing files")
for trend in fastest_growing_paths(history, limit) {
lines.push(
pad_left(format_bytes(trend.net_change), 12) + " " + trend.path,
)
}
}
}
lines.join("\n")
}
///|
pub fn history_to_json(history : SnapshotHistory) -> String {
ToJson::to_json(history).stringify(indent=2)
}
///|
pub fn history_from_json(source : String) -> SnapshotHistory raise {
let history : SnapshotHistory = @json.from_json(@json.parse(source))
if history.format_version != 1 {
raise Failure("Unsupported snapshot history version")
}
let mut previous = -1L
for snapshot in history.snapshots {
if snapshot.root != history.root {
raise Failure("Snapshot root does not match history root")
}
if snapshot.created_at <= previous {
raise Failure("Snapshot history is not ordered by creation time")
}
previous = snapshot.created_at
}
history
}
///|
pub async fn write_history(path : String, history : SnapshotHistory) -> Unit {
@fs.write_file(path, history_to_json(history))
}
///|
pub async fn read_history(path : String) -> SnapshotHistory {
history_from_json(@fs.read_file(path).text())
}