mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-20 20:22:10 +00:00
Throughput improvements when processing large blocks of text
Mostly comes from detecting the case of runs of ASCII printable characters and using a special fast path for them. Improves benchmarks as per benchmark.py by 15-30% for text heavy workloads
This commit is contained in:
parent
69025901b3
commit
e62ec6d540
5 changed files with 222 additions and 22 deletions
|
|
@ -22,9 +22,11 @@ from kitty.utils import read_screen_size
|
|||
BENCHMARK_WINDOW_ID = 1
|
||||
ALL_BENCHMARKS = ('ascii', 'unicode', 'unique_unicode', 'csi', 'images', 'long_escape_codes')
|
||||
|
||||
|
||||
def perf_output() -> str:
|
||||
return os.path.join(tempfile.gettempdir(), 'kitty-benchmark.perf')
|
||||
|
||||
|
||||
# Set by the re-exec wrapper so we don't recurse when --perf is in argv.
|
||||
_UNDER_PERF_ENV = '_KITTY_BENCHMARK_UNDER_PERF'
|
||||
|
||||
|
|
|
|||
|
|
@ -186,6 +186,8 @@ Detailed list of changes
|
|||
|
||||
- Improve throughput when processing large amounts of text with a scrollback buffer much larger than the CPU cache by ~35% by prefetching scrollback memory before it is written to
|
||||
|
||||
- Improve throughput when processing large amounts of text by ~50% for ASCII and ~15% for Unicode by writing runs of plain ASCII chars to the screen in batches and skipping unnecessary bookkeeping in the scrolling and tab handling hot paths
|
||||
|
||||
- The :opt:`scrollbar` option now takes a new value ``scrolled-or-hovered`` to also show the scrollbar when the mouse moves over the scrollbar region (:pull:`10345`)
|
||||
|
||||
- A new :code:`kitten @ screenshot` remote control command to take a pixel perfect PNG screenshot of an OS Window, tab or window
|
||||
|
|
|
|||
|
|
@ -439,18 +439,18 @@ index_selection(const Screen *self, Selections *selections, bool up, index_type
|
|||
}
|
||||
|
||||
|
||||
#define INDEX_DOWN \
|
||||
linebuf_reverse_index(self->linebuf, top, bottom); \
|
||||
linebuf_clear_line(self->linebuf, top, true); \
|
||||
if (self->linebuf == self->main_linebuf && self->last_visited_prompt.is_set) { \
|
||||
if (self->last_visited_prompt.scrolled_by > 0) self->last_visited_prompt.scrolled_by--; \
|
||||
else if (self->last_visited_prompt.y < self->lines - 1) self->last_visited_prompt.y++; \
|
||||
else self->last_visited_prompt.is_set = false; \
|
||||
} \
|
||||
INDEX_GRAPHICS(1) \
|
||||
self->is_dirty = true; \
|
||||
index_selection(self, &self->selections, false, top, bottom); \
|
||||
clear_selection(&self->url_ranges);
|
||||
#define INDEX_DOWN \
|
||||
linebuf_reverse_index(self->linebuf, top, bottom); \
|
||||
linebuf_clear_line(self->linebuf, top, true); \
|
||||
if (self->linebuf == self->main_linebuf && self->last_visited_prompt.is_set) { \
|
||||
if (self->last_visited_prompt.scrolled_by > 0) self->last_visited_prompt.scrolled_by--; \
|
||||
else if (self->last_visited_prompt.y < self->lines - 1) self->last_visited_prompt.y++; \
|
||||
else self->last_visited_prompt.is_set = false; \
|
||||
} \
|
||||
INDEX_GRAPHICS(1) \
|
||||
self->is_dirty = true; \
|
||||
if (UNLIKELY(self->selections.count)) index_selection(self, &self->selections, false, top, bottom); \
|
||||
if (UNLIKELY(self->url_ranges.count || self->url_ranges.in_progress)) clear_selection(&self->url_ranges);
|
||||
|
||||
|
||||
static void
|
||||
|
|
@ -844,8 +844,8 @@ selection_has_screen_line(const Selections *selections, const int y) {
|
|||
|
||||
static void
|
||||
clear_intersecting_selections(Screen *self, index_type y) {
|
||||
if (selection_has_screen_line(&self->selections, y)) clear_selection(&self->selections);
|
||||
if (selection_has_screen_line(&self->url_ranges, y)) clear_selection(&self->url_ranges);
|
||||
if (UNLIKELY(self->selections.count) && selection_has_screen_line(&self->selections, y)) clear_selection(&self->selections);
|
||||
if (UNLIKELY(self->url_ranges.count) && selection_has_screen_line(&self->url_ranges, y)) clear_selection(&self->url_ranges);
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -867,8 +867,13 @@ init_segmentation_state(Screen *self, text_loop_state *s) {
|
|||
init_prev_cell(self, s);
|
||||
grapheme_segmentation_reset(&s->seg);
|
||||
if (s->prev.cc) {
|
||||
text_in_cell(s->prev.cc, self->text_cache, self->lc);
|
||||
for (index_type i = 0; i < self->lc->count; i++) s->seg = grapheme_segmentation_step(s->seg, char_props_for(self->lc->chars[i]));
|
||||
if (LIKELY(!s->prev.cc->ch_is_idx)) {
|
||||
// single codepoint in cell, no need for the ListOfChars machinery
|
||||
s->seg = grapheme_segmentation_step(s->seg, char_props_for(s->prev.cc->ch_or_idx));
|
||||
} else {
|
||||
text_in_cell(s->prev.cc, self->text_cache, self->lc);
|
||||
for (index_type i = 0; i < self->lc->count; i++) s->seg = grapheme_segmentation_step(s->seg, char_props_for(self->lc->chars[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1064,6 +1069,8 @@ screen_garbage_collect_text_cache(Screen *self) {
|
|||
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);
|
||||
// the GC remaps all cache indices so the memoized tab indices are stale
|
||||
memset(&self->tab_cache, 0, sizeof(self->tab_cache));
|
||||
}
|
||||
|
||||
static bool
|
||||
|
|
@ -1304,6 +1311,35 @@ draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_
|
|||
draw_control_char(self, s, ch);
|
||||
continue;
|
||||
}
|
||||
if (LIKELY(!self->charset.current && !self->modes.mIRM && self->cursor->x < self->columns)) {
|
||||
// Batched fast path: write a run of printable ASCII chars into the
|
||||
// current line in one go, keeping all state in locals so it is not
|
||||
// reloaded for every char. Stops at the first char needing the
|
||||
// scalar path: non-ASCII, control char or an existing multicell cell.
|
||||
const size_t limit = MIN(num_chars - i, (size_t)(self->columns - self->cursor->x));
|
||||
CPUCell *cp = s->cp + self->cursor->x;
|
||||
size_t n = 0;
|
||||
while (n < limit && (chars[i + n] - 32u) < 95u && !cp[n].is_multicell) n++;
|
||||
if (n) {
|
||||
GPUCell *gp = s->gp + self->cursor->x;
|
||||
const CPUCell cc = s->cc;
|
||||
const GPUCell g = s->g;
|
||||
for (size_t j = 0; j < n; j++) {
|
||||
cp[j] = cc;
|
||||
cell_set_char(cp + j, chars[i + j]);
|
||||
}
|
||||
for (size_t j = 0; j < n; j++) gp[j] = g;
|
||||
self->last_graphic_char = chars[i + n - 1];
|
||||
s->prev.y = self->cursor->y;
|
||||
s->prev.x = self->cursor->x + n - 1;
|
||||
s->prev.cc = cp + n - 1;
|
||||
self->cursor->x += n;
|
||||
s->seg = (GraphemeSegmentationResult){.grapheme_break = GBP_None};
|
||||
i += n - 1;
|
||||
continue;
|
||||
}
|
||||
// n == 0: the cell under the cursor is a multicell, let the scalar path handle it
|
||||
}
|
||||
char_width = 1;
|
||||
s->seg = (GraphemeSegmentationResult){.grapheme_break = GBP_None};
|
||||
} else {
|
||||
|
|
@ -2262,10 +2298,17 @@ screen_tab(Screen *self) {
|
|||
CPUCell *c = cpu_cell + i;
|
||||
cell_set_char(c, ' ');
|
||||
}
|
||||
self->lc->count = 2;
|
||||
self->lc->chars[0] = '\t';
|
||||
self->lc->chars[1] = diff;
|
||||
cell_set_chars(cpu_cell, self->text_cache, self->lc);
|
||||
char_type idx;
|
||||
if (diff < arraysz(self->tab_cache.idx_plus_1) && self->tab_cache.idx_plus_1[diff]) idx = self->tab_cache.idx_plus_1[diff] - 1;
|
||||
else {
|
||||
self->lc->count = 2;
|
||||
self->lc->chars[0] = '\t';
|
||||
self->lc->chars[1] = diff;
|
||||
idx = tc_get_or_insert_chars(self->text_cache, self->lc);
|
||||
if (diff < arraysz(self->tab_cache.idx_plus_1)) self->tab_cache.idx_plus_1[diff] = idx + 1;
|
||||
}
|
||||
cpu_cell->ch_or_idx = idx;
|
||||
cpu_cell->ch_is_idx = true;
|
||||
}
|
||||
}
|
||||
self->cursor->x = found;
|
||||
|
|
@ -2397,8 +2440,8 @@ screen_cursor_to_column(Screen *self, unsigned int column) {
|
|||
} \
|
||||
linebuf_clear_line(self->linebuf, bottom, true); \
|
||||
self->is_dirty = true; \
|
||||
index_selection(self, &self->selections, true, top, bottom); \
|
||||
clear_selection(&self->url_ranges);
|
||||
if (UNLIKELY(self->selections.count)) index_selection(self, &self->selections, true, top, bottom); \
|
||||
if (UNLIKELY(self->url_ranges.count || self->url_ranges.in_progress)) clear_selection(&self->url_ranges);
|
||||
|
||||
void
|
||||
screen_index(Screen *self) {
|
||||
|
|
|
|||
|
|
@ -214,6 +214,12 @@ typedef struct {
|
|||
} paused_rendering;
|
||||
CharsetState charset;
|
||||
ListOfChars *lc;
|
||||
struct {
|
||||
// Memoized text cache indices for the ('\t', diff) text written by
|
||||
// screen_tab(), keyed by diff. Entries are idx + 1 with 0 meaning
|
||||
// unset. Must be cleared when the text cache is garbage collected.
|
||||
char_type idx_plus_1[16];
|
||||
} tab_cache;
|
||||
monotonic_t parsing_at;
|
||||
ExtraCursors extra_cursors;
|
||||
struct {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,153 @@ class TestScreen(BaseTest):
|
|||
self.ae(str(s.line(4)), 'ab123')
|
||||
self.ae((s.cursor.x, s.cursor.y), (2, 4))
|
||||
|
||||
def test_draw_batched_ascii_run(self):
|
||||
# The batched fast path for runs of printable ASCII in draw_text_loop
|
||||
# must behave identically to drawing char by char
|
||||
|
||||
# a run crossing multiple line wraps
|
||||
s = self.create_screen()
|
||||
s.draw('abcdefghijkl')
|
||||
self.ae(str(s.line(0)), 'abcde')
|
||||
self.ae(str(s.line(1)), 'fghij')
|
||||
self.ae(str(s.line(2)), 'kl')
|
||||
self.assertTrue(s.linebuf.is_continued(1))
|
||||
self.assertTrue(s.linebuf.is_continued(2))
|
||||
self.ae((s.cursor.x, s.cursor.y), (2, 2))
|
||||
|
||||
# without line-wrap the run must stop at the right margin and
|
||||
# overwrite the last column
|
||||
s = self.create_screen()
|
||||
s.reset_mode(DECAWM)
|
||||
s.draw('abcdefgh')
|
||||
self.ae(str(s.line(0)), 'abcdh')
|
||||
self.ae((s.cursor.x, s.cursor.y), (5, 0))
|
||||
|
||||
# insert mode must bypass the batch and insert per char
|
||||
s = self.create_screen()
|
||||
s.draw('12345')
|
||||
s.cursor_position(1, 1)
|
||||
s.set_mode(IRM)
|
||||
s.draw('abc')
|
||||
self.ae(str(s.line(0)), 'abc12')
|
||||
self.ae((s.cursor.x, s.cursor.y), (3, 0))
|
||||
|
||||
# charset translation must still be applied to every char of a run
|
||||
s = self.create_screen()
|
||||
parse_bytes(s, b'\x1b(0qqq\x1b(B')
|
||||
self.ae(str(s.line(0)), '─' * 3)
|
||||
|
||||
# overwriting the first cell of a wide char nukes the whole wide char
|
||||
s = self.create_screen()
|
||||
s.draw('ニチ')
|
||||
s.cursor_position(1, 1)
|
||||
s.draw('ab')
|
||||
self.ae(str(s.line(0)), 'abチ')
|
||||
# overwriting the second cell of a wide char replaces it with spaces
|
||||
s = self.create_screen()
|
||||
s.draw('ニチ')
|
||||
s.cursor_position(1, 2)
|
||||
s.draw('a')
|
||||
self.ae(str(s.line(0)), ' aチ')
|
||||
|
||||
# a run drawn over a multiline multicell char must be handled by the
|
||||
# scalar path
|
||||
s = self.create_screen()
|
||||
draw_multicell(s, 'X', scale=2)
|
||||
before_line1 = str(s.line(1))
|
||||
s.cursor_position(1, 1)
|
||||
s.draw('abc')
|
||||
self.ae(str(s.line(0)), 'abc')
|
||||
self.ae(str(s.line(1)), before_line1)
|
||||
# drawing on a lower row of a multiline char moves the cursor past it
|
||||
s = self.create_screen()
|
||||
draw_multicell(s, 'X', scale=2)
|
||||
s.cursor_position(2, 1)
|
||||
s.draw('ab')
|
||||
self.ae(str(s.line(0)), 'X')
|
||||
self.ae((s.cursor.x, s.cursor.y), (4, 1))
|
||||
|
||||
# REP must repeat the last char of a batched run
|
||||
s = self.create_screen()
|
||||
s.draw('abc')
|
||||
parse_bytes(s, b'\x1b[2b')
|
||||
self.ae(str(s.line(0)), 'abccc')
|
||||
|
||||
# a combining char following a run attaches to the run's last char,
|
||||
# also when the run ends exactly at the right margin
|
||||
s = self.create_screen()
|
||||
s.draw('ab́cd')
|
||||
self.ae(str(s.line(0)), 'ab́cd')
|
||||
self.ae((s.cursor.x, s.cursor.y), (4, 0))
|
||||
s = self.create_screen()
|
||||
s.draw('abcdé')
|
||||
self.ae(str(s.line(0)), 'abcdé')
|
||||
self.ae((s.cursor.x, s.cursor.y), (5, 0))
|
||||
|
||||
# a wide ZWJ emoji following a run
|
||||
s = self.create_screen()
|
||||
s.draw('ab\U0001f468\U0001f466')
|
||||
self.ae(str(s.line(0)), 'ab\U0001f468\U0001f466')
|
||||
self.ae((s.cursor.x, s.cursor.y), (4, 0))
|
||||
|
||||
# the active hyperlink must be applied to all cells of a run
|
||||
s = self.create_screen()
|
||||
parse_bytes(s, '\x1b]8;;http://x.com\x1b\\'.encode('utf-8'))
|
||||
s.draw('abc')
|
||||
parse_bytes(s, '\x1b]8;;\x1b\\'.encode('utf-8'))
|
||||
s.draw('d')
|
||||
self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 0))
|
||||
|
||||
# SGR attributes must be applied to all cells of a run
|
||||
s = self.create_screen()
|
||||
parse_bytes(s, b'\x1b[31mabc\x1b[39md')
|
||||
self.ae(s.line(0).as_ansi(), '\x1b[31mabc\x1b[39md')
|
||||
|
||||
# drawing over a selection must clear it
|
||||
s = self.create_screen()
|
||||
s.draw('hello')
|
||||
s.start_selection(0, 0)
|
||||
s.update_selection(4, 0)
|
||||
self.ae(''.join(s.text_for_selection()), 'hello')
|
||||
s.cursor_position(1, 1)
|
||||
s.draw('bye')
|
||||
self.ae(s.text_for_selection(), ())
|
||||
# also when the run wraps onto the selected line
|
||||
s = self.create_screen()
|
||||
s.draw('hello')
|
||||
s.carriage_return(), s.linefeed()
|
||||
s.draw('world')
|
||||
s.start_selection(0, 1)
|
||||
s.update_selection(4, 1)
|
||||
self.ae(''.join(s.text_for_selection()), 'world')
|
||||
s.cursor_position(1, 1)
|
||||
s.draw('a' * (s.columns + 1))
|
||||
self.ae(s.text_for_selection(), ())
|
||||
|
||||
# scrolling must keep an existing selection anchored to its content
|
||||
s = self.create_screen()
|
||||
for i in range(s.lines):
|
||||
if i:
|
||||
s.carriage_return(), s.linefeed()
|
||||
s.draw(str(i) * s.columns)
|
||||
s.start_selection(0, 0)
|
||||
s.update_selection(4, 0)
|
||||
self.ae(''.join(s.text_for_selection()), '00000')
|
||||
s.linefeed()
|
||||
self.ae(''.join(s.text_for_selection()), '00000')
|
||||
|
||||
# tabs at various offsets, exercising the memoized tab text lookup
|
||||
s = self.create_screen(cols=32)
|
||||
s.draw('a\tbc\td\te')
|
||||
self.ae((s.cursor.x, s.cursor.y), (25, 0))
|
||||
self.ae(s.line(0).as_ansi(), 'a\tbc\td\te')
|
||||
# a tab spanning more columns than the memo covers
|
||||
s = self.create_screen(cols=40)
|
||||
parse_bytes(s, b'\x1b[3g') # clear all tab stops
|
||||
s.draw('a\tb')
|
||||
self.ae((s.cursor.x, s.cursor.y), (40, 0))
|
||||
self.ae(s.line(0).as_ansi(), 'a\tb')
|
||||
|
||||
def test_draw_char(self):
|
||||
# Test in line-wrap, non-insert mode
|
||||
s = self.create_screen()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue