diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-08-24 22:15:19 +0200 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2026-08-24 22:15:19 +0200 |
| commit | f4c1017d0bceb49b8fb8b99865e3c4da6e4d2614 (patch) | |
| tree | cc3a5d6fd6c261f44f2bf81b24f42c2e18b00468 /pkgs/by-name/fi/fish-patched/patches | |
| parent | pkgs/fish-patched: Add history id patch (diff) | |
| download | nixos-config-f4c1017d0bceb49b8fb8b99865e3c4da6e4d2614.zip | |
pkgs/fish-patched: Improve performance and reduce stutters
Diffstat (limited to 'pkgs/by-name/fi/fish-patched/patches')
7 files changed, 1269 insertions, 0 deletions
diff --git a/pkgs/by-name/fi/fish-patched/patches/0003-feat-history-Add-turtle-as-history-backend.patch b/pkgs/by-name/fi/fish-patched/patches/0003-feat-history-Add-turtle-as-history-backend.patch new file mode 100644 index 00000000..8eeb03b4 --- /dev/null +++ b/pkgs/by-name/fi/fish-patched/patches/0003-feat-history-Add-turtle-as-history-backend.patch @@ -0,0 +1,843 @@ +From 26e8fbb5a24dd8b9cac67adcb59e714b32f95aa6 Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Sat, 22 Aug 2026 22:42:44 +0200 +Subject: [PATCH 03/10] feat(history): Add turtle as history backend + +--- + Cargo.toml | 4 + + src/history/history.rs | 285 ++--------------------------- + src/history/mod.rs | 2 + + src/history/turtle.rs | 400 +++++++++++++++++++++++++++++++++++++++++ + src/parser.rs | 5 + + src/reader/reader.rs | 4 +- + 6 files changed, 424 insertions(+), 276 deletions(-) + create mode 100644 src/history/turtle.rs + +diff --git a/Cargo.toml b/Cargo.toml +index aa91b61b7..137fbed24 100644 +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -11,6 +11,7 @@ repository = "https://github.com/fish-shell/fish-shell" + license = "GPL-2.0-only AND LGPL-2.0-or-later AND MIT AND PSF-2.0" + + [workspace.dependencies] ++turtle-api = { version = "20.0.0" } + anstyle = "1.0.13" + anyhow = "1.0.102" + assert_matches = "1.5.0" +@@ -115,6 +116,9 @@ homepage = "https://fishshell.com" + readme = "README.rst" + + [dependencies] ++turtle-api.workspace = true ++tokio = { version = "1.53.1", features = ["rt"] } ++uuid = { version = "1.24.0", features = ["v7"] } + assert_matches.workspace = true + bitflags.workspace = true + cfg-if.workspace = true +diff --git a/src/history/history.rs b/src/history/history.rs +index e1d04ad49..90cbfe6a9 100644 +--- a/src/history/history.rs ++++ b/src/history/history.rs +@@ -14,6 +14,7 @@ + //! `src/fs.rs`. By default, `flock()` is used for locking. If that is unavailable, an imperfect + //! fallback solution attempts to detect races and retries if a race is detected. + ++use crate::history::turtle::HistoryDb; + use crate::{ + ast::{self, Kind, Node as _}, + common::valid_var_name, +@@ -334,7 +335,7 @@ struct HistoryImpl { + /// Deleted item contents, and the scope of the deletion. + deleted_items: HashMap<WString, DeletionScope>, + /// The history file contents. +- file_contents: Option<HistoryFile>, ++ file_contents: Option<HistoryDb>, + /// The file ID of the history file. + history_file_id: FileId, // INVALID_FILE_ID + /// The boundary timestamp distinguishes old items from new items. Items whose timestamps are <= +@@ -358,6 +359,8 @@ fn history_file_path(&self) -> std::io::Result<Option<WString>> { + return Ok(None); + } + ++ return Ok(Some(L!("/run/user/1000/turtle.sock").to_owned())); ++ + let mut path = if let Some(custom_dir) = &self.custom_directory { + custom_dir.clone() + } else { +@@ -422,10 +425,7 @@ fn add(&mut self, item: HistoryItem, pending: bool, do_save: bool) { + } + + /// Internal function. +- fn clear_file_state(&mut self) { +- // Erase everything we know about our file. +- self.file_contents = None; +- } ++ fn clear_file_state(&mut self) {} + + /// Returns a timestamp for new items - see the implementation for a subtlety. + fn timestamps_as_of_now(&self) -> Timestamps { +@@ -451,32 +451,16 @@ fn timestamps_as_of_now(&self) -> Timestamps { + + /// Loads old items if necessary. + /// Return a reference to the loaded history file. +- fn load_old_if_needed(&mut self) -> &HistoryFile { ++ fn load_old_if_needed(&mut self) -> &HistoryDb { + if let Some(ref file_contents) = self.file_contents { + return file_contents; + } + let Ok(Some(history_path)) = self.history_file_path() else { +- return self.file_contents.insert(HistoryFile::create_empty()); ++ return self.file_contents.insert(HistoryDb::create_empty()); + }; + + let _profiler = TimeProfiler::new("load_old"); +- let file_contents = match lock_and_load(&history_path, RawHistoryFile::create) { +- Ok((file_id, history_file)) => { +- self.history_file_id = file_id; +- let _profiler = TimeProfiler::new("populate_from_file_contents"); +- let file_contents = history_file.decode(Some(self.boundary_timestamp)); +- flogf!( +- history, +- "Loaded %u old items", +- file_contents.offsets().len() +- ); +- file_contents +- } +- Err(e) => { +- flog!(history_file, "Error reading from history file:", e); +- HistoryFile::create_empty() +- } +- }; ++ let file_contents = HistoryDb::load(&history_path, self.boundary_timestamp); + self.file_contents.insert(file_contents) + } + +@@ -524,260 +508,11 @@ fn remove_ephemeral_items(&mut self) { + usize::min(self.first_unwritten_new_item_index, self.new_items.len()); + } + +- /// Given an existing history file, write a new history file to `dst`. +- fn rewrite_to_temporary_file( +- &self, +- existing_file: &File, +- dst: &mut File, +- ) -> std::io::Result<()> { +- // We are reading FROM existing_file and writing TO dst +- +- // Make an LRU cache to save only the last N elements. +- +- /// When we rewrite the history, the number of items we keep. +- const HISTORY_SAVE_MAX: NonZeroUsize = NonZeroUsize::new(1024 * 256).unwrap(); +- let mut lru = LruCache::new(HISTORY_SAVE_MAX); +- +- // Read in existing items (which may have changed out from underneath us, so don't trust our +- // old file contents). +- let file_id = file_id_for_file(existing_file); +- if let Ok(local_file) = RawHistoryFile::create(existing_file, file_id) { +- for offset in local_file.offsets(None) { +- // Try decoding an old item. +- let Some(old_item) = local_file.decode_item(offset) else { +- continue; +- }; +- if old_item.is_empty() { +- continue; +- } +- +- // Check if this item should be deleted. +- if let Some(&scope) = self.deleted_items.get(old_item.str()) { +- // If old item is newer than session always erase if in deleted. +- // If old item is older and in deleted items don't erase if added by clear_session. +- let delete = old_item.first_added_timestamp() > self.boundary_timestamp +- || scope == DeletionScope::AllSessions; +- if delete { +- continue; +- } +- } +- lru.add_item(old_item); +- } +- } +- +- // Insert any unwritten new items +- for item in self +- .new_items +- .iter() +- .skip(self.first_unwritten_new_item_index) +- { +- if item.should_write_to_disk() { +- lru.add_item(item.clone()); +- } +- } +- +- // Stable-sort our items by timestamp +- // This is because we may have read "old" items with a later timestamp than our "new" items +- // This is the essential step that roughly orders items by history +- let mut items: Vec<_> = lru.into_iter().map(|(_key, item)| item).collect(); +- items.sort_by_key(HistoryItem::last_added_timestamp); +- +- /// Default buffer size for flushing to the history file. +- const HISTORY_OUTPUT_BUFFER_SIZE: usize = 64 * 1024; +- // Write them out. +- let mut buffer = BufWriter::with_capacity(HISTORY_OUTPUT_BUFFER_SIZE + 128, dst); +- for item in items { +- item.write_to(&mut buffer)?; +- } +- buffer.flush()?; +- Ok(()) +- } +- +- /// Saves history by rewriting the file. +- fn save_internal_via_rewrite(&mut self, history_path: &wstr) -> std::io::Result<()> { +- flogf!( +- history, +- "Saving %u items via rewrite", +- self.new_items.len() - self.first_unwritten_new_item_index +- ); +- +- let rewrite = +- |old_file: &File, tmp_file: &mut File| -> std::io::Result<PotentialUpdate<()>> { +- let result = self.rewrite_to_temporary_file(old_file, tmp_file); +- if let Err(err) = result { +- flog!( +- history_file, +- "Error writing to temporary history file:", +- err +- ); +- return Err(err); +- } +- Ok(PotentialUpdate { +- do_save: true, +- data: (), +- }) +- }; +- +- let (file_id, _) = rewrite_via_temporary_file(history_path, rewrite)?; +- self.history_file_id = file_id; +- +- // We've saved everything, so we have no more unsaved items. +- self.first_unwritten_new_item_index = self.new_items.len(); +- +- // We deleted our deleted items. +- self.deleted_items.clear(); +- +- // Our history has been written to the file, so clear our state so we can re-reference the +- // file. +- self.clear_file_state(); +- +- Ok(()) +- } +- +- /// Saves history by appending to the file. +- fn save_internal_via_appending(&mut self, history_path: &wstr) -> std::io::Result<()> { +- flogf!( +- history, +- "Saving %u items via appending", +- self.new_items.len() - self.first_unwritten_new_item_index +- ); +- // No deleting allowed. +- assert!(self.deleted_items.is_empty()); +- +- let mut locked_history_file = +- LockedFile::new(LockingMode::Exclusive(WriteMethod::Append), history_path)?; +- +- // Check if the file was modified since it was last read. +- // If someone has replaced the file, forget our file state. +- if file_id_for_file(locked_history_file.get()) != self.history_file_id { +- self.clear_file_state(); +- } +- +- // We took the exclusive lock. Append to the file. +- // Note that this is sketchy for a few reasons: +- // - Another shell may have appended its own items with a later timestamp, so our file may +- // no longer be sorted by timestamp. +- // - Another shell may have appended the same items, so our file may now contain +- // duplicates. +- // +- // Originally we always rewrote the file on saving, which avoided both of these problems. +- // However, appending allows us to save history after every command, which is nice! +- // +- // Periodically we "clean up" the file by rewriting it, so that most of the time it doesn't +- // have duplicates, although we don't yet sort by timestamp (the timestamp isn't really used +- // for much anyways). +- +- // So far so good. Write all items at or after first_unwritten_new_item_index. Note that we +- // write even a pending item - pending items are ignored by history within the command +- // itself, but should still be written to the file. +- // Use a small buffer size for appending, as we usually only have 1 item. +- // Buffer everything and then write it all at once to avoid tearing writes (O_APPEND). +- let mut buffer = Vec::new(); +- let mut new_first_index = self.first_unwritten_new_item_index; +- while new_first_index < self.new_items.len() { +- let item = &self.new_items[new_first_index]; +- if item.should_write_to_disk() { +- // Can't error writing to a buffer. +- item.write_to(&mut buffer).unwrap(); +- } +- // We wrote or skipped this item, hooray. +- new_first_index += 1; +- } +- locked_history_file.get_mut().write_all(&buffer)?; +- fsync(locked_history_file.get())?; +- self.first_unwritten_new_item_index = new_first_index; +- +- // Since we just modified the file, update our history_file_id to match its current state +- // Otherwise we'll think the file has been changed by someone else the next time we go to +- // write. +- // We don't update `self.file_contents` since we only appended to the file, and everything we +- // appended remains in our new_items +- self.history_file_id = file_id_for_file(locked_history_file.get()); +- +- Ok(()) +- } +- + /// Saves history. +- fn save(&mut self, vacuum: bool) { +- // Nothing to do if there's no new items. +- if self.first_unwritten_new_item_index >= self.new_items.len() +- && self.deleted_items.is_empty() +- { +- return; +- } +- +- // Compact our new items so we don't have duplicates. +- self.compact_new_items(); +- +- if self.name.is_empty() { +- // We're in the "incognito" mode. Pretend we've saved the history. +- self.first_unwritten_new_item_index = self.new_items.len(); +- self.deleted_items.clear(); +- self.clear_file_state(); +- return; +- } +- +- let history_path = match self.history_file_path() { +- Ok(history_path) => history_path.unwrap(), +- Err(e) => { +- flog!(history, "Saving history failed:", e); +- return; +- } +- }; +- +- // Try saving. If we have items to delete, we have to rewrite the file. If we do not, we can +- // append to it. +- let mut ok = false; +- if !vacuum && self.deleted_items.is_empty() { +- // Try doing a fast append. +- if let Err(e) = self.save_internal_via_appending(&history_path) { +- flog!(history, "Appending to history failed:", e); +- } else { +- ok = true; +- } +- } +- if !ok { +- // We did not or could not append; rewrite the file ("vacuum" it). +- if let Err(e) = self.save_internal_via_rewrite(&history_path) { +- flog!(history, "Rewriting history failed:", e); +- } +- } +- } ++ fn save(&mut self, vacuum: bool) {} + + /// Saves history unless doing so is disabled. +- fn save_unless_disabled(&mut self) { +- // Respect disable_automatic_save_counter. +- if self.disable_automatic_save_counter > 0 { +- return; +- } +- +- // We may or may not vacuum. We try to vacuum every `VACUUM_FREQUENCY` items, but start the +- // countdown at a random number so that even if the user never runs more than 25 commands, we'll +- // eventually vacuum. If countdown_to_vacuum is None, it means we haven't yet picked a value for +- // the counter. +- let countdown_to_vacuum = self +- .countdown_to_vacuum +- .get_or_insert_with(|| rand::rng().random_range(0..VACUUM_FREQUENCY)); +- +- // Determine if we're going to vacuum. +- let mut vacuum = false; +- if *countdown_to_vacuum == 0 { +- *countdown_to_vacuum = VACUUM_FREQUENCY; +- vacuum = true; +- } +- +- // Update our countdown. +- assert!(*countdown_to_vacuum > 0); +- *countdown_to_vacuum -= 1; +- +- // This might be a good candidate for moving to a background thread. +- let _profiler = TimeProfiler::new(if vacuum { +- "save vacuum" +- } else { +- "save no vacuum" +- }); +- self.save(vacuum); +- } ++ fn save_unless_disabled(&mut self) {} + + fn new(name: WString, custom_directory: Option<WString>) -> Self { + Self { +diff --git a/src/history/mod.rs b/src/history/mod.rs +index 7ef4d4417..0de867740 100644 +--- a/src/history/mod.rs ++++ b/src/history/mod.rs +@@ -3,4 +3,6 @@ + mod history; + mod yaml_backend; + ++pub(crate) mod turtle; ++ + pub use history::*; +diff --git a/src/history/turtle.rs b/src/history/turtle.rs +new file mode 100644 +index 000000000..ffcdea108 +--- /dev/null ++++ b/src/history/turtle.rs +@@ -0,0 +1,400 @@ ++use std::{ ++ collections::HashMap, ++ sync::{ ++ Arc, OnceLock, RwLock, ++ atomic::{AtomicBool, Ordering}, ++ mpsc, ++ }, ++ thread::{self, JoinHandle, Thread}, ++ time::{Duration, SystemTime, UNIX_EPOCH}, ++}; ++ ++use fish_widestring::{L, WString}; ++use turtle_api::{ ++ client::{HistoryClient, OffsetDateTime, Probe, Range}, ++ history::{History, HistoryId}, ++}; ++ ++use crate::{env::Environment as _, flog, flogf, history::HistoryItem}; ++ ++pub(crate) struct HistoryDb; ++ ++#[derive(Debug)] ++struct HistoryDbInner { ++ range: Option<Range>, ++ session: uuid::Uuid, ++ ++ loaded_history: Vec<History>, ++ ++ handler: Handler, ++} ++ ++impl HistoryDbInner { ++ fn loaded_history(&mut self) -> &[History] { ++ let should_wait = if self.loaded_history.is_empty() { ++ // We are probably running this the first time. ++ // Make sure, that we actually load something. ++ true ++ } else { ++ false ++ }; ++ ++ if NEW_LOADED_HISTORY_AVAILABLE.load(Ordering::Relaxed) || should_wait { ++ if let Some(pre_loaded_history) = self.handler.load_history_resp(should_wait) { ++ self.loaded_history = pre_loaded_history; ++ } ++ ++ flogf!( ++ history, ++ "Loaded history was requested, returning %d entries.", ++ self.loaded_history.len() ++ ); ++ ++ NEW_LOADED_HISTORY_AVAILABLE.store(false, Ordering::Relaxed); ++ } ++ ++ &self.loaded_history ++ } ++} ++ ++impl HistoryDb { ++ fn with_inner_mut<T>(mut fun: impl FnMut(&mut HistoryDbInner) -> T) -> T { ++ let mut inner = INNER.write().expect("Should not be poisioned"); ++ let inner = (*inner).get_mut().expect("Should be initialized"); ++ ++ let output = fun(inner); ++ ++ output ++ } ++} ++ ++impl HistoryDb { ++ /// Create an empty history file. ++ pub(super) fn create_empty() -> Self { ++ flog!(history, "turtle: Creating new empty hist"); ++ ++ Self ++ } ++ ++ /// Return the offsets of items in this file. ++ pub(super) fn offsets(&self) -> Vec<HistoryId> { ++ let out: Vec<_> = Self::with_inner_mut(|inner| { ++ inner ++ .loaded_history() ++ .iter() ++ .map(|h| h.id.clone()) ++ .collect() ++ }); ++ out ++ } ++ ++ /// Return whether this file is empty. ++ pub(super) fn is_empty(&self) -> bool { ++ self.offsets().is_empty() ++ } ++ ++ /// Load from on-disk file. ++ pub(super) fn load(history_path: &WString, _boundary_timestamp: SystemTime) -> Self { ++ flogf!(history, "turtle: Loading hist from %s", history_path); ++ ++ let inner = { ++ // let range = Some(Range { ++ // start: OffsetDateTime::from_unix_timestamp( ++ // boundary_timestamp ++ // .duration_since(UNIX_EPOCH) ++ // .expect("Also valid") ++ // .as_secs() as i64, ++ // ) ++ // .expect("Valid"), ++ // end: OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()), ++ // }); ++ let range = None; ++ ++ let handler = Handler::start(history_path.to_string()); ++ handler.emit(HandleHistoryCmd::Load { range }); ++ ++ let inner = HistoryDbInner { ++ session: uuid::Uuid::now_v7(), ++ range, ++ loaded_history: vec![], ++ handler, ++ }; ++ ++ inner ++ }; ++ ++ let static_inner = INNER.write().expect("should not be poisened"); ++ (*static_inner) ++ .set(inner) ++ .expect("Should not have been initialized before"); ++ ++ Self ++ } ++ ++ /// Decode an item at a given offset. ++ pub(super) fn decode_item(&self, id: HistoryId) -> Option<HistoryItem> { ++ Self::with_inner_mut(|inner| { ++ inner.loaded_history().iter().find(|h| h.id == id).map(|h| { ++ HistoryItem::new( ++ WString::from_str(&h.command), ++ super::Timestamps { ++ last_added: UNIX_EPOCH ++ + Duration::from_nanos_u128(h.timestamp.unix_timestamp_nanos() as u128), ++ first_added: UNIX_EPOCH ++ + Duration::from_nanos_u128(h.timestamp.unix_timestamp_nanos() as u128), ++ }, ++ super::PersistenceMode::Disk, ++ ) ++ }) ++ }) ++ } ++} ++ ++static INNER: RwLock<OnceLock<HistoryDbInner>> = RwLock::new(OnceLock::new()); ++ ++pub(crate) fn handle_preexec(parser: &mut crate::parser::Parser, command: WString) { ++ HistoryDb::with_inner_mut(|inner| { ++ let now = OffsetDateTime::now_local().expect("Should have local time zone"); ++ let command = command.to_string(); ++ let history: History = History::daemon() ++ .timestamp(now) ++ .command(command) ++ .cwd(parser.vars().get_pwd_slash().to_string()) ++ .session(inner.session) ++ .hostname( ++ parser ++ .vars() ++ .get(L!("hostname")) ++ .map_or(String::from("<unknown>"), |s| s.as_string().to_string()), ++ ) ++ .author( ++ parser ++ .vars() ++ .get(L!("USER")) ++ .map_or(String::from("<unknown>"), |s| s.as_string().to_string()), ++ ) ++ .build() ++ .into(); ++ ++ inner.handler.emit(HandleHistoryCmd::Start { history }); ++ }); ++} ++ ++pub(crate) fn handle_postexec(parser: &mut crate::parser::Parser, command: WString) { ++ HistoryDb::with_inner_mut(|inner| { ++ inner.handler.emit(HandleHistoryCmd::End { ++ command: command.to_string(), ++ when: OffsetDateTime::now_local().expect("to work"), ++ exit_code: i64::from(parser.last_statuses().status), ++ }); ++ inner ++ .handler ++ .emit(HandleHistoryCmd::Load { range: inner.range }); ++ }); ++} ++ ++pub(crate) fn handle_shutdow() { ++ flog!(history, "Shutting down history db"); ++ ++ // If the `load` function was never called, inner will be None. ++ // Therefore we only need to run shutdown, when inner was loaded. ++ let inner_is_initialized = { ++ let read = INNER.read().expect("should be not-poisioned"); ++ read.get().is_some() ++ }; ++ ++ if inner_is_initialized { ++ HistoryDb::with_inner_mut(|inner| inner.handler.stop()); ++ } ++} ++ ++#[derive(Debug)] ++struct Handler { ++ thread: Option<JoinHandle<()>>, ++ ++ cmd_tx: Option<mpsc::Sender<HandleHistoryCmd>>, ++ ++ returned_loaded_history: Arc<RwLock<Option<Vec<History>>>>, ++} ++ ++#[derive(Debug)] ++enum HandleHistoryCmd { ++ Start { ++ history: History, ++ }, ++ End { ++ command: String, ++ when: OffsetDateTime, ++ exit_code: i64, ++ }, ++ Load { ++ range: Option<Range>, ++ }, ++} ++ ++static HANDLER_IS_SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); ++static LOAD_HISTORY_THREAD: RwLock<Option<Thread>> = RwLock::new(None); ++static LOAD_HISTORY_WAITING: AtomicBool = AtomicBool::new(false); ++static NEW_LOADED_HISTORY_AVAILABLE: AtomicBool = AtomicBool::new(false); ++ ++impl Handler { ++ fn start(daemon_socket: String) -> Self { ++ let (cmd_tx, cmd_rx) = mpsc::channel(); ++ let returned_loaded_history = Arc::new(RwLock::new(None)); ++ ++ let loaded_history_return = Arc::clone(&returned_loaded_history); ++ let thread = std::thread::spawn(move || { ++ let mut running_history: HashMap<String, (HistoryId, OffsetDateTime)> = HashMap::new(); ++ ++ let rt = { ++ let mut b = tokio::runtime::Builder::new_current_thread(); ++ b.enable_all(); ++ b.build() ++ .expect("to work, as all the fish code is otherwise sync.") ++ }; ++ let mut client = match rt.block_on(turtle_api::client::probe(daemon_socket.clone())) { ++ Probe::Ready(_) => rt ++ .block_on(HistoryClient::new(daemon_socket)) ++ .expect("We probed, this client should be accessible"), ++ Probe::NeedsRestart(err) => { ++ flogf!(history, "Turtle daemon needs restart: %s", err); ++ todo!(); ++ } ++ Probe::Unreachable(report) => { ++ flogf!(history, "Turtle daemon unreachable: %s", report.to_string()); ++ todo!(); ++ } ++ }; ++ ++ while let Ok(cmd) = cmd_rx.recv() { ++ match cmd { ++ HandleHistoryCmd::Start { history } => { ++ let command = history.command.clone(); ++ ++ flogf!(history, " > Starting cmd '%s'", command); ++ ++ let history_id = history.id; ++ let start_time = history.timestamp; ++ ++ rt.block_on(client.start_history(history)) ++ .expect("client to still work"); ++ ++ running_history.insert(command, (history_id, start_time)); ++ } ++ HandleHistoryCmd::End { ++ command, ++ when: now, ++ exit_code, ++ } => { ++ let (id, start_time) = running_history ++ .remove(command.as_str()) ++ .expect("to contain it"); ++ ++ let duration = Duration::from_nanos_u128( ++ (now - start_time).whole_nanoseconds() as u128, ++ ); ++ ++ flogf!( ++ history, ++ " < Ending cmd '%s' with exit %d and duration %d ms", ++ command, ++ exit_code, ++ duration.as_millis() as u64, ++ ); ++ ++ rt.block_on(client.end_history(id.to_string(), duration, exit_code)) ++ .expect("client to still work"); ++ } ++ HandleHistoryCmd::Load { range } => { ++ if !HANDLER_IS_SHUTTING_DOWN.load(Ordering::Relaxed) { ++ let loaded_history = { ++ let base = rt.block_on(client.history(range)); ++ base.expect("the client to still work") ++ }; ++ ++ let mut output = loaded_history_return.write().expect("not poisioned"); ++ (*output) = Some(loaded_history); ++ NEW_LOADED_HISTORY_AVAILABLE.store(true, Ordering::Relaxed); ++ ++ if LOAD_HISTORY_WAITING.load(Ordering::Relaxed) { ++ let read = LOAD_HISTORY_THREAD.read().expect("not poisioned"); ++ let t = read ++ .as_ref() ++ .expect("is some, as a thread is marked as waiting"); ++ ++ LOAD_HISTORY_WAITING.store(false, Ordering::Relaxed); ++ t.unpark(); ++ } ++ } ++ } ++ } ++ } ++ }); ++ ++ Self { ++ thread: Some(thread), ++ cmd_tx: Some(cmd_tx), ++ returned_loaded_history, ++ } ++ } ++ ++ /// This is effectively a [`Drop`] impl, but we can't use the trait directly as statics don't ++ /// get dropped. ++ fn stop(&mut self) { ++ HANDLER_IS_SHUTTING_DOWN.store(true, Ordering::Relaxed); ++ ++ let thread = self.thread.take().expect("is some"); ++ let cmd_tx = self.cmd_tx.take().expect("is some"); ++ ++ // Tell the handler, that we won't send more cmds ++ drop(cmd_tx); ++ ++ thread.join().expect("should not panic"); ++ ++ flog!(history, "History db shutdown completed."); ++ } ++ ++ fn load_history_resp(&self, should_wait: bool) -> Option<Vec<History>> { ++ let mut output = None; ++ let rx = &self.returned_loaded_history; ++ ++ if let Ok(read) = rx.try_read() { ++ read.clone_into(&mut output); ++ } ++ ++ if should_wait && output.is_none() { ++ { ++ { ++ let me = thread::current(); ++ let mut write = LOAD_HISTORY_THREAD.write().expect("not poisioned"); ++ (*write) = Some(me); ++ } ++ ++ LOAD_HISTORY_WAITING.store(true, Ordering::Relaxed); ++ while LOAD_HISTORY_WAITING.load(Ordering::Relaxed) { ++ thread::park(); ++ } ++ ++ { ++ let mut write = LOAD_HISTORY_THREAD.write().expect("not poisioned"); ++ (*write) = None; ++ } ++ } ++ ++ let read = rx.read().expect("not poisioned"); ++ read.clone_into(&mut output); ++ } ++ ++ output ++ } ++ ++ fn emit(&self, cmd: HandleHistoryCmd) { ++ flogf!(history, "Emitting handler cmd: %s ", format!("{:?}", cmd)); ++ ++ self.cmd_tx ++ .as_ref() ++ .expect("was initialized") ++ .send(cmd) ++ .expect("receiver should not have hung up"); ++ } ++} +diff --git a/src/parser.rs b/src/parser.rs +index ffbc28633..6440b7476 100644 +--- a/src/parser.rs ++++ b/src/parser.rs +@@ -437,6 +437,11 @@ pub struct Parser { + #[cfg(test)] + pub test_only_suppress_stderr: bool, + } ++impl Drop for Parser { ++ fn drop(&mut self) { ++ crate::history::turtle::handle_shutdow(); ++ } ++} + + #[derive(Copy, Clone, Default)] + pub struct ParserEnvSetMode { +diff --git a/src/reader/reader.rs b/src/reader/reader.rs +index 14fbc89c0..6848a7f1c 100644 +--- a/src/reader/reader.rs ++++ b/src/reader/reader.rs +@@ -871,6 +871,7 @@ fn read_i(parser: &mut Parser) { + L!("fish_preexec").to_owned(), + vec![command.clone()], + ); ++ crate::history::turtle::handle_preexec(reader.parser, command.clone()); + let eval_res = reader_run_command(reader.parser, &command); + signal_clear_cancel(); + if !eval_res.no_status { +@@ -884,6 +885,7 @@ fn read_i(parser: &mut Parser) { + BufferedOutputter::new(Outputter::stdoutput()).write_command(Osc133CommandFinished { + exit_status: reader.parser.last_status(), + }); ++ crate::history::turtle::handle_postexec(reader.parser, command.clone()); + event::fire_generic(reader.parser, L!("fish_postexec").to_owned(), vec![command]); + // Allow any pending history items to be returned in the history array. + reader.history.resolve_pending(); +@@ -2663,7 +2665,7 @@ fn readline( + self.clear_pager(); + } + +- if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as _ { ++ if EXIT_STATE.load(Ordering::Relaxed) != ExitState::FinishedHandlers as u8 { + // The order of the two conditions below is important. Try to restore the mode + // in all cases, but only complain if interactive. + if let Some(old_modes) = old_modes { +-- +2.55.0 + diff --git a/pkgs/by-name/fi/fish-patched/patches/0004-history-turtle-Fix-id-misuse-and-history-return-orde.patch b/pkgs/by-name/fi/fish-patched/patches/0004-history-turtle-Fix-id-misuse-and-history-return-orde.patch new file mode 100644 index 00000000..4eb3220b --- /dev/null +++ b/pkgs/by-name/fi/fish-patched/patches/0004-history-turtle-Fix-id-misuse-and-history-return-orde.patch @@ -0,0 +1,50 @@ +From c4f1ff5812024e50f8c6cf825a81b654afada2a4 Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Sun, 23 Aug 2026 23:34:58 +0200 +Subject: [PATCH 04/10] history/turtle: Fix id misuse and history return order + +--- + src/history/turtle.rs | 16 +++++++++++++--- + 1 file changed, 13 insertions(+), 3 deletions(-) + +diff --git a/src/history/turtle.rs b/src/history/turtle.rs +index ffcdea108..862b7346b 100644 +--- a/src/history/turtle.rs ++++ b/src/history/turtle.rs +@@ -42,6 +42,10 @@ fn loaded_history(&mut self) -> &[History] { + if NEW_LOADED_HISTORY_AVAILABLE.load(Ordering::Relaxed) || should_wait { + if let Some(pre_loaded_history) = self.handler.load_history_resp(should_wait) { + self.loaded_history = pre_loaded_history; ++ ++ // HACK(@bpeetz): For some reason, the returned history is reversed. ++ // So we un-reverse it here. <2026-08-23> ++ self.loaded_history.reverse(); + } + + flogf!( +@@ -273,13 +277,19 @@ fn start(daemon_socket: String) -> Self { + + flogf!(history, " > Starting cmd '%s'", command); + +- let history_id = history.id; + let start_time = history.timestamp; + +- rt.block_on(client.start_history(history)) ++ let reply = rt ++ .block_on(client.start_history(history)) + .expect("client to still work"); + +- running_history.insert(command, (history_id, start_time)); ++ // NOTE(@bpeetz): We already _have_ an HistoryId on our history, ++ // but the turtle daemon will assign a new one. ++ // Therefore, we need to make sure, that we only use the id it has assigned. ++ // <2026-08-23> ++ let real_id = HistoryId::from(reply.id); ++ ++ running_history.insert(command, (real_id, start_time)); + } + HandleHistoryCmd::End { + command, +-- +2.55.0 + diff --git a/pkgs/by-name/fi/fish-patched/patches/0005-history-history-Always-save-history-even-in-private-.patch b/pkgs/by-name/fi/fish-patched/patches/0005-history-history-Always-save-history-even-in-private-.patch new file mode 100644 index 00000000..f55a6096 --- /dev/null +++ b/pkgs/by-name/fi/fish-patched/patches/0005-history-history-Always-save-history-even-in-private-.patch @@ -0,0 +1,32 @@ +From b113638a4a3283b95029fe67fe575ae37e1b768a Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Mon, 24 Aug 2026 21:12:01 +0200 +Subject: [PATCH 05/10] history/history: Always save history, even in private + mode + +--- + src/history/history.rs | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/history/history.rs b/src/history/history.rs +index 90cbfe6a9..b721e04df 100644 +--- a/src/history/history.rs ++++ b/src/history/history.rs +@@ -355,12 +355,12 @@ impl HistoryImpl { + /// Because the `path_get_data` function does not return error information, + /// we cannot provide more detail about the reason for the failure here. + fn history_file_path(&self) -> std::io::Result<Option<WString>> { ++ return Ok(Some(L!("/run/user/1000/turtle.sock").to_owned())); ++ + if self.name.is_empty() { + return Ok(None); + } + +- return Ok(Some(L!("/run/user/1000/turtle.sock").to_owned())); +- + let mut path = if let Some(custom_dir) = &self.custom_directory { + custom_dir.clone() + } else { +-- +2.55.0 + diff --git a/pkgs/by-name/fi/fish-patched/patches/0006-history-turtle-Don-t-block-at-startup-and-wait-until.patch b/pkgs/by-name/fi/fish-patched/patches/0006-history-turtle-Don-t-block-at-startup-and-wait-until.patch new file mode 100644 index 00000000..00fbc192 --- /dev/null +++ b/pkgs/by-name/fi/fish-patched/patches/0006-history-turtle-Don-t-block-at-startup-and-wait-until.patch @@ -0,0 +1,101 @@ +From c159bc0d6be03ce566a5a0e144a503d174aee91f Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Mon, 24 Aug 2026 21:18:39 +0200 +Subject: [PATCH 06/10] history/turtle: Don't block at startup, and wait until + history is loaded + +--- + src/history/turtle.rs | 49 +++---------------------------------------- + 1 file changed, 3 insertions(+), 46 deletions(-) + +diff --git a/src/history/turtle.rs b/src/history/turtle.rs +index 862b7346b..a1009a162 100644 +--- a/src/history/turtle.rs ++++ b/src/history/turtle.rs +@@ -31,16 +31,8 @@ struct HistoryDbInner { + + impl HistoryDbInner { + fn loaded_history(&mut self) -> &[History] { +- let should_wait = if self.loaded_history.is_empty() { +- // We are probably running this the first time. +- // Make sure, that we actually load something. +- true +- } else { +- false +- }; +- +- if NEW_LOADED_HISTORY_AVAILABLE.load(Ordering::Relaxed) || should_wait { +- if let Some(pre_loaded_history) = self.handler.load_history_resp(should_wait) { ++ if NEW_LOADED_HISTORY_AVAILABLE.load(Ordering::Relaxed) { ++ if let Some(pre_loaded_history) = self.handler.load_history_resp() { + self.loaded_history = pre_loaded_history; + + // HACK(@bpeetz): For some reason, the returned history is reversed. +@@ -237,8 +229,6 @@ enum HandleHistoryCmd { + } + + static HANDLER_IS_SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); +-static LOAD_HISTORY_THREAD: RwLock<Option<Thread>> = RwLock::new(None); +-static LOAD_HISTORY_WAITING: AtomicBool = AtomicBool::new(false); + static NEW_LOADED_HISTORY_AVAILABLE: AtomicBool = AtomicBool::new(false); + + impl Handler { +@@ -325,16 +315,6 @@ fn start(daemon_socket: String) -> Self { + let mut output = loaded_history_return.write().expect("not poisioned"); + (*output) = Some(loaded_history); + NEW_LOADED_HISTORY_AVAILABLE.store(true, Ordering::Relaxed); +- +- if LOAD_HISTORY_WAITING.load(Ordering::Relaxed) { +- let read = LOAD_HISTORY_THREAD.read().expect("not poisioned"); +- let t = read +- .as_ref() +- .expect("is some, as a thread is marked as waiting"); +- +- LOAD_HISTORY_WAITING.store(false, Ordering::Relaxed); +- t.unpark(); +- } + } + } + } +@@ -364,7 +344,7 @@ fn stop(&mut self) { + flog!(history, "History db shutdown completed."); + } + +- fn load_history_resp(&self, should_wait: bool) -> Option<Vec<History>> { ++ fn load_history_resp(&self) -> Option<Vec<History>> { + let mut output = None; + let rx = &self.returned_loaded_history; + +@@ -372,29 +352,6 @@ fn load_history_resp(&self, should_wait: bool) -> Option<Vec<History>> { + read.clone_into(&mut output); + } + +- if should_wait && output.is_none() { +- { +- { +- let me = thread::current(); +- let mut write = LOAD_HISTORY_THREAD.write().expect("not poisioned"); +- (*write) = Some(me); +- } +- +- LOAD_HISTORY_WAITING.store(true, Ordering::Relaxed); +- while LOAD_HISTORY_WAITING.load(Ordering::Relaxed) { +- thread::park(); +- } +- +- { +- let mut write = LOAD_HISTORY_THREAD.write().expect("not poisioned"); +- (*write) = None; +- } +- } +- +- let read = rx.read().expect("not poisioned"); +- read.clone_into(&mut output); +- } +- + output + } + +-- +2.55.0 + diff --git a/pkgs/by-name/fi/fish-patched/patches/0007-history-turtle-Don-t-reverse-newly-loaded-history.patch b/pkgs/by-name/fi/fish-patched/patches/0007-history-turtle-Don-t-reverse-newly-loaded-history.patch new file mode 100644 index 00000000..1e75acaf --- /dev/null +++ b/pkgs/by-name/fi/fish-patched/patches/0007-history-turtle-Don-t-reverse-newly-loaded-history.patch @@ -0,0 +1,27 @@ +From dd41eb10eb76289504ff4c5457434c81112cb008 Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Mon, 24 Aug 2026 21:28:04 +0200 +Subject: [PATCH 07/10] history/turtle: Don't reverse newly loaded history + +--- + src/history/turtle.rs | 4 ---- + 1 file changed, 4 deletions(-) + +diff --git a/src/history/turtle.rs b/src/history/turtle.rs +index a1009a162..2143f38a9 100644 +--- a/src/history/turtle.rs ++++ b/src/history/turtle.rs +@@ -34,10 +34,6 @@ fn loaded_history(&mut self) -> &[History] { + if NEW_LOADED_HISTORY_AVAILABLE.load(Ordering::Relaxed) { + if let Some(pre_loaded_history) = self.handler.load_history_resp() { + self.loaded_history = pre_loaded_history; +- +- // HACK(@bpeetz): For some reason, the returned history is reversed. +- // So we un-reverse it here. <2026-08-23> +- self.loaded_history.reverse(); + } + + flogf!( +-- +2.55.0 + diff --git a/pkgs/by-name/fi/fish-patched/patches/0008-history-turtle-Reverse-history-upon-load.patch b/pkgs/by-name/fi/fish-patched/patches/0008-history-turtle-Reverse-history-upon-load.patch new file mode 100644 index 00000000..b7331637 --- /dev/null +++ b/pkgs/by-name/fi/fish-patched/patches/0008-history-turtle-Reverse-history-upon-load.patch @@ -0,0 +1,28 @@ +From 5afaf63f339cfc183a8728363fbb34a710f0e2dc Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Mon, 24 Aug 2026 21:32:51 +0200 +Subject: [PATCH 08/10] history/turtle: Reverse history upon load + +For _some_ reason, that seems to improve performance by 5x?! +(I presume I'm just measuring wrong.) +--- + src/history/turtle.rs | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/src/history/turtle.rs b/src/history/turtle.rs +index 2143f38a9..4ac931a19 100644 +--- a/src/history/turtle.rs ++++ b/src/history/turtle.rs +@@ -34,6 +34,9 @@ fn loaded_history(&mut self) -> &[History] { + if NEW_LOADED_HISTORY_AVAILABLE.load(Ordering::Relaxed) { + if let Some(pre_loaded_history) = self.handler.load_history_resp() { + self.loaded_history = pre_loaded_history; ++ ++ // PERFORMANCE: That seems to improve performance? <2026-08-24> ++ self.loaded_history.reverse(); + } + + flogf!( +-- +2.55.0 + diff --git a/pkgs/by-name/fi/fish-patched/patches/0009-history-turtle-Do-more-work-in-the-command-handler-w.patch b/pkgs/by-name/fi/fish-patched/patches/0009-history-turtle-Do-more-work-in-the-command-handler-w.patch new file mode 100644 index 00000000..9e5fbecc --- /dev/null +++ b/pkgs/by-name/fi/fish-patched/patches/0009-history-turtle-Do-more-work-in-the-command-handler-w.patch @@ -0,0 +1,188 @@ +From b316b4952d46ad964aa8397f9a3e57de8ba98b3b Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Mon, 24 Aug 2026 21:43:45 +0200 +Subject: [PATCH 09/10] history/turtle: Do more work in the command handler, + when loading history + +--- + src/history/turtle.rs | 94 +++++++++++++++++++++++++++++-------------- + 1 file changed, 63 insertions(+), 31 deletions(-) + +diff --git a/src/history/turtle.rs b/src/history/turtle.rs +index 4ac931a19..15515fe15 100644 +--- a/src/history/turtle.rs ++++ b/src/history/turtle.rs +@@ -5,7 +5,7 @@ + atomic::{AtomicBool, Ordering}, + mpsc, + }, +- thread::{self, JoinHandle, Thread}, ++ thread::JoinHandle, + time::{Duration, SystemTime, UNIX_EPOCH}, + }; + +@@ -19,30 +19,40 @@ + + pub(crate) struct HistoryDb; + ++#[derive(Debug, Clone)] ++struct LoadedHistory { ++ map: HashMap<HistoryId, HistoryItem>, ++ ++ /// This field is effectively the same as `map.keys().collect()`. ++ /// ++ /// It just a cache. ++ keys: Vec<HistoryId>, ++} ++ + #[derive(Debug)] + struct HistoryDbInner { + range: Option<Range>, + session: uuid::Uuid, + +- loaded_history: Vec<History>, ++ loaded_history: LoadedHistory, + + handler: Handler, + } + + impl HistoryDbInner { +- fn loaded_history(&mut self) -> &[History] { ++ fn loaded_history(&mut self) -> &LoadedHistory { + if NEW_LOADED_HISTORY_AVAILABLE.load(Ordering::Relaxed) { + if let Some(pre_loaded_history) = self.handler.load_history_resp() { + self.loaded_history = pre_loaded_history; + + // PERFORMANCE: That seems to improve performance? <2026-08-24> +- self.loaded_history.reverse(); ++ self.loaded_history.keys.reverse(); + } + + flogf!( + history, + "Loaded history was requested, returning %d entries.", +- self.loaded_history.len() ++ self.loaded_history.map.len() + ); + + NEW_LOADED_HISTORY_AVAILABLE.store(false, Ordering::Relaxed); +@@ -73,14 +83,7 @@ pub(super) fn create_empty() -> Self { + + /// Return the offsets of items in this file. + pub(super) fn offsets(&self) -> Vec<HistoryId> { +- let out: Vec<_> = Self::with_inner_mut(|inner| { +- inner +- .loaded_history() +- .iter() +- .map(|h| h.id.clone()) +- .collect() +- }); +- out ++ Self::with_inner_mut(|inner| inner.loaded_history().keys.clone()) + } + + /// Return whether this file is empty. +@@ -111,7 +114,10 @@ pub(super) fn load(history_path: &WString, _boundary_timestamp: SystemTime) -> S + let inner = HistoryDbInner { + session: uuid::Uuid::now_v7(), + range, +- loaded_history: vec![], ++ loaded_history: LoadedHistory { ++ map: HashMap::new(), ++ keys: vec![], ++ }, + handler, + }; + +@@ -128,20 +134,7 @@ pub(super) fn load(history_path: &WString, _boundary_timestamp: SystemTime) -> S + + /// Decode an item at a given offset. + pub(super) fn decode_item(&self, id: HistoryId) -> Option<HistoryItem> { +- Self::with_inner_mut(|inner| { +- inner.loaded_history().iter().find(|h| h.id == id).map(|h| { +- HistoryItem::new( +- WString::from_str(&h.command), +- super::Timestamps { +- last_added: UNIX_EPOCH +- + Duration::from_nanos_u128(h.timestamp.unix_timestamp_nanos() as u128), +- first_added: UNIX_EPOCH +- + Duration::from_nanos_u128(h.timestamp.unix_timestamp_nanos() as u128), +- }, +- super::PersistenceMode::Disk, +- ) +- }) +- }) ++ Self::with_inner_mut(|inner| inner.loaded_history().map.get(&id).map(ToOwned::to_owned)) + } + } + +@@ -209,7 +202,7 @@ struct Handler { + + cmd_tx: Option<mpsc::Sender<HandleHistoryCmd>>, + +- returned_loaded_history: Arc<RwLock<Option<Vec<History>>>>, ++ returned_loaded_history: Arc<RwLock<Option<LoadedHistory>>>, + } + + #[derive(Debug)] +@@ -311,8 +304,47 @@ fn start(daemon_socket: String) -> Self { + base.expect("the client to still work") + }; + ++ let mut loaded_history_map = HashMap::new(); ++ ++ for item in loaded_history { ++ loaded_history_map.insert(item.id, { ++ HistoryItem::new( ++ WString::from_str(&item.command), ++ super::Timestamps { ++ last_added: UNIX_EPOCH ++ + Duration::from_nanos_u128( ++ item.timestamp.unix_timestamp_nanos() as u128, ++ ), ++ first_added: UNIX_EPOCH ++ + Duration::from_nanos_u128( ++ item.timestamp.unix_timestamp_nanos() as u128, ++ ), ++ }, ++ super::PersistenceMode::Disk, ++ ) ++ }); ++ } ++ ++ let keys = { ++ let mut base: Vec<_> = loaded_history_map ++ .iter() ++ .map(|(key, h)| (key, h.first_added_timestamp())) ++ .collect(); ++ ++ base.sort_by_key(|(_, time)| *time); ++ ++ // reverse this first here, so we can re-reverse it in the ++ // `loaded_history` function. ++ base.reverse(); ++ ++ base.into_iter().map(|(key, _)| key).copied().collect() ++ }; ++ + let mut output = loaded_history_return.write().expect("not poisioned"); +- (*output) = Some(loaded_history); ++ (*output) = Some(LoadedHistory { ++ keys, ++ map: loaded_history_map, ++ }); + NEW_LOADED_HISTORY_AVAILABLE.store(true, Ordering::Relaxed); + } + } +@@ -343,7 +375,7 @@ fn stop(&mut self) { + flog!(history, "History db shutdown completed."); + } + +- fn load_history_resp(&self) -> Option<Vec<History>> { ++ fn load_history_resp(&self) -> Option<LoadedHistory> { + let mut output = None; + let rx = &self.returned_loaded_history; + +-- +2.55.0 + |
