Skip to content

Commit 14349d8

Browse files
committed
Add adaptive GC RSS brake
1 parent 7c999be commit 14349d8

8 files changed

Lines changed: 423 additions & 1 deletion

File tree

Doc/library/gc.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ The :mod:`gc` module provides the following functions:
115115
With the third generation, things are a bit more complicated,
116116
see `Collecting the oldest generation <https://devguide.python.org/garbage_collector/#collecting-the-oldest-generation>`_ for more information.
117117

118+
If :envvar:`PYTHON_GC_ADAPTIVE` is enabled, the interpreter may adjust
119+
*threshold0* at runtime based on recent generation ``0`` collection cost
120+
and collection yield. On Linux, adaptive mode also samples current process
121+
RSS and page-fault activity as a soft brake on threshold growth. It does
122+
not target RSS directly. *threshold1* and *threshold2* are not adjusted by
123+
adaptive mode.
124+
118125

119126
.. function:: get_count()
120127

Doc/using/cmdline.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,25 @@ conflict.
888888
.. versionadded:: 3.4
889889

890890

891+
.. envvar:: PYTHON_GC_ADAPTIVE
892+
893+
If this environment variable is set to a positive integer, Python enables
894+
adaptive cyclic garbage collector threshold tuning at startup. The
895+
adaptive mode changes only the youngest-generation allocation threshold
896+
according to recent collection cost and collection yield. On Linux, it
897+
also samples current RSS and page-fault activity as a soft brake on
898+
threshold growth. The middle- and oldest-generation thresholds keep their
899+
configured values.
900+
901+
902+
.. envvar:: PYTHON_GC_ADAPTIVE_MAX_THRESHOLD0
903+
904+
If :envvar:`PYTHON_GC_ADAPTIVE` is enabled, this environment variable sets
905+
the maximum adaptive value for the youngest-generation allocation
906+
threshold. The value must be a positive integer. The default is
907+
``5000000``.
908+
909+
891910
.. envvar:: PYTHONPROFILEIMPORTTIME
892911

893912
If this environment variable is set to a non-empty string, Python will

Include/internal/pycore_gc.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,20 @@ struct gc_generation_stats {
154154
Py_ssize_t uncollectable;
155155
};
156156

157+
struct gc_threshold_controller {
158+
int enabled;
159+
int min_threshold0;
160+
int max_threshold0;
161+
int window_collections;
162+
Py_ssize_t window_candidates;
163+
Py_ssize_t window_collected;
164+
_PyTime_t window_start;
165+
_PyTime_t gc_time;
166+
Py_ssize_t last_rss_pages;
167+
long last_minor_faults;
168+
long last_major_faults;
169+
};
170+
157171
struct _gc_runtime_state {
158172
/* List of objects that still need to be cleaned up, singly linked
159173
* via their gc headers' gc_prev pointers. */
@@ -176,6 +190,7 @@ struct _gc_runtime_state {
176190
PyObject *garbage;
177191
/* a list of callbacks to be invoked when collection is performed */
178192
PyObject *callbacks;
193+
struct gc_threshold_controller threshold_controller;
179194
/* This is the number of objects that survived the last full
180195
collection. It approximates the number of long lived objects
181196
tracked by the GC.

Include/internal/pycore_time.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ struct _time_runtime_state {
1818
#endif
1919
};
2020

21+
extern int _PyTime_GetProcessTimeWithInfo(
22+
_PyTime_t *t,
23+
_Py_clock_info_t *info);
24+
2125

2226
#ifdef __cplusplus
2327
}

Lib/test/test_gc.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,49 @@ def test_collect_generations(self):
397397
self.assertEqual((e, f), (0, 1))
398398
self.assertEqual((h, i), (0, 0))
399399

400+
def test_adaptive_threshold_env_var(self):
401+
code = textwrap.dedent("""
402+
import gc
403+
404+
start = gc.get_threshold()
405+
keep = []
406+
for _ in range(8):
407+
keep.extend([] for _ in range(1000))
408+
gc.collect(0)
409+
end = gc.get_threshold()
410+
411+
assert start == (700, 10, 10), start
412+
assert end[0] > start[0], end
413+
assert end[1:] == start[1:], end
414+
""")
415+
assert_python_ok("-c", code, PYTHON_GC_ADAPTIVE="1")
416+
417+
def test_adaptive_threshold_env_var_ignored_by_E(self):
418+
code = textwrap.dedent("""
419+
import gc
420+
421+
keep = []
422+
for _ in range(8):
423+
keep.extend([] for _ in range(1000))
424+
gc.collect(0)
425+
assert gc.get_threshold() == (700, 10, 10)
426+
""")
427+
assert_python_ok("-E", "-c", code, PYTHON_GC_ADAPTIVE="1")
428+
429+
def test_adaptive_max_threshold_env_var(self):
430+
code = textwrap.dedent("""
431+
import gc
432+
433+
keep = []
434+
for _ in range(8):
435+
keep.extend([] for _ in range(1000))
436+
gc.collect(0)
437+
assert gc.get_threshold() == (900, 10, 10), gc.get_threshold()
438+
""")
439+
assert_python_ok("-c", code,
440+
PYTHON_GC_ADAPTIVE="1",
441+
PYTHON_GC_ADAPTIVE_MAX_THRESHOLD0="900")
442+
400443
def test_trashcan(self):
401444
class Ouch:
402445
n = 0

0 commit comments

Comments
 (0)