mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-28 11:11:47 +02:00
Garbage collect the text cache periodically so unique cell texts do not grow memory without bound
The TextCache interns every unique multi-codepoint cell text for the lifetime of the window with no eviction, so a stream of unique texts (for example random combining mark sequences) grows memory without bound, several hundred MB/hour at moderate throughput. Mirror the existing hyperlink pool garbage collection: every 8192 newly interned entries, steal the cache contents and have the Screen remap the index in every live cell (history buffer, main and alt line buffers, paused rendering snapshot and overlay line), re-interning only entries still referenced by some cell. Entries whose last reference scrolled out of the history buffer are freed. See #10249 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1204,6 +1204,43 @@ class TestScreen(BaseTest):
|
||||
self.ae('2', s.hyperlink_at(1, 3))
|
||||
self.ae(s.current_url_text(), 'Z Z')
|
||||
|
||||
def test_text_cache_garbage_collection(self):
|
||||
# unique multi-codepoint cell texts, single width base + combining mark
|
||||
def unique_text(i):
|
||||
return chr(0x100 + i // 0x70) + chr(0x300 + i % 0x70)
|
||||
|
||||
s = self.create_screen()
|
||||
base = s.text_cache_count()
|
||||
for i in range(10):
|
||||
s.draw(unique_text(i))
|
||||
self.ae(s.text_cache_count(), base + 10)
|
||||
before = tuple(str(s.line(y)) for y in range(s.lines))
|
||||
s.garbage_collect_text_cache()
|
||||
# all entries are still referenced by cells, so all survive
|
||||
self.ae(s.text_cache_count(), base + 10)
|
||||
self.ae(before, tuple(str(s.line(y)) for y in range(s.lines)))
|
||||
|
||||
# scroll all multi-codepoint cells out of the screen and the history
|
||||
# buffer, then intern one more entry, which gets a high index
|
||||
for i in range(s.lines * 3):
|
||||
s.linefeed()
|
||||
s.carriage_return()
|
||||
s.draw(unique_text(10))
|
||||
s.garbage_collect_text_cache()
|
||||
# only the surviving entry remains and its index was remapped
|
||||
# without changing the cell's text
|
||||
self.ae(s.text_cache_count(), base + 1)
|
||||
self.ae(str(s.line(s.cursor.y)).rstrip(), unique_text(10))
|
||||
|
||||
# the periodic GC keeps the cache bounded when unique cell texts
|
||||
# are continuously created and scrolled out, as in the DoS scenario
|
||||
s = self.create_screen()
|
||||
num = 3 * 8192 + 100
|
||||
for i in range(num):
|
||||
s.draw(unique_text(i))
|
||||
self.assertLess(s.text_cache_count(), 8192 + 2 * s.lines * s.columns)
|
||||
self.ae(str(s.line(s.cursor.y)).rstrip()[-2:], unique_text(num - 1))
|
||||
|
||||
def test_bottom_margin(self):
|
||||
s = self.create_screen(cols=80, lines=6, scrollback=4)
|
||||
s.set_margins(0, 5)
|
||||
|
||||
Reference in New Issue
Block a user