Skip to content

Optimize rendering: precompute FileEntry fields, reduce allocations#45

Merged
leszek3737 merged 3 commits into
mainfrom
fix33
May 18, 2026
Merged

Optimize rendering: precompute FileEntry fields, reduce allocations#45
leszek3737 merged 3 commits into
mainfrom
fix33

Conversation

@leszek3737

@leszek3737 leszek3737 commented May 18, 2026

Copy link
Copy Markdown
Owner
  • Precompute time_str, size_str, name_width in FileEntry at load time
  • Cache function bar labels as const [&str; 10] instead of format!() per frame
  • Remove .into_owned() from viewer render and search loops
  • Use memchr for line offset computation (~10x faster)
  • Cap viewer visual lines at 1M to prevent unbounded memory
  • ASCII fast-path for viewer search (byte-level when line is ASCII)
  • Fix ASCII fast-path bug: check line.is_ascii() not query.is_ascii()
  • Move path.clone() after guard in sync_watcher_paths
  • Cache hotlist/user-menu picker strings, rebuild on mutation
  • Fix missing rebuild_hotlist_cache() in config.rs apply_to_state()
  • to_string_lossy().into_owned() instead of .to_string()
  • NatKeySegment Vec -> Box<[u8]> to eliminate excess capacity
  • name_key: skip clone when name is already lowercase
  • format_time: Cow -> String (callers always owned)
  • Remove dead display_line field from FileEntry

Summary by Sourcery

Optimize UI rendering and viewer performance by precomputing and caching frequently used values, while tightening memory usage and synchronization behavior.

Bug Fixes:

  • Fix viewer ASCII search path by checking whether lines are ASCII rather than the search query.
  • Ensure viewer search results are accumulated locally before assigning to state to avoid inconsistent match indexing.
  • Guard watcher synchronization against redundant updates by comparing paths before cloning.
  • Keep hotlist and user-menu picker caches in sync when configuration or user actions modify underlying collections.
  • Initialize synthetic ".." directory entries with valid precomputed display fields to avoid uninitialized formatting.

Enhancements:

  • Precompute file size, time, and name width in FileEntry and reuse them in panel rendering.
  • Cache function-bar key and label texts, directory hotlist strings, and user menu strings instead of rebuilding them each frame.
  • Use memchr-based line offset computation and an ASCII fast path in the viewer to speed up search and navigation.
  • Limit viewer visual line computation to a maximum of 1,000,000 lines to bound memory usage.
  • Reduce allocations in natural sort key segments by storing bytes in boxed slices and avoid unnecessary clones in name-based sorting.
  • Centralize time formatting in app types and simplify its return type to a plain String.

Build:

  • Add the memchr crate dependency for optimized byte searching.

Tests:

  • Update panel time-formatting tests and natural sort tests to reflect new return types and storage representations.

…cations

- Precompute time_str, size_str, name_width in FileEntry at load time
- Cache function bar labels as const [&str; 10] instead of format!() per frame
- Remove .into_owned() from viewer render and search loops
- Use memchr for line offset computation (~10x faster)
- Cap viewer visual lines at 1M to prevent unbounded memory
- ASCII fast-path for viewer search (byte-level when line is ASCII)
- Fix ASCII fast-path bug: check line.is_ascii() not query.is_ascii()
- Move path.clone() after guard in sync_watcher_paths
- Cache hotlist/user-menu picker strings, rebuild on mutation
- Fix missing rebuild_hotlist_cache() in config.rs apply_to_state()
- to_string_lossy().into_owned() instead of .to_string()
- NatKeySegment Vec<u8> -> Box<[u8]> to eliminate excess capacity
- name_key: skip clone when name is already lowercase
- format_time: Cow -> String (callers always owned)
- Remove dead display_line field from FileEntry
@sourcery-ai

sourcery-ai Bot commented May 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Optimizes rendering and search performance by precomputing per-file display fields, caching frequently built UI strings, reducing allocations and clones across panels, viewer, sort, and pickers, and adds safety and speed improvements to viewer line handling and natural sort keys.

Sequence diagram for hotlist picker flow with cached strings

sequenceDiagram
  actor User
  participant Pickers as pickers
  participant AppState as state
  participant Render as render
  participant Dialogs as dialogs

  User->>Pickers: handle_hotlist_picker(state, key, len)
  alt add_current_directory
    Pickers->>AppState: directory_hotlist.push(cur)
    Pickers->>AppState: rebuild_hotlist_cache()
  else delete_selected_directory
    Pickers->>AppState: directory_hotlist.remove(picker_selected)
    Pickers->>AppState: rebuild_hotlist_cache()
  end

  User->>Render: render_list_picker_overlay(f, area, colors, state)
  Render->>AppState: read cached_hotlist_strings
  Render->>Dialogs: render_list_picker_with_colors(f, title, cached_hotlist_strings, selected, hints, colors)
Loading

File-Level Changes

Change Details Files
Precompute and store display-related fields on FileEntry and update all constructors and consumers accordingly.
  • Extend FileEntry with time_str, size_str, and name_width fields to hold preformatted metadata and display width.
  • Move format_time implementation to app::types returning owned String and use it from FileEntryBuilder and fs::reader::build_file_entry.
  • Update FileEntryBuilder, read_directory(..), and tests/mocks to populate the new fields, including the synthetic ".." entry and mouse tests.
  • Modify panel line formatting functions to use entry.size_str, entry.time_str, and entry.name_width instead of recomputing size/time and string widths.
src/app/types.rs
src/fs/reader.rs
src/ui/panels/mod.rs
src/input/mouse.rs
src/ui/panels/tests.rs
Cache static UI strings for function bar and picker dialogs to avoid per-frame or per-open allocations.
  • Introduce FN_KEY_TEXTS and FN_LABEL_TEXTS constant arrays and switch function bar rendering to use &str constants instead of format! calls.
  • Add cached_hotlist_strings and cached_user_menu_strings to AppState, initialize them, and provide rebuild_hotlist_cache and rebuild_user_menu_cache helpers.
  • Change hotlist and user-menu picker rendering to use the cached string vectors instead of rebuilding Strings every frame.
  • Ensure hotlist/user menu caches are rebuilt whenever the underlying collections mutate (pickers, quick_cd, config apply, menu open).
src/ui/panels/mod.rs
src/app/types.rs
src/render.rs
src/input/pickers.rs
src/app/config.rs
src/input/dialogs.rs
src/input/menu_actions.rs
Optimize viewer internals: faster line offset computation, reduced allocations during search, ASCII fast-path fix, and safe cap on visual lines.
  • Replace manual newline scan in ViewerState::compute_line_offsets with memchr::memchr_iter for ~10x faster line offset computation and add memchr dependency.
  • Avoid cloning lines in the search loop by using get_line(..) by reference and accumulating matches into local vectors before assigning to self.search_matches and self.search_matches_by_line.
  • Add an ASCII fast-path for search positioning: when the line is ASCII, compute char positions and match lengths directly from byte indices, and fix the bug by checking line.is_ascii() rather than query.is_ascii().
  • Introduce MAX_VISUAL_LINES (1_000_000) when computing visual_heights; if line_count exceeds the cap, clear visual_heights and return early to prevent unbounded memory usage.
src/ui/viewer.rs
Cargo.toml
Reduce allocations and capacity overhead in sorting and string conversions.
  • Change NatKeySegment::Text/Num to hold Box<[u8]> instead of Vec and adjust natsort_key construction and tests accordingly, eliminating excess capacity.
  • Optimize name_key for case-insensitive sorting by skipping the backup-clone when the lowercase name equals the original.
  • Use to_string_lossy().into_owned() instead of to_string_lossy().to_string() for user and group name lookups to avoid an extra allocation.
  • Remove dead or unused fields and types, such as the display_line field from FileEntry and the previous Cow-based format_time API.
src/ops/natsort.rs
src/ops/sorting.rs
src/fs/reader.rs
src/app/types.rs
Small behavioral and correctness fixes around watcher sync and state initialization.
  • Delay cloning panel paths in sync_watcher_paths until after the last_synced guard to avoid unnecessary clones.
  • Initialize AppState::directory_hotlist with a cloned current_dir and cached_hotlist_strings with its display string, aligning with new caches.
  • Update tests expecting format_time to now assert on a non-empty String instead of a Cow.
src/app/watcher_sync.rs
src/app/types.rs
src/ui/panels/tests.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The cached fields on FileEntry (time_str, size_str, name_width) are now computed in multiple places (FileEntryBuilder::build, build_file_entry, manual construction in tests, read_directory for ..); consider centralizing this logic (e.g., a constructor/helper on FileEntry) to avoid drift if the formatting or width rules change.
  • The hotlist/user-menu string caches (cached_hotlist_strings, cached_user_menu_strings) depend on callers remembering to invoke rebuild_*_cache; it may be safer to encapsulate all mutations of directory_hotlist and user_menu_entries behind helper methods that update the caches automatically to prevent future inconsistencies.
  • The 1M cap on visual_heights in the viewer (MAX_VISUAL_LINES) clears the vector and returns early when exceeded, effectively disabling visual layout; consider either surfacing this state (e.g., a flag or message) or documenting the behavior so it’s clear how very large files are handled and that this is an intentional fallback.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The cached fields on `FileEntry` (`time_str`, `size_str`, `name_width`) are now computed in multiple places (`FileEntryBuilder::build`, `build_file_entry`, manual construction in tests, `read_directory` for `..`); consider centralizing this logic (e.g., a constructor/helper on `FileEntry`) to avoid drift if the formatting or width rules change.
- The hotlist/user-menu string caches (`cached_hotlist_strings`, `cached_user_menu_strings`) depend on callers remembering to invoke `rebuild_*_cache`; it may be safer to encapsulate all mutations of `directory_hotlist` and `user_menu_entries` behind helper methods that update the caches automatically to prevent future inconsistencies.
- The 1M cap on `visual_heights` in the viewer (`MAX_VISUAL_LINES`) clears the vector and returns early when exceeded, effectively disabling visual layout; consider either surfacing this state (e.g., a flag or message) or documenting the behavior so it’s clear how very large files are handled and that this is an intentional fallback.

## Individual Comments

### Comment 1
<location path="src/ui/viewer.rs" line_range="637-640" />
<code_context>
             0
         };
         let width = content_width.max(1);
+        const MAX_VISUAL_LINES: usize = 1_000_000;
         self.visual_heights = (0..self.line_count)
             .map(|i| {
</code_context>
<issue_to_address>
**issue (bug_risk):** Clear `visual_offsets` as well when bailing out on very large files

When `line_count` exceeds `MAX_VISUAL_LINES`, we clear `visual_heights` and return, but leave `visual_offsets` unchanged. This leaves stale offsets alongside an empty heights vector, which can desynchronize viewer state and cause incorrect scrolling or even panics if callers assume both vectors stay aligned. Please also clear `visual_offsets` (and any related layout state) in this early return path to keep the structures consistent.
</issue_to_address>

### Comment 2
<location path="src/app/types.rs" line_range="611-617" />
<code_context>
         self
     }
     pub fn build(self) -> FileEntry {
+        let time_str = format_time(self.cha.mtime().unwrap_or(std::time::UNIX_EPOCH));
+        let size_str = if self.cha.is_dir() {
+            "     <DIR>".to_string()
+        } else {
+            format!("{:>10}", format_size(self.cha.len()))
+        };
+        let name_width = UnicodeWidthStr::width(self.name.as_str());
         FileEntry {
             name: self.name,
</code_context>
<issue_to_address>
**suggestion:** Avoid duplicating `FileEntry` formatting logic between the builder and `fs::reader`

`time_str`, `size_str`, and `name_width` are computed here in `FileEntryBuilder::build` and similarly in `fs::reader::build_file_entry`. This duplication risks the two code paths drifting (e.g., one getting formatting or width updates while the other doesn’t). Consider extracting a shared helper (such as a `FileEntry::with_cached_fields(...)` constructor or standalone function) that takes `cha` and `name` and populates these derived fields in one place.

Suggested implementation:

```rust
    pub fn build(self) -> FileEntry {
        let (time_str, size_str, name_width) = FileEntry::cached_fields(
            self.cha.mtime().unwrap_or(std::time::UNIX_EPOCH),
            self.cha.is_dir(),
            self.cha.len(),
            self.name.as_str(),
        );

        FileEntry {
            name: self.name,
            path: self.path,
            group: self.group,
            selected: self.selected,
            mime_type: self.mime_type,
            time_str,
            size_str,
            name_width,
        }
    }
}

impl FileEntry {
    pub fn cached_fields(
        mtime: std::time::SystemTime,
        is_dir: bool,
        len: u64,
        name: &str,
    ) -> (String, String, usize) {
        let time_str = format_time(mtime);

        let size_str = if is_dir {
            "     <DIR>".to_string()
        } else {
            format!("{:>10}", format_size(len))
        };

        let name_width = UnicodeWidthStr::width(name);

        (time_str, size_str, name_width)
    }
}

```

To fully remove the duplication you mentioned, you should also update `fs::reader::build_file_entry` (or any other place that manually computes `time_str`, `size_str`, and `name_width`) to call:

```rust
let (time_str, size_str, name_width) = FileEntry::cached_fields(
    cha.mtime().unwrap_or(std::time::UNIX_EPOCH),
    cha.is_dir(),
    cha.len(),
    name.as_str(),
);
```

and then use these values instead of recomputing them locally.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/ui/viewer.rs
Comment thread src/app/types.rs Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several performance optimizations and structural improvements, such as pre-calculating display strings and widths for FileEntry, implementing string caching for the hotlist and user menu, and utilizing the memchr crate for faster line offset computations. Additionally, NatKeySegment was refactored to use boxed slices for better memory efficiency, and a safety limit was added to visual height calculations in the viewer. Feedback from the reviewer identifies critical compilation errors where struct fields were incorrectly accessed as methods in src/app/types.rs and src/fs/reader.rs. There is also a concern regarding an optimization in the sorting logic that alters tie-breaking behavior for case-insensitive matches, potentially affecting the expected sort order.

Comment thread src/app/types.rs Outdated
Comment thread src/app/types.rs Outdated
Comment thread src/fs/reader.rs Outdated
Comment thread src/fs/reader.rs Outdated
Comment thread src/ops/sorting.rs
@leszek3737
leszek3737 merged commit 78d179f into main May 18, 2026
3 checks passed
@leszek3737
leszek3737 deleted the fix33 branch May 18, 2026 05:50
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