Move hyperlink hashmap to verstable

Also speedup getting url for hyperlink id by using a second hash map for
it. Change behavior when there are too many hyperlinks to discard new
hyperlinks instead of remapping old hyperlink id and making some old
hyperlink wrong.
This commit is contained in:
Kovid Goyal
2024-07-09 15:11:54 +05:30
parent 9727ea1cac
commit 2955c3f7a7
4 changed files with 116 additions and 128 deletions

View File

@@ -6,56 +6,47 @@
*/ */
#include "hyperlink.h" #include "hyperlink.h"
#include "kitty-uthash.h" #include "lineops.h"
#include <string.h> #include <string.h>
#define MAX_KEY_LEN 2048 #define MAX_KEY_LEN 2048
#define MAX_ID_LEN 256 #define MAX_ID_LEN 256
#define MAX_ADDS_BEFORE_GC 256
typedef struct { #define NAME hyperlink_map
const char *key; #define KEY_TY const char*
hyperlink_id_type id; #define VAL_TY hyperlink_id_type
UT_hash_handle hh; #include "kitty-verstable.h"
} HyperLinkEntry;
#define NAME hyperlink_id_map
#define KEY_TY hyperlink_id_type
#define VAL_TY const char*
#include "kitty-verstable.h"
typedef struct { typedef struct {
HyperLinkEntry *hyperlinks; hyperlink_map map;
unsigned int max_link_id, num_of_adds_since_garbage_collection; hyperlink_id_map idmap;
hyperlink_id_type max_link_id, num_of_adds_since_garbage_collection;
} HyperLinkPool; } HyperLinkPool;
static void
free_hyperlink_entry(HyperLinkEntry *s) {
free((void*)s->key);
free(s);
}
static void static void
clear_pool(HyperLinkPool *pool) { clear_pool(HyperLinkPool *pool) {
if (pool->hyperlinks) { for (hyperlink_map_itr i = vt_first(&pool->map); !vt_is_end(i); i = vt_next(i)) free((char*)i.data->key);
HyperLinkEntry *tmp, *s; vt_cleanup(&pool->map); vt_cleanup(&pool->idmap);
HASH_ITER(hh, pool->hyperlinks, s, tmp) { pool->max_link_id = 0; pool->num_of_adds_since_garbage_collection = 0;
HASH_DEL(pool->hyperlinks, s);
free_hyperlink_entry(s); s = NULL;
}
pool->max_link_id = 0;
}
} }
HYPERLINK_POOL_HANDLE HYPERLINK_POOL_HANDLE
alloc_hyperlink_pool(void) { alloc_hyperlink_pool(void) {
return calloc(1, sizeof(HyperLinkPool)); HyperLinkPool *ans = calloc(1, sizeof(HyperLinkPool));
if (ans) { vt_init(&ans->map); vt_init(&ans->idmap); }
return (HYPERLINK_POOL_HANDLE)ans;
} }
void void
clear_hyperlink_pool(HYPERLINK_POOL_HANDLE h) { clear_hyperlink_pool(HYPERLINK_POOL_HANDLE h) {
if (h) { if (h) clear_pool((HyperLinkPool*)h);
HyperLinkPool *pool = (HyperLinkPool*)h;
clear_pool(pool);
}
} }
@@ -68,29 +59,68 @@ free_hyperlink_pool(HYPERLINK_POOL_HANDLE h) {
} }
} }
#define NAME id_id_map
#define KEY_TY hyperlink_id_type
#define VAL_TY hyperlink_id_type
#include "kitty-verstable.h"
static const char*
dupstr(const char *src, size_t len) {
char *ans = malloc(len+1);
if (!ans) fatal("Out of memory");
memcpy(ans, src, len); ans[len] = 0;
return ans;
}
static void
process_cell(HyperLinkPool *pool, id_id_map *map, hyperlink_id_map *clone, CPUCell *c) {
if (!c->hyperlink_id) return;
id_id_map_itr n = vt_get(map, c->hyperlink_id);
hyperlink_id_type new_id;
if (vt_is_end(n)) {
hyperlink_id_map_itr i = vt_get(clone, c->hyperlink_id);
if (vt_is_end(i)) new_id = 0;
else {
new_id = ++pool->max_link_id;
if (vt_is_end(vt_insert(map, c->hyperlink_id, new_id))) fatal("Out of memory");
const char *key = i.data->val;
if (vt_is_end(vt_insert(&pool->map, key, new_id))) fatal("Out of memory");
if (vt_is_end(vt_insert(&pool->idmap, new_id, key))) fatal("Out of memory");
vt_erase_itr(clone, i);
}
} else new_id = n.data->val;
c->hyperlink_id = new_id;
}
static void
remap_hyperlink_ids(Screen *self, id_id_map *map, hyperlink_id_map *clone) {
HyperLinkPool *pool = (HyperLinkPool*)self->hyperlink_pool;
if (self->historybuf->count) {
for (index_type y = self->historybuf->count; y-- > 0;) {
CPUCell *cells = historybuf_cpu_cells(self->historybuf, y);
for (index_type x = 0; x < self->historybuf->xnum; x++) process_cell(pool, map, clone, cells + x);
}
}
LineBuf *second = self->linebuf, *first = second == self->main_linebuf ? self->alt_linebuf : self->main_linebuf;
for (index_type i = 0; i < self->lines * self->columns; i++) process_cell(pool, map, clone, first->cpu_cell_buf + i);
for (index_type i = 0; i < self->lines * self->columns; i++) process_cell(pool, map, clone, second->cpu_cell_buf + i);
}
void void
screen_garbage_collect_hyperlink_pool(Screen *screen) { screen_garbage_collect_hyperlink_pool(Screen *screen) {
HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool; HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
pool->num_of_adds_since_garbage_collection = 0; pool->num_of_adds_since_garbage_collection = 0;
if (!pool->max_link_id) return; if (!pool->max_link_id) return;
hyperlink_id_type *map = calloc(HYPERLINK_MAX_NUMBER + 4, sizeof(hyperlink_id_type)); pool->max_link_id = 0;
if (!map) fatal("Out of memory"); id_id_map map = {0};
hyperlink_id_type num = remap_hyperlink_ids(screen, map); vt_init(&map);
if (num) { hyperlink_id_map clone = {0};
HyperLinkEntry *s, *tmp; if (!vt_init_clone(&clone, &pool->idmap)) fatal("Out of memory");
pool->max_link_id = 0; vt_cleanup(&pool->map); vt_cleanup(&pool->idmap);
HASH_ITER(hh, pool->hyperlinks, s, tmp) { remap_hyperlink_ids(screen, &map, &clone);
if (map[s->id]) { for (hyperlink_id_map_itr i = vt_first(&clone); !vt_is_end(i); i = vt_next(i)) free((char*)i.data->val);
s->id = map[s->id]; vt_clear(&map); vt_clear(&clone);
pool->max_link_id = MAX(pool->max_link_id, s->id);
} else {
HASH_DEL(pool->hyperlinks, s);
free_hyperlink_entry(s); s = NULL;
}
}
} else clear_pool(pool);
free(map);
} }
@@ -103,57 +133,40 @@ get_id_for_hyperlink(Screen *screen, const char *id, const char *url) {
if (keylen < 0) keylen = strlen(key); if (keylen < 0) keylen = strlen(key);
else keylen = MIN(keylen, MAX_KEY_LEN - 2); // snprintf returns how many chars it would have written in case of truncation else keylen = MIN(keylen, MAX_KEY_LEN - 2); // snprintf returns how many chars it would have written in case of truncation
key[keylen] = 0; key[keylen] = 0;
HyperLinkEntry *s = NULL; hyperlink_map_itr itr = vt_get(&pool->map, key);
if (pool->hyperlinks) { if (!vt_is_end(itr)) return itr.data->val;
HASH_FIND_STR(pool->hyperlinks, key, s); if (pool->max_link_id >= HYPERLINK_MAX_NUMBER) {
if (s) { if (pool->num_of_adds_since_garbage_collection > 128) screen_garbage_collect_hyperlink_pool(screen);
// Remove and re-add s so that it is the last entry in the hash table and if (pool->max_link_id >= HYPERLINK_MAX_NUMBER) {
// The first entry is discarded when hash table is full. log_error("Too many hyperlinks, discarding hyperlink: %s", key);
HASH_DEL(pool->hyperlinks, s); return 0;
HASH_ADD_KEYPTR(hh, pool->hyperlinks, s->key, strlen(s->key), s);
return s->id;
} }
} }
hyperlink_id_type new_id = 0; hyperlink_id_type new_id = ++pool->max_link_id;
if (pool->num_of_adds_since_garbage_collection >= MAX_ADDS_BEFORE_GC) screen_garbage_collect_hyperlink_pool(screen); const char *skey = dupstr(key, keylen);
if (pool->max_link_id >= HYPERLINK_MAX_NUMBER && pool->hyperlinks) { if (vt_is_end(vt_insert(&pool->map, skey, new_id))) fatal("Out of memory");
log_error("Too many hyperlinks, discarding oldest, this means some hyperlinks might be incorrect"); if (vt_is_end(vt_insert(&pool->idmap, new_id, skey))) fatal("Out of memory");
new_id = pool->hyperlinks->id;
HyperLinkEntry *s = pool->hyperlinks;
HASH_DEL(pool->hyperlinks, s);
free_hyperlink_entry(s); s = NULL;
}
s = malloc(sizeof(HyperLinkEntry));
if (!s) fatal("Out of memory");
s->key = malloc(keylen + 1);
if (!s->key) fatal("Out of memory");
memcpy((void*)s->key, key, keylen + 1);
s->id = new_id ? new_id : ++pool->max_link_id;
HASH_ADD_KEYPTR(hh, pool->hyperlinks, s->key, keylen, s);
pool->num_of_adds_since_garbage_collection++; pool->num_of_adds_since_garbage_collection++;
return s->id; return new_id;
} }
const char* const char*
get_hyperlink_for_id(const HYPERLINK_POOL_HANDLE handle, hyperlink_id_type id, bool only_url) { get_hyperlink_for_id(const HYPERLINK_POOL_HANDLE handle, hyperlink_id_type id, bool only_url) {
const HyperLinkPool *pool = (HyperLinkPool*)handle; HyperLinkPool *pool = (HyperLinkPool*)handle;
HyperLinkEntry *s, *tmp; hyperlink_id_map_itr itr = vt_get(&pool->idmap, id);
HASH_ITER(hh, pool->hyperlinks, s, tmp) { if (vt_is_end(itr)) return NULL;
if (s->id == id) return only_url ? strstr(s->key, ":") + 1 : s->key; return only_url ? strstr(itr.data->val, ":") + 1 : itr.data->val;
}
return NULL;
} }
PyObject* PyObject*
screen_hyperlinks_as_list(Screen *screen) { screen_hyperlinks_as_set(Screen *screen) {
HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool; HyperLinkPool *pool = (HyperLinkPool*)screen->hyperlink_pool;
PyObject *ans = PyList_New(0); RAII_PyObject(ans, PySet_New(0));
HyperLinkEntry *s, *tmp; if (ans) {
HASH_ITER(hh, pool->hyperlinks, s, tmp) { for (hyperlink_map_itr itr = vt_first(&pool->map); !vt_is_end(itr); itr = vt_next(itr)) {
PyObject *e = Py_BuildValue("sH", s->key, s->id); RAII_PyObject(e, Py_BuildValue("sH", itr.data->key, itr.data->val));
PyList_Append(ans, e); if (!e || PySet_Add(ans, e) != 0) return NULL;
Py_DECREF(e); }
} }
return ans; Py_XINCREF(ans); return ans;
} }

View File

@@ -12,6 +12,5 @@ HYPERLINK_POOL_HANDLE alloc_hyperlink_pool(void);
void free_hyperlink_pool(HYPERLINK_POOL_HANDLE); void free_hyperlink_pool(HYPERLINK_POOL_HANDLE);
void clear_hyperlink_pool(HYPERLINK_POOL_HANDLE); void clear_hyperlink_pool(HYPERLINK_POOL_HANDLE);
hyperlink_id_type get_id_for_hyperlink(Screen*, const char*, const char*); hyperlink_id_type get_id_for_hyperlink(Screen*, const char*, const char*);
hyperlink_id_type remap_hyperlink_ids(Screen *self, hyperlink_id_type *map); PyObject* screen_hyperlinks_as_set(Screen *screen);
PyObject* screen_hyperlinks_as_list(Screen *screen);
void screen_garbage_collect_hyperlink_pool(Screen *screen); void screen_garbage_collect_hyperlink_pool(Screen *screen);

View File

@@ -606,30 +606,6 @@ set_active_hyperlink(Screen *self, char *id, char *url) {
} }
} }
hyperlink_id_type
remap_hyperlink_ids(Screen *self, hyperlink_id_type *map) {
#define PROCESS_CELL(cell) { hid = (cell).hyperlink_id; if (hid) { if (!map[hid]) map[hid] = ++num; (cell).hyperlink_id = map[hid]; }}
hyperlink_id_type num = 0, hid;
if (self->historybuf->count) {
for (index_type y = self->historybuf->count; y-- > 0;) {
CPUCell *cells = historybuf_cpu_cells(self->historybuf, y);
for (index_type x = 0; x < self->historybuf->xnum; x++) {
PROCESS_CELL(cells[x]);
}
}
}
LineBuf *second = self->linebuf, *first = second == self->main_linebuf ? self->alt_linebuf : self->main_linebuf;
for (index_type i = 0; i < self->lines * self->columns; i++) {
PROCESS_CELL(first->cpu_cell_buf[i]);
}
for (index_type i = 0; i < self->lines * self->columns; i++) {
PROCESS_CELL(second->cpu_cell_buf[i]);
}
return num;
#undef PROCESS_CELL
}
static bool is_flag_pair(char_type a, char_type b) { static bool is_flag_pair(char_type a, char_type b) {
return is_flag_codepoint(a) && is_flag_codepoint(b); return is_flag_codepoint(a) && is_flag_codepoint(b);
} }
@@ -3467,8 +3443,8 @@ has_selection(Screen *self, PyObject *a UNUSED) {
} }
static PyObject* static PyObject*
hyperlinks_as_list(Screen *self, PyObject *args UNUSED) { hyperlinks_as_set(Screen *self, PyObject *args UNUSED) {
return screen_hyperlinks_as_list(self); return screen_hyperlinks_as_set(self);
} }
static PyObject* static PyObject*
@@ -4802,7 +4778,7 @@ static PyMethodDef methods[] = {
MND(erase_in_display, METH_VARARGS) MND(erase_in_display, METH_VARARGS)
MND(clear_scrollback, METH_NOARGS) MND(clear_scrollback, METH_NOARGS)
MND(scroll_until_cursor_prompt, METH_VARARGS) MND(scroll_until_cursor_prompt, METH_VARARGS)
MND(hyperlinks_as_list, METH_NOARGS) MND(hyperlinks_as_set, METH_NOARGS)
MND(garbage_collect_hyperlink_pool, METH_NOARGS) MND(garbage_collect_hyperlink_pool, METH_NOARGS)
MND(hyperlink_for_id, METH_O) MND(hyperlink_for_id, METH_O)
MND(reverse_scroll, METH_VARARGS) MND(reverse_scroll, METH_VARARGS)

View File

@@ -825,32 +825,32 @@ class TestScreen(BaseTest):
s.draw('hij') s.draw('hij')
self.ae(s.line(1).hyperlink_ids(), (1, 1, 2, 2, 2)) self.ae(s.line(1).hyperlink_ids(), (1, 1, 2, 2, 2))
set_link() set_link()
self.ae([('a:url-a', 1), (':url-b', 2)], s.hyperlinks_as_list()) self.ae({('a:url-a', 1), (':url-b', 2)}, s.hyperlinks_as_set())
s.garbage_collect_hyperlink_pool() s.garbage_collect_hyperlink_pool()
self.ae([('a:url-a', 1), (':url-b', 2)], s.hyperlinks_as_list()) self.ae({('a:url-a', 1), (':url-b', 2)}, s.hyperlinks_as_set())
for i in range(s.lines + 2): for i in range(s.lines + 2):
s.linefeed() s.linefeed()
s.garbage_collect_hyperlink_pool() s.garbage_collect_hyperlink_pool()
self.ae([('a:url-a', 1), (':url-b', 2)], s.hyperlinks_as_list()) self.ae({('a:url-a', 1), (':url-b', 2)}, s.hyperlinks_as_set())
for i in range(s.lines * 2): for i in range(s.lines * 2):
s.linefeed() s.linefeed()
s.garbage_collect_hyperlink_pool() s.garbage_collect_hyperlink_pool()
self.assertFalse(s.hyperlinks_as_list()) self.assertFalse(s.hyperlinks_as_set())
set_link('url-a', 'x') set_link('url-a', 'x')
s.draw('a') s.draw('a')
set_link('url-a', 'y') set_link('url-a', 'y')
s.draw('a') s.draw('a')
set_link() set_link()
self.ae([('x:url-a', 1), ('y:url-a', 2)], s.hyperlinks_as_list()) self.ae({('x:url-a', 1), ('y:url-a', 2)}, s.hyperlinks_as_set())
s = self.create_screen() s = self.create_screen()
set_link('u' * 2048) set_link('u' * 2048)
s.draw('a') s.draw('a')
self.ae([(':' + 'u' * 2045, 1)], s.hyperlinks_as_list()) self.ae({(':' + 'u' * 2045, 1)}, s.hyperlinks_as_set())
s = self.create_screen() s = self.create_screen()
set_link('u' * 2048, 'i' * 300) set_link('u' * 2048, 'i' * 300)
s.draw('a') s.draw('a')
self.ae([('i'*256 + ':' + 'u' * (2045 - 256), 1)], s.hyperlinks_as_list()) self.ae({('i'*256 + ':' + 'u' * (2045 - 256), 1)}, s.hyperlinks_as_set())
s = self.create_screen() s = self.create_screen()
set_link('1'), s.draw('1') set_link('1'), s.draw('1')
@@ -859,19 +859,19 @@ class TestScreen(BaseTest):
s.cursor.x = 1 s.cursor.x = 1
set_link(), s.draw('X') set_link(), s.draw('X')
self.ae(s.line(0).hyperlink_ids(), (1, 0, 3, 0, 0)) self.ae(s.line(0).hyperlink_ids(), (1, 0, 3, 0, 0))
self.ae([(':1', 1), (':2', 2), (':3', 3)], s.hyperlinks_as_list()) self.ae({(':1', 1), (':2', 2), (':3', 3)}, s.hyperlinks_as_set())
s.garbage_collect_hyperlink_pool() s.garbage_collect_hyperlink_pool()
self.ae([(':1', 1), (':3', 2)], s.hyperlinks_as_list()) self.ae({(':1', 1), (':3', 2)}, s.hyperlinks_as_set())
set_link('3'), s.draw('3') set_link('3'), s.draw('3')
self.ae([(':1', 1), (':3', 2)], s.hyperlinks_as_list()) self.ae({(':1', 1), (':3', 2)}, s.hyperlinks_as_set())
set_link('4'), s.draw('4') set_link('4'), s.draw('4')
self.ae([(':1', 1), (':3', 2), (':4', 3)], s.hyperlinks_as_list()) self.ae({(':1', 1), (':3', 2), (':4', 3)}, s.hyperlinks_as_set())
s = self.create_screen() s = self.create_screen()
set_link('1'), s.draw('1') set_link('1'), s.draw('1')
set_link('2'), s.draw('2') set_link('2'), s.draw('2')
set_link('1'), s.draw('1') set_link('1'), s.draw('1')
self.ae([(':2', 2), (':1', 1)], s.hyperlinks_as_list()) self.ae({(':2', 2), (':1', 1)}, s.hyperlinks_as_set())
s = self.create_screen() s = self.create_screen()
set_link('1'), s.draw('12'), set_link(), s.draw('X'), set_link('1'), s.draw('3') set_link('1'), s.draw('12'), set_link(), s.draw('X'), set_link('1'), s.draw('3')