mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-15 12:46:50 +00:00
Shaped run cache: bound real memory use, not requested bytes
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
The running byte counter summed the sizes requested from the arenas. The arenas serve allocations from fixed size blocks and strand the tail of a block whenever the next allocation does not fit, so the counter understated real usage. For runs of 17 to 20 columns, where the value allocation lands just over half a block, only one value fits per block and the cache grew to ~2MB against a 1MB cap. As the limit applies per Font, that overshoot is multiplied across every font in a FontGroup. Track bytes actually malloced in the arena itself instead, so the cap check stays O(1), which was the point of the counter, while measuring the same thing the walking count used to.
This commit is contained in:
parent
dd25eb8538
commit
e436d55b71
2 changed files with 20 additions and 15 deletions
|
|
@ -33,6 +33,11 @@ typedef struct MA_BLOCK_TYPE_NAME {
|
|||
typedef struct MA_TYPE_NAME {
|
||||
MA_BLOCK_TYPE_NAME *blocks;
|
||||
size_t count, capacity;
|
||||
// total number of bytes malloced by this arena, for callers that need to
|
||||
// bound their memory use. Note that this is larger than the sum of the
|
||||
// sizes passed to _get() since blocks are allocated in fixed size chunks
|
||||
// and the tail of a block is wasted when the next allocation does not fit.
|
||||
size_t allocated;
|
||||
} MA_TYPE_NAME;
|
||||
|
||||
static inline void
|
||||
|
|
@ -62,11 +67,13 @@ MA_CAT(MA_NAME, _get)(MA_TYPE_NAME *self, size_t sz) {
|
|||
free(chunk);
|
||||
return NULL;
|
||||
}
|
||||
self->allocated += (capacity - self->capacity) * sizeof(MA_BLOCK_TYPE_NAME);
|
||||
self->capacity = capacity;
|
||||
self->blocks = blocks;
|
||||
}
|
||||
self->blocks[count - 1] = (MA_BLOCK_TYPE_NAME){.capacity = block_sz, .buf = chunk};
|
||||
self->count = count;
|
||||
self->allocated += block_sz;
|
||||
}
|
||||
char *ans = (char *)self->blocks[self->count - 1].buf + self->blocks[self->count - 1].used;
|
||||
self->blocks[self->count - 1].used += required_size;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ typedef struct HashTable {
|
|||
shaped_run_map table;
|
||||
KeyMonotonicArena keys;
|
||||
ValMonotonicArena vals;
|
||||
size_t used; // arena-rounded key+value payload of live entries
|
||||
struct {
|
||||
ShapedRunKey *key;
|
||||
size_t capacity;
|
||||
|
|
@ -184,15 +183,21 @@ shaped_run_get(
|
|||
return true;
|
||||
}
|
||||
|
||||
// Must count bytes actually malloced, not the sum of the requested sizes. The
|
||||
// arenas hand out space from fixed size blocks and strand the tail of a block
|
||||
// whenever the next allocation does not fit in it, which for run lengths that
|
||||
// happen to produce a value just over half a block wastes almost half of every
|
||||
// block. Counting requested sizes instead understates real usage by up to ~2x.
|
||||
static size_t
|
||||
arena_round(size_t sz) {
|
||||
size_t n = (sz / SHAPED_RUN_MA_BLOCK_SIZE) * SHAPED_RUN_MA_BLOCK_SIZE;
|
||||
return n < sz ? n + SHAPED_RUN_MA_BLOCK_SIZE : n;
|
||||
shaped_run_live_bytes(const HashTable *ht) {
|
||||
const size_t buckets = vt_bucket_count(&ht->table);
|
||||
const size_t scratch = ht->scratch.key ? sizeof(ShapedRunKey) + ht->scratch.capacity : 0;
|
||||
return sizeof(HashTable) + scratch + ht->keys.allocated + ht->vals.allocated + buckets * (sizeof(shaped_run_map_bucket) + sizeof(uint16_t));
|
||||
}
|
||||
|
||||
static bool
|
||||
shaped_run_at_cap(const HashTable *ht) {
|
||||
return vt_size(&ht->table) >= SHAPED_RUN_MAX_ENTRIES || ht->used >= SHAPED_RUN_MAX_BYTES;
|
||||
return vt_size(&ht->table) >= SHAPED_RUN_MAX_ENTRIES || shaped_run_live_bytes(ht) >= SHAPED_RUN_MAX_BYTES;
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -201,7 +206,6 @@ shaped_run_drop(HashTable *ht) {
|
|||
Key_free_all(&ht->keys);
|
||||
Val_free_all(&ht->vals);
|
||||
vt_init(&ht->table);
|
||||
ht->used = 0;
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -220,10 +224,8 @@ shaped_run_put(
|
|||
if ((num_glyphs && (!info || !positions)) || (num_groups && !groups)) return;
|
||||
#define scratch ht->scratch.key
|
||||
if (shaped_run_at_cap(ht)) shaped_run_drop(ht);
|
||||
const size_t key_sz = sizeof(ShapedRunKey) + scratch->keysz_in_bytes;
|
||||
const size_t vsz = val_size(num_glyphs, num_groups);
|
||||
ShapedRunKey *key = Key_get(&ht->keys, key_sz);
|
||||
ShapedRunValue *val = key ? Val_get(&ht->vals, vsz) : NULL;
|
||||
ShapedRunKey *key = Key_get(&ht->keys, sizeof(ShapedRunKey) + scratch->keysz_in_bytes);
|
||||
ShapedRunValue *val = key ? Val_get(&ht->vals, val_size(num_glyphs, num_groups)) : NULL;
|
||||
// the arenas cannot free individual allocations, so on failure drop the
|
||||
// whole cache rather than leaking what was allocated for this entry
|
||||
if (!val) {
|
||||
|
|
@ -238,10 +240,6 @@ shaped_run_put(
|
|||
memcpy(val_positions(val), positions, num_glyphs * sizeof(positions[0]));
|
||||
}
|
||||
if (num_groups) memcpy(val_groups(val), groups, num_groups * sizeof(groups[0]));
|
||||
if (vt_is_end(vt_insert(&ht->table, key, val))) {
|
||||
shaped_run_drop(ht);
|
||||
return;
|
||||
}
|
||||
ht->used += arena_round(key_sz) + arena_round(vsz);
|
||||
if (vt_is_end(vt_insert(&ht->table, key, val))) shaped_run_drop(ht);
|
||||
#undef scratch
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue