Skip to content

fix: stop image map area ids leaking, without an unbounded obarray - #16

Open
sudo-human wants to merge 1 commit into
chiply:mainfrom
sudo-human:fix/image-cache-leak-area-ids
Open

fix: stop image map area ids leaking, without an unbounded obarray#16
sudo-human wants to merge 1 commit into
chiply:mainfrom
sudo-human:fix/image-cache-leak-area-ids

Conversation

@sudo-human

@sudo-human sudo-human commented Jul 9, 2026

Copy link
Copy Markdown

Problem

svg-margin--cell-area tags each clickable/help hot-spot with a fresh make-symbol area id on every render. Emacs looks images up in its cache with equal, and two uninterned symbols of the same name are never equal — so a byte-identical composite image rebuilt on the next render is a cache miss and is retained for image-cache-eviction-delay (300s by default) instead of reused.

On a buffer with a :help/:action indicator on many lines — e.g. diff-hl on a merge-conflict file, where nearly every line is a hunk — a frequent re-render (idle timer, flydiff, after-change) regenerates hundreds of never-deduped SVG images repeatedly. The image cache climbs steadily until eviction, which presents as a memory leak.

Why it's fiddly — three conflicting constraints on the area id

  1. Must be a symbol — Emacs composes the click event by combining the id with the mouse event (e.g. [ID mouse-1], per Image Descriptors in the Elisp manual).
  2. Must be distinct per cell — a shared id makes mouse-highlight treat two hot-spots as one and stop re-firing help-echo between them.
  3. Must be stable across renders of the same cell — otherwise the composite image is never equal and the cache never hits (the bug above).

make-symbol satisfies (1) and (2) but not (3). Interning the name — even in a private obarray — satisfies all three, but because the name embeds the buffer position, it accumulates a permanent symbol for every position ever drawn. A long session that shifts positions (typing above the indicators, or a growing file) then leaks without bound instead. Interned symbols are never GC'd.

Fix

Memoize an uninterned symbol per cell in a small buffer-local table that is rotated each render (svg-margin--area-rotate, svg-margin--area-id):

  • A cell still present reuses its symbol from the previous generation → image stays equalcache hit.
  • A cell that disappears is left only in the retired table, dropped at the next rotation, and garbage-collected.
  • The live set is bounded by the cells of the last two renders — nothing accumulates.

This keeps all three constraints: symbol type (clicks work), distinct per cell (hover works), stable across renders (cache hits), and now bounded (no leak either way).

Verification

Measured the live area-symbol set across editing patterns (before → after):

Scenario fresh make-symbol private obarray this PR
stable file, 100 renders leaks image cache ok bounded, cache hits
delete lines from end leaks image cache ok bounded
growing file (new positions) leaks image cache unbounded obarray bounded (live ≤ 2 gens)
edit above indicators leaks image cache unbounded obarray bounded (live ≤ 2 gens)

Tests

Added tests asserting the id is a symbol, uninterned in the global obarray, distinct per cell, stable across renders (including across a rotation), and bounded under position churn.

Full suite: 60 passed, 1 skipped (image-builds, needs a GUI/librsvg), 0 unexpected. Byte-compiles with no warnings; checkdoc clean.

@sudo-human

Copy link
Copy Markdown
Author

Hey, this is a vibe coded fix with claude. I don't really have much elsip knowledge apart from write my config 😅

`svg-margin--cell-area' tagged each clickable/help hot-spot with a fresh
`make-symbol' area id on every render. Emacs looks images up in its cache
with `equal', and two uninterned symbols of the same name are never `equal',
so a byte-identical image rebuilt on the next render is a cache MISS and is
retained for `image-cache-eviction-delay' (300s by default).

On a buffer with a `:help'/`:action' indicator on many lines -- e.g. diff-hl
on a merge-conflict file, where nearly every line is a hunk -- a frequent
re-render (idle timer, flydiff, after-change) regenerates hundreds of
never-deduped SVG images repeatedly and the image cache grows until eviction,
which reads as a memory leak.

The area id has three hard constraints, which is what makes this fiddly:
  1. It must be a symbol -- Emacs composes the click event by combining the
     id with the mouse event (e.g. [ID mouse-1]).
  2. It must be distinct per cell -- a shared id makes mouse-highlight treat
     two hot-spots as one and stop re-firing help-echo between them.
  3. It must be the SAME across renders of the same cell -- otherwise the
     composite image is never `equal' and the cache never hits (the bug).
A fresh `make-symbol' satisfies 1 and 2 but not 3; interning the name (even in
a private obarray) satisfies all three but, because the name embeds the buffer
position, accumulates a permanent symbol for every position ever drawn -- so a
long session that shifts positions (typing above the indicators, a growing
file) leaks without bound instead.

Memoize an UNINTERNED symbol per cell in a small buffer-local table that is
rotated each render: a cell still present reuses its symbol from the previous
generation (cache hit), a cell that disappears is dropped at the next rotation
and garbage-collected. The live set is bounded by the cells of the last two
renders, so nothing accumulates.

Tests assert the id is a symbol, uninterned in the global obarray, distinct
per cell, stable across renders, and bounded under position churn.
@sudo-human
sudo-human force-pushed the fix/image-cache-leak-area-ids branch from 02ee941 to 9d7c15d Compare July 9, 2026 11:50
@sudo-human sudo-human changed the title fix: intern image-map area ids so the image cache can dedup them fix: stop image map area ids leaking, without an unbounded obarray Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant