Skip to content

Add performance optimizations to the UI hot paths - #11

Merged
quantdale merged 1 commit into
mainfrom
claude/performance-optimizations-6hcku0
Jul 25, 2026
Merged

quantdale merged 1 commit into
mainfrom
claude/performance-optimizations-6hcku0

Conversation

@quantdale

Copy link
Copy Markdown
Owner

The whole UI is rebuilt inside update(), which egui runs on every repaint —
including every mouse move — so per-frame work scaled with the size of the
snippet library instead of with what is on screen. Measured on a 1000-snippet
library (release build, 2 KB bodies), one grid frame took ~65 ms; it now takes
~2.4 ms, and the search filter went from ~2 ms/frame to ~5 us/frame.

  • Virtualize the card grid. card_grid() lays out only the rows intersecting
    the viewport (plus a row of overscan) and reserves the height of the skipped
    rows, so the scrollbar and every card position are unchanged. Card rects for
    drag-and-drop now come from grid_card_rect(), computed from the grid origin,
    so a drop onto a gap that was never rendered still lands in the right slot.
    Row widget ids are keyed on the row index (push_id) so they no longer shift
    as rows above the viewport are skipped.
  • Cache per-snippet derived text (Derived): the lowercase title/body/category
    the search matches against and the collapsed card preview. These were
    recomputed from scratch on every frame, lowercasing every body in the library
    and re-collapsing every visible preview.
  • Memoize the visible-card index list (FilterCache), keyed on the query, the
    category filter, and a library generation counter, reusing its allocation
    across frames. All snippet mutations now run through snippets_changed(),
    which is what keeps the caches consistent with snippets.
  • Stop rebuilding egui::Visuals every frame; apply the theme at startup and
    when the selection changes. Add Theme::name() -> &'static str (Display
    defers to it) so naming a theme doesn't allocate.
  • Drop per-frame clones: the category list handed to the editor, the save-error
    banner text, and the combo-box labels. Move the editor's body into the
    snippet on save instead of copying it.
  • Stop collapsing a whole snippet body just to preview its first 220 chars, and
    sort the canonical category list once per batch instead of once per snippet at
    startup (Config::add_categories); skip the allocations in same_category
    for the ASCII names it is called with.

Tests cover the visible-row range, computed-vs-rendered card rects, the paint
work of a ten-times-larger library, and the memoized filter against a fresh
scan; docs record the new invariants.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01Fbmt6KmCj5NsuPVQLvnt8J

The whole UI is rebuilt inside `update()`, which egui runs on every repaint —
including every mouse move — so per-frame work scaled with the size of the
snippet library instead of with what is on screen. Measured on a 1000-snippet
library (release build, 2 KB bodies), one grid frame took ~65 ms; it now takes
~2.4 ms, and the search filter went from ~2 ms/frame to ~5 us/frame.

- Virtualize the card grid. `card_grid()` lays out only the rows intersecting
  the viewport (plus a row of overscan) and reserves the height of the skipped
  rows, so the scrollbar and every card position are unchanged. Card rects for
  drag-and-drop now come from `grid_card_rect()`, computed from the grid origin,
  so a drop onto a gap that was never rendered still lands in the right slot.
  Row widget ids are keyed on the row index (`push_id`) so they no longer shift
  as rows above the viewport are skipped.
- Cache per-snippet derived text (`Derived`): the lowercase title/body/category
  the search matches against and the collapsed card preview. These were
  recomputed from scratch on every frame, lowercasing every body in the library
  and re-collapsing every visible preview.
- Memoize the visible-card index list (`FilterCache`), keyed on the query, the
  category filter, and a library generation counter, reusing its allocation
  across frames. All snippet mutations now run through `snippets_changed()`,
  which is what keeps the caches consistent with `snippets`.
- Stop rebuilding `egui::Visuals` every frame; apply the theme at startup and
  when the selection changes. Add `Theme::name() -> &'static str` (`Display`
  defers to it) so naming a theme doesn't allocate.
- Drop per-frame clones: the category list handed to the editor, the save-error
  banner text, and the combo-box labels. Move the editor's body into the
  snippet on save instead of copying it.
- Stop collapsing a whole snippet body just to preview its first 220 chars, and
  sort the canonical category list once per batch instead of once per snippet at
  startup (`Config::add_categories`); skip the allocations in `same_category`
  for the ASCII names it is called with.

Tests cover the visible-row range, computed-vs-rendered card rects, the paint
work of a ten-times-larger library, and the memoized filter against a fresh
scan; docs record the new invariants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fbmt6KmCj5NsuPVQLvnt8J
@quantdale
quantdale merged commit 2eb1905 into main Jul 25, 2026
5 checks passed
@quantdale
quantdale deleted the claude/performance-optimizations-6hcku0 branch July 25, 2026 14:08
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.

2 participants