Skip to content

Optimize panel refresh and hot paths#46

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

Optimize panel refresh and hot paths#46
leszek3737 merged 3 commits into
mainfrom
opti

Conversation

@leszek3737

@leszek3737 leszek3737 commented May 18, 2026

Copy link
Copy Markdown
Owner

Summary

Optimize panel refresh, comparisons, search, and viewer hot paths while preserving panel focus and improving case-insensitive sorting behavior.

Enhancements:

  • Adjust case-insensitive name sorting and tie-breaking, updating tests to cover mixed-case and duplicate names.
  • Optimize directory comparison by reducing allocations and intermediate maps when computing differences.
  • Introduce a centralized panel-visible-entries rebuild helper and use it from search, menu, dialogs, and watcher-sync paths to reuse cached data, preserve cursor focus, and avoid unnecessary disk refreshes.
  • Improve viewer performance by basing visual row calculations on precomputed offsets and using memchr-based search in the viewer buffer.
  • Speed up file search by tuning buffering and using memchr/memmem for binary detection and case-sensitive pattern matching, skipping non-matching lines early.
  • Optimize chunked copy performance and progress reporting via larger buffers and throttled, byte-based progress checks.
  • Reduce allocations and small inefficiencies in panel suffix construction, help dialog rendering, path index building, and related UI code.

Tests:

  • Add tests to ensure search Enter behavior preserves current entry focus and refreshes from disk only when cached entries are dirty.
  • Add menu tests to verify sort and filter-reset actions keep the current entry focused.

@sourcery-ai

sourcery-ai Bot commented May 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors several hot paths (sorting, search, panel refresh, viewer, compare, copy, search I/O, dialogs) to reduce allocations, avoid unnecessary full-panel refreshes, and preserve cursor/focus, while tightening tests and adding memchr-based byte searching and better watcher sync behavior.

Sequence diagram for optimized panel refresh on search/filter changes

sequenceDiagram
    actor User
    participant ModeDispatch as input_mode_dispatch
    participant AppState
    participant PanelOps as panel_ops

    User->>ModeDispatch: clear_search_state(state)
    ModeDispatch->>AppState: mutate active_panel.filter = None
    ModeDispatch->>PanelOps: refresh_active(state)
    Note over PanelOps: Only if panel.unfiltered_dirty || unfiltered_entries.is_empty()

    User->>ModeDispatch: handle_search_mode(state, key, terminal_height)
    ModeDispatch->>AppState: update state.search_query
    ModeDispatch->>AppState: panel.filter = Some(query)
    alt panel.unfiltered_dirty || unfiltered_entries.is_empty()
        ModeDispatch->>PanelOps: refresh_active(state)
    else reuse_unfiltered_entries
        ModeDispatch->>PanelOps: rebuild_visible_entries(panel)
    end
Loading

Flow diagram for panel_ops::rebuild_visible_entries hot path

flowchart TD
    A["rebuild_visible_entries(panel)"] --> B["sync_unfiltered_selection(panel)"]
    B --> C["current_panel_entry_name(panel)"]
    C --> D["filtered_sorted_entries(
        &panel.unfiltered_entries,
        panel.filter,
        panel.sort_mode,
        panel.sort_options,
        panel.show_hidden
    )"]
    D --> E["panel.entries = result"]
    E --> F["panel.recalculate_selection_stats()"]
    F --> G["restore_panel_cursor(panel, current_name)"]
    G --> H["panel.ensure_cursor_visible(
        current_visible_height()
    )"]
Loading

File-Level Changes

Change Details Files
Replace tuple-based sort_by_cached_key with comparator-based sort and unify case-insensitive name comparison to reduce allocations and clarify ordering semantics.
  • Change size/mtime/btime sort modes to use entries.sort_by with chained cmp/then_with instead of sort_by_cached_key tuples.
  • Remove name_key helper and introduce cmp_name_insensitive implementing two-step lowercase+tie-break comparison.
  • Update name sort test expectations and add a lowercase/uppercase tie case ensuring cmp_name_insensitive behavior is covered.
src/ops/sorting.rs
Optimize search mode and menu/filter panel refresh to reuse cached unfiltered entries, rebuild visible entries in-place, and preserve current entry focus.
  • Introduce panel_ops::rebuild_visible_entries that filters/sorts from unfiltered_entries, preserves selection and cursor, and ensures visibility.
  • Replace various refresh_active calls in search handling, dialogs, menu actions, and Ctrl-; toggles with rebuild_visible_entries when only filter/sort/view changes.
  • Adjust clear_search_state/initiate_search/handle_search_mode to refresh from disk only when unfiltered cache is dirty or empty, otherwise just rebuild visible entries.
  • Modify watcher_sync::rebuild_visible_entries to delegate to panel_ops::rebuild_visible_entries while preserving current-entry restoration.
  • Add tests covering search enter preserving focus, refreshing when unfiltered cache is dirty, and menu sort/reset filter preserving focus.
src/app/panel_ops.rs
src/input/mode_dispatch.rs
src/input/dialogs.rs
src/input/menu_actions.rs
src/app/watcher_sync.rs
src/tests/search.rs
src/tests/menu.rs
src/main.rs
Tighten watcher job syncing and panel refresh behavior in the main loop to avoid redundant refreshes.
  • Change run_app to always call sync_watcher_job_state after job polling without triggering an extra full refresh_both on resume; instead rely on marking dirty for a single refresh in the main loop.
  • Slightly reorder terminal.size() acquisition in dispatch_key_event to occur only after validating key kind/repeat handling.
src/main.rs
Optimize viewer line/row mapping and searching to use precomputed offsets and memchr/memmem for byte search.
  • Use visual_offsets.last() to compute total_visual_rows instead of summing visual_heights.
  • Change logical_to_visual_row to use visual_offsets for O(1) mapping with fallback to total_visual_rows when out-of-bounds.
  • Replace manual byte search in find_bytes with memchr::memchr for single bytes and memchr::memmem::find for multi-byte needles.
src/ui/viewer.rs
Improve file search performance by reusing buffers, using memchr/memmem for scanning, and short‑circuiting non‑matches in case-sensitive mode.
  • Construct BufReader with a fixed MAX_CONTENT_LINE_BYTES capacity instead of default.
  • Replace line.contains(&0) with memchr(0, line) for binary detection.
  • Add a fast path in case-sensitive search that skips UTF-8 decoding when memmem::find does not find the pattern in the line.
src/ops/search.rs
Reduce allocations and improve micro-efficiency in UI helpers and path indexing.
  • Pre-reserve panel.path_index capacity from unfiltered_entries length before filling it.
  • Avoid intermediate Vec allocation by passing message directly to Paragraph::new in help dialog rendering.
  • Preallocate String capacities and use write! to construct status suffix strings efficiently, and avoid an extra size_str temporary in status bar metadata formatting.
src/fs/reader.rs
src/ui/dialogs.rs
src/ui/panels/mod.rs
Reduce overhead of compare and copy operations by avoiding intermediate maps, pre-sizing collections, and throttling progress updates by bytes and time.
  • Refactor compare_entries to build only right_meta, iterate left entries directly while tracking differing/unique stats and seen_right, and pre-size HashSets for marks.
  • Increase chunk_copy buffer size to 256 KiB, introduce PROGRESS_CHECK_BYTES and bytes_since_progress_check to limit progress_tx sends based on both bytes and time thresholds.
src/ops/compare.rs
src/ops/chunk_copy.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

@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 centralizes panel entry management through a new rebuild_visible_entries function and introduces several performance optimizations across the codebase, including the use of memchr for searching, pre-allocating collection capacities, and reducing redundant system calls in the copy and UI logic. Feedback identifies a performance regression in the case-insensitive sorting implementation due to repeated string allocations and suggests passing terminal dimensions directly to the rebuild function to avoid frequent ioctl calls during search operations.

Comment thread src/ops/sorting.rs Outdated
Comment thread src/app/panel_ops.rs Outdated

@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 new case-insensitive name comparison (cmp_name_insensitive) now allocates lowercase Strings on every comparison and sort_by re-computes these per comparison, which can significantly regress performance compared to the previous sort_by_cached_key; consider caching lowercase (and possibly original) names per entry or restoring a key-based approach to avoid repeated allocations.
  • Several call sites now delegate to panel_ops::rebuild_visible_entries, which restores cursor based on the current entry; if a caller wants to preserve or prefer a different entry (e.g. a path from an external event), it might be cleaner to expose an overload that accepts a preferred name instead of relying on additional cursor adjustments afterward.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new case-insensitive name comparison (`cmp_name_insensitive`) now allocates lowercase `String`s on every comparison and `sort_by` re-computes these per comparison, which can significantly regress performance compared to the previous `sort_by_cached_key`; consider caching lowercase (and possibly original) names per entry or restoring a key-based approach to avoid repeated allocations.
- Several call sites now delegate to `panel_ops::rebuild_visible_entries`, which restores cursor based on the current entry; if a caller wants to preserve or prefer a different entry (e.g. a path from an external event), it might be cleaner to expose an overload that accepts a preferred name instead of relying on additional cursor adjustments afterward.

## Individual Comments

### Comment 1
<location path="src/ops/sorting.rs" line_range="175-182" />
<code_context>
     }
 }

+fn cmp_name_insensitive(a: &str, b: &str) -> Ordering {
+    let a_lower = a.to_lowercase();
+    let b_lower = b.to_lowercase();
+
+    a_lower.cmp(&b_lower).then_with(|| {
+        let a_tie = if a_lower == a { "" } else { a };
+        let b_tie = if b_lower == b { "" } else { b };
+        a_tie.cmp(b_tie)
+    })
+}
</code_context>
<issue_to_address>
**issue (performance):** Case-insensitive name comparison now allocates per comparison, which can significantly degrade sort performance on large directory listings.

Previously, `sort_by_cached_key` + `name_key` ensured each lowercase name was allocated once. Now `cmp_name_insensitive` does `to_lowercase` on both sides for every `sort_by` comparison, leading to `O(n log n)` allocations and a likely slowdown on large/long directory listings.

To keep the new tie‑breaking while avoiding repeated allocations, consider either:
- Returning to a cached-key sort (e.g., `sort_by_cached_key` on `(lower, original)`), or
- Storing a precomputed lowercase name on `FileEntry`.

Both options avoid per-comparison allocations while preserving behavior.
</issue_to_address>

### Comment 2
<location path="src/tests/menu.rs" line_range="203" />
<code_context>
+#[test]
+fn menu_sort_preserves_current_entry_focus() {
</code_context>
<issue_to_address>
**issue (testing):** This test does not currently prove that sorting occurred, only that focus stayed on the same index.

With the initial state `entries = ["zeta.txt", "alpha.txt"]` and `cursor = 0`, the final assertion `current_entry() == "zeta.txt"` would still pass if the menu action were a no‑op, so the test doesn’t prove that sorting occurred.

To ensure it does:
- Assert the final order of `entries` (e.g. `["alpha.txt", "zeta.txt"]` after the sort).
- Assert that the focused entry remains "zeta.txt" even though its index changes (cursor moves with it).

This will cause the test to fail if sorting is removed or broken, not just if focus behavior changes.
</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/ops/sorting.rs Outdated
Comment thread src/tests/menu.rs
@leszek3737
leszek3737 merged commit db2ff9c into main May 18, 2026
5 checks passed
@leszek3737
leszek3737 deleted the opti branch May 18, 2026 10:47
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