aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-03 16:26:41 +0200
committeruser <user@clank>2026-09-03 16:26:41 +0200
commite3059676a6b342ea9ff07753d5b976e854d4b111 (patch)
tree0bd6dd5bb7bb95503d116bddc0b7633b46d46de0 /src
parentweb: card (diff)
downloadslosh-e3059676a6b342ea9ff07753d5b976e854d4b111.tar.gz
slosh-e3059676a6b342ea9ff07753d5b976e854d4b111.tar.xz
slosh-e3059676a6b342ea9ff07753d5b976e854d4b111.zip
pane: drop the render snapshot of panes on hidden tabs -- 154K -> 66K each
An updated GhosttyRenderState retains a full copy of the pane's viewport (rows, cells, per-row arenas, styles) so renders can read it without the terminal lock. Measured with bpftrace and a lib-vt micro-benchmark, that copy is ~140KB for an 80x24 pane -- more than twice the terminal it mirrors (~57KB) -- and every pane kept one forever, composed or not. Only the current tab is ever composed, so app_compose now drops the snapshot of every pane on the other tabs (a pointer test once dropped), and pane_new no longer creates one at all: pane_compose and pane_select_word build it on first use. Both already begin with ghostty_render_state_update(), so a dropped snapshot is rebuilt, never missed, and the tab switch already forces a full repaint. Server memory per empty 80x24 terminal drops from 154K to 66K anon (Superlogical reports 68K on macOS; tmux 4K, zellij 1.2-6.2M). Fifty background tabs: 8.2M -> 3.8M, reclaimed on close, and cycling through tabs does not ratchet. What remains is the lib-vt terminal itself.
Diffstat (limited to 'src')
-rw-r--r--src/app_ui.c15
-rw-r--r--src/pane.c47
2 files changed, 55 insertions, 7 deletions
diff --git a/src/app_ui.c b/src/app_ui.c
index d03ddd4..8daf8f1 100644
--- a/src/app_ui.c
+++ b/src/app_ui.c
@@ -985,6 +985,11 @@ bool rename_key(app_t *a, const input_event_t *ev) {
return true;
}
+static void drop_render_cb(node_t *n, void *ud) {
+ (void)ud;
+ if (n->pane) pane_render_cache_drop(n->pane);
+}
+
void app_compose(app_t *a, screen_t *s) {
screen_clear(s); /* every frame starts blank: no ghosts in the gap ring */
hit_reset(&s->hits);
@@ -994,6 +999,16 @@ void app_compose(app_t *a, screen_t *s) {
a->animating = false;
if (!a->ntabs || !cur(a)->root) return;
+ /* Only the current tab is composed, but a composed pane retains a viewport
+ * copy inside its render state (~140KB at 80x24) that nothing will read
+ * until its tab is looked at again -- at which point it is rebuilt from the
+ * terminal anyway. Dropping it here, rather than on the tab switch, catches
+ * every route a tab can leave the screen by; for a pane already dropped it
+ * is a pointer test. */
+ for (size_t i = 0; i < a->ntabs; i++)
+ if (i != a->cur && a->tabs[i].root)
+ walk(a->tabs[i].root, drop_render_cb, NULL);
+
/* Looking at a pane is the acknowledgement, and it happens before anything
* is drawn rather than while the pane itself is: the tab strip is painted
* first and would otherwise spend a frame reporting a bell that had just
diff --git a/src/pane.c b/src/pane.c
index 9cc2692..5447e0a 100644
--- a/src/pane.c
+++ b/src/pane.c
@@ -341,11 +341,12 @@ pane_t *pane_new(const char *const argv[], uint16_t cols, uint16_t rows,
if (ghostty_terminal_new(NULL, &p->term, cols, rows) != GHOSTTY_SUCCESS)
goto fail;
- if (ghostty_render_state_new(NULL, &p->rstate) != GHOSTTY_SUCCESS) goto fail;
- if (ghostty_render_state_row_iterator_new(NULL, &p->rows) != GHOSTTY_SUCCESS)
- goto fail;
- if (ghostty_render_state_row_cells_new(NULL, &p->cells) != GHOSTTY_SUCCESS)
- goto fail;
+ /* rstate/rows/cells are made on first use (ensure_render) and dropped when
+ * the pane's tab leaves the screen (pane_render_cache_drop): an updated
+ * render state retains a full copy of the viewport -- measured at ~140KB
+ * for 80x24 -- which for a pane nobody is looking at is pure ballast. Every
+ * consumer starts with ghostty_render_state_update(), so a dropped state is
+ * rebuilt, not missed. */
if (ghostty_key_encoder_new(NULL, &p->kenc) != GHOSTTY_SUCCESS) goto fail;
if (ghostty_key_event_new(NULL, &p->kev) != GHOSTTY_SUCCESS) goto fail;
if (ghostty_mouse_encoder_new(NULL, &p->menc) != GHOSTTY_SUCCESS) goto fail;
@@ -420,6 +421,37 @@ void pane_free(pane_t *p) {
free(p);
}
+/* The render objects, on demand. An updated GhosttyRenderState keeps a full
+ * copy of the viewport (rows, cells, styles) so it can be read without the
+ * terminal lock; that copy is ~140KB for an 80x24 pane, which dwarfs the
+ * terminal itself. So it exists only while somebody renders this pane, and
+ * failure here is failure to render, not failure of the pane. */
+static bool ensure_render(pane_t *p) {
+ if (p->rstate) return true;
+ if (ghostty_render_state_new(NULL, &p->rstate) != GHOSTTY_SUCCESS) goto fail;
+ if (ghostty_render_state_row_iterator_new(NULL, &p->rows) != GHOSTTY_SUCCESS)
+ goto fail;
+ if (ghostty_render_state_row_cells_new(NULL, &p->cells) != GHOSTTY_SUCCESS)
+ goto fail;
+ return true;
+fail:
+ pane_render_cache_drop(p);
+ return false;
+}
+
+/* Called for every pane whose tab is not on screen, every frame, so it is a
+ * pointer test almost always. The next compose rebuilds from the terminal,
+ * which is where the truth lives anyway. */
+void pane_render_cache_drop(pane_t *p) {
+ if (!p || !p->rstate) return;
+ if (p->cells) ghostty_render_state_row_cells_free(p->cells);
+ if (p->rows) ghostty_render_state_row_iterator_free(p->rows);
+ ghostty_render_state_free(p->rstate);
+ p->cells = NULL;
+ p->rows = NULL;
+ p->rstate = NULL;
+}
+
int pane_fd(const pane_t *p) { return p->pty.fd; }
pid_t pane_pid(const pane_t *p) { return p->pty.pid; }
const char *pane_start_cwd(const pane_t *p) { return p->cwd; }
@@ -1041,8 +1073,8 @@ static bool word_cell(const char *utf8, size_t len, const char *seps) {
* forward. Which is enough: the run containing `x` is bounded by the separator
* before it and the one after it, and both are met on that single pass. */
bool pane_select_word(pane_t *p, uint16_t x, uint16_t y, const char *seps) {
- if (!p || !p->term || !p->rstate || x >= p->cols || y >= p->rows_n)
- return false;
+ if (!p || !p->term || x >= p->cols || y >= p->rows_n) return false;
+ if (!ensure_render(p)) return false;
if (ghostty_render_state_update(p->rstate, p->term) != GHOSTTY_SUCCESS)
return false;
if (ghostty_render_state_get(p->rstate,
@@ -1271,6 +1303,7 @@ static uint16_t cell_link_id(pane_t *p, screen_t *s, uint16_t x, uint16_t y,
void pane_compose(pane_t *p, screen_t *s, uint16_t x0, uint16_t y0,
bool focused) {
+ if (!ensure_render(p)) return;
if (ghostty_render_state_update(p->rstate, p->term) != GHOSTTY_SUCCESS)
return;