From 4ec3eb7cee2015cba8a340f9435eff15edc33809 Mon Sep 17 00:00:00 2001 From: dailz Date: Tue, 14 Apr 2026 09:58:20 +0800 Subject: [PATCH] fix(core): cap incremental scan in get_line() to prevent O(N) blocking Add SCAN_AHEAD_LIMIT (10000 lines) to get_line() in Sampling state. Without this, jumping to end-of-file (G) during progressive loading would scan the entire file byte-by-byte on the main thread, blocking the UI and consuming excessive memory. Lines beyond the scanned region + limit now return None, which the TUI renders as empty. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- crates/core/src/io/progressive_reader.rs | 26 ++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/crates/core/src/io/progressive_reader.rs b/crates/core/src/io/progressive_reader.rs index 5e62482..62e8ee9 100644 --- a/crates/core/src/io/progressive_reader.rs +++ b/crates/core/src/io/progressive_reader.rs @@ -389,6 +389,11 @@ pub fn spawn_visual_height_rebuild( /// Maximum bytes to scan during initial open for the Sampling state. const INITIAL_SCAN_BYTES: usize = 64 * 1024; +/// Maximum number of additional lines to scan beyond what's already cached +/// in a single `get_line()` call. Prevents O(N) blocking when the user +/// jumps far ahead (e.g. `G` to end-of-file) during the Loading state. +const SCAN_AHEAD_LIMIT: usize = 10_000; + pub struct ProgressiveFileReader { path: PathBuf, pub state: ReaderState, @@ -512,32 +517,23 @@ impl ProgressiveFileReader { let mut newlines = scanned_newlines.borrow_mut(); let mut up_to = scanned_up_to.borrow_mut(); - // We need `idx + 1` newlines to have `idx` lines available. - // Line i starts after the i-th newline (or at byte 0 for line 0) - // and ends at the (i+1)-th newline (or end of file). - // So to return line `idx`, we need at least `idx + 1` newline positions - // (the idx-th newline marks end of line idx-1/start of line idx, - // and the (idx+1)-th newline marks end of line idx). - // Actually: line 0 starts at byte 0, ends at newline[0]. - // line 1 starts at newline[0]+1, ends at newline[1]. - // line i starts at newline[i-1]+1, ends at newline[i]. - // So to serve line idx, we need newline positions up to index idx. + let scan_limit = newlines.len() + SCAN_AHEAD_LIMIT; - // Extend scan if needed - while newlines.len() <= idx && *up_to < mmap_data.len() { + // Extend scan if needed, but stop at scan_limit to avoid O(N) blocking + while newlines.len() <= idx + && newlines.len() < scan_limit + && *up_to < mmap_data.len() + { let remaining = &mmap_data[*up_to..]; if let Some(rel_pos) = memchr::memchr(b'\n', remaining) { newlines.push(*up_to + rel_pos); *up_to += rel_pos + 1; } else { - // No more newlines; rest of file is the last line *up_to = mmap_data.len(); break; } } - // Check if line idx is beyond what's available - // If idx == newlines.len(), it's the last line (after last newline, no trailing \n) if idx > newlines.len() { return None; }