Repository navigation
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
Description
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 ifcoord.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":.columnonly"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.
# 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
github-actions commentedon Oct 13, 2026
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
kenmclennan commentedon Oct 14, 2026
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 thatupdate_cell()increments on every call (~906), including when the new value equals the old one. So a singleupdate_cell()anywhere in the table changes the key of every cell, and theLRUCache(10000)fills with entries that can never be hit again.We hit this in a dashboard that refreshes a
DataTableonce a second viaupdate_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 skipupdate_cell()when the value is unchanged, which avoids most of it.Two cheap options upstream, either of which would help:
update_cell()a no-op (no_update_countbump) when the new value equals the stored one;