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.
Files changed (180) hide show
  1. cavisson_pythonagent-0.0.1.dist-info/METADATA +32 -0
  2. cavisson_pythonagent-0.0.1.dist-info/RECORD +180 -0
  3. cavisson_pythonagent-0.0.1.dist-info/WHEEL +5 -0
  4. cavisson_pythonagent-0.0.1.dist-info/licenses/LICENSE +19 -0
  5. cavisson_pythonagent-0.0.1.dist-info/top_level.txt +1 -0
  6. pythonagent/__init__.py +22 -0
  7. pythonagent/agent/__init__.py +219 -0
  8. pythonagent/agent/internal/__init__.py +1 -0
  9. pythonagent/agent/internal/agent.py +1651 -0
  10. pythonagent/agent/internal/framesinfo.py +152 -0
  11. pythonagent/agent/internal/heap_dump.py +39 -0
  12. pythonagent/agent/internal/intercept_module.py +70 -0
  13. pythonagent/agent/internal/logs.py +122 -0
  14. pythonagent/agent/internal/metadata/__init__.py +0 -0
  15. pythonagent/agent/internal/metadata/agent_meta_data.py +276 -0
  16. pythonagent/agent/internal/proc_compat.py +75 -0
  17. pythonagent/agent/internal/profile.py +47 -0
  18. pythonagent/agent/internal/provider.py +83 -0
  19. pythonagent/agent/internal/thread_dump.py +85 -0
  20. pythonagent/agent/internal/udp.py +245 -0
  21. pythonagent/agent/internal/udp_message.py +800 -0
  22. pythonagent/agent/probes/Instrumentation/__init__.py +340 -0
  23. pythonagent/agent/probes/Instrumentation/find.py +243 -0
  24. pythonagent/agent/probes/Instrumentation/module_version_resolver.py +155 -0
  25. pythonagent/agent/probes/Instrumentation/new_parser.py +64 -0
  26. pythonagent/agent/probes/Instrumentation/parser.py +129 -0
  27. pythonagent/agent/probes/__init__.py +219 -0
  28. pythonagent/agent/probes/base.py +303 -0
  29. pythonagent/agent/probes/cache/__init__.py +51 -0
  30. pythonagent/agent/probes/cache/redis.py +119 -0
  31. pythonagent/agent/probes/cache/redis_asyncio.py +83 -0
  32. pythonagent/agent/probes/coroutines/__init__.py +1 -0
  33. pythonagent/agent/probes/coroutines/asyncio.py +63 -0
  34. pythonagent/agent/probes/elasticdb/__init__.py +7 -0
  35. pythonagent/agent/probes/elasticdb/aelastic.py +54 -0
  36. pythonagent/agent/probes/frameworks/__init__.py +27 -0
  37. pythonagent/agent/probes/frameworks/agentprofiler.py +105 -0
  38. pythonagent/agent/probes/frameworks/aiohttp_web.py +155 -0
  39. pythonagent/agent/probes/frameworks/aisess.py +151 -0
  40. pythonagent/agent/probes/frameworks/asgi.py +340 -0
  41. pythonagent/agent/probes/frameworks/bottle.py +27 -0
  42. pythonagent/agent/probes/frameworks/cherry.py +25 -0
  43. pythonagent/agent/probes/frameworks/django.py +128 -0
  44. pythonagent/agent/probes/frameworks/falcon.py +21 -0
  45. pythonagent/agent/probes/frameworks/fastapi.py +35 -0
  46. pythonagent/agent/probes/frameworks/flask.py +30 -0
  47. pythonagent/agent/probes/frameworks/pyramid.py +56 -0
  48. pythonagent/agent/probes/frameworks/test.py +108 -0
  49. pythonagent/agent/probes/frameworks/tornado_async_web.py +117 -0
  50. pythonagent/agent/probes/frameworks/tornado_web.py +133 -0
  51. pythonagent/agent/probes/frameworks/wsgi.py +353 -0
  52. pythonagent/agent/probes/grpc/__init__.py +76 -0
  53. pythonagent/agent/probes/grpc/client_interceptor.py +132 -0
  54. pythonagent/agent/probes/grpc/server_interceptor.py +129 -0
  55. pythonagent/agent/probes/havoc/__init__.py +0 -0
  56. pythonagent/agent/probes/havoc/custom_memory_stress.py +186 -0
  57. pythonagent/agent/probes/havoc/custom_thread_stress.py +187 -0
  58. pythonagent/agent/probes/havoc/havoc_constants.py +218 -0
  59. pythonagent/agent/probes/havoc/havoc_manager.py +981 -0
  60. pythonagent/agent/probes/http/__init__.py +49 -0
  61. pythonagent/agent/probes/http/aiohttp_client.py +59 -0
  62. pythonagent/agent/probes/http/boto.py +12 -0
  63. pythonagent/agent/probes/http/httplib.py +110 -0
  64. pythonagent/agent/probes/http/httpx_client.py +116 -0
  65. pythonagent/agent/probes/http/requests.py +15 -0
  66. pythonagent/agent/probes/http/tornado_httpclient.py +85 -0
  67. pythonagent/agent/probes/http/urllib3.py +16 -0
  68. pythonagent/agent/probes/langchain/__init__.py +21 -0
  69. pythonagent/agent/probes/langchain/base_tool.py +95 -0
  70. pythonagent/agent/probes/langchain/langchain_community.py +136 -0
  71. pythonagent/agent/probes/langchain/langchain_core.py +32 -0
  72. pythonagent/agent/probes/langchain/langchain_openai.py +110 -0
  73. pythonagent/agent/probes/logging/__init__.py +106 -0
  74. pythonagent/agent/probes/message_brokers/__init__.py +4 -0
  75. pythonagent/agent/probes/message_brokers/pika.py +126 -0
  76. pythonagent/agent/probes/mongodb/__init__.py +6 -0
  77. pythonagent/agent/probes/mongodb/pymongo.py +286 -0
  78. pythonagent/agent/probes/openai/__init__.py +3 -0
  79. pythonagent/agent/probes/openai/openai.py +797 -0
  80. pythonagent/agent/probes/span.py +101 -0
  81. pythonagent/agent/probes/sql/__init__.py +13 -0
  82. pythonagent/agent/probes/sql/botocores3.py +51 -0
  83. pythonagent/agent/probes/sql/dbapi.py +285 -0
  84. pythonagent/agent/probes/sql/dynamodb.py +90 -0
  85. pythonagent/agent/probes/sql/mysql_connector.py +24 -0
  86. pythonagent/agent/probes/sql/mysql_connector_cext.py +24 -0
  87. pythonagent/agent/probes/sql/mysqldb.py +43 -0
  88. pythonagent/agent/probes/sql/psycopg2.py +174 -0
  89. pythonagent/agent/probes/sql/pymysql.py +25 -0
  90. pythonagent/bootstrap/__init__.py +0 -0
  91. pythonagent/bootstrap/cav_gunicorn.py +26 -0
  92. pythonagent/bootstrap/cavagent_lambda_wrapper.py +291 -0
  93. pythonagent/bootstrap/run.py +47 -0
  94. pythonagent/bootstrap/sitecustomize.py +287 -0
  95. pythonagent/cavisson/netdiagnostics/CavAgent/instrumentationprofile.json +26 -0
  96. pythonagent/cavisson/netdiagnostics/CavAgent/interceptor_points.txt +29 -0
  97. pythonagent/cavisson/netdiagnostics/python/CavAgent/instrumentationprofile.json +42 -0
  98. pythonagent/cavisson/netdiagnostics/python/CavAgent/interceptor_points.txt +29 -0
  99. pythonagent/cavisson/netdiagnostics/python/config/ndsettings.conf +6 -0
  100. pythonagent/config.py +279 -0
  101. pythonagent/find.py +72 -0
  102. pythonagent/find_mod_cls_name.py +54 -0
  103. pythonagent/lang.py +131 -0
  104. pythonagent/lib.py +91 -0
  105. pythonagent/main/__init__.py +0 -0
  106. pythonagent/main/pytrace/__init__.py +79 -0
  107. pythonagent/main/pytrace/commands/__init__.py +0 -0
  108. pythonagent/main/pytrace/commands/auto_discovery.py +25 -0
  109. pythonagent/main/pytrace/commands/run.py +401 -0
  110. pythonagent/main/pytrace/pytrace.py +133 -0
  111. pythonagent/main/wsgi.py +6 -0
  112. pythonagent/main.py +27 -0
  113. pythonagent/run.py +46 -0
  114. pythonagent/sqins.py +8 -0
  115. pythonagent/test.py +73 -0
  116. pythonagent/utils.py +168 -0
  117. pythonagent/vendor/__init__.py +0 -0
  118. pythonagent/vendor/pympler/__init__.py +1 -0
  119. pythonagent/vendor/pympler/asizeof.py +2810 -0
  120. pythonagent/vendor/pympler/charts.py +62 -0
  121. pythonagent/vendor/pympler/classtracker.py +590 -0
  122. pythonagent/vendor/pympler/classtracker_stats.py +780 -0
  123. pythonagent/vendor/pympler/garbagegraph.py +80 -0
  124. pythonagent/vendor/pympler/mprofile.py +97 -0
  125. pythonagent/vendor/pympler/muppy.py +275 -0
  126. pythonagent/vendor/pympler/panels.py +115 -0
  127. pythonagent/vendor/pympler/process.py +238 -0
  128. pythonagent/vendor/pympler/py.typed +0 -0
  129. pythonagent/vendor/pympler/refbrowser.py +451 -0
  130. pythonagent/vendor/pympler/refgraph.py +350 -0
  131. pythonagent/vendor/pympler/summary.py +321 -0
  132. pythonagent/vendor/pympler/tracker.py +267 -0
  133. pythonagent/vendor/pympler/util/__init__.py +0 -0
  134. pythonagent/vendor/pympler/util/bottle.py +3809 -0
  135. pythonagent/vendor/pympler/util/compat.py +23 -0
  136. pythonagent/vendor/pympler/util/stringutils.py +77 -0
  137. pythonagent/vendor/pympler/web.py +346 -0
  138. pythonagent/vendor/werkzeug/__init__.py +20 -0
  139. pythonagent/vendor/werkzeug/_compat.py +228 -0
  140. pythonagent/vendor/werkzeug/_internal.py +473 -0
  141. pythonagent/vendor/werkzeug/_reloader.py +341 -0
  142. pythonagent/vendor/werkzeug/datastructures.py +3120 -0
  143. pythonagent/vendor/werkzeug/debug/__init__.py +498 -0
  144. pythonagent/vendor/werkzeug/debug/console.py +218 -0
  145. pythonagent/vendor/werkzeug/debug/repr.py +297 -0
  146. pythonagent/vendor/werkzeug/debug/tbtools.py +628 -0
  147. pythonagent/vendor/werkzeug/exceptions.py +829 -0
  148. pythonagent/vendor/werkzeug/filesystem.py +64 -0
  149. pythonagent/vendor/werkzeug/formparser.py +584 -0
  150. pythonagent/vendor/werkzeug/http.py +1307 -0
  151. pythonagent/vendor/werkzeug/local.py +420 -0
  152. pythonagent/vendor/werkzeug/middleware/__init__.py +25 -0
  153. pythonagent/vendor/werkzeug/middleware/dispatcher.py +66 -0
  154. pythonagent/vendor/werkzeug/middleware/http_proxy.py +219 -0
  155. pythonagent/vendor/werkzeug/middleware/lint.py +408 -0
  156. pythonagent/vendor/werkzeug/middleware/profiler.py +132 -0
  157. pythonagent/vendor/werkzeug/middleware/proxy_fix.py +169 -0
  158. pythonagent/vendor/werkzeug/middleware/shared_data.py +293 -0
  159. pythonagent/vendor/werkzeug/posixemulation.py +117 -0
  160. pythonagent/vendor/werkzeug/routing.py +2210 -0
  161. pythonagent/vendor/werkzeug/security.py +249 -0
  162. pythonagent/vendor/werkzeug/serving.py +1117 -0
  163. pythonagent/vendor/werkzeug/test.py +1123 -0
  164. pythonagent/vendor/werkzeug/testapp.py +241 -0
  165. pythonagent/vendor/werkzeug/urls.py +1138 -0
  166. pythonagent/vendor/werkzeug/useragents.py +202 -0
  167. pythonagent/vendor/werkzeug/utils.py +778 -0
  168. pythonagent/vendor/werkzeug/wrappers/__init__.py +36 -0
  169. pythonagent/vendor/werkzeug/wrappers/accept.py +50 -0
  170. pythonagent/vendor/werkzeug/wrappers/auth.py +33 -0
  171. pythonagent/vendor/werkzeug/wrappers/base_request.py +673 -0
  172. pythonagent/vendor/werkzeug/wrappers/base_response.py +700 -0
  173. pythonagent/vendor/werkzeug/wrappers/common_descriptors.py +341 -0
  174. pythonagent/vendor/werkzeug/wrappers/cors.py +100 -0
  175. pythonagent/vendor/werkzeug/wrappers/etag.py +304 -0
  176. pythonagent/vendor/werkzeug/wrappers/json.py +145 -0
  177. pythonagent/vendor/werkzeug/wrappers/request.py +49 -0
  178. pythonagent/vendor/werkzeug/wrappers/response.py +84 -0
  179. pythonagent/vendor/werkzeug/wrappers/user_agent.py +14 -0
  180. 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()