Skip to content

DataTable: _row_render_cache / _line_cache keys include the absolute cursor coordinate, so rows the cursor isn't on miss the cache after every cursor move #6736

Open

Description

@clembie

This was discovered while trying to understand performance issues related to an application that uses DataTable to display and edit large grids of data. tl;dr — a real bug, but not one whose fix brings massive performance improvement. :/

Textual version: 8.2.8 (Python 3.12, Linux)

Summary

_render_line_in_row and _render_line (_data_table.py ~2272 and ~2437) put
the full cursor and hover Coordinate in their cache keys. The render body only
uses them through should_highlight(...), which reduces them to a per-cell
boolean, as _cell_render_cache (~2124) already does in its key. With
cursor_type="cell", rows the cursor isn't on render identically wherever the
cursor is, yet their keys change on every move, so those entries are never hit
again.

Effect

Each cursor move adds _row_render_cache and _line_cache entries that are
never reused. In testing, we used (500×24 table, 200×50 viewport) that was about 11 per
move. Both caches are capped at 1,000, so they churn constantly.

The speed impact is small: a fix measured 1.0–1.2×, because dirty-line tracking
only re-requests about 2 of 50 lines per move. So, reporting it as a cheap
efficiency fix, not a major bottleneck.

Suggested fix

Reduce each coordinate to what the body reads before it goes into the key:

  • "cell": the coordinate if coord.row == row_index, otherwise a sentinel
    (not -1, which the header row uses, obvi)
  • "row": Coordinate(row_index, 0) on a match, otherwise the sentinel
  • "column": .column only
  • "none": the sentinel

We implemented this as a mixin around _render_line_in_row. _render_line reads
self.cursor_coordinate directly, so it needs the same reduction inline. Works across
all four cursor types.

Related Issue (sort of)

#6665: a different cache with the same pattern, a key holding more than the
result depends on (there it causes retention in memory, here churn).

Reproduction

A cursor_type="cell" DataTable with 500×24 rows at 200×50. Press right 50
times and log len(table._line_cache) and len(table._row_render_cache): both
grow on every move, though only the cursor's row looks different.

See attached ideas / workarounds.

cursor_cache_fix.py

# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 8.2.8  |
| Rich    | 14.2.0 |

## Python

| Name           | Value                                                                        |
|----------------|------------------------------------------------------------------------------|
| Version        | 3.12.7                                                                       |
| Implementation | CPython                                                                      |
| Compiler       | GCC 11.4.0                                                                   |
| Executable     | /home/mongrel/.cache/pypoetry/virtualenvs/thegrid-J5c0Cy1W-py3.12/bin/python |

## Operating System

| Name    | Value                                                            |
|---------|------------------------------------------------------------------|
| System  | Linux                                                            |
| Release | 7.1.5-76070105-generic                                           |
| Version | #202607241434~1787955023~24.04~38a419f SMP PREEMPT_DYNAMIC Fri A |

## Terminal

| Name                 | Value           |
|----------------------|-----------------|
| Terminal Application | ghostty (1.3.1) |
| TERM                 | xterm-ghostty   |
| COLORTERM            | truecolor       |
| FORCE_COLOR          | *Not set*       |
| NO_COLOR             | *Not set*       |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=167, height=62 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 167                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 62                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |

Activity

  1. github-actions commented on Oct 13, 2026

    @github-actions

    We found the following entries in the FAQ which you may find helpful:

    Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

    This project is developed and maintained by Will McGugan. Consider sponsoring Will's work on this project (and others).

    This is an automated reply, generated by FAQtory

  2. kenmclennan commented on Oct 14, 2026

    @kenmclennan

    Another instance of the same pattern, in _cell_render_cache (8.2.8, _data_table.py ~2124).

    The cell cache key includes self._update_count, a table-wide counter that update_cell() increments on every call (~906), including when the new value equals the old one. So a single update_cell() anywhere in the table changes the key of every cell, and the LRUCache(10000) fills with entries that can never be hit again.

    We hit this in a dashboard that refreshes a DataTable once a second via update_cell(). The cell cache grew by about 6 entries per refresh until the LRU saturated, settling around 100MB RSS. We've since changed our app to skip update_cell() when the value is unchanged, which avoids most of it.

    Two cheap options upstream, either of which would help:

    • make update_cell() a no-op (no _update_count bump) when the new value equals the stored one;
    • key cells on a per-cell version rather than the table-wide count, so updating one cell does not invalidate the rest.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions