FerriteEditor Memory Optimization

January 26, 2026 · View on GitHub

Overview

This document describes the memory management strategy for the FerriteEditor, particularly for handling large files efficiently.

Performance Achievements

MetricBefore (egui TextEdit)After (FerriteEditor)
30K line file RAM~1.5 GB~90 MB
ResponsivenessLaggySmooth
Tab close cleanupMemory leakedProperly freed

Architecture

Data Storage

The editor uses a rope-based text buffer (ropey::Rope) instead of a flat String:

FerriteEditor
├── buffer: TextBuffer (Rope)     ← O(log n) operations
├── line_cache: LineCache         ← 200 cached galleys max
├── history: EditHistory          ← Operation-based, not snapshots
├── view: ViewState               ← Viewport tracking
└── selections: Vec<Selection>    ← Cursor positions

Memory-Efficient Patterns

  1. Virtual Scrolling: Only visible lines are rendered each frame
  2. Galley Caching: LRU cache of 200 text layouts, auto-evicted
  3. Operation-Based Undo: Stores edit operations, not full content copies
  4. Viewport-Aware Features: Syntax highlighting, search, etc. only process visible content

Tab Closure Cleanup

When a tab is closed, cleanup_ferrite_editor() is called to free memory:

// In src/editor/widget.rs
pub fn cleanup_ferrite_editor(ctx: &egui::Context, tab_id: usize) {
    ctx.data_mut(|data| {
        let storage = data.get_temp_mut_or_default::<FerriteEditorStorage>(egui::Id::NULL);
        if let Some(mut editor) = storage.editors.remove(&tab_id) {
            // Explicitly clear large data structures before drop
            editor.line_cache.invalidate();
            editor.search_matches.clear();
            editor.search_matches.shrink_to_fit();
            // editor drops here, freeing TextBuffer and remaining fields
        }
        storage.content_hashes.remove(&tab_id);
        storage.content_lengths.remove(&tab_id);
    });
}

Cleanup Call Sites

The cleanup is triggered from cleanup_tab_state() in app.rs:

  • Tab close button clicked
  • Ctrl+W keyboard shortcut
  • File deletion in file tree
  • Unsaved changes dialog (discard)

Debug vs Release Performance

Critical: Always use --release builds for performance testing.

AspectDebug BuildRelease Build
OptimizationNone (-O0)Maximum (-O3)
Bounds checksEvery accessOptimized away
Typical speed1x10-100x faster

Debug builds have significant overhead from:

  • Bounds checking on every array/rope access
  • Debug assertions in egui and ropey
  • No inlining of hot paths
  • Overflow checking on math operations

Memory Behavior Notes

Some memory retention after closing large files is expected:

  1. Allocator pooling: Windows/Linux allocators keep freed memory for reuse
  2. egui caches: Font atlas, texture atlas persist
  3. Fragmentation: Small allocations between freed blocks

This is not a leak - opening/closing the same file repeatedly should NOT cause unbounded growth.

  • src/editor/widget.rs - EditorWidget, FerriteEditorStorage, cleanup
  • src/editor/ferrite/buffer.rs - TextBuffer (Rope wrapper)
  • src/editor/ferrite/line_cache.rs - Galley caching with LRU eviction
  • src/editor/ferrite/history.rs - Operation-based undo/redo
  • src/app.rs - Tab lifecycle, cleanup_tab_state()

Complexity Tiers

See docs/technical/editor/architecture.md for the full complexity tier system:

TierPer-Frame OK?Examples
O(1)Alwaysline_count(), is_dirty()
O(log N)Alwaysget_line(), rope ops
O(visible)YesRender visible lines
O(N)User-initiated onlyFind All, Save