Issue #13390: New function :func:`sys.getallocatedblocks()` returns the number of memory blocks currently allocated.

Also, the ``-R`` option to regrtest uses this function to guard against memory allocation leaks.
This commit is contained in:
Antoine Pitrou 2012-12-09 14:28:26 +01:00
parent b4b8f234d4
commit f9d0b1256f
9 changed files with 123 additions and 22 deletions

View File

@ -393,6 +393,20 @@ always available.
.. versionadded:: 3.1 .. versionadded:: 3.1
.. function:: getallocatedblocks()
Return the number of memory blocks currently allocated by the interpreter,
regardless of their size. This function is mainly useful for debugging
small memory leaks. Because of the interpreter's internal caches, the
result can vary from call to call; you may have to call
:func:`_clear_type_cache()` to get more predictable results.
.. versionadded:: 3.4
.. impl-detail::
Not all Python implementations may be able to return this information.
.. function:: getcheckinterval() .. function:: getcheckinterval()
Return the interpreter's "check interval"; see :func:`setcheckinterval`. Return the interpreter's "check interval"; see :func:`setcheckinterval`.

View File

@ -98,6 +98,8 @@ PyAPI_FUNC(void *) PyObject_Malloc(size_t);
PyAPI_FUNC(void *) PyObject_Realloc(void *, size_t); PyAPI_FUNC(void *) PyObject_Realloc(void *, size_t);
PyAPI_FUNC(void) PyObject_Free(void *); PyAPI_FUNC(void) PyObject_Free(void *);
/* This function returns the number of allocated memory blocks, regardless of size */
PyAPI_FUNC(Py_ssize_t) _Py_GetAllocatedBlocks(void);
/* Macros */ /* Macros */
#ifdef WITH_PYMALLOC #ifdef WITH_PYMALLOC

View File

@ -615,7 +615,7 @@ def main(tests=None, testdir=None, verbose=0, quiet=False,
sys.exit(2) sys.exit(2)
from queue import Queue from queue import Queue
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
debug_output_pat = re.compile(r"\[\d+ refs\]$") debug_output_pat = re.compile(r"\[\d+ refs, \d+ blocks\]$")
output = Queue() output = Queue()
pending = MultiprocessTests(tests) pending = MultiprocessTests(tests)
opt_args = support.args_from_interpreter_flags() opt_args = support.args_from_interpreter_flags()
@ -1320,33 +1320,50 @@ def dash_R(the_module, test, indirect_test, huntrleaks):
del sys.modules[the_module.__name__] del sys.modules[the_module.__name__]
exec('import ' + the_module.__name__) exec('import ' + the_module.__name__)
deltas = []
nwarmup, ntracked, fname = huntrleaks nwarmup, ntracked, fname = huntrleaks
fname = os.path.join(support.SAVEDCWD, fname) fname = os.path.join(support.SAVEDCWD, fname)
repcount = nwarmup + ntracked repcount = nwarmup + ntracked
rc_deltas = [0] * repcount
alloc_deltas = [0] * repcount
print("beginning", repcount, "repetitions", file=sys.stderr) print("beginning", repcount, "repetitions", file=sys.stderr)
print(("1234567890"*(repcount//10 + 1))[:repcount], file=sys.stderr) print(("1234567890"*(repcount//10 + 1))[:repcount], file=sys.stderr)
sys.stderr.flush() sys.stderr.flush()
dash_R_cleanup(fs, ps, pic, zdc, abcs)
for i in range(repcount): for i in range(repcount):
rc_before = sys.gettotalrefcount()
run_the_test() run_the_test()
alloc_after, rc_after = dash_R_cleanup(fs, ps, pic, zdc, abcs)
sys.stderr.write('.') sys.stderr.write('.')
sys.stderr.flush() sys.stderr.flush()
dash_R_cleanup(fs, ps, pic, zdc, abcs)
rc_after = sys.gettotalrefcount()
if i >= nwarmup: if i >= nwarmup:
deltas.append(rc_after - rc_before) rc_deltas[i] = rc_after - rc_before
alloc_deltas[i] = alloc_after - alloc_before
alloc_before, rc_before = alloc_after, rc_after
print(file=sys.stderr) print(file=sys.stderr)
if any(deltas): # These checkers return False on success, True on failure
msg = '%s leaked %s references, sum=%s' % (test, deltas, sum(deltas)) def check_rc_deltas(deltas):
return any(deltas)
def check_alloc_deltas(deltas):
# At least 1/3rd of 0s
if 3 * deltas.count(0) < len(deltas):
return True
# Nothing else than 1s, 0s and -1s
if not set(deltas) <= {1,0,-1}:
return True
return False
failed = False
for deltas, item_name, checker in [
(rc_deltas, 'references', check_rc_deltas),
(alloc_deltas, 'memory blocks', check_alloc_deltas)]:
if checker(deltas):
msg = '%s leaked %s %s, sum=%s' % (
test, deltas[nwarmup:], item_name, sum(deltas))
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
sys.stderr.flush() sys.stderr.flush()
with open(fname, "a") as refrep: with open(fname, "a") as refrep:
print(msg, file=refrep) print(msg, file=refrep)
refrep.flush() refrep.flush()
return True failed = True
return False return failed
def dash_R_cleanup(fs, ps, pic, zdc, abcs): def dash_R_cleanup(fs, ps, pic, zdc, abcs):
import gc, copyreg import gc, copyreg
@ -1412,8 +1429,11 @@ def dash_R_cleanup(fs, ps, pic, zdc, abcs):
else: else:
ctypes._reset_cache() ctypes._reset_cache()
# Collect cyclic trash. # Collect cyclic trash and read memory statistics immediately after.
func1 = sys.getallocatedblocks
func2 = sys.gettotalrefcount
gc.collect() gc.collect()
return func1(), func2()
def warm_caches(): def warm_caches():
# char cache # char cache

View File

@ -1772,7 +1772,7 @@ def strip_python_stderr(stderr):
This will typically be run on the result of the communicate() method This will typically be run on the result of the communicate() method
of a subprocess.Popen object. of a subprocess.Popen object.
""" """
stderr = re.sub(br"\[\d+ refs\]\r?\n?", b"", stderr).strip() stderr = re.sub(br"\[\d+ refs, \d+ blocks\]\r?\n?", b"", stderr).strip()
return stderr return stderr
def args_from_interpreter_flags(): def args_from_interpreter_flags():

View File

@ -6,6 +6,7 @@ import textwrap
import warnings import warnings
import operator import operator
import codecs import codecs
import gc
# count the number of test runs, used to create unique # count the number of test runs, used to create unique
# strings to intern in test_intern() # strings to intern in test_intern()
@ -611,6 +612,29 @@ class SysModuleTest(unittest.TestCase):
ret, out, err = assert_python_ok(*args) ret, out, err = assert_python_ok(*args)
self.assertIn(b"free PyDictObjects", err) self.assertIn(b"free PyDictObjects", err)
@unittest.skipUnless(hasattr(sys, "getallocatedblocks"),
"sys.getallocatedblocks unavailable on this build")
def test_getallocatedblocks(self):
# Some sanity checks
a = sys.getallocatedblocks()
self.assertIs(type(a), int)
self.assertGreater(a, 0)
try:
# While we could imagine a Python session where the number of
# multiple buffer objects would exceed the sharing of references,
# it is unlikely to happen in a normal test run.
self.assertLess(a, sys.gettotalrefcount())
except AttributeError:
# gettotalrefcount() not available
pass
gc.collect()
b = sys.getallocatedblocks()
self.assertLessEqual(b, a)
gc.collect()
c = sys.getallocatedblocks()
self.assertIn(c, range(b - 50, b + 50))
class SizeofTest(unittest.TestCase): class SizeofTest(unittest.TestCase):
def setUp(self): def setUp(self):

View File

@ -163,6 +163,9 @@ Core and Builtins
Library Library
------- -------
- Issue #13390: New function :func:`sys.getallocatedblocks()` returns the
number of memory blocks currently allocated.
- Issue #16628: Fix a memory leak in ctypes.resize(). - Issue #16628: Fix a memory leak in ctypes.resize().
- Issue #13614: Fix setup.py register failure with invalid rst in description. - Issue #13614: Fix setup.py register failure with invalid rst in description.
@ -433,6 +436,9 @@ Extension Modules
Tests Tests
----- -----
- Issue #13390: The ``-R`` option to regrtest now also checks for memory
allocation leaks, using :func:`sys.getallocatedblocks()`.
- Issue #16559: Add more tests for the json module, including some from the - Issue #16559: Add more tests for the json module, including some from the
official test suite at json.org. Patch by Serhiy Storchaka. official test suite at json.org. Patch by Serhiy Storchaka.

View File

@ -525,6 +525,15 @@ static size_t ntimes_arena_allocated = 0;
/* High water mark (max value ever seen) for narenas_currently_allocated. */ /* High water mark (max value ever seen) for narenas_currently_allocated. */
static size_t narenas_highwater = 0; static size_t narenas_highwater = 0;
static Py_ssize_t _Py_AllocatedBlocks = 0;
Py_ssize_t
_Py_GetAllocatedBlocks(void)
{
return _Py_AllocatedBlocks;
}
/* Allocate a new arena. If we run out of memory, return NULL. Else /* Allocate a new arena. If we run out of memory, return NULL. Else
* allocate a new arena, and return the address of an arena_object * allocate a new arena, and return the address of an arena_object
* describing the new arena. It's expected that the caller will set * describing the new arena. It's expected that the caller will set
@ -785,6 +794,8 @@ PyObject_Malloc(size_t nbytes)
if (nbytes > PY_SSIZE_T_MAX) if (nbytes > PY_SSIZE_T_MAX)
return NULL; return NULL;
_Py_AllocatedBlocks++;
/* /*
* This implicitly redirects malloc(0). * This implicitly redirects malloc(0).
*/ */
@ -901,6 +912,7 @@ PyObject_Malloc(size_t nbytes)
* and free list are already initialized. * and free list are already initialized.
*/ */
bp = pool->freeblock; bp = pool->freeblock;
assert(bp != NULL);
pool->freeblock = *(block **)bp; pool->freeblock = *(block **)bp;
UNLOCK(); UNLOCK();
return (void *)bp; return (void *)bp;
@ -958,7 +970,12 @@ redirect:
*/ */
if (nbytes == 0) if (nbytes == 0)
nbytes = 1; nbytes = 1;
return (void *)malloc(nbytes); {
void *result = malloc(nbytes);
if (!result)
_Py_AllocatedBlocks--;
return result;
}
} }
/* free */ /* free */
@ -978,6 +995,8 @@ PyObject_Free(void *p)
if (p == NULL) /* free(NULL) has no effect */ if (p == NULL) /* free(NULL) has no effect */
return; return;
_Py_AllocatedBlocks--;
#ifdef WITH_VALGRIND #ifdef WITH_VALGRIND
if (UNLIKELY(running_on_valgrind > 0)) if (UNLIKELY(running_on_valgrind > 0))
goto redirect; goto redirect;

View File

@ -39,8 +39,9 @@
#define PRINT_TOTAL_REFS() #define PRINT_TOTAL_REFS()
#else /* Py_REF_DEBUG */ #else /* Py_REF_DEBUG */
#define PRINT_TOTAL_REFS() fprintf(stderr, \ #define PRINT_TOTAL_REFS() fprintf(stderr, \
"[%" PY_FORMAT_SIZE_T "d refs]\n", \ "[%" PY_FORMAT_SIZE_T "d refs, " \
_Py_GetRefTotal()) "%" PY_FORMAT_SIZE_T "d blocks]\n", \
_Py_GetRefTotal(), _Py_GetAllocatedBlocks())
#endif #endif
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -894,6 +894,19 @@ one higher than you might expect, because it includes the (temporary)\n\
reference as an argument to getrefcount()." reference as an argument to getrefcount()."
); );
static PyObject *
sys_getallocatedblocks(PyObject *self)
{
return PyLong_FromSsize_t(_Py_GetAllocatedBlocks());
}
PyDoc_STRVAR(getallocatedblocks_doc,
"getallocatedblocks() -> integer\n\
\n\
Return the number of memory blocks currently allocated, regardless of their\n\
size."
);
#ifdef COUNT_ALLOCS #ifdef COUNT_ALLOCS
static PyObject * static PyObject *
sys_getcounts(PyObject *self) sys_getcounts(PyObject *self)
@ -1062,6 +1075,8 @@ static PyMethodDef sys_methods[] = {
{"getdlopenflags", (PyCFunction)sys_getdlopenflags, METH_NOARGS, {"getdlopenflags", (PyCFunction)sys_getdlopenflags, METH_NOARGS,
getdlopenflags_doc}, getdlopenflags_doc},
#endif #endif
{"getallocatedblocks", (PyCFunction)sys_getallocatedblocks, METH_NOARGS,
getallocatedblocks_doc},
#ifdef COUNT_ALLOCS #ifdef COUNT_ALLOCS
{"getcounts", (PyCFunction)sys_getcounts, METH_NOARGS}, {"getcounts", (PyCFunction)sys_getcounts, METH_NOARGS},
#endif #endif