Per-Frame O(N) Cache Elimination

May 18, 2026 · View on GitHub

Eliminates 7 per-frame O(N) operations on tab.content using the existing content_version: u64 counter on Tab for cache invalidation.

Problem

Several code paths scanned the entire tab.content string every frame:

SiteOperationCost (50MB file)
status_bar.rsTextStats::from_text()~25ms (word/char/line counting)
central_panel.rs / mod.rstab.title()is_modified()~15ms (hash or string compare)
mod.rsneeds_cjk() / needs_complex_script_fonts()~10ms (char scan)
mod.rsshould_auto_save()hash_content()~10ms
mod.rsfrontmatter update_from_content() clone + hash~30ms
editor.rsshow_raw_editor() content clone~25ms
editor.rsshow_rendered_editor() content clone~25ms

Solution

Cache Pattern

Each cached value uses a version guard:

if tab.content_version != tab.cached_xxx_version {
    tab.cached_xxx = compute_xxx(&tab.content);
    tab.cached_xxx_version = tab.content_version;
}

content_version increments on undo, redo, and increment_content_version() calls — never during scroll or idle frames.

Cached Fields on Tab (state.rs)

FieldGuardsSource
cached_text_statscached_text_stats_versionTextStats::from_text()
cached_is_modifiedcached_is_modified_version + cached_is_modified_save_versionis_modified_uncached()
cached_needs_cjkcached_needs_cjk_versionfonts::needs_cjk()
cached_needs_complex_scriptcached_needs_complex_script_versionfonts::needs_complex_script_fonts()
last_auto_save_content_versionReplaces hash_content() call

Dual-Version Guard for is_modified

is_modified() depends on both content and save state. It uses two version counters:

  • content_version — changes on edits
  • save_version — changes on mark_saved()

Both must match for the cache to be valid.

Per-Frame Warm-Up

AppState::warm_tab_caches() is called at the start of each update() frame to pre-compute is_modified_cached() for all tabs. This ensures subsequent &self calls to title() and is_modified() are O(1).

MarkdownEditor Clone Elimination

  • Raw editor: Replaced content.clone() + string compare with response.changed() from egui's TextEdit output.
  • Rendered editor: Eliminated the full content clone. rebuild_markdown() already ignores the original parameter. get_focused_element() now uses the current content directly (self-corrects on the next frame if an edit happened).

Frontmatter Panel

Replaced DefaultHasher content hashing with a (tab_id, content_version) cache key. The content clone only happens when the active tab or its content actually changes (not every frame).

Note (v0.3.x): The first cut of this change keyed on content_version alone, which is unsafe because the counter is per-tab and starts at 0 for every new tab — two unedited tabs share key 0 and the panel never re-parses on tab switch. Pairing with the stable Tab.id is required; see docs/technical/ui/frontmatter-panel.md (Caching).

Files Changed

FileChange
src/state.rsAdded 12 cache fields to Tab, is_modified_cached(), text_stats(), needs_cjk_cached(), needs_complex_script_cached(), warm_tab_caches()
src/app/mod.rsCall warm_tab_caches() at frame start; use cached font detection
src/app/status_bar.rsUse tab.text_stats() instead of TextStats::from_text()
src/ui/frontmatter_panel.rsupdate_from_content_versioned(content, tab_id, content_version) with (tab_id, content_version) cache key (initial version used content_version alone — see note above)
src/markdown/editor.rsEliminate content clones in both editor modes