From ec5c0ea0ed01dd4598e1b492a0194686d1aeadc5 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 17:13:56 -0400 Subject: [PATCH 1/4] Skip empty held-subtree traversal during layout and publication --- .../README.md | 31 + .../RESULT.json | 94 ++ .../after.json | 316 +++++ .../before.json | 1097 +++++++++++++++++ src/textual/_compositor.py | 9 +- src/textual/screen.py | 8 +- 6 files changed, 1548 insertions(+), 7 deletions(-) create mode 100644 evidence/sidebar-layout-invalidation-20261006/README.md create mode 100644 evidence/sidebar-layout-invalidation-20261006/RESULT.json create mode 100644 evidence/sidebar-layout-invalidation-20261006/after.json create mode 100644 evidence/sidebar-layout-invalidation-20261006/before.json diff --git a/evidence/sidebar-layout-invalidation-20261006/README.md b/evidence/sidebar-layout-invalidation-20261006/README.md new file mode 100644 index 0000000000..d397d9331f --- /dev/null +++ b/evidence/sidebar-layout-invalidation-20261006/README.md @@ -0,0 +1,31 @@ +# Skip held-subtree traversal when none is held + +The sidebar profile exposed complete committed-map ancestry scans in +`Compositor._arrange_root` and `deferred_regions` even with no held roots. +Screen also scanned callback ancestry and retained layout members against an +empty root set. The existing owners now derive the empty result immediately. +Nonempty roots use the original matching, damage and callback rules. No map, +cache, mask, queue or size-dependent measurement contract changed. + +Before/after AST uses the existing audit Package parser. Native production249, +tests467; dependency Toad production288/tests403/tools40: no parse omissions. +Lexical sites do not resolve arbitrary external dynamic overrides. Original +Screen/Compositor/Widget owners and Toad viewport/anchor hooks were read. + +One matched source App run used Toad437e/Core574 with the changed native source, +508 widgets/10 tabs, no provider. It exited0 with empty stderr. Ancestry visits +fell2746→1242 (left) and3230→1166 (right); empty deferred-region work fell from +~0.8–1.3ms to~0.001ms. Overall medians41.8/47.4ms do not establish an improvement +over the original43.8/41ms. This is a small removed cost, not solved latency. + +The six existing publication checks produced5PASS and one preserved failure. +The failing test explicitly assumes the Screen pump waits inside an admitted +async callback. Merged#75 instead admits callbacks to the original sender's +queue; MessagePump.call_after_refresh documents that lifetime. A later batch +cannot recall an already admitted callable. This patch changes neither that +contract nor the test. The other controls exercise actual batches, held subtree +geometry/hits/callbacks, inline publication and translucent backdrops. + +RESULT.json references the exact original/new profiles and App logs. No +installed pin, public runtime or frozen original evidence changed. The App +fixture cleaned its authored roots; no child operation is left running. diff --git a/evidence/sidebar-layout-invalidation-20261006/RESULT.json b/evidence/sidebar-layout-invalidation-20261006/RESULT.json new file mode 100644 index 0000000000..886753f8cb --- /dev/null +++ b/evidence/sidebar-layout-invalidation-20261006/RESULT.json @@ -0,0 +1,94 @@ +{ + "scope": "Existing source App sidebar width changes; no installed/live speed claim", + "app_exit": 0, + "app": { + "boundary": "completed headless _display; not terminal pixels", + "tabs": 10, + "pointer": true, + "active_widgets": 508, + "toggle_first_paint": { + "#channels-sidebar": { + "median_ms": 41.8, + "p95_ms": 63.7, + "max_ms": 166.3 + }, + "#thread-sidebar": { + "median_ms": 47.4, + "p95_ms": 52.5, + "max_ms": 91.5 + } + } + }, + "profiles": { + "channels": { + "before": { + "_arrange_root": { + "calls": 1, + "cumulative_ms": 40.386162000000006 + }, + "deferred_regions": { + "calls": 1, + "cumulative_ms": 1.3299400000000001 + }, + "walk_ancestors": { + "calls": 2746, + "cumulative_ms": 3.615679 + } + }, + "after": { + "_arrange_root": { + "calls": 1, + "cumulative_ms": 38.969518 + }, + "deferred_regions": { + "calls": 1, + "cumulative_ms": 0.001002 + }, + "walk_ancestors": { + "calls": 1242, + "cumulative_ms": 1.8329140000000002 + } + } + }, + "thread": { + "before": { + "_arrange_root": { + "calls": 1, + "cumulative_ms": 17.421249 + }, + "deferred_regions": { + "calls": 1, + "cumulative_ms": 0.7913190000000001 + }, + "walk_ancestors": { + "calls": 3230, + "cumulative_ms": 3.083367 + } + }, + "after": { + "_arrange_root": { + "calls": 1, + "cumulative_ms": 17.359883 + }, + "deferred_regions": { + "calls": 1, + "cumulative_ms": 0.000921 + }, + "walk_ancestors": { + "calls": 1166, + "cumulative_ms": 1.335096 + } + } + } + }, + "raw_sha256": { + "/home/ts/.cache/agent-scratch/native-sidebar-invalidation-20261006/app.stdout.log": "e931e3a60e42ec6a3c7bbda9862412551f3e9477f3a3aeb6ecbe91179100518d", + "/home/ts/.cache/agent-scratch/native-sidebar-invalidation-20261006/app.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.cache/agent-scratch/native-sidebar-invalidation-20261006/toggle-channels.pstats": "d165e9aec8ea0fd26d8d3cdb0fccdf19924599f64de4039bf26685137205a03e", + "/home/ts/.cache/agent-scratch/native-sidebar-invalidation-20261006/toggle-thread.pstats": "a94dbded3baa0b5ce704230d524e1c7f93ac11733ecc82964ee911f3614d9288", + "/home/ts/.cache/agent-scratch/tab-sidebar-native-app-20261006/toggle-channels.pstats": "62677e584d201d63342784575c10fa63a2017d6ce85d3d16d8811302c1c20bf0", + "/home/ts/.cache/agent-scratch/tab-sidebar-native-app-20261006/toggle-thread.pstats": "631271560a14978c886afbd94602dd885eb25489c5ad06e9196b868dafac6ce1" + }, + "publication_controls": "5 passed; 1 retained test expects the Screen-task callback custody replaced by merged #75. Raw failure preserved; no test or callback behavior changed.", + "remaining": "No reliable toggle latency improvement. Width-dependent measurement and paint remain required. The original profiles also include Core MroDispatch handler discovery; no Core edit." +} diff --git a/evidence/sidebar-layout-invalidation-20261006/after.json b/evidence/sidebar-layout-invalidation-20261006/after.json new file mode 100644 index 0000000000..9b02709f83 --- /dev/null +++ b/evidence/sidebar-layout-invalidation-20261006/after.json @@ -0,0 +1,316 @@ +{ + "tree": "680ddb871ce7b588b1e016555f10ff210515ec7b", + "parsed": 249, + "unparsed": [], + "sites": [ + { + "file": "src/textual/_compositor.py", + "line": 855, + "name": "_arrange_root", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/_compositor.py", + "line": 1572, + "name": "deferred_regions", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/_compositor.py", + "line": 717, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 767, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1653, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1810, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 816, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 872, + "name": "_layout_mutation_roots", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1308, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1308, + "name": "_layout_mutation_roots", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1291, + "name": "_layout_mutation_roots", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1292, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/app.py", + "line": 3850, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/layouts/stream.py", + "line": 58, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1207, + "name": "_prepare_compositor_refresh", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/screen.py", + "line": 1211, + "name": "_layout_mutation_roots", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/screen.py", + "line": 1321, + "name": "_sender_refresh_pending", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/screen.py", + "line": 1409, + "name": "_refresh_layout", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/screen.py", + "line": 326, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Store" + }, + { + "file": "src/textual/screen.py", + "line": 1390, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1456, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Store" + }, + { + "file": "src/textual/screen.py", + "line": 1524, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1526, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1600, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1681, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1303, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1422, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1455, + "name": "_layout_mutation_roots", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1525, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1566, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1619, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1221, + "name": "_prepare_compositor_refresh", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1239, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1306, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1479, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1568, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1527, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1438, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1457, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1201, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/scroll_view.py", + "line": 86, + "name": "_size_updated", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/scroll_view.py", + "line": 102, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/widget.py", + "line": 4357, + "name": "_size_updated", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/widget.py", + "line": 1408, + "name": "_sender_refresh_pending", + "kind": "Attribute", + "context": "Load" + } + ], + "limitations": "Lexical AST sites; external dynamic overrides are not resolved. Existing native and Toad declaration/caller sites were read before changes." +} diff --git a/evidence/sidebar-layout-invalidation-20261006/before.json b/evidence/sidebar-layout-invalidation-20261006/before.json new file mode 100644 index 0000000000..09ce43d358 --- /dev/null +++ b/evidence/sidebar-layout-invalidation-20261006/before.json @@ -0,0 +1,1097 @@ +[ + { + "repo": "/home/ts/wt/textual-frame-publication-20261006", + "head": "8733e54c21e415881c1c30ea66431bd54196cc58", + "root": "src/textual", + "parsed": 249, + "unparsed": [], + "sites": [ + { + "file": "src/textual/_compositor.py", + "line": 855, + "name": "_arrange_root", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/_compositor.py", + "line": 1571, + "name": "deferred_regions", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/_compositor.py", + "line": 717, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 767, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1650, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1807, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 816, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 872, + "name": "_layout_mutation_roots", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1307, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1307, + "name": "_layout_mutation_roots", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1290, + "name": "_layout_mutation_roots", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/_compositor.py", + "line": 1291, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/app.py", + "line": 3850, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/layouts/stream.py", + "line": 58, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1207, + "name": "_prepare_compositor_refresh", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/screen.py", + "line": 1211, + "name": "_layout_mutation_roots", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/screen.py", + "line": 1321, + "name": "_sender_refresh_pending", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/screen.py", + "line": 1409, + "name": "_refresh_layout", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/screen.py", + "line": 326, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Store" + }, + { + "file": "src/textual/screen.py", + "line": 1390, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1456, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Store" + }, + { + "file": "src/textual/screen.py", + "line": 1524, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1526, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1600, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1681, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1303, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1422, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1455, + "name": "_layout_mutation_roots", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1525, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1566, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1619, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1221, + "name": "_prepare_compositor_refresh", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1239, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1306, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1479, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1568, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1527, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1438, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1457, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/screen.py", + "line": 1201, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/scroll_view.py", + "line": 86, + "name": "_size_updated", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/scroll_view.py", + "line": 102, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/textual/widget.py", + "line": 4357, + "name": "_size_updated", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/textual/widget.py", + "line": 1408, + "name": "_sender_refresh_pending", + "kind": "Attribute", + "context": "Load" + } + ] + }, + { + "repo": "/home/ts/wt/textual-frame-publication-20261006", + "head": "8733e54c21e415881c1c30ea66431bd54196cc58", + "root": "tests", + "parsed": 467, + "unparsed": [], + "sites": [ + { + "file": "tests/test_batch_paint_admission.py", + "line": 17, + "name": "_prepare_compositor_refresh", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 148, + "name": "_layout_mutation_roots", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 151, + "name": "_prepare_compositor_refresh", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 103, + "name": "_prepare_compositor_refresh", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 19, + "name": "_prepare_compositor_refresh", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 152, + "name": "_layout_mutation_roots", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 51, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 269, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 105, + "name": "_prepare_compositor_refresh", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 207, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_batch_paint_admission.py", + "line": 242, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_closed_presentation_lifetime.py", + "line": 57, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_compositor.py", + "line": 251, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_compositor.py", + "line": 106, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_compositor.py", + "line": 162, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_compositor.py", + "line": 194, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_compositor_frame_damage.py", + "line": 192, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_compositor_frame_damage.py", + "line": 224, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_compositor_frame_damage.py", + "line": 279, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_compositor_frame_damage.py", + "line": 241, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_compositor_frame_damage.py", + "line": 296, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_geometry_reader_acquisition.py", + "line": 18, + "name": "_arrange_root", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "tests/test_geometry_reader_acquisition.py", + "line": 20, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_geometry_reader_acquisition.py", + "line": 63, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_refresh_callback_owner.py", + "line": 75, + "name": "_prepare_compositor_refresh", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "tests/test_refresh_callback_owner.py", + "line": 96, + "name": "_prepare_compositor_refresh", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_subtree_geometry_cache.py", + "line": 26, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_subtree_geometry_cache.py", + "line": 40, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_subtree_geometry_cache.py", + "line": 49, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_subtree_geometry_cache.py", + "line": 24, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_subtree_geometry_cache.py", + "line": 42, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_subtree_geometry_cache.py", + "line": 47, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 233, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 137, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 187, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 188, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 201, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 226, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 262, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 61, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 65, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 107, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 157, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_viewport_geometry_targets.py", + "line": 162, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 37, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 41, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 88, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 99, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 115, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 129, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 142, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 160, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 171, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/test_widget_measurement_commit.py", + "line": 49, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + } + ] + }, + { + "repo": "/home/ts/wt/toad-sidebar-context-pointer-20261006", + "head": "437e987f287a6e7403bee829ad1a3ec356850ca6", + "root": "src/toad", + "parsed": 288, + "unparsed": [], + "sites": [ + { + "file": "src/toad/screens/workspace.py", + "line": 157, + "name": "_layout_mutation_roots", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 160, + "name": "_prepare_compositor_refresh", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 179, + "name": "_refresh_layout", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 265, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 52, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 219, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 267, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 197, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 204, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 263, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/toad/screens/workspace.py", + "line": 209, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/toad/widgets/history_anchor.py", + "line": 221, + "name": "_size_updated", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/toad/widgets/history_anchor.py", + "line": 225, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "src/toad/widgets/viewport_body.py", + "line": 554, + "name": "_size_updated", + "kind": "FunctionDef", + "context": "NoneType" + }, + { + "file": "src/toad/widgets/viewport_body.py", + "line": 555, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + } + ] + }, + { + "repo": "/home/ts/wt/toad-sidebar-context-pointer-20261006", + "head": "437e987f287a6e7403bee829ad1a3ec356850ca6", + "root": "tests", + "parsed": 403, + "unparsed": [], + "sites": [ + { + "file": "tests/activity_spinners_pilot.py", + "line": 92, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/cold_thread_latency_pilot.py", + "line": 165, + "name": "_size_updated", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/cold_thread_latency_pilot.py", + "line": 154, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/cold_thread_latency_pilot.py", + "line": 236, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/goal_geometry_pilot.py", + "line": 32, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/history_anchor_geometry_pilot.py", + "line": 67, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/profile_hot_paths_pilot.py", + "line": 69, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/relationship_poll_reflow_pilot.py", + "line": 70, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/relationship_poll_reflow_pilot.py", + "line": 88, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/relationship_poll_reflow_pilot.py", + "line": 98, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/retained_fragment_geometry_pilot.py", + "line": 68, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/retained_fragment_geometry_pilot.py", + "line": 96, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/retained_fragment_geometry_pilot.py", + "line": 97, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/retained_fragment_geometry_pilot.py", + "line": 119, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/retained_tool_text_pilot.py", + "line": 153, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/retained_tool_text_pilot.py", + "line": 163, + "name": "_prepare_compositor_refresh", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/retained_tool_text_pilot.py", + "line": 53, + "name": "deferred_regions", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/scroll_select_latency_pilot.py", + "line": 104, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/scroll_select_latency_pilot.py", + "line": 120, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Store" + }, + { + "file": "tests/scroll_select_latency_pilot.py", + "line": 108, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/scroll_select_latency_pilot.py", + "line": 116, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/sidebar_metadata_reflow_pilot.py", + "line": 49, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/sidebar_preparation_boundary_pilot.py", + "line": 27, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/throbber_busy_pilot.py", + "line": 48, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/throbber_scroll_cost_pilot.py", + "line": 48, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/unread_paint_only_pilot.py", + "line": 61, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/unread_paint_only_pilot.py", + "line": 82, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/view_switch_latency.py", + "line": 128, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/view_switch_latency.py", + "line": 139, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/viewport_body_lifetime_pilot.py", + "line": 188, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/viewport_layout_reentry_pilot.py", + "line": 33, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tests/viewport_layout_reentry_pilot.py", + "line": 47, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Store" + } + ] + }, + { + "repo": "/home/ts/wt/toad-sidebar-context-pointer-20261006", + "head": "437e987f287a6e7403bee829ad1a3ec356850ca6", + "root": "tools", + "parsed": 40, + "unparsed": [], + "sites": [ + { + "file": "tools/performance/benchmark_spinner.py", + "line": 58, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tools/performance/sidebar_validation_driver.py", + "line": 271, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tools/performance/sidebar_validation_driver.py", + "line": 291, + "name": "_arrange_root", + "kind": "Attribute", + "context": "Store" + }, + { + "file": "tools/performance/sidebar_validation_driver.py", + "line": 251, + "name": "_refresh_layout", + "kind": "Attribute", + "context": "Load" + }, + { + "file": "tools/performance/sidebar_validation_driver.py", + "line": 634, + "name": "_layout_widgets", + "kind": "Attribute", + "context": "Load" + } + ] + } +] diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index eaa32e9806..bfe113cf3f 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -875,9 +875,10 @@ def _arrange_root( mutation_paths: set[Widget] = set() for owner in mutation_roots: mutation_paths.update(owner.walk_ancestors(with_self=True)) - for node, geometry in previous.items(): - for owner in mutation_roots.intersection(node.walk_ancestors(with_self=True)): - held[owner][node] = geometry + if mutation_roots: + for node, geometry in previous.items(): + for owner in mutation_roots.intersection(node.walk_ancestors(with_self=True)): + held[owner][node] = geometry for owner in mutation_paths: self._subtree_geometry.pop(owner, None) map: CompositorMap = {} @@ -1576,6 +1577,8 @@ def deferred_regions(self, roots: Iterable[Widget]) -> tuple[Region, ...]: Overflowing descendants retain their own original clipped bounds. """ roots = set(roots) + if not roots: + return () regions: set[Region] = set() placed: set[Widget] = set() for widget, geometry in self._published_map.items(): diff --git a/src/textual/screen.py b/src/textual/screen.py index d9f40bc15a..b59a14f5d4 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -1320,9 +1320,9 @@ def _on_timer_update(self) -> None: def _sender_refresh_pending(self, sender: Widget, roots: tuple[Widget, ...]) -> bool: """Admit the original spatial sender, including its backdrop publication.""" - ancestry = set(sender.walk_ancestors(with_self=True)) - if ancestry.intersection(roots) or any( - sender in root.walk_ancestors(with_self=True) for root in roots + if roots and ( + set(sender.walk_ancestors(with_self=True)).intersection(roots) + or any(sender in root.walk_ancestors(with_self=True) for root in roots) ): return True owner = sender.screen @@ -1459,7 +1459,7 @@ def _refresh_layout(self, size: Size | None = None, scroll: bool = False) -> Non member for member in members if set(member.walk_ancestors(with_self=True)).intersection(mutation_roots) }) - } + } if mutation_roots else {} Hide = events.Hide Show = events.Show From 534771a3e7505fe0305d3b8878add4c83fe5a300 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 17:14:29 -0400 Subject: [PATCH 2/4] Retain original publication control failure beside scoped result --- .../publication-checks.log | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 evidence/sidebar-layout-invalidation-20261006/publication-checks.log diff --git a/evidence/sidebar-layout-invalidation-20261006/publication-checks.log b/evidence/sidebar-layout-invalidation-20261006/publication-checks.log new file mode 100644 index 0000000000..bb107a3725 --- /dev/null +++ b/evidence/sidebar-layout-invalidation-20261006/publication-checks.log @@ -0,0 +1,12 @@ +.F.... [100%] +=================================== FAILURES =================================== +________ test_refresh_callback_queue_survives_a_batch_between_callbacks ________ +tests/test_batch_paint_admission.py:95: in test_refresh_callback_queue_survives_a_batch_between_callbacks + assert order == ["first"] +E AssertionError: assert ['first', 'second'] == ['first'] +E +E Left contains one more item: 'second' +E Use -v to get more diff +=========================== short test summary info ============================ +FAILED tests/test_batch_paint_admission.py::test_refresh_callback_queue_survives_a_batch_between_callbacks +1 failed, 5 passed in 0.85s From 771e2bdc573f43edef3f33f03325a3c6ace6aeb1 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 17:18:42 -0400 Subject: [PATCH 3/4] Align retained batch callback control with sender-owned completion --- .../README.md | 15 ++++++++------- .../RESULT.json | 5 +++-- .../sender-completion-check.log | 2 ++ tests/test_batch_paint_admission.py | 13 ++++++++----- 4 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 evidence/sidebar-layout-invalidation-20261006/sender-completion-check.log diff --git a/evidence/sidebar-layout-invalidation-20261006/README.md b/evidence/sidebar-layout-invalidation-20261006/README.md index d397d9331f..e65959f286 100644 --- a/evidence/sidebar-layout-invalidation-20261006/README.md +++ b/evidence/sidebar-layout-invalidation-20261006/README.md @@ -18,13 +18,14 @@ fell2746→1242 (left) and3230→1166 (right); empty deferred-region work fell f ~0.8–1.3ms to~0.001ms. Overall medians41.8/47.4ms do not establish an improvement over the original43.8/41ms. This is a small removed cost, not solved latency. -The six existing publication checks produced5PASS and one preserved failure. -The failing test explicitly assumes the Screen pump waits inside an admitted -async callback. Merged#75 instead admits callbacks to the original sender's -queue; MessagePump.call_after_refresh documents that lifetime. A later batch -cannot recall an already admitted callable. This patch changes neither that -contract nor the test. The other controls exercise actual batches, held subtree -geometry/hits/callbacks, inline publication and translucent backdrops. +The original six-check batch produced5PASS and one preserved failure: its +callback test expected the Screen to wait inside an admitted async callback. +The existing control now asserts the actual sender task, preserved callback +order and completion inside a later batch after original publication admission. +That changed control alone passed1/.21s; the other five were not repeated. +MessagePump.call_after_refresh and merged#75 own this lifetime. No production +callback rule changed. The five existing controls cover actual batches, held +subtree geometry/hits/callbacks, inline publication and translucent backdrops. RESULT.json references the exact original/new profiles and App logs. No installed pin, public runtime or frozen original evidence changed. The App diff --git a/evidence/sidebar-layout-invalidation-20261006/RESULT.json b/evidence/sidebar-layout-invalidation-20261006/RESULT.json index 886753f8cb..9563a3e012 100644 --- a/evidence/sidebar-layout-invalidation-20261006/RESULT.json +++ b/evidence/sidebar-layout-invalidation-20261006/RESULT.json @@ -89,6 +89,7 @@ "/home/ts/.cache/agent-scratch/tab-sidebar-native-app-20261006/toggle-channels.pstats": "62677e584d201d63342784575c10fa63a2017d6ce85d3d16d8811302c1c20bf0", "/home/ts/.cache/agent-scratch/tab-sidebar-native-app-20261006/toggle-thread.pstats": "631271560a14978c886afbd94602dd885eb25489c5ad06e9196b868dafac6ce1" }, - "publication_controls": "5 passed; 1 retained test expects the Screen-task callback custody replaced by merged #75. Raw failure preserved; no test or callback behavior changed.", - "remaining": "No reliable toggle latency improvement. Width-dependent measurement and paint remain required. The original profiles also include Core MroDispatch handler discovery; no Core edit." + "publication_controls": "Original batch5PASS/1 preserved pre75 Screen-custody failure; migrated existing sender-completion control alonePASS. Other five not repeated.", + "remaining": "No reliable toggle latency improvement. Width-dependent measurement and paint remain required. The original profiles also include Core MroDispatch handler discovery; no Core edit.", + "sender_control_sha256": "364fae48cbe8f4ef8e995f8fb3224d43792e784eb9919ceefea2fe0106918edf" } diff --git a/evidence/sidebar-layout-invalidation-20261006/sender-completion-check.log b/evidence/sidebar-layout-invalidation-20261006/sender-completion-check.log new file mode 100644 index 0000000000..a9bd3ae991 --- /dev/null +++ b/evidence/sidebar-layout-invalidation-20261006/sender-completion-check.log @@ -0,0 +1,2 @@ +. [100%] +1 passed in 0.26s diff --git a/tests/test_batch_paint_admission.py b/tests/test_batch_paint_admission.py index bfba560685..5b269d40ab 100644 --- a/tests/test_batch_paint_admission.py +++ b/tests/test_batch_paint_admission.py @@ -68,7 +68,7 @@ async def test_async_widget_batch_retains_damage_and_refresh_callbacks() -> None assert app.query_one("#new", Label).is_attached -async def test_refresh_callback_queue_survives_a_batch_between_callbacks() -> None: +async def test_admitted_refresh_callbacks_keep_sender_order_during_later_batch() -> None: app = BatchPaintApp() entered = asyncio.Event() release = asyncio.Event() @@ -76,11 +76,13 @@ async def test_refresh_callback_queue_survives_a_batch_between_callbacks() -> No order: list[str] = [] async def first() -> None: + assert asyncio.current_task() is app.task order.append("first") entered.set() await release.wait() def second() -> None: + assert asyncio.current_task() is app.task order.append("second") completed.set() @@ -89,12 +91,13 @@ def second() -> None: app.call_after_refresh(second) await asyncio.wait_for(entered.wait(), 1) with app.batch_update(): - release.set() - # The screen message pump is waiting inside first; let it proceed. - await asyncio.sleep(0.05) assert order == ["first"] assert not completed.is_set() - await asyncio.wait_for(completed.wait(), 1) + release.set() + # Publication already admitted both callbacks. The sender owns + # their execution and order; a later batch holds new frames, + # not this sender's previously admitted callback messages. + await asyncio.wait_for(completed.wait(), 1) assert order == ["first", "second"] From 95c0b14db6829113b923ba8bf199e2d68a207510 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 17:20:09 -0400 Subject: [PATCH 4/4] Correct retained sender-control duration to its actual log --- evidence/sidebar-layout-invalidation-20261006/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evidence/sidebar-layout-invalidation-20261006/README.md b/evidence/sidebar-layout-invalidation-20261006/README.md index e65959f286..4b874bb393 100644 --- a/evidence/sidebar-layout-invalidation-20261006/README.md +++ b/evidence/sidebar-layout-invalidation-20261006/README.md @@ -22,7 +22,7 @@ The original six-check batch produced5PASS and one preserved failure: its callback test expected the Screen to wait inside an admitted async callback. The existing control now asserts the actual sender task, preserved callback order and completion inside a later batch after original publication admission. -That changed control alone passed1/.21s; the other five were not repeated. +That changed control alone passed1/.26s; the other five were not repeated. MessagePump.call_after_refresh and merged#75 own this lifetime. No production callback rule changed. The five existing controls cover actual batches, held subtree geometry/hits/callbacks, inline publication and translucent backdrops.