mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
fix(git-status): throttle opportunistic probes per repo, not per cwd
The counts a probe produces are repo-wide — `git diff --numstat HEAD` ignores the subdirectory it ran in — so panes at `repo/`, `repo/src` and `repo/docs` were three ways of asking one question, and a window activation spent one full-repo diff on each. Count the throttle against the work-tree root once a probe has resolved one for the cwd, and stamp the clock when the probe is *claimed* rather than when it lands: without that, panes claiming in the same instant all pass a throttle nothing has answered yet, which is exactly the shape a window activation has. In-flight dedup stays keyed by cwd, since it brackets a specific spawn that finish_probe has to release.
This commit is contained in:
Generated
+16
-124
@@ -2500,7 +2500,7 @@ version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
|
||||
dependencies = [
|
||||
"roxmltree 0.20.0",
|
||||
"roxmltree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3151,7 +3151,7 @@ dependencies = [
|
||||
"raw-window-handle",
|
||||
"refineable",
|
||||
"regex",
|
||||
"resvg 0.45.1",
|
||||
"resvg",
|
||||
"scheduler",
|
||||
"schemars",
|
||||
"seahash",
|
||||
@@ -3167,7 +3167,7 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"ttf-parser",
|
||||
"url",
|
||||
"usvg 0.45.1",
|
||||
"usvg",
|
||||
"util_macros",
|
||||
"uuid",
|
||||
"waker-fn",
|
||||
@@ -3209,7 +3209,7 @@ dependencies = [
|
||||
"paste",
|
||||
"raw-window-handle",
|
||||
"regex",
|
||||
"resvg 0.45.1",
|
||||
"resvg",
|
||||
"ropey",
|
||||
"rust-i18n",
|
||||
"schemars",
|
||||
@@ -4056,12 +4056,6 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
|
||||
|
||||
[[package]]
|
||||
name = "imagesize"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c"
|
||||
|
||||
[[package]]
|
||||
name = "imgref"
|
||||
version = "1.12.2"
|
||||
@@ -4494,18 +4488,6 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"euclid",
|
||||
"polycool",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kv-log-macro"
|
||||
version = "1.0.7"
|
||||
@@ -6307,15 +6289,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polycool"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.7.2"
|
||||
@@ -7084,26 +7057,12 @@ dependencies = [
|
||||
"log",
|
||||
"pico-args",
|
||||
"rgb",
|
||||
"svgtypes 0.15.3",
|
||||
"tiny-skia 0.11.4",
|
||||
"usvg 0.45.1",
|
||||
"svgtypes",
|
||||
"tiny-skia",
|
||||
"usvg",
|
||||
"zune-jpeg 0.4.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resvg"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9be183ad6a216aa96f33e4c8033b0988b8b3ea6fd2359d19af5bac4643fd8e81"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pico-args",
|
||||
"rgb",
|
||||
"svgtypes 0.16.1",
|
||||
"tiny-skia 0.12.0",
|
||||
"usvg 0.47.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.6.0"
|
||||
@@ -7158,15 +7117,6 @@ version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.10.0-rc.18"
|
||||
@@ -8512,17 +8462,7 @@ version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
|
||||
dependencies = [
|
||||
"kurbo 0.11.3",
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "svgtypes"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d"
|
||||
dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"kurbo",
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
@@ -8859,22 +8799,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png 0.17.16",
|
||||
"tiny-skia-path 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png 0.18.1",
|
||||
"tiny-skia-path 0.12.0",
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8888,17 +8813,6 @@ dependencies = [
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
@@ -9292,13 +9206,13 @@ dependencies = [
|
||||
"portable-pty",
|
||||
"regex",
|
||||
"reqwest_client",
|
||||
"resvg 0.47.0",
|
||||
"resvg",
|
||||
"russh",
|
||||
"russh-sftp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2 0.11.0",
|
||||
"sha2 0.10.9",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"tokio",
|
||||
@@ -9465,45 +9379,23 @@ dependencies = [
|
||||
"data-url",
|
||||
"flate2",
|
||||
"fontdb",
|
||||
"imagesize 0.13.0",
|
||||
"kurbo 0.11.3",
|
||||
"imagesize",
|
||||
"kurbo",
|
||||
"log",
|
||||
"pico-args",
|
||||
"roxmltree 0.20.0",
|
||||
"roxmltree",
|
||||
"rustybuzz",
|
||||
"simplecss",
|
||||
"siphasher",
|
||||
"strict-num",
|
||||
"svgtypes 0.15.3",
|
||||
"tiny-skia-path 0.11.4",
|
||||
"svgtypes",
|
||||
"tiny-skia-path",
|
||||
"unicode-bidi",
|
||||
"unicode-script",
|
||||
"unicode-vo",
|
||||
"xmlwriter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.47.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d46cf96c5f498d36b7a9693bc6a7075c0bb9303189d61b2249b0dc3d309c07de"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"data-url",
|
||||
"flate2",
|
||||
"imagesize 0.14.0",
|
||||
"kurbo 0.13.1",
|
||||
"log",
|
||||
"pico-args",
|
||||
"roxmltree 0.21.1",
|
||||
"simplecss",
|
||||
"siphasher",
|
||||
"strict-num",
|
||||
"svgtypes 0.16.1",
|
||||
"tiny-skia-path 0.12.0",
|
||||
"xmlwriter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
|
||||
@@ -193,34 +193,70 @@ impl GitStatusCache {
|
||||
/// call — rather than by a rare edge like a command ending.
|
||||
///
|
||||
/// Unlike [`begin_probe`](Self::begin_probe) this declines instead of
|
||||
/// queueing: a probe already in flight, or one that landed less than
|
||||
/// `min_interval` ago, drops the trigger entirely (no dirty mark, no
|
||||
/// queueing: a probe already in flight, or one against a repo probed less
|
||||
/// than `min_interval` ago, drops the trigger entirely (no dirty mark, no
|
||||
/// rerun). That's the whole point of the two entry points — the rare edges
|
||||
/// must never be missed, while these signals repeat on their own, so a
|
||||
/// count that's a second stale beats a `git` storm across every pane of a
|
||||
/// repo the moment the user alt-tabs back.
|
||||
///
|
||||
/// The throttle counts per *repo*, not per cwd (see
|
||||
/// [`throttle_key`](Self::throttle_key)), and the claim stamps the clock
|
||||
/// rather than waiting for the landing: without that, a dozen panes
|
||||
/// scattered over one repo's subdirectories would all claim in the same
|
||||
/// instant — each of them passing a throttle no probe had answered yet —
|
||||
/// and produce a dozen identical full-repo diffs.
|
||||
pub fn begin_probe_throttled(&mut self, cwd: &Path, min_interval: Duration) -> bool {
|
||||
if self.in_flight.contains(cwd) {
|
||||
return false;
|
||||
}
|
||||
let key = self.throttle_key(cwd).to_path_buf();
|
||||
if self
|
||||
.last_probe
|
||||
.get(cwd)
|
||||
.get(&key)
|
||||
.is_some_and(|at| at.elapsed() < min_interval)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.last_probe.insert(key, Instant::now());
|
||||
self.in_flight.insert(cwd.to_path_buf());
|
||||
true
|
||||
}
|
||||
|
||||
/// What the opportunistic throttle counts against: the work-tree root once
|
||||
/// some probe has answered for `cwd`, and `cwd` itself before that.
|
||||
///
|
||||
/// The counts a probe produces are repo-wide — `git diff --numstat HEAD`
|
||||
/// ignores which subdirectory it ran in — so panes at `repo/`, `repo/src`
|
||||
/// and `repo/docs` are three ways of asking one question, and want one
|
||||
/// shared clock rather than one each. In-flight dedup stays keyed by cwd:
|
||||
/// it brackets a specific spawn, and [`finish_probe`](Self::finish_probe)
|
||||
/// has to be able to release exactly what was claimed.
|
||||
///
|
||||
/// Before any probe has landed the root is simply unknown, so the first
|
||||
/// sweep over a repo still costs one probe per distinct cwd; every sweep
|
||||
/// after that collapses to one.
|
||||
fn throttle_key<'a>(&'a self, cwd: &'a Path) -> &'a Path {
|
||||
match self.roots.get(cwd) {
|
||||
Some(Some(root)) => root,
|
||||
_ => cwd,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fold a landed probe for `cwd` into the cache. A failed diff inside a
|
||||
/// live repo keeps the root's previous counts (a transient `git` error is
|
||||
/// not "the tree went clean"). Returns whether the cwd was re-triggered
|
||||
/// while this probe flew — the caller should start one more probe.
|
||||
pub fn finish_probe(&mut self, cwd: &Path, snapshot: Option<RepoSnapshot>) -> bool {
|
||||
self.in_flight.remove(cwd);
|
||||
self.last_probe.insert(cwd.to_path_buf(), Instant::now());
|
||||
// Re-stamp on landing so the gap is measured from fresh counts, and
|
||||
// under the root this probe just resolved — which is how a cwd first
|
||||
// learns to share its repo's clock (at claim time it had none).
|
||||
let key = match &snapshot {
|
||||
Some(snap) => snap.root.clone(),
|
||||
None => self.throttle_key(cwd).to_path_buf(),
|
||||
};
|
||||
self.last_probe.insert(key, Instant::now());
|
||||
match snapshot {
|
||||
Some(snap) => {
|
||||
let (added, removed) = snap.counts.unwrap_or_else(|| {
|
||||
@@ -516,4 +552,41 @@ mod tests {
|
||||
assert!(!cache.finish_probe(cwd, Some(snap("/repo", "main", Some((1, 0))))));
|
||||
assert!(cache.begin_probe(cwd));
|
||||
}
|
||||
|
||||
/// The throttle is per repo, not per cwd: panes sitting in different
|
||||
/// subdirectories ask one question (the counts are repo-wide), so once the
|
||||
/// cache knows where they live, a window activation costs one probe for
|
||||
/// the repo rather than one per pane.
|
||||
#[test]
|
||||
fn throttle_collapses_subdirectories_of_one_repo() {
|
||||
let mut cache = GitStatusCache::default();
|
||||
let (top, src, docs) = (
|
||||
Path::new("/repo"),
|
||||
Path::new("/repo/src"),
|
||||
Path::new("/repo/docs"),
|
||||
);
|
||||
let gap = Duration::from_secs(60);
|
||||
|
||||
// Nothing known yet, so each cwd is its own key and each gets a probe.
|
||||
for cwd in [top, src, docs] {
|
||||
assert!(cache.begin_probe_throttled(cwd, gap));
|
||||
assert!(!cache.finish_probe(cwd, Some(snap("/repo", "main", Some((3, 1))))));
|
||||
}
|
||||
|
||||
// Now all three resolve to `/repo`, so the next sweep collapses: the
|
||||
// first pane to ask spends the probe and the rest ride on it.
|
||||
assert!(!cache.begin_probe_throttled(top, gap));
|
||||
assert!(!cache.begin_probe_throttled(src, gap));
|
||||
|
||||
// …and the claim itself is what stops the stampede — with the clock
|
||||
// wound back far enough to let one through, the *others* still decline
|
||||
// while it is in flight, even though nothing has landed yet.
|
||||
assert!(cache.begin_probe_throttled(docs, Duration::ZERO));
|
||||
assert!(!cache.begin_probe_throttled(top, gap));
|
||||
assert!(!cache.begin_probe_throttled(src, gap));
|
||||
|
||||
// A pane elsewhere is untouched by any of it.
|
||||
let other = Path::new("/other");
|
||||
assert!(cache.begin_probe_throttled(other, gap));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -2083,8 +2083,11 @@ impl Tty7App {
|
||||
/// right after switching back.
|
||||
///
|
||||
/// Panes sharing a cwd fold into one probe in the shared cache, and the
|
||||
/// throttle there drops anything probed in the last moment, so the cost of
|
||||
/// a window with many panes is bounded by the number of distinct repos.
|
||||
/// throttle there counts per repo rather than per cwd, so once the cache
|
||||
/// knows where each pane lives the cost of a window with many panes is
|
||||
/// bounded by the number of distinct repos — not by the number of
|
||||
/// subdirectories they happen to sit in, which would be the same full-repo
|
||||
/// `git diff` asked several times over.
|
||||
fn refresh_git_status_all(&mut self, cx: &mut Context<Self>) {
|
||||
for leaf in self.tabs.iter().flat_map(|tab| tab.pane.leaves()) {
|
||||
leaf.update(cx, |view, cx| view.refresh_git_status_now(cx));
|
||||
|
||||
Reference in New Issue
Block a user