From f65438d6a5b97f8dd903d0371dcb89e114364de4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2026 07:53:09 +0530 Subject: [PATCH] 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 --- docs/changelog.rst | 2 ++ kitty/boss.py | 9 +++++++- kitty/child-monitor.c | 2 ++ kitty/child.py | 50 +++++++++++++++++++++++++++++++------------ 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9be9fe3ba..976479549 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -243,6 +243,8 @@ Detailed list of changes - 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`) +- Improve performance of using active process data when rendering the tab bar by only scanning processes once per second (:iss:`9862`) + 0.46.2 [2026-03-21] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/boss.py b/kitty/boss.py index 829832432..b59bf3804 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -27,7 +27,7 @@ from weakref import WeakValueDictionary from kitty.types import WindowResizeDrag -from .child import cached_process_data, default_env, set_default_env +from .child import cached_process_data, default_env, process_data_cache, set_default_env from .cli import create_opts, green, parse_args from .cli_stub import CLIOptions, SaveAsSessionOptions from .clipboard import ( @@ -1923,6 +1923,13 @@ class Boss: if tm is not None: tm.mark_tab_bar_dirty() + def cache_process_data(self, enable: bool) -> None: + ' Turn on caching of process data. Must be called in enable/disable pairs. ' + if enable: + self.process_data_cache_active = process_data_cache.start_caching() + else: + process_data_cache.stop_caching(self.process_data_cache_active) + def update_tab_bar_data(self, os_window_id: int) -> None: tm = self.os_window_map.get(os_window_id) if tm is not None: diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 9ce3d0b35..7f4b6685a 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -1001,6 +1001,7 @@ render(monotonic_t now, bool input_read) { const bool scan_for_animated_images = global_state.check_for_active_animated_images; global_state.check_for_active_animated_images = false; + call_boss(cache_process_data, "O", Py_True); for (size_t i = 0; i < global_state.num_os_windows; i++) { OSWindow *w = global_state.os_windows + i; @@ -1021,6 +1022,7 @@ render(monotonic_t now, bool input_read) { } last_render_at = now; + call_boss(cache_process_data, "O", Py_False); #undef TD } diff --git a/kitty/child.py b/kitty/child.py index 1e3629549..0f361c799 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -8,6 +8,7 @@ from collections import defaultdict from collections.abc import Generator, Sequence from contextlib import contextmanager, suppress from itertools import count +from time import monotonic from typing import TYPE_CHECKING, DefaultDict, Iterable, Mapping, Optional, TypedDict import kitty.fast_data_types as fast_data_types @@ -96,27 +97,48 @@ def checked_terminfo_dir() -> str | None: return terminfo_dir if os.path.isdir(terminfo_dir) else None -def processes_in_group(grp: int) -> list[int]: - gmap: DefaultDict[int, list[int]] | None = getattr(process_group_map, 'cached_map', None) - if gmap is None: - try: - gmap = process_group_map() - except Exception: - gmap = defaultdict(list) - return gmap.get(grp, []) +class CachedProcessData: + + cached_result: DefaultDict[int, list[int]] | None = None + cache_active: bool = False + cache_at: float = 0 + ttl: float = 1 + + def process_group_map(self) -> DefaultDict[int, list[int]]: + if self.cached_result is None or not self.cache_active: + try: + self.cached_result = process_group_map() + except Exception: + self.cached_result = defaultdict(list) + self.cache_at = monotonic() + return self.cached_result + + def processes_in_group(self, grp: int) -> list[int]: + return self.process_group_map()[grp] + + def start_caching(self, refresh: bool = False) -> bool: + prev, self.cache_active = self.cache_active, True + if refresh or monotonic() - self.cache_at > self.ttl: + self.cached_result = None + self.cache_at = 0 + return prev + + def stop_caching(self, prev: bool) -> None: + self.cache_active = prev + self.cached_result = None + + +process_data_cache = CachedProcessData() +processes_in_group = process_data_cache.processes_in_group @contextmanager def cached_process_data() -> Generator[None, None, None]: - try: - cm = process_group_map() - except Exception: - cm = defaultdict(list) - setattr(process_group_map, 'cached_map', cm) + orig = process_data_cache.start_caching(refresh=True) try: yield finally: - delattr(process_group_map, 'cached_map') + process_data_cache.stop_caching(orig) def session_id(pids: Iterable[int]) -> int: