Skip to content

Bootstrap re-measures every cell: Textual's dimension pass is 74% of a 50k-row cold build #210

Description

@hellices

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):

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.

  1. 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.
  2. 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).

Correctness requirements

  • Absorbed widths must equal Textual's measurement for every cell shape we
    render: plain str, Text with styles, markup-bearing custom-column values
    (Custom columns per resource kind via config (labels/annotations/JSONPath) #45), embedded newlines, and full-width (CJK) characters.
  • 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions