Compare commits
2 Commits
4fc423314b
...
76a5dd5b26
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76a5dd5b26 | ||
|
|
1f27270d66 |
@@ -228,6 +228,8 @@ impl App {
|
|||||||
// Before the counters are taken, so they measure what is
|
// Before the counters are taken, so they measure what is
|
||||||
// actually reachable.
|
// actually reachable.
|
||||||
sweep_orphan_checkpoints(&kv_cache_path(), &projects);
|
sweep_orphan_checkpoints(&kv_cache_path(), &projects);
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
crate::engine::sweep_transient_cache(&transient_cache_path());
|
||||||
let context_limit = preferences.context_tokens.max(0) as u32;
|
let context_limit = preferences.context_tokens.max(0) as u32;
|
||||||
// Reopen on the project we left, with a fresh draft chat.
|
// Reopen on the project we left, with a fresh draft chat.
|
||||||
let last_project = preferences
|
let last_project = preferences
|
||||||
|
|||||||
@@ -552,8 +552,7 @@ impl App {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
pub(super) fn session_title(&self, session_id: i32) -> Option<String> {
|
||||||
fn session_title(&self, session_id: i32) -> Option<String> {
|
|
||||||
self.projects
|
self.projects
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|project| &project.sessions)
|
.flat_map(|project| &project.sessions)
|
||||||
|
|||||||
@@ -417,9 +417,16 @@ impl App {
|
|||||||
let mut rows = column![].spacing(7);
|
let mut rows = column![].spacing(7);
|
||||||
for entry in &usage.entries {
|
for entry in &usage.entries {
|
||||||
let path = entry.path.clone();
|
let path = entry.path.clone();
|
||||||
|
let label = entry
|
||||||
|
.session
|
||||||
|
.and_then(|id| self.session_title(id))
|
||||||
|
.map_or_else(
|
||||||
|
|| entry.name.clone(),
|
||||||
|
|title| format!("{}: {}", entry.name, shorten(&title)),
|
||||||
|
);
|
||||||
rows = rows.push(
|
rows = rows.push(
|
||||||
row![
|
row![
|
||||||
text(entry.name.clone()).size(12),
|
text(label).size(12),
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
text(format!(
|
text(format!(
|
||||||
"{} · {} old",
|
"{} · {} old",
|
||||||
@@ -499,6 +506,15 @@ const AGE_COLORS: [Color; 5] = [
|
|||||||
Color::from_rgb(0.45, 0.45, 0.48),
|
Color::from_rgb(0.45, 0.45, 0.48),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Keeps a long session title from crowding out the size and the action.
|
||||||
|
fn shorten(title: &str) -> String {
|
||||||
|
const LIMIT: usize = 44;
|
||||||
|
if title.chars().count() <= LIMIT {
|
||||||
|
return title.to_owned();
|
||||||
|
}
|
||||||
|
title.chars().take(LIMIT - 1).chain(['…']).collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn portion(bytes: u64, capacity: u64) -> u16 {
|
fn portion(bytes: u64, capacity: u64) -> u16 {
|
||||||
((bytes.saturating_mul(1000) / capacity.max(1)) as u16).max(1)
|
((bytes.saturating_mul(1000) / capacity.max(1)) as u16).max(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ use validation::{validate_dspark, validate_main};
|
|||||||
|
|
||||||
pub(crate) use validation::validate_model_artifact;
|
pub(crate) use validation::validate_model_artifact;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(crate) use kvstore::sweep_unreachable as sweep_transient_cache;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub(crate) use metal::configure_sources as configure_metal_sources;
|
pub(crate) use metal::configure_sources as configure_metal_sources;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -68,6 +69,8 @@ impl KvStore {
|
|||||||
directory: directory.to_owned(),
|
directory: directory.to_owned(),
|
||||||
budget_bytes,
|
budget_bytes,
|
||||||
};
|
};
|
||||||
|
// Reclaim first, so the budget is measured against the whole directory.
|
||||||
|
sweep_unreachable(directory);
|
||||||
// A lowered budget takes effect at once, like ds4's eviction on open.
|
// A lowered budget takes effect at once, like ds4's eviction on open.
|
||||||
store.evict(None);
|
store.evict(None);
|
||||||
Ok(store)
|
Ok(store)
|
||||||
@@ -174,6 +177,41 @@ impl KvStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes what the index cannot reach: a checkpoint whose index is missing or
|
||||||
|
/// unreadable can never be found, and an index whose checkpoint is gone is
|
||||||
|
/// dead weight. Without this, an interrupted store or a checkpoint written
|
||||||
|
/// before the index existed occupies disc that eviction cannot even see, and
|
||||||
|
/// the directory grows past the budget unnoticed.
|
||||||
|
pub(crate) fn sweep_unreachable(directory: &Path) {
|
||||||
|
let Ok(files) = fs::read_dir(directory) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let reachable = files
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.map(|file| file.path())
|
||||||
|
.filter(|path| path.extension().is_some_and(|value| value == "meta"))
|
||||||
|
.filter_map(|path| read_entry(&path).ok())
|
||||||
|
.filter(|entry| entry.checkpoint.is_file())
|
||||||
|
.map(|entry| entry.checkpoint)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let Ok(files) = fs::read_dir(directory) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for file in files.flatten() {
|
||||||
|
let path = file.path();
|
||||||
|
let dead = match path.extension().and_then(|value| value.to_str()) {
|
||||||
|
Some("bin") => !reachable.contains(&path),
|
||||||
|
Some("meta") => !reachable.contains(&path.with_extension("bin")),
|
||||||
|
// Staging files from an interrupted index write.
|
||||||
|
Some("tmp") => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if dead {
|
||||||
|
let _ = fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn eviction_score(entry: &Entry, now: u64) -> f64 {
|
fn eviction_score(entry: &Entry, now: u64) -> f64 {
|
||||||
let age = now.saturating_sub(entry.last_used.max(entry.created_at)) as f64;
|
let age = now.saturating_sub(entry.last_used.max(entry.created_at)) as f64;
|
||||||
let hits = f64::from(entry.hits) * f64::exp2(-age / HIT_HALF_LIFE_SECONDS);
|
let hits = f64::from(entry.hits) * f64::exp2(-age / HIT_HALF_LIFE_SECONDS);
|
||||||
@@ -364,6 +402,29 @@ mod tests {
|
|||||||
fs::remove_dir_all(directory).unwrap();
|
fs::remove_dir_all(directory).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opening_reclaims_checkpoints_the_index_cannot_reach() {
|
||||||
|
let directory = temporary_directory("unreachable");
|
||||||
|
let store = KvStore::open(&directory, GIB).unwrap();
|
||||||
|
let indexed = add(&store, b"indexed", 100, 32);
|
||||||
|
let stray = directory.join("deadbeef.bin");
|
||||||
|
let widow = directory.join("widow.meta");
|
||||||
|
let staging = directory.join("indexed.meta.tmp");
|
||||||
|
fs::write(&stray, vec![0; 4_096]).unwrap();
|
||||||
|
fs::write(&widow, vec![0; 64]).unwrap();
|
||||||
|
fs::write(&staging, vec![0; 64]).unwrap();
|
||||||
|
|
||||||
|
KvStore::open(&directory, GIB).unwrap();
|
||||||
|
|
||||||
|
assert!(indexed.checkpoint.is_file());
|
||||||
|
assert!(index_path(&indexed.checkpoint).is_file());
|
||||||
|
assert!(!stray.exists());
|
||||||
|
assert!(!widow.exists());
|
||||||
|
assert!(!staging.exists());
|
||||||
|
|
||||||
|
fs::remove_dir_all(directory).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn budget_evicts_the_least_valuable_unprotected_entry() {
|
fn budget_evicts_the_least_valuable_unprotected_entry() {
|
||||||
let directory = temporary_directory("evict");
|
let directory = temporary_directory("evict");
|
||||||
|
|||||||
@@ -694,6 +694,8 @@ const CACHE_ENTRY_ROWS: usize = 12;
|
|||||||
pub(crate) struct KvCacheEntry {
|
pub(crate) struct KvCacheEntry {
|
||||||
pub(crate) path: PathBuf,
|
pub(crate) path: PathBuf,
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
|
/// Set for session checkpoints, so the view can name the conversation.
|
||||||
|
pub(crate) session: Option<i32>,
|
||||||
pub(crate) bytes: u64,
|
pub(crate) bytes: u64,
|
||||||
pub(crate) age_seconds: u64,
|
pub(crate) age_seconds: u64,
|
||||||
}
|
}
|
||||||
@@ -762,6 +764,10 @@ pub(crate) fn kv_cache_report(root: &Path, budget_bytes: u64) -> KvCacheReport {
|
|||||||
}
|
}
|
||||||
report.entries.push(KvCacheEntry {
|
report.entries.push(KvCacheEntry {
|
||||||
name: entry_name(&path, transient),
|
name: entry_name(&path, transient),
|
||||||
|
session: (!transient)
|
||||||
|
.then(|| path.file_stem().and_then(|value| value.to_str()))
|
||||||
|
.flatten()
|
||||||
|
.and_then(|stem| stem.parse().ok()),
|
||||||
bytes: bytes.saturating_add(
|
bytes: bytes.saturating_add(
|
||||||
fs::metadata(path.with_extension("meta")).map_or(0, |index| index.len()),
|
fs::metadata(path.with_extension("meta")).map_or(0, |index| index.len()),
|
||||||
),
|
),
|
||||||
@@ -924,6 +930,9 @@ mod tests {
|
|||||||
assert_eq!(report.entries.len(), 2);
|
assert_eq!(report.entries.len(), 2);
|
||||||
assert_eq!(report.entries[0].bytes, 140);
|
assert_eq!(report.entries[0].bytes, 140);
|
||||||
assert_eq!(report.entries[1].name, "Session 7");
|
assert_eq!(report.entries[1].name, "Session 7");
|
||||||
|
// The session id survives on the row, so the view can add its title.
|
||||||
|
assert_eq!(report.entries[1].session, Some(7));
|
||||||
|
assert_eq!(report.entries[0].session, None);
|
||||||
|
|
||||||
fs::remove_dir_all(root).unwrap();
|
fs::remove_dir_all(root).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user