mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-04 04:06:06 -05:00
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:
parent
fcd6327297
commit
fe8ca23cfe
12 changed files with 170 additions and 86 deletions
94
vt.c
94
vt.c
|
|
@ -611,21 +611,22 @@ action_utf8_print(struct terminal *term, wchar_t wc)
|
|||
xassert(col >= 0 && col < term->cols);
|
||||
wchar_t base = row->cells[col].wc;
|
||||
wchar_t UNUSED last = base;
|
||||
size_t search_start_index = 0;
|
||||
uint32_t key = base ^ wc;
|
||||
|
||||
/* Is base cell already a cluster? */
|
||||
const struct composed *composed =
|
||||
(base >= CELL_COMB_CHARS_LO &&
|
||||
base < (CELL_COMB_CHARS_LO + term->composed_count))
|
||||
? &term->composed[base - CELL_COMB_CHARS_LO]
|
||||
(base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI)
|
||||
? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO)
|
||||
: NULL;
|
||||
|
||||
if (composed != NULL) {
|
||||
search_start_index = base - CELL_COMB_CHARS_LO;
|
||||
base = composed->chars[0];
|
||||
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 (grapheme_clustering) {
|
||||
/* 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;
|
||||
if (wanted_count > ALEN(composed->chars)) {
|
||||
if (wanted_count > 255) {
|
||||
xassert(composed != NULL);
|
||||
|
||||
#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
|
||||
|
|
@ -691,47 +692,46 @@ action_utf8_print(struct terminal *term, wchar_t wc)
|
|||
wanted_count--;
|
||||
}
|
||||
|
||||
xassert(wanted_count <= ALEN(composed->chars));
|
||||
xassert(wanted_count <= 255);
|
||||
|
||||
/* Look for existing combining chain */
|
||||
for (size_t i = search_start_index; i < term->composed_count; i++) {
|
||||
const struct composed *cc = &term->composed[i];
|
||||
while (true) {
|
||||
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
|
||||
* it’s 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;
|
||||
|
||||
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)
|
||||
continue;
|
||||
bool match = composed != NULL
|
||||
? 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;
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Allocate new chain */
|
||||
|
||||
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)) {
|
||||
if (unlikely(term->composed_count >=
|
||||
(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO)))
|
||||
{
|
||||
/* We reached our maximum number of allowed composed
|
||||
* character chains. Fall through here and print the
|
||||
* current zero-width character to the current cell */
|
||||
|
|
@ -739,18 +739,32 @@ action_utf8_print(struct terminal *term, wchar_t wc)
|
|||
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;
|
||||
|
||||
if (wc == 0xfe0f && grapheme_width < 2)
|
||||
grapheme_width = 2;
|
||||
else
|
||||
grapheme_width += width;
|
||||
new_cc.width = grapheme_width;
|
||||
new_cc->width = grapheme_width;
|
||||
|
||||
term->composed_count++;
|
||||
term->composed = xrealloc(term->composed, term->composed_count * sizeof(term->composed[0]));
|
||||
term->composed[term->composed_count - 1] = new_cc;
|
||||
key = composed_insert(&term->composed, 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;
|
||||
goto out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue