Add performance optimizations to the UI hot paths - #11
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
card_grid()lays out only the rows intersectingthe 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 shiftas rows above the viewport are skipped.
Derived): the lowercase title/body/categorythe 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.
FilterCache), keyed on the query, thecategory 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.egui::Visualsevery frame; apply the theme at startup andwhen the selection changes. Add
Theme::name() -> &'static str(Displaydefers to it) so naming a theme doesn't allocate.
banner text, and the combo-box labels. Move the editor's body into the
snippet on save instead of copying it.
sort the canonical category list once per batch instead of once per snippet at
startup (
Config::add_categories); skip the allocations insame_categoryfor 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