diff --git a/crates/pi-natives/src/glob.rs b/crates/pi-natives/src/glob.rs index f3789fa32..578b875e2 100644 --- a/crates/pi-natives/src/glob.rs +++ b/crates/pi-natives/src/glob.rs @@ -221,6 +221,7 @@ fn filter_entries( fn collect_sorted_matches_uncached( glob_set: &GlobSet, config: &GlobConfig, + on_match: Option<&ThreadsafeFunction>, ct: &task::CancelToken, ) -> Result> { let builder = fs_cache::build_walker( @@ -232,6 +233,7 @@ fn collect_sorted_matches_uncached( ); let mut top_matches = BinaryHeap::with_capacity(config.max_results.min(1024)); let mut visited = 0usize; + let mut streamed_matches = 0usize; for entry in builder.build() { if visited == 0 || visited >= 128 { @@ -258,6 +260,12 @@ fn collect_sorted_matches_uncached( continue; }; matched_entry.file_type = effective_file_type; + if streamed_matches < config.max_results { + streamed_matches += 1; + if let Some(callback) = on_match { + callback.call(Ok(matched_entry.clone()), ThreadsafeFunctionCallMode::NonBlocking); + } + } push_bounded_match(&mut top_matches, matched_entry, config.max_results); } @@ -290,29 +298,30 @@ fn run_glob( fs_cache::ScanDetail::Minimal }, }; - let mut matches = - if config.sort_by_mtime && !config.use_cache && config.max_results != usize::MAX { - collect_sorted_matches_uncached(&glob_set, &config, &ct)? - } else if config.use_cache { - let scan = fs_cache::get_or_scan(&config.root, scan_options, &ct)?; - let mut matches = filter_entries(&scan.entries, &glob_set, &config, on_match, &ct)?; - // Empty-result recheck: if we got zero matches from a cached scan that's old - // enough, force a rescan and try once more before returning empty. - if matches.is_empty() && scan.cache_age_ms >= fs_cache::empty_recheck_ms() { - let fresh = fs_cache::force_rescan(&config.root, scan_options, true, &ct)?; - matches = filter_entries(&fresh, &glob_set, &config, on_match, &ct)?; - } - matches - } else { - let fresh = fs_cache::force_rescan(&config.root, scan_options, false, &ct)?; - filter_entries(&fresh, &glob_set, &config, on_match, &ct)? - }; + let streams_bounded_sorted_partials = + config.sort_by_mtime && !config.use_cache && config.max_results != usize::MAX; + let mut matches = if streams_bounded_sorted_partials { + collect_sorted_matches_uncached(&glob_set, &config, on_match, &ct)? + } else if config.use_cache { + let scan = fs_cache::get_or_scan(&config.root, scan_options, &ct)?; + let mut matches = filter_entries(&scan.entries, &glob_set, &config, on_match, &ct)?; + // Empty-result recheck: if we got zero matches from a cached scan that's old + // enough, force a rescan and try once more before returning empty. + if matches.is_empty() && scan.cache_age_ms >= fs_cache::empty_recheck_ms() { + let fresh = fs_cache::force_rescan(&config.root, scan_options, true, &ct)?; + matches = filter_entries(&fresh, &glob_set, &config, on_match, &ct)?; + } + matches + } else { + let fresh = fs_cache::force_rescan(&config.root, scan_options, false, &ct)?; + filter_entries(&fresh, &glob_set, &config, on_match, &ct)? + }; if config.sort_by_mtime { // Sorting mode: rank by mtime descending, then apply max-results truncation. matches.sort_by(compare_matches_by_rank); matches.truncate(config.max_results); - if let Some(callback) = on_match { + if !streams_bounded_sorted_partials && let Some(callback) = on_match { for matched_entry in &matches { callback.call(Ok(matched_entry.clone()), ThreadsafeFunctionCallMode::NonBlocking); } diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index 6f0cdfeed..cec624100 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Bounded sorted `glob()` scans to `maxResults` during uncached traversal and capped `onMatch` callbacks to returned matches so broad OMP `find` scans cannot grow parent-process memory independently of the requested limit ([#1761](https://github.com/can1357/oh-my-pi/issues/1761)). +- Bounded sorted `glob()` scans to `maxResults` during uncached traversal and capped `onMatch` callbacks to bounded traversal progress so broad OMP `find` scans cannot grow parent-process memory independently of the requested limit ([#1761](https://github.com/can1357/oh-my-pi/issues/1761)). ## [15.7.0] - 2026-05-31 diff --git a/packages/natives/test/native.test.ts b/packages/natives/test/native.test.ts index 9395d5bac..10af31913 100644 --- a/packages/natives/test/native.test.ts +++ b/packages/natives/test/native.test.ts @@ -407,7 +407,7 @@ describe("pi-natives", () => { expect(result.matches).toHaveLength(0); }); - it("should bound sorted callbacks to maxResults", async () => { + it("should stream bounded sorted callbacks during traversal", async () => { const scopedDir = await fs.mkdtemp(path.join(os.tmpdir(), "natives-glob-limit-")); try { for (let i = 0; i < 40; i++) { @@ -433,7 +433,7 @@ describe("pi-natives", () => { await Bun.sleep(10); expect(result.matches).toHaveLength(5); expect(streamedPaths).toHaveLength(5); - expect(new Set(streamedPaths)).toEqual(new Set(result.matches.map(match => match.path))); + expect(streamedPaths.length).toBeGreaterThan(0); } finally { await fs.rm(scopedDir, { recursive: true, force: true }); }