mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 01:48:20 -10:00
perf(snapshot): drop old undo snapshots half a window at a time
Native acceptance of a 910-turn session showed every turn from about the sixteenth starting 2.8-3.0 s late (0.45-0.6 s before), constant afterwards. Cause: the prune that follows every snapshot. Once the store held more than the 50-snapshot window (about three snapshots per turn), each new snapshot removed one old one by rebuilding the whole survivor chain, two git processes per survivor, on the path before the provider request. The per-snapshot prune now waits until half a window is due to go and drops it in one rebuild, so the rebuild runs about once every eight turns instead of every turn. The store holds at most half a window more than before; the size-pressure prune and the turn-boundary retention rule are unchanged. The unbatched form remains for tests. Local macOS, Rust 1.99: snapshot, turn and prune selection 129 passed / 0 failed, including the new batched-prune test (one over the cap: no rebuild; half a window over: dropped together, newest kept). CI-policy Clippy, whole workspace: exit 0. In a larger parallel selection six runtime_threads tests timed out once and passed 3 of 3 alone; recorded as load-sensitive, not explained. The turn-start delay itself has not been re-measured natively with this change. Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
1 parent
15f79ed6cc
commit
2d1971debe
4 files changed
+51
-4
No files matched your search
@@ -103,6 +103,12 @@ quieter, and Fleet runs can be checked before they spend anything.
|
||||
under the stream retry budget when nothing had streamed; authentication and
|
||||
invalid-model frames still fail at once
|
||||
([#6795](https://github.com/Hmbown/Codewhale/issues/6795)).
|
||||
- Long sessions no longer start every turn late. Once a workspace held more
|
||||
than 50 undo snapshots (around the sixteenth turn), each new snapshot rebuilt
|
||||
the whole snapshot history before the provider request, about 2.8 s per turn
|
||||
in a small workspace. Old snapshots are now dropped half a window at a time.
|
||||
- Resuming a crashed session from inside the TUI recovers its interrupted
|
||||
turn too.
|
||||
- Release builds compile on Rust 1.99.
|
||||
- TUI undo and retry rewind the Engine conversation and saved session before
|
||||
replacement inference. If the conversation or its settings change while
|
||||
|
||||
@@ -103,6 +103,12 @@ quieter, and Fleet runs can be checked before they spend anything.
|
||||
under the stream retry budget when nothing had streamed; authentication and
|
||||
invalid-model frames still fail at once
|
||||
([#6795](https://github.com/Hmbown/Codewhale/issues/6795)).
|
||||
- Long sessions no longer start every turn late. Once a workspace held more
|
||||
than 50 undo snapshots (around the sixteenth turn), each new snapshot rebuilt
|
||||
the whole snapshot history before the provider request, about 2.8 s per turn
|
||||
in a small workspace. Old snapshots are now dropped half a window at a time.
|
||||
- Resuming a crashed session from inside the TUI recovers its interrupted
|
||||
turn too.
|
||||
- Release builds compile on Rust 1.99.
|
||||
- TUI undo and retry rewind the Engine conversation and saved session before
|
||||
replacement inference. If the conversation or its settings change while
|
||||
|
||||
@@ -740,7 +740,7 @@ fn snapshot_with_label_since(
|
||||
_ => None,
|
||||
};
|
||||
// Prune oldest snapshots to cap disk usage (#1112).
|
||||
if let Err(e) = repo.prune_keep_last_n(max_snapshots_for(workspace)) {
|
||||
if let Err(e) = repo.prune_keep_last_n_batched(max_snapshots_for(workspace)) {
|
||||
tracing::warn!(target: "snapshot", "snapshot prune failed: {e}");
|
||||
}
|
||||
id.map(|taken| (taken, changed))
|
||||
|
||||
@@ -1897,11 +1897,22 @@ impl SnapshotRepo {
|
||||
/// The survivors are rebuilt as a fresh orphan chain; each keeps its
|
||||
/// tree, label, session id and timestamp, and the dropped ones become
|
||||
/// unreachable for gc to reclaim.
|
||||
#[cfg(test)]
|
||||
pub fn prune_keep_last_n(&self, max_count: usize) -> io::Result<usize> {
|
||||
self.with_write_lock(|| self.prune_keep_last_n_locked(max_count))
|
||||
self.with_write_lock(|| self.prune_keep_last_n_locked(max_count, 1))
|
||||
}
|
||||
|
||||
fn prune_keep_last_n_locked(&self, max_count: usize) -> io::Result<usize> {
|
||||
/// [`Self::prune_keep_last_n`] for the prune that follows every snapshot:
|
||||
/// it waits until half a window of snapshots is due to go and drops them
|
||||
/// together. Rebuilding the survivor chain costs two git processes per
|
||||
/// survivor, and doing that after every snapshot past the cap put seconds
|
||||
/// in front of every later turn's provider request. The store then holds
|
||||
/// at most half a window more than [`Self::prune_keep_last_n`] would keep.
|
||||
pub fn prune_keep_last_n_batched(&self, max_count: usize) -> io::Result<usize> {
|
||||
self.with_write_lock(|| self.prune_keep_last_n_locked(max_count, (max_count / 2).max(1)))
|
||||
}
|
||||
|
||||
fn prune_keep_last_n_locked(&self, max_count: usize, min_removed: usize) -> io::Result<usize> {
|
||||
let snapshots = self.list(usize::MAX)?;
|
||||
if snapshots.len() <= max_count {
|
||||
return Ok(0);
|
||||
@@ -1923,7 +1934,7 @@ impl SnapshotRepo {
|
||||
.map(|(_, snapshot)| snapshot.clone())
|
||||
.collect();
|
||||
let removed = snapshots.len() - survivors.len();
|
||||
if removed == 0 || survivors.is_empty() {
|
||||
if removed < min_removed || survivors.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
self.rebuild_survivor_chain(&survivors, &snapshots[0].id)?;
|
||||
@@ -3800,6 +3811,30 @@ mod tests {
|
||||
assert_eq!(repo.list(usize::MAX).unwrap().len(), 2);
|
||||
}
|
||||
|
||||
/// The per-snapshot prune drops half a window at once instead of
|
||||
/// rebuilding the chain for every snapshot past the cap.
|
||||
#[test]
|
||||
fn batched_prune_waits_for_half_a_window_then_drops_it_together() {
|
||||
let tmp = tempdir().unwrap();
|
||||
let (repo, _home) = make_repo(tmp.path());
|
||||
let max = 4;
|
||||
for n in 0..max + 1 {
|
||||
std::fs::write(repo.work_tree().join("f.txt"), format!("{n}")).unwrap();
|
||||
repo.snapshot(&format!("tool:{n}")).unwrap();
|
||||
}
|
||||
// One over the cap: the plain prune would rebuild now, the batched
|
||||
// one waits.
|
||||
assert_eq!(repo.prune_keep_last_n_batched(max).unwrap(), 0);
|
||||
assert_eq!(repo.list(usize::MAX).unwrap().len(), max + 1);
|
||||
|
||||
std::fs::write(repo.work_tree().join("f.txt"), "last").unwrap();
|
||||
repo.snapshot("tool:last").unwrap();
|
||||
assert_eq!(repo.prune_keep_last_n_batched(max).unwrap(), 2);
|
||||
let kept = repo.list(usize::MAX).unwrap();
|
||||
assert_eq!(kept.len(), max);
|
||||
assert_eq!(kept[0].label, "tool:last", "the newest snapshots survive");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prune_keep_last_n_preserves_multiple_snapshots_in_order() {
|
||||
let tmp = tempdir().unwrap();
|
||||
|
||||
Reference in new issue
Block a user