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.
fcft’s view of how many columns a grapheme cluster is may differ from
our own. Make sure the rendered glyph matches the number of columns
that were allocated when the cluster was printed.
When checking if we already have a compose chain for the current
sequence of characters, don’t search the list from the beginning,
unless we have to.
Taking the following things into consideration:
* New compose chains are always appended at the end of the list
* If the current sequence is 3 or more characters, it *must* consist
of an existing compose chain, plus the new character.
Thus, when searching, start at index 0 if we only have two characters,
since then the base cell originally contained a regular base
character, and not a compose chain. I.e. the new chain may be
_anywhere_ in the chain list.
If however we have a sequence of three or more characters, start at
the index the *base* chain was at. If the chain we’re searching for
exists, it *must* have been added *after* the base chain, and thus
it *must* be located *after* the base chain in the chain list.
Before the grapheme cluster segmentation work, we limited the number
of combining characters to base+5. I.e. 6 in total.
For a while now, we’ve had it bumped all the way up to 20. This was
the reason the unicode-random benchmark ran so much slower (i.e. cache
contention).
Looking at emoji’s, there are a couple that need 6 code points,
and *three* that needs 7.
Now, with the limit at 7 chars, and the new ‘width’ member, the
composed struct is 8 bytes larger than before.
We already have all the widths needed to calculate the new one; it’s
the base characters width (base_width), or the previous combining
chain’s width (composed->width) plus the new characters’s
width (width).
If an image was split up across the scrollback, and the first image
chunk was resized due to it being blended with an already existing
sixel, we crashed.
The reason being the second chunk, emitted *after* the scrollback,
tried to memcpy() image data from a now-free:d image buffer.
The fix is pretty simple: create copies for *all* chunks when an image
has to be split up across the scrollback. Previously, the first chunk
re-used the source image buffer.
Closes#608
And flesh out the description of the ‘terminfo’ option, and how it
relates to the new ‘terminfo-install-location’.
Also add instructions on how to manually generate the terminfo files.
Add a new meson option, ‘terminfo-install-location’, that allows you
to customize _where_ the terminfo files are installed, relative to the
installation prefix.
It also recognizes the special value ‘disabled’, in which case the
terminfo files are not installed at all. The terminfo files _are_
however built (allowing us to catch build errors), and foot still
defaults to the ‘foot’ terminfo.
It defaults to $datadir/terminfo
If (the other option) ‘terminfo’ is set to disabled (or tic cannot be
found), terminfo-install-location is automatically set to ‘disabled’.
We now track override data (length + malloc:ed string) in a struct,
and push this struct to our overrides linked list.
There’s a new function, push_override() that takes a string,
calculates its length, strdup() it and pushes it to the linked
list. This function also length-checks the string, to ensure we don’t
overflow.
This way, we don’t have to loop the overrides list twice; once when
calculating the total length of all overrides, and once when sending
the overrides to the server.
Now, we can update the total length as we add overrides (i.e while
parsing the command line, instead of afterwards).
This means we only have to loop the list once, and that’s when sending
it. This also means there’s no longer any need to malloc an array
holding the lengths of each override.
Send a generic “overrides” list to the server, containing options in
text, on the format “section.key=value”.
This reduces the size of the base client/server protocol packet, as
well as opens up for a generic -o,--override command line option (not
yet implemented).
When the connecting client overrides config options, clone the
server’s configuration, and then convert the overridden options to
config overrides, and apply them using config_override_apply().
When destroying the client, free the cloned config using the regular
config_free().