Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions evidence/native-damage-band-reduction-20261005/SCOPE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Reduce original damage intervals once per vertical band

Base: merged Text68 dd625069e50cc0b909832b549a485abc55067149.

The existing `Compositor._regions_to_spans` expands every damaged rectangle
into one interval per row, then sorts and merges the same active intervals
again on each row. Output rows are necessary; repeated interval acquisition
and union inside a band without rectangle edges is unnecessary work.

Keep that original reducer and its exact ordered output. Use rectangle start
and end edges to reduce active horizontal intervals once per changing vertical
band, then emit those original spans on each required row. Preserve overlap
multiplicity, touching intervals, gaps, negative origins and complete bounds.
No new retained resource, cache, map, timer, flag, type, API or Toad caller.

The complete Package AST before/after from Text68 is retained as caller
source evidence: native249/tests460 and Toad288/tests397/tools41 with0parse
omissions. The current reducer has four native callers and seven test callers;
all render paths continue to use the same owner. Dynamic private-method
rebinding and unregistered external callers remain unproved.

Source reasoning and coherent implementation first; final proportionate
interval and affected native App controls last. Native68 original14+2 controls
and negative are immutable, not rerun. No build/installed prefix/SDK/provider/
media purpose; no measured gain or physical cadence claim.

## Published algorithm and caller closure

Production83ea6b17eab3ac0a917451070a954b7299a1a91a, one file23+/23-.
The original reducer now consumes each input rectangle once into its vertical
start/end edge deltas. Counter preserves original overlapping interval
multiplicity; intervals are reduced only when that active set changes at a
vertical edge. Each required output row still receives the original ordered
spans. Empty gaps are skipped and no source rectangle is expanded into a
per-row temporary range list. Counter.elements preserves original interval
multiplicity before merging; no retained state survives the generator.

`owner-consumers.json`: actual source48f79749ef7521ff727cbc7be0feba1794e99c6a,
production249/tests460, parse omissions0, one reducer declaration and four
native consumers. Original Text68 pinned Toad dependency census is retained;
all source signatures and related consumers remain unchanged here. Existing
full, partial, inline/export and original-body capture paths use this reducer.
One source fact remains one owner; no separate fast path or uncached reducer.

## Final pure source controls

ONE batch8PASS/0.18s, controller1.3053654759423807s, exit0. Source48f79749e,
production83ea6b17e. Exact logSHA
`c513a336404899b0e4e59540faac60f730d81e38e236b1b942334605c26575ed`.
Existing empty/single/partial/full overlap, different-row and same-row gaps,
and adjacent intervals passed. The added whole-contract case preserves
different rectangle lifetimes with equal horizontal spans, negative origins,
one-shot input, distant empty gaps, nonpositive height and original
zero-width span output. No App, installed prefix, SDK/native process, build,
provider, media or accepted Native68 controls were run.

Draft / pure source-qualified only. No installed frame/materialization or
physical scrolling result is inferred. Full observed source/paint workflow
acceptance belongs to the next meaningful joined Heis cohort; issued46067 is
unchanged. CPU time and cadence are unmeasured. Band reduction still sorts
active intervals at each real source edge; it does not remove legitimate
output rows or promise a universally faster result for every damage shape.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"base": "dd625069e50cc0b909832b549a485abc55067149",
"source": "48f79749ef7521ff727cbc7be0feba1794e99c6a",
"before_dependency_evidence": "evidence/native-damage-span-admission-20261005/before.json",
"before_after_native_evidence": "evidence/native-damage-span-admission-20261005/after.json",
"roots": [
{
"root": "src/textual",
"parsed": 249,
"omitted": []
},
{
"root": "tests",
"parsed": 460,
"omitted": []
}
],
"sites": [
{
"path": "src/textual/_compositor.py",
"line": 628,
"node": "FunctionDef",
"source": " def _regions_to_spans("
},
{
"path": "src/textual/_compositor.py",
"line": 1559,
"node": "Attribute",
"source": "self._regions_to_spans"
},
{
"path": "src/textual/_compositor.py",
"line": 1588,
"node": "Attribute",
"source": "self._regions_to_spans"
},
{
"path": "src/textual/_compositor.py",
"line": 1607,
"node": "Attribute",
"source": "self._regions_to_spans"
},
{
"path": "src/textual/_compositor.py",
"line": 1647,
"node": "Attribute",
"source": "self._regions_to_spans"
},
{
"path": "tests/test_compositor_regions_to_spans.py",
"line": 6,
"node": "Attribute",
"source": "Compositor._regions_to_spans"
},
{
"path": "tests/test_compositor_regions_to_spans.py",
"line": 11,
"node": "Attribute",
"source": "Compositor._regions_to_spans"
},
{
"path": "tests/test_compositor_regions_to_spans.py",
"line": 19,
"node": "Attribute",
"source": "Compositor._regions_to_spans"
},
{
"path": "tests/test_compositor_regions_to_spans.py",
"line": 28,
"node": "Attribute",
"source": "Compositor._regions_to_spans"
},
{
"path": "tests/test_compositor_regions_to_spans.py",
"line": 37,
"node": "Attribute",
"source": "Compositor._regions_to_spans"
},
{
"path": "tests/test_compositor_regions_to_spans.py",
"line": 42,
"node": "Attribute",
"source": "Compositor._regions_to_spans"
},
{
"path": "tests/test_compositor_regions_to_spans.py",
"line": 51,
"node": "Attribute",
"source": "Compositor._regions_to_spans"
},
{
"path": "tests/test_compositor_regions_to_spans.py",
"line": 70,
"node": "Attribute",
"source": "Compositor._regions_to_spans"
}
],
"limits": "Native AST method/name/attribute ownership evidence; original pinned Toad production/tests/tools relation retained. No dynamic rebinding or external private consumers inferred."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"source": "48f79749ef7521ff727cbc7be0feba1794e99c6a",
"production_source": "83ea6b17eab3ac0a917451070a954b7299a1a91a",
"argv": [
"/usr/bin/python",
"-m",
"pytest",
"-q",
"tests/test_compositor_regions_to_spans.py"
],
"seconds": 1.3053654759423807,
"exit_code": 0,
"log": "/home/ts/wt/textual-native-subtree-strips-20261002/.artifacts/native-damage-band-reduction-20261005/source-interval01.log",
"log_sha256": "c513a336404899b0e4e59540faac60f730d81e38e236b1b942334605c26575ed",
"scope": "ONE final pure native interval reducer batch after coherent implementation; no App/installed process/SDK/build/media/provider. Native68 App controls not repeated.",
"installed_qualified": false,
"retained_log": "evidence/native-damage-band-reduction-20261005/source-interval01.log"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
........ [100%]
8 passed in 0.18s
46 changes: 23 additions & 23 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dataclasses import dataclass
from abc import ABC, abstractmethod
from bisect import bisect_left, bisect_right
from collections import Counter
from contextlib import contextmanager
from functools import cached_property
from types import MappingProxyType
Expand Down Expand Up @@ -636,30 +637,29 @@ def _regions_to_spans(
Returns:
Yields tuples of (Y, X1, X2).
"""
inline_ranges: dict[int, list[tuple[int, int]]] = {}
setdefault = inline_ranges.setdefault
edges: dict[int, Counter[tuple[int, int]]] = {}
for region_x, region_y, width, height in regions:
span = (region_x, region_x + width)
for y in range(region_y, region_y + height):
setdefault(y, []).append(span)

slice_remaining = slice(1, None)
for y, ranges in sorted(inline_ranges.items()):
if len(ranges) == 1:
# Special case of 1 span
yield (y, *ranges[0])
else:
ranges.sort()
x1, x2 = ranges[0]
for next_x1, next_x2 in ranges[slice_remaining]:
if next_x1 <= x2:
if next_x2 > x2:
x2 = next_x2
else:
yield (y, x1, x2)
x1 = next_x1
x2 = next_x2
yield (y, x1, x2)
if height > 0:
span = (region_x, region_x + width)
edges.setdefault(region_y, Counter())[span] += 1
edges.setdefault(region_y + height, Counter())[span] -= 1

active: Counter[tuple[int, int]] = Counter()
spans: list[tuple[int, int]] = []
previous_y = 0
for y, changes in sorted(edges.items()):
if spans:
for row in range(previous_y, y):
for span in spans:
yield (row, *span)
active.update(changes)
spans = []
for x1, x2 in sorted(active.elements()):
if spans and x1 <= spans[-1][1]:
spans[-1] = (spans[-1][0], max(x2, spans[-1][1]))
else:
spans.append((x1, x2))
previous_y = y

def __rich_repr__(self) -> rich.repr.Result:
yield "size", self.size
Expand Down
24 changes: 24 additions & 0 deletions tests/test_compositor_regions_to_spans.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,27 @@ def test_regions_to_ranges_directly_adjacent_ranges_merged():
(0, 0, 2),
(1, 0, 2),
]


def test_damage_bands_keep_overlap_multiplicity_origins_and_empty_gaps():
regions = [
Region(-2, -3, 4, 4),
Region(-2, -1, 4, 4),
Region(2, -2, 2, 4),
Region(9, 0, 2, 2),
Region(3, 5, 1, 0),
Region(6, 7, 1, -1),
Region(100, 1000, 0, 1),
]
# Equal horizontal intervals are independently owned rectangles: one can
# end while the other still supplies damage. A one-shot input and distant
# band retain the original reducer's order and zero-width span contract.
assert list(Compositor._regions_to_spans(iter(regions))) == [
(-3, -2, 2),
(-2, -2, 4),
(-1, -2, 4),
(0, -2, 4), (0, 9, 11),
(1, -2, 4), (1, 9, 11),
(2, -2, 2),
(1000, 100, 100),
]
Loading