From 8425b94ddd1844e19e0baf9be8e1448c703ad67d Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 20 Oct 2026 08:16:26 -0400 Subject: [PATCH 1/5] Workaround a massive Genreation 2 GC sweep at exit Use os._exit() to bypass garbage collection via "raise SystemExit" --- mathics/repl.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/mathics/repl.py b/mathics/repl.py index 4bed73c743..0c96a4d2ba 100644 --- a/mathics/repl.py +++ b/mathics/repl.py @@ -384,9 +384,38 @@ def interactive_eval_loop( except EOFError: print("\n\nGoodbye!\n") break - except SystemExit: - # raise to pass the error code on, e.g. Quit[1] - raise + except SystemExit as e: + # As of Oct. 2026 we have huge Python garbage collection problems. + # To mitigate we use os._exit() to Py_Finalize() and avoid + # the Generation 2 GC sweep. + + # Why do we have massive garbage collection issues? + # 1. Massive Object Graph in Generation 2: + # Loading a package like Rubi populates tens of thousands of complex expression + # trees, pattern rules, and symbol bindings. As Mathics3 + # executes, these long-lived objects are promoted to Generation 2 + # (the oldest GC generation). + + # 2. There is a Bottleneck is Reference-Cycle Traversal: + # Because Wolfram Language expressions are deeply nested and often + # form circular references (e.g., parent/child links, symbol table + # bindings, pattern environments), Python cannot simply deallocate + # them via fast reference counting. Instead, PyGC_Collect() must + # iterate over every object in the Generation 2 doubly-linked list + # (gc_list_size) to find unreachable sub-graphs. + + # Flush standard streams so terminal output isn't lost + sys.stdout.flush() + sys.stderr.flush() + + # Extract numeric exit code (default to 0 if None) + code = e.code if isinstance(e.code, int) else 0 + + os._exit(code) + + # When we have addressed why we great massive Generation 2 + # garbage we can run: + # raise finally: shell.reset_lineno() return result From 57bd86aa348662fff236bfabbc2d4a8c2a5397c7 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 20 Oct 2026 08:47:03 -0400 Subject: [PATCH 2/5] Speed up exiting Mathics3 We have massive garbage collection issues on exit, possibly because there is massive Generation 2 (oldest GC generation) long-lived object it needs to free. As a workaround, disable garbage collection before final exit. --- pyproject.toml | 1 + test/conftest.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 test/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 0cf57ab0b0..e9087204d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ dev = [ "pexpect", "pre-commit", "pytest", + "pytest-benchmark", ] full = [ "ipywidgets", diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000000..80a53ed1bb --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,11 @@ +# Code that gets run before pytest runs.. +import gc + + +def pytest_sessionfinish(session, exitstatus): + """Executes after all tests have completed. + + Disables cyclic GC right before pytest exits, preventing PyGC_Collect from + walking Generation 2 during _Py_Finalize. + """ + gc.disable() From 97e4bfe4bde69655d14ad89ed2151a7ec8541966 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 20 Oct 2026 13:13:41 -0400 Subject: [PATCH 3/5] Remove overlap with PR 2062 --- mathics/repl.py | 35 +++-------------------------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/mathics/repl.py b/mathics/repl.py index 0c96a4d2ba..4bed73c743 100644 --- a/mathics/repl.py +++ b/mathics/repl.py @@ -384,38 +384,9 @@ def interactive_eval_loop( except EOFError: print("\n\nGoodbye!\n") break - except SystemExit as e: - # As of Oct. 2026 we have huge Python garbage collection problems. - # To mitigate we use os._exit() to Py_Finalize() and avoid - # the Generation 2 GC sweep. - - # Why do we have massive garbage collection issues? - # 1. Massive Object Graph in Generation 2: - # Loading a package like Rubi populates tens of thousands of complex expression - # trees, pattern rules, and symbol bindings. As Mathics3 - # executes, these long-lived objects are promoted to Generation 2 - # (the oldest GC generation). - - # 2. There is a Bottleneck is Reference-Cycle Traversal: - # Because Wolfram Language expressions are deeply nested and often - # form circular references (e.g., parent/child links, symbol table - # bindings, pattern environments), Python cannot simply deallocate - # them via fast reference counting. Instead, PyGC_Collect() must - # iterate over every object in the Generation 2 doubly-linked list - # (gc_list_size) to find unreachable sub-graphs. - - # Flush standard streams so terminal output isn't lost - sys.stdout.flush() - sys.stderr.flush() - - # Extract numeric exit code (default to 0 if None) - code = e.code if isinstance(e.code, int) else 0 - - os._exit(code) - - # When we have addressed why we great massive Generation 2 - # garbage we can run: - # raise + except SystemExit: + # raise to pass the error code on, e.g. Quit[1] + raise finally: shell.reset_lineno() return result From 0a08ffc9075d6fd68da5455a61ee0c61b0352499 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 20 Oct 2026 13:24:59 -0400 Subject: [PATCH 4/5] gc.disable -> gc.freeze gc.freeze (not gc.disable) prevents PyGC_Collect from walking Generation 2 during _Py_Finalize. --- test/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 80a53ed1bb..7a9804d47d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,7 +5,7 @@ def pytest_sessionfinish(session, exitstatus): """Executes after all tests have completed. - Disables cyclic GC right before pytest exits, preventing PyGC_Collect from + Freeze objects right before pytest exits. This prevents PyGC_Collect from walking Generation 2 during _Py_Finalize. """ - gc.disable() + gc.freeze() From f09f6750a950e7f56c14dc1cefe62860ae1fb046 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 20 Oct 2026 13:44:12 -0400 Subject: [PATCH 5/5] Comment changes --- test/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/conftest.py b/test/conftest.py index 7a9804d47d..4c57d33686 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -6,6 +6,6 @@ def pytest_sessionfinish(session, exitstatus): """Executes after all tests have completed. Freeze objects right before pytest exits. This prevents PyGC_Collect from - walking Generation 2 during _Py_Finalize. + walking Generation 2 objects during _Py_Finalize. """ gc.freeze()