aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Python/gc_free_threading.c
diff options
context:
space:
mode:
authorNeil Schemenauer <nas-github@arctrix.com>2025-05-05 10:17:05 -0700
committerGitHub <noreply@github.com>2025-05-05 17:17:05 +0000
commit5c245ffce71b5a23e0022bb5d1eaf645fe96ddbb (patch)
tree492197325b25c4fa58ef1aa2d8e291d8b289b739 /Python/gc_free_threading.c
parent8e08ac9f32d89bf387c75bb6d0710a7b59026b5b (diff)
downloadcpython-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.c217
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);
}