fix(natives): streamed bounded glob progress
Preserved bounded onMatch progress during uncached sorted glob traversal so timed-out find scans can still return partial matches without unbounded callback growth.
This commit is contained in:
@@ -221,6 +221,7 @@ fn filter_entries(
|
||||
fn collect_sorted_matches_uncached(
|
||||
glob_set: &GlobSet,
|
||||
config: &GlobConfig,
|
||||
on_match: Option<&ThreadsafeFunction<GlobMatch>>,
|
||||
ct: &task::CancelToken,
|
||||
) -> Result<Vec<GlobMatch>> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user