viztracer 1.1.1__cp313-cp313-win32.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 (109) hide show
  1. viztracer/__init__.py +19 -0
  2. viztracer/__main__.py +8 -0
  3. viztracer/attach.py +67 -0
  4. viztracer/attach_process/LICENSE +203 -0
  5. viztracer/attach_process/__init__.py +0 -0
  6. viztracer/attach_process/add_code_to_python_process.py +582 -0
  7. viztracer/attach_process/attach_x86.dll +0 -0
  8. viztracer/attach_process/inject_dll_amd64.exe +0 -0
  9. viztracer/attach_process/linux_and_mac/lldb_prepare.py +54 -0
  10. viztracer/attach_process/run_code_on_dllmain_amd64.dll +0 -0
  11. viztracer/attach_process/run_code_on_dllmain_x86.dll +0 -0
  12. viztracer/cellmagic.py +70 -0
  13. viztracer/code_monkey.py +353 -0
  14. viztracer/decorator.py +164 -0
  15. viztracer/event_base.py +81 -0
  16. viztracer/functree.py +135 -0
  17. viztracer/html/flamegraph.html +34 -0
  18. viztracer/html/trace_viewer_embedder.html +203 -0
  19. viztracer/html/trace_viewer_full.html +10207 -0
  20. viztracer/main.py +701 -0
  21. viztracer/modules/eventnode.c +188 -0
  22. viztracer/modules/eventnode.h +73 -0
  23. viztracer/modules/pythoncapi_compat.h +1726 -0
  24. viztracer/modules/quicktime.c +177 -0
  25. viztracer/modules/quicktime.h +104 -0
  26. viztracer/modules/snaptrace.c +2207 -0
  27. viztracer/modules/snaptrace.h +134 -0
  28. viztracer/modules/snaptrace_member.c +483 -0
  29. viztracer/modules/util.c +45 -0
  30. viztracer/modules/util.h +22 -0
  31. viztracer/modules/vcompressor/vc_dump.c +1131 -0
  32. viztracer/modules/vcompressor/vc_dump.h +49 -0
  33. viztracer/modules/vcompressor/vcompressor.c +396 -0
  34. viztracer/modules/vcompressor/vcompressor.h +15 -0
  35. viztracer/patch.py +317 -0
  36. viztracer/report_builder.py +311 -0
  37. viztracer/snaptrace.cp313-win32.pyd +0 -0
  38. viztracer/snaptrace.pyi +77 -0
  39. viztracer/util.py +196 -0
  40. viztracer/vcompressor.cp313-win32.pyd +0 -0
  41. viztracer/vcompressor.pyi +10 -0
  42. viztracer/viewer.py +529 -0
  43. viztracer/vizcounter.py +20 -0
  44. viztracer/vizevent.py +31 -0
  45. viztracer/vizlogging.py +20 -0
  46. viztracer/vizobject.py +28 -0
  47. viztracer/vizplugin.py +143 -0
  48. viztracer/viztracer.py +472 -0
  49. viztracer/web_dist/LICENSE +189 -0
  50. viztracer/web_dist/index.html +127 -0
  51. viztracer/web_dist/service_worker.js +279 -0
  52. viztracer/web_dist/trace_processor +300 -0
  53. viztracer/web_dist/v52.0-6b9586def/assets/MaterialSymbolsOutlined.woff2 +0 -0
  54. viztracer/web_dist/v52.0-6b9586def/assets/Roboto-100.woff2 +0 -0
  55. viztracer/web_dist/v52.0-6b9586def/assets/Roboto-300.woff2 +0 -0
  56. viztracer/web_dist/v52.0-6b9586def/assets/Roboto-400.woff2 +0 -0
  57. viztracer/web_dist/v52.0-6b9586def/assets/Roboto-500.woff2 +0 -0
  58. viztracer/web_dist/v52.0-6b9586def/assets/RobotoCondensed-Light.woff2 +0 -0
  59. viztracer/web_dist/v52.0-6b9586def/assets/RobotoCondensed-Regular.woff2 +0 -0
  60. viztracer/web_dist/v52.0-6b9586def/assets/RobotoMono-Regular.woff2 +0 -0
  61. viztracer/web_dist/v52.0-6b9586def/assets/brand.png +0 -0
  62. viztracer/web_dist/v52.0-6b9586def/assets/catapult_trace_viewer.html +3946 -0
  63. viztracer/web_dist/v52.0-6b9586def/assets/catapult_trace_viewer.js +7539 -0
  64. viztracer/web_dist/v52.0-6b9586def/assets/favicon.png +0 -0
  65. viztracer/web_dist/v52.0-6b9586def/assets/logo-128.png +0 -0
  66. viztracer/web_dist/v52.0-6b9586def/assets/logo-3d.png +0 -0
  67. viztracer/web_dist/v52.0-6b9586def/assets/rec_atrace.png +0 -0
  68. viztracer/web_dist/v52.0-6b9586def/assets/rec_battery_counters.png +0 -0
  69. viztracer/web_dist/v52.0-6b9586def/assets/rec_board_voltage.png +0 -0
  70. viztracer/web_dist/v52.0-6b9586def/assets/rec_cpu_coarse.png +0 -0
  71. viztracer/web_dist/v52.0-6b9586def/assets/rec_cpu_fine.png +0 -0
  72. viztracer/web_dist/v52.0-6b9586def/assets/rec_cpu_freq.png +0 -0
  73. viztracer/web_dist/v52.0-6b9586def/assets/rec_cpu_voltage.png +0 -0
  74. viztracer/web_dist/v52.0-6b9586def/assets/rec_frame_timeline.png +0 -0
  75. viztracer/web_dist/v52.0-6b9586def/assets/rec_ftrace.png +0 -0
  76. viztracer/web_dist/v52.0-6b9586def/assets/rec_gpu_mem_total.png +0 -0
  77. viztracer/web_dist/v52.0-6b9586def/assets/rec_java_heap_dump.png +0 -0
  78. viztracer/web_dist/v52.0-6b9586def/assets/rec_lmk.png +0 -0
  79. viztracer/web_dist/v52.0-6b9586def/assets/rec_logcat.png +0 -0
  80. viztracer/web_dist/v52.0-6b9586def/assets/rec_long_trace.png +0 -0
  81. viztracer/web_dist/v52.0-6b9586def/assets/rec_mem_hifreq.png +0 -0
  82. viztracer/web_dist/v52.0-6b9586def/assets/rec_meminfo.png +0 -0
  83. viztracer/web_dist/v52.0-6b9586def/assets/rec_native_heap_profiler.png +0 -0
  84. viztracer/web_dist/v52.0-6b9586def/assets/rec_one_shot.png +0 -0
  85. viztracer/web_dist/v52.0-6b9586def/assets/rec_profiling.png +0 -0
  86. viztracer/web_dist/v52.0-6b9586def/assets/rec_ps_stats.png +0 -0
  87. viztracer/web_dist/v52.0-6b9586def/assets/rec_ring_buf.png +0 -0
  88. viztracer/web_dist/v52.0-6b9586def/assets/rec_syscalls.png +0 -0
  89. viztracer/web_dist/v52.0-6b9586def/assets/rec_vmstat.png +0 -0
  90. viztracer/web_dist/v52.0-6b9586def/assets/scheduling_latency.png +0 -0
  91. viztracer/web_dist/v52.0-6b9586def/assets/vscode-icon.png +0 -0
  92. viztracer/web_dist/v52.0-6b9586def/engine_bundle.js +3 -0
  93. viztracer/web_dist/v52.0-6b9586def/frontend_bundle.js +5495 -0
  94. viztracer/web_dist/v52.0-6b9586def/index.html +127 -0
  95. viztracer/web_dist/v52.0-6b9586def/manifest.json +52 -0
  96. viztracer/web_dist/v52.0-6b9586def/perfetto.css +5737 -0
  97. viztracer/web_dist/v52.0-6b9586def/stdlib_docs.json +1 -0
  98. viztracer/web_dist/v52.0-6b9586def/trace_config_utils.wasm +0 -0
  99. viztracer/web_dist/v52.0-6b9586def/trace_processor.wasm +0 -0
  100. viztracer/web_dist/v52.0-6b9586def/trace_processor_memory64.wasm +0 -0
  101. viztracer/web_dist/v52.0-6b9586def/traceconv.wasm +0 -0
  102. viztracer/web_dist/v52.0-6b9586def/traceconv_bundle.js +2 -0
  103. viztracer-1.1.1.dist-info/METADATA +326 -0
  104. viztracer-1.1.1.dist-info/RECORD +109 -0
  105. viztracer-1.1.1.dist-info/WHEEL +5 -0
  106. viztracer-1.1.1.dist-info/entry_points.txt +3 -0
  107. viztracer-1.1.1.dist-info/licenses/LICENSE +222 -0
  108. viztracer-1.1.1.dist-info/licenses/NOTICE.txt +27 -0
  109. viztracer-1.1.1.dist-info/top_level.txt +1 -0
viztracer/patch.py ADDED
@@ -0,0 +1,317 @@
1
+ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2
+ # For details: https://github.com/gaogaotiantian/viztracer/blob/master/NOTICE.txt
3
+
4
+ from __future__ import annotations
5
+
6
+ import functools
7
+ import os
8
+ import re
9
+ import shutil
10
+ import sys
11
+ import textwrap
12
+ from multiprocessing import Process
13
+ from typing import Any, Callable, Sequence, no_type_check
14
+
15
+ from .viztracer import VizTracer
16
+
17
+
18
+ def patch_subprocess(viz_args: list[str]) -> None:
19
+ import shlex
20
+ import subprocess
21
+
22
+ # Try to detect the end of the python argument list and parse out various invocation patterns:
23
+ # `file.py args` | - args | `-- file.py args` | `-cprint(5) args` | `-Esm mod args`
24
+ py_arg_pat = re.compile("([^-].+)|-$|(--)$|-([a-z]+)?(c|m)(.+)?", re.IGNORECASE)
25
+ # Note: viztracer doesn't really work in interactive mode and arg handling is weird.
26
+ # Unlikely to be used in practice anyway so we just skip wrapping interactive python processes.
27
+ interactive_pat = re.compile("-[A-Za-z]*?i[A-Za-z]*$")
28
+
29
+ def build_command(args: Sequence[str]) -> list[str] | None:
30
+ py_args: list[str] = []
31
+ mode: list[str] | None = []
32
+ script = None
33
+ args_iter = iter(args[1:])
34
+ for arg in args_iter:
35
+ if interactive_pat.match(arg):
36
+ return None
37
+
38
+ match = py_arg_pat.match(arg)
39
+ if match:
40
+ file, ddash, cm_py_args, cm, cm_arg = match.groups()
41
+ if file:
42
+ # file.py [script args]
43
+ script = file
44
+ elif ddash:
45
+ # -- file.py [script args]
46
+ script = next(args_iter, None)
47
+ elif cm:
48
+ # -m mod [script args]
49
+ if cm_py_args:
50
+ # "-[pyopts]m"
51
+ py_args.append(f"-{cm_py_args}")
52
+ mode = [f"-{cm}"]
53
+ # -m mod | -mmod
54
+ cm_arg = cm_arg or next(args_iter, None)
55
+ if cm_arg is not None:
56
+ if cm_arg.split('.')[0] == "viztracer":
57
+ # Avoid tracing viztracer subprocess
58
+ # This is mainly used to avoid tracing --open
59
+ return None
60
+ mode.append(cm_arg)
61
+ else:
62
+ mode = None
63
+ break
64
+
65
+ # -pyopts
66
+ py_args.append(arg)
67
+ if arg in ("-X", "-W", "--check-hash-based-pycs"):
68
+ arg_next = next(args_iter, None)
69
+ if arg_next is not None:
70
+ py_args.append(arg_next)
71
+ else:
72
+ return None
73
+
74
+ if script:
75
+ return [sys.executable, *py_args, "-m", "viztracer", "--quiet", *viz_args, "--", script, *args_iter]
76
+ elif mode:
77
+ return [sys.executable, *py_args, "-m", "viztracer", "--quiet", *viz_args, *mode, "--", *args_iter]
78
+ return None
79
+
80
+ def is_python_entry(path: str) -> bool:
81
+ real_path = shutil.which(path)
82
+ if real_path is None:
83
+ return False
84
+ try:
85
+ with open(real_path, "rb") as f:
86
+ if f.read(2) == b"#!":
87
+ executable = f.readline().decode("utf-8").strip()
88
+ if "python" in executable.split('/')[-1]:
89
+ return True
90
+ except Exception: # pragma: no cover
91
+ pass
92
+ return False
93
+
94
+ @functools.wraps(subprocess.Popen.__init__)
95
+ def subprocess_init(self: subprocess.Popen[Any], args: str | Sequence[Any] | Any, **kwargs: Any) -> None:
96
+ new_args = args
97
+ if isinstance(new_args, str):
98
+ new_args = shlex.split(new_args, posix=sys.platform != "win32")
99
+ if isinstance(new_args, Sequence):
100
+ if "python" in os.path.basename(new_args[0]):
101
+ new_args = build_command(new_args)
102
+ elif is_python_entry(new_args[0]):
103
+ new_args = ["python", "-m", "viztracer", "--quiet", *viz_args, "--", *new_args]
104
+ else:
105
+ new_args = None
106
+ if new_args is not None and kwargs.get("shell") and isinstance(args, str):
107
+ # For shell=True, we should convert the commands back to string
108
+ # if it was passed as string
109
+ # This is mostly for Unix shell
110
+ new_args = " ".join(new_args)
111
+
112
+ if new_args is None:
113
+ new_args = args
114
+ assert hasattr(subprocess_init, "__wrapped__") # for mypy
115
+ subprocess_init.__wrapped__(self, new_args, **kwargs)
116
+
117
+ # We need to filter the arguments as there are something we may not want
118
+ if "-m" in viz_args:
119
+ # If it's a module run, we don't want to use that module for subprocess
120
+ idx = viz_args.index("-m")
121
+ viz_args.pop(idx)
122
+ viz_args.pop(idx)
123
+
124
+ setattr(subprocess.Popen, "__originit__", subprocess.Popen.__init__)
125
+ setattr(subprocess.Popen, "__init__", subprocess_init)
126
+
127
+
128
+ def patch_multiprocessing(tracer: VizTracer, viz_args: list[str]) -> None:
129
+
130
+ # For fork process
131
+ def func_after_fork(tracer: VizTracer):
132
+ tracer.register_exit()
133
+
134
+ tracer.clear()
135
+ tracer.reset_stack()
136
+
137
+ if tracer._afterfork_cb:
138
+ tracer._afterfork_cb(tracer, *tracer._afterfork_args, **tracer._afterfork_kwargs)
139
+
140
+ import multiprocessing.spawn
141
+ import multiprocessing.util
142
+ from multiprocessing.util import register_after_fork # type: ignore
143
+
144
+ register_after_fork(tracer, func_after_fork)
145
+
146
+ if sys.platform == "win32":
147
+ # For spawn process on Windows
148
+ @functools.wraps(multiprocessing.spawn.get_command_line)
149
+ def get_command_line(**kwds) -> list[str]:
150
+ """
151
+ Returns prefix of command line used for spawning a child process
152
+ """
153
+ if getattr(sys, 'frozen', False): # pragma: no cover
154
+ return ([sys.executable, '--multiprocessing-fork']
155
+ + ['%s=%r' % item for item in kwds.items()])
156
+ else:
157
+ prog = textwrap.dedent(f"""
158
+ from multiprocessing.spawn import spawn_main;
159
+ from viztracer.patch import patch_spawned_process;
160
+ patch_spawned_process({tracer.init_kwargs}, {viz_args});
161
+ spawn_main(%s)
162
+ """)
163
+ prog %= ', '.join('%s=%r' % item for item in kwds.items())
164
+ opts = multiprocessing.util._args_from_interpreter_flags() # type: ignore
165
+ return [multiprocessing.spawn._python_exe] + opts + ['-c', prog, '--multiprocessing-fork'] # type: ignore
166
+
167
+ multiprocessing.spawn.get_command_line = get_command_line
168
+ else:
169
+ # POSIX
170
+ # For forkserver process and spawned process
171
+ # We patch spawnv_passfds to trace forkserver parent process so the forked
172
+ # children can be traced
173
+ _spawnv_passfds = multiprocessing.util.spawnv_passfds
174
+
175
+ @functools.wraps(_spawnv_passfds)
176
+ def spawnv_passfds(path, args, passfds):
177
+ if "-c" in args:
178
+ idx = args.index("-c")
179
+ cmd = args[idx + 1]
180
+ if "forkserver" in cmd:
181
+ # forkserver will not end before main process, avoid deadlock by --patch_only
182
+ args = (
183
+ args[:idx]
184
+ + ["-m", "viztracer", "--patch_only", *viz_args]
185
+ + ["--subprocess_child", "--dump_raw", "-o", tracer.output_file]
186
+ + args[idx:]
187
+ )
188
+ elif "resource_tracker" not in cmd:
189
+ # We don't trace resource_tracker as it does not quit before the main process
190
+ # This is a normal spawned process. Only one of spawnv_passfds and spawn._main
191
+ # can be patched. forkserver process will use spawn._main after forking a child,
192
+ # so on POSIX we patch spawnv_passfds which has a similar effect on spawned processes.
193
+ args = (
194
+ args[:idx]
195
+ + ["-m", "viztracer", *viz_args]
196
+ + ["--subprocess_child", "--dump_raw", "-o", tracer.output_file]
197
+ + args[idx:]
198
+ )
199
+ ret = _spawnv_passfds(path, args, passfds)
200
+ return ret
201
+
202
+ multiprocessing.util.spawnv_passfds = spawnv_passfds # type: ignore
203
+
204
+
205
+ class SpawnProcess:
206
+ def __init__(
207
+ self,
208
+ viztracer_kwargs: dict[str, Any],
209
+ run: Callable,
210
+ target: Callable,
211
+ args: list[Any],
212
+ kwargs: dict[str, Any],
213
+ cmdline_args: list[str]):
214
+ self._viztracer_kwargs = viztracer_kwargs
215
+ self._run = run
216
+ self._target = target
217
+ self._args = args
218
+ self._kwargs = kwargs
219
+ self._cmdline_args = cmdline_args
220
+ self._exiting = False
221
+
222
+ def run(self) -> None:
223
+ import viztracer
224
+
225
+ tracer = viztracer.VizTracer(**self._viztracer_kwargs)
226
+ install_all_hooks(tracer, self._cmdline_args)
227
+ tracer.register_exit()
228
+ if not self._viztracer_kwargs.get("log_sparse"):
229
+ tracer.start()
230
+ self._run()
231
+
232
+
233
+ def patch_spawned_process(viztracer_kwargs: dict[str, Any], cmdline_args: list[str]):
234
+ import multiprocessing.spawn
235
+ from multiprocessing import process, reduction # type: ignore
236
+ from multiprocessing.spawn import prepare
237
+
238
+ @no_type_check
239
+ @functools.wraps(multiprocessing.spawn._main)
240
+ def _main(fd, parent_sentinel) -> Any:
241
+ with os.fdopen(fd, 'rb', closefd=True) as from_parent:
242
+ process.current_process()._inheriting = True
243
+ try:
244
+ preparation_data = reduction.pickle.load(from_parent)
245
+ prepare(preparation_data)
246
+ self: Process = reduction.pickle.load(from_parent)
247
+ sp = SpawnProcess(viztracer_kwargs, self.run, self._target, self._args, self._kwargs, cmdline_args)
248
+ self.run = sp.run
249
+ finally:
250
+ del process.current_process()._inheriting
251
+ return self._bootstrap(parent_sentinel)
252
+
253
+ multiprocessing.spawn._main = _main # type: ignore
254
+
255
+
256
+ def filter_args(args: list[str]) -> list[str]:
257
+ new_args = []
258
+ i = 0
259
+ while i < len(args):
260
+ arg = args[i]
261
+ if arg == "-u" or arg == "--unique_output_file":
262
+ i += 1
263
+ continue
264
+ elif arg == "-o" or arg == "--output_file":
265
+ i += 2
266
+ continue
267
+ new_args.append(arg)
268
+ i += 1
269
+ return new_args
270
+
271
+
272
+ def install_all_hooks(
273
+ tracer: VizTracer,
274
+ args: list[str],
275
+ patch_multiprocess: bool = True) -> None:
276
+
277
+ args = filter_args(args)
278
+
279
+ # multiprocess hook
280
+ if patch_multiprocess:
281
+ patch_multiprocessing(tracer, args)
282
+ patch_subprocess(args + ["--subprocess_child", "--dump_raw", "-o", tracer.output_file])
283
+
284
+ # If we want to hook fork correctly with file waiter, we need to
285
+ # use os.register_at_fork to write the file, and make sure
286
+ # os.exec won't clear viztracer so that the file lives forever.
287
+ # This is basically equivalent to py3.8 + Linux
288
+ if hasattr(sys, "addaudithook"):
289
+ if hasattr(os, "register_at_fork") and patch_multiprocess:
290
+ def audit_hook(event, _): # pragma: no cover
291
+ if event == "os.exec":
292
+ tracer.exit_routine()
293
+ sys.addaudithook(audit_hook) # type: ignore
294
+
295
+ def callback():
296
+ if "--patch_only" in args:
297
+ # We use --patch_only for forkserver process so we need to
298
+ # turn on tracer in the forked child process and register
299
+ # for exit routine
300
+ tracer.register_exit()
301
+ tracer.start()
302
+ else:
303
+ # otherwise we need to add a new exit_routine callback because the one
304
+ # from parent won't be executed as it has a different pid.
305
+ # Also make sure to label the file because it's a new process
306
+ import multiprocessing.util
307
+ multiprocessing.util.Finalize(tracer, tracer.exit_routine, exitpriority=-1)
308
+ tracer.label_file_to_write()
309
+ os.register_at_fork(after_in_child=callback) # type: ignore
310
+
311
+ if tracer.log_audit is not None:
312
+ audit_regex_list = [re.compile(regex) for regex in tracer.log_audit]
313
+
314
+ def audit_hook(event, _): # pragma: no cover
315
+ if len(audit_regex_list) == 0 or any((regex.fullmatch(event) for regex in audit_regex_list)):
316
+ tracer.log_instant(event, args={"args": [str(arg) for arg in args]})
317
+ sys.addaudithook(audit_hook) # type: ignore
@@ -0,0 +1,311 @@
1
+ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2
+ # For details: https://github.com/gaogaotiantian/viztracer/blob/master/NOTICE.txt
3
+
4
+ try:
5
+ import orjson as json # type: ignore
6
+ except ImportError:
7
+ import json # type: ignore
8
+
9
+ import gzip
10
+ import importlib
11
+ import os
12
+ import re
13
+ import tokenize
14
+ from string import Template
15
+ from typing import Any, Sequence, TextIO
16
+
17
+ from . import __version__
18
+ from .util import color_print, same_line_print
19
+
20
+
21
+ def get_json(data: dict[str, Any] | str | tuple[str, dict]) -> dict[str, Any]:
22
+ # This function will return a json object if data is already json object
23
+ # or a opened file or a file path
24
+ if isinstance(data, dict):
25
+ # This is an object already
26
+ return data
27
+ elif isinstance(data, str):
28
+ with open(data, encoding="utf-8") as f:
29
+ json_str = f.read()
30
+ elif isinstance(data, tuple):
31
+ path, args = data
32
+ if args['type'] == 'torch':
33
+ with open(path, encoding="utf-8") as f:
34
+ json_str = f.read()
35
+ ret = json.loads(json_str)
36
+ base_offset = args['base_offset']
37
+ # torch 2.4.0+ uses baseTimeNanoseconds to store the offset
38
+ # before that they simply use the absolute timestamp which is
39
+ # equivalent to baseTimeNanoseconds = 0
40
+ torch_offset = ret.get('baseTimeNanoseconds', 0)
41
+ # convert to us
42
+ offset_diff = (torch_offset - base_offset) / 1000
43
+
44
+ for event in ret['traceEvents']:
45
+ if 'ts' in event:
46
+ event['ts'] += offset_diff
47
+ if event['ph'] == 'M':
48
+ # Pop metadata timestamp so it won't overwrite
49
+ # process and thread names
50
+ event.pop('ts', None)
51
+
52
+ ret.pop("baseTimeNanoseconds", None)
53
+ ret.pop("displayTimeUnit", None)
54
+ ret.pop("traceName")
55
+ ret.pop("deviceProperties")
56
+ ret.pop("schemaVersion")
57
+
58
+ ret['viztracer_metadata'] = {}
59
+
60
+ return ret
61
+
62
+ return json.loads(json_str)
63
+
64
+
65
+ class ReportBuilder:
66
+ def __init__(
67
+ self,
68
+ data: Sequence[str | dict | tuple[str, dict]] | dict[str, Any],
69
+ verbose: int = 1,
70
+ align: bool = False,
71
+ minimize_memory: bool = False,
72
+ base_time: int | None = None) -> None:
73
+ self.data = data
74
+ self.verbose = verbose
75
+ self.combined_json: dict = {}
76
+ self.entry_number_threshold = 4000000
77
+ self.align = align
78
+ self.minimize_memory = minimize_memory
79
+ self.jsons: list[dict] = []
80
+ self.invalid_json_paths: list[str] = []
81
+ self.json_loaded = False
82
+ self.base_time = base_time
83
+ self.final_messages: list[tuple[str, dict]] = []
84
+ if not isinstance(data, (dict, list, tuple)):
85
+ raise TypeError("Invalid data type for ReportBuilder")
86
+ if isinstance(data, (list, tuple)):
87
+ for path in data:
88
+ if isinstance(path, dict):
89
+ continue
90
+ if isinstance(path, tuple):
91
+ path = path[0]
92
+ if not isinstance(path, str):
93
+ raise TypeError("Path should be a string")
94
+ if not os.path.exists(path):
95
+ raise ValueError(f"{path} does not exist")
96
+ if not path.endswith(".json"):
97
+ raise ValueError(f"{path} is not a json file")
98
+
99
+ def load_jsons(self) -> None:
100
+ if not self.json_loaded:
101
+ self.json_loaded = True
102
+ if isinstance(self.data, dict):
103
+ self.jsons = [get_json(self.data)]
104
+ elif isinstance(self.data, (list, tuple)):
105
+ self.jsons = []
106
+ self.invalid_json_paths = []
107
+ for idx, j in enumerate(self.data):
108
+ if self.verbose > 0:
109
+ same_line_print(f"Loading trace data from processes {idx}/{len(self.data)}")
110
+ try:
111
+ self.jsons.append(get_json(j))
112
+ except json.JSONDecodeError:
113
+ assert isinstance(j, str)
114
+ self.invalid_json_paths.append(j)
115
+ if len(self.invalid_json_paths) > 0:
116
+ self.final_messages.append(("invalid_json", {"paths": self.invalid_json_paths}))
117
+
118
+ def combine_json(self) -> None:
119
+ if self.verbose > 0:
120
+ same_line_print("Combining trace data")
121
+ if self.combined_json:
122
+ return
123
+ if not self.jsons:
124
+ if self.invalid_json_paths:
125
+ raise ValueError("No valid json files found")
126
+ else:
127
+ raise ValueError("Can't get report of nothing")
128
+ if self.align:
129
+ for one in self.jsons:
130
+ self.align_events(one["traceEvents"], one['viztracer_metadata'].get('sync_marker', None))
131
+ self.combined_json = self.jsons[0]
132
+ if "viztracer_metadata" not in self.combined_json:
133
+ self.combined_json["viztracer_metadata"] = {}
134
+ for one in self.jsons[1:]:
135
+ if "traceEvents" in one:
136
+ self.combined_json["traceEvents"].extend(one["traceEvents"])
137
+ if one.get("viztracer_metadata", {}).get("overflow", False):
138
+ self.combined_json["viztracer_metadata"]["overflow"] = True
139
+ if one.get("viztracer_metadata", {}).get("baseTimeNanoseconds") is not None:
140
+ self.combined_json["viztracer_metadata"]["baseTimeNanoseconds"] = \
141
+ one["viztracer_metadata"]["baseTimeNanoseconds"]
142
+ if "file_info" in one:
143
+ if "file_info" not in self.combined_json:
144
+ self.combined_json["file_info"] = {"files": {}, "functions": {}}
145
+ self.combined_json["file_info"]["files"].update(one["file_info"]["files"])
146
+ self.combined_json["file_info"]["functions"].update(one["file_info"]["functions"])
147
+
148
+ def align_events(self, original_events: list[dict[str, Any]], sync_marker: float | None = None) -> list[dict[str, Any]]:
149
+ """
150
+ Apply an offset to all the trace events, making the start timestamp 0
151
+ This is useful when comparing multiple runs of the same script
152
+
153
+ If sync_marker is not None then sync_marker be used as an offset
154
+
155
+ This function will change the timestamp in place, and return the original list
156
+ """
157
+ if sync_marker is None:
158
+ offset_ts = min((event["ts"] for event in original_events if "ts" in event))
159
+ else:
160
+ offset_ts = sync_marker
161
+
162
+ for event in original_events:
163
+ if "ts" in event:
164
+ event["ts"] -= offset_ts
165
+ return original_events
166
+
167
+ def prepare_json(self, file_info: bool = True, display_time_unit: str | None = None) -> None:
168
+ # This will prepare self.combined_json to be ready to output
169
+ self.load_jsons()
170
+ self.combine_json()
171
+ if self.verbose > 0:
172
+ entries = len(self.combined_json["traceEvents"])
173
+ same_line_print(f"Dumping trace data, total entries: {entries}")
174
+ self.final_messages.append(("total_entries", {"total_entries": entries}))
175
+ if self.combined_json["viztracer_metadata"].get("overflow", False):
176
+ self.final_messages.append(("overflow", {}))
177
+
178
+ if display_time_unit is not None:
179
+ self.combined_json["displayTimeUnit"] = display_time_unit
180
+
181
+ self.combined_json["viztracer_metadata"]["version"] = __version__
182
+
183
+ if self.base_time is not None:
184
+ self.combined_json["viztracer_metadata"]["baseTimeNanoseconds"] = self.base_time
185
+
186
+ if file_info:
187
+ if "file_info" not in self.combined_json:
188
+ self.combined_json["file_info"] = {"files": {}, "functions": {}}
189
+ pattern = re.compile(r".*\((.*):([0-9]*)\)")
190
+ file_dict = self.combined_json["file_info"]["files"]
191
+ func_dict = self.combined_json["file_info"]["functions"]
192
+ for event in self.combined_json["traceEvents"]:
193
+ if event["ph"] == 'X':
194
+ if event["name"] not in func_dict:
195
+ func_dict[event["name"]] = None
196
+ m = pattern.match(event["name"])
197
+ if m is not None:
198
+ file_name = m.group(1)
199
+ lineno = int(m.group(2))
200
+ if file_name not in file_dict:
201
+ content = self.get_source_from_filename(file_name)
202
+ if content is None:
203
+ continue
204
+ file_dict[file_name] = [content, content.count("\n")]
205
+ func_dict[event["name"]] = [file_name, lineno]
206
+ unknown_func_dict = set(func for func in func_dict if func_dict[func] is None)
207
+ for func in unknown_func_dict:
208
+ del func_dict[func]
209
+
210
+ @classmethod
211
+ def get_source_from_filename(cls, filename: str) -> str | None:
212
+ if filename.startswith("<frozen "):
213
+ m = re.match(r"<frozen (.*)>", filename)
214
+ if not m:
215
+ return None
216
+ module_name = m.group(1)
217
+ try:
218
+ module = importlib.import_module(module_name)
219
+ except ImportError:
220
+ return None
221
+ if hasattr(module, "__file__") and module.__file__ is not None:
222
+ filename = module.__file__
223
+ else:
224
+ return None
225
+ try:
226
+ with tokenize.open(filename) as f:
227
+ return f.read()
228
+ except Exception:
229
+ return None
230
+
231
+ def generate_report(
232
+ self,
233
+ output_file: TextIO,
234
+ output_format: str,
235
+ file_info: bool = True) -> None:
236
+ sub = {}
237
+ if output_format == "html":
238
+ self.prepare_json(file_info=file_info, display_time_unit="ns")
239
+ with open(os.path.join(os.path.dirname(__file__), "html/trace_viewer_embedder.html"), encoding="utf-8") as f:
240
+ tmpl = f.read()
241
+ with open(os.path.join(os.path.dirname(__file__), "html/trace_viewer_full.html"), encoding="utf-8") as f:
242
+ sub["trace_viewer_full"] = f.read()
243
+ if json.__name__ == "orjson":
244
+ sub["json_data"] = json.dumps(self.combined_json) \
245
+ .decode("utf-8") \
246
+ .replace("</script>", "<\\/script>")
247
+ else:
248
+ sub["json_data"] = json.dumps(self.combined_json) \
249
+ .replace("</script>", "<\\/script>") # type: ignore
250
+ output_file.write(Template(tmpl).substitute(sub))
251
+ elif output_format == "json":
252
+ self.prepare_json(file_info=file_info)
253
+ if json.__name__ == "orjson":
254
+ output_file.write(json.dumps(self.combined_json).decode("utf-8"))
255
+ else:
256
+ if self.minimize_memory:
257
+ json.dump(self.combined_json, output_file) # type: ignore
258
+ else:
259
+ output_file.write(json.dumps(self.combined_json)) # type: ignore
260
+
261
+ def save(self, output_file: str | TextIO = "result.html", file_info: bool = True) -> None:
262
+ if isinstance(output_file, str):
263
+ file_type = output_file.split(".")[-1]
264
+
265
+ if file_type == "html":
266
+ with open(output_file, "w", encoding="utf-8") as f:
267
+ self.generate_report(f, output_format="html", file_info=file_info)
268
+ elif file_type == "json":
269
+ with open(output_file, "w", encoding="utf-8") as f:
270
+ self.generate_report(f, output_format="json", file_info=file_info)
271
+ elif file_type == "gz":
272
+ with gzip.open(output_file, "wt") as f:
273
+ self.generate_report(f, output_format="json", file_info=file_info)
274
+ else:
275
+ raise Exception("Only html, json and gz are supported")
276
+ else:
277
+ self.generate_report(output_file, output_format="json", file_info=file_info)
278
+
279
+ if isinstance(output_file, str):
280
+ self.final_messages.append(("view_command", {"output_file": os.path.abspath(output_file)}))
281
+
282
+ self.print_messages()
283
+
284
+ def print_messages(self):
285
+ if self.verbose > 0:
286
+ same_line_print("")
287
+ for msg_type, msg_args in self.final_messages:
288
+ if msg_type == "overflow":
289
+ print("")
290
+ color_print("WARNING", ("Circular buffer is full, you lost some early data, "
291
+ "but you still have the most recent data."))
292
+ color_print("WARNING", (" If you need more buffer, use \"viztracer --tracer_entries <entry_number>\""))
293
+ color_print("WARNING", " Or, you can try the filter options to filter out some data you don't need")
294
+ color_print("WARNING", " use --quiet to shut me up")
295
+ print("")
296
+ elif msg_type == "total_entries":
297
+ print(f"Total Entries: {msg_args['total_entries']}")
298
+ elif msg_type == "view_command":
299
+ report_abspath = os.path.abspath(msg_args["output_file"])
300
+ print("Use the following command to open the report:")
301
+ if " " in report_abspath:
302
+ color_print("OKGREEN", f"vizviewer \"{report_abspath}\"")
303
+ else:
304
+ color_print("OKGREEN", f"vizviewer {report_abspath}")
305
+ elif msg_type == "invalid_json":
306
+ print("")
307
+ color_print("WARNING", "Found and ignored invalid json file, you may lost some process data.")
308
+ color_print("WARNING", "Invalid json file:")
309
+ for msg in msg_args["paths"]:
310
+ color_print("WARNING", f" {msg}")
311
+ print("")
Binary file