Skip to content

Commit f65438d

Browse files
committed
Improve performance of using active process data when rendering the tab bar by only scanning processes once per second
We dont bother with configurable ttl. Instead treat the start of caching as the instant when cache freshness is checked. And ensure that cache is re-used for every OS Window. Fixes #9862 Fixes #9872
1 parent 15a6a51 commit f65438d

4 files changed

Lines changed: 48 additions & 15 deletions

File tree

docs/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ Detailed list of changes
243243

244244
- Render block elements from the Unicode Symbols for Legacy Computing Supplement block (U+1CC00–U+1CEBF): separated block quadrants, separated block sextants, one sixteenth blocks, and one quarter block partial fills (:disc:`9849`)
245245

246+
- Improve performance of using active process data when rendering the tab bar by only scanning processes once per second (:iss:`9862`)
247+
246248
0.46.2 [2026-03-21]
247249
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
248250

kitty/boss.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from kitty.types import WindowResizeDrag
2929

30-
from .child import cached_process_data, default_env, set_default_env
30+
from .child import cached_process_data, default_env, process_data_cache, set_default_env
3131
from .cli import create_opts, green, parse_args
3232
from .cli_stub import CLIOptions, SaveAsSessionOptions
3333
from .clipboard import (
@@ -1923,6 +1923,13 @@ def on_activity_since_last_focus(self, window: Window) -> None:
19231923
if tm is not None:
19241924
tm.mark_tab_bar_dirty()
19251925

1926+
def cache_process_data(self, enable: bool) -> None:
1927+
' Turn on caching of process data. Must be called in enable/disable pairs. '
1928+
if enable:
1929+
self.process_data_cache_active = process_data_cache.start_caching()
1930+
else:
1931+
process_data_cache.stop_caching(self.process_data_cache_active)
1932+
19261933
def update_tab_bar_data(self, os_window_id: int) -> None:
19271934
tm = self.os_window_map.get(os_window_id)
19281935
if tm is not None:

kitty/child-monitor.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,7 @@ render(monotonic_t now, bool input_read) {
10011001

10021002
const bool scan_for_animated_images = global_state.check_for_active_animated_images;
10031003
global_state.check_for_active_animated_images = false;
1004+
call_boss(cache_process_data, "O", Py_True);
10041005

10051006
for (size_t i = 0; i < global_state.num_os_windows; i++) {
10061007
OSWindow *w = global_state.os_windows + i;
@@ -1021,6 +1022,7 @@ render(monotonic_t now, bool input_read) {
10211022

10221023
}
10231024
last_render_at = now;
1025+
call_boss(cache_process_data, "O", Py_False);
10241026
#undef TD
10251027
}
10261028

kitty/child.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from collections.abc import Generator, Sequence
99
from contextlib import contextmanager, suppress
1010
from itertools import count
11+
from time import monotonic
1112
from typing import TYPE_CHECKING, DefaultDict, Iterable, Mapping, Optional, TypedDict
1213

1314
import kitty.fast_data_types as fast_data_types
@@ -96,27 +97,48 @@ def checked_terminfo_dir() -> str | None:
9697
return terminfo_dir if os.path.isdir(terminfo_dir) else None
9798

9899

99-
def processes_in_group(grp: int) -> list[int]:
100-
gmap: DefaultDict[int, list[int]] | None = getattr(process_group_map, 'cached_map', None)
101-
if gmap is None:
102-
try:
103-
gmap = process_group_map()
104-
except Exception:
105-
gmap = defaultdict(list)
106-
return gmap.get(grp, [])
100+
class CachedProcessData:
101+
102+
cached_result: DefaultDict[int, list[int]] | None = None
103+
cache_active: bool = False
104+
cache_at: float = 0
105+
ttl: float = 1
106+
107+
def process_group_map(self) -> DefaultDict[int, list[int]]:
108+
if self.cached_result is None or not self.cache_active:
109+
try:
110+
self.cached_result = process_group_map()
111+
except Exception:
112+
self.cached_result = defaultdict(list)
113+
self.cache_at = monotonic()
114+
return self.cached_result
115+
116+
def processes_in_group(self, grp: int) -> list[int]:
117+
return self.process_group_map()[grp]
118+
119+
def start_caching(self, refresh: bool = False) -> bool:
120+
prev, self.cache_active = self.cache_active, True
121+
if refresh or monotonic() - self.cache_at > self.ttl:
122+
self.cached_result = None
123+
self.cache_at = 0
124+
return prev
125+
126+
def stop_caching(self, prev: bool) -> None:
127+
self.cache_active = prev
128+
self.cached_result = None
129+
130+
131+
process_data_cache = CachedProcessData()
132+
processes_in_group = process_data_cache.processes_in_group
107133

108134

109135
@contextmanager
110136
def cached_process_data() -> Generator[None, None, None]:
111-
try:
112-
cm = process_group_map()
113-
except Exception:
114-
cm = defaultdict(list)
115-
setattr(process_group_map, 'cached_map', cm)
137+
orig = process_data_cache.start_caching(refresh=True)
116138
try:
117139
yield
118140
finally:
119-
delattr(process_group_map, 'cached_map')
141+
process_data_cache.stop_caching(orig)
120142

121143

122144
def session_id(pids: Iterable[int]) -> int:

0 commit comments

Comments
 (0)