Report thread storage changes after rollout migration (#38579)

## What changed

Measure thread storage before and after `codex migrate-rollouts --apply` and
include the formatted sizes in the human-readable migration report. The total
includes active and archived rollout files, the thread history database, and
its WAL and shared-memory sidecars.

Keep dry-run and JSON output unchanged, and omit the storage summary if either
measurement fails.

GitOrigin-RevId: 1775c0b6849570a814becd94e196dd05908667a1
This commit is contained in:
Owen Lin
2026-08-14 13:40:35 +00:00
committed by copyberry
parent 1c4f42863c
commit 42d842a91a
+82 -1
View File
@@ -1,6 +1,7 @@
use std::io;
use std::io::IsTerminal;
use std::io::Write;
use std::path::Path;
use std::time::Duration;
use std::time::Instant;
@@ -73,6 +74,17 @@ pub(crate) async fn run(
};
let json = command.json;
let verbose = command.verbose;
let thread_history_db_path = config.sqlite.thread_history_db_path();
let thread_storage_before = if mode == RolloutMigrationMode::Apply && !json {
thread_storage_bytes(
config.codex_home.as_path(),
thread_history_db_path.as_path(),
)
.await
.ok()
} else {
None
};
let state_db = if mode == RolloutMigrationMode::Apply {
Some(
codex_rollout::state_db::try_init(&config)
@@ -97,11 +109,21 @@ pub(crate) async fn run(
.await;
progress.finish();
let report = result?;
let thread_storage = match thread_storage_before {
Some(before) => thread_storage_bytes(
config.codex_home.as_path(),
thread_history_db_path.as_path(),
)
.await
.ok()
.map(|after| (before, after)),
None => None,
};
if json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
print_human_report(&report, mode, verbose, progress.elapsed());
print_human_report(&report, mode, verbose, progress.elapsed(), thread_storage);
}
if report
@@ -272,6 +294,7 @@ fn print_human_report(
mode: RolloutMigrationMode,
verbose: bool,
elapsed: Duration,
thread_storage: Option<(u64, u64)>,
) {
let mut counts = MigrationCounts::default();
for outcome in &report.outcomes {
@@ -304,6 +327,13 @@ fn print_human_report(
counts.failed,
),
}
if let Some((before, after)) = thread_storage {
println!(
"Disk used for thread storage: {} -> {}",
format_bytes(before),
format_bytes(after)
);
}
if mode == RolloutMigrationMode::DryRun && counts.eligible > 0 {
println!("Run `codex migrate-rollouts --apply` to migrate eligible sessions.");
}
@@ -367,3 +397,54 @@ fn format_elapsed(elapsed: Duration) -> String {
}
format!("{hours}h{:02}m{:02}s", minutes % 60, seconds % 60)
}
async fn thread_storage_bytes(codex_home: &Path, thread_history_db_path: &Path) -> io::Result<u64> {
let mut bytes = 0_u64;
let mut directories = vec![
codex_home.join(codex_rollout::SESSIONS_SUBDIR),
codex_home.join(codex_rollout::ARCHIVED_SESSIONS_SUBDIR),
];
while let Some(directory) = directories.pop() {
let mut entries = match tokio::fs::read_dir(&directory).await {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => continue,
Err(error) => return Err(error),
};
while let Some(entry) = entries.next_entry().await? {
let file_type = entry.file_type().await?;
if file_type.is_dir() {
directories.push(entry.path());
} else if file_type.is_file() {
bytes = bytes.saturating_add(entry.metadata().await?.len());
}
}
}
for suffix in ["", "-wal", "-shm"] {
let mut path = thread_history_db_path.as_os_str().to_owned();
path.push(suffix);
match tokio::fs::metadata(path).await {
Ok(metadata) => bytes = bytes.saturating_add(metadata.len()),
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error),
}
}
Ok(bytes)
}
fn format_bytes(bytes: u64) -> String {
const KIB: f64 = 1024.0;
const MIB: f64 = KIB * 1024.0;
const GIB: f64 = MIB * 1024.0;
let bytes = bytes as f64;
if bytes >= GIB {
format!("{:.1} GB", bytes / GIB)
} else if bytes >= MIB {
format!("{:.1} MB", bytes / MIB)
} else if bytes >= KIB {
format!("{:.1} KB", bytes / KIB)
} else {
format!("{} B", bytes as u64)
}
}