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,780 @@
1
+ """
2
+ Provide saving, loading and presenting gathered `ClassTracker` statistics.
3
+ """
4
+
5
+ from typing import (
6
+ Any, Dict, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING, Union
7
+ )
8
+
9
+ import os
10
+ import pickle
11
+ import sys
12
+ from copy import deepcopy
13
+ from .util.stringutils import trunc, pp, pp_timestamp
14
+
15
+ from .asizeof import Asized
16
+
17
+ if TYPE_CHECKING:
18
+ from .classtracker import TrackedObject, ClassTracker, Snapshot
19
+
20
+
21
+ __all__ = ["Stats", "ConsoleStats", "HtmlStats"]
22
+
23
+
24
+ def _ref2key(ref: Asized) -> str:
25
+ return ref.name.split(':')[0]
26
+
27
+
28
+ def _merge_asized(base: Asized, other: Asized, level: int = 0) -> None:
29
+ """
30
+ Merge **Asized** instances `base` and `other` into `base`.
31
+ """
32
+ base.size += other.size
33
+ base.flat += other.flat
34
+ if level > 0:
35
+ base.name = _ref2key(base)
36
+ # Add refs from other to base. Any new refs are appended.
37
+ base.refs = list(base.refs) # we may need to append items
38
+ refs = {}
39
+ for ref in base.refs:
40
+ refs[_ref2key(ref)] = ref
41
+ for ref in other.refs:
42
+ key = _ref2key(ref)
43
+ if key in refs:
44
+ _merge_asized(refs[key], ref, level=level + 1)
45
+ else:
46
+ # Don't modify existing Asized instances => deepcopy
47
+ base.refs.append(deepcopy(ref))
48
+ base.refs[-1].name = key
49
+
50
+
51
+ def _merge_objects(tref: float, merged: Asized, obj: 'TrackedObject') -> None:
52
+ """
53
+ Merge the snapshot size information of multiple tracked objects. The
54
+ tracked object `obj` is scanned for size information at time `tref`.
55
+ The sizes are merged into **Asized** instance `merged`.
56
+ """
57
+ size = None
58
+ for (timestamp, tsize) in obj.snapshots:
59
+ if timestamp == tref:
60
+ size = tsize
61
+ if size:
62
+ _merge_asized(merged, size)
63
+
64
+
65
+ def _format_trace(trace: List[Tuple]) -> str:
66
+ """
67
+ Convert the (stripped) stack-trace to a nice readable format. The stack
68
+ trace `trace` is a list of frame records as returned by
69
+ **inspect.stack** but without the frame objects.
70
+ Returns a string.
71
+ """
72
+ lines = []
73
+ for fname, lineno, func, src, _ in trace:
74
+ if src:
75
+ for line in src:
76
+ lines.append(' ' + line.strip() + '\n')
77
+ lines.append(' %s:%4d in %s\n' % (fname, lineno, func))
78
+ return ''.join(lines)
79
+
80
+
81
+ class Stats(object):
82
+ """
83
+ Presents the memory statistics gathered by a `ClassTracker` based on user
84
+ preferences.
85
+ """
86
+
87
+ def __init__(self, tracker: 'Optional[ClassTracker]' = None,
88
+ filename: Optional[str] = None,
89
+ stream: Optional[IO] = None):
90
+ """
91
+ Initialize the data log structures either from a `ClassTracker`
92
+ instance (argument `tracker`) or a previously dumped file (argument
93
+ `filename`).
94
+
95
+ :param tracker: ClassTracker instance
96
+ :param filename: filename of previously dumped statistics
97
+ :param stream: where to print statistics, defaults to ``sys.stdout``
98
+ """
99
+ if stream:
100
+ self.stream = stream
101
+ else:
102
+ self.stream = sys.stdout
103
+ self.tracker = tracker
104
+ self.index = {} # type: Dict[str, List[TrackedObject]]
105
+ self.snapshots = [] # type: List[Snapshot]
106
+ if tracker:
107
+ self.index = tracker.index
108
+ self.snapshots = tracker.snapshots
109
+ self.history = tracker.history
110
+ self.sorted = [] # type: List[TrackedObject]
111
+ if filename:
112
+ self.load_stats(filename)
113
+
114
+ def load_stats(self, fdump: Union[str, IO[bytes]]) -> None:
115
+ """
116
+ Load the data from a dump file.
117
+ The argument `fdump` can be either a filename or an open file object
118
+ that requires read access.
119
+ """
120
+ if isinstance(fdump, str):
121
+ fdump = open(fdump, 'rb')
122
+ self.index = pickle.load(fdump)
123
+ self.snapshots = pickle.load(fdump)
124
+ self.sorted = []
125
+
126
+ def dump_stats(self, fdump: Union[str, IO[bytes]], close: bool = True
127
+ ) -> None:
128
+ """
129
+ Dump the logged data to a file.
130
+ The argument `file` can be either a filename or an open file object
131
+ that requires write access. `close` controls if the file is closed
132
+ before leaving this method (the default behaviour).
133
+ """
134
+ if self.tracker:
135
+ self.tracker.stop_periodic_snapshots()
136
+
137
+ if isinstance(fdump, str):
138
+ fdump = open(fdump, 'wb')
139
+ pickle.dump(self.index, fdump, protocol=pickle.HIGHEST_PROTOCOL)
140
+ pickle.dump(self.snapshots, fdump, protocol=pickle.HIGHEST_PROTOCOL)
141
+ if close:
142
+ fdump.close()
143
+
144
+ def _init_sort(self) -> None:
145
+ """
146
+ Prepare the data to be sorted.
147
+ If not yet sorted, import all tracked objects from the tracked index.
148
+ Extend the tracking information by implicit information to make
149
+ sorting easier (DSU pattern).
150
+ """
151
+ if not self.sorted:
152
+ # Identify the snapshot that tracked the largest amount of memory.
153
+ tmax = None
154
+ maxsize = 0
155
+ for snapshot in self.snapshots:
156
+ if snapshot.tracked_total > maxsize:
157
+ tmax = snapshot.timestamp
158
+ for key in list(self.index.keys()):
159
+ for tobj in self.index[key]:
160
+ tobj.classname = key # type: ignore
161
+ tobj.size = tobj.get_max_size() # type: ignore
162
+ tobj.tsize = tobj.get_size_at_time(tmax) # type: ignore
163
+ self.sorted.extend(self.index[key])
164
+
165
+ def sort_stats(self, *args: str) -> 'Stats':
166
+ """
167
+ Sort the tracked objects according to the supplied criteria. The
168
+ argument is a string identifying the basis of a sort (example: 'size'
169
+ or 'classname'). When more than one key is provided, then additional
170
+ keys are used as secondary criteria when there is equality in all keys
171
+ selected before them. For example, ``sort_stats('name', 'size')`` will
172
+ sort all the entries according to their class name, and resolve all
173
+ ties (identical class names) by sorting by size. The criteria are
174
+ fields in the tracked object instances. Results are stored in the
175
+ ``self.sorted`` list which is used by ``Stats.print_stats()`` and other
176
+ methods. The fields available for sorting are:
177
+
178
+ 'classname'
179
+ the name with which the class was registered
180
+ 'name'
181
+ the classname
182
+ 'birth'
183
+ creation timestamp
184
+ 'death'
185
+ destruction timestamp
186
+ 'size'
187
+ the maximum measured size of the object
188
+ 'tsize'
189
+ the measured size during the largest snapshot
190
+ 'repr'
191
+ string representation of the object
192
+
193
+ Note that sorts on size are in descending order (placing most memory
194
+ consuming items first), whereas name, repr, and creation time searches
195
+ are in ascending order (alphabetical).
196
+
197
+ The function returns self to allow calling functions on the result::
198
+
199
+ stats.sort_stats('size').reverse_order().print_stats()
200
+ """
201
+
202
+ criteria = ('classname', 'tsize', 'birth', 'death',
203
+ 'name', 'repr', 'size')
204
+
205
+ if not set(criteria).issuperset(set(args)):
206
+ raise ValueError("Invalid sort criteria")
207
+
208
+ if not args:
209
+ args = criteria
210
+
211
+ def args_to_tuple(obj: 'TrackedObject') -> Tuple[str, ...]:
212
+ keys: List[str] = []
213
+ for attr in args:
214
+ attribute = getattr(obj, attr, '')
215
+ if attr in ('tsize', 'size'):
216
+ attribute = -int(attribute)
217
+ keys.append(attribute)
218
+ return tuple(keys)
219
+
220
+ self._init_sort()
221
+ self.sorted.sort(key=args_to_tuple)
222
+
223
+ return self
224
+
225
+ def reverse_order(self) -> 'Stats':
226
+ """
227
+ Reverse the order of the tracked instance index `self.sorted`.
228
+ """
229
+ self._init_sort()
230
+ self.sorted.reverse()
231
+ return self
232
+
233
+ def annotate(self) -> None:
234
+ """
235
+ Annotate all snapshots with class-based summaries.
236
+ """
237
+ for snapshot in self.snapshots:
238
+ self.annotate_snapshot(snapshot)
239
+
240
+ def annotate_snapshot(self, snapshot: 'Snapshot'
241
+ ) -> Dict[str, Dict[str, Any]]:
242
+ """
243
+ Store additional statistical data in snapshot.
244
+ """
245
+ if snapshot.classes is not None:
246
+ return snapshot.classes
247
+
248
+ snapshot.classes = {}
249
+
250
+ for classname in list(self.index.keys()):
251
+ total = 0
252
+ active = 0
253
+ merged = Asized(0, 0)
254
+ for tobj in self.index[classname]:
255
+ _merge_objects(snapshot.timestamp, merged, tobj)
256
+ total += tobj.get_size_at_time(snapshot.timestamp)
257
+ if (tobj.birth < snapshot.timestamp and
258
+ (tobj.death is None or
259
+ tobj.death > snapshot.timestamp)):
260
+ active += 1
261
+
262
+ try:
263
+ pct = total * 100.0 / snapshot.total
264
+ except ZeroDivisionError: # pragma: no cover
265
+ pct = 0
266
+ try:
267
+ avg = total / active
268
+ except ZeroDivisionError:
269
+ avg = 0
270
+
271
+ snapshot.classes[classname] = dict(sum=total,
272
+ avg=avg,
273
+ pct=pct,
274
+ active=active)
275
+ snapshot.classes[classname]['merged'] = merged
276
+
277
+ return snapshot.classes
278
+
279
+ @property
280
+ def tracked_classes(self) -> List[str]:
281
+ """Return a list of all tracked classes occurring in any snapshot."""
282
+ return sorted(list(self.index.keys()))
283
+
284
+
285
+ class ConsoleStats(Stats):
286
+ """
287
+ Presentation layer for `Stats` to be used in text-based consoles.
288
+ """
289
+
290
+ def _print_refs(self, refs: Iterable[Asized], total: int,
291
+ prefix: str = ' ', level: int = 1, minsize: int = 0,
292
+ minpct: float = 0.1) -> None:
293
+ """
294
+ Print individual referents recursively.
295
+ """
296
+ lrefs = list(refs)
297
+ lrefs.sort(key=lambda x: x.size)
298
+ lrefs.reverse()
299
+ for ref in lrefs:
300
+ if ref.size > minsize and (ref.size * 100.0 / total) > minpct:
301
+ self.stream.write('%-50s %-14s %3d%% [%d]\n' % (
302
+ trunc(prefix + str(ref.name), 50),
303
+ pp(ref.size),
304
+ int(ref.size * 100.0 / total),
305
+ level
306
+ ))
307
+ self._print_refs(ref.refs, total, prefix=prefix + ' ',
308
+ level=level + 1)
309
+
310
+ def print_object(self, tobj: 'TrackedObject') -> None:
311
+ """
312
+ Print the gathered information of object `tobj` in human-readable
313
+ format.
314
+ """
315
+ if tobj.death:
316
+ self.stream.write('%-32s ( free ) %-35s\n' % (
317
+ trunc(tobj.name, 32, left=True), trunc(tobj.repr, 35)))
318
+ else:
319
+ self.stream.write('%-32s 0x%08x %-35s\n' % (
320
+ trunc(tobj.name, 32, left=True),
321
+
322
+ tobj.id,
323
+ trunc(tobj.repr, 35)
324
+ ))
325
+ if tobj.trace:
326
+ self.stream.write(_format_trace(tobj.trace))
327
+ for (timestamp, size) in tobj.snapshots:
328
+ self.stream.write(' %-30s %s\n' % (
329
+ pp_timestamp(timestamp), pp(size.size)
330
+ ))
331
+ self._print_refs(size.refs, size.size)
332
+ if tobj.death is not None:
333
+ self.stream.write(' %-30s finalize\n' % (
334
+ pp_timestamp(tobj.death),
335
+ ))
336
+
337
+ def print_stats(self, clsname: Optional[str] = None, limit: float = 1.0
338
+ ) -> None:
339
+ """
340
+ Write tracked objects to stdout. The output can be filtered and
341
+ pruned. Only objects are printed whose classname contain the substring
342
+ supplied by the `clsname` argument. The output can be pruned by
343
+ passing a `limit` value.
344
+
345
+ :param clsname: Only print objects whose classname contain the given
346
+ substring.
347
+ :param limit: If `limit` is a float smaller than one, only the supplied
348
+ percentage of the total tracked data is printed. If `limit` is
349
+ bigger than one, this number of tracked objects are printed.
350
+ Tracked objects are first filtered, and then pruned (if specified).
351
+ """
352
+ if self.tracker:
353
+ self.tracker.stop_periodic_snapshots()
354
+
355
+ if not self.sorted:
356
+ self.sort_stats()
357
+
358
+ _sorted = self.sorted
359
+
360
+ if clsname:
361
+ _sorted = [
362
+ to for to in _sorted
363
+ if clsname in to.classname # type: ignore
364
+ ]
365
+
366
+ if limit < 1.0:
367
+ limit = max(1, int(len(self.sorted) * limit))
368
+ _sorted = _sorted[:int(limit)]
369
+
370
+ # Emit per-instance data
371
+ for tobj in _sorted:
372
+ self.print_object(tobj)
373
+
374
+ def print_summary(self) -> None:
375
+ """
376
+ Print per-class summary for each snapshot.
377
+ """
378
+ # Emit class summaries for each snapshot
379
+ classlist = self.tracked_classes
380
+
381
+ fobj = self.stream
382
+
383
+ fobj.write('---- SUMMARY ' + '-' * 66 + '\n')
384
+ for snapshot in self.snapshots:
385
+ classes = self.annotate_snapshot(snapshot)
386
+ fobj.write('%-35s %11s %12s %12s %5s\n' % (
387
+ trunc(snapshot.desc, 35),
388
+ 'active',
389
+ pp(snapshot.asizeof_total),
390
+ 'average',
391
+ 'pct'
392
+ ))
393
+ for classname in classlist:
394
+ info = classes[classname]
395
+ fobj.write(' %-33s %11d %12s %12s %4d%%\n' % (
396
+ trunc(classname, 33),
397
+ info['active'],
398
+ pp(info['sum']),
399
+ pp(info['avg']),
400
+ info['pct']
401
+ ))
402
+ fobj.write('-' * 79 + '\n')
403
+
404
+
405
+ class HtmlStats(Stats):
406
+ """
407
+ Output the `ClassTracker` statistics as HTML pages and graphs.
408
+ """
409
+
410
+ style = """<style type="text/css">
411
+ table { width:100%; border:1px solid #000; border-spacing:0px; }
412
+ td, th { border:0px; }
413
+ div { width:200px; padding:10px; background-color:#FFEECC; }
414
+ #nb { border:0px; }
415
+ #tl { margin-top:5mm; margin-bottom:5mm; }
416
+ #p1 { padding-left: 5px; }
417
+ #p2 { padding-left: 50px; }
418
+ #p3 { padding-left: 100px; }
419
+ #p4 { padding-left: 150px; }
420
+ #p5 { padding-left: 200px; }
421
+ #p6 { padding-left: 210px; }
422
+ #p7 { padding-left: 220px; }
423
+ #hl { background-color:#FFFFCC; }
424
+ #r1 { background-color:#BBBBBB; }
425
+ #r2 { background-color:#CCCCCC; }
426
+ #r3 { background-color:#DDDDDD; }
427
+ #r4 { background-color:#EEEEEE; }
428
+ #r5,#r6,#r7 { background-color:#FFFFFF; }
429
+ #num { text-align:right; }
430
+ </style>
431
+ """
432
+
433
+ nopylab_msg = """<div color="#FFCCCC">Could not generate %s chart!
434
+ Install <a href="http://matplotlib.sourceforge.net/">Matplotlib</a>
435
+ to generate charts.</div>\n"""
436
+
437
+ chart_tag = '<img src="%s">\n'
438
+ header = "<html><head><title>%s</title>%s</head><body>\n"
439
+ tableheader = '<table border="1">\n'
440
+ tablefooter = '</table>\n'
441
+ footer = '</body></html>\n'
442
+
443
+ refrow = """<tr id="r%(level)d">
444
+ <td id="p%(level)d">%(name)s</td>
445
+ <td id="num">%(size)s</td>
446
+ <td id="num">%(pct)3.1f%%</td></tr>"""
447
+
448
+ def _print_refs(self, fobj: IO, refs: Iterable[Asized], total: int,
449
+ level: int = 1, minsize: int = 0, minpct: float = 0.1
450
+ ) -> None:
451
+ """
452
+ Print individual referents recursively.
453
+ """
454
+ lrefs = list(refs)
455
+ lrefs.sort(key=lambda x: x.size)
456
+ lrefs.reverse()
457
+ if level == 1:
458
+ fobj.write('<table>\n')
459
+ for ref in lrefs:
460
+ if ref.size > minsize and (ref.size * 100.0 / total) > minpct:
461
+ data = dict(level=level,
462
+ name=trunc(str(ref.name), 128),
463
+ size=pp(ref.size),
464
+ pct=ref.size * 100.0 / total)
465
+ fobj.write(self.refrow % data)
466
+ self._print_refs(fobj, ref.refs, total, level=level + 1)
467
+ if level == 1:
468
+ fobj.write("</table>\n")
469
+
470
+ class_summary = """<p>%(cnt)d instances of %(cls)s were registered. The
471
+ average size is %(avg)s, the minimal size is %(min)s, the maximum size
472
+ is %(max)s.</p>\n"""
473
+ class_snapshot = '''<h3>Snapshot: %(name)s, %(total)s occupied by instances
474
+ of class %(cls)s</h3>\n'''
475
+
476
+ def print_class_details(self, fname: str, classname: str) -> None:
477
+ """
478
+ Print detailed statistics and instances for the class `classname`. All
479
+ data will be written to the file `fname`.
480
+ """
481
+ fobj = open(fname, "w")
482
+ fobj.write(self.header % (classname, self.style))
483
+
484
+ fobj.write("<h1>%s</h1>\n" % (classname))
485
+
486
+ sizes = [tobj.get_max_size() for tobj in self.index[classname]]
487
+ total = 0
488
+ for s in sizes:
489
+ total += s
490
+ data = {'cnt': len(self.index[classname]), 'cls': classname}
491
+ data['avg'] = pp(total / len(sizes))
492
+ data['max'] = pp(max(sizes))
493
+ data['min'] = pp(min(sizes))
494
+ fobj.write(self.class_summary % data)
495
+
496
+ fobj.write(self.charts[classname])
497
+
498
+ fobj.write("<h2>Coalesced Referents per Snapshot</h2>\n")
499
+ for snapshot in self.snapshots:
500
+ if snapshot.classes and classname in snapshot.classes:
501
+ merged = snapshot.classes[classname]['merged']
502
+ fobj.write(self.class_snapshot % {
503
+ 'name': snapshot.desc,
504
+ 'cls': classname,
505
+ 'total': pp(merged.size),
506
+ })
507
+ if merged.refs:
508
+ self._print_refs(fobj, merged.refs, merged.size)
509
+ else:
510
+ fobj.write('<p>No per-referent sizes recorded.</p>\n')
511
+
512
+ fobj.write("<h2>Instances</h2>\n")
513
+ for tobj in self.index[classname]:
514
+ fobj.write('<table id="tl" width="100%" rules="rows">\n')
515
+ fobj.write('<tr><td id="hl" width="140px">Instance</td>' +
516
+ '<td id="hl">%s at 0x%08x</td></tr>\n' %
517
+ (tobj.name, tobj.id))
518
+ if tobj.repr:
519
+ fobj.write("<tr><td>Representation</td>" +
520
+ "<td>%s&nbsp;</td></tr>\n" % tobj.repr)
521
+ fobj.write("<tr><td>Lifetime</td><td>%s - %s</td></tr>\n" %
522
+ (pp_timestamp(tobj.birth), pp_timestamp(tobj.death)))
523
+ if tobj.trace:
524
+ trace = "<pre>%s</pre>" % (_format_trace(tobj.trace))
525
+ fobj.write("<tr><td>Instantiation</td><td>%s</td></tr>\n" %
526
+ trace)
527
+ for (timestamp, size) in tobj.snapshots:
528
+ fobj.write("<tr><td>%s</td>" % pp_timestamp(timestamp))
529
+ if not size.refs:
530
+ fobj.write("<td>%s</td></tr>\n" % pp(size.size))
531
+ else:
532
+ fobj.write("<td>%s" % pp(size.size))
533
+ self._print_refs(fobj, size.refs, size.size)
534
+ fobj.write("</td></tr>\n")
535
+ fobj.write("</table>\n")
536
+
537
+ fobj.write(self.footer)
538
+ fobj.close()
539
+
540
+ snapshot_cls_header = """<tr>
541
+ <th id="hl">Class</th>
542
+ <th id="hl" align="right">Instance #</th>
543
+ <th id="hl" align="right">Total</th>
544
+ <th id="hl" align="right">Average size</th>
545
+ <th id="hl" align="right">Share</th></tr>\n"""
546
+
547
+ snapshot_cls = """<tr>
548
+ <td>%(cls)s</td>
549
+ <td align="right">%(active)d</td>
550
+ <td align="right">%(sum)s</td>
551
+ <td align="right">%(avg)s</td>
552
+ <td align="right">%(pct)3.2f%%</td></tr>\n"""
553
+
554
+ snapshot_summary = """<p>Total virtual memory assigned to the program
555
+ at that time was %(sys)s, which includes %(overhead)s profiling
556
+ overhead. The ClassTracker tracked %(tracked)s in total. The measurable
557
+ objects including code objects but excluding overhead have a total size
558
+ of %(asizeof)s.</p>\n"""
559
+
560
+ def relative_path(self, filepath: str, basepath: Optional[str] = None
561
+ ) -> str:
562
+ """
563
+ Convert the filepath path to a relative path against basepath. By
564
+ default basepath is self.basedir.
565
+ """
566
+ if basepath is None:
567
+ basepath = self.basedir
568
+ if not basepath:
569
+ return filepath
570
+ if filepath.startswith(basepath):
571
+ filepath = filepath[len(basepath):]
572
+ if filepath and filepath[0] == os.sep:
573
+ filepath = filepath[1:]
574
+ return filepath
575
+
576
+ def create_title_page(self, filename: str, title: str = '') -> None:
577
+ """
578
+ Output the title page.
579
+ """
580
+ fobj = open(filename, "w")
581
+ fobj.write(self.header % (title, self.style))
582
+
583
+ fobj.write("<h1>%s</h1>\n" % title)
584
+ fobj.write("<h2>Memory distribution over time</h2>\n")
585
+ fobj.write(self.charts['snapshots'])
586
+
587
+ fobj.write("<h2>Snapshots statistics</h2>\n")
588
+ fobj.write('<table id="nb">\n')
589
+
590
+ classlist = list(self.index.keys())
591
+ classlist.sort()
592
+
593
+ for snapshot in self.snapshots:
594
+ fobj.write('<tr><td>\n')
595
+ fobj.write('<table id="tl" rules="rows">\n')
596
+ fobj.write("<h3>%s snapshot at %s</h3>\n" % (
597
+ snapshot.desc or 'Untitled',
598
+ pp_timestamp(snapshot.timestamp)
599
+ ))
600
+
601
+ data = {}
602
+ data['sys'] = pp(snapshot.system_total.vsz)
603
+ data['tracked'] = pp(snapshot.tracked_total)
604
+ data['asizeof'] = pp(snapshot.asizeof_total)
605
+ data['overhead'] = pp(getattr(snapshot, 'overhead', 0))
606
+
607
+ fobj.write(self.snapshot_summary % data)
608
+
609
+ if snapshot.tracked_total:
610
+ fobj.write(self.snapshot_cls_header)
611
+ for classname in classlist:
612
+ if snapshot.classes:
613
+ info = snapshot.classes[classname].copy()
614
+ path = self.relative_path(self.links[classname])
615
+ info['cls'] = '<a href="%s">%s</a>' % (path, classname)
616
+ info['sum'] = pp(info['sum'])
617
+ info['avg'] = pp(info['avg'])
618
+ fobj.write(self.snapshot_cls % info)
619
+ fobj.write('</table>')
620
+ fobj.write('</td><td>\n')
621
+ if snapshot.tracked_total:
622
+ fobj.write(self.charts[snapshot])
623
+ fobj.write('</td></tr>\n')
624
+
625
+ fobj.write("</table>\n")
626
+ fobj.write(self.footer)
627
+ fobj.close()
628
+
629
+ def create_lifetime_chart(self, classname: str, filename: str = '') -> str:
630
+ """
631
+ Create chart that depicts the lifetime of the instance registered with
632
+ `classname`. The output is written to `filename`.
633
+ """
634
+ try:
635
+ from pylab import figure, title, xlabel, ylabel, plot, savefig
636
+ except ImportError:
637
+ return HtmlStats.nopylab_msg % (classname + " lifetime")
638
+
639
+ cnt = []
640
+ for tobj in self.index[classname]:
641
+ cnt.append([tobj.birth, 1])
642
+ if tobj.death:
643
+ cnt.append([tobj.death, -1])
644
+ cnt.sort()
645
+ for i in range(1, len(cnt)):
646
+ cnt[i][1] += cnt[i - 1][1]
647
+
648
+ x = [t for [t, c] in cnt]
649
+ y = [c for [t, c] in cnt]
650
+
651
+ figure()
652
+ xlabel("Execution time [s]")
653
+ ylabel("Instance #")
654
+ title("%s instances" % classname)
655
+ plot(x, y, 'o')
656
+ savefig(filename)
657
+
658
+ return self.chart_tag % (os.path.basename(filename))
659
+
660
+ def create_snapshot_chart(self, filename: str = '') -> str:
661
+ """
662
+ Create chart that depicts the memory allocation over time apportioned
663
+ to the tracked classes.
664
+ """
665
+ try:
666
+ from pylab import (figure, title, xlabel, ylabel, plot, fill,
667
+ legend, savefig)
668
+ import matplotlib.mlab as mlab
669
+ except ImportError:
670
+ return self.nopylab_msg % ("memory allocation")
671
+
672
+ classlist = self.tracked_classes
673
+
674
+ times = [snapshot.timestamp for snapshot in self.snapshots]
675
+ base = [0.0] * len(self.snapshots)
676
+ poly_labels = []
677
+ polys = []
678
+ for cn in classlist:
679
+ pct = [snapshot.classes[cn]['pct'] for snapshot in self.snapshots
680
+ if snapshot.classes is not None]
681
+ if pct and max(pct) > 3.0:
682
+ sz = [float(fp.classes[cn]['sum']) / (1024 * 1024)
683
+ for fp in self.snapshots
684
+ if fp.classes is not None]
685
+ sz = [sx + sy for sx, sy in zip(base, sz)]
686
+ xp, yp = mlab.poly_between(times, base, sz)
687
+ polys.append(((xp, yp), {'label': cn}))
688
+ poly_labels.append(cn)
689
+ base = sz
690
+
691
+ figure()
692
+ title("Snapshot Memory")
693
+ xlabel("Execution Time [s]")
694
+ ylabel("Virtual Memory [MiB]")
695
+
696
+ sizes = [float(fp.asizeof_total) / (1024 * 1024)
697
+ for fp in self.snapshots]
698
+ plot(times, sizes, 'r--', label='Total')
699
+ sizes = [float(fp.tracked_total) / (1024 * 1024)
700
+ for fp in self.snapshots]
701
+ plot(times, sizes, 'b--', label='Tracked total')
702
+
703
+ for (args, kwds) in polys:
704
+ fill(*args, **kwds)
705
+ legend(loc=2)
706
+ savefig(filename)
707
+
708
+ return self.chart_tag % (self.relative_path(filename))
709
+
710
+ def create_pie_chart(self, snapshot: 'Snapshot', filename: str = '') -> str:
711
+ """
712
+ Create a pie chart that depicts the distribution of the allocated
713
+ memory for a given `snapshot`. The chart is saved to `filename`.
714
+ """
715
+ try:
716
+ from pylab import figure, title, pie, axes, savefig
717
+ from pylab import sum as pylab_sum
718
+ except ImportError:
719
+ return self.nopylab_msg % ("pie_chart")
720
+
721
+ # Don't bother illustrating a pie without pieces.
722
+ if not snapshot.tracked_total or snapshot.classes is None:
723
+ return ''
724
+
725
+ classlist = []
726
+ sizelist = []
727
+ for k, v in list(snapshot.classes.items()):
728
+ if v['pct'] > 3.0:
729
+ classlist.append(k)
730
+ sizelist.append(v['sum'])
731
+ sizelist.insert(0, snapshot.asizeof_total - pylab_sum(sizelist))
732
+ classlist.insert(0, 'Other')
733
+
734
+ title("Snapshot (%s) Memory Distribution" % (snapshot.desc))
735
+ figure(figsize=(8, 8))
736
+ axes([0.1, 0.1, 0.8, 0.8])
737
+ pie(sizelist, labels=classlist)
738
+ savefig(filename, dpi=50)
739
+
740
+ return self.chart_tag % (self.relative_path(filename))
741
+
742
+ def create_html(self, fname: str, title: str = "ClassTracker Statistics"
743
+ ) -> None:
744
+ """
745
+ Create HTML page `fname` and additional files in a directory derived
746
+ from `fname`.
747
+ """
748
+ # Create a folder to store the charts and additional HTML files.
749
+ self.basedir = os.path.dirname(os.path.abspath(fname))
750
+ self.filesdir = os.path.splitext(fname)[0] + '_files'
751
+ if not os.path.isdir(self.filesdir):
752
+ os.mkdir(self.filesdir)
753
+ self.filesdir = os.path.abspath(self.filesdir)
754
+ self.links = {} # type: Dict[str, str]
755
+
756
+ # Annotate all snapshots in advance
757
+ self.annotate()
758
+
759
+ # Create charts. The tags to show the images are returned and stored in
760
+ # the self.charts dictionary. This allows to return alternative text if
761
+ # the chart creation framework is not available.
762
+ self.charts = {} # type: Dict[Union[str, Snapshot], str]
763
+ fn = os.path.join(self.filesdir, 'timespace.png')
764
+ self.charts['snapshots'] = self.create_snapshot_chart(fn)
765
+
766
+ for fp, idx in zip(self.snapshots, list(range(len(self.snapshots)))):
767
+ fn = os.path.join(self.filesdir, 'fp%d.png' % (idx))
768
+ self.charts[fp] = self.create_pie_chart(fp, fn)
769
+
770
+ for cn in list(self.index.keys()):
771
+ fn = os.path.join(self.filesdir, cn.replace('.', '_') + '-lt.png')
772
+ self.charts[cn] = self.create_lifetime_chart(cn, fn)
773
+
774
+ # Create HTML pages first for each class and then the index page.
775
+ for cn in list(self.index.keys()):
776
+ fn = os.path.join(self.filesdir, cn.replace('.', '_') + '.html')
777
+ self.links[cn] = fn
778
+ self.print_class_details(fn, cn)
779
+
780
+ self.create_title_page(fname, title=title)