cavisson-pythonagent 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cavisson_pythonagent-0.0.1.dist-info/METADATA +32 -0
- cavisson_pythonagent-0.0.1.dist-info/RECORD +180 -0
- cavisson_pythonagent-0.0.1.dist-info/WHEEL +5 -0
- cavisson_pythonagent-0.0.1.dist-info/licenses/LICENSE +19 -0
- cavisson_pythonagent-0.0.1.dist-info/top_level.txt +1 -0
- pythonagent/__init__.py +22 -0
- pythonagent/agent/__init__.py +219 -0
- pythonagent/agent/internal/__init__.py +1 -0
- pythonagent/agent/internal/agent.py +1651 -0
- pythonagent/agent/internal/framesinfo.py +152 -0
- pythonagent/agent/internal/heap_dump.py +39 -0
- pythonagent/agent/internal/intercept_module.py +70 -0
- pythonagent/agent/internal/logs.py +122 -0
- pythonagent/agent/internal/metadata/__init__.py +0 -0
- pythonagent/agent/internal/metadata/agent_meta_data.py +276 -0
- pythonagent/agent/internal/proc_compat.py +75 -0
- pythonagent/agent/internal/profile.py +47 -0
- pythonagent/agent/internal/provider.py +83 -0
- pythonagent/agent/internal/thread_dump.py +85 -0
- pythonagent/agent/internal/udp.py +245 -0
- pythonagent/agent/internal/udp_message.py +800 -0
- pythonagent/agent/probes/Instrumentation/__init__.py +340 -0
- pythonagent/agent/probes/Instrumentation/find.py +243 -0
- pythonagent/agent/probes/Instrumentation/module_version_resolver.py +155 -0
- pythonagent/agent/probes/Instrumentation/new_parser.py +64 -0
- pythonagent/agent/probes/Instrumentation/parser.py +129 -0
- pythonagent/agent/probes/__init__.py +219 -0
- pythonagent/agent/probes/base.py +303 -0
- pythonagent/agent/probes/cache/__init__.py +51 -0
- pythonagent/agent/probes/cache/redis.py +119 -0
- pythonagent/agent/probes/cache/redis_asyncio.py +83 -0
- pythonagent/agent/probes/coroutines/__init__.py +1 -0
- pythonagent/agent/probes/coroutines/asyncio.py +63 -0
- pythonagent/agent/probes/elasticdb/__init__.py +7 -0
- pythonagent/agent/probes/elasticdb/aelastic.py +54 -0
- pythonagent/agent/probes/frameworks/__init__.py +27 -0
- pythonagent/agent/probes/frameworks/agentprofiler.py +105 -0
- pythonagent/agent/probes/frameworks/aiohttp_web.py +155 -0
- pythonagent/agent/probes/frameworks/aisess.py +151 -0
- pythonagent/agent/probes/frameworks/asgi.py +340 -0
- pythonagent/agent/probes/frameworks/bottle.py +27 -0
- pythonagent/agent/probes/frameworks/cherry.py +25 -0
- pythonagent/agent/probes/frameworks/django.py +128 -0
- pythonagent/agent/probes/frameworks/falcon.py +21 -0
- pythonagent/agent/probes/frameworks/fastapi.py +35 -0
- pythonagent/agent/probes/frameworks/flask.py +30 -0
- pythonagent/agent/probes/frameworks/pyramid.py +56 -0
- pythonagent/agent/probes/frameworks/test.py +108 -0
- pythonagent/agent/probes/frameworks/tornado_async_web.py +117 -0
- pythonagent/agent/probes/frameworks/tornado_web.py +133 -0
- pythonagent/agent/probes/frameworks/wsgi.py +353 -0
- pythonagent/agent/probes/grpc/__init__.py +76 -0
- pythonagent/agent/probes/grpc/client_interceptor.py +132 -0
- pythonagent/agent/probes/grpc/server_interceptor.py +129 -0
- pythonagent/agent/probes/havoc/__init__.py +0 -0
- pythonagent/agent/probes/havoc/custom_memory_stress.py +186 -0
- pythonagent/agent/probes/havoc/custom_thread_stress.py +187 -0
- pythonagent/agent/probes/havoc/havoc_constants.py +218 -0
- pythonagent/agent/probes/havoc/havoc_manager.py +981 -0
- pythonagent/agent/probes/http/__init__.py +49 -0
- pythonagent/agent/probes/http/aiohttp_client.py +59 -0
- pythonagent/agent/probes/http/boto.py +12 -0
- pythonagent/agent/probes/http/httplib.py +110 -0
- pythonagent/agent/probes/http/httpx_client.py +116 -0
- pythonagent/agent/probes/http/requests.py +15 -0
- pythonagent/agent/probes/http/tornado_httpclient.py +85 -0
- pythonagent/agent/probes/http/urllib3.py +16 -0
- pythonagent/agent/probes/langchain/__init__.py +21 -0
- pythonagent/agent/probes/langchain/base_tool.py +95 -0
- pythonagent/agent/probes/langchain/langchain_community.py +136 -0
- pythonagent/agent/probes/langchain/langchain_core.py +32 -0
- pythonagent/agent/probes/langchain/langchain_openai.py +110 -0
- pythonagent/agent/probes/logging/__init__.py +106 -0
- pythonagent/agent/probes/message_brokers/__init__.py +4 -0
- pythonagent/agent/probes/message_brokers/pika.py +126 -0
- pythonagent/agent/probes/mongodb/__init__.py +6 -0
- pythonagent/agent/probes/mongodb/pymongo.py +286 -0
- pythonagent/agent/probes/openai/__init__.py +3 -0
- pythonagent/agent/probes/openai/openai.py +797 -0
- pythonagent/agent/probes/span.py +101 -0
- pythonagent/agent/probes/sql/__init__.py +13 -0
- pythonagent/agent/probes/sql/botocores3.py +51 -0
- pythonagent/agent/probes/sql/dbapi.py +285 -0
- pythonagent/agent/probes/sql/dynamodb.py +90 -0
- pythonagent/agent/probes/sql/mysql_connector.py +24 -0
- pythonagent/agent/probes/sql/mysql_connector_cext.py +24 -0
- pythonagent/agent/probes/sql/mysqldb.py +43 -0
- pythonagent/agent/probes/sql/psycopg2.py +174 -0
- pythonagent/agent/probes/sql/pymysql.py +25 -0
- pythonagent/bootstrap/__init__.py +0 -0
- pythonagent/bootstrap/cav_gunicorn.py +26 -0
- pythonagent/bootstrap/cavagent_lambda_wrapper.py +291 -0
- pythonagent/bootstrap/run.py +47 -0
- pythonagent/bootstrap/sitecustomize.py +287 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/instrumentationprofile.json +26 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/instrumentationprofile.json +42 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/config/ndsettings.conf +6 -0
- pythonagent/config.py +279 -0
- pythonagent/find.py +72 -0
- pythonagent/find_mod_cls_name.py +54 -0
- pythonagent/lang.py +131 -0
- pythonagent/lib.py +91 -0
- pythonagent/main/__init__.py +0 -0
- pythonagent/main/pytrace/__init__.py +79 -0
- pythonagent/main/pytrace/commands/__init__.py +0 -0
- pythonagent/main/pytrace/commands/auto_discovery.py +25 -0
- pythonagent/main/pytrace/commands/run.py +401 -0
- pythonagent/main/pytrace/pytrace.py +133 -0
- pythonagent/main/wsgi.py +6 -0
- pythonagent/main.py +27 -0
- pythonagent/run.py +46 -0
- pythonagent/sqins.py +8 -0
- pythonagent/test.py +73 -0
- pythonagent/utils.py +168 -0
- pythonagent/vendor/__init__.py +0 -0
- pythonagent/vendor/pympler/__init__.py +1 -0
- pythonagent/vendor/pympler/asizeof.py +2810 -0
- pythonagent/vendor/pympler/charts.py +62 -0
- pythonagent/vendor/pympler/classtracker.py +590 -0
- pythonagent/vendor/pympler/classtracker_stats.py +780 -0
- pythonagent/vendor/pympler/garbagegraph.py +80 -0
- pythonagent/vendor/pympler/mprofile.py +97 -0
- pythonagent/vendor/pympler/muppy.py +275 -0
- pythonagent/vendor/pympler/panels.py +115 -0
- pythonagent/vendor/pympler/process.py +238 -0
- pythonagent/vendor/pympler/py.typed +0 -0
- pythonagent/vendor/pympler/refbrowser.py +451 -0
- pythonagent/vendor/pympler/refgraph.py +350 -0
- pythonagent/vendor/pympler/summary.py +321 -0
- pythonagent/vendor/pympler/tracker.py +267 -0
- pythonagent/vendor/pympler/util/__init__.py +0 -0
- pythonagent/vendor/pympler/util/bottle.py +3809 -0
- pythonagent/vendor/pympler/util/compat.py +23 -0
- pythonagent/vendor/pympler/util/stringutils.py +77 -0
- pythonagent/vendor/pympler/web.py +346 -0
- pythonagent/vendor/werkzeug/__init__.py +20 -0
- pythonagent/vendor/werkzeug/_compat.py +228 -0
- pythonagent/vendor/werkzeug/_internal.py +473 -0
- pythonagent/vendor/werkzeug/_reloader.py +341 -0
- pythonagent/vendor/werkzeug/datastructures.py +3120 -0
- pythonagent/vendor/werkzeug/debug/__init__.py +498 -0
- pythonagent/vendor/werkzeug/debug/console.py +218 -0
- pythonagent/vendor/werkzeug/debug/repr.py +297 -0
- pythonagent/vendor/werkzeug/debug/tbtools.py +628 -0
- pythonagent/vendor/werkzeug/exceptions.py +829 -0
- pythonagent/vendor/werkzeug/filesystem.py +64 -0
- pythonagent/vendor/werkzeug/formparser.py +584 -0
- pythonagent/vendor/werkzeug/http.py +1307 -0
- pythonagent/vendor/werkzeug/local.py +420 -0
- pythonagent/vendor/werkzeug/middleware/__init__.py +25 -0
- pythonagent/vendor/werkzeug/middleware/dispatcher.py +66 -0
- pythonagent/vendor/werkzeug/middleware/http_proxy.py +219 -0
- pythonagent/vendor/werkzeug/middleware/lint.py +408 -0
- pythonagent/vendor/werkzeug/middleware/profiler.py +132 -0
- pythonagent/vendor/werkzeug/middleware/proxy_fix.py +169 -0
- pythonagent/vendor/werkzeug/middleware/shared_data.py +293 -0
- pythonagent/vendor/werkzeug/posixemulation.py +117 -0
- pythonagent/vendor/werkzeug/routing.py +2210 -0
- pythonagent/vendor/werkzeug/security.py +249 -0
- pythonagent/vendor/werkzeug/serving.py +1117 -0
- pythonagent/vendor/werkzeug/test.py +1123 -0
- pythonagent/vendor/werkzeug/testapp.py +241 -0
- pythonagent/vendor/werkzeug/urls.py +1138 -0
- pythonagent/vendor/werkzeug/useragents.py +202 -0
- pythonagent/vendor/werkzeug/utils.py +778 -0
- pythonagent/vendor/werkzeug/wrappers/__init__.py +36 -0
- pythonagent/vendor/werkzeug/wrappers/accept.py +50 -0
- pythonagent/vendor/werkzeug/wrappers/auth.py +33 -0
- pythonagent/vendor/werkzeug/wrappers/base_request.py +673 -0
- pythonagent/vendor/werkzeug/wrappers/base_response.py +700 -0
- pythonagent/vendor/werkzeug/wrappers/common_descriptors.py +341 -0
- pythonagent/vendor/werkzeug/wrappers/cors.py +100 -0
- pythonagent/vendor/werkzeug/wrappers/etag.py +304 -0
- pythonagent/vendor/werkzeug/wrappers/json.py +145 -0
- pythonagent/vendor/werkzeug/wrappers/request.py +49 -0
- pythonagent/vendor/werkzeug/wrappers/response.py +84 -0
- pythonagent/vendor/werkzeug/wrappers/user_agent.py +14 -0
- pythonagent/vendor/werkzeug/wsgi.py +1000 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
|
|
2
|
+
from .refgraph import ReferenceGraph
|
|
3
|
+
from .util.stringutils import trunc, pp
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import gc
|
|
7
|
+
|
|
8
|
+
__all__ = ['GarbageGraph', 'start_debug_garbage', 'end_debug_garbage']
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GarbageGraph(ReferenceGraph):
|
|
12
|
+
"""
|
|
13
|
+
The ``GarbageGraph`` is a ``ReferenceGraph`` that illustrates the objects
|
|
14
|
+
building reference cycles. The garbage collector is switched to debug mode
|
|
15
|
+
(all identified garbage is stored in `gc.garbage`) and the garbage
|
|
16
|
+
collector is invoked. The collected objects are then illustrated in a
|
|
17
|
+
directed graph.
|
|
18
|
+
|
|
19
|
+
Large graphs can be reduced to the actual cycles by passing ``reduce=True``
|
|
20
|
+
to the constructor.
|
|
21
|
+
|
|
22
|
+
It is recommended to disable the garbage collector when using the
|
|
23
|
+
``GarbageGraph``.
|
|
24
|
+
|
|
25
|
+
>>> from pympler.garbagegraph import GarbageGraph, start_debug_garbage
|
|
26
|
+
>>> start_debug_garbage()
|
|
27
|
+
>>> l = []
|
|
28
|
+
>>> l.append(l)
|
|
29
|
+
>>> del l
|
|
30
|
+
>>> gb = GarbageGraph()
|
|
31
|
+
>>> gb.render('garbage.eps')
|
|
32
|
+
True
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, reduce=False, collectable=True):
|
|
35
|
+
"""
|
|
36
|
+
Initialize the GarbageGraph with the objects identified by the garbage
|
|
37
|
+
collector. If `collectable` is true, every reference cycle is recorded.
|
|
38
|
+
Otherwise only uncollectable objects are reported.
|
|
39
|
+
"""
|
|
40
|
+
if collectable:
|
|
41
|
+
gc.set_debug(gc.DEBUG_SAVEALL)
|
|
42
|
+
else:
|
|
43
|
+
gc.set_debug(0)
|
|
44
|
+
gc.collect()
|
|
45
|
+
|
|
46
|
+
ReferenceGraph.__init__(self, gc.garbage, reduce)
|
|
47
|
+
|
|
48
|
+
def print_stats(self, stream=None):
|
|
49
|
+
"""
|
|
50
|
+
Log annotated garbage objects to console or file.
|
|
51
|
+
|
|
52
|
+
:param stream: open file, uses sys.stdout if not given
|
|
53
|
+
"""
|
|
54
|
+
if not stream: # pragma: no cover
|
|
55
|
+
stream = sys.stdout
|
|
56
|
+
self.metadata.sort(key=lambda x: -x.size)
|
|
57
|
+
stream.write('%-10s %8s %-12s %-46s\n' % ('id', 'size', 'type',
|
|
58
|
+
'representation'))
|
|
59
|
+
for g in self.metadata:
|
|
60
|
+
stream.write('0x%08x %8d %-12s %-46s\n' % (g.id, g.size,
|
|
61
|
+
trunc(g.type, 12),
|
|
62
|
+
trunc(g.str, 46)))
|
|
63
|
+
stream.write('Garbage: %8d collected objects (%s in cycles): %12s\n' %
|
|
64
|
+
(self.count, self.num_in_cycles, pp(self.total_size)))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def start_debug_garbage():
|
|
68
|
+
"""
|
|
69
|
+
Turn off garbage collector to analyze *collectable* reference cycles.
|
|
70
|
+
"""
|
|
71
|
+
gc.collect()
|
|
72
|
+
gc.disable()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def end_debug_garbage():
|
|
76
|
+
"""
|
|
77
|
+
Turn garbage collection on and disable debug output.
|
|
78
|
+
"""
|
|
79
|
+
gc.set_debug(0)
|
|
80
|
+
gc.enable()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory usage profiler for Python.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
import inspect
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import muppy
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MProfiler(object):
|
|
12
|
+
"""A memory usage profiler class.
|
|
13
|
+
|
|
14
|
+
Memory data for each function is stored as a 3-element list in the
|
|
15
|
+
dictionary self.memories. The index is always a codepoint (see below).
|
|
16
|
+
The following are the definitions of the members:
|
|
17
|
+
|
|
18
|
+
[0] = The number of times this function was called
|
|
19
|
+
[1] = Minimum memory consumption when this function was measured.
|
|
20
|
+
[2] = Maximum memory consumption when this function was measured.
|
|
21
|
+
|
|
22
|
+
A codepoint is a list of 3-tuple of the type
|
|
23
|
+
(filename, functionname, linenumber). You can omit either element, which
|
|
24
|
+
will cause the profiling to be triggered if any of the other criteria
|
|
25
|
+
match. E.g.
|
|
26
|
+
- (None, foo, None), will profile any foo function,
|
|
27
|
+
- (bar, foo, None) will profile only the foo function from the bar file,
|
|
28
|
+
- (bar, foo, 17) will profile only line 17 of the foo function defined
|
|
29
|
+
in the file bar.
|
|
30
|
+
|
|
31
|
+
Additionally, you can define on what events you want the profiling be
|
|
32
|
+
triggered. Possible events are defined in
|
|
33
|
+
http://docs.python.org/lib/debugger-hooks.html.
|
|
34
|
+
|
|
35
|
+
If you do not define either codepoints or events, the profiler will
|
|
36
|
+
record the memory usage in at every codepoint and event.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, codepoints=None, events=None):
|
|
41
|
+
"""
|
|
42
|
+
keyword arguments:
|
|
43
|
+
codepoints -- a list of points in code to monitor (defaults to all
|
|
44
|
+
codepoints)
|
|
45
|
+
events -- a list of events to monitor (defaults to all events)
|
|
46
|
+
"""
|
|
47
|
+
self.memories = {}
|
|
48
|
+
self.codepoints = codepoints
|
|
49
|
+
self.events = events
|
|
50
|
+
|
|
51
|
+
def codepoint_included(self, codepoint):
|
|
52
|
+
"""Check if codepoint matches any of the defined codepoints."""
|
|
53
|
+
if self.codepoints is None:
|
|
54
|
+
return True
|
|
55
|
+
for cp in self.codepoints:
|
|
56
|
+
mismatch = False
|
|
57
|
+
for i in range(len(cp)):
|
|
58
|
+
if (cp[i] is not None) and (cp[i] != codepoint[i]):
|
|
59
|
+
mismatch = True
|
|
60
|
+
break
|
|
61
|
+
if not mismatch:
|
|
62
|
+
return True
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def profile(self, frame, event, arg): # arg req to match signature
|
|
66
|
+
"""Profiling method used to profile matching codepoints and events."""
|
|
67
|
+
if (self.events is None) or (event in self.events):
|
|
68
|
+
frame_info = inspect.getframeinfo(frame)
|
|
69
|
+
cp = (frame_info[0], frame_info[2], frame_info[1])
|
|
70
|
+
if self.codepoint_included(cp):
|
|
71
|
+
objects = muppy.get_objects()
|
|
72
|
+
size = muppy.get_size(objects)
|
|
73
|
+
if cp not in self.memories:
|
|
74
|
+
self.memories[cp] = [0, 0, 0, 0]
|
|
75
|
+
self.memories[cp][0] = 1
|
|
76
|
+
self.memories[cp][1] = size
|
|
77
|
+
self.memories[cp][2] = size
|
|
78
|
+
else:
|
|
79
|
+
self.memories[cp][0] += 1
|
|
80
|
+
if self.memories[cp][1] > size:
|
|
81
|
+
self.memories[cp][1] = size
|
|
82
|
+
if self.memories[cp][2] < size:
|
|
83
|
+
self.memories[cp][2] = size
|
|
84
|
+
|
|
85
|
+
def run(self, cmd):
|
|
86
|
+
sys.setprofile(self.profile)
|
|
87
|
+
try:
|
|
88
|
+
exec(cmd)
|
|
89
|
+
finally:
|
|
90
|
+
sys.setprofile(None)
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
p = MProfiler()
|
|
96
|
+
p.run("print('hello')")
|
|
97
|
+
print(p.memories)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
|
2
|
+
|
|
3
|
+
import gc
|
|
4
|
+
|
|
5
|
+
from . import summary
|
|
6
|
+
from .util import compat
|
|
7
|
+
|
|
8
|
+
from inspect import isframe, stack
|
|
9
|
+
|
|
10
|
+
from sys import getsizeof
|
|
11
|
+
|
|
12
|
+
from .asizeof import _Py_TPFLAGS_HAVE_GC
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ignore_object(obj: Any) -> bool:
|
|
16
|
+
try:
|
|
17
|
+
return isframe(obj)
|
|
18
|
+
except ReferenceError:
|
|
19
|
+
return True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_objects(remove_dups: bool = True, include_frames: bool = False
|
|
23
|
+
) -> List[Any]:
|
|
24
|
+
"""Return a list of all known objects excluding frame objects.
|
|
25
|
+
|
|
26
|
+
If (outer) frame objects shall be included, pass `include_frames=True`. In
|
|
27
|
+
order to prevent building reference cycles, the current frame object (of
|
|
28
|
+
the caller of get_objects) is ignored. This will not prevent creating
|
|
29
|
+
reference cycles if the object list is passed up the call-stack. Therefore,
|
|
30
|
+
frame objects are not included by default.
|
|
31
|
+
|
|
32
|
+
Keyword arguments:
|
|
33
|
+
remove_dups -- if True, all duplicate objects will be removed.
|
|
34
|
+
include_frames -- if True, includes frame objects.
|
|
35
|
+
"""
|
|
36
|
+
gc.collect()
|
|
37
|
+
|
|
38
|
+
# Do not initialize local variables before calling gc.get_objects or those
|
|
39
|
+
# will be included in the list. Furthermore, ignore frame objects to
|
|
40
|
+
# prevent reference cycles.
|
|
41
|
+
tmp = gc.get_objects()
|
|
42
|
+
tmp = [o for o in tmp if not ignore_object(o)]
|
|
43
|
+
|
|
44
|
+
res = []
|
|
45
|
+
for o in tmp:
|
|
46
|
+
# gc.get_objects returns only container objects, but we also want
|
|
47
|
+
# the objects referenced by them
|
|
48
|
+
refs = get_referents(o)
|
|
49
|
+
for ref in refs:
|
|
50
|
+
if not gc.is_tracked(ref):
|
|
51
|
+
# we already got the container objects, now we only add
|
|
52
|
+
# non-container objects
|
|
53
|
+
res.append(ref)
|
|
54
|
+
res.extend(tmp)
|
|
55
|
+
if remove_dups:
|
|
56
|
+
res = _remove_duplicates(res)
|
|
57
|
+
|
|
58
|
+
if include_frames:
|
|
59
|
+
for sf in stack()[2:]:
|
|
60
|
+
res.append(sf[0])
|
|
61
|
+
return res
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_size(objects: List[Any]) -> int:
|
|
65
|
+
"""Compute the total size of all elements in objects."""
|
|
66
|
+
res = 0
|
|
67
|
+
for o in objects:
|
|
68
|
+
try:
|
|
69
|
+
res += getsizeof(o)
|
|
70
|
+
except AttributeError:
|
|
71
|
+
print("IGNORING: type=%s; o=%s" % (str(type(o)), str(o)))
|
|
72
|
+
return res
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_diff(left: List[Any], right: List[Any]) -> Dict[str, List[Any]]:
|
|
76
|
+
"""Get the difference of both lists.
|
|
77
|
+
|
|
78
|
+
The result will be a dict with this form {'+': [], '-': []}.
|
|
79
|
+
Items listed in '+' exist only in the right list,
|
|
80
|
+
items listed in '-' exist only in the left list.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
res = {'+': [], '-': []} # type: Dict[str, List[Any]]
|
|
84
|
+
|
|
85
|
+
def partition(objects: List[Any]) -> Dict[type, List[Any]]:
|
|
86
|
+
"""Partition the passed object list."""
|
|
87
|
+
res = {} # type: Dict[type, List[Any]]
|
|
88
|
+
for o in objects:
|
|
89
|
+
t = type(o)
|
|
90
|
+
if type(o) not in res:
|
|
91
|
+
res[t] = []
|
|
92
|
+
res[t].append(o)
|
|
93
|
+
return res
|
|
94
|
+
|
|
95
|
+
def get_not_included(foo: List[Any], bar: Dict[type, List[Any]]
|
|
96
|
+
) -> List[Any]:
|
|
97
|
+
"""Compare objects from foo with objects defined in the values of
|
|
98
|
+
bar (set of partitions).
|
|
99
|
+
Returns a list of all objects included in list, but not dict values.
|
|
100
|
+
"""
|
|
101
|
+
res = [] # type: List[Any]
|
|
102
|
+
for o in foo:
|
|
103
|
+
if not compat.object_in_list(type(o), bar):
|
|
104
|
+
res.append(o)
|
|
105
|
+
elif not compat.object_in_list(o, bar[type(o)]):
|
|
106
|
+
res.append(o)
|
|
107
|
+
return res
|
|
108
|
+
|
|
109
|
+
# Create partitions of both lists. This will reduce the time required for
|
|
110
|
+
# the comparison
|
|
111
|
+
left_objects = partition(left)
|
|
112
|
+
right_objects = partition(right)
|
|
113
|
+
# and then do the diff
|
|
114
|
+
res['+'] = get_not_included(right, left_objects)
|
|
115
|
+
res['-'] = get_not_included(left, right_objects)
|
|
116
|
+
return res
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def sort(objects: List[Any]) -> List[Any]:
|
|
120
|
+
"""Sort objects by size in bytes."""
|
|
121
|
+
objects = sorted(objects, key=getsizeof)
|
|
122
|
+
return objects
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def filter(objects: List[Any], Type: Optional[type] = None, min: int = -1,
|
|
126
|
+
max: int = -1) -> List[Any]:
|
|
127
|
+
"""Filter objects.
|
|
128
|
+
|
|
129
|
+
The filter can be by type, minimum size, and/or maximum size.
|
|
130
|
+
|
|
131
|
+
Keyword arguments:
|
|
132
|
+
Type -- object type to filter by
|
|
133
|
+
min -- minimum object size
|
|
134
|
+
max -- maximum object size
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
res = [] # type: List[Any]
|
|
138
|
+
if min > max and max > -1:
|
|
139
|
+
raise ValueError("minimum must be smaller than maximum")
|
|
140
|
+
|
|
141
|
+
if Type is not None:
|
|
142
|
+
objects = [o for o in objects if isinstance(o, Type)]
|
|
143
|
+
if min > -1:
|
|
144
|
+
objects = [o for o in objects if getsizeof(o) > min]
|
|
145
|
+
if max > -1:
|
|
146
|
+
objects = [o for o in objects if getsizeof(o) < max]
|
|
147
|
+
return objects
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_referents(object: Any, level: int = 1) -> List[Any]:
|
|
151
|
+
"""Get all referents of an object up to a certain level.
|
|
152
|
+
|
|
153
|
+
The referents will not be returned in a specific order and
|
|
154
|
+
will not contain duplicate objects. Duplicate objects will be removed.
|
|
155
|
+
|
|
156
|
+
Keyword arguments:
|
|
157
|
+
level -- level of indirection to which referents considered.
|
|
158
|
+
|
|
159
|
+
This function is recursive.
|
|
160
|
+
|
|
161
|
+
"""
|
|
162
|
+
res = gc.get_referents(object)
|
|
163
|
+
level -= 1
|
|
164
|
+
if level > 0:
|
|
165
|
+
for o in res:
|
|
166
|
+
res.extend(get_referents(o, level))
|
|
167
|
+
res = _remove_duplicates(res)
|
|
168
|
+
return res
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _get_usage(function: Callable, *args: Any) -> Optional[List]:
|
|
172
|
+
"""Test if more memory is used after the function has been called.
|
|
173
|
+
|
|
174
|
+
The function will be invoked twice and only the second measurement will be
|
|
175
|
+
considered. Thus, memory used in initialisation (e.g. loading modules)
|
|
176
|
+
will not be included in the result. The goal is to identify memory leaks
|
|
177
|
+
caused by functions which use more and more memory.
|
|
178
|
+
|
|
179
|
+
Any arguments next to the function will be passed on to the function
|
|
180
|
+
on invocation.
|
|
181
|
+
|
|
182
|
+
Note that this function is currently experimental, because it is not
|
|
183
|
+
tested thoroughly and performs poorly.
|
|
184
|
+
|
|
185
|
+
"""
|
|
186
|
+
# The usage of a function is calculated by creating one summary of all
|
|
187
|
+
# objects before the function is invoked and afterwards. These summaries
|
|
188
|
+
# are compared and the diff is returned.
|
|
189
|
+
# This function works in a 2-steps process. Before the actual function is
|
|
190
|
+
# invoked an empty dummy function is measurement to identify the overhead
|
|
191
|
+
# involved in the measuring process. This overhead then is subtracted from
|
|
192
|
+
# the measurement performed on the passed function. The result reflects the
|
|
193
|
+
# actual usage of a function call.
|
|
194
|
+
# Also, a measurement is performed twice, allowing the adjustment to
|
|
195
|
+
# initializing things, e.g. modules
|
|
196
|
+
|
|
197
|
+
res = None
|
|
198
|
+
|
|
199
|
+
def _get_summaries(function: Callable, *args: Any) -> Tuple:
|
|
200
|
+
"""Get a 2-tuple containing one summary from before, and one summary
|
|
201
|
+
from after the function has been invoked.
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
s_before = summary.summarize(get_objects())
|
|
205
|
+
function(*args)
|
|
206
|
+
s_after = summary.summarize(get_objects())
|
|
207
|
+
return (s_before, s_after)
|
|
208
|
+
|
|
209
|
+
def _get_usage(function: Callable, *args: Any) -> List:
|
|
210
|
+
"""Get the usage of a function call.
|
|
211
|
+
This function is to be used only internally. The 'real' get_usage
|
|
212
|
+
function is a wrapper around _get_usage, but the workload is done
|
|
213
|
+
here.
|
|
214
|
+
|
|
215
|
+
"""
|
|
216
|
+
# init before calling
|
|
217
|
+
(s_before, s_after) = _get_summaries(function, *args)
|
|
218
|
+
# ignore all objects used for the measurement
|
|
219
|
+
ignore = []
|
|
220
|
+
if s_before != s_after:
|
|
221
|
+
ignore.append(s_before)
|
|
222
|
+
for row in s_before:
|
|
223
|
+
# ignore refs from summary and frame (loop)
|
|
224
|
+
if len(gc.get_referrers(row)) == 2:
|
|
225
|
+
ignore.append(row)
|
|
226
|
+
for item in row:
|
|
227
|
+
# ignore refs from summary and frame (loop)
|
|
228
|
+
if len(gc.get_referrers(item)) == 2:
|
|
229
|
+
ignore.append(item)
|
|
230
|
+
for o in ignore:
|
|
231
|
+
s_after = summary._subtract(s_after, o)
|
|
232
|
+
res = summary.get_diff(s_before, s_after)
|
|
233
|
+
return summary._sweep(res)
|
|
234
|
+
|
|
235
|
+
# calibrate; twice for initialization
|
|
236
|
+
def noop() -> None:
|
|
237
|
+
pass
|
|
238
|
+
offset = _get_usage(noop)
|
|
239
|
+
offset = _get_usage(noop)
|
|
240
|
+
# perform operation twice to handle objects possibly used in
|
|
241
|
+
# initialisation
|
|
242
|
+
tmp = _get_usage(function, *args)
|
|
243
|
+
tmp = _get_usage(function, *args)
|
|
244
|
+
tmp = summary.get_diff(offset, tmp)
|
|
245
|
+
tmp = summary._sweep(tmp)
|
|
246
|
+
if len(tmp) != 0:
|
|
247
|
+
res = tmp
|
|
248
|
+
return res
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _is_containerobject(o: Any) -> bool:
|
|
252
|
+
"""Is the passed object a container object."""
|
|
253
|
+
return bool(getattr(type(o), '__flags__', 0) & _Py_TPFLAGS_HAVE_GC)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _remove_duplicates(objects: List[Any]) -> List[Any]:
|
|
257
|
+
"""Remove duplicate objects.
|
|
258
|
+
|
|
259
|
+
Inspired by http://www.peterbe.com/plog/uniqifiers-benchmark
|
|
260
|
+
|
|
261
|
+
"""
|
|
262
|
+
seen = set() # type: Set[int]
|
|
263
|
+
result = []
|
|
264
|
+
for item in objects:
|
|
265
|
+
marker = id(item)
|
|
266
|
+
if marker in seen:
|
|
267
|
+
continue
|
|
268
|
+
seen.add(marker)
|
|
269
|
+
result.append(item)
|
|
270
|
+
return result
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def print_summary() -> None:
|
|
274
|
+
"""Print a summary of all known objects."""
|
|
275
|
+
summary.print_(summary.summarize(get_objects()))
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Expose a memory-profiling panel to the Django Debug toolbar.
|
|
3
|
+
|
|
4
|
+
Shows process memory information (virtual size, resident set size) and model
|
|
5
|
+
instances for the current request.
|
|
6
|
+
|
|
7
|
+
Requires Django and Django Debug toolbar:
|
|
8
|
+
|
|
9
|
+
https://github.com/jazzband/django-debug-toolbar
|
|
10
|
+
|
|
11
|
+
Pympler adds a memory panel as a third party addon (not included in the
|
|
12
|
+
django-debug-toolbar). It can be added by overriding the `DEBUG_TOOLBAR_PANELS`
|
|
13
|
+
setting in the Django project settings::
|
|
14
|
+
|
|
15
|
+
DEBUG_TOOLBAR_PANELS = (
|
|
16
|
+
'debug_toolbar.panels.timer.TimerDebugPanel',
|
|
17
|
+
'pympler.panels.MemoryPanel',
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
Pympler also needs to be added to the `INSTALLED_APPS` in the Django settings::
|
|
21
|
+
|
|
22
|
+
INSTALLED_APPS = INSTALLED_APPS + ('debug_toolbar', 'pympler')
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from .classtracker import ClassTracker
|
|
26
|
+
from .process import ProcessMemoryInfo
|
|
27
|
+
from .util.stringutils import pp
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from debug_toolbar.panels import Panel
|
|
31
|
+
from django.apps import apps
|
|
32
|
+
from django.template import Context, Template
|
|
33
|
+
from django.template.loader import render_to_string
|
|
34
|
+
from django.http.request import HttpRequest
|
|
35
|
+
from django.http.response import HttpResponse
|
|
36
|
+
except ImportError:
|
|
37
|
+
class Panel(object): # type: ignore
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
class Template(object): # type: ignore
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
class Context(object): # type: ignore
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
class HttpRequest(object): # type: ignore
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
class HttpResponse(object): # type: ignore
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MemoryPanel(Panel):
|
|
54
|
+
|
|
55
|
+
name = 'pympler'
|
|
56
|
+
|
|
57
|
+
title = 'Memory'
|
|
58
|
+
|
|
59
|
+
template = 'memory_panel.html'
|
|
60
|
+
|
|
61
|
+
classes = [Context, Template]
|
|
62
|
+
|
|
63
|
+
def process_request(self, request: HttpRequest) -> HttpResponse:
|
|
64
|
+
self._tracker = ClassTracker()
|
|
65
|
+
for cls in apps.get_models() + self.classes:
|
|
66
|
+
self._tracker.track_class(cls)
|
|
67
|
+
self._tracker.create_snapshot('before')
|
|
68
|
+
self.record_stats({'before': ProcessMemoryInfo()})
|
|
69
|
+
response = super(MemoryPanel, self).process_request(request)
|
|
70
|
+
self.record_stats({'after': ProcessMemoryInfo()})
|
|
71
|
+
self._tracker.create_snapshot('after')
|
|
72
|
+
stats = self._tracker.stats
|
|
73
|
+
stats.annotate()
|
|
74
|
+
self.record_stats({'stats': stats})
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
def enable_instrumentation(self) -> None:
|
|
78
|
+
self._tracker = ClassTracker()
|
|
79
|
+
for cls in apps.get_models() + self.classes:
|
|
80
|
+
self._tracker.track_class(cls)
|
|
81
|
+
|
|
82
|
+
def disable_instrumentation(self) -> None:
|
|
83
|
+
self._tracker.detach_all_classes()
|
|
84
|
+
|
|
85
|
+
def nav_subtitle(self) -> str:
|
|
86
|
+
context = self.get_stats()
|
|
87
|
+
before = context['before']
|
|
88
|
+
after = context['after']
|
|
89
|
+
rss = after.rss
|
|
90
|
+
delta = rss - before.rss
|
|
91
|
+
delta = ('(+%s)' % pp(delta)) if delta > 0 else ''
|
|
92
|
+
return "%s %s" % (pp(rss), delta)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def content(self) -> str:
|
|
96
|
+
context = self.get_stats()
|
|
97
|
+
before = context['before']
|
|
98
|
+
after = context['after']
|
|
99
|
+
stats = context['stats']
|
|
100
|
+
rows = [('Resident set size', after.rss),
|
|
101
|
+
('Virtual size', after.vsz),
|
|
102
|
+
]
|
|
103
|
+
rows.extend(after - before)
|
|
104
|
+
rows = [(key, pp(value)) for key, value in rows]
|
|
105
|
+
rows.extend(after.os_specific)
|
|
106
|
+
|
|
107
|
+
classes = []
|
|
108
|
+
snapshot = stats.snapshots[-1]
|
|
109
|
+
for model in stats.tracked_classes:
|
|
110
|
+
history = [cnt for _, cnt in stats.history[model]]
|
|
111
|
+
size = snapshot.classes.get(model, {}).get('sum', 0)
|
|
112
|
+
if history and history[-1] > 0:
|
|
113
|
+
classes.append((model, history, pp(size)))
|
|
114
|
+
context.update({'rows': rows, 'classes': classes})
|
|
115
|
+
return render_to_string(self.template, context)
|