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:
parent
b4b8f234d4
commit
f9d0b1256f
|
@ -393,6 +393,20 @@ always available.
|
|||
.. 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()
|
||||
|
||||
Return the interpreter's "check interval"; see :func:`setcheckinterval`.
|
||||
|
|
|
@ -98,6 +98,8 @@ PyAPI_FUNC(void *) PyObject_Malloc(size_t);
|
|||
PyAPI_FUNC(void *) PyObject_Realloc(void *, size_t);
|
||||
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 */
|
||||
#ifdef WITH_PYMALLOC
|
||||
|
|
|
@ -615,7 +615,7 @@ def main(tests=None, testdir=None, verbose=0, quiet=False,
|
|||
sys.exit(2)
|
||||
from queue import Queue
|
||||
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()
|
||||
pending = MultiprocessTests(tests)
|
||||
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__]
|
||||
exec('import ' + the_module.__name__)
|
||||
|
||||
deltas = []
|
||||
nwarmup, ntracked, fname = huntrleaks
|
||||
fname = os.path.join(support.SAVEDCWD, fname)
|
||||
repcount = nwarmup + ntracked
|
||||
rc_deltas = [0] * repcount
|
||||
alloc_deltas = [0] * repcount
|
||||
|
||||
print("beginning", repcount, "repetitions", file=sys.stderr)
|
||||
print(("1234567890"*(repcount//10 + 1))[:repcount], file=sys.stderr)
|
||||
sys.stderr.flush()
|
||||
dash_R_cleanup(fs, ps, pic, zdc, abcs)
|
||||
for i in range(repcount):
|
||||
rc_before = sys.gettotalrefcount()
|
||||
run_the_test()
|
||||
alloc_after, rc_after = dash_R_cleanup(fs, ps, pic, zdc, abcs)
|
||||
sys.stderr.write('.')
|
||||
sys.stderr.flush()
|
||||
dash_R_cleanup(fs, ps, pic, zdc, abcs)
|
||||
rc_after = sys.gettotalrefcount()
|
||||
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)
|
||||
if any(deltas):
|
||||
msg = '%s leaked %s references, sum=%s' % (test, deltas, sum(deltas))
|
||||
# These checkers return False on success, True on failure
|
||||
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)
|
||||
sys.stderr.flush()
|
||||
with open(fname, "a") as refrep:
|
||||
print(msg, file=refrep)
|
||||
refrep.flush()
|
||||
return True
|
||||
return False
|
||||
failed = True
|
||||
return failed
|
||||
|
||||
def dash_R_cleanup(fs, ps, pic, zdc, abcs):
|
||||
import gc, copyreg
|
||||
|
@ -1412,8 +1429,11 @@ def dash_R_cleanup(fs, ps, pic, zdc, abcs):
|
|||
else:
|
||||
ctypes._reset_cache()
|
||||
|
||||
# Collect cyclic trash.
|
||||
# Collect cyclic trash and read memory statistics immediately after.
|
||||
func1 = sys.getallocatedblocks
|
||||
func2 = sys.gettotalrefcount
|
||||
gc.collect()
|
||||
return func1(), func2()
|
||||
|
||||
def warm_caches():
|
||||
# char cache
|
||||
|
|
|
@ -1772,7 +1772,7 @@ def strip_python_stderr(stderr):
|
|||
This will typically be run on the result of the communicate() method
|
||||
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
|
||||
|
||||
def args_from_interpreter_flags():
|
||||
|
|
|
@ -6,6 +6,7 @@ import textwrap
|
|||
import warnings
|
||||
import operator
|
||||
import codecs
|
||||
import gc
|
||||
|
||||
# count the number of test runs, used to create unique
|
||||
# strings to intern in test_intern()
|
||||
|
@ -611,6 +612,29 @@ class SysModuleTest(unittest.TestCase):
|
|||
ret, out, err = assert_python_ok(*args)
|
||||
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):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -163,6 +163,9 @@ Core and Builtins
|
|||
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 #13614: Fix setup.py register failure with invalid rst in description.
|
||||
|
@ -433,6 +436,9 @@ Extension Modules
|
|||
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
|
||||
official test suite at json.org. Patch by Serhiy Storchaka.
|
||||
|
||||
|
|
|
@ -525,6 +525,15 @@ static size_t ntimes_arena_allocated = 0;
|
|||
/* High water mark (max value ever seen) for narenas_currently_allocated. */
|
||||
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, and return the address of an arena_object
|
||||
* 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)
|
||||
return NULL;
|
||||
|
||||
_Py_AllocatedBlocks++;
|
||||
|
||||
/*
|
||||
* This implicitly redirects malloc(0).
|
||||
*/
|
||||
|
@ -901,6 +912,7 @@ PyObject_Malloc(size_t nbytes)
|
|||
* and free list are already initialized.
|
||||
*/
|
||||
bp = pool->freeblock;
|
||||
assert(bp != NULL);
|
||||
pool->freeblock = *(block **)bp;
|
||||
UNLOCK();
|
||||
return (void *)bp;
|
||||
|
@ -958,7 +970,12 @@ redirect:
|
|||
*/
|
||||
if (nbytes == 0)
|
||||
nbytes = 1;
|
||||
return (void *)malloc(nbytes);
|
||||
{
|
||||
void *result = malloc(nbytes);
|
||||
if (!result)
|
||||
_Py_AllocatedBlocks--;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/* free */
|
||||
|
@ -978,6 +995,8 @@ PyObject_Free(void *p)
|
|||
if (p == NULL) /* free(NULL) has no effect */
|
||||
return;
|
||||
|
||||
_Py_AllocatedBlocks--;
|
||||
|
||||
#ifdef WITH_VALGRIND
|
||||
if (UNLIKELY(running_on_valgrind > 0))
|
||||
goto redirect;
|
||||
|
|
|
@ -39,8 +39,9 @@
|
|||
#define PRINT_TOTAL_REFS()
|
||||
#else /* Py_REF_DEBUG */
|
||||
#define PRINT_TOTAL_REFS() fprintf(stderr, \
|
||||
"[%" PY_FORMAT_SIZE_T "d refs]\n", \
|
||||
_Py_GetRefTotal())
|
||||
"[%" PY_FORMAT_SIZE_T "d refs, " \
|
||||
"%" PY_FORMAT_SIZE_T "d blocks]\n", \
|
||||
_Py_GetRefTotal(), _Py_GetAllocatedBlocks())
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
|
@ -894,6 +894,19 @@ one higher than you might expect, because it includes the (temporary)\n\
|
|||
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
|
||||
static PyObject *
|
||||
sys_getcounts(PyObject *self)
|
||||
|
@ -1062,6 +1075,8 @@ static PyMethodDef sys_methods[] = {
|
|||
{"getdlopenflags", (PyCFunction)sys_getdlopenflags, METH_NOARGS,
|
||||
getdlopenflags_doc},
|
||||
#endif
|
||||
{"getallocatedblocks", (PyCFunction)sys_getallocatedblocks, METH_NOARGS,
|
||||
getallocatedblocks_doc},
|
||||
#ifdef COUNT_ALLOCS
|
||||
{"getcounts", (PyCFunction)sys_getcounts, METH_NOARGS},
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue