diff options
author | Neil Schemenauer <nas-github@arctrix.com> | 2025-05-05 10:17:05 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-05-05 17:17:05 +0000 |
commit | 5c245ffce71b5a23e0022bb5d1eaf645fe96ddbb (patch) | |
tree | 492197325b25c4fa58ef1aa2d8e291d8b289b739 /Python/gc_free_threading.c | |
parent | 8e08ac9f32d89bf387c75bb6d0710a7b59026b5b (diff) | |
download | cpython-5c245ffce71b5a23e0022bb5d1eaf645fe96ddbb.tar.gz cpython-5c245ffce71b5a23e0022bb5d1eaf645fe96ddbb.zip |
gh-132917: Check resident set size (RSS) before GC trigger. (gh-133399)
For the free-threaded build, check the process resident set size (RSS)
increase before triggering a full automatic garbage collection. If the RSS
has not increased 10% since the last collection then it is deferred.
Diffstat (limited to 'Python/gc_free_threading.c')
-rw-r--r-- | Python/gc_free_threading.c | 217 |
1 files changed, 212 insertions, 5 deletions
diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 2db75e0fd41..1e769ca48a8 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -17,6 +17,29 @@ #include "pydtrace.h" +// Platform-specific includes for get_current_rss(). +#ifdef _WIN32 + #include <windows.h> + #include <psapi.h> // For GetProcessMemoryInfo +#elif defined(__linux__) + #include <unistd.h> // For sysconf, getpid +#elif defined(__APPLE__) + #include <mach/mach.h> + #include <unistd.h> // For sysconf, getpid +#elif defined(__FreeBSD__) + #include <sys/types.h> + #include <sys/sysctl.h> + #include <sys/user.h> // Requires sys/user.h for kinfo_proc definition + #include <kvm.h> + #include <unistd.h> // For sysconf, getpid + #include <fcntl.h> // For O_RDONLY + #include <limits.h> // For _POSIX2_LINE_MAX +#elif defined(__OpenBSD__) + #include <sys/types.h> + #include <sys/sysctl.h> + #include <sys/user.h> // For kinfo_proc + #include <unistd.h> // For sysconf, getpid +#endif // enable the "mark alive" pass of GC #define GC_ENABLE_MARK_ALIVE 1 @@ -1878,6 +1901,180 @@ cleanup_worklist(struct worklist *worklist) } } +// Return the current resident set size (RSS) of the process, in units of KB. +// Returns -1 if this operation is not supported or on failure. +static Py_ssize_t +get_current_rss(void) +{ +#ifdef _WIN32 + // Windows implementation using GetProcessMemoryInfo + PROCESS_MEMORY_COUNTERS pmc; + HANDLE hProcess = GetCurrentProcess(); + if (NULL == hProcess) { + // Should not happen for the current process + return -1; + } + + // GetProcessMemoryInfo returns non-zero on success + if (GetProcessMemoryInfo(hProcess, &pmc, sizeof(pmc))) { + // pmc.WorkingSetSize is in bytes. Convert to KB. + return (Py_ssize_t)(pmc.WorkingSetSize / 1024); + } + else { + return -1; + } + +#elif __linux__ + // Linux implementation using /proc/self/statm + long page_size_bytes = sysconf(_SC_PAGE_SIZE); + if (page_size_bytes <= 0) { + return -1; + } + + FILE *fp = fopen("/proc/self/statm", "r"); + if (fp == NULL) { + return -1; + } + + // Second number is resident size in pages + long rss_pages; + if (fscanf(fp, "%*d %ld", &rss_pages) != 1) { + fclose(fp); + return -1; + } + fclose(fp); + + // Sanity check + if (rss_pages < 0 || rss_pages > 1000000000) { + return -1; + } + + // Convert unit to KB + return (Py_ssize_t)rss_pages * (page_size_bytes / 1024); + +#elif defined(__APPLE__) + // --- MacOS (Darwin) --- + mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; + mach_task_basic_info_data_t info; + kern_return_t kerr; + + kerr = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count); + if (kerr != KERN_SUCCESS) { + return -1; + } + // info.resident_size is in bytes. Convert to KB. + return (Py_ssize_t)(info.resident_size / 1024); + +#elif defined(__FreeBSD__) + long page_size_kb = sysconf(_SC_PAGESIZE) / 1024; + if (page_size_kb <= 0) { + return -1; + } + + // Using /dev/null for vmcore avoids needing dump file. + // NULL for kernel file uses running kernel. + char errbuf[_POSIX2_LINE_MAX]; // For kvm error messages + kvm_t *kd = kvm_openfiles(NULL, "/dev/null", NULL, O_RDONLY, errbuf); + if (kd == NULL) { + return -1; + } + + // KERN_PROC_PID filters for the specific process ID + // n_procs will contain the number of processes returned (should be 1 or 0) + pid_t pid = getpid(); + int n_procs; + struct kinfo_proc *kp = kvm_getprocs(kd, KERN_PROC_PID, pid, &n_procs); + if (kp == NULL) { + kvm_close(kd); + return -1; + } + + Py_ssize_t rss_kb = -1; + if (n_procs > 0) { + // kp[0] contains the info for our process + // ki_rssize is in pages. Convert to KB. + rss_kb = (Py_ssize_t)kp->ki_rssize * page_size_kb; + } + else { + // Process with PID not found, shouldn't happen for self. + rss_kb = -1; + } + + kvm_close(kd); + return rss_kb; + +#elif defined(__OpenBSD__) + long page_size_kb = sysconf(_SC_PAGESIZE) / 1024; + if (page_size_kb <= 0) { + return -1; + } + + struct kinfo_proc kp; + pid_t pid = getpid(); + int mib[6]; + size_t len = sizeof(kp); + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = pid; + mib[4] = sizeof(struct kinfo_proc); // size of the structure we want + mib[5] = 1; // want 1 structure back + if (sysctl(mib, 6, &kp, &len, NULL, 0) == -1) { + return -1; + } + + if (len > 0) { + // p_vm_rssize is in pages on OpenBSD. Convert to KB. + return (Py_ssize_t)kp.p_vm_rssize * page_size_kb; + } + else { + // Process info not returned + return -1; + } +#else + // Unsupported platform + return -1; +#endif +} + +static bool +gc_should_collect_rss(GCState *gcstate) +{ + Py_ssize_t rss = get_current_rss(); + if (rss < 0) { + // Reading RSS is not support or failed. + return true; + } + int threshold = gcstate->young.threshold; + Py_ssize_t deferred = _Py_atomic_load_ssize_relaxed(&gcstate->deferred_count); + if (deferred > threshold * 40) { + // Too many new container objects since last GC, even though RSS + // might not have increased much. This is intended to avoid resource + // exhaustion if some objects consume resources but don't result in a + // RSS increase. We use 40x as the factor here because older versions + // of Python would do full collections after roughly every 70,000 new + // container objects. + return true; + } + Py_ssize_t last_rss = gcstate->last_rss; + Py_ssize_t rss_threshold = Py_MAX(last_rss / 10, 128); + if ((rss - last_rss) > rss_threshold) { + // The RSS has increased too much, do a collection. + return true; + } + else { + // The RSS has not increased enough, defer the collection and clear + // the young object count so we don't check RSS again on the next call + // to gc_should_collect(). + PyMutex_Lock(&gcstate->mutex); + gcstate->deferred_count += gcstate->young.count; + gcstate->young.count = 0; + PyMutex_Unlock(&gcstate->mutex); + return false; + } +} + static bool gc_should_collect(GCState *gcstate) { @@ -1887,11 +2084,17 @@ gc_should_collect(GCState *gcstate) if (count <= threshold || threshold == 0 || !gc_enabled) { return false; } - // Avoid quadratic behavior by scaling threshold to the number of live - // objects. A few tests rely on immediate scheduling of the GC so we ignore - // the scaled threshold if generations[1].threshold is set to zero. - return (count > gcstate->long_lived_total / 4 || - gcstate->old[0].threshold == 0); + if (gcstate->old[0].threshold == 0) { + // A few tests rely on immediate scheduling of the GC so we ignore the + // extra conditions if generations[1].threshold is set to zero. + return true; + } + if (count < gcstate->long_lived_total / 4) { + // Avoid quadratic behavior by scaling threshold to the number of live + // objects. + return false; + } + return gc_should_collect_rss(gcstate); } static void @@ -1940,6 +2143,7 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state, } state->gcstate->young.count = 0; + state->gcstate->deferred_count = 0; for (int i = 1; i <= generation; ++i) { state->gcstate->old[i-1].count = 0; } @@ -2033,6 +2237,9 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state, // to be freed. delete_garbage(state); + // Store the current RSS, possibly smaller now that we deleted garbage. + state->gcstate->last_rss = get_current_rss(); + // Append objects with legacy finalizers to the "gc.garbage" list. handle_legacy_finalizers(state); } |