You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After #208 a repaint costs only what changed, but the first build of a large
view still blocks the UI for seconds. Measured on this machine with 50,000 pods
(ResourceTable.show() on a mounted table, then letting Textual settle):
rows
cold show() + settle
1,000
~0.13 s
10,000
~0.93 s
50,000
~4.8 s
The same cost is paid again on every path that has to rebuild rather than diff:
switching kind, changing the sort, applying a custom view, and the reorder
fallback (a row inserted mid-table — add_row can only append).
Where the time actually goes
Splitting the 50k cold build by wall clock:
phase
time
show() itself (build cells + add_row loop)
~1.3 s
Textual settling afterwards
~3.5 s
So ~74% of the freeze is not our cell construction and not add_row — it is DataTable._update_dimensions(), which runs from on_idle over every newly
added row and, for each one, calls _get_row_renderables() (re-deriving the
cells through default_cell_formatter, allocating fresh Text objects) and
then rich.measure() on every cell to grow column.content_width.
At 50,000 rows × 14 columns that is 700,000 renderable constructions and
700,000 measure() calls — to compute fourteen integers we could have derived
while we were already holding the cells.
Proposal
ResourceTable already knows every cell it emitted (_emitted, added in #208)
and already has _cell_width() — a cheap width function written to match DataTable's own measurement, used today by the in-place diff.
After seeding rows, fold the emitted cells into column.content_width
directly, with a cheap "cannot grow this column" skip: for an ASCII cell,
the display width can never exceed len(), so len(raw) <= width and raw.isascii() settles the overwhelming majority of cells with two C-level
calls and no measurement at all.
Override _update_dimensions() to pass an empty row set to super() when
the widths were already absorbed. The superclass still recomputes virtual_size exactly as before — only the per-row measuring loop is
skipped. If a future Textual renames the hook, the override simply stops
being called and behaviour degrades to today's slow-but-correct path.
Prototype results (50,000 pods, same fixture)
cold show() + settle
today
5007 ms
absorbing widths
2391 ms
absorbing widths + ASCII skip
1645 ms
Resulting column.content_width values are identical to the ones Textual
computes today (verified against a control DataTable seeded with the same
cells).
Rows added outside show() must still be measured by the superclass.
Rows added by the in-place diff must have their widths absorbed too, so a
newly appeared long name still widens its column.
Rows are always height 1 (add_row's default) and never labelled, so the
auto-height and row-label branches of _update_dimensions are unreachable
from this widget; the fast path must not be taken if that ever changes.
Acceptance criteria
Bootstrapping a large view does not re-render and re-measure every cell.
Column widths are provably identical to the superclass result, including
CJK and markup cells.
Cold build of 50k rows drops by ~3x.
Not in scope
Batch row insertion that bypasses DataTable.add_row's per-row overhead
(worth ~0.3 s of the remaining 1.6 s, but reaches much deeper into Textual
internals).
Chunked seeding across event-loop turns, which would hide the remaining cost
rather than remove it. Worth revisiting only if the reduced freeze is still
unacceptable.
Problem
After #208 a repaint costs only what changed, but the first build of a large
view still blocks the UI for seconds. Measured on this machine with 50,000 pods
(
ResourceTable.show()on a mounted table, then letting Textual settle):show()+ settleThe same cost is paid again on every path that has to rebuild rather than diff:
switching kind, changing the sort, applying a custom view, and the reorder
fallback (a row inserted mid-table —
add_rowcan only append).Where the time actually goes
Splitting the 50k cold build by wall clock:
show()itself (build cells +add_rowloop)So ~74% of the freeze is not our cell construction and not
add_row— it isDataTable._update_dimensions(), which runs fromon_idleover every newlyadded row and, for each one, calls
_get_row_renderables()(re-deriving thecells through
default_cell_formatter, allocating freshTextobjects) andthen
rich.measure()on every cell to growcolumn.content_width.At 50,000 rows × 14 columns that is 700,000 renderable constructions and
700,000
measure()calls — to compute fourteen integers we could have derivedwhile we were already holding the cells.
Proposal
ResourceTablealready knows every cell it emitted (_emitted, added in #208)and already has
_cell_width()— a cheap width function written to matchDataTable's own measurement, used today by the in-place diff.column.content_widthdirectly, with a cheap "cannot grow this column" skip: for an ASCII cell,
the display width can never exceed
len(), solen(raw) <= width and raw.isascii()settles the overwhelming majority of cells with two C-levelcalls and no measurement at all.
_update_dimensions()to pass an empty row set tosuper()whenthe widths were already absorbed. The superclass still recomputes
virtual_sizeexactly as before — only the per-row measuring loop isskipped. If a future Textual renames the hook, the override simply stops
being called and behaviour degrades to today's slow-but-correct path.
Prototype results (50,000 pods, same fixture)
show()+ settleResulting
column.content_widthvalues are identical to the ones Textualcomputes today (verified against a control
DataTableseeded with the samecells).
Correctness requirements
render: plain
str,Textwith styles, markup-bearing custom-column values(Custom columns per resource kind via config (labels/annotations/JSONPath) #45), embedded newlines, and full-width (CJK) characters.
show()must still be measured by the superclass.newly appeared long name still widens its column.
add_row's default) and never labelled, so theauto-height and row-label branches of
_update_dimensionsare unreachablefrom this widget; the fast path must not be taken if that ever changes.
Acceptance criteria
CJK and markup cells.
Not in scope
DataTable.add_row's per-row overhead(worth ~0.3 s of the remaining 1.6 s, but reaches much deeper into Textual
internals).
rather than remove it. Worth revisiting only if the reduced freeze is still
unacceptable.