Skip to content

Add a new tracemalloc module to trace memory allocations #63074

Closed

Description

@vstinner
BPO 18874
Nosy @tim-one, @rhettinger, @jcea, @amauryfa, @abalkin, @vstinner, @ericsnowcurrently, @JimJJewett, @vajrasky
Dependencies
  • bpo-16742: PyOS_Readline drops GIL and calls PyOS_StdioReadline, which isn't thread safe
  • Files
  • 8b34364a66a9.patch
  • 4430e893d89f.patch
  • pack.patch
  • 3de17d13002d.patch
  • start_nframe.patch
  • minor_makeup_test_tracemalloc.patch
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = <Date 2013-11-26.12:59:26.321>
    created_at = <Date 2013-08-28.23:53:11.383>
    labels = ['type-feature']
    title = 'Add a new tracemalloc module to trace memory allocations'
    updated_at = <Date 2013-12-10.17:49:41.675>
    user = 'https://github.com/vstinner'

    bugs.python.org fields:

    activity = <Date 2013-12-10.17:49:41.675>
    actor = 'jcea'
    assignee = 'none'
    closed = True
    closed_date = <Date 2013-11-26.12:59:26.321>
    closer = 'vstinner'
    components = []
    creation = <Date 2013-08-28.23:53:11.383>
    creator = 'vstinner'
    dependencies = ['16742']
    files = ['32555', '32724', '32734', '32753', '32789', '32828']
    hgrepos = ['206']
    issue_num = 18874
    keywords = ['patch']
    message_count = 57.0
    messages = ['196436', '196437', '196438', '196439', '196474', '196484', '196525', '196720', '196879', '197951', '197952', '197954', '198765', '198767', '199039', '199040', '199041', '199085', '199086', '199123', '199386', '200341', '202024', '202438', '202492', '203343', '203485', '203508', '203534', '203536', '203587', '203589', '203615', '203616', '203620', '203621', '203624', '203659', '203669', '203670', '203676', '203815', '203862', '203947', '203988', '203990', '204297', '204309', '204310', '204431', '204432', '204435', '204444', '204472', '204618', '204619', '204622']
    nosy_count = 11.0
    nosy_names = ['tim.peters', 'rhettinger', 'jcea', 'amaury.forgeotdarc', 'belopolsky', 'vstinner', 'neologix', 'python-dev', 'eric.snow', 'Jim.Jewett', 'vajrasky']
    pr_nums = []
    priority = 'normal'
    resolution = 'fixed'
    stage = None
    status = 'closed'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue18874'
    versions = ['Python 3.4']

    Activity

    1. vstinner commented on Aug 28, 2013

      @vstinner
      MemberAuthor

      Thanks to the PEP-445, it becomes possible to trace easily memory allocations. I propose to add a new tracemalloc module computing the memory usage per file and per line number.

      I implemented the module as a third party module for Python 2.5-3.4, but it requires to patch and recompile Python:
      https://pypi.python.org/pypi/pytracemalloc

      My proposed implementation is different:

      • use a simple C implementation of an hash table called "cfuhash" (coming from the libcfu project) instead of depending on the glib library ; I simplified and adapted the implementation for my usage

      • no enable() / disable() function: tracemalloc can only be enabled before startup by setting PYTHONTRACEMALLOC=1 environment variable

      • traces (size of the memory block, Python filename, Python line number) are stored directly in the memory block, not in a separated hash table

      I chose PYTHONTRACEMALLOC env var instead of enable()/disable() functions to be able to really trace *all* memory allocated by Python, especially memory allocated at startup, during Python initialization.

      TODO:

      • The (high-level) API should be reviewed and discussed

      • The documention should be improved

      • PyMem_Raw API is not hooked yet because the code is not thread-safe (it relies on the Python GIL)

      • Filenames should not be encoded: the hash table should directly use Unicode strings (PyObject*), encoding a filename allocates memory and may call the garbage collector

      • Memory is not released at exit

      For the documentation, see the following page:
      https://pypi.python.org/pypi/pytracemalloc

      See also the https://bitbucket.org/haypo/pyfailmalloc project and the issue bpo-18408.

    2. vstinner commented on Aug 29, 2013

      @vstinner
      MemberAuthor

      Example of output:

      $ PYTHONTRACEMALLOC=1 ./python -m test
      ...
      == CPython 3.4.0a1+ (default:2ce9e5f6b47c+, Aug 29 2013, 02:03:02) [GCC 4.7.2 20121109 (Red Hat 4.7.2-8)]
      ==   Linux-3.9.4-200.fc18.x86_64-x86_64-with-fedora-18-Spherical_Cow little-endian
      ==   /home/haypo/prog/python/tracemalloc/build/test_python_11087
      ...
      [  1/380] test_grammar
      [  2/380] test_opcodes
      [  3/380] test_dict
      [  4/380] test_builtin
      [  5/380] test_exceptions
      [  6/380] test_types
      [  7/380] test_unittest

      2013-08-29 02:06:22: Top 25 allocations per file and line
      #1: <frozen importlib._bootstrap>:704: size=5 MiB, count=56227, average=105 B
      #2: .../tracemalloc/Lib/linecache.py:127: size=1004 KiB, count=8706, average=118 B
      #3: .../Lib/unittest/mock.py:1764: size=895 KiB, count=7841, average=116 B
      #4: .../Lib/unittest/mock.py:1805: size=817 KiB, count=15101, average=55 B
      #5: .../Lib/test/test_dict.py:35: size=768 KiB, count=8, average=96 KiB
      #6: <frozen importlib._bootstrap>:274: size=703 KiB, count=4604, average=156 B
      #7: ???:?: size=511 KiB, count=4445, average=117 B
      #8: .../Lib/unittest/mock.py:350: size=370 KiB, count=1227, average=308 B
      #9: .../Lib/unittest/case.py:306: size=343 KiB, count=1390, average=253 B
      #10: .../Lib/unittest/case.py:496: size=330 KiB, count=650, average=521 B
      #11: .../Lib/unittest/case.py:327: size=291 KiB, count=717, average=416 B
      #12: .../Lib/collections/init.py:368: size=239 KiB, count=2170, average=113 B
      #13: .../Lib/test/test_grammar.py:132: size=195 KiB, count=1250, average=159 B
      #14: .../Lib/unittest/mock.py:379: size=118 KiB, count=152, average=800 B
      #15: .../tracemalloc/Lib/contextlib.py:37: size=102 KiB, count=672, average=156 B
      #16: <frozen importlib._bootstrap>:1430: size=91 KiB, count=1193, average=78 B
      #17: .../tracemalloc/Lib/inspect.py:1399: size=79 KiB, count=104, average=784 B
      #18: .../tracemalloc/Lib/abc.py:133: size=77 KiB, count=275, average=289 B
      #19: .../Lib/unittest/case.py:43: size=73 KiB, count=593, average=127 B
      #20: .../Lib/unittest/mock.py:491: size=67 KiB, count=153, average=450 B
      #21: <frozen importlib._bootstrap>:1438: size=64 KiB, count=20, average=3321 B
      #22: .../Lib/unittest/case.py:535: size=56 KiB, count=76, average=766 B
      #23: .../tracemalloc/Lib/sre_compile.py:508: size=54 KiB, count=115, average=485 B
      #24: .../Lib/unittest/case.py:300: size=48 KiB, count=616, average=80 B
      #25: .../Lib/test/regrtest.py:1207: size=48 KiB, count=2, average=24 KiB
      7333 more: size=4991 KiB, count=28051, average=182 B
      Total Python memory: size=17 MiB, count=136358, average=136 B
      Total process memory: size=42 MiB (ignore tracemalloc: 22 KiB)

    3. vstinner commented on Aug 29, 2013

      @vstinner
      MemberAuthor

      tracemalloc.get_process_memory() should be moved to the shutil module.

    4. vstinner commented on Aug 29, 2013

      @vstinner
      MemberAuthor

      I'm not sure that the DisplayGarbage class is still useful. The PEP-442 has been implemented in Python 3.4, so objects with __del__() method are no more "uncollectable". I don't know which kind of objects can enter the gc.garbage list (by default, not using gc.set_debug(gc.DEBUG_SAVEALL)).

    5. amauryfa commented on Aug 29, 2013

      @amauryfa
      Contributor

      Is it really impossible to use a standard Python dict instead of cfuhash?

    6. vstinner commented on Aug 29, 2013

      @vstinner
      MemberAuthor

      Is it really impossible to use a standard Python dict instead of cfuhash?

      I don't know if it is possible or not, I didn't try. Using PyDict has many requirements:

      • the GIL must be held: I plan to hook also PyMem_Raw (which is called without the GIL being held)
      • PyDict uses PyMem_Malloc() which would be a reentrant call
      • the API uses PyObject* whereas tracemalloc uses C types and C structures
      • PyDict can raises Python exceptions, which is not something expected from a memory allocator (especially PyMem_Malloc)
      • etc.

      The implementation is not done yet. I would like to be able to compute the size of memory allocated by the _tracemalloc module: size of hash tables.

    7. vstinner commented on Aug 30, 2013

      @vstinner
      MemberAuthor

      Ok, let's start with a first patch. It works in the common cases, because they are some corner cases like subinterpreter which might still crash.

      The hook on PyMem_RawMalloc() takes the GIL. It is disabled because it has still bugs (it introduces a deadlock in some corner cases!)

    8. vstinner commented on Sep 1, 2013

      @vstinner
      MemberAuthor

      New patch (version 2):

      • an hash table entry can now contain the data directly instead of a pointer to the data
      • _tracemalloc._get_stats() copies the hash table to not have to lock the hash table too long, which will be especially useful for TRACE_RAW_MALLOC
      • Add get_object_trace() function which returns a namedtuple
      • get_process_memory() returns a namedtuple
      • DisplayGarbage class has been removed: the PEP-442 has been implemented, it is no more interesting to trace uncollectable objects because they became very rare. If you still want to do that, get_object_trace() is available

      TODO:

      • TRACE_RAW_MALLOC define is still disabled because it introduces a deadlock when PyMem_RawMalloc() is called indirectly by Py_NewInterpreter()
      • minor pending FIXME in the code
      • review the high level API: add an helper to build a diff between two snapshots and drop colors (not portable)?

      test_capi deadlock with TRACE_RAW_MALLOC:

      test_subinterps (test.test_capi.SubinterpreterTest) ... ^C
      Program received signal SIGINT, Interrupt.
      0xb7fdd424 in __kernel_vsyscall ()
      (gdb) where
      ...
      #2  0x081abc1c in PyCOND_TIMEDWAIT (cond=0x83470e0, mut=0x8347110, us=5000) at [Python/condvar.h:103](https://github.com/python/cpython/blob/main/Python/condvar.h#L103)
      #3  0x081ac55c in take_gil (tstate=0x8349c78) at [Python/ceval_gil.h:224](https://github.com/python/cpython/blob/main/Python/ceval_gil.h#L224)
      #4  0x081ad0a9 in PyEval_RestoreThread (tstate=0x8349c78) at [Python/ceval.c:443](https://github.com/python/cpython/blob/main/Python/ceval.c#L443)
      #5  0x081f08e3 in PyGILState_Ensure () at [Python/pystate.c:786](https://github.com/python/cpython/blob/main/Python/pystate.c#L786)
      #6  0x0808df1b in trace_get_filename (trace=0x84c26b8, gil_held=0) at ./Modules/_tracemalloc.c:555
      #7  0x0808e314 in trace_log_alloc (ptr=0x84c9f18, size=52, gil_held=0) at ./Modules/_tracemalloc.c:698
      #8  0x0808e449 in trace_malloc (ctx=0x8342890, size=52, gil_held=0) at ./Modules/_tracemalloc.c:744
      #9  0x0808e5b8 in trace_raw_malloc (ctx=0x8342890, size=52) at ./Modules/_tracemalloc.c:811
      #10 0x0805d32a in PyMem_RawMalloc (size=52) at [Objects/obmalloc.c:256](https://github.com/python/cpython/blob/main/Objects/obmalloc.c#L256)
      #11 0x081eeb79 in PyInterpreterState_New () at [Python/pystate.c:63](https://github.com/python/cpython/blob/main/Python/pystate.c#L63)
      #12 0x08060c8b in Py_NewInterpreter () at [Python/pythonrun.c:700](https://github.com/python/cpython/blob/main/Python/pythonrun.c#L700)
      #13 0xb74d4b90 in run_in_subinterp ()
      #14 0x080e9e7a in PyCFunction_Call ()
      ...
      
    9. vstinner commented on Sep 3, 2013

      @vstinner
      MemberAuthor

      Oh, another TODO: drop the (optional) dependency to psutil. Implement get_process_memory() in C (in the _tracemalloc module).

    10. vstinner commented on Sep 16, 2013

      @vstinner
      MemberAuthor

      Updated patch.

    11. vstinner commented on Sep 16, 2013

      @vstinner
      MemberAuthor

      Include/tracemalloc.h: PyTraceMalloc_DisableTemporary() and PyTraceMalloc_RestoreTemporary() should be removed, they were tests to try to fix issues with subinterpreter when tracing PyMem_RawMalloc().

      Lib/test/regrtest.py: These changes should not be commited, they are just here to create easily snapshot files (./python -X tracemalloc -m test).

    12. vstinner commented on Sep 16, 2013

      @vstinner
      MemberAuthor

      TODO list:

      • remove dependency to psutil
      • enable PYMEM_RAW: fix subinterpreter issue, check which variables are protected by which lock
      • test the command line interface?
      • don't trace objects created by get_stats(), reentrant flag should be a thread-local variable
      • add an hash table of tracebacks: 86% of tracebacks are duplicated

      See also the TODO.txt file in the tracemalloc repository.

    13. vstinner commented on Oct 1, 2013

      @vstinner
      MemberAuthor

      The issue bpo-16742 must be fixed to be able to trace memory blocks allocated by PyMem_RawMalloc().

    14. vstinner commented on Oct 1, 2013

      @vstinner
      MemberAuthor

      For the development, it would also be nice to fix bpo-18948.

    15. 30 remaining items

    16. python-dev commented on Nov 23, 2013

      python-devmannequin
      Mannequin

      New changeset 6e2089dbc5ad by Victor Stinner in branch 'default':
      Issue bpo-18874: Implement the PEP-454 (tracemalloc)
      http://hg.python.org/cpython/rev/6e2089dbc5ad

    17. python-dev commented on Nov 23, 2013

      python-devmannequin
      Mannequin

      New changeset 66db0c66a6ee by Victor Stinner in branch 'default':
      Issue bpo-18874: Remove tracemalloc.set_traceback_limit()
      http://hg.python.org/cpython/rev/66db0c66a6ee

    18. vajrasky commented on Nov 25, 2013

      vajraskymannequin
      Mannequin

      Here is the patch to tidy up the Lib/test_tracemalloc.py.

      It fixed the typo, and removed unused import and unused variables.

    19. vstinner commented on Nov 25, 2013

      @vstinner
      MemberAuthor

      Here is the patch to tidy up the Lib/test_tracemalloc.py.
      It fixed the typo, and removed unused import and unused variables.

      Thanks, I applied your patch.

      changeset: 87547:841dec769a04
      tag: tip
      user: Victor Stinner <victor.stinner@gmail.com>
      date: Mon Nov 25 09:29:45 2013 +0100
      files: Lib/test/test_tracemalloc.py
      description:
      Cleanup test_tracemalloc.py. Patch written by Vajrasky Kok.

    20. vstinner commented on Nov 25, 2013

      @vstinner
      MemberAuthor

      > Here is the patch to tidy up the Lib/test_tracemalloc.py.
      > It fixed the typo, and removed unused import and unused variables.

      Thanks, I applied your patch.

      Oh, except:

      •    data = [allocate_bytes(123) for count in range(1000)]
        

      + [allocate_bytes(123) for count in range(1000)]

      If you don't store the result, the memory is immediatly released.

    21. vstinner commented on Nov 25, 2013

      @vstinner
      MemberAuthor

      @neologix: I run test_tracemalloc on "IRIX64 silicon 6.5 07202013 IP35" (our IRIX buildbot) which uses a 32-bit MIPS CPU, and the test pass. The code was compiled with gcc 3.4.6. So packed structures works also on 32-bit MIPS. Well, it is not surprising, int and void* have the same size: 32 bit.

    22. python-dev commented on Nov 25, 2013

      python-devmannequin
      Mannequin

      New changeset 2da28004dfac by Victor Stinner in branch 'default':
      Issue bpo-18874: tracemalloc: explain the purpose of get_traces.tracebacks in a comment
      http://hg.python.org/cpython/rev/2da28004dfac

      New changeset b6414aa8cf77 by Victor Stinner in branch 'default':
      Issue bpo-18874: apply Jim Jewett's patch on tracemalloc doc
      http://hg.python.org/cpython/rev/b6414aa8cf77

    23. python-dev commented on Nov 25, 2013

      python-devmannequin
      Mannequin

      New changeset a2811425dbde by Victor Stinner in branch 'default':
      Issue bpo-18874: allow to call tracemalloc.Snapshot.statistics(cumulative=True)
      http://hg.python.org/cpython/rev/a2811425dbde

    24. python-dev commented on Nov 26, 2013

      python-devmannequin
      Mannequin

      New changeset cf2c4cd08dd9 by Victor Stinner in branch 'default':
      Issue bpo-18874: tracemalloc: Comment the trace_t structure
      http://hg.python.org/cpython/rev/cf2c4cd08dd9

      New changeset f06a50c2bf85 by Victor Stinner in branch 'default':
      Issue bpo-18874: make it more explicit than set_reentrant() only accept 0 or 1
      http://hg.python.org/cpython/rev/f06a50c2bf85

      New changeset 02668e24d72d by Victor Stinner in branch 'default':
      Issue bpo-18874: Fix typo
      http://hg.python.org/cpython/rev/02668e24d72d

    25. vstinner commented on Nov 26, 2013

      @vstinner
      MemberAuthor

      The code has been merged. I didn't see any test_tracemalloc on buildbots. I tried to address all remarks on the Rietveld reviews. So I'm now closing the issue.

      Please open new issue if you have more remarks. For example, I opened:

      • bpo-19798: tracemalloc: rename "max_size" to "peak_size" in get_traced_memory() result
      • bpo-19787: tracemalloc: set_reentrant() should not have to call PyThread_delete_key()
      • bpo-19786: tracemalloc: remove arbitrary limit of 100 frames

      Thanks for your reviews.

    26. jimjjewett commented on Nov 27, 2013

      jimjjewettmannequin
      Mannequin

      These comments refer to http://hg.python.org/cpython/file/5c9af8194d3b/Doc/library/tracemalloc.rst which is newer than the current patches.

      I believe I have consolidated the still-open issues that I see (including those that aren't mine) for this file:

      http://hg.python.org/cpython/file/5c9af8194d3b/Doc/library/tracemalloc.rst

      (line 24, new comment; also repeated at line 315)

      frame (1 frame). To store 25 frames at startup: set the
      :envvar:`PYTHONTRACEMALLOC` environment variable to ``25``, or use the
      :option:`-X` ``tracemalloc=25`` command line option.
      ->

      frame (1 frame). To store 25 frames at startup: set the
      :envvar:`PYTHONTRACEMALLOC` environment variable to ``25``, or use the
      :option:`-X` ``tracemalloc=25`` command line option. If tracing is

      started later, the maximum number of frames is a parameter to the

      :func:`start` function.

      (line 111)

      If the system has little free memory, snapshots can be written on disk using
      the :meth:`Snapshot.dump` method to analyze the snapshot offline. Then use the
      :meth:`Snapshot.load` method reload the snapshot.

      ->

      Snapshots can be stored to disk using :meth:`Snapshot.dump` and reloaded using :meth:`Snapshot.load`.

      (line 180, new comment)
      ... The traceback is
      where the :mod:`importlib` loaded data most recently: on the ``import pdb``
      line of the :mod:`doctest` module. The traceback may change if a new module is
      loaded.

      ->

      The traceback represents the call stack when those allocations were made.

      As this particular summary is itemized by traceback, it also represents

      the call stack for all 903 of those allocations.

      (line 252: new comment)
      .. function:: clear_traces()

      Clear traces of memory blocks allocated by Python.

      Add "Future allocations will still be traced."

      Is this just a shortcut for stop();start() ? If so, please say so. If not, please explain why.

      (Line 311: new comment)
      to measure how much memory is used
      ->
      for an approximate measure of how much memory is used

      I understand your reluctance to get into too many details, but ... well, it isn't a complete measure. It misses the python objects of tracemalloc itself, it misses the overhead of the module, it misses any filenames that were kept alive only by tracing, etc.

      (Line 372, new comment)
      If *inclusive* is ``True`` (include), only trace memory blocks allocated
      in a file with a name matching :attr:`filename_pattern` at line number
      :attr:`lineno`.

        If \*inclusive* is \`\`False\`\` (exclude), ignore memory blocks allocated in
        a file with a name matching :attr:\`filename_pattern\` at line number
        :attr:\`lineno\`.
      

      ->
      If *inclusive* is ``True`` (include), then trace memory blocks allocated
      in a file with a name matching :attr:`filename_pattern` at line number
      :attr:`lineno`. Also excludes any blocks not selected by any filter.

        If *inclusive* is ``False`` (exclude), ignore memory blocks allocated in
        a file with a name matching :attr:`filename_pattern` at line number
        :attr:`lineno`, even if they would otherwise be included.
      

      (Line 394: new comment)
      This attribute is ignored if the traceback limit is less than 2.
      ->
      This attribute is meaningless if the traceback limit is 1.

      Just after Line 428, not quite a new issue:
      Compute the differences with an old snapshot. Get statistics as a sorted
      list of :class:`StatisticDiff` instances grouped by *group_by*.

      ->

        Compute the differences with an old snapshot. Get statistics as a sorted
        list of :class:`StatisticDiff` instances grouped by *group_by*.
      
        Note that applying different Filters can cause spurious differences; users planning to archive a Snapshot may therefore wish to first add various annotations.
      

      (Line 454, new comment)
      All inclusive filters are applied at once, a trace is ignored if no
      inclusive filters match it. A trace is ignored if at least one exclusive
      filter matchs it.
      ->
      If there are any inclusive filters, then they are all applied, and traces which do not match at least one of them are excluded. A trace is also ignored if at least one exclusive
      filter matches it.

      Line 544, new comment:

      .. attribute:: count_diff

        Difference of number of memory blocks between the old and the new
        snapshots (``int``): ``0`` if the memory blocks have been allocated in
        the new snapshot.
      

      ->
      .. attribute:: count_diff

        Difference of number of memory blocks between the old and the new
        snapshots (``int``).  count_diff==count if the memory blocks do not appear in the old snapshot.
      

      Line 558, similar to above:
      the new snapshots (int): 0 if the memory blocks have been
      allocated in the new snapshot.
      ->
      the new snapshots (int). size_diff==size if the memory blocks do not appear in the old snapshot.

    27. jimjjewett commented on Nov 27, 2013

      jimjjewettmannequin
      Mannequin

      Drat: forgot one at line 277

      .. function:: get_traced_memory()

      Get the current size and maximum size of memory blocks traced by the
      :mod:`tracemalloc` module as a tuple: ``(size: int, max_size: int)``.

      I have a tendency to read "maximum size" as the most it is willing/able to trace, rather than the most it has traced at a single time so far. I therefore prefer "peak_size".

      Also, this doesn't suggests that the object is strictly a tuple, and that results.size or results.peak_size will not work; is that intentional?

    28. vstinner commented on Nov 27, 2013

      @vstinner
      MemberAuthor

      @jim: This issue has been closed, please don't comment closed issues. I created a new issue for your new comments: bpo-19818.

    29. transferred this issue fromon Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      type-featureA feature request or enhancement

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions