From e453340c98b1a65461a007b7983b19066cc29905 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 19:02:15 -0400 Subject: [PATCH 1/6] Skip geometry comparisons when full-screen damage is already owned --- evidence/focus-rule-supply-20261006/README.md | 8 ++++++++ src/textual/_compositor.py | 15 ++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/evidence/focus-rule-supply-20261006/README.md b/evidence/focus-rule-supply-20261006/README.md index 022237e7cf..757dfe0c97 100644 --- a/evidence/focus-rule-supply-20261006/README.md +++ b/evidence/focus-rule-supply-20261006/README.md @@ -9,3 +9,11 @@ AST before: 249 native production, 467 test and 288 Toad production modules, zer Seven focused controls passed, including real mounted descendants/components across CSS read and reparse. One original matched sidebar App finished zero with empty stderr, 508 widgets/10 tabs, no provider. Right profile: same three focus updates, own declaration discovery 7.05ms -> .47ms and full method 19.03ms -> 11.22ms. Left full method 18.51ms -> 7.99ms. This is a single source headless observation. First-display medians 41.8/42.2ms versus 40.1/45.6ms, tails vary, paint was slower: no reliable overall frame or live speedup claim. RESULT.json binds original raw profiles/logs and source evidence. No new build, install, provider, public operation or repeated benchmark. The existing empty-damage pending_for checkpoint remains on this same PR, independently scoped. + +## Fresh live geometry profile followthrough + +The existing sidebar-live-frame-profile-20261006 profile has two sampling errors. It shows deferred_regions under pending_for, and _damage_geometry under scrolling reflow; transition counts do not measure CPU time or damage state. + +A concrete redundant decision remained in Compositor._damage_geometry: when the entire screen was already dirty, track_damage was false but the method still compared every new placement with the prior map. Those comparisons supplied no damage or resize result. The owner now wakes the original idle publication and returns immediately for that state; partial damage keeps both previous/current clipped rectangles and removed placements. Held exclusions still partition the retained full-screen damage at rendering. No root-bounds shortcut, ancestry index or cache. + +Existing Package parsed 249 production modules with zero omissions: one _damage_geometry declaration, three callers in reflow/reflow_visible/full_map, no additional result consumers. New source compiles and diff-check passes. The prior CSS App result remains at 6c5c0a055, before this additional geometry change. No App/profile/control rerun or performance claim for this source-only reduction. Nonempty descendant geometry still requires the original membership/overflow relation; the samples alone do not establish redundant arrangement or its duration. diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 65535b5159..c06fa461ae 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -789,20 +789,21 @@ def _damage_geometry( rectangles without hashing complete geometry records into temporary sets. Widget size publication owns resize notification separately. """ - track_damage = self.size.region not in self._dirty_regions + if self.size.region in self._dirty_regions: + owner.check_idle() + return for widget, geometry in after.items(): previous = before.get(widget) if previous == geometry: continue if previous is not None: - if track_damage and (region := previous.visible_region): + if region := previous.visible_region: self._dirty_regions.add(region) - if track_damage and (region := geometry.visible_region): + if region := geometry.visible_region: + self._dirty_regions.add(region) + for widget, geometry in before.items(): + if widget not in after and (region := geometry.visible_region): self._dirty_regions.add(region) - if track_damage: - for widget, geometry in before.items(): - if widget not in after and (region := geometry.visible_region): - self._dirty_regions.add(region) if self._dirty_regions: owner.check_idle() From 8532547a2072df79ee50600d0946ac9fa29f134c Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 19:15:08 -0400 Subject: [PATCH 2/6] Exercise full-damage reflow with held placement and partial publication --- tests/test_batch_paint_admission.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_batch_paint_admission.py b/tests/test_batch_paint_admission.py index 5b269d40ab..9af828671f 100644 --- a/tests/test_batch_paint_admission.py +++ b/tests/test_batch_paint_admission.py @@ -207,6 +207,15 @@ async def test_partial_publication_keeps_geometry_and_owner_callbacks(): assert screen.publications and all(roots == (holder,) for roots in screen.publications) assert compositor._dirty_regions compositor._dirty_regions.add(compositor.size.region) + full_damage = set(compositor._dirty_regions) + # Reflow still commits geometry while full-screen damage already + # owns repaint. Held placements must survive and damage must remain + # available for the partial publisher, rather than being consumed. + screen._refresh_layout() + assert compositor._dirty_regions == full_damage + assert compositor.find_widget(holder) == before + assert compositor.find_widget(original) == original_geometry + assert compositor.get_widget_at(1, 1)[0] is original update = compositor.render_update(excluded_regions=compositor.deferred_regions((holder,))) assert isinstance(update, ChopsUpdate) cells = list(ChopsUpdate._span_cuts(update.spans, update.cuts, 0)) From 457462f602520f5c4a11b1abc2030d6a094dc6e9 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 19:15:40 -0400 Subject: [PATCH 3/6] Check retained geometry damage before Screen consumes it in publication --- tests/test_batch_paint_admission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_batch_paint_admission.py b/tests/test_batch_paint_admission.py index 9af828671f..cc622bc5a3 100644 --- a/tests/test_batch_paint_admission.py +++ b/tests/test_batch_paint_admission.py @@ -48,7 +48,7 @@ async def test_async_widget_batch_retains_damage_and_refresh_callbacks() -> None screen.preparation_batches.clear() async with holder.batch(): await holder.mount(Label("new retained source", id="new")) - screen._refresh_layout() + compositor.reflow(screen, screen.size) app.call_after_refresh(painted.set) await pilot.pause() damage = set(screen._compositor._dirty_regions) From cbeaf02dbe2a8a1bd84b2a3f845d12cb4a42a025 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 19:15:53 -0400 Subject: [PATCH 4/6] Apply the pre-publication assertion to the intended held-geometry control --- tests/test_batch_paint_admission.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_batch_paint_admission.py b/tests/test_batch_paint_admission.py index cc622bc5a3..24051ac511 100644 --- a/tests/test_batch_paint_admission.py +++ b/tests/test_batch_paint_admission.py @@ -48,7 +48,7 @@ async def test_async_widget_batch_retains_damage_and_refresh_callbacks() -> None screen.preparation_batches.clear() async with holder.batch(): await holder.mount(Label("new retained source", id="new")) - compositor.reflow(screen, screen.size) + screen._refresh_layout() app.call_after_refresh(painted.set) await pilot.pause() damage = set(screen._compositor._dirty_regions) @@ -211,7 +211,7 @@ async def test_partial_publication_keeps_geometry_and_owner_callbacks(): # Reflow still commits geometry while full-screen damage already # owns repaint. Held placements must survive and damage must remain # available for the partial publisher, rather than being consumed. - screen._refresh_layout() + compositor.reflow(screen, screen.size) assert compositor._dirty_regions == full_damage assert compositor.find_widget(holder) == before assert compositor.find_widget(original) == original_geometry From a0e8615d4a442b4c989a5b9eddcc986ad787739d Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 19:17:25 -0400 Subject: [PATCH 5/6] Qualify full-damage geometry commit and affected sidebar App --- .../full-damage-geometry-20261006/README.md | 9 +++ .../full-damage-geometry-20261006/RESULT.json | 22 +++++++ .../full-damage-geometry-20261006/source.json | 64 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 evidence/full-damage-geometry-20261006/README.md create mode 100644 evidence/full-damage-geometry-20261006/RESULT.json create mode 100644 evidence/full-damage-geometry-20261006/source.json diff --git a/evidence/full-damage-geometry-20261006/README.md b/evidence/full-damage-geometry-20261006/README.md new file mode 100644 index 0000000000..055bce65ee --- /dev/null +++ b/evidence/full-damage-geometry-20261006/README.md @@ -0,0 +1,9 @@ +# Full-damage geometry comparison + +Compositor owns committed geometry and pending paint damage. Reflow, visible reflow and full-map acquisition commit the new placements before _damage_geometry. When the original damage set already contains the current full-screen rectangle, the old method disabled all additions but still compared every new placement. Those comparison results supplied no resize, placement or damage output. Return after the same original owner.check_idle instead; partial damage still retains both old/current clipped rectangles and removed placements. The renderer continues to exclude held cells from full damage and retain those cells until release. + +Source evidence parses 249 production modules before and after, zero omissions; one declaration and the same three consumers. No ancestry index, cached geometry, root-bounds shortcut, state or callback owner change. Arbitrary external dynamic callers are not resolved by lexical AST. + +The changed existing control commits real geometry under full damage while holding a mounted subtree. It checks exact damage retention, original placements/hits, excluded cut cells, unrelated paint, quiet held callbacks and release. PASS/.39s. First assertion incorrectly used Screen._refresh_layout, which also paints before returning; that valid publication consumed the full-screen region. A correction initially landed at the wrong method call, leaving the selected test unchanged; both refusals remain. The final control calls the existing Compositor.reflow before the original partial publisher; no assertion was relaxed. + +One original source sidebar App PASS/empty stderr, 508 widgets/10 tabs/no provider. Left/right first-display medians38.2/39.1ms and tails vary; no reliable overall speedup, installed, live or terminal-pixel claim. Prior CSS controls were not repeated. RESULT.json binds the actual source and original logs/profiles. New source result remains separate from the completed live cohort. diff --git a/evidence/full-damage-geometry-20261006/RESULT.json b/evidence/full-damage-geometry-20261006/RESULT.json new file mode 100644 index 0000000000..2eb6de838d --- /dev/null +++ b/evidence/full-damage-geometry-20261006/RESULT.json @@ -0,0 +1,22 @@ +{ + "source": "cbeaf02dbe2a8a1bd84b2a3f845d12cb4a42a025", + "contract": "1 passed in 0.39s", + "app_exit": 0, + "app_widgets": 508, + "app_tabs": 10, + "providers": 0, + "stderr_bytes": 0, + "toad_source": "d7273c666ea04ff13c1a677bb5851637da273490", + "core_source": "37563f36f6c54a2a1272e90c7c90c2108b1026e6", + "limit": "Source App completed headless display only; no installed/live/physical acceptance or reliable overall performance improvement.", + "keepers": { + "evidence/full-damage-geometry-20261006/source.json": "d9ed028d10a4d44a6973ab2a702cb893a65ae48bd6ca282bf833ba024b5c7b3f", + "evidence/full-damage-geometry-20261006/geometry-control.log": "036bb9390fb408c39eb19a61d865bc4044aa29eeea8139e62a988a5a83930a4d", + "evidence/full-damage-geometry-20261006/original-after-publication-control-refusal.log": "5e5f39f7df94ab4772d1b0dc021facb9880d1d4461292fb55d5d523fbf9670f8", + "evidence/full-damage-geometry-20261006/unchanged-target-after-wrong-edit-refusal.log": "5e5f39f7df94ab4772d1b0dc021facb9880d1d4461292fb55d5d523fbf9670f8", + "/home/ts/.cache/agent-scratch/full-damage-geometry-20261006/app.stdout.log": "5ec6d51cba69af35fb3bc449e236531adda47283ed25cdedb12ef816c0b23f20", + "/home/ts/.cache/agent-scratch/full-damage-geometry-20261006/app.stderr.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "/home/ts/.cache/agent-scratch/full-damage-geometry-20261006/toggle-channels.pstats": "4ee1510a69f80ca1c2027353de43805b55ac331f4123a6879e453beb3db11921", + "/home/ts/.cache/agent-scratch/full-damage-geometry-20261006/toggle-thread.pstats": "dfb8aa7a5de3c27cefcf3d193bcccf7d4d9cdabd45c37b831a01fe8f34055a6a" + } +} diff --git a/evidence/full-damage-geometry-20261006/source.json b/evidence/full-damage-geometry-20261006/source.json new file mode 100644 index 0000000000..0d72e331ed --- /dev/null +++ b/evidence/full-damage-geometry-20261006/source.json @@ -0,0 +1,64 @@ +[ + { + "revision": "7efdd92c37762cd963e02a51dfc5ef349342d045", + "parsed": 249, + "omissions": [], + "sites": [ + [ + "src/textual/_compositor.py", + 782, + "declaration", + "def _damage_geometry(self, before: Mapping[Widget, MapGeometry], after: Mapping[Widget, MapGeometry], owner: Widget) -> None:\n \"\"\"Retain both paint rectangles from the original scene change.\n\n Compare each new placement with its previous one. Retain both clipped\n rectangles without hashing complete geometry records into temporary\n sets. Widget size publication owns resize notification separately.\n \"\"\"\n track_damage = self.size.region not in self._dirty_regions\n for widget, geometry in after.items():\n previous = before.get(widget)\n if previous == geometry:\n continue\n if previous is not None:\n if track_damage and (region := previous.visible_region):\n self._dirty_regions.add(region)\n if track_damage and (region := geometry.visible_region):\n self._dirty_regions.add(region)\n if track_damage:\n for widget, geometry in before.items():\n if widget not in after and (region := geometry.visible_region):\n self._dirty_regions.add(region)\n if self._dirty_regions:\n owner.check_idle()" + ], + [ + "src/textual/_compositor.py", + 741, + "consumer", + "self._damage_geometry(previous_scene, map, parent)" + ], + [ + "src/textual/_compositor.py", + 778, + "consumer", + "self._damage_geometry(previous_scene, map, parent)" + ], + [ + "src/textual/_compositor.py", + 821, + "consumer", + "self._damage_geometry(previous, map, self.root)" + ] + ] + }, + { + "revision": "cbeaf02dbe2a8a1bd84b2a3f845d12cb4a42a025", + "parsed": 249, + "omissions": [], + "sites": [ + [ + "src/textual/_compositor.py", + 782, + "declaration", + "def _damage_geometry(self, before: Mapping[Widget, MapGeometry], after: Mapping[Widget, MapGeometry], owner: Widget) -> None:\n \"\"\"Retain both paint rectangles from the original scene change.\n\n Compare each new placement with its previous one. Retain both clipped\n rectangles without hashing complete geometry records into temporary\n sets. Widget size publication owns resize notification separately.\n \"\"\"\n if self.size.region in self._dirty_regions:\n owner.check_idle()\n return\n for widget, geometry in after.items():\n previous = before.get(widget)\n if previous == geometry:\n continue\n if previous is not None:\n if (region := previous.visible_region):\n self._dirty_regions.add(region)\n if (region := geometry.visible_region):\n self._dirty_regions.add(region)\n for widget, geometry in before.items():\n if widget not in after and (region := geometry.visible_region):\n self._dirty_regions.add(region)\n if self._dirty_regions:\n owner.check_idle()" + ], + [ + "src/textual/_compositor.py", + 741, + "consumer", + "self._damage_geometry(previous_scene, map, parent)" + ], + [ + "src/textual/_compositor.py", + 778, + "consumer", + "self._damage_geometry(previous_scene, map, parent)" + ], + [ + "src/textual/_compositor.py", + 822, + "consumer", + "self._damage_geometry(previous, map, self.root)" + ] + ] + } +] From 48a1fab982d28b92670ac741f7b7984f42e25de2 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Tue, 6 Oct 2026 19:17:47 -0400 Subject: [PATCH 6/6] Preserve original geometry control results alongside qualification --- .../geometry-control.log | 2 ++ .../original-after-publication-control-refusal.log | 13 +++++++++++++ .../unchanged-target-after-wrong-edit-refusal.log | 13 +++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 evidence/full-damage-geometry-20261006/geometry-control.log create mode 100644 evidence/full-damage-geometry-20261006/original-after-publication-control-refusal.log create mode 100644 evidence/full-damage-geometry-20261006/unchanged-target-after-wrong-edit-refusal.log diff --git a/evidence/full-damage-geometry-20261006/geometry-control.log b/evidence/full-damage-geometry-20261006/geometry-control.log new file mode 100644 index 0000000000..95c9b0cc89 --- /dev/null +++ b/evidence/full-damage-geometry-20261006/geometry-control.log @@ -0,0 +1,2 @@ +. [100%] +1 passed in 0.39s diff --git a/evidence/full-damage-geometry-20261006/original-after-publication-control-refusal.log b/evidence/full-damage-geometry-20261006/original-after-publication-control-refusal.log new file mode 100644 index 0000000000..11c6e65670 --- /dev/null +++ b/evidence/full-damage-geometry-20261006/original-after-publication-control-refusal.log @@ -0,0 +1,13 @@ +F [100%] +=================================== FAILURES =================================== +_________ test_partial_publication_keeps_geometry_and_owner_callbacks __________ +tests/test_batch_paint_admission.py:215: in test_partial_publication_keeps_geometry_and_owner_callbacks + assert compositor._dirty_regions == full_damage +E assert {Region(x=0, ...20, height=3)} == {Region(x=0, ...20, height=3)} +E +E Extra items in the right set: +E Region(x=0, y=0, width=40, height=8) +E Use -v to get more diff +=========================== short test summary info ============================ +FAILED tests/test_batch_paint_admission.py::test_partial_publication_keeps_geometry_and_owner_callbacks +1 failed in 0.29s diff --git a/evidence/full-damage-geometry-20261006/unchanged-target-after-wrong-edit-refusal.log b/evidence/full-damage-geometry-20261006/unchanged-target-after-wrong-edit-refusal.log new file mode 100644 index 0000000000..11c6e65670 --- /dev/null +++ b/evidence/full-damage-geometry-20261006/unchanged-target-after-wrong-edit-refusal.log @@ -0,0 +1,13 @@ +F [100%] +=================================== FAILURES =================================== +_________ test_partial_publication_keeps_geometry_and_owner_callbacks __________ +tests/test_batch_paint_admission.py:215: in test_partial_publication_keeps_geometry_and_owner_callbacks + assert compositor._dirty_regions == full_damage +E assert {Region(x=0, ...20, height=3)} == {Region(x=0, ...20, height=3)} +E +E Extra items in the right set: +E Region(x=0, y=0, width=40, height=8) +E Use -v to get more diff +=========================== short test summary info ============================ +FAILED tests/test_batch_paint_admission.py::test_partial_publication_keeps_geometry_and_owner_callbacks +1 failed in 0.29s