diff --git a/evidence/relative-height-measurement-resource-20261007/README.md b/evidence/relative-height-measurement-resource-20261007/README.md new file mode 100644 index 0000000000..510c96597d --- /dev/null +++ b/evidence/relative-height-measurement-resource-20261007/README.md @@ -0,0 +1,58 @@ +# Relative-height measurement source + +Widget's original height-dependency resource now holds the separate computed +box and relative-descendant answers under its original structural/layout epoch. +It does not derive relative stretch from content or box independence. +Unchanged native descendants answer through that resource instead of walking +their children again. Paint-only refresh does not retire it. + +NodeList publishes membership, display constraints and prune admission to +ancestor epochs. Styles publishes actual geometry-rule changes through the +measurement owner before idle, including raw rule writes. Authored layout +changes propagate through auto-size ancestry; the relative query stops at +fixed-size children. Reparenting invalidates old and new native custody. + +The original query remains live for custom container, child-list, display, +relative-height and scalar implementations. Source uncertainty propagates +through the actual auto-child path; it is not inferred from labels or bounds. +Custom content/layout behavior remains governed by its separate HeightDependency +declaration and is not used as a certificate for relative stretch. + +Direct message-pump closure also changes native display before detach. The +existing pump now supplies a polymorphic closing hook; DOMNode publishes the +same NodeList update used by prune admission. This completes the native source +lifetime rather than keeping a stale relative answer until physical removal. + +21 affected checks passed across the retained logs: 17 first-pass positives, +three corrected authored checks, and one live child-selection check. Three +initial authored oracles incorrectly assumed the imperative `50%` setter keeps +a PERCENT scalar; ScalarProperty normalizes it to HEIGHT, which the original +relative predicate does not recognize. controls-first.log preserves those +failures. The corrected checks use the original FRACTION behavior, without a +product parser or predicate change. + +The existing loaded source Toad App completed with empty stderr, 10 tabs and +568 widgets, growing to 592 after three authored response appends. Every height +answer matched the original Git property on the same acquired tree; queries +preserved actual geometry before and after those content changes. Five +alternating unprofiled trials of 20 query sweeps measured median 54.24→45.86ms +loaded and 58.61→46.55ms after the authored burst. In the latter actual cProfile, +is_container calls fell from 18,840 to 4,126. This is query work, not a live +frame/whole-burst speed claim. The authored burst completed to idle in .262s; +there is no original-versus-changed burst-latency comparison. + +The final closing-hook rename is a complete declaration/caller rename to +_message_pump_closing, avoiding the event-handler namespace for a custom +Closing message. Original transition conditions remain explicit; an already +closed pump does not republish its display retirement. + +Custom getters without native mutation publication remain live, and their +ancestors cannot retain an uncertain answer. Native membership and rules must +still be changed through their original owners; direct replacement of private +NodeList/RenderStyles storage or mutation of native declarations without a +source publication is not certified by these epochs. External dynamic source +resolution cannot be established by the lexical AST record alone. + +No provider/input, package/public runtime or saved-data operation is part of +this change. Original profile gaps remain correlation, not duration attribution. +Matching installation and live scroll/burst acceptance remain Parent's work. diff --git a/evidence/relative-height-measurement-resource-20261007/app.stderr.log b/evidence/relative-height-measurement-resource-20261007/app.stderr.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/evidence/relative-height-measurement-resource-20261007/app.stdout.log b/evidence/relative-height-measurement-resource-20261007/app.stdout.log new file mode 100644 index 0000000000..7836819909 --- /dev/null +++ b/evidence/relative-height-measurement-resource-20261007/app.stdout.log @@ -0,0 +1,84 @@ +{ + "scope": "source App height queries and authored response geometry, not live frame latency", + "tabs": 10, + "sweeps": 20, + "phases": { + "loaded": { + "widgets": 568, + "seconds": { + "original": [ + 0.045427089906297624, + 0.05181200604420155, + 0.06757318798918277, + 0.08904787409119308, + 0.05424024909734726 + ], + "retained": [ + 0.03314762795343995, + 0.045864460058510303, + 0.03516618604771793, + 0.07208854996133596, + 0.05582378106191754 + ] + }, + "median_seconds": { + "original": 0.05424024909734726, + "retained": 0.045864460058510303 + }, + "profile_actual_calls": { + "original": { + "_has_relative_children_height": 16280, + "_relative_children_height": 0, + "is_container": 16280 + }, + "retained": { + "_has_relative_children_height": 11360, + "_relative_children_height": 14068, + "is_container": 4419 + } + }, + "answers_equal": true, + "geometry_unchanged_during_queries": true + }, + "after_authored_response_burst": { + "widgets": 592, + "seconds": { + "original": [ + 0.05860715894959867, + 0.05218407104257494, + 0.05673567100893706, + 0.0629461359931156, + 0.07115682295989245 + ], + "retained": [ + 0.053681319928728044, + 0.04390930093359202, + 0.045053731999360025, + 0.05055906006600708, + 0.04655407601967454 + ] + }, + "median_seconds": { + "original": 0.05860715894959867, + "retained": 0.04655407601967454 + }, + "profile_actual_calls": { + "original": { + "_has_relative_children_height": 18680, + "_relative_children_height": 0, + "is_container": 18840 + }, + "retained": { + "_has_relative_children_height": 11839, + "_relative_children_height": 16380, + "is_container": 4126 + } + }, + "answers_equal": true, + "geometry_unchanged_during_queries": true + } + }, + "authored_burst_to_idle_seconds": 0.26241247402504086, + "inputs": 0, + "providers": 0 +} diff --git a/evidence/relative-height-measurement-resource-20261007/controls-corrected.log b/evidence/relative-height-measurement-resource-20261007/controls-corrected.log new file mode 100644 index 0000000000..07be1cf0f0 --- /dev/null +++ b/evidence/relative-height-measurement-resource-20261007/controls-corrected.log @@ -0,0 +1,2 @@ +... [100%] +3 passed in 0.48s diff --git a/evidence/relative-height-measurement-resource-20261007/controls-first.log b/evidence/relative-height-measurement-resource-20261007/controls-first.log new file mode 100644 index 0000000000..1db9f1df02 --- /dev/null +++ b/evidence/relative-height-measurement-resource-20261007/controls-first.log @@ -0,0 +1,22 @@ +FF.F................ [100%] +=================================== FAILURES =================================== +_________ test_native_relative_source_changes_before_idle_and_reparent _________ +tests/test_relative_height_resource.py:20: in test_native_relative_source_changes_before_idle_and_reparent + assert first._has_relative_children_height +E assert False +E + where False = VerticalGroup()._has_relative_children_height +__ test_opaque_display_and_container_getters_do_not_retire_with_native_epochs __ +tests/test_relative_height_resource.py:74: in test_opaque_display_and_container_getters_do_not_retire_with_native_epochs + assert display_parent._has_relative_children_height is enabled +E assert False is True +E + where False = VerticalGroup()._has_relative_children_height +_______ test_direct_pump_retirement_publishes_display_before_detachment ________ +tests/test_relative_height_resource.py:108: in test_direct_pump_retirement_publishes_display_before_detachment + assert parent._has_relative_children_height +E assert False +E + where False = VerticalGroup()._has_relative_children_height +=========================== short test summary info ============================ +FAILED tests/test_relative_height_resource.py::test_native_relative_source_changes_before_idle_and_reparent +FAILED tests/test_relative_height_resource.py::test_opaque_display_and_container_getters_do_not_retire_with_native_epochs +FAILED tests/test_relative_height_resource.py::test_direct_pump_retirement_publishes_display_before_detachment +3 failed, 17 passed in 1.68s diff --git a/evidence/relative-height-measurement-resource-20261007/custom-children.log b/evidence/relative-height-measurement-resource-20261007/custom-children.log new file mode 100644 index 0000000000..c91be48cb8 --- /dev/null +++ b/evidence/relative-height-measurement-resource-20261007/custom-children.log @@ -0,0 +1,2 @@ +. [100%] +1 passed in 0.37s diff --git a/evidence/relative-height-measurement-resource-20261007/source-after.json b/evidence/relative-height-measurement-resource-20261007/source-after.json new file mode 100644 index 0000000000..08e6bbc56a --- /dev/null +++ b/evidence/relative-height-measurement-resource-20261007/source-after.json @@ -0,0 +1,1538 @@ +{ + "native": { + "modules": 249, + "omissions": [], + "sites": [ + [ + "src/textual/_compositor.py", + 90, + "Load", + "widget._nodes._updates" + ], + [ + "src/textual/_measurement.py", + 20, + "Load", + "widget._has_relative_children_height" + ], + [ + "src/textual/_node_list.py", + 57, + "Store", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 87, + "Store", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 100, + "Store", + "nodes._updates" + ], + [ + "src/textual/_node_list.py", + 245, + "Load", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 247, + "Load", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 266, + "Load", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 271, + "Load", + "self._updates" + ], + [ + "src/textual/app.py", + 3687, + "Store", + "widget._closing" + ], + [ + "src/textual/app.py", + 3688, + "Store", + "widget._closed" + ], + [ + "src/textual/app.py", + 3689, + "Store", + "widget._pruning" + ], + [ + "src/textual/app.py", + 3877, + "Load", + "self._closed" + ], + [ + "src/textual/await_remove.py", + 63, + "Store", + "node._pruning" + ], + [ + "src/textual/await_remove.py", + 136, + "Load", + "node._closing" + ], + [ + "src/textual/await_remove.py", + 136, + "Load", + "node._closed" + ], + [ + "src/textual/css/styles.py", + 987, + "Store", + "self._updates" + ], + [ + "src/textual/css/styles.py", + 1478, + "Load", + "self._base_styles._updates" + ], + [ + "src/textual/css/styles.py", + 1478, + "Load", + "self._inline_styles._updates" + ], + [ + "src/textual/dom.py", + 152, + "declaration", + "_style_geometry_updated" + ], + [ + "src/textual/dom.py", + 985, + "declaration", + "_message_pump_closing" + ], + [ + "src/textual/dom.py", + 272, + "Store", + "self._pruning" + ], + [ + "src/textual/dom.py", + 170, + "Load", + "self._style_geometry_updated" + ], + [ + "src/textual/dom.py", + 986, + "Load", + "super()._message_pump_closing" + ], + [ + "src/textual/dom.py", + 184, + "Load", + "node._style_geometry_updated" + ], + [ + "src/textual/dom.py", + 1606, + "Load", + "base_node._nodes._updates" + ], + [ + "src/textual/dom.py", + 1631, + "Load", + "base_node._nodes._updates" + ], + [ + "src/textual/dom.py", + 1741, + "Load", + "base_node._nodes._updates" + ], + [ + "src/textual/dom.py", + 1003, + "Load", + "self._closing" + ], + [ + "src/textual/dom.py", + 1003, + "Load", + "self._closed" + ], + [ + "src/textual/dom.py", + 1003, + "Load", + "self._pruning" + ], + [ + "src/textual/message_pump.py", + 576, + "declaration", + "_message_pump_closing" + ], + [ + "src/textual/message_pump.py", + 132, + "Store", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 133, + "Store", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 386, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 583, + "Store", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 1014, + "Load", + "self.app._closing" + ], + [ + "src/textual/message_pump.py", + 393, + "Store", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 414, + "Store", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 581, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 581, + "Load", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 584, + "Load", + "self._message_pump_closing" + ], + [ + "src/textual/message_pump.py", + 617, + "Store", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 618, + "Store", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 713, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 998, + "Load", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 998, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 619, + "Load", + "self._message_pump_closing" + ], + [ + "src/textual/message_pump.py", + 332, + "Load", + "parent._closed" + ], + [ + "src/textual/message_pump.py", + 332, + "Load", + "parent._closing" + ], + [ + "src/textual/message_pump.py", + 391, + "Load", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 391, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 392, + "Load", + "self._message_pump_closing" + ], + [ + "src/textual/message_pump.py", + 412, + "Load", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 412, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 413, + "Load", + "self._message_pump_closing" + ], + [ + "src/textual/message_pump.py", + 724, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 724, + "Load", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 766, + "Load", + "self._closed" + ], + [ + "src/textual/reactive.py", + 578, + "Load", + "node._closing" + ], + [ + "src/textual/reactive.py", + 578, + "Load", + "node._closed" + ], + [ + "src/textual/reactive.py", + 590, + "Load", + "node._closing" + ], + [ + "src/textual/reactive.py", + 590, + "Load", + "node._closed" + ], + [ + "src/textual/reactive.py", + 476, + "Load", + "reactable._closing" + ], + [ + "src/textual/reactive.py", + 476, + "Load", + "reactable._closed" + ], + [ + "src/textual/reactive.py", + 473, + "Load", + "reactable._closing" + ], + [ + "src/textual/screen.py", + 522, + "Load", + "self._nodes._updates" + ], + [ + "src/textual/screen.py", + 1542, + "Load", + "widget._pruning" + ], + [ + "src/textual/screen.py", + 1542, + "Load", + "widget._closed" + ], + [ + "src/textual/screen.py", + 1555, + "Load", + "widget._pruning" + ], + [ + "src/textual/screen.py", + 1555, + "Load", + "widget._closed" + ], + [ + "src/textual/screen.py", + 1704, + "Load", + "self._closing" + ], + [ + "src/textual/screen.py", + 1711, + "Load", + "self._closing" + ], + [ + "src/textual/signal.py", + 77, + "Load", + "node._closing" + ], + [ + "src/textual/signal.py", + 77, + "Load", + "node._closed" + ], + [ + "src/textual/signal.py", + 121, + "Load", + "owner._pruning" + ], + [ + "src/textual/signal.py", + 128, + "Load", + "node._pruning" + ], + [ + "src/textual/widget.py", + 323, + "declaration", + "_style_geometry_updated" + ], + [ + "src/textual/widget.py", + 332, + "declaration", + "_measurement_updated" + ], + [ + "src/textual/widget.py", + 1904, + "declaration", + "_height_dependency_answers" + ], + [ + "src/textual/widget.py", + 2655, + "declaration", + "_has_relative_children_height" + ], + [ + "src/textual/widget.py", + 2659, + "declaration", + "_relative_children_height" + ], + [ + "src/textual/widget.py", + 334, + "Store", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 501, + "Store", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 324, + "Load", + "super()._style_geometry_updated" + ], + [ + "src/textual/widget.py", + 345, + "Load", + "self._measurement_updated" + ], + [ + "src/textual/widget.py", + 1009, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1025, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1041, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1055, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1069, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1426, + "Load", + "self._nodes._updates" + ], + [ + "src/textual/widget.py", + 1426, + "Load", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 1539, + "Load", + "self._closing" + ], + [ + "src/textual/widget.py", + 1539, + "Load", + "self._pruning" + ], + [ + "src/textual/widget.py", + 1777, + "Load", + "self._closing" + ], + [ + "src/textual/widget.py", + 1777, + "Load", + "self._pruning" + ], + [ + "src/textual/widget.py", + 1778, + "Load", + "parent._closing" + ], + [ + "src/textual/widget.py", + 1778, + "Load", + "parent._pruning" + ], + [ + "src/textual/widget.py", + 1858, + "Load", + "self._pruning" + ], + [ + "src/textual/widget.py", + 1892, + "Load", + "self._nodes._updates" + ], + [ + "src/textual/widget.py", + 1892, + "Load", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 1912, + "Load", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 1916, + "Store", + "self._height_dependency_cache" + ], + [ + "src/textual/widget.py", + 1920, + "Load", + "self._height_dependency_answers" + ], + [ + "src/textual/widget.py", + 1953, + "Load", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 2671, + "Load", + "self._height_dependency_answers" + ], + [ + "src/textual/widget.py", + 329, + "Load", + "self._measurement_updated" + ], + [ + "src/textual/widget.py", + 349, + "Load", + "ancestor._measurement_updated" + ], + [ + "src/textual/widget.py", + 1044, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1058, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1073, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1912, + "Load", + "nodes._updates" + ], + [ + "src/textual/widget.py", + 1954, + "Load", + "nodes._updates" + ], + [ + "src/textual/widget.py", + 2657, + "Load", + "self._relative_children_height" + ], + [ + "src/textual/widget.py", + 4164, + "Load", + "cls._has_relative_children_height" + ], + [ + "src/textual/widget.py", + 4164, + "Load", + "Widget._has_relative_children_height" + ], + [ + "src/textual/widget.py", + 4872, + "Load", + "self._closing" + ], + [ + "src/textual/widget.py", + 1014, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1030, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 2072, + "Load", + "self._has_relative_children_height" + ], + [ + "src/textual/widget.py", + 2695, + "Load", + "type(child)._has_relative_children_height" + ], + [ + "src/textual/widget.py", + 2695, + "Load", + "Widget._has_relative_children_height" + ], + [ + "src/textual/widget.py", + 2696, + "Load", + "child._relative_children_height" + ], + [ + "src/textual/widget.py", + 2698, + "Load", + "child._has_relative_children_height" + ], + [ + "src/textual/widgets/_directory_tree.py", + 228, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_footer.py", + 361, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 361, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_footer.py", + 437, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 437, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_footer.py", + 451, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 451, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_footer.py", + 393, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 393, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_footer.py", + 355, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 356, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_log.py", + 75, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_log.py", + 260, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_log.py", + 107, + "Load", + "self._updates" + ], + [ + "src/textual/widgets/_log.py", + 239, + "Load", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 128, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 138, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 171, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 181, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 248, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 257, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 290, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 355, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 431, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 513, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 781, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 875, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 944, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 1078, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 1331, + "Load", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 1333, + "Load", + "node._updates" + ] + ] + }, + "toad": { + "modules": 288, + "omissions": [], + "sites": [ + [ + "src/toad/ansi/_ansi.py", + 1357, + "Store", + "self._updates" + ], + [ + "src/toad/ansi/_ansi.py", + 1393, + "Load", + "self._updates" + ], + [ + "src/toad/ansi/_ansi.py", + 1412, + "Store", + "self._updates" + ], + [ + "src/toad/ansi/_ansi.py", + 1413, + "Load", + "self._updates" + ], + [ + "src/toad/ansi/_ansi.py", + 1654, + "Load", + "self._updates" + ], + [ + "src/toad/channel_preparation.py", + 157, + "Store", + "self._closed" + ], + [ + "src/toad/channel_preparation.py", + 180, + "Load", + "self._closed" + ], + [ + "src/toad/channel_preparation.py", + 195, + "Store", + "self._closed" + ], + [ + "src/toad/navigation_preparation.py", + 204, + "Store", + "self._closed" + ], + [ + "src/toad/navigation_preparation.py", + 236, + "Store", + "self._closed" + ], + [ + "src/toad/navigation_preparation.py", + 213, + "Load", + "self._closed" + ], + [ + "src/toad/navigation_preparation.py", + 225, + "Load", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 57, + "Store", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 63, + "Load", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 86, + "Load", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 110, + "Store", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 83, + "Load", + "self._closed" + ], + [ + "src/toad/render_protocol.py", + 107, + "Load", + "client._closed" + ], + [ + "src/toad/render_protocol.py", + 115, + "Load", + "client._closed" + ], + [ + "src/toad/render_runtime.py", + 38, + "Store", + "self._closed" + ], + [ + "src/toad/render_runtime.py", + 67, + "Load", + "self._closed" + ], + [ + "src/toad/render_runtime.py", + 74, + "Load", + "self._closed" + ], + [ + "src/toad/render_runtime.py", + 116, + "Store", + "self._closed" + ], + [ + "src/toad/render_service.py", + 80, + "Store", + "self._closed" + ], + [ + "src/toad/render_service.py", + 112, + "Load", + "self._closed" + ], + [ + "src/toad/render_service.py", + 165, + "Store", + "self._closed" + ], + [ + "src/toad/render_service.py", + 169, + "Store", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 232, + "Store", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 270, + "Load", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 300, + "Store", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 295, + "Load", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 267, + "Load", + "self._closed" + ], + [ + "src/toad/screens/main.py", + 262, + "Load", + "self._closing" + ], + [ + "src/toad/screens/main.py", + 262, + "Load", + "self._closed" + ], + [ + "src/toad/transcript_publication.py", + 66, + "Load", + "view._closing" + ], + [ + "src/toad/transcript_publication.py", + 380, + "Load", + "view._pruning" + ], + [ + "src/toad/transcript_state.py", + 62, + "Load", + "widget._closing" + ], + [ + "src/toad/transcript_state.py", + 64, + "Load", + "widget._pruning" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 119, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 119, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 252, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 252, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 473, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 473, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/conversation.py", + 172, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 101, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 101, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 111, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 111, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 115, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 115, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 108, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 108, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/project_panel.py", + 197, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/project_panel.py", + 204, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/project_panel.py", + 212, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/project_panel.py", + 194, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/side_bar.py", + 713, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/side_bar.py", + 721, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/side_bar.py", + 726, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/sidebar_tree.py", + 70, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/sidebar_tree.py", + 70, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/streaming_markdown.py", + 96, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/streaming_markdown.py", + 90, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/streaming_markdown.py", + 106, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/tool_call.py", + 63, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/tool_content.py", + 163, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/viewport_body.py", + 582, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 669, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 998, + "Load", + "self.window._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1008, + "Load", + "self.window._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 443, + "Load", + "body._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 517, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 675, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1091, + "Load", + "self.window._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1189, + "Load", + "owner._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1203, + "Load", + "owner._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1123, + "Load", + "owner._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1132, + "Load", + "owner._closing" + ], + [ + "src/toad/widgets/worker_static.py", + 51, + "Store", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 65, + "Store", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 160, + "Store", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 102, + "Load", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 102, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/worker_static.py", + 132, + "Load", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 145, + "Load", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 145, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/worker_static.py", + 139, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 273, + "Store", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 279, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 282, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 316, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 373, + "Store", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 347, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 353, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 356, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 320, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 363, + "Load", + "self._closed" + ] + ] + } +} diff --git a/evidence/relative-height-measurement-resource-20261007/source-before.json b/evidence/relative-height-measurement-resource-20261007/source-before.json new file mode 100644 index 0000000000..1f1fba0937 --- /dev/null +++ b/evidence/relative-height-measurement-resource-20261007/source-before.json @@ -0,0 +1,1424 @@ +{ + "native": { + "modules": 249, + "omissions": [], + "sites": [ + [ + "src/textual/_compositor.py", + 90, + "Load", + "widget._nodes._updates" + ], + [ + "src/textual/_measurement.py", + 20, + "Load", + "widget._has_relative_children_height" + ], + [ + "src/textual/_node_list.py", + 57, + "Store", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 87, + "Store", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 100, + "Store", + "nodes._updates" + ], + [ + "src/textual/_node_list.py", + 245, + "Load", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 247, + "Load", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 266, + "Load", + "self._updates" + ], + [ + "src/textual/_node_list.py", + 271, + "Load", + "self._updates" + ], + [ + "src/textual/app.py", + 3687, + "Store", + "widget._closing" + ], + [ + "src/textual/app.py", + 3688, + "Store", + "widget._closed" + ], + [ + "src/textual/app.py", + 3689, + "Store", + "widget._pruning" + ], + [ + "src/textual/app.py", + 3877, + "Load", + "self._closed" + ], + [ + "src/textual/await_remove.py", + 63, + "Store", + "node._pruning" + ], + [ + "src/textual/await_remove.py", + 136, + "Load", + "node._closing" + ], + [ + "src/textual/await_remove.py", + 136, + "Load", + "node._closed" + ], + [ + "src/textual/css/styles.py", + 987, + "Store", + "self._updates" + ], + [ + "src/textual/css/styles.py", + 1478, + "Load", + "self._base_styles._updates" + ], + [ + "src/textual/css/styles.py", + 1478, + "Load", + "self._inline_styles._updates" + ], + [ + "src/textual/dom.py", + 152, + "declaration", + "_style_geometry_updated" + ], + [ + "src/textual/dom.py", + 272, + "Store", + "self._pruning" + ], + [ + "src/textual/dom.py", + 170, + "Load", + "self._style_geometry_updated" + ], + [ + "src/textual/dom.py", + 184, + "Load", + "node._style_geometry_updated" + ], + [ + "src/textual/dom.py", + 1600, + "Load", + "base_node._nodes._updates" + ], + [ + "src/textual/dom.py", + 1625, + "Load", + "base_node._nodes._updates" + ], + [ + "src/textual/dom.py", + 1735, + "Load", + "base_node._nodes._updates" + ], + [ + "src/textual/dom.py", + 997, + "Load", + "self._closing" + ], + [ + "src/textual/dom.py", + 997, + "Load", + "self._closed" + ], + [ + "src/textual/dom.py", + 997, + "Load", + "self._pruning" + ], + [ + "src/textual/message_pump.py", + 132, + "Store", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 133, + "Store", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 386, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 576, + "Store", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 1005, + "Load", + "self.app._closing" + ], + [ + "src/textual/message_pump.py", + 391, + "Store", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 410, + "Store", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 574, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 574, + "Load", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 609, + "Store", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 610, + "Store", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 704, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 989, + "Load", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 989, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 332, + "Load", + "parent._closed" + ], + [ + "src/textual/message_pump.py", + 332, + "Load", + "parent._closing" + ], + [ + "src/textual/message_pump.py", + 715, + "Load", + "self._closed" + ], + [ + "src/textual/message_pump.py", + 715, + "Load", + "self._closing" + ], + [ + "src/textual/message_pump.py", + 757, + "Load", + "self._closed" + ], + [ + "src/textual/reactive.py", + 578, + "Load", + "node._closing" + ], + [ + "src/textual/reactive.py", + 578, + "Load", + "node._closed" + ], + [ + "src/textual/reactive.py", + 590, + "Load", + "node._closing" + ], + [ + "src/textual/reactive.py", + 590, + "Load", + "node._closed" + ], + [ + "src/textual/reactive.py", + 476, + "Load", + "reactable._closing" + ], + [ + "src/textual/reactive.py", + 476, + "Load", + "reactable._closed" + ], + [ + "src/textual/reactive.py", + 473, + "Load", + "reactable._closing" + ], + [ + "src/textual/screen.py", + 522, + "Load", + "self._nodes._updates" + ], + [ + "src/textual/screen.py", + 1542, + "Load", + "widget._pruning" + ], + [ + "src/textual/screen.py", + 1542, + "Load", + "widget._closed" + ], + [ + "src/textual/screen.py", + 1555, + "Load", + "widget._pruning" + ], + [ + "src/textual/screen.py", + 1555, + "Load", + "widget._closed" + ], + [ + "src/textual/screen.py", + 1704, + "Load", + "self._closing" + ], + [ + "src/textual/screen.py", + 1711, + "Load", + "self._closing" + ], + [ + "src/textual/signal.py", + 77, + "Load", + "node._closing" + ], + [ + "src/textual/signal.py", + 77, + "Load", + "node._closed" + ], + [ + "src/textual/signal.py", + 121, + "Load", + "owner._pruning" + ], + [ + "src/textual/signal.py", + 128, + "Load", + "node._pruning" + ], + [ + "src/textual/widget.py", + 323, + "declaration", + "_style_geometry_updated" + ], + [ + "src/textual/widget.py", + 332, + "declaration", + "_measurement_updated" + ], + [ + "src/textual/widget.py", + 2642, + "declaration", + "_has_relative_children_height" + ], + [ + "src/textual/widget.py", + 334, + "Store", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 501, + "Store", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 1911, + "Store", + "self._height_dependency_cache" + ], + [ + "src/textual/widget.py", + 324, + "Load", + "super()._style_geometry_updated" + ], + [ + "src/textual/widget.py", + 345, + "Load", + "self._measurement_updated" + ], + [ + "src/textual/widget.py", + 1009, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1025, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1041, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1055, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1069, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1426, + "Load", + "self._nodes._updates" + ], + [ + "src/textual/widget.py", + 1426, + "Load", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 1539, + "Load", + "self._closing" + ], + [ + "src/textual/widget.py", + 1539, + "Load", + "self._pruning" + ], + [ + "src/textual/widget.py", + 1777, + "Load", + "self._closing" + ], + [ + "src/textual/widget.py", + 1777, + "Load", + "self._pruning" + ], + [ + "src/textual/widget.py", + 1778, + "Load", + "parent._closing" + ], + [ + "src/textual/widget.py", + 1778, + "Load", + "parent._pruning" + ], + [ + "src/textual/widget.py", + 1858, + "Load", + "self._pruning" + ], + [ + "src/textual/widget.py", + 1892, + "Load", + "self._nodes._updates" + ], + [ + "src/textual/widget.py", + 1892, + "Load", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 1906, + "Load", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 1940, + "Load", + "self._layout_updates" + ], + [ + "src/textual/widget.py", + 329, + "Load", + "self._measurement_updated" + ], + [ + "src/textual/widget.py", + 349, + "Load", + "ancestor._measurement_updated" + ], + [ + "src/textual/widget.py", + 1044, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1058, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1073, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1906, + "Load", + "nodes._updates" + ], + [ + "src/textual/widget.py", + 1941, + "Load", + "nodes._updates" + ], + [ + "src/textual/widget.py", + 4117, + "Load", + "cls._has_relative_children_height" + ], + [ + "src/textual/widget.py", + 4117, + "Load", + "Widget._has_relative_children_height" + ], + [ + "src/textual/widget.py", + 4825, + "Load", + "self._closing" + ], + [ + "src/textual/widget.py", + 1014, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 1030, + "Load", + "parent._nodes._updates" + ], + [ + "src/textual/widget.py", + 2059, + "Load", + "self._has_relative_children_height" + ], + [ + "src/textual/widget.py", + 2655, + "Load", + "child._has_relative_children_height" + ], + [ + "src/textual/widgets/_directory_tree.py", + 228, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_footer.py", + 361, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 361, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_footer.py", + 437, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 437, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_footer.py", + 451, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 451, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_footer.py", + 393, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 393, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_footer.py", + 355, + "Load", + "self._closing" + ], + [ + "src/textual/widgets/_footer.py", + 356, + "Load", + "self._pruning" + ], + [ + "src/textual/widgets/_log.py", + 75, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_log.py", + 260, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_log.py", + 107, + "Load", + "self._updates" + ], + [ + "src/textual/widgets/_log.py", + 239, + "Load", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 128, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 138, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 171, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 181, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 248, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 257, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 290, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 355, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 431, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 513, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 781, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 875, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 944, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 1078, + "Store", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 1331, + "Load", + "self._updates" + ], + [ + "src/textual/widgets/_tree.py", + 1333, + "Load", + "node._updates" + ] + ] + }, + "toad": { + "modules": 288, + "omissions": [], + "sites": [ + [ + "src/toad/ansi/_ansi.py", + 1357, + "Store", + "self._updates" + ], + [ + "src/toad/ansi/_ansi.py", + 1393, + "Load", + "self._updates" + ], + [ + "src/toad/ansi/_ansi.py", + 1412, + "Store", + "self._updates" + ], + [ + "src/toad/ansi/_ansi.py", + 1413, + "Load", + "self._updates" + ], + [ + "src/toad/ansi/_ansi.py", + 1654, + "Load", + "self._updates" + ], + [ + "src/toad/channel_preparation.py", + 157, + "Store", + "self._closed" + ], + [ + "src/toad/channel_preparation.py", + 180, + "Load", + "self._closed" + ], + [ + "src/toad/channel_preparation.py", + 195, + "Store", + "self._closed" + ], + [ + "src/toad/navigation_preparation.py", + 204, + "Store", + "self._closed" + ], + [ + "src/toad/navigation_preparation.py", + 236, + "Store", + "self._closed" + ], + [ + "src/toad/navigation_preparation.py", + 213, + "Load", + "self._closed" + ], + [ + "src/toad/navigation_preparation.py", + 225, + "Load", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 57, + "Store", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 63, + "Load", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 86, + "Load", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 110, + "Store", + "self._closed" + ], + [ + "src/toad/render_processes.py", + 83, + "Load", + "self._closed" + ], + [ + "src/toad/render_protocol.py", + 107, + "Load", + "client._closed" + ], + [ + "src/toad/render_protocol.py", + 115, + "Load", + "client._closed" + ], + [ + "src/toad/render_runtime.py", + 38, + "Store", + "self._closed" + ], + [ + "src/toad/render_runtime.py", + 67, + "Load", + "self._closed" + ], + [ + "src/toad/render_runtime.py", + 74, + "Load", + "self._closed" + ], + [ + "src/toad/render_runtime.py", + 116, + "Store", + "self._closed" + ], + [ + "src/toad/render_service.py", + 80, + "Store", + "self._closed" + ], + [ + "src/toad/render_service.py", + 112, + "Load", + "self._closed" + ], + [ + "src/toad/render_service.py", + 165, + "Store", + "self._closed" + ], + [ + "src/toad/render_service.py", + 169, + "Store", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 232, + "Store", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 270, + "Load", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 300, + "Store", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 295, + "Load", + "self._closed" + ], + [ + "src/toad/render_zmq.py", + 267, + "Load", + "self._closed" + ], + [ + "src/toad/screens/main.py", + 262, + "Load", + "self._closing" + ], + [ + "src/toad/screens/main.py", + 262, + "Load", + "self._closed" + ], + [ + "src/toad/transcript_publication.py", + 66, + "Load", + "view._closing" + ], + [ + "src/toad/transcript_publication.py", + 380, + "Load", + "view._pruning" + ], + [ + "src/toad/transcript_state.py", + 62, + "Load", + "widget._closing" + ], + [ + "src/toad/transcript_state.py", + 64, + "Load", + "widget._pruning" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 119, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 119, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 252, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 252, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 473, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/comms_sidebar.py", + 473, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/conversation.py", + 172, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 101, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 101, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 111, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 111, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 115, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 115, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 108, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/prepared_markdown.py", + 108, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/project_panel.py", + 197, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/project_panel.py", + 204, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/project_panel.py", + 212, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/project_panel.py", + 194, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/side_bar.py", + 713, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/side_bar.py", + 721, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/side_bar.py", + 726, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/sidebar_tree.py", + 70, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/sidebar_tree.py", + 70, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/streaming_markdown.py", + 96, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/streaming_markdown.py", + 90, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/streaming_markdown.py", + 106, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/tool_call.py", + 63, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/tool_content.py", + 163, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/viewport_body.py", + 582, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 669, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 998, + "Load", + "self.window._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1008, + "Load", + "self.window._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 443, + "Load", + "body._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 517, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 675, + "Load", + "self._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1091, + "Load", + "self.window._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1189, + "Load", + "owner._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1203, + "Load", + "owner._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1123, + "Load", + "owner._closing" + ], + [ + "src/toad/widgets/viewport_body.py", + 1132, + "Load", + "owner._closing" + ], + [ + "src/toad/widgets/worker_static.py", + 51, + "Store", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 65, + "Store", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 160, + "Store", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 102, + "Load", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 102, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/worker_static.py", + 132, + "Load", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 145, + "Load", + "self._closed" + ], + [ + "src/toad/widgets/worker_static.py", + 145, + "Load", + "self._pruning" + ], + [ + "src/toad/widgets/worker_static.py", + 139, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 273, + "Store", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 279, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 282, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 316, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 373, + "Store", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 347, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 353, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 356, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 320, + "Load", + "self._closed" + ], + [ + "src/toad/work_preparation.py", + 363, + "Load", + "self._closed" + ] + ] + } +} diff --git a/src/textual/dom.py b/src/textual/dom.py index a6146012e6..5a2cb03fa9 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -982,6 +982,12 @@ def _selector_names(self) -> set[str]: def _child_nodes_removed(self) -> None: """Retire child-derived ownership at the structural removal boundary.""" + def _message_pump_closing(self) -> None: + super()._message_pump_closing() + # Native display changes before child custody is physically removed. + # Publish through the same source as pruning and display constraints. + self._nodes.updated() + @property def display(self) -> bool: """Should the DOM node be displayed? diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 4118680361..43264fd449 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -388,6 +388,8 @@ async def _get_message(self) -> Message: message = await self._message_queue.get() if message is None: + if not (self._closing or self._closed): + self._message_pump_closing() self._closed = True raise MessagePumpClosed("The message pump is now closed") return message @@ -407,6 +409,8 @@ def _peek_message(self) -> Message | None: except QueueEmpty: return None if message is None: + if not (self._closing or self._closed): + self._message_pump_closing() self._closed = True raise MessagePumpClosed("The message pump is now closed") return message @@ -569,11 +573,15 @@ def _on_invoke_later(self, message: messages.InvokeLater) -> None: # rather than the ambient caller, owns the publication being awaited. self.app.screen._invoke_later(message.callback, self) + def _message_pump_closing(self) -> None: + """Publish a consumer's response to original pump retirement.""" + async def _close_messages(self, wait: bool = True) -> None: """Close message queue, and optionally wait for queue to finish processing.""" if self._closed or self._closing: return self._closing = True + self._message_pump_closing() self._clear_signal_subscriptions() Reactive._clear_watch_subscriptions(self) if self._timers: @@ -608,6 +616,7 @@ def _start_messages(self) -> None: else: self._closing = True self._closed = True + self._message_pump_closing() async def _process_messages(self, **kwargs: Any) -> None: """Own the real pump task through subclass startup, dispatch and exit.""" diff --git a/src/textual/widget.py b/src/textual/widget.py index 9fe65c19a4..20679de80f 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -42,7 +42,7 @@ from rich.text import Text from typing_extensions import Self -from textual.css.styles import StylesBase +from textual.css.styles import RenderStyles, StylesBase if TYPE_CHECKING: from textual.app import RenderResult @@ -1901,14 +1901,27 @@ def _arrangement_depends_on_available_height(self) -> bool: self._height_arrangement_cache = proof return proof[1] - def _box_depends_on_available_height(self) -> bool: + def _height_dependency_answers(self) -> dict[str, bool]: + """Computed height answers from the current native measurement source. + + Structure includes descendant membership/display publication; styles + publish descendant scalar changes before idle. Authored layout changes + retire the same resource. Paint alone does not discard these answers. + """ nodes = self.__dict__.get("_nodes") epoch = (nodes._updates if nodes is not None else 0, self._layout_updates) cached = self.__dict__.get("_height_dependency_cache") - if cached is not None and cached[0] == epoch: - return cached[1] + if cached is None or cached[0] != epoch: + cached = epoch, {} + self._height_dependency_cache = cached + return cached[1] + + def _box_depends_on_available_height(self) -> bool: + answers = self._height_dependency_answers() + if "box" in answers: + return answers["box"] dependent = box_depends_on_available_height(self) - self._height_dependency_cache = (epoch, dependent) + answers["box"] = dependent return dependent def _get_box_model( @@ -2641,21 +2654,55 @@ def _has_relative_children_width(self) -> bool: @property def _has_relative_children_height(self) -> bool: """Do any children (or progeny) have a relative height?""" + return self._relative_children_height()[0] + + def _relative_children_height(self) -> tuple[bool, bool]: + """The height answer and whether its reads have native source epochs. + + Unknown getters remain live. Their uncertainty follows the actual auto + child path into the parent; content/layout independence is not used to + guess this separate box-stretch answer. + """ + native = ( + type(self).is_container is Widget.is_container + and type(self).children is DOMNode.children + and type(self.styles) is RenderStyles + ) + answers = self._height_dependency_answers() + if native and "relative_children" in answers: + return answers["relative_children"], True if not self.is_container: - return False - for child in self.children: - styles = child.styles - if not child.display: - continue - height = styles.height - if height is None: - continue - if styles.is_relative_height or ( - height.is_auto and child._has_relative_children_height - ): - return True - return False + result = False + else: + result = False + for child in self.children: + styles = child.styles + native = native and ( + type(child).display is DOMNode.display + and type(styles) is RenderStyles + ) + if not child.display: + continue + height = styles.height + if height is None: + continue + native = native and type(height) is Scalar + if styles.is_relative_height: + result = True + break + if height.is_auto: + if type(child)._has_relative_children_height is Widget._has_relative_children_height: + relative, child_native = child._relative_children_height() + else: + relative, child_native = child._has_relative_children_height, False + native = native and child_native + if relative: + result = True + break + if native: + answers["relative_children"] = result + return result, native @property def is_on_screen(self) -> bool: diff --git a/tests/relative_height_resource_pilot.py b/tests/relative_height_resource_pilot.py new file mode 100644 index 0000000000..3583c61fe2 --- /dev/null +++ b/tests/relative_height_resource_pilot.py @@ -0,0 +1,114 @@ +"""Confirm native height answer reuse on the existing loaded Toad App.""" + +import ast +import asyncio +import cProfile +import json +import os +from pathlib import Path +import pstats +import statistics +import subprocess +import tempfile +from time import perf_counter + +from agent_comms.comms import wire +from agent_comms.threads import Thread +from sidebar_collapse_latency_pilot import FrameApp +from textual.widget import Widget +from toad.widgets.agent_response import AgentResponse + + +def original_relative_property(): + source = subprocess.check_output( + ["git", "show", "22f65423d:src/textual/widget.py"], text=True + ) + owner = next(node for node in ast.parse(source).body + if isinstance(node, ast.ClassDef) and node.name == "Widget") + function = next(node for node in owner.body + if isinstance(node, ast.FunctionDef) and node.name == "_has_relative_children_height") + function.decorator_list = [] + module = ast.Module(body=[function], type_ignores=[]) + namespace = {} + exec(compile(ast.fix_missing_locations(module), "original_widget.py", "exec"), namespace) + return property(namespace[function.name]) + + +async def main(output: Path): + output.mkdir(parents=True, exist_ok=True) + current = Widget._has_relative_children_height + original = original_relative_property() + with tempfile.TemporaryDirectory(prefix="height-resource-", dir=output) as folder: + root = Path(folder) + os.environ.update(XDG_CONFIG_HOME=str(root / "config"), + XDG_STATE_HOME=str(root / "state"), + XDG_DATA_HOME=str(root / "data"), AGENT_COMMS_ROOT=str(root / "wire")) + comms = wire(root / "wire") + comms.registry.declare(Thread(root.name, frozenset(), str(root))) + for index in range(24): + comms.registry.declare(Thread(f"worker-{index:02}", frozenset(), str(root))) + app = FrameApp(project_dir=str(root)) + try: + async with app.run_test(size=(120, 42)) as pilot: + for _ in range(9): + await app.session_navigation.new(app.session_navigation.default_source) + responses = [AgentResponse(f"## Response {index}\n\n" + "paragraph\n\n" * 10) + for index in range(12)] + await app.selected_session.conversation.contents.mount(*responses) + await pilot.pause() + results = {} + for phase in ("loaded", "after_authored_response_burst"): + if phase == "after_authored_response_burst": + burst_start = perf_counter() + for index, response in enumerate(responses[-3:]): + await response.append(f"\n\nAuthored geometry continuation {index}.\n") + await pilot.pause() + burst_seconds = perf_counter() - burst_start + assert all("Authored geometry continuation" in response.source + for response in responses[-3:]) + widgets = tuple(app.screen.walk_children()) + geometry = tuple(widget.region for widget in widgets) + Widget._has_relative_children_height = original + expected = tuple(widget._has_relative_children_height for widget in widgets) + timings = {"original": [], "retained": []} + profile_counts = {} + for trial in range(6): + for name, declaration in (("original", original), ("retained", current))[::1 if trial % 2 == 0 else -1]: + Widget._has_relative_children_height = declaration + profile = cProfile.Profile() if trial == 0 else None + if profile: + profile.enable() + start = perf_counter() + for _ in range(20): + assert tuple(widget._has_relative_children_height for widget in widgets) == expected + elapsed = perf_counter() - start + if profile: + profile.disable() + profile.dump_stats(str(output / f"{phase}-{name}.pstats")) + profile_counts[name] = { + function: sum(value[1] for (_, _, entry), value in pstats.Stats(profile).stats.items() + if entry == function) + for function in ("_has_relative_children_height", "_relative_children_height", "is_container") + } + else: + timings[name].append(elapsed) + Widget._has_relative_children_height = current + assert tuple(widget.region for widget in widgets) == geometry + results[phase] = {"widgets": len(widgets), "seconds": timings, + "median_seconds": {name: statistics.median(values) for name, values in timings.items()}, + "profile_actual_calls": profile_counts, + "answers_equal": True, "geometry_unchanged_during_queries": True} + assert app._exception is None + result = {"scope": "source App height queries and authored response geometry, not live frame latency", + "tabs": 10, "sweeps": 20, "phases": results, + "authored_burst_to_idle_seconds": burst_seconds, + "inputs": 0, "providers": 0} + (output / "result.json").write_text(json.dumps(result, indent=2) + "\n") + print(json.dumps(result, indent=2), flush=True) + finally: + Widget._has_relative_children_height = current + + +if __name__ == "__main__": + import sys + asyncio.run(main(Path(sys.argv[1]))) diff --git a/tests/test_relative_height_resource.py b/tests/test_relative_height_resource.py new file mode 100644 index 0000000000..5a23d7b9cb --- /dev/null +++ b/tests/test_relative_height_resource.py @@ -0,0 +1,138 @@ +"""Native source epochs and opaque getters have distinct height lifetimes.""" + +from fractions import Fraction + +from textual.app import App +from textual.containers import VerticalGroup +from textual.geometry import Size +from textual.widgets import Static + + +async def test_native_relative_source_changes_before_idle_and_reparent(): + app = App() + async with app.run_test() as pilot: + leaf = Static("relative") + leaf.styles.height = "1fr" + inner = VerticalGroup(leaf) + first, second = VerticalGroup(inner), VerticalGroup() + await app.mount(first, second) + await pilot.pause() + assert first._has_relative_children_height + assert not second._has_relative_children_height + resource = first._height_dependency_answers() + first.refresh(repaint=True) + assert first._height_dependency_answers() is resource + assert first._has_relative_children_height + + for visible in (False, True): + leaf.set_display_constraint("relative-source", visible) + assert first._has_relative_children_height is visible + leaf.styles.height = 3 + assert not first._has_relative_children_height + leaf.styles.height = "1fr" + assert first._has_relative_children_height + inner.reparent(second) + assert not first._has_relative_children_height + assert second._has_relative_children_height + await pilot.pause() + assert second._get_box_model(Size(40, 60), app.size, Fraction(40), Fraction(60)).height == 60 + removal = leaf.remove() + # Pruning owns immediate display retirement, before native unmount. + assert not second._has_relative_children_height + await removal + + +async def test_opaque_display_and_container_getters_do_not_retire_with_native_epochs(): + class LiveDisplay(Static): + show = True + + @property + def display(self): + return self.show and super().display + + class LiveContainer(VerticalGroup): + show_container = True + + @property + def is_container(self): + return self.show_container and super().is_container + + app = App() + async with app.run_test() as pilot: + display = LiveDisplay("live") + display.styles.height = "1fr" + leaf = Static("leaf") + leaf.styles.height = "1fr" + container = LiveContainer(leaf) + display_parent, container_parent = VerticalGroup(display), VerticalGroup(container) + await app.mount(display_parent, container_parent) + await pilot.pause() + for enabled in (True, False, True, False): + display.show = enabled + container.show_container = enabled + assert display_parent._has_relative_children_height is enabled + assert container_parent._has_relative_children_height is enabled + assert "relative_children" not in display_parent._height_dependency_answers() + assert "relative_children" not in container_parent._height_dependency_answers() + + +async def test_opaque_relative_getter_remains_live_even_when_it_calls_native_super(): + class LiveRelative(VerticalGroup): + relative = False + + @property + def _has_relative_children_height(self): + return self.relative or super()._has_relative_children_height + + app = App() + async with app.run_test() as pilot: + child = LiveRelative(Static("fixed")) + parent = VerticalGroup(child) + await app.mount(parent) + await pilot.pause() + for relative in (False, True, False): + child.relative = relative + assert parent._has_relative_children_height is relative + assert "relative_children" not in parent._height_dependency_answers() + + +async def test_direct_pump_retirement_publishes_display_before_detachment(): + app = App() + async with app.run_test() as pilot: + child = Static("relative") + child.styles.height = "1fr" + parent = VerticalGroup(child) + await app.mount(parent) + await pilot.pause() + assert parent._has_relative_children_height + assert child in parent.displayed_children + await child._close_messages(wait=False) + assert not child.display + assert not parent._has_relative_children_height + assert child not in parent.displayed_children + await pilot.pause() + + +async def test_custom_child_selection_has_its_own_live_lifetime(): + class LiveChildren(VerticalGroup): + include_children = True + + @property + def children(self): + return super().children if self.include_children else () + + app = App() + async with app.run_test() as pilot: + leaf = Static("relative") + leaf.styles.height = "1fr" + child = LiveChildren(leaf) + parent = VerticalGroup(child) + await app.mount(parent) + await pilot.pause() + try: + for include in (True, False, True): + child.include_children = include + assert parent._has_relative_children_height is include + assert "relative_children" not in parent._height_dependency_answers() + finally: + child.include_children = True