mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-08-04 14:46:03 +00:00
Merge branch 'fix/text-cache-gc' of https://github.com/Sinity/kitty
Some checks are pending
CI / Linux (python=3.14 cc=clang sanitize=1) (push) Waiting to run
CI / Linux (python=3.12 cc=gcc sanitize=0) (push) Waiting to run
CI / Linux (python=3.13 cc=gcc sanitize=1) (push) Waiting to run
CI / Linux package (push) Waiting to run
CI / Bundle test (macos-latest) (push) Waiting to run
CI / Bundle test (ubuntu-latest) (push) Waiting to run
CI / macOS Brew (push) Waiting to run
CI / Test ./dev.sh and benchmark (push) Waiting to run
CodeQL / CodeQL-Build (actions, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, macos-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (go, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (python, ubuntu-latest) (push) Waiting to run
Depscan / Scan dependencies for vulnerabilities (push) Waiting to run
Some checks are pending
CI / Linux (python=3.14 cc=clang sanitize=1) (push) Waiting to run
CI / Linux (python=3.12 cc=gcc sanitize=0) (push) Waiting to run
CI / Linux (python=3.13 cc=gcc sanitize=1) (push) Waiting to run
CI / Linux package (push) Waiting to run
CI / Bundle test (macos-latest) (push) Waiting to run
CI / Bundle test (ubuntu-latest) (push) Waiting to run
CI / macOS Brew (push) Waiting to run
CI / Test ./dev.sh and benchmark (push) Waiting to run
CodeQL / CodeQL-Build (actions, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, macos-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (go, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (python, ubuntu-latest) (push) Waiting to run
Depscan / Scan dependencies for vulnerabilities (push) Waiting to run
This commit is contained in:
commit
7ab90de166
6 changed files with 189 additions and 2 deletions
|
|
@ -905,6 +905,45 @@ set_active_hyperlink(Screen *self, char *id, char *url) {
|
|||
}
|
||||
}
|
||||
|
||||
static void
|
||||
text_cache_gc_process_cells(TextCache *tc, TextCacheGCData *gc, CPUCell *cells, size_t count) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
CPUCell *c = cells + i;
|
||||
if (!c->ch_is_idx) continue;
|
||||
char_type new_idx;
|
||||
if (tc_gc_map_index(tc, gc, c->ch_or_idx, &new_idx)) c->ch_or_idx = new_idx;
|
||||
else cell_set_char(c, 0); // stale index, should not happen
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
text_cache_gc_process_linebuf(TextCache *tc, TextCacheGCData *gc, LineBuf *lb) {
|
||||
if (lb) text_cache_gc_process_cells(tc, gc, lb->cpu_cell_buf, (size_t)lb->ynum * lb->xnum);
|
||||
}
|
||||
|
||||
void
|
||||
screen_garbage_collect_text_cache(Screen *self) {
|
||||
// The TextCache interns unique cell texts forever; remap every live cell
|
||||
// index onto a fresh cache so entries that scrolled out of the history
|
||||
// buffer are freed. Mirrors screen_garbage_collect_hyperlink_pool().
|
||||
TextCacheGCData *gc = tc_gc_begin(self->text_cache);
|
||||
if (!gc) return; // allocation failure, cache left unchanged
|
||||
if (self->historybuf->count) {
|
||||
for (index_type y = self->historybuf->count; y-- > 0;) {
|
||||
CPUCell *cells = historybuf_cpu_cells(self->historybuf, y);
|
||||
text_cache_gc_process_cells(self->text_cache, gc, cells, self->historybuf->xnum);
|
||||
}
|
||||
}
|
||||
text_cache_gc_process_linebuf(self->text_cache, gc, self->main_linebuf);
|
||||
text_cache_gc_process_linebuf(self->text_cache, gc, self->alt_linebuf);
|
||||
text_cache_gc_process_linebuf(self->text_cache, gc, self->paused_rendering.linebuf);
|
||||
if (self->overlay_line.cpu_cells) text_cache_gc_process_cells(
|
||||
self->text_cache, gc, self->overlay_line.cpu_cells, self->overlay_line.xnum);
|
||||
if (self->overlay_line.original_line.cpu_cells) text_cache_gc_process_cells(
|
||||
self->text_cache, gc, self->overlay_line.original_line.cpu_cells, self->overlay_line.xnum);
|
||||
tc_gc_end(self->text_cache, gc);
|
||||
}
|
||||
|
||||
static bool
|
||||
add_combining_char(Screen *self, char_type ch, index_type x, index_type y) {
|
||||
CPUCell *cpu_cells = linebuf_cpu_cells_for_line(self->linebuf, y);
|
||||
|
|
@ -1212,6 +1251,7 @@ draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_
|
|||
}
|
||||
|
||||
#define PREPARE_FOR_DRAW_TEXT \
|
||||
if (tc_should_gc(self->text_cache)) screen_garbage_collect_text_cache(self); \
|
||||
const bool force_underline = OPT(underline_hyperlinks) == UNDERLINE_ALWAYS && self->active_hyperlink_id != 0; \
|
||||
CellAttrs attrs = cursor_to_attrs(self->cursor); \
|
||||
if (force_underline) attrs.decoration = OPT(url_style); \
|
||||
|
|
@ -4454,6 +4494,12 @@ update_overlay_line_data(Screen *self, uint8_t *data) {
|
|||
#define WRAP2B(name) static PyObject* name(Screen *self, PyObject *args) { unsigned int a, b; int p; if(!PyArg_ParseTuple(args, "IIp", &a, &b, &p)) return NULL; screen_##name(self, a, b, (bool)p); Py_RETURN_NONE; }
|
||||
|
||||
WRAP0(garbage_collect_hyperlink_pool)
|
||||
WRAP0(garbage_collect_text_cache)
|
||||
|
||||
static PyObject*
|
||||
text_cache_count(Screen *self, PyObject *a UNUSED) {
|
||||
return PyLong_FromUnsignedLong((unsigned long)tc_num_entries(self->text_cache));
|
||||
}
|
||||
|
||||
static PyObject*
|
||||
has_selection(Screen *self, PyObject *a UNUSED) {
|
||||
|
|
@ -6267,6 +6313,8 @@ static PyMethodDef methods[] = {
|
|||
MND(scroll_until_cursor_prompt, METH_VARARGS)
|
||||
MND(hyperlinks_as_set, METH_NOARGS)
|
||||
MND(garbage_collect_hyperlink_pool, METH_NOARGS)
|
||||
MND(garbage_collect_text_cache, METH_NOARGS)
|
||||
MND(text_cache_count, METH_NOARGS)
|
||||
MND(hyperlink_for_id, METH_O)
|
||||
MND(reverse_scroll, METH_VARARGS)
|
||||
MND(scroll_prompt_to_bottom, METH_NOARGS)
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ typedef struct {
|
|||
#define render_lines_for_screen(screen) (screen->lines + pixel_scroll_enabled(screen))
|
||||
|
||||
void screen_align(Screen*);
|
||||
void screen_garbage_collect_text_cache(Screen *screen);
|
||||
void screen_restore_cursor(Screen *);
|
||||
void screen_save_cursor(Screen *);
|
||||
void screen_restore_modes(Screen *);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ typedef struct TextCache {
|
|||
chars_map map;
|
||||
unsigned refcnt;
|
||||
CharsMonotonicArena arena;
|
||||
unsigned adds_since_last_gc;
|
||||
} TextCache;
|
||||
static uint64_t hash_chars(Chars k) { return vt_hash_bytes(k.chars, sizeof(k.chars[0]) * k.count); }
|
||||
static bool cmpr_chars(Chars a, Chars b) { return a.count == b.count && memcmp(a.chars, b.chars, sizeof(a.chars[0]) * a.count) == 0; }
|
||||
|
|
@ -144,6 +145,66 @@ char_type
|
|||
tc_get_or_insert_chars(TextCache *self, const ListOfChars *chars) {
|
||||
Chars key = {.count=chars->count, .chars=chars->chars};
|
||||
chars_map_itr i = vt_get(&self->map, key);
|
||||
if (vt_is_end(i)) return copy_and_insert(self, key);
|
||||
if (vt_is_end(i)) { self->adds_since_last_gc++; return copy_and_insert(self, key); }
|
||||
return i.data->val;
|
||||
}
|
||||
|
||||
char_type
|
||||
tc_num_entries(const TextCache *self) { return self->array.count; }
|
||||
|
||||
// Interned cell texts are referenced from cells by index, so entries cannot
|
||||
// be evicted individually. Instead, periodically garbage collect: the owner
|
||||
// of all index-holding cells (Screen) calls tc_gc_begin(), remaps every live
|
||||
// cell index via tc_gc_map_index() -- which re-interns just the entries that
|
||||
// are still referenced -- and finishes with tc_gc_end(). Entries no longer
|
||||
// referenced by any cell (typically unique texts that have scrolled out of
|
||||
// the history buffer) are freed. Without this, a stream of unique
|
||||
// multi-codepoint cells (for example random combining marks) grows the cache
|
||||
// without bound for the lifetime of the window.
|
||||
#define TEXT_CACHE_ADDS_BETWEEN_GCS 8192u
|
||||
|
||||
bool
|
||||
tc_should_gc(const TextCache *self) { return self->adds_since_last_gc > TEXT_CACHE_ADDS_BETWEEN_GCS; }
|
||||
|
||||
struct TextCacheGCData {
|
||||
Chars *old_items; char_type old_count;
|
||||
CharsMonotonicArena old_arena;
|
||||
// old index -> new index + 1, 0 means not yet remapped
|
||||
char_type *map;
|
||||
};
|
||||
|
||||
TextCacheGCData*
|
||||
tc_gc_begin(TextCache *self) {
|
||||
TextCacheGCData *gc = calloc(1, sizeof(TextCacheGCData));
|
||||
if (!gc) return NULL;
|
||||
gc->map = calloc(MAX(1u, (size_t)self->array.count), sizeof(gc->map[0]));
|
||||
Chars *fresh = malloc(256 * sizeof(self->array.items[0]));
|
||||
if (!gc->map || !fresh) { free(gc->map); free(fresh); free(gc); return NULL; }
|
||||
gc->old_items = self->array.items; gc->old_count = self->array.count;
|
||||
gc->old_arena = self->arena;
|
||||
self->array.items = fresh; self->array.capacity = 256; self->array.count = 0;
|
||||
zero_at_ptr(&self->arena);
|
||||
vt_cleanup(&self->map); vt_init(&self->map);
|
||||
return gc;
|
||||
}
|
||||
|
||||
bool
|
||||
tc_gc_map_index(TextCache *self, TextCacheGCData *gc, char_type old_idx, char_type *new_idx) {
|
||||
if (old_idx >= gc->old_count) return false;
|
||||
if (!gc->map[old_idx]) {
|
||||
Chars key = gc->old_items[old_idx];
|
||||
chars_map_itr i = vt_get(&self->map, key);
|
||||
char_type nidx = vt_is_end(i) ? copy_and_insert(self, key) : i.data->val;
|
||||
gc->map[old_idx] = nidx + 1;
|
||||
}
|
||||
*new_idx = gc->map[old_idx] - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
tc_gc_end(TextCache *self, TextCacheGCData *gc) {
|
||||
self->adds_since_last_gc = 0;
|
||||
free(gc->map); free(gc->old_items);
|
||||
Chars_free_all(&gc->old_arena);
|
||||
free(gc);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,18 @@ TextCache* tc_decref(TextCache *self);
|
|||
void tc_chars_at_index(const TextCache *self, char_type idx, ListOfChars *ans);
|
||||
unsigned tc_chars_at_index_ansi(const TextCache *self, char_type idx, ANSIBuf *output);
|
||||
char_type tc_get_or_insert_chars(TextCache *self, const ListOfChars *chars);
|
||||
char_type tc_num_entries(const TextCache *self);
|
||||
|
||||
// Garbage collection: TextCache interns unique cell texts forever, so a
|
||||
// stream of unique multi-codepoint cells grows it without bound. The GC
|
||||
// mirrors the hyperlink pool design: steal the current entries, then have
|
||||
// the owner (Screen) remap every live cell index via tc_gc_map_index(),
|
||||
// which re-interns only referenced entries into the fresh cache.
|
||||
bool tc_should_gc(const TextCache *self);
|
||||
typedef struct TextCacheGCData TextCacheGCData;
|
||||
TextCacheGCData* tc_gc_begin(TextCache *self);
|
||||
bool tc_gc_map_index(TextCache *self, TextCacheGCData *gc, char_type old_idx, char_type *new_idx);
|
||||
void tc_gc_end(TextCache *self, TextCacheGCData *gc);
|
||||
char_type tc_first_char_at_index(const TextCache *self, char_type idx);
|
||||
char_type tc_last_char_at_index(const TextCache *self, char_type idx);
|
||||
bool tc_chars_at_index_without_alloc(const TextCache *self, char_type idx, ListOfChars *ans);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -150,6 +150,27 @@ func unicode() (r result, err error) {
|
|||
return result{desc, data_sz, duration, reps}, nil
|
||||
}
|
||||
|
||||
func unique_unicode() (r result, err error) {
|
||||
const cell_count = 256 * 1024
|
||||
const combining_count = 0x70
|
||||
var data strings.Builder
|
||||
data.Grow(cell_count * 10)
|
||||
for i := range cell_count {
|
||||
q := i
|
||||
data.WriteByte('a')
|
||||
for range 3 {
|
||||
data.WriteRune(rune(0x300 + q%combining_count))
|
||||
q /= combining_count
|
||||
}
|
||||
}
|
||||
const desc = "Unique multi-codepoint Unicode cells"
|
||||
duration, data_sz, reps, err := benchmark_data(desc, data.String(), opts)
|
||||
if err != nil {
|
||||
return result{}, err
|
||||
}
|
||||
return result{desc, data_sz, duration, reps}, nil
|
||||
}
|
||||
|
||||
func ascii_with_csi() (r result, err error) {
|
||||
const sz = 1024*1024 + 17
|
||||
out := make([]byte, 0, sz+48)
|
||||
|
|
@ -244,7 +265,7 @@ func present_result(r result, col_width int) {
|
|||
|
||||
func all_benchamrks() []string {
|
||||
return []string{
|
||||
"ascii", "unicode", "csi", "images", "long_escape_codes",
|
||||
"ascii", "unicode", "unique_unicode", "csi", "images", "long_escape_codes",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,6 +297,13 @@ func main(args []string) (err error) {
|
|||
results = append(results, r)
|
||||
}
|
||||
|
||||
if slices.Index(args, "unique_unicode") >= 0 {
|
||||
if r, err = unique_unicode(); err != nil {
|
||||
return err
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
if slices.Index(args, "csi") >= 0 {
|
||||
if r, err = ascii_with_csi(); err != nil {
|
||||
return err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue