arc system df (#11)

* arc(01KK9YGJ6EBM3XKQBZC23GQ88F): toolchain (success)

Arc-Run: 01KK9YGJ6EBM3XKQBZC23GQ88F
Arc-Completed: 2
Arc-Checkpoint: cf6f964b878309dc8d79a9aeea0fa9c7bd4cff8f

* arc(01KK9YGJ6EBM3XKQBZC23GQ88F): preflight_compile (success)

Arc-Run: 01KK9YGJ6EBM3XKQBZC23GQ88F
Arc-Completed: 3
Arc-Checkpoint: ff8dba601f46c90b5b37d78804e77c42bbc4e712

* arc(01KK9YGJ6EBM3XKQBZC23GQ88F): preflight_lint (success)

Arc-Run: 01KK9YGJ6EBM3XKQBZC23GQ88F
Arc-Completed: 4
Arc-Checkpoint: 0d6b2bd079847ef5be18f8e3100ed76fc846ded3

* arc(01KK9YGJ6EBM3XKQBZC23GQ88F): implement (success)

Arc-Run: 01KK9YGJ6EBM3XKQBZC23GQ88F
Arc-Completed: 5
Arc-Checkpoint: 4bd461b413389877d773ef248ecdd798bce43471

* arc(01KK9YGJ6EBM3XKQBZC23GQ88F): simplify (success)

Arc-Run: 01KK9YGJ6EBM3XKQBZC23GQ88F
Arc-Completed: 6
Arc-Checkpoint: c8ad93c70cc29c0dd5680a75c22292ca7bac874c

* arc(01KK9YGJ6EBM3XKQBZC23GQ88F): verify (success)

Arc-Run: 01KK9YGJ6EBM3XKQBZC23GQ88F
Arc-Completed: 7
Arc-Checkpoint: efb5421ea6bd57a350ba3606b1ab20e98142d68f

---------

Co-authored-by: arc <arc@local>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arc-1e68f1[bot] 2026-03-09 15:15:27 -04:00 committed by GitHub
parent d1523ea36f
commit 6cdb91e5fa
4 changed files with 317 additions and 2 deletions

1
Cargo.lock generated
View file

@ -447,6 +447,7 @@ dependencies = [
"tracing",
"ulid",
"uuid",
"walkdir",
]
[[package]]

View file

@ -100,6 +100,8 @@ enum PrCommand {
enum SystemCommand {
/// Delete old workflow runs
Prune(arc_workflows::cli::runs::RunsPruneArgs),
/// Show disk usage
Df(arc_workflows::cli::runs::DfArgs),
}
#[derive(Subcommand)]
@ -354,6 +356,9 @@ async fn main() -> Result<()> {
SystemCommand::Prune(args) => {
arc_workflows::cli::runs::prune_command(&args)?;
}
SystemCommand::Df(args) => {
arc_workflows::cli::runs::df_command(&args)?;
}
},
}

View file

@ -47,6 +47,7 @@ console.workspace = true
indicatif.workspace = true
tokio-util.workspace = true
tracing.workspace = true
walkdir.workspace = true
reqwest.workspace = true
[dev-dependencies]
mockito = "1"

View file

@ -183,11 +183,14 @@ fn parse_label_filters(label_args: &[String]) -> Vec<(String, String)> {
.collect()
}
pub(crate) fn default_logs_base() -> PathBuf {
fn default_data_dir() -> PathBuf {
dirs::home_dir()
.expect("could not determine home directory")
.join(".arc")
.join("logs")
}
pub(crate) fn default_logs_base() -> PathBuf {
default_data_dir().join("logs")
}
/// Find a run directory by prefix match against run IDs.
@ -274,6 +277,209 @@ pub fn list_command(args: &RunsListArgs) -> Result<()> {
Ok(())
}
#[derive(Args)]
pub struct DfArgs {
/// Show per-run breakdown
#[arg(short, long)]
pub verbose: bool,
}
fn dir_size(path: &Path) -> u64 {
walkdir::WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter_map(|e| e.metadata().ok())
.filter(|m| m.is_file())
.map(|m| m.len())
.sum()
}
fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{bytes} B")
}
}
pub fn df_command(args: &DfArgs) -> Result<()> {
let data_dir = default_data_dir();
let logs_base = data_dir.join("logs");
df_from(args, &data_dir, &logs_base)
}
pub fn df_from(args: &DfArgs, data_dir: &Path, logs_base: &Path) -> Result<()> {
// --- Runs ---
let runs = scan_runs(logs_base)?;
let mut active_count = 0u64;
let mut total_run_size = 0u64;
let mut reclaimable_run_size = 0u64;
struct RunSizeInfo {
run_id: String,
workflow_name: String,
status: String,
start_time: String,
size: u64,
}
let mut run_details: Vec<RunSizeInfo> = Vec::new();
for run in &runs {
let size = dir_size(&run.path);
total_run_size += size;
let is_active = run.status == "running";
if is_active {
active_count += 1;
} else {
reclaimable_run_size += size;
}
if args.verbose {
run_details.push(RunSizeInfo {
run_id: run.run_id.clone(),
workflow_name: run.workflow_name.clone(),
status: run.status.clone(),
start_time: run.start_time.clone(),
size,
});
}
}
// --- Logs ---
let mut log_count = 0u64;
let mut total_log_size = 0u64;
if let Ok(entries) = std::fs::read_dir(logs_base) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "log" {
if let Ok(meta) = path.metadata() {
log_count += 1;
total_log_size += meta.len();
}
}
}
}
}
}
// --- Databases ---
let mut db_count = 0u64;
let mut total_db_size = 0u64;
if let Ok(entries) = std::fs::read_dir(data_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
let name = entry.file_name().to_string_lossy().to_string();
if name.ends_with(".db") || name.ends_with(".db-wal") || name.ends_with(".db-shm") {
if let Ok(meta) = path.metadata() {
db_count += 1;
total_db_size += meta.len();
}
}
}
}
}
// --- Summary table ---
let run_reclaim_pct = if total_run_size > 0 {
(reclaimable_run_size as f64 / total_run_size as f64 * 100.0) as u64
} else {
0
};
let log_reclaim_pct = if total_log_size > 0 { 100 } else { 0 };
println!(
"{:<14}{:>5}{:>11}{:>12}{:>16}",
"TYPE", "COUNT", "ACTIVE", "SIZE", "RECLAIMABLE"
);
println!(
"{:<14}{:>5}{:>11}{:>12}{:>12} ({run_reclaim_pct}%)",
"Runs",
runs.len(),
active_count,
format_size(total_run_size),
format_size(reclaimable_run_size),
);
println!(
"{:<14}{:>5}{:>11}{:>12}{:>12} ({log_reclaim_pct}%)",
"Logs",
log_count,
"-",
format_size(total_log_size),
format_size(total_log_size),
);
println!(
"{:<14}{:>5}{:>11}{:>12}{:>12} (0%)",
"Databases",
db_count,
"-",
format_size(total_db_size),
format_size(0),
);
println!();
println!("Data directory: {}", data_dir.display());
// --- Verbose per-run breakdown ---
if args.verbose {
println!();
println!(
"{:<30} {:<18} {:<10} {:>5} {:>12}",
"RUN ID", "WORKFLOW", "STATUS", "AGE", "SIZE"
);
let now = chrono::Utc::now();
for detail in &run_details {
let run_id_display = if detail.run_id.len() > 28 {
format!("{}...", &detail.run_id[..25])
} else {
detail.run_id.clone()
};
let workflow_display = if detail.workflow_name.len() > 16 {
format!("{}...", &detail.workflow_name[..13])
} else {
detail.workflow_name.clone()
};
let age = if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&detail.start_time) {
let dur = now.signed_duration_since(dt);
if dur.num_days() > 0 {
format!("{}d", dur.num_days())
} else if dur.num_hours() > 0 {
format!("{}h", dur.num_hours())
} else {
format!("{}m", dur.num_minutes().max(1))
}
} else {
"-".to_string()
};
let reclaimable_marker = if detail.status != "running" { " *" } else { "" };
println!(
"{:<30} {:<18} {:<10} {:>5} {:>10}{}",
run_id_display,
workflow_display,
detail.status,
age,
format_size(detail.size),
reclaimable_marker,
);
}
println!();
println!("* = reclaimable");
}
Ok(())
}
pub fn prune_command(args: &RunsPruneArgs) -> Result<()> {
let base = default_logs_base();
prune_from(args, &base)
@ -689,6 +895,108 @@ mod tests {
assert!(result.is_empty());
}
#[test]
fn dir_size_works() {
let tmp = tempfile::tempdir().unwrap();
let base = tmp.path();
// Create nested files with known sizes
fs::write(base.join("a.txt"), "hello").unwrap(); // 5 bytes
let sub = base.join("sub");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("b.txt"), "world!").unwrap(); // 6 bytes
assert_eq!(dir_size(base), 11);
}
#[test]
fn df_reports_run_sizes() {
let tmp = tempfile::tempdir().unwrap();
let data_dir = tmp.path();
let logs_base = data_dir.join("logs");
fs::create_dir(&logs_base).unwrap();
// Running run
make_run_dir(
&logs_base,
"20260308-RUNNING",
Some(serde_json::json!({
"run_id": "running-1",
"workflow_name": "code-review",
"goal": "",
"start_time": "2026-03-08T10:00:00Z",
"node_count": 1,
"edge_count": 0
})),
None,
true,
);
// Add a file to give it size
fs::write(
logs_base.join("20260308-RUNNING").join("data.bin"),
vec![0u8; 100],
)
.unwrap();
// Completed run
make_run_dir(
&logs_base,
"20260307-DONE",
Some(serde_json::json!({
"run_id": "done-1",
"workflow_name": "deploy",
"goal": "",
"start_time": "2026-03-07T10:00:00Z",
"node_count": 1,
"edge_count": 0
})),
Some(serde_json::json!({
"timestamp": "2026-03-07T10:01:00Z",
"status": "success",
"duration_ms": 60000
})),
false,
);
fs::write(
logs_base.join("20260307-DONE").join("data.bin"),
vec![0u8; 200],
)
.unwrap();
let args = DfArgs { verbose: false };
// Should not panic
df_from(&args, data_dir, &logs_base).unwrap();
}
#[test]
fn df_reports_log_files() {
let tmp = tempfile::tempdir().unwrap();
let data_dir = tmp.path();
let logs_base = data_dir.join("logs");
fs::create_dir(&logs_base).unwrap();
fs::write(logs_base.join("cli-2026-03-08.log"), vec![0u8; 500]).unwrap();
fs::write(logs_base.join("serve-2026-03-08.log"), vec![0u8; 300]).unwrap();
let args = DfArgs { verbose: false };
df_from(&args, data_dir, &logs_base).unwrap();
}
#[test]
fn df_reports_database_files() {
let tmp = tempfile::tempdir().unwrap();
let data_dir = tmp.path();
let logs_base = data_dir.join("logs");
fs::create_dir(&logs_base).unwrap();
fs::write(data_dir.join("arc.db"), vec![0u8; 1024]).unwrap();
fs::write(data_dir.join("arc.db-wal"), vec![0u8; 512]).unwrap();
fs::write(data_dir.join("arc.db-shm"), vec![0u8; 32]).unwrap();
let args = DfArgs { verbose: false };
df_from(&args, data_dir, &logs_base).unwrap();
}
#[test]
fn find_run_by_prefix_no_match() {
let dir = tempfile::tempdir().unwrap();