-
Notifications
You must be signed in to change notification settings - Fork 10
Add memory cap to indexer to prevent OOM on large monorepos #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
36c2596
Add memory cap to indexer to prevent OOM on large monorepos
shengyfu 5b947e7
Bound peak indexing memory via flush-and-continue (complete index)
shengyfu 88ffcd3
Cap CPU usage during the initial index build
shengyfu b4899c6
Fix memory-bounded bulk index flush coordination
shengyfu 876c041
Address review: skip unreadable reader entries, guard overflow, use c…
shengyfu db8d36f
Fix macOS sysctl mib pointer mutability and drop unused import
shengyfu 0214ff9
Harden append-overlay merge and CLI memory flag against overflow/corr…
shengyfu 61a8768
Bump version to 0.1.21
shengyfu 1dc9e52
Close watcher race during final append-only publish handoff
shengyfu 6ae5920
Validate posting offset in u64 before narrowing; shrink memory_cap fi…
shengyfu 409c4be
Guard posting start offset against mmap length in nth_trigram_raw
shengyfu 38e23bf
Initialize PROCESS_MEMORY_COUNTERS.cb before GetProcessMemoryInfo
shengyfu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| //! CPU budget for the background indexer. | ||
| //! | ||
| //! Indexing's CPU-heavy work (reading files + extracting trigrams) runs in | ||
| //! parallel via rayon, which by default fans out across every logical core and | ||
| //! can saturate the host. To stay a good neighbor — especially when tgrep is | ||
| //! embedded in another tool — the indexer confines that work to a bounded | ||
| //! worker pool sized from a CPU budget expressed as a percentage of cores. | ||
|
|
||
| use std::thread::available_parallelism; | ||
|
|
||
| /// Number of worker threads to use for the parallel indexing work, given a CPU | ||
| /// budget expressed as a percentage of logical cores (1–100). | ||
| /// | ||
| /// Always at least 1 and never more than the available core count, so a 50% | ||
| /// budget on an 8-core host yields 4 threads, and any budget on a single-core | ||
| /// host yields 1. | ||
| pub fn index_thread_count(max_cpu_percent: u8) -> usize { | ||
| let cores = available_parallelism().map(|n| n.get()).unwrap_or(1); | ||
| let pct = max_cpu_percent.clamp(1, 100) as usize; | ||
| (cores * pct).div_ceil(100).clamp(1, cores) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn clamps_to_at_least_one_thread() { | ||
| // Even a 1% budget must keep indexing alive with a single worker. | ||
| assert!(index_thread_count(1) >= 1); | ||
| } | ||
|
|
||
| #[test] | ||
| fn never_exceeds_core_count() { | ||
| let cores = available_parallelism().map(|n| n.get()).unwrap_or(1); | ||
| assert!(index_thread_count(100) <= cores); | ||
| // Out-of-range percentages are clamped to the 1..=100 budget. | ||
| assert!(index_thread_count(u8::MAX) <= cores); | ||
| } | ||
|
|
||
| #[test] | ||
| fn full_budget_uses_all_cores() { | ||
| let cores = available_parallelism().map(|n| n.get()).unwrap_or(1); | ||
| assert_eq!(index_thread_count(100), cores); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| //! Cross-platform process memory introspection. | ||
| //! | ||
| //! Provides functions to query the current process's resident set size (RSS) | ||
| //! and the host's total physical memory — used to enforce an indexing memory | ||
| //! budget so tgrep doesn't OOM-kill the host on large monorepos. | ||
|
|
||
| /// Returns the current process's resident set size in bytes, or `None` if | ||
| /// the platform query fails. | ||
| #[cfg(target_os = "windows")] | ||
| pub fn process_rss_bytes() -> Option<u64> { | ||
| use std::mem::MaybeUninit; | ||
| use windows_sys::Win32::System::ProcessStatus::GetProcessMemoryInfo; | ||
| use windows_sys::Win32::System::ProcessStatus::PROCESS_MEMORY_COUNTERS; | ||
| use windows_sys::Win32::System::Threading::GetCurrentProcess; | ||
|
|
||
| unsafe { | ||
| let handle = GetCurrentProcess(); | ||
| let mut counters = MaybeUninit::<PROCESS_MEMORY_COUNTERS>::zeroed(); | ||
| let size = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32; | ||
| // GetProcessMemoryInfo requires `cb` to hold the struct size on input. | ||
| (*counters.as_mut_ptr()).cb = size; | ||
| let ok = GetProcessMemoryInfo(handle, counters.as_mut_ptr(), size); | ||
| if ok != 0 { | ||
| let counters = counters.assume_init(); | ||
| Some(counters.WorkingSetSize as u64) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(target_os = "windows")] | ||
| pub fn total_physical_memory_bytes() -> Option<u64> { | ||
| use std::mem::MaybeUninit; | ||
| use windows_sys::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX}; | ||
|
|
||
| unsafe { | ||
| let mut status = MaybeUninit::<MEMORYSTATUSEX>::zeroed(); | ||
| (*status.as_mut_ptr()).dwLength = std::mem::size_of::<MEMORYSTATUSEX>() as u32; | ||
| let ok = GlobalMemoryStatusEx(status.as_mut_ptr()); | ||
| if ok != 0 { | ||
| Some(status.assume_init().ullTotalPhys) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| pub fn process_rss_bytes() -> Option<u64> { | ||
| let statm = std::fs::read_to_string("/proc/self/statm").ok()?; | ||
| let rss_pages: u64 = statm.split_whitespace().nth(1)?.parse().ok()?; | ||
| let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; | ||
| if page_size <= 0 { | ||
| return None; | ||
| } | ||
| Some(rss_pages * page_size as u64) | ||
| } | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| pub fn total_physical_memory_bytes() -> Option<u64> { | ||
| let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?; | ||
| for line in meminfo.lines() { | ||
| if let Some(rest) = line.strip_prefix("MemTotal:") { | ||
| let kb: u64 = rest.trim().strip_suffix("kB")?.trim().parse().ok()?; | ||
| return Some(kb * 1024); | ||
| } | ||
| } | ||
| None | ||
| } | ||
|
|
||
| #[cfg(target_os = "macos")] | ||
| pub fn process_rss_bytes() -> Option<u64> { | ||
| use std::mem::MaybeUninit; | ||
| unsafe { | ||
| // `ru_maxrss` reports the *peak* RSS, so it never decreases after a | ||
| // flush reclaims memory and would keep the process looking over-budget | ||
| // forever. Query the *current* resident size via proc_pidinfo instead. | ||
| let mut info = MaybeUninit::<libc::proc_taskinfo>::zeroed(); | ||
| let size = std::mem::size_of::<libc::proc_taskinfo>() as libc::c_int; | ||
| let ret = libc::proc_pidinfo( | ||
| libc::getpid(), | ||
| libc::PROC_PIDTASKINFO, | ||
| 0, | ||
| info.as_mut_ptr() as *mut libc::c_void, | ||
| size, | ||
| ); | ||
| if ret == size { | ||
| Some(info.assume_init().pti_resident_size) | ||
| } else { | ||
| None | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(target_os = "macos")] | ||
| pub fn total_physical_memory_bytes() -> Option<u64> { | ||
| unsafe { | ||
| let mut size: u64 = 0; | ||
| let mut len = std::mem::size_of::<u64>(); | ||
| let mut mib = [libc::CTL_HW, libc::HW_MEMSIZE]; | ||
| let ret = libc::sysctl( | ||
| mib.as_mut_ptr(), | ||
| 2, | ||
| &mut size as *mut u64 as *mut libc::c_void, | ||
| &mut len, | ||
| std::ptr::null_mut(), | ||
| 0, | ||
| ); | ||
| if ret == 0 { Some(size) } else { None } | ||
| } | ||
| } | ||
|
|
||
| // Fallback for unsupported platforms | ||
| #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] | ||
| pub fn process_rss_bytes() -> Option<u64> { | ||
| None | ||
| } | ||
|
|
||
| #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] | ||
| pub fn total_physical_memory_bytes() -> Option<u64> { | ||
| None | ||
| } | ||
|
|
||
| /// Compute the default memory cap: 50% of physical RAM, with a floor of 512 MB | ||
| /// and a ceiling of 16 GB. Returns bytes. | ||
| pub fn default_memory_cap_bytes() -> u64 { | ||
| const FLOOR: u64 = 512 * 1024 * 1024; // 512 MB | ||
| const CEILING: u64 = 16 * 1024 * 1024 * 1024; // 16 GB | ||
|
|
||
| let half_ram = total_physical_memory_bytes() | ||
| .map(|total| total / 2) | ||
| .unwrap_or(4 * 1024 * 1024 * 1024); // fallback: 4 GB | ||
|
|
||
| half_ram.clamp(FLOOR, CEILING) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| // On supported platforms the queries must succeed and return non-zero | ||
| // values. This guards regressions like a missing `PROCESS_MEMORY_COUNTERS.cb` | ||
| // (or `MEMORYSTATUSEX.dwLength`) initialization, which would make the call | ||
| // fail and silently disable the memory cap. | ||
| #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] | ||
| #[test] | ||
| fn process_rss_is_nonzero() { | ||
| let rss = process_rss_bytes().expect("process RSS query should succeed"); | ||
| assert!(rss > 0, "process RSS should be non-zero, got {rss}"); | ||
| } | ||
|
|
||
| #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] | ||
| #[test] | ||
| fn total_physical_memory_is_nonzero() { | ||
| let total = | ||
| total_physical_memory_bytes().expect("total physical memory query should succeed"); | ||
| assert!( | ||
| total > 0, | ||
| "total physical memory should be non-zero, got {total}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn default_cap_is_within_bounds() { | ||
| let cap = default_memory_cap_bytes(); | ||
| assert!(cap >= 512 * 1024 * 1024); | ||
| assert!(cap <= 16 * 1024 * 1024 * 1024); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.