perf: prevent repeated status fetches in large repos (#2824)

* perf: prevent repeated status fetches in large repos

Replace time-based cache invalidation with a generation counter.

The old `StatusParams` included a millisecond timestamp (tick) in its
hash, causing the cache to invalidate on every UI tick. For large repos
with millions of files, this led to repeated index loading and 5+ minute load times.

This change uses a different strategy to manage cache invalidation:
- Remove tick from StatusParams hash
- Add generation counter that increments after each fetch completes
- Include generation in cache hash

This ensures:
- No repeated fetches while one is already pending, making gitui usable
  in large repos
- New fetch starts immediately after completion, keeping gitui responsive
  in small repos
- External file changes will still be detected on the next fetch cycle

* fix changelog

---------

Co-authored-by: Daniel Stoll <dstoll@radix.trade>
Co-authored-by: extrawurst <776816+extrawurst@users.noreply.github.com>
Co-authored-by: extrawurst <mail@rusticorn.com>
This commit is contained in:
Danny Stoll 2026-03-19 20:12:14 -05:00 committed by GitHub
parent 3cf7a818d1
commit 49555ce966
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 13 additions and 17 deletions

View file

@ -8,14 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
<<<<<<< feat/version-message
* improve `gitui --version` message [[@hlsxx](https://github.com/hlsxx)] ([#2838](https://github.com/gitui-org/gitui/issues/2838))
=======
* rust msrv bumped to `1.88`
### Fixed
* fix extremely slow status loading in large repositories by replacing time-based cache invalidation with generation counter [[@DannyStoll1](https://github.com/DannyStoll1)] ([#2823](https://github.com/gitui-org/gitui/issues/2823))
* fix panic when renaming or updating remote URL with no remotes configured [[@xvchris](https://github.com/xvchris)] ([#2868](https://github.com/gitui-org/gitui/issues/2868))
>>>>>>> master
## [0.28.0] - 2025-12-14

View file

@ -10,19 +10,11 @@ use crossbeam_channel::Sender;
use std::{
hash::Hash,
sync::{
atomic::{AtomicUsize, Ordering},
atomic::{AtomicU64, AtomicUsize, Ordering},
Arc, Mutex,
},
time::{SystemTime, UNIX_EPOCH},
};
fn current_tick() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("time before unix epoch!")
.as_millis()
}
#[derive(Default, Hash, Clone)]
pub struct Status {
pub items: Vec<StatusItem>,
@ -31,19 +23,17 @@ pub struct Status {
///
#[derive(Default, Hash, Copy, Clone, PartialEq, Eq)]
pub struct StatusParams {
tick: u128,
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
}
impl StatusParams {
///
pub fn new(
pub const fn new(
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
) -> Self {
Self {
tick: current_tick(),
status_type,
config,
}
@ -59,6 +49,8 @@ pub struct AsyncStatus {
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
repo: RepoPath,
/// Counter that increments after each completed fetch.
generation: Arc<AtomicU64>,
}
impl AsyncStatus {
@ -73,6 +65,7 @@ impl AsyncStatus {
last: Arc::new(Mutex::new(Status::default())),
sender,
pending: Arc::new(AtomicUsize::new(0)),
generation: Arc::new(AtomicU64::new(0)),
}
}
@ -97,12 +90,14 @@ impl AsyncStatus {
return Ok(None);
}
let hash_request = hash(&params);
let generation = self.generation.load(Ordering::Relaxed);
let hash_request = hash(&(params, generation));
log::trace!(
"request: [hash: {}] (type: {:?})",
"request: [hash: {}] (type: {:?}, gen: {})",
hash_request,
params.status_type,
generation,
);
{
@ -118,6 +113,7 @@ impl AsyncStatus {
let arc_current = Arc::clone(&self.current);
let arc_last = Arc::clone(&self.last);
let arc_generation = Arc::clone(&self.generation);
let sender = self.sender.clone();
let arc_pending = Arc::clone(&self.pending);
let status_type = params.status_type;
@ -138,6 +134,8 @@ impl AsyncStatus {
log::error!("fetch_helper: {e}");
}
// Increment generation to invalidate cache for next request
arc_generation.fetch_add(1, Ordering::Relaxed);
arc_pending.fetch_sub(1, Ordering::Relaxed);
sender