composed: store compose chains in a binary search tree

The previous implementation stored compose chains in a dynamically
allocated array. Adding a chain was easy: resize the array and append
the new chain at the end. Looking up a compose chain given a compose
chain key/index was also easy: just index into the array.

However, searching for a pre-existing chain given a codepoint sequence
was very slow. Since the array wasn’t sorted, we typically had to scan
through the entire array, just to realize that there is no
pre-existing chain, and that we need to add a new one.

Since this happens for *each* codepoint in a grapheme cluster, things
quickly became really slow.

Things were ok:ish as long as the compose chain struct was small, as
that made it possible to hold all the chains in the cache. Once the
number of chains reached a certain point, or when we were forced to
bump maximum number of allowed codepoints in a chain, we started
thrashing the cache and things got much much worse.

So what can we do?

We can’t sort the array, because

a) that would invalidate all existing chain keys in the grid (and
iterating the entire scrollback and updating compose keys is *not* an
option).

b) inserting a chain becomes slow as we need to first find _where_ to
insert it, and then memmove() the rest of the array.

This patch uses a binary search tree to store the chains instead of a
simple array.

The tree is sorted on a “key”, which is the XOR of all codepoints,
truncated to the CELL_COMB_CHARS_HI-CELL_COMB_CHARS_LO range.

The grid now stores CELL_COMB_CHARS_LO+key, instead of
CELL_COMB_CHARS_LO+index.

Since the key is truncated, collisions may occur. This is handled by
incrementing the key by 1.

Lookup is of course slower than before, O(log n) instead of
O(1).

Insertion is slightly slower as well: technically it’s O(log n)
instead of O(1). However, we also need to take into account the
re-allocating the array will occasionally force a full copy of the
array when it cannot simply be growed.

But finding a pre-existing chain is now *much* faster: O(log n)
instead of O(n). In most cases, the first lookup will either
succeed (return a true match), or fail (return NULL). However, since
key collisions are possible, it may also return false matches. This
means we need to verify the contents of the chain before deciding to
use it instead of inserting a new chain. But remember that this
comparison was being done for each and every chain in the previous
implementation.

With lookups being much faster, and in particular, no longer requiring
us to check the chain contents for every singlec chain, we can now use
a dynamically allocated ‘chars’ array in the chain. This was
previously a hardcoded array of 10 chars.

Using a dynamic allocated array means looking in the array is slower,
since we now need two loads: one to load the pointer, and a second to
load _from_ the pointer.

As a result, the base size of a compose chain (i.e. an “empty” chain)
has now been reduced from 48 bytes to 32. A chain with two codepoints
is 40 bytes. This means we have up to 4 codepoints while still using
less, or the same amount, of memory as before.

Furthermore, the Unicode random test (i.e. write random “unicode”
chars) is now **faster** than current master (i.e. before text-shaping
support was added), **with** test-shaping enabled. With text-shaping
disabled, we’re _even_ faster.
This commit is contained in:
Daniel Eklöf 2021-06-24 13:17:07 +02:00
parent fcd6327297
commit fe8ca23cfe
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
12 changed files with 170 additions and 86 deletions

76
composed.c Normal file
View file

@ -0,0 +1,76 @@
#include "composed.h"
#include <stdlib.h>
#include <stdbool.h>
#include "debug.h"
struct composed *
composed_lookup(struct composed *root, uint32_t key)
{
struct composed *node = root;
while (node != NULL) {
if (key == node->key)
return node;
node = key < node->key ? node->left : node->right;
}
return NULL;
}
uint32_t
composed_insert(struct composed **root, struct composed *node)
{
node->left = node->right = NULL;
if (*root == NULL) {
*root = node;
return node->key;
}
uint32_t key = node->key;
struct composed *prev = NULL;
struct composed *n = *root;
while (n != NULL) {
if (n->key == node->key) {
/* TODO: wrap around at (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO) */
key++;
}
prev = n;
n = key < n->key ? n->left : n->right;
}
xassert(prev != NULL);
xassert(n == NULL);
/* May have been changed */
node->key = key;
if (key < prev->key) {
xassert(prev->left == NULL);
prev->left = node;
} else {
xassert(prev->right == NULL);
prev->right = node;
}
return key;
}
void
composed_free(struct composed *root)
{
if (root == NULL)
return;
composed_free(root->left);
composed_free(root->right);
free(root->chars);
free(root);
}

18
composed.h Normal file
View file

@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
#include <wchar.h>
struct composed {
wchar_t *chars;
struct composed *left;
struct composed *right;
uint32_t key;
uint8_t count;
uint8_t width;
};
struct composed *composed_lookup(struct composed *root, uint32_t key);
uint32_t composed_insert(struct composed **root, struct composed *node);
void composed_free(struct composed *root);

View file

@ -223,11 +223,10 @@ extract_one(const struct terminal *term, const struct row *row,
ctx->newline_count = 0; ctx->newline_count = 0;
ctx->empty_count = 0; ctx->empty_count = 0;
if (cell->wc >= CELL_COMB_CHARS_LO && if (cell->wc >= CELL_COMB_CHARS_LO && cell->wc <= CELL_COMB_CHARS_HI)
cell->wc < (CELL_COMB_CHARS_LO + term->composed_count))
{ {
const struct composed *composed const struct composed *composed = composed_lookup(
= &term->composed[cell->wc - CELL_COMB_CHARS_LO]; term->composed, cell->wc - CELL_COMB_CHARS_LO);
if (!ensure_size(ctx, composed->count)) if (!ensure_size(ctx, composed->count))
goto err; goto err;

4
grid.c
View file

@ -416,9 +416,7 @@ grid_resize_and_reflow(
struct grid *grid, int new_rows, int new_cols, struct grid *grid, int new_rows, int new_cols,
int old_screen_rows, int new_screen_rows, int old_screen_rows, int new_screen_rows,
size_t tracking_points_count, size_t tracking_points_count,
struct coord *const _tracking_points[static tracking_points_count], struct coord *const _tracking_points[static tracking_points_count])
size_t compose_count, const struct
composed composed[static compose_count])
{ {
#if defined(TIME_REFLOW) && TIME_REFLOW #if defined(TIME_REFLOW) && TIME_REFLOW
struct timeval start; struct timeval start;

4
grid.h
View file

@ -19,9 +19,7 @@ void grid_resize_and_reflow(
struct grid *grid, int new_rows, int new_cols, struct grid *grid, int new_rows, int new_cols,
int old_screen_rows, int new_screen_rows, int old_screen_rows, int new_screen_rows,
size_t tracking_points_count, size_t tracking_points_count,
struct coord *const _tracking_points[static tracking_points_count], struct coord *const _tracking_points[static tracking_points_count]);
size_t compose_count,
const struct composed composed[static compose_count]);
static inline int static inline int
grid_row_absolute(const struct grid *grid, int row_no) grid_row_absolute(const struct grid *grid, int row_no)

View file

@ -147,6 +147,7 @@ misc = static_library(
vtlib = static_library( vtlib = static_library(
'vtlib', 'vtlib',
'base64.c', 'base64.h', 'base64.c', 'base64.h',
'composed.c', 'composed.h',
'csi.c', 'csi.h', 'csi.c', 'csi.h',
'dcs.c', 'dcs.h', 'dcs.c', 'dcs.h',
'osc.c', 'osc.h', 'osc.c', 'osc.h',

View file

@ -557,10 +557,9 @@ render_cell(struct terminal *term, pixman_image_t *pix,
cell_cols = single->cols; cell_cols = single->cols;
} }
else if (base >= CELL_COMB_CHARS_LO && else if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI)
base < (CELL_COMB_CHARS_LO + term->composed_count))
{ {
composed = &term->composed[base - CELL_COMB_CHARS_LO]; composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO);
base = composed->chars[0]; base = composed->chars[0];
if (term->conf->can_shape_grapheme && term->conf->tweak.grapheme_shaping) { if (term->conf->can_shape_grapheme && term->conf->tweak.grapheme_shaping) {
@ -3339,8 +3338,8 @@ maybe_resize(struct terminal *term, int width, int height, bool force)
/* Resize grids */ /* Resize grids */
grid_resize_and_reflow( grid_resize_and_reflow(
&term->normal, new_normal_grid_rows, new_cols, old_rows, new_rows, &term->normal, new_normal_grid_rows, new_cols, old_rows, new_rows,
term->selection.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points, term->selection.end.row >= 0 ? ALEN(tracking_points) : 0,
term->composed_count, term->composed); tracking_points);
grid_resize_without_reflow( grid_resize_without_reflow(
&term->alt, new_alt_grid_rows, new_cols, old_rows, new_rows); &term->alt, new_alt_grid_rows, new_cols, old_rows, new_rows);

View file

@ -245,10 +245,9 @@ matches_cell(const struct terminal *term, const struct cell *cell, size_t search
wchar_t base = cell->wc; wchar_t base = cell->wc;
const struct composed *composed = NULL; const struct composed *composed = NULL;
if (base >= CELL_COMB_CHARS_LO && if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI)
base < (CELL_COMB_CHARS_LO + term->composed_count))
{ {
composed = &term->composed[base - CELL_COMB_CHARS_LO]; composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO);
base = composed->chars[0]; base = composed->chars[0];
} }

View file

@ -246,11 +246,8 @@ selection_find_word_boundary_left(struct terminal *term, struct coord *pos,
c = r->cells[pos->col].wc; c = r->cells[pos->col].wc;
} }
if (c >= CELL_COMB_CHARS_LO && if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI)
c < (CELL_COMB_CHARS_LO + term->composed_count)) c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0];
{
c = term->composed[c - CELL_COMB_CHARS_LO].chars[0];
}
bool initial_is_space = c == 0 || iswspace(c); bool initial_is_space = c == 0 || iswspace(c);
bool initial_is_delim = bool initial_is_delim =
@ -286,11 +283,8 @@ selection_find_word_boundary_left(struct terminal *term, struct coord *pos,
c = row->cells[next_col].wc; c = row->cells[next_col].wc;
} }
if (c >= CELL_COMB_CHARS_LO && if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI)
c < (CELL_COMB_CHARS_LO + term->composed_count)) c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0];
{
c = term->composed[c - CELL_COMB_CHARS_LO].chars[0];
}
bool is_space = c == 0 || iswspace(c); bool is_space = c == 0 || iswspace(c);
bool is_delim = bool is_delim =
@ -325,11 +319,8 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos,
c = r->cells[pos->col].wc; c = r->cells[pos->col].wc;
} }
if (c >= CELL_COMB_CHARS_LO && if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI)
c < (CELL_COMB_CHARS_LO + term->composed_count)) c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0];
{
c = term->composed[c - CELL_COMB_CHARS_LO].chars[0];
}
bool initial_is_space = c == 0 || iswspace(c); bool initial_is_space = c == 0 || iswspace(c);
bool initial_is_delim = bool initial_is_delim =
@ -367,11 +358,8 @@ selection_find_word_boundary_right(struct terminal *term, struct coord *pos,
c = row->cells[next_col].wc; c = row->cells[next_col].wc;
} }
if (c >= CELL_COMB_CHARS_LO && if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI)
c < (CELL_COMB_CHARS_LO + term->composed_count)) c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0];
{
c = term->composed[c - CELL_COMB_CHARS_LO].chars[0];
}
bool is_space = c == 0 || iswspace(c); bool is_space = c == 0 || iswspace(c);
bool is_delim = bool is_delim =

View file

@ -1132,7 +1132,6 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper,
.normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()},
.alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()},
.grid = &term->normal, .grid = &term->normal,
.composed_count = 0,
.composed = NULL, .composed = NULL,
.alt_scrolling = conf->mouse.alternate_scroll_mode, .alt_scrolling = conf->mouse.alternate_scroll_mode,
.meta = { .meta = {
@ -1431,7 +1430,7 @@ term_destroy(struct terminal *term)
grid_free(&term->normal); grid_free(&term->normal);
grid_free(&term->alt); grid_free(&term->alt);
free(term->composed); composed_free(term->composed);
free(term->window_title); free(term->window_title);
tll_free_and_free(term->window_title_stack, free); tll_free_and_free(term->window_title_stack, free);

View file

@ -16,6 +16,7 @@
#include <fcft/fcft.h> #include <fcft/fcft.h>
//#include "config.h" //#include "config.h"
#include "composed.h"
#include "debug.h" #include "debug.h"
#include "fdm.h" #include "fdm.h"
#include "macros.h" #include "macros.h"
@ -84,12 +85,6 @@ struct damage {
int lines; int lines;
}; };
struct composed {
wchar_t chars[10];
uint8_t count;
int width;
};
struct row_uri_range { struct row_uri_range {
int start; int start;
int end; int end;

94
vt.c
View file

@ -611,21 +611,22 @@ action_utf8_print(struct terminal *term, wchar_t wc)
xassert(col >= 0 && col < term->cols); xassert(col >= 0 && col < term->cols);
wchar_t base = row->cells[col].wc; wchar_t base = row->cells[col].wc;
wchar_t UNUSED last = base; wchar_t UNUSED last = base;
size_t search_start_index = 0; uint32_t key = base ^ wc;
/* Is base cell already a cluster? */ /* Is base cell already a cluster? */
const struct composed *composed = const struct composed *composed =
(base >= CELL_COMB_CHARS_LO && (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI)
base < (CELL_COMB_CHARS_LO + term->composed_count)) ? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO)
? &term->composed[base - CELL_COMB_CHARS_LO]
: NULL; : NULL;
if (composed != NULL) { if (composed != NULL) {
search_start_index = base - CELL_COMB_CHARS_LO;
base = composed->chars[0]; base = composed->chars[0];
last = composed->chars[composed->count - 1]; last = composed->chars[composed->count - 1];
key = composed->key ^ wc;
} }
key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO;
#if defined(FOOT_GRAPHEME_CLUSTERING) #if defined(FOOT_GRAPHEME_CLUSTERING)
if (grapheme_clustering) { if (grapheme_clustering) {
/* Check if we're on a grapheme cluster break */ /* Check if we're on a grapheme cluster break */
@ -677,7 +678,7 @@ action_utf8_print(struct terminal *term, wchar_t wc)
} }
size_t wanted_count = composed != NULL ? composed->count + 1 : 2; size_t wanted_count = composed != NULL ? composed->count + 1 : 2;
if (wanted_count > ALEN(composed->chars)) { if (wanted_count > 255) {
xassert(composed != NULL); xassert(composed != NULL);
#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
@ -691,47 +692,46 @@ action_utf8_print(struct terminal *term, wchar_t wc)
wanted_count--; wanted_count--;
} }
xassert(wanted_count <= ALEN(composed->chars)); xassert(wanted_count <= 255);
/* Look for existing combining chain */ /* Look for existing combining chain */
for (size_t i = search_start_index; i < term->composed_count; i++) { while (true) {
const struct composed *cc = &term->composed[i]; const struct composed *cc = composed_lookup(term->composed, key);
if (cc == NULL)
break;
if (cc->chars[0] != base) /*
* We may have a key collisison, so need to check that
* its a true match. If not, bumb the key and try
* again.
*/
if (cc->chars[0] != base ||
cc->count != wanted_count ||
cc->chars[wanted_count - 1] != wc)
{
key++;
continue; continue;
if (cc->count != wanted_count)
continue;
bool match = true;
for (size_t j = 1; j < wanted_count - 1; j++) {
if (cc->chars[j] != composed->chars[j]) {
match = false;
break;
}
} }
if (!match)
continue;
if (cc->chars[wanted_count - 1] != wc) bool match = composed != NULL
continue; ? memcmp(&cc->chars[1], &composed->chars[1],
(wanted_count - 2) * sizeof(cc->chars[0])) == 0
: true;
wc = CELL_COMB_CHARS_LO + i; if (!match) {
key++;
continue;
}
wc = CELL_COMB_CHARS_LO + cc->key;
width = cc->width; width = cc->width;
goto out; goto out;
} }
/* Allocate new chain */ if (unlikely(term->composed_count >=
(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO)))
struct composed new_cc; {
new_cc.count = wanted_count;
new_cc.chars[0] = base;
for (size_t i = 1; i < wanted_count - 1; i++)
new_cc.chars[i] = composed->chars[i];
new_cc.chars[wanted_count - 1] = wc;
if (unlikely(term->composed_count >= CELL_COMB_CHARS_HI)) {
/* We reached our maximum number of allowed composed /* We reached our maximum number of allowed composed
* character chains. Fall through here and print the * character chains. Fall through here and print the
* current zero-width character to the current cell */ * current zero-width character to the current cell */
@ -739,18 +739,32 @@ action_utf8_print(struct terminal *term, wchar_t wc)
goto out; goto out;
} }
/* Allocate new chain */
struct composed *new_cc = xmalloc(sizeof(*new_cc));
new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0]));
new_cc->key = key;
new_cc->count = wanted_count;
new_cc->chars[0] = base;
new_cc->chars[wanted_count - 1] = wc;
if (composed != NULL) {
memcpy(&new_cc->chars[1], &composed->chars[1],
(wanted_count - 2) * sizeof(new_cc->chars[0]));
}
int grapheme_width = composed != NULL ? composed->width : base_width; int grapheme_width = composed != NULL ? composed->width : base_width;
if (wc == 0xfe0f && grapheme_width < 2) if (wc == 0xfe0f && grapheme_width < 2)
grapheme_width = 2; grapheme_width = 2;
else else
grapheme_width += width; grapheme_width += width;
new_cc.width = grapheme_width; new_cc->width = grapheme_width;
term->composed_count++; term->composed_count++;
term->composed = xrealloc(term->composed, term->composed_count * sizeof(term->composed[0])); key = composed_insert(&term->composed, new_cc);
term->composed[term->composed_count - 1] = new_cc; wc = CELL_COMB_CHARS_LO + key;
xassert(wc <= CELL_COMB_CHARS_HI);
wc = CELL_COMB_CHARS_LO + term->composed_count - 1;
width = grapheme_width; width = grapheme_width;
goto out; goto out;
} }