mirror of https://github.com/python/cpython
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
|
.. 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`.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue