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,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate charts from gathered data.
|
|
3
|
+
|
|
4
|
+
Requires **matplotlib**.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .classtracker_stats import Stats
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import matplotlib
|
|
11
|
+
matplotlib.use('Agg')
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
|
|
14
|
+
def tracker_timespace(filename: str, stats: Stats) -> None:
|
|
15
|
+
"""
|
|
16
|
+
Create a time-space chart from a ``Stats`` instance.
|
|
17
|
+
"""
|
|
18
|
+
classlist = list(stats.index.keys())
|
|
19
|
+
classlist.sort()
|
|
20
|
+
|
|
21
|
+
for snapshot in stats.snapshots:
|
|
22
|
+
stats.annotate_snapshot(snapshot)
|
|
23
|
+
|
|
24
|
+
timestamps = [fp.timestamp for fp in stats.snapshots]
|
|
25
|
+
offsets = [0] * len(stats.snapshots)
|
|
26
|
+
poly_labels = []
|
|
27
|
+
polys = []
|
|
28
|
+
for clsname in classlist:
|
|
29
|
+
pct = [fp.classes[clsname]['pct'] for fp in stats.snapshots
|
|
30
|
+
if fp.classes and clsname in fp.classes]
|
|
31
|
+
if max(pct) > 3.0:
|
|
32
|
+
sizes = [fp.classes[clsname]['sum'] for fp in stats.snapshots
|
|
33
|
+
if fp.classes and clsname in fp.classes]
|
|
34
|
+
sizes = [float(x) / (1024 * 1024) for x in sizes]
|
|
35
|
+
sizes = [offset + size for offset, size in zip(offsets, sizes)]
|
|
36
|
+
poly = matplotlib.mlab.poly_between(timestamps, offsets, sizes)
|
|
37
|
+
polys.append((poly, {'label': clsname}))
|
|
38
|
+
poly_labels.append(clsname)
|
|
39
|
+
offsets = sizes
|
|
40
|
+
|
|
41
|
+
fig = plt.figure(figsize=(10, 4))
|
|
42
|
+
axis = fig.add_subplot(111)
|
|
43
|
+
|
|
44
|
+
axis.set_title("Snapshot Memory")
|
|
45
|
+
axis.set_xlabel("Execution Time [s]")
|
|
46
|
+
axis.set_ylabel("Virtual Memory [MiB]")
|
|
47
|
+
|
|
48
|
+
totals = [float(x.asizeof_total) / (1024 * 1024)
|
|
49
|
+
for x in stats.snapshots]
|
|
50
|
+
axis.plot(timestamps, totals, 'r--', label='Total')
|
|
51
|
+
tracked = [float(x.tracked_total) / (1024 * 1024)
|
|
52
|
+
for x in stats.snapshots]
|
|
53
|
+
axis.plot(timestamps, tracked, 'b--', label='Tracked total')
|
|
54
|
+
|
|
55
|
+
for (args, kwds) in polys:
|
|
56
|
+
axis.fill(*args, **kwds)
|
|
57
|
+
axis.legend(loc=2) # TODO fill legend
|
|
58
|
+
fig.savefig(filename)
|
|
59
|
+
|
|
60
|
+
except ImportError:
|
|
61
|
+
def tracker_timespace(filename: str, stats: Stats) -> None:
|
|
62
|
+
pass
|
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The `ClassTracker` is a facility delivering insight into the memory
|
|
3
|
+
distribution of a Python program. It can introspect memory consumption of
|
|
4
|
+
certain classes and objects. Facilities are provided to track and size
|
|
5
|
+
individual objects or all instances of certain classes. Tracked objects are
|
|
6
|
+
sized recursively to provide an overview of memory distribution between the
|
|
7
|
+
different tracked objects.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable, Dict, IO, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from functools import partial
|
|
14
|
+
from inspect import stack, isclass
|
|
15
|
+
from threading import Thread, Lock
|
|
16
|
+
from time import sleep, time
|
|
17
|
+
from weakref import ref as weakref_ref
|
|
18
|
+
|
|
19
|
+
from .classtracker_stats import ConsoleStats
|
|
20
|
+
from .util.stringutils import safe_repr
|
|
21
|
+
|
|
22
|
+
from . import asizeof
|
|
23
|
+
from . import process
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = ["ClassTracker"]
|
|
27
|
+
|
|
28
|
+
# Fixpoint for program start relative time stamp.
|
|
29
|
+
_local_start = time()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _ClassObserver(object):
|
|
33
|
+
"""
|
|
34
|
+
Stores options for tracked classes.
|
|
35
|
+
The observer also keeps the original constructor of the observed class.
|
|
36
|
+
"""
|
|
37
|
+
__slots__ = ('init', 'name', 'detail', 'keep', 'trace')
|
|
38
|
+
|
|
39
|
+
def __init__(self, init: Callable, name: str, detail: int, keep: bool,
|
|
40
|
+
trace: bool):
|
|
41
|
+
self.init = init
|
|
42
|
+
self.name = name
|
|
43
|
+
self.detail = detail
|
|
44
|
+
self.keep = keep
|
|
45
|
+
self.trace = trace
|
|
46
|
+
|
|
47
|
+
def modify(self, name: str, detail: int, keep: bool, trace: bool) -> None:
|
|
48
|
+
self.name = name
|
|
49
|
+
self.detail = detail
|
|
50
|
+
self.keep = keep
|
|
51
|
+
self.trace = trace
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_time() -> float:
|
|
55
|
+
"""
|
|
56
|
+
Get a timestamp relative to the program start time.
|
|
57
|
+
"""
|
|
58
|
+
return time() - _local_start
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TrackedObject(object):
|
|
62
|
+
"""
|
|
63
|
+
Stores size and lifetime information of a tracked object. A weak reference
|
|
64
|
+
is attached to monitor the object without preventing its deletion.
|
|
65
|
+
"""
|
|
66
|
+
__slots__ = ("ref", "id", "repr", "name", "birth", "death", "trace",
|
|
67
|
+
"snapshots", "_resolution_level", "__dict__")
|
|
68
|
+
|
|
69
|
+
def __init__(self, instance: Any, name: str, resolution_level: int = 0,
|
|
70
|
+
trace: bool = False, on_delete: Optional[Callable] = None):
|
|
71
|
+
"""
|
|
72
|
+
Create a weak reference for 'instance' to observe an object but which
|
|
73
|
+
won't prevent its deletion (which is monitored by the finalize
|
|
74
|
+
callback). The size of the object is recorded in 'snapshots' as
|
|
75
|
+
(timestamp, size) tuples.
|
|
76
|
+
"""
|
|
77
|
+
self.ref = weakref_ref(instance, self.finalize)
|
|
78
|
+
self.id = id(instance)
|
|
79
|
+
self.repr = ''
|
|
80
|
+
self.name = name
|
|
81
|
+
self.birth = _get_time()
|
|
82
|
+
self.death = None # type: Optional[float]
|
|
83
|
+
self._resolution_level = resolution_level
|
|
84
|
+
self.trace = None # type: Optional[List[Tuple]]
|
|
85
|
+
|
|
86
|
+
if trace:
|
|
87
|
+
self._save_trace()
|
|
88
|
+
|
|
89
|
+
initial_size = asizeof.basicsize(instance) or 0
|
|
90
|
+
size = asizeof.Asized(initial_size, initial_size)
|
|
91
|
+
self.snapshots = [(self.birth, size)]
|
|
92
|
+
self.on_delete = on_delete
|
|
93
|
+
|
|
94
|
+
def __getstate__(self) -> Dict:
|
|
95
|
+
"""
|
|
96
|
+
Make the object serializable for dump_stats. Read the available slots
|
|
97
|
+
and store the values in a dictionary. Derived values (stored in the
|
|
98
|
+
dict) are not pickled as those can be reconstructed based on the other
|
|
99
|
+
data. References cannot be serialized, ignore 'ref' as well.
|
|
100
|
+
"""
|
|
101
|
+
state = {}
|
|
102
|
+
for name in getattr(TrackedObject, '__slots__', ()):
|
|
103
|
+
if hasattr(self, name) and name not in ['ref', '__dict__']:
|
|
104
|
+
state[name] = getattr(self, name)
|
|
105
|
+
return state
|
|
106
|
+
|
|
107
|
+
def __setstate__(self, state: Dict) -> None:
|
|
108
|
+
"""
|
|
109
|
+
Restore the state from pickled data. Needed because a slotted class is
|
|
110
|
+
used.
|
|
111
|
+
"""
|
|
112
|
+
for key, value in list(state.items()):
|
|
113
|
+
setattr(self, key, value)
|
|
114
|
+
|
|
115
|
+
def _save_trace(self) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Save current stack trace as formatted string.
|
|
118
|
+
"""
|
|
119
|
+
stack_trace = stack()
|
|
120
|
+
try:
|
|
121
|
+
self.trace = []
|
|
122
|
+
for frm in stack_trace[5:]: # eliminate our own overhead
|
|
123
|
+
self.trace.insert(0, frm[1:])
|
|
124
|
+
finally:
|
|
125
|
+
del stack_trace
|
|
126
|
+
|
|
127
|
+
def track_size(self, ts: float, sizer: asizeof.Asizer) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Store timestamp and current size for later evaluation.
|
|
130
|
+
The 'sizer' is a stateful sizing facility that excludes other tracked
|
|
131
|
+
objects.
|
|
132
|
+
"""
|
|
133
|
+
obj = self.ref()
|
|
134
|
+
self.snapshots.append(
|
|
135
|
+
(ts, sizer.asized(obj, detail=self._resolution_level))
|
|
136
|
+
)
|
|
137
|
+
if obj is not None:
|
|
138
|
+
self.repr = safe_repr(obj, clip=128)
|
|
139
|
+
|
|
140
|
+
def get_max_size(self) -> int:
|
|
141
|
+
"""
|
|
142
|
+
Get the maximum of all sampled sizes.
|
|
143
|
+
"""
|
|
144
|
+
return max([s.size for (_, s) in self.snapshots])
|
|
145
|
+
|
|
146
|
+
def get_size_at_time(self, timestamp: float) -> int:
|
|
147
|
+
"""
|
|
148
|
+
Get the size of the object at a specific time (snapshot).
|
|
149
|
+
If the object was not alive/sized at that instant, return 0.
|
|
150
|
+
"""
|
|
151
|
+
size = 0
|
|
152
|
+
for (t, s) in self.snapshots:
|
|
153
|
+
if t == timestamp:
|
|
154
|
+
size = s.size
|
|
155
|
+
return size
|
|
156
|
+
|
|
157
|
+
def set_resolution_level(self, resolution_level: int) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Set resolution level to a new value. The next size estimation will
|
|
160
|
+
respect the new value. This is useful to set different levels for
|
|
161
|
+
different instances of tracked classes.
|
|
162
|
+
"""
|
|
163
|
+
self._resolution_level = resolution_level
|
|
164
|
+
|
|
165
|
+
def finalize(self, ref: weakref_ref) -> None:
|
|
166
|
+
"""
|
|
167
|
+
Mark the reference as dead and remember the timestamp. It would be
|
|
168
|
+
great if we could measure the pre-destruction size. Unfortunately, the
|
|
169
|
+
object is gone by the time the weakref callback is called. However,
|
|
170
|
+
weakref callbacks are useful to be informed when tracked objects died
|
|
171
|
+
without the need of destructors.
|
|
172
|
+
|
|
173
|
+
If the object is destroyed at the end of the program execution, it's
|
|
174
|
+
not possible to import modules anymore. Hence, the finalize callback
|
|
175
|
+
just does nothing (self.death stays None).
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
self.death = _get_time()
|
|
179
|
+
if self.on_delete:
|
|
180
|
+
self.on_delete()
|
|
181
|
+
except Exception: # pragma: no cover
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def track_object_creation(time_series: List[Tuple[float, int]]) -> None:
|
|
186
|
+
num_instances = time_series[-1][1] if time_series else 0
|
|
187
|
+
time_series.append((_get_time(), num_instances+1))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def track_object_deletion(time_series: List[Tuple[float, int]]) -> None:
|
|
191
|
+
num_instances = time_series[-1][1]
|
|
192
|
+
time_series.append((_get_time(), num_instances-1))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class PeriodicThread(Thread):
|
|
196
|
+
"""
|
|
197
|
+
Thread object to take snapshots periodically.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, tracker: 'ClassTracker', interval: float, *args: Any,
|
|
201
|
+
**kwargs: Any):
|
|
202
|
+
"""
|
|
203
|
+
Create thread with given interval and associated with the given
|
|
204
|
+
tracker.
|
|
205
|
+
"""
|
|
206
|
+
self.interval = interval
|
|
207
|
+
self.tracker = tracker
|
|
208
|
+
self.stop = False
|
|
209
|
+
super(PeriodicThread, self).__init__(*args, **kwargs)
|
|
210
|
+
|
|
211
|
+
def run(self) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Loop until a stop signal is set.
|
|
214
|
+
"""
|
|
215
|
+
self.stop = False
|
|
216
|
+
while not self.stop:
|
|
217
|
+
self.tracker.create_snapshot()
|
|
218
|
+
sleep(self.interval)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class Snapshot(object):
|
|
222
|
+
"""Sample sizes of objects and the process at an instant."""
|
|
223
|
+
|
|
224
|
+
def __init__(self, timestamp: float, description: str = '') -> None:
|
|
225
|
+
"""Initialize process-wide size information."""
|
|
226
|
+
self.tracked_total = 0
|
|
227
|
+
self.asizeof_total = 0
|
|
228
|
+
self.overhead = 0
|
|
229
|
+
self.timestamp = timestamp
|
|
230
|
+
self.system_total = process.ProcessMemoryInfo()
|
|
231
|
+
self.desc = description
|
|
232
|
+
self.classes = None # type: Optional[Dict[str, Dict[str, Any]]]
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def total(self) -> int:
|
|
236
|
+
"""
|
|
237
|
+
Return the total (virtual) size of the process in bytes. If process
|
|
238
|
+
information is not available, get the best number available, even if it
|
|
239
|
+
is a poor approximation of reality.
|
|
240
|
+
"""
|
|
241
|
+
if self.system_total.available:
|
|
242
|
+
return self.system_total.vsz
|
|
243
|
+
elif self.asizeof_total: # pragma: no cover
|
|
244
|
+
return self.asizeof_total
|
|
245
|
+
else: # pragma: no cover
|
|
246
|
+
return self.tracked_total
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def label(self) -> str:
|
|
250
|
+
"""Return timestamped label for this snapshot, or a raw timestamp."""
|
|
251
|
+
if not self.desc:
|
|
252
|
+
return "%.3fs" % self.timestamp
|
|
253
|
+
return "%s (%.3fs)" % (self.desc, self.timestamp)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class ClassTracker(object):
|
|
257
|
+
|
|
258
|
+
def __init__(self, stream: Optional[IO] = None):
|
|
259
|
+
"""
|
|
260
|
+
Creates a new `ClassTracker` object.
|
|
261
|
+
|
|
262
|
+
:param stream: Output stream to use when printing statistics via
|
|
263
|
+
``stats``.
|
|
264
|
+
"""
|
|
265
|
+
# Dictionaries of TrackedObject objects associated with the actual
|
|
266
|
+
# objects that are tracked. 'index' uses the class name as the key and
|
|
267
|
+
# associates a list of tracked objects. It contains all TrackedObject
|
|
268
|
+
# instances, including those of dead objects.
|
|
269
|
+
self.index = defaultdict(list) # type: Dict[str, List[TrackedObject]]
|
|
270
|
+
|
|
271
|
+
# 'objects' uses the id (address) as the key and associates the tracked
|
|
272
|
+
# object with it. TrackedObject's referring to dead objects are
|
|
273
|
+
# replaced lazily, i.e. when the id is recycled by another tracked
|
|
274
|
+
# object.
|
|
275
|
+
self.objects = {} # type: Dict[int, Any]
|
|
276
|
+
|
|
277
|
+
# List of `Snapshot` objects.
|
|
278
|
+
self.snapshots = [] # type: List[Snapshot]
|
|
279
|
+
|
|
280
|
+
# Time series of instance count for each tracked class.
|
|
281
|
+
self.history = defaultdict(list) \
|
|
282
|
+
# type: Dict[str, List[Tuple[float, int]]]
|
|
283
|
+
|
|
284
|
+
# Keep objects alive by holding a strong reference.
|
|
285
|
+
self._keepalive = [] # type: List[Any]
|
|
286
|
+
|
|
287
|
+
# Dictionary of class observers identified by classname.
|
|
288
|
+
self._observers = {} # type: Dict[type, _ClassObserver]
|
|
289
|
+
|
|
290
|
+
# Thread object responsible for background monitoring
|
|
291
|
+
self._periodic_thread = None # type: Optional[PeriodicThread]
|
|
292
|
+
|
|
293
|
+
self._stream = stream
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def stats(self) -> ConsoleStats:
|
|
297
|
+
"""
|
|
298
|
+
Return a ``ConsoleStats`` instance initialized with the current state
|
|
299
|
+
of the class tracker.
|
|
300
|
+
"""
|
|
301
|
+
return ConsoleStats(tracker=self, stream=self._stream)
|
|
302
|
+
|
|
303
|
+
def _tracker(self, _observer_: _ClassObserver, _self_: Any, *args: Any,
|
|
304
|
+
**kwds: Any) -> None:
|
|
305
|
+
"""
|
|
306
|
+
Injected constructor for tracked classes.
|
|
307
|
+
Call the actual constructor of the object and track the object. Attach
|
|
308
|
+
to the object before calling the constructor to track the object with
|
|
309
|
+
the parameters of the most specialized class.
|
|
310
|
+
"""
|
|
311
|
+
self.track_object(_self_,
|
|
312
|
+
name=_observer_.name,
|
|
313
|
+
resolution_level=_observer_.detail,
|
|
314
|
+
keep=_observer_.keep,
|
|
315
|
+
trace=_observer_.trace)
|
|
316
|
+
_observer_.init(_self_, *args, **kwds)
|
|
317
|
+
|
|
318
|
+
def _inject_constructor(self, cls: type, func: Callable, name: str,
|
|
319
|
+
resolution_level: int, keep: bool, trace: bool,
|
|
320
|
+
) -> None:
|
|
321
|
+
"""
|
|
322
|
+
Modifying Methods in Place - after the recipe 15.7 in the Python
|
|
323
|
+
Cookbook by Ken Seehof. The original constructors may be restored
|
|
324
|
+
later.
|
|
325
|
+
"""
|
|
326
|
+
try:
|
|
327
|
+
constructor = cls.__init__ # type: ignore
|
|
328
|
+
except AttributeError:
|
|
329
|
+
def constructor(self: Any, *_args: Any, **_kwargs: Any) -> None:
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
# Possible name clash between keyword arguments of the tracked class'
|
|
333
|
+
# constructor and the curried arguments of the injected constructor.
|
|
334
|
+
# Therefore, the additional argument has a 'magic' name to make it less
|
|
335
|
+
# likely that an argument name clash occurs.
|
|
336
|
+
observer = _ClassObserver(constructor,
|
|
337
|
+
name,
|
|
338
|
+
resolution_level,
|
|
339
|
+
keep,
|
|
340
|
+
trace)
|
|
341
|
+
self._observers[cls] = observer
|
|
342
|
+
|
|
343
|
+
def new_constructor(*args: Any, **kwargs: Any) -> None:
|
|
344
|
+
return func(observer, *args, **kwargs)
|
|
345
|
+
|
|
346
|
+
cls.__init__ = new_constructor # type: ignore
|
|
347
|
+
|
|
348
|
+
def _is_tracked(self, cls: type) -> bool:
|
|
349
|
+
"""
|
|
350
|
+
Determine if the class is tracked.
|
|
351
|
+
"""
|
|
352
|
+
return cls in self._observers
|
|
353
|
+
|
|
354
|
+
def _track_modify(self, cls: type, name: str, detail: int, keep: bool,
|
|
355
|
+
trace: bool) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Modify settings of a tracked class
|
|
358
|
+
"""
|
|
359
|
+
self._observers[cls].modify(name, detail, keep, trace)
|
|
360
|
+
|
|
361
|
+
def _restore_constructor(self, cls: type) -> None:
|
|
362
|
+
"""
|
|
363
|
+
Restore the original constructor, lose track of class.
|
|
364
|
+
"""
|
|
365
|
+
cls.__init__ = self._observers[cls].init # type: ignore
|
|
366
|
+
del self._observers[cls]
|
|
367
|
+
|
|
368
|
+
def track_change(self, instance: Any, resolution_level: int = 0) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Change tracking options for the already tracked object 'instance'.
|
|
371
|
+
If instance is not tracked, a KeyError will be raised.
|
|
372
|
+
"""
|
|
373
|
+
tobj = self.objects[id(instance)]
|
|
374
|
+
tobj.set_resolution_level(resolution_level)
|
|
375
|
+
|
|
376
|
+
def track_object(self, instance: Any, name: Optional[str] = None,
|
|
377
|
+
resolution_level: int = 0, keep: bool = False,
|
|
378
|
+
trace: bool = False) -> None:
|
|
379
|
+
"""
|
|
380
|
+
Track object 'instance' and sample size and lifetime information. Not
|
|
381
|
+
all objects can be tracked; trackable objects are class instances and
|
|
382
|
+
other objects that can be weakly referenced. When an object cannot be
|
|
383
|
+
tracked, a `TypeError` is raised.
|
|
384
|
+
|
|
385
|
+
:param resolution_level: The recursion depth up to which referents are
|
|
386
|
+
sized individually. Resolution level 0 (default) treats the object
|
|
387
|
+
as an opaque entity, 1 sizes all direct referents individually, 2
|
|
388
|
+
also sizes the referents of the referents and so forth.
|
|
389
|
+
:param keep: Prevent the object's deletion by keeping a (strong)
|
|
390
|
+
reference to the object.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
# Check if object is already tracked. This happens if track_object is
|
|
394
|
+
# called multiple times for the same object or if an object inherits
|
|
395
|
+
# from multiple tracked classes. In the latter case, the most
|
|
396
|
+
# specialized class wins. To detect id recycling, the weak reference
|
|
397
|
+
# is checked. If it is 'None' a tracked object is dead and another one
|
|
398
|
+
# takes the same 'id'.
|
|
399
|
+
if id(instance) in self.objects and \
|
|
400
|
+
self.objects[id(instance)].ref() is not None:
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
name = name if name else instance.__class__.__name__
|
|
404
|
+
|
|
405
|
+
track_object_creation(self.history[name])
|
|
406
|
+
on_delete = partial(track_object_deletion, self.history[name])
|
|
407
|
+
|
|
408
|
+
tobj = TrackedObject(instance,
|
|
409
|
+
name,
|
|
410
|
+
resolution_level=resolution_level,
|
|
411
|
+
trace=trace,
|
|
412
|
+
on_delete=on_delete)
|
|
413
|
+
|
|
414
|
+
self.index[name].append(tobj)
|
|
415
|
+
self.objects[id(instance)] = tobj
|
|
416
|
+
|
|
417
|
+
if keep:
|
|
418
|
+
self._keepalive.append(instance)
|
|
419
|
+
|
|
420
|
+
def track_class(self, cls: type, name: Optional[str] = None,
|
|
421
|
+
resolution_level: int = 0, keep: bool = False,
|
|
422
|
+
trace: bool = False) -> None:
|
|
423
|
+
"""
|
|
424
|
+
Track all objects of the class `cls`. Objects of that type that already
|
|
425
|
+
exist are *not* tracked. If `track_class` is called for a class already
|
|
426
|
+
tracked, the tracking parameters are modified. Instantiation traces can
|
|
427
|
+
be generated by setting `trace` to True.
|
|
428
|
+
A constructor is injected to begin instance tracking on creation
|
|
429
|
+
of the object. The constructor calls `track_object` internally.
|
|
430
|
+
|
|
431
|
+
:param cls: class to be tracked, may be an old-style or a new-style
|
|
432
|
+
class
|
|
433
|
+
:param name: reference the class by a name, default is the
|
|
434
|
+
concatenation of module and class name
|
|
435
|
+
:param resolution_level: The recursion depth up to which referents are
|
|
436
|
+
sized individually. Resolution level 0 (default) treats the object
|
|
437
|
+
as an opaque entity, 1 sizes all direct referents individually, 2
|
|
438
|
+
also sizes the referents of the referents and so forth.
|
|
439
|
+
:param keep: Prevent the object's deletion by keeping a (strong)
|
|
440
|
+
reference to the object.
|
|
441
|
+
:param trace: Save instantiation stack trace for each instance
|
|
442
|
+
"""
|
|
443
|
+
if not isclass(cls):
|
|
444
|
+
raise TypeError("only class objects can be tracked")
|
|
445
|
+
if name is None:
|
|
446
|
+
name = cls.__module__ + '.' + cls.__name__
|
|
447
|
+
if self._is_tracked(cls):
|
|
448
|
+
self._track_modify(cls, name, resolution_level, keep, trace)
|
|
449
|
+
else:
|
|
450
|
+
self._inject_constructor(cls, self._tracker, name,
|
|
451
|
+
resolution_level, keep, trace)
|
|
452
|
+
|
|
453
|
+
def detach_class(self, cls: type) -> None:
|
|
454
|
+
"""
|
|
455
|
+
Stop tracking class 'cls'. Any new objects of that type are not
|
|
456
|
+
tracked anymore. Existing objects are still tracked.
|
|
457
|
+
"""
|
|
458
|
+
self._restore_constructor(cls)
|
|
459
|
+
|
|
460
|
+
def detach_all_classes(self) -> None:
|
|
461
|
+
"""
|
|
462
|
+
Detach from all tracked classes.
|
|
463
|
+
"""
|
|
464
|
+
classes = list(self._observers.keys())
|
|
465
|
+
for cls in classes:
|
|
466
|
+
self.detach_class(cls)
|
|
467
|
+
|
|
468
|
+
def detach_all(self) -> None:
|
|
469
|
+
"""
|
|
470
|
+
Detach from all tracked classes and objects.
|
|
471
|
+
Restore the original constructors and cleanse the tracking lists.
|
|
472
|
+
"""
|
|
473
|
+
self.detach_all_classes()
|
|
474
|
+
self.objects.clear()
|
|
475
|
+
self.index.clear()
|
|
476
|
+
self._keepalive[:] = []
|
|
477
|
+
|
|
478
|
+
def clear(self) -> None:
|
|
479
|
+
"""
|
|
480
|
+
Clear all gathered data and detach from all tracked objects/classes.
|
|
481
|
+
"""
|
|
482
|
+
self.detach_all()
|
|
483
|
+
self.snapshots[:] = []
|
|
484
|
+
|
|
485
|
+
def close(self) -> None:
|
|
486
|
+
"""
|
|
487
|
+
Detach from tracked classes by removing injected constructors. Makes it
|
|
488
|
+
possible to use ClassTracker in `contextlib.closing` to safely remove
|
|
489
|
+
profiling hooks when the tracker goes out of scope::
|
|
490
|
+
|
|
491
|
+
import contextlib
|
|
492
|
+
with contextlib.closing(ClassTracker()) as tracker:
|
|
493
|
+
tracker.track_class(Foo)
|
|
494
|
+
|
|
495
|
+
"""
|
|
496
|
+
self.detach_all_classes()
|
|
497
|
+
|
|
498
|
+
#
|
|
499
|
+
# Background Monitoring
|
|
500
|
+
#
|
|
501
|
+
|
|
502
|
+
def start_periodic_snapshots(self, interval: float = 1.0) -> None:
|
|
503
|
+
"""
|
|
504
|
+
Start a thread which takes snapshots periodically. The `interval`
|
|
505
|
+
specifies the time in seconds the thread waits between taking
|
|
506
|
+
snapshots. The thread is started as a daemon allowing the program to
|
|
507
|
+
exit. If periodic snapshots are already active, the interval is
|
|
508
|
+
updated.
|
|
509
|
+
"""
|
|
510
|
+
if not self._periodic_thread:
|
|
511
|
+
self._periodic_thread = PeriodicThread(self, interval,
|
|
512
|
+
name='BackgroundMonitor')
|
|
513
|
+
self._periodic_thread.setDaemon(True)
|
|
514
|
+
self._periodic_thread.start()
|
|
515
|
+
else:
|
|
516
|
+
self._periodic_thread.interval = interval
|
|
517
|
+
|
|
518
|
+
def stop_periodic_snapshots(self) -> None:
|
|
519
|
+
"""
|
|
520
|
+
Post a stop signal to the thread that takes the periodic snapshots. The
|
|
521
|
+
function waits for the thread to terminate which can take some time
|
|
522
|
+
depending on the configured interval.
|
|
523
|
+
"""
|
|
524
|
+
if self._periodic_thread and self._periodic_thread.is_alive():
|
|
525
|
+
self._periodic_thread.stop = True
|
|
526
|
+
self._periodic_thread.join()
|
|
527
|
+
self._periodic_thread = None
|
|
528
|
+
|
|
529
|
+
#
|
|
530
|
+
# Snapshots
|
|
531
|
+
#
|
|
532
|
+
|
|
533
|
+
snapshot_lock = Lock()
|
|
534
|
+
|
|
535
|
+
def create_snapshot(self, description: str = '',
|
|
536
|
+
compute_total: bool = False) -> None:
|
|
537
|
+
"""
|
|
538
|
+
Collect current per instance statistics and saves total amount of
|
|
539
|
+
memory associated with the Python process.
|
|
540
|
+
|
|
541
|
+
If `compute_total` is `True`, the total consumption of all objects
|
|
542
|
+
known to *asizeof* is computed. The latter might be very slow if many
|
|
543
|
+
objects are mapped into memory at the time the snapshot is taken.
|
|
544
|
+
Therefore, `compute_total` is set to `False` by default.
|
|
545
|
+
|
|
546
|
+
The overhead of the `ClassTracker` structure is also computed.
|
|
547
|
+
|
|
548
|
+
Snapshots can be taken asynchronously. The function is protected with a
|
|
549
|
+
lock to prevent race conditions.
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
try:
|
|
553
|
+
# TODO: It is not clear what happens when memory is allocated or
|
|
554
|
+
# released while this function is executed but it will likely lead
|
|
555
|
+
# to inconsistencies. Either pause all other threads or don't size
|
|
556
|
+
# individual objects in asynchronous mode.
|
|
557
|
+
self.snapshot_lock.acquire()
|
|
558
|
+
|
|
559
|
+
timestamp = _get_time()
|
|
560
|
+
|
|
561
|
+
sizer = asizeof.Asizer()
|
|
562
|
+
objs = [tobj.ref() for tobj in list(self.objects.values())]
|
|
563
|
+
sizer.exclude_refs(*objs)
|
|
564
|
+
|
|
565
|
+
# The objects need to be sized in a deterministic order. Sort the
|
|
566
|
+
# objects by its creation date which should at least work for
|
|
567
|
+
# non-parallel execution. The "proper" fix would be to handle
|
|
568
|
+
# shared data separately.
|
|
569
|
+
tracked_objects = list(self.objects.values())
|
|
570
|
+
tracked_objects.sort(key=lambda x: x.birth)
|
|
571
|
+
for tobj in tracked_objects:
|
|
572
|
+
tobj.track_size(timestamp, sizer)
|
|
573
|
+
|
|
574
|
+
snapshot = Snapshot(timestamp, str(description))
|
|
575
|
+
snapshot.tracked_total = sizer.total
|
|
576
|
+
if compute_total:
|
|
577
|
+
snapshot.asizeof_total = asizeof.asizeof(all=True, code=True)
|
|
578
|
+
|
|
579
|
+
# Compute overhead of all structures, use sizer to exclude tracked
|
|
580
|
+
# objects(!)
|
|
581
|
+
snapshot.overhead = 0
|
|
582
|
+
if snapshot.tracked_total:
|
|
583
|
+
snapshot.overhead = sizer.asizeof(self)
|
|
584
|
+
if snapshot.asizeof_total:
|
|
585
|
+
snapshot.asizeof_total -= snapshot.overhead
|
|
586
|
+
|
|
587
|
+
self.snapshots.append(snapshot)
|
|
588
|
+
|
|
589
|
+
finally:
|
|
590
|
+
self.snapshot_lock.release()
|