Skip to content

Commit

Permalink
gh-108634: Py_TRACE_REFS uses a hash table (#108663)
Browse files Browse the repository at this point in the history
Python built with "configure --with-trace-refs" (tracing references)
is now ABI compatible with Python release build and debug build.
Moreover, it now also supports the Limited API.

Change Py_TRACE_REFS build:

* Remove _PyObject_EXTRA_INIT macro.
* The PyObject structure no longer has two extra members (_ob_prev
  and _ob_next).
* Use a hash table (_Py_hashtable_t) to trace references (all
  objects): PyInterpreterState.object_state.refchain.
* Py_TRACE_REFS build is now ABI compatible with release build and
  debug build.
* Limited C API extensions can now be built with Py_TRACE_REFS:
  xxlimited, xxlimited_35, _testclinic_limited.
* No longer rename PyModule_Create2() and PyModule_FromDefAndSpec2()
  functions to PyModule_Create2TraceRefs() and
  PyModule_FromDefAndSpec2TraceRefs().
* _Py_PrintReferenceAddresses() is now called before
  finalize_interp_delete() which deletes the refchain hash table.
* test_tracemalloc find_trace() now also filters by size to ignore
  the memory allocated by _PyRefchain_Trace().

Test changes for Py_TRACE_REFS:

* Add test.support.Py_TRACE_REFS constant.
* Add test_sys.test_getobjects() to test sys.getobjects() function.
* test_exceptions skips test_recursion_normalizing_with_no_memory()
  and test_memory_error_in_PyErr_PrintEx() if Python is built with
  Py_TRACE_REFS.
* test_repl skips test_no_memory().
* test_capi skisp test_set_nomemory().
  • Loading branch information
vstinner authored Aug 31, 2023
1 parent 013a99a commit 13a0007
Show file tree
Hide file tree
Showing 31 changed files with 292 additions and 243 deletions.
22 changes: 0 additions & 22 deletions Doc/c-api/typeobj.rst
Original file line number Diff line number Diff line change
Expand Up @@ -528,28 +528,6 @@ type objects) *must* have the :c:member:`~PyVarObject.ob_size` field.
This field is inherited by subtypes.


.. c:member:: PyObject* PyObject._ob_next
PyObject* PyObject._ob_prev
These fields are only present when the macro ``Py_TRACE_REFS`` is defined
(see the :option:`configure --with-trace-refs option <--with-trace-refs>`).

Their initialization to ``NULL`` is taken care of by the
``PyObject_HEAD_INIT`` macro. For :ref:`statically allocated objects
<static-types>`, these fields always remain ``NULL``. For :ref:`dynamically
allocated objects <heap-types>`, these two fields are used to link the
object into a doubly linked list of *all* live objects on the heap.

This could be used for various debugging purposes; currently the only uses
are the :func:`sys.getobjects` function and to print the objects that are
still alive at the end of a run when the environment variable
:envvar:`PYTHONDUMPREFS` is set.

**Inheritance:**

These fields are not inherited by subtypes.


PyVarObject Slots
-----------------

Expand Down
13 changes: 9 additions & 4 deletions Doc/using/configure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,7 @@ See also the :ref:`Python Development Mode <devmode>` and the
.. versionchanged:: 3.8
Release builds and debug builds are now ABI compatible: defining the
``Py_DEBUG`` macro no longer implies the ``Py_TRACE_REFS`` macro (see the
:option:`--with-trace-refs` option), which introduces the only ABI
incompatibility.
:option:`--with-trace-refs` option).


Debug options
Expand All @@ -447,8 +446,14 @@ Debug options
* Add :func:`sys.getobjects` function.
* Add :envvar:`PYTHONDUMPREFS` environment variable.

This build is not ABI compatible with release build (default build) or debug
build (``Py_DEBUG`` and ``Py_REF_DEBUG`` macros).
The :envvar:`PYTHONDUMPREFS` environment variable can be used to dump
objects and reference counts still alive at Python exit.

:ref:`Statically allocated objects <static-types>` are not traced.

.. versionchanged:: 3.13
This build is now ABI compatible with release build and :ref:`debug build
<debug-build>`.

.. versionadded:: 3.8

Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,11 @@ Build Changes
* SQLite 3.15.2 or newer is required to build the :mod:`sqlite3` extension module.
(Contributed by Erlend Aasland in :gh:`105875`.)

* Python built with :file:`configure` :option:`--with-trace-refs` (tracing
references) is now ABI compatible with Python release build and
:ref:`debug build <debug-build>`.
(Contributed by Victor Stinner in :gh:`108634`.)


C API Changes
=============
Expand Down Expand Up @@ -900,6 +905,10 @@ New Features
(with an underscore prefix).
(Contributed by Victor Stinner in :gh:`108014`.)

* Python built with :file:`configure` :option:`--with-trace-refs` (tracing
references) now supports the :ref:`Limited API <limited-c-api>`.
(Contributed by Victor Stinner in :gh:`108634`.)

Porting to Python 3.13
----------------------

Expand Down
5 changes: 3 additions & 2 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ PyAPI_FUNC(int) _PyObject_IsFreed(PyObject *);
backwards compatible solution */
#define _PyObject_HEAD_INIT(type) \
{ \
_PyObject_EXTRA_INIT \
.ob_refcnt = _Py_IMMORTAL_REFCNT, \
.ob_type = (type) \
},
Expand Down Expand Up @@ -184,6 +183,8 @@ _PyType_HasFeature(PyTypeObject *type, unsigned long feature) {
extern void _PyType_InitCache(PyInterpreterState *interp);

extern void _PyObject_InitState(PyInterpreterState *interp);
extern void _PyObject_FiniState(PyInterpreterState *interp);
extern bool _PyRefchain_IsTraced(PyInterpreterState *interp, PyObject *obj);

/* Inline functions trading binary compatibility for speed:
_PyObject_Init() is the fast version of PyObject_Init(), and
Expand Down Expand Up @@ -302,7 +303,7 @@ extern void _PyDebug_PrintTotalRefs(void);
#endif

#ifdef Py_TRACE_REFS
extern void _Py_AddToAllObjects(PyObject *op, int force);
extern void _Py_AddToAllObjects(PyObject *op);
extern void _Py_PrintReferences(PyInterpreterState *, FILE *);
extern void _Py_PrintReferenceAddresses(PyInterpreterState *, FILE *);
#endif
Expand Down
11 changes: 6 additions & 5 deletions Include/internal/pycore_object_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#include "pycore_hashtable.h" // _Py_hashtable_t

struct _py_object_runtime_state {
#ifdef Py_REF_DEBUG
Py_ssize_t interpreter_leaks;
Expand All @@ -20,11 +22,10 @@ struct _py_object_state {
Py_ssize_t reftotal;
#endif
#ifdef Py_TRACE_REFS
/* Head of circular doubly-linked list of all objects. These are linked
* together via the _ob_prev and _ob_next members of a PyObject, which
* exist only in a Py_TRACE_REFS build.
*/
PyObject refchain;
// Hash table storing all objects. The key is the object pointer
// (PyObject*) and the value is always the number 1 (as uintptr_t).
// See _PyRefchain_IsTraced() and _PyRefchain_Trace() functions.
_Py_hashtable_t *refchain;
#endif
int _not_used;
};
Expand Down
2 changes: 1 addition & 1 deletion Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ extern PyTypeObject _PyExc_MemoryError;
#ifdef Py_TRACE_REFS
# define _py_object_state_INIT(INTERP) \
{ \
.refchain = {&INTERP.object_state.refchain, &INTERP.object_state.refchain}, \
.refchain = NULL, \
}
#else
# define _py_object_state_INIT(INTERP) \
Expand Down
8 changes: 0 additions & 8 deletions Include/modsupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,6 @@ PyAPI_FUNC(int) PyModule_ExecDef(PyObject *module, PyModuleDef *def);
#define PYTHON_ABI_VERSION 3
#define PYTHON_ABI_STRING "3"

#ifdef Py_TRACE_REFS
/* When we are tracing reference counts, rename module creation functions so
modules compiled with incompatible settings will generate a
link-time error. */
#define PyModule_Create2 PyModule_Create2TraceRefs
#define PyModule_FromDefAndSpec2 PyModule_FromDefAndSpec2TraceRefs
#endif

PyAPI_FUNC(PyObject *) PyModule_Create2(PyModuleDef*, int apiver);

#ifdef Py_LIMITED_API
Expand Down
21 changes: 0 additions & 21 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,6 @@ whose size is determined when the object is allocated.
# define Py_REF_DEBUG
#endif

#if defined(Py_LIMITED_API) && defined(Py_TRACE_REFS)
# error Py_LIMITED_API is incompatible with Py_TRACE_REFS
#endif

#ifdef Py_TRACE_REFS
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA \
PyObject *_ob_next; \
PyObject *_ob_prev;

#define _PyObject_EXTRA_INIT _Py_NULL, _Py_NULL,

#else
# define _PyObject_HEAD_EXTRA
# define _PyObject_EXTRA_INIT
#endif

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD PyObject ob_base;

Expand Down Expand Up @@ -130,14 +113,12 @@ check by comparing the reference count field to the immortality reference count.
#ifdef Py_BUILD_CORE
#define PyObject_HEAD_INIT(type) \
{ \
_PyObject_EXTRA_INIT \
{ _Py_IMMORTAL_REFCNT }, \
(type) \
},
#else
#define PyObject_HEAD_INIT(type) \
{ \
_PyObject_EXTRA_INIT \
{ 1 }, \
(type) \
},
Expand All @@ -164,8 +145,6 @@ check by comparing the reference count field to the immortality reference count.
* in addition, be cast to PyVarObject*.
*/
struct _object {
_PyObject_HEAD_EXTRA

#if (defined(__GNUC__) || defined(__clang__)) \
&& !(defined __STDC_VERSION__ && __STDC_VERSION__ >= 201112L)
// On C99 and older, anonymous union is a GCC and clang extension
Expand Down
6 changes: 0 additions & 6 deletions Include/pyport.h
Original file line number Diff line number Diff line change
Expand Up @@ -684,12 +684,6 @@ extern char * _getpty(int *, int, mode_t, int);
# endif
#endif

/* Check that ALT_SOABI is consistent with Py_TRACE_REFS:
./configure --with-trace-refs should must be used to define Py_TRACE_REFS */
#if defined(ALT_SOABI) && defined(Py_TRACE_REFS)
# error "Py_TRACE_REFS ABI is not compatible with release and debug ABI"
#endif

#if defined(__ANDROID__) || defined(__VXWORKS__)
// Use UTF-8 as the locale encoding, ignore the LC_CTYPE locale.
// See _Py_GetLocaleEncoding(), PyUnicode_DecodeLocale()
Expand Down
5 changes: 2 additions & 3 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,9 +779,6 @@ def python_is_optimized():

_header = 'nP'
_align = '0n'
if hasattr(sys, "getobjects"):
_header = '2P' + _header
_align = '0P'
_vheader = _header + 'n'

def calcobjsize(fmt):
Expand Down Expand Up @@ -2469,3 +2466,5 @@ def adjust_int_max_str_digits(max_digits):
#Windows doesn't have os.uname() but it doesn't support s390x.
skip_on_s390x = unittest.skipIf(hasattr(os, 'uname') and os.uname().machine == 's390x',
'skipped on s390x')

Py_TRACE_REFS = hasattr(sys, 'getobjects')
3 changes: 3 additions & 0 deletions Lib/test/test_capi/test_mem.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ def test_pyobject_forbidden_bytes_is_freed(self):
def test_pyobject_freed_is_freed(self):
self.check_pyobject_is_freed('check_pyobject_freed_is_freed')

# Python built with Py_TRACE_REFS fail with a fatal error in
# _PyRefchain_Trace() on memory allocation error.
@unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build')
def test_set_nomemory(self):
code = """if 1:
import _testcapi
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1484,6 +1484,9 @@ def recurse_in_body_and_except():


@cpython_only
# Python built with Py_TRACE_REFS fail with a fatal error in
# _PyRefchain_Trace() on memory allocation error.
@unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build')
def test_recursion_normalizing_with_no_memory(self):
# Issue #30697. Test that in the abort that occurs when there is no
# memory left and the size of the Python frames stack is greater than
Expand Down Expand Up @@ -1652,6 +1655,9 @@ def test_unhandled(self):
self.assertTrue(report.endswith("\n"))

@cpython_only
# Python built with Py_TRACE_REFS fail with a fatal error in
# _PyRefchain_Trace() on memory allocation error.
@unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build')
def test_memory_error_in_PyErr_PrintEx(self):
code = """if 1:
import _testcapi
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from test.support import os_helper
from test.support import (
STDLIB_DIR, swap_attr, swap_item, cpython_only, is_emscripten,
is_wasi, run_in_subinterp, run_in_subinterp_with_config)
is_wasi, run_in_subinterp, run_in_subinterp_with_config, Py_TRACE_REFS)
from test.support.import_helper import (
forget, make_legacy_pyc, unlink, unload, DirsOnSysPath, CleanImport)
from test.support.os_helper import (
Expand Down Expand Up @@ -2555,7 +2555,7 @@ def test_basic_multiple_interpreters_main_no_reset(self):
def test_basic_multiple_interpreters_deleted_no_reset(self):
# without resetting; already loaded in a deleted interpreter

if hasattr(sys, 'getobjects'):
if Py_TRACE_REFS:
# It's a Py_TRACE_REFS build.
# This test breaks interpreter isolation a little,
# which causes problems on Py_TRACE_REF builds.
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import unittest
import subprocess
from textwrap import dedent
from test import support
from test.support import cpython_only, has_subprocess_support, SuppressCrashReport
from test.support.script_helper import kill_python

Expand Down Expand Up @@ -59,6 +60,9 @@ def run_on_interactive_mode(source):
class TestInteractiveInterpreter(unittest.TestCase):

@cpython_only
# Python built with Py_TRACE_REFS fail with a fatal error in
# _PyRefchain_Trace() on memory allocation error.
@unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build')
def test_no_memory(self):
# Issue #30696: Fix the interactive interpreter looping endlessly when
# no memory. Check also that the fix does not break the interactive
Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,27 @@ def test_stdlib_dir(self):
self.assertEqual(os.path.normpath(sys._stdlib_dir),
os.path.normpath(expected))

@unittest.skipUnless(hasattr(sys, 'getobjects'), 'need sys.getobjects()')
def test_getobjects(self):
# sys.getobjects(0)
all_objects = sys.getobjects(0)
self.assertIsInstance(all_objects, list)
self.assertGreater(len(all_objects), 0)

# sys.getobjects(0, MyType)
class MyType:
pass
size = 100
my_objects = [MyType() for _ in range(size)]
get_objects = sys.getobjects(0, MyType)
self.assertEqual(len(get_objects), size)
for obj in get_objects:
self.assertIsInstance(obj, MyType)

# sys.getobjects(3, MyType)
get_objects = sys.getobjects(3, MyType)
self.assertEqual(len(get_objects), 3)


@test.support.cpython_only
class UnraisableHookTest(unittest.TestCase):
Expand Down
20 changes: 11 additions & 9 deletions Lib/test/test_tracemalloc.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,11 @@ def test_set_traceback_limit(self):
self.assertEqual(len(traceback), 1)
self.assertEqual(traceback, obj_traceback)

def find_trace(self, traces, traceback):
def find_trace(self, traces, traceback, size):
# filter also by size to ignore the memory allocated by
# _PyRefchain_Trace() if Python is built with Py_TRACE_REFS.
for trace in traces:
if trace[2] == traceback._frames:
if trace[2] == traceback._frames and trace[1] == size:
return trace

self.fail("trace not found")
Expand All @@ -186,11 +188,10 @@ def test_get_traces(self):
obj, obj_traceback = allocate_bytes(obj_size)

traces = tracemalloc._get_traces()
trace = self.find_trace(traces, obj_traceback)
trace = self.find_trace(traces, obj_traceback, obj_size)

self.assertIsInstance(trace, tuple)
domain, size, traceback, length = trace
self.assertEqual(size, obj_size)
self.assertEqual(traceback, obj_traceback._frames)

tracemalloc.stop()
Expand All @@ -208,17 +209,18 @@ def allocate_bytes4(size):
# Ensure that two identical tracebacks are not duplicated
tracemalloc.stop()
tracemalloc.start(4)
obj_size = 123
obj1, obj1_traceback = allocate_bytes4(obj_size)
obj2, obj2_traceback = allocate_bytes4(obj_size)
obj1_size = 123
obj2_size = 125
obj1, obj1_traceback = allocate_bytes4(obj1_size)
obj2, obj2_traceback = allocate_bytes4(obj2_size)

traces = tracemalloc._get_traces()

obj1_traceback._frames = tuple(reversed(obj1_traceback._frames))
obj2_traceback._frames = tuple(reversed(obj2_traceback._frames))

trace1 = self.find_trace(traces, obj1_traceback)
trace2 = self.find_trace(traces, obj2_traceback)
trace1 = self.find_trace(traces, obj1_traceback, obj1_size)
trace2 = self.find_trace(traces, obj2_traceback, obj2_size)
domain1, size1, traceback1, length1 = trace1
domain2, size2, traceback2, length2 = trace2
self.assertIs(traceback2, traceback1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Python built with :file:`configure` :option:`--with-trace-refs` (tracing
references) is now ABI compatible with Python release build and :ref:`debug
build <debug-build>`. Patch by Victor Stinner.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Python built with :file:`configure` :option:`--with-trace-refs` (tracing
references) now supports the :ref:`Limited API <limited-c-api>`. Patch by
Victor Stinner.
Loading

0 comments on commit 13a0007

Please sign in to comment.