From f4c1017d0bceb49b8fb8b99865e3c4da6e4d2614 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Mon, 24 Aug 2026 22:15:19 +0200 Subject: pkgs/fish-patched: Improve performance and reduce stutters --- ...eat-history-Add-turtle-as-history-backend.patch | 843 --------------------- ...tle-Fix-id-misuse-and-history-return-orde.patch | 50 -- pkgs/by-name/fi/fish-patched/package.nix | 9 +- ...eat-history-Add-turtle-as-history-backend.patch | 843 +++++++++++++++++++++ ...tle-Fix-id-misuse-and-history-return-orde.patch | 50 ++ ...tory-Always-save-history-even-in-private-.patch | 32 + ...tle-Don-t-block-at-startup-and-wait-until.patch | 101 +++ ...turtle-Don-t-reverse-newly-loaded-history.patch | 27 + ...-history-turtle-Reverse-history-upon-load.patch | 28 + ...tle-Do-more-work-in-the-command-handler-w.patch | 188 +++++ 10 files changed, 1274 insertions(+), 897 deletions(-) delete mode 100644 pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch delete mode 100644 pkgs/by-name/fi/fish-patched/0002-history-turtle-Fix-id-misuse-and-history-return-orde.patch create mode 100644 pkgs/by-name/fi/fish-patched/patches/0003-feat-history-Add-turtle-as-history-backend.patch create mode 100644 pkgs/by-name/fi/fish-patched/patches/0004-history-turtle-Fix-id-misuse-and-history-return-orde.patch create mode 100644 pkgs/by-name/fi/fish-patched/patches/0005-history-history-Always-save-history-even-in-private-.patch create mode 100644 pkgs/by-name/fi/fish-patched/patches/0006-history-turtle-Don-t-block-at-startup-and-wait-until.patch create mode 100644 pkgs/by-name/fi/fish-patched/patches/0007-history-turtle-Don-t-reverse-newly-loaded-history.patch create mode 100644 pkgs/by-name/fi/fish-patched/patches/0008-history-turtle-Reverse-history-upon-load.patch create mode 100644 pkgs/by-name/fi/fish-patched/patches/0009-history-turtle-Do-more-work-in-the-command-handler-w.patch (limited to 'pkgs/by-name/fi/fish-patched') diff --git a/pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch b/pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch deleted file mode 100644 index 3e98b298..00000000 --- a/pkgs/by-name/fi/fish-patched/0001-feat-history-Add-turtle-as-history-backend.patch +++ /dev/null @@ -1,843 +0,0 @@ -From 26e8fbb5a24dd8b9cac67adcb59e714b32f95aa6 Mon Sep 17 00:00:00 2001 -From: Benedikt Peetz -Date: Sat, 22 Aug 2026 22:42:44 +0200 -Subject: [PATCH 3/3] 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, - /// The history file contents. -- file_contents: Option, -+ file_contents: Option, - /// 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> { - 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> { -- 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) -> 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, -+ session: uuid::Uuid, -+ -+ loaded_history: Vec, -+ -+ 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(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 { -+ 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 { -+ 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> = 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(""), |s| s.as_string().to_string()), -+ ) -+ .author( -+ parser -+ .vars() -+ .get(L!("USER")) -+ .map_or(String::from(""), |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>, -+ -+ cmd_tx: Option>, -+ -+ returned_loaded_history: Arc>>>, -+} -+ -+#[derive(Debug)] -+enum HandleHistoryCmd { -+ Start { -+ history: History, -+ }, -+ End { -+ command: String, -+ when: OffsetDateTime, -+ exit_code: i64, -+ }, -+ Load { -+ range: Option, -+ }, -+} -+ -+static HANDLER_IS_SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); -+static LOAD_HISTORY_THREAD: RwLock> = 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 = 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> { -+ 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/0002-history-turtle-Fix-id-misuse-and-history-return-orde.patch b/pkgs/by-name/fi/fish-patched/0002-history-turtle-Fix-id-misuse-and-history-return-orde.patch deleted file mode 100644 index 24df9782..00000000 --- a/pkgs/by-name/fi/fish-patched/0002-history-turtle-Fix-id-misuse-and-history-return-orde.patch +++ /dev/null @@ -1,50 +0,0 @@ -From 853a5b5cdbdaa21f5c0a739d43d878af0874e19e Mon Sep 17 00:00:00 2001 -From: Benedikt Peetz -Date: Sun, 23 Aug 2026 23:34:58 +0200 -Subject: [PATCH] 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/package.nix b/pkgs/by-name/fi/fish-patched/package.nix index 9ea39968..9d4bac75 100644 --- a/pkgs/by-name/fi/fish-patched/package.nix +++ b/pkgs/by-name/fi/fish-patched/package.nix @@ -29,8 +29,9 @@ in patches = (prev.patches or []) - ++ [ - ./0001-feat-history-Add-turtle-as-history-backend.patch - ./0002-history-turtle-Fix-id-misuse-and-history-return-orde.patch - ]; + ++ ( + builtins.map (name: ./patches + "/${name}") + (builtins.attrNames + (builtins.readDir ./patches)) + ); }) 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 +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, + /// The history file contents. +- file_contents: Option, ++ file_contents: Option, + /// 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> { + 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> { +- 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) -> 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, ++ session: uuid::Uuid, ++ ++ loaded_history: Vec, ++ ++ 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(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 { ++ 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 { ++ 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> = 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(""), |s| s.as_string().to_string()), ++ ) ++ .author( ++ parser ++ .vars() ++ .get(L!("USER")) ++ .map_or(String::from(""), |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>, ++ ++ cmd_tx: Option>, ++ ++ returned_loaded_history: Arc>>>, ++} ++ ++#[derive(Debug)] ++enum HandleHistoryCmd { ++ Start { ++ history: History, ++ }, ++ End { ++ command: String, ++ when: OffsetDateTime, ++ exit_code: i64, ++ }, ++ Load { ++ range: Option, ++ }, ++} ++ ++static HANDLER_IS_SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); ++static LOAD_HISTORY_THREAD: RwLock> = 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 = 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> { ++ 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 +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 +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> { ++ 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 +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> = 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> { ++ fn load_history_resp(&self) -> Option> { + let mut output = None; + let rx = &self.returned_loaded_history; + +@@ -372,29 +352,6 @@ fn load_history_resp(&self, should_wait: bool) -> Option> { + 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 +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 +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 +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, ++ ++ /// This field is effectively the same as `map.keys().collect()`. ++ /// ++ /// It just a cache. ++ keys: Vec, ++} ++ + #[derive(Debug)] + struct HistoryDbInner { + range: Option, + session: uuid::Uuid, + +- loaded_history: Vec, ++ 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 { +- 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 { +- 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>, + +- returned_loaded_history: Arc>>>, ++ returned_loaded_history: Arc>>, + } + + #[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> { ++ fn load_history_resp(&self) -> Option { + let mut output = None; + let rx = &self.returned_loaded_history; + +-- +2.55.0 + -- cgit v1.3.1