From cc725dfae10765b1cfc2fe377dd4c3831a202d0c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Wed, 7 Oct 2026 16:07:47 -0400 Subject: [PATCH 1/2] Avoid empty handler collections during native event dispatch --- src/textual/message_pump.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 43264fd449..c75772914a 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -836,7 +836,7 @@ def _get_dispatch_methods( """ from textual.widget import Widget - methods_dispatched: set[Callable] = set() + methods_dispatched: set[Callable] | None = None message_types = message.__class__.__mro__ message_mro: list[type[Message]] | None = None private_method_name = f"_{method_name}" @@ -856,9 +856,11 @@ def _get_dispatch_methods( _type for _type in message_types if issubclass(_type, Message) ] for message_class in message_mro: - handlers = decorated_handlers.get(message_class, []) + handlers = decorated_handlers.get(message_class, ()) for method, selectors in handlers: + if methods_dispatched is None: + methods_dispatched = set() if method in methods_dispatched: continue if not selectors: From 165340ac83b2cd36bd6af9ae1866fa1b61977a29 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Wed, 7 Oct 2026 16:09:07 -0400 Subject: [PATCH 2/2] Record matched channel App and limit handler allocation performance claim --- evidence/handler-allocation-20261007/README.md | 15 +++++++++++++++ .../handler-allocation-20261007/app-result.json | 15 +++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 evidence/handler-allocation-20261007/README.md create mode 100644 evidence/handler-allocation-20261007/app-result.json diff --git a/evidence/handler-allocation-20261007/README.md b/evidence/handler-allocation-20261007/README.md new file mode 100644 index 0000000000..fac4ef24d7 --- /dev/null +++ b/evidence/handler-allocation-20261007/README.md @@ -0,0 +1,15 @@ +# Handler lookup allocations + +The existing metaclass already collects decorated declarations and parses selectors. No declaration registry or event cache is needed. Named handlers must remain live: an awaited decorated handler can replace the named handler that follows it. C3 order, instance binding, selectors and bubbling remain runtime work. + +The original lookup constructed an empty list on every missing decorated-handler match, including each message ancestor in each decorated class table. It also allocated a duplicate-tracking set even when no decorated candidate existed. Lookup now uses the immutable empty tuple on a miss and creates its set at the first decorated candidate. Existing declaration lists and post-yield duplicate accounting are unchanged. + +The existing refactor-audit Package parsed 250 production modules without omissions. `_get_dispatch_methods` is used by `_on_message` and idle delivery; both use the same changed implementation. No other declaration-table writer was found beyond `_MessagePumpMeta`; dynamic class mappings and decorated lists remain live. + +Fourteen affected original decorator/message checks passed in 2.18 seconds, including inherited duplicate suppression, C3/private precedence, selector matching and live replacement during awaited dispatch. + +One original channel App check passed: 160 authored private messages, 20 arrivals, focused history paging; sequence113 to73, 88 mounted rows, no App error, empty stderr. Installed Toad/Core came from viewer-cut-and-header-delivery-20261007; only native source was supplied. The installed message-pump diff consists exactly of these allocation changes. No provider, installed-package write or public operation. Original fixture teardown joined its private work; individual child birth identities were not collected. + +Raw result/profile/logs: /home/ts/.cache/agent-scratch/native-handler-allocation-20261007. Previous profile: /home/ts/.cache/agent-scratch/installed-viewer-cut-header-20261007/ui.pstats. + +Profiling recorded229908→214884 lookup calls, own time.9755→.9351s and inclusive1.5684→1.4606s. Own time per invocation4.24→4.35microseconds did not improve. Heartbeatp95 went46.13→35.11ms, maximum182.39→227.40ms; arrangement workload and final scroll position differed. These runs do not establish a reliable responsiveness or overall speed gain. Layout remains substantial required work. diff --git a/evidence/handler-allocation-20261007/app-result.json b/evidence/handler-allocation-20261007/app-result.json new file mode 100644 index 0000000000..d6495c2f1d --- /dev/null +++ b/evidence/handler-allocation-20261007/app-result.json @@ -0,0 +1,15 @@ +{ + "main_loop_samples": 2387, + "median_ms": 6.166572915390134, + "p95_ms": 35.106428898870945, + "max_ms": 227.4037009337917, + "gaps_above_50ms": 61, + "mounted_rows": 88, + "oldest_sequence": 73, + "app_error": null, + "profiling": true, + "initial_scroll": 215, + "final_scroll": 803, + "initial_oldest": 113, + "older_page": 73 +}