viztracer 1.1.0__cp314-cp314-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.
Potentially problematic release.
This version of viztracer might be problematic. Click here for more details.
- viztracer/__init__.py +19 -0
- viztracer/__main__.py +8 -0
- viztracer/attach.py +67 -0
- viztracer/attach_process/LICENSE +203 -0
- viztracer/attach_process/__init__.py +0 -0
- viztracer/attach_process/add_code_to_python_process.py +582 -0
- viztracer/attach_process/attach_x86.dll +0 -0
- viztracer/attach_process/inject_dll_amd64.exe +0 -0
- viztracer/attach_process/linux_and_mac/lldb_prepare.py +54 -0
- viztracer/attach_process/run_code_on_dllmain_amd64.dll +0 -0
- viztracer/attach_process/run_code_on_dllmain_x86.dll +0 -0
- viztracer/cellmagic.py +70 -0
- viztracer/code_monkey.py +353 -0
- viztracer/decorator.py +164 -0
- viztracer/event_base.py +81 -0
- viztracer/functree.py +135 -0
- viztracer/html/flamegraph.html +34 -0
- viztracer/html/trace_viewer_embedder.html +203 -0
- viztracer/html/trace_viewer_full.html +10207 -0
- viztracer/main.py +699 -0
- viztracer/modules/eventnode.c +172 -0
- viztracer/modules/eventnode.h +73 -0
- viztracer/modules/pythoncapi_compat.h +1726 -0
- viztracer/modules/quicktime.c +177 -0
- viztracer/modules/quicktime.h +104 -0
- viztracer/modules/snaptrace.c +2205 -0
- viztracer/modules/snaptrace.h +134 -0
- viztracer/modules/snaptrace_member.c +483 -0
- viztracer/modules/util.c +45 -0
- viztracer/modules/util.h +22 -0
- viztracer/modules/vcompressor/vc_dump.c +1131 -0
- viztracer/modules/vcompressor/vc_dump.h +49 -0
- viztracer/modules/vcompressor/vcompressor.c +396 -0
- viztracer/modules/vcompressor/vcompressor.h +15 -0
- viztracer/patch.py +307 -0
- viztracer/report_builder.py +311 -0
- viztracer/snaptrace.cp314-win32.pyd +0 -0
- viztracer/snaptrace.pyi +77 -0
- viztracer/util.py +196 -0
- viztracer/vcompressor.cp314-win32.pyd +0 -0
- viztracer/vcompressor.pyi +10 -0
- viztracer/viewer.py +528 -0
- viztracer/vizcounter.py +20 -0
- viztracer/vizevent.py +31 -0
- viztracer/vizlogging.py +20 -0
- viztracer/vizobject.py +28 -0
- viztracer/vizplugin.py +143 -0
- viztracer/viztracer.py +472 -0
- viztracer/web_dist/LICENSE +189 -0
- viztracer/web_dist/index.html +127 -0
- viztracer/web_dist/service_worker.js +279 -0
- viztracer/web_dist/trace_processor +300 -0
- viztracer/web_dist/v52.0-6b9586def/assets/MaterialSymbolsOutlined.woff2 +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/Roboto-100.woff2 +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/Roboto-300.woff2 +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/Roboto-400.woff2 +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/Roboto-500.woff2 +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/RobotoCondensed-Light.woff2 +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/RobotoCondensed-Regular.woff2 +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/RobotoMono-Regular.woff2 +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/brand.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/catapult_trace_viewer.html +3946 -0
- viztracer/web_dist/v52.0-6b9586def/assets/catapult_trace_viewer.js +7539 -0
- viztracer/web_dist/v52.0-6b9586def/assets/favicon.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/logo-128.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/logo-3d.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_atrace.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_battery_counters.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_board_voltage.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_cpu_coarse.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_cpu_fine.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_cpu_freq.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_cpu_voltage.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_frame_timeline.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_ftrace.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_gpu_mem_total.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_java_heap_dump.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_lmk.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_logcat.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_long_trace.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_mem_hifreq.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_meminfo.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_native_heap_profiler.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_one_shot.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_profiling.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_ps_stats.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_ring_buf.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_syscalls.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/rec_vmstat.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/scheduling_latency.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/assets/vscode-icon.png +0 -0
- viztracer/web_dist/v52.0-6b9586def/engine_bundle.js +3 -0
- viztracer/web_dist/v52.0-6b9586def/frontend_bundle.js +5495 -0
- viztracer/web_dist/v52.0-6b9586def/index.html +127 -0
- viztracer/web_dist/v52.0-6b9586def/manifest.json +52 -0
- viztracer/web_dist/v52.0-6b9586def/perfetto.css +5737 -0
- viztracer/web_dist/v52.0-6b9586def/stdlib_docs.json +1 -0
- viztracer/web_dist/v52.0-6b9586def/trace_config_utils.wasm +0 -0
- viztracer/web_dist/v52.0-6b9586def/trace_processor.wasm +0 -0
- viztracer/web_dist/v52.0-6b9586def/trace_processor_memory64.wasm +0 -0
- viztracer/web_dist/v52.0-6b9586def/traceconv.wasm +0 -0
- viztracer/web_dist/v52.0-6b9586def/traceconv_bundle.js +2 -0
- viztracer-1.1.0.dist-info/METADATA +316 -0
- viztracer-1.1.0.dist-info/RECORD +109 -0
- viztracer-1.1.0.dist-info/WHEEL +5 -0
- viztracer-1.1.0.dist-info/entry_points.txt +3 -0
- viztracer-1.1.0.dist-info/licenses/LICENSE +222 -0
- viztracer-1.1.0.dist-info/licenses/NOTICE.txt +27 -0
- viztracer-1.1.0.dist-info/top_level.txt +1 -0
viztracer/patch.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
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
|
+
mode.append(cm_arg)
|
|
57
|
+
else:
|
|
58
|
+
mode = None
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
# -pyopts
|
|
62
|
+
py_args.append(arg)
|
|
63
|
+
|
|
64
|
+
if script:
|
|
65
|
+
return [sys.executable, *py_args, "-m", "viztracer", "--quiet", *viz_args, "--", script, *args_iter]
|
|
66
|
+
elif mode:
|
|
67
|
+
return [sys.executable, *py_args, "-m", "viztracer", "--quiet", *viz_args, *mode, "--", *args_iter]
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def is_python_entry(path: str) -> bool:
|
|
71
|
+
real_path = shutil.which(path)
|
|
72
|
+
if real_path is None:
|
|
73
|
+
return False
|
|
74
|
+
try:
|
|
75
|
+
with open(real_path, "rb") as f:
|
|
76
|
+
if f.read(2) == b"#!":
|
|
77
|
+
executable = f.readline().decode("utf-8").strip()
|
|
78
|
+
if "python" in executable.split('/')[-1]:
|
|
79
|
+
return True
|
|
80
|
+
except Exception: # pragma: no cover
|
|
81
|
+
pass
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
@functools.wraps(subprocess.Popen.__init__)
|
|
85
|
+
def subprocess_init(self: subprocess.Popen[Any], args: str | Sequence[Any] | Any, **kwargs: Any) -> None:
|
|
86
|
+
new_args = args
|
|
87
|
+
if isinstance(new_args, str):
|
|
88
|
+
new_args = shlex.split(new_args, posix=sys.platform != "win32")
|
|
89
|
+
if isinstance(new_args, Sequence):
|
|
90
|
+
if "python" in os.path.basename(new_args[0]):
|
|
91
|
+
new_args = build_command(new_args)
|
|
92
|
+
elif is_python_entry(new_args[0]):
|
|
93
|
+
new_args = ["python", "-m", "viztracer", "--quiet", *viz_args, "--", *new_args]
|
|
94
|
+
else:
|
|
95
|
+
new_args = None
|
|
96
|
+
if new_args is not None and kwargs.get("shell") and isinstance(args, str):
|
|
97
|
+
# For shell=True, we should convert the commands back to string
|
|
98
|
+
# if it was passed as string
|
|
99
|
+
# This is mostly for Unix shell
|
|
100
|
+
new_args = " ".join(new_args)
|
|
101
|
+
|
|
102
|
+
if new_args is None:
|
|
103
|
+
new_args = args
|
|
104
|
+
assert hasattr(subprocess_init, "__wrapped__") # for mypy
|
|
105
|
+
subprocess_init.__wrapped__(self, new_args, **kwargs)
|
|
106
|
+
|
|
107
|
+
# We need to filter the arguments as there are something we may not want
|
|
108
|
+
if "-m" in viz_args:
|
|
109
|
+
# If it's a module run, we don't want to use that module for subprocess
|
|
110
|
+
idx = viz_args.index("-m")
|
|
111
|
+
viz_args.pop(idx)
|
|
112
|
+
viz_args.pop(idx)
|
|
113
|
+
|
|
114
|
+
setattr(subprocess.Popen, "__originit__", subprocess.Popen.__init__)
|
|
115
|
+
setattr(subprocess.Popen, "__init__", subprocess_init)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def patch_multiprocessing(tracer: VizTracer, viz_args: list[str]) -> None:
|
|
119
|
+
|
|
120
|
+
# For fork process
|
|
121
|
+
def func_after_fork(tracer: VizTracer):
|
|
122
|
+
tracer.register_exit()
|
|
123
|
+
|
|
124
|
+
tracer.clear()
|
|
125
|
+
tracer.reset_stack()
|
|
126
|
+
|
|
127
|
+
if tracer._afterfork_cb:
|
|
128
|
+
tracer._afterfork_cb(tracer, *tracer._afterfork_args, **tracer._afterfork_kwargs)
|
|
129
|
+
|
|
130
|
+
import multiprocessing.spawn
|
|
131
|
+
import multiprocessing.util
|
|
132
|
+
from multiprocessing.util import register_after_fork # type: ignore
|
|
133
|
+
|
|
134
|
+
register_after_fork(tracer, func_after_fork)
|
|
135
|
+
|
|
136
|
+
if sys.platform == "win32":
|
|
137
|
+
# For spawn process on Windows
|
|
138
|
+
@functools.wraps(multiprocessing.spawn.get_command_line)
|
|
139
|
+
def get_command_line(**kwds) -> list[str]:
|
|
140
|
+
"""
|
|
141
|
+
Returns prefix of command line used for spawning a child process
|
|
142
|
+
"""
|
|
143
|
+
if getattr(sys, 'frozen', False): # pragma: no cover
|
|
144
|
+
return ([sys.executable, '--multiprocessing-fork']
|
|
145
|
+
+ ['%s=%r' % item for item in kwds.items()])
|
|
146
|
+
else:
|
|
147
|
+
prog = textwrap.dedent(f"""
|
|
148
|
+
from multiprocessing.spawn import spawn_main;
|
|
149
|
+
from viztracer.patch import patch_spawned_process;
|
|
150
|
+
patch_spawned_process({tracer.init_kwargs}, {viz_args});
|
|
151
|
+
spawn_main(%s)
|
|
152
|
+
""")
|
|
153
|
+
prog %= ', '.join('%s=%r' % item for item in kwds.items())
|
|
154
|
+
opts = multiprocessing.util._args_from_interpreter_flags() # type: ignore
|
|
155
|
+
return [multiprocessing.spawn._python_exe] + opts + ['-c', prog, '--multiprocessing-fork'] # type: ignore
|
|
156
|
+
|
|
157
|
+
multiprocessing.spawn.get_command_line = get_command_line
|
|
158
|
+
else:
|
|
159
|
+
# POSIX
|
|
160
|
+
# For forkserver process and spawned process
|
|
161
|
+
# We patch spawnv_passfds to trace forkserver parent process so the forked
|
|
162
|
+
# children can be traced
|
|
163
|
+
_spawnv_passfds = multiprocessing.util.spawnv_passfds
|
|
164
|
+
|
|
165
|
+
@functools.wraps(_spawnv_passfds)
|
|
166
|
+
def spawnv_passfds(path, args, passfds):
|
|
167
|
+
if "-c" in args:
|
|
168
|
+
idx = args.index("-c")
|
|
169
|
+
cmd = args[idx + 1]
|
|
170
|
+
if "forkserver" in cmd:
|
|
171
|
+
# forkserver will not end before main process, avoid deadlock by --patch_only
|
|
172
|
+
args = (
|
|
173
|
+
args[:idx]
|
|
174
|
+
+ ["-m", "viztracer", "--patch_only", *viz_args]
|
|
175
|
+
+ ["--subprocess_child", "--dump_raw", "-o", tracer.output_file]
|
|
176
|
+
+ args[idx:]
|
|
177
|
+
)
|
|
178
|
+
elif "resource_tracker" not in cmd:
|
|
179
|
+
# We don't trace resource_tracker as it does not quit before the main process
|
|
180
|
+
# This is a normal spawned process. Only one of spawnv_passfds and spawn._main
|
|
181
|
+
# can be patched. forkserver process will use spawn._main after forking a child,
|
|
182
|
+
# so on POSIX we patch spawnv_passfds which has a similar effect on spawned processes.
|
|
183
|
+
args = (
|
|
184
|
+
args[:idx]
|
|
185
|
+
+ ["-m", "viztracer", *viz_args]
|
|
186
|
+
+ ["--subprocess_child", "--dump_raw", "-o", tracer.output_file]
|
|
187
|
+
+ args[idx:]
|
|
188
|
+
)
|
|
189
|
+
ret = _spawnv_passfds(path, args, passfds)
|
|
190
|
+
return ret
|
|
191
|
+
|
|
192
|
+
multiprocessing.util.spawnv_passfds = spawnv_passfds # type: ignore
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class SpawnProcess:
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
viztracer_kwargs: dict[str, Any],
|
|
199
|
+
run: Callable,
|
|
200
|
+
target: Callable,
|
|
201
|
+
args: list[Any],
|
|
202
|
+
kwargs: dict[str, Any],
|
|
203
|
+
cmdline_args: list[str]):
|
|
204
|
+
self._viztracer_kwargs = viztracer_kwargs
|
|
205
|
+
self._run = run
|
|
206
|
+
self._target = target
|
|
207
|
+
self._args = args
|
|
208
|
+
self._kwargs = kwargs
|
|
209
|
+
self._cmdline_args = cmdline_args
|
|
210
|
+
self._exiting = False
|
|
211
|
+
|
|
212
|
+
def run(self) -> None:
|
|
213
|
+
import viztracer
|
|
214
|
+
|
|
215
|
+
tracer = viztracer.VizTracer(**self._viztracer_kwargs)
|
|
216
|
+
install_all_hooks(tracer, self._cmdline_args)
|
|
217
|
+
tracer.register_exit()
|
|
218
|
+
if not self._viztracer_kwargs.get("log_sparse"):
|
|
219
|
+
tracer.start()
|
|
220
|
+
self._run()
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def patch_spawned_process(viztracer_kwargs: dict[str, Any], cmdline_args: list[str]):
|
|
224
|
+
import multiprocessing.spawn
|
|
225
|
+
from multiprocessing import process, reduction # type: ignore
|
|
226
|
+
from multiprocessing.spawn import prepare
|
|
227
|
+
|
|
228
|
+
@no_type_check
|
|
229
|
+
@functools.wraps(multiprocessing.spawn._main)
|
|
230
|
+
def _main(fd, parent_sentinel) -> Any:
|
|
231
|
+
with os.fdopen(fd, 'rb', closefd=True) as from_parent:
|
|
232
|
+
process.current_process()._inheriting = True
|
|
233
|
+
try:
|
|
234
|
+
preparation_data = reduction.pickle.load(from_parent)
|
|
235
|
+
prepare(preparation_data)
|
|
236
|
+
self: Process = reduction.pickle.load(from_parent)
|
|
237
|
+
sp = SpawnProcess(viztracer_kwargs, self.run, self._target, self._args, self._kwargs, cmdline_args)
|
|
238
|
+
self.run = sp.run
|
|
239
|
+
finally:
|
|
240
|
+
del process.current_process()._inheriting
|
|
241
|
+
return self._bootstrap(parent_sentinel)
|
|
242
|
+
|
|
243
|
+
multiprocessing.spawn._main = _main # type: ignore
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def filter_args(args: list[str]) -> list[str]:
|
|
247
|
+
new_args = []
|
|
248
|
+
i = 0
|
|
249
|
+
while i < len(args):
|
|
250
|
+
arg = args[i]
|
|
251
|
+
if arg == "-u" or arg == "--unique_output_file":
|
|
252
|
+
i += 1
|
|
253
|
+
continue
|
|
254
|
+
elif arg == "-o" or arg == "--output_file":
|
|
255
|
+
i += 2
|
|
256
|
+
continue
|
|
257
|
+
new_args.append(arg)
|
|
258
|
+
i += 1
|
|
259
|
+
return new_args
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def install_all_hooks(
|
|
263
|
+
tracer: VizTracer,
|
|
264
|
+
args: list[str],
|
|
265
|
+
patch_multiprocess: bool = True) -> None:
|
|
266
|
+
|
|
267
|
+
args = filter_args(args)
|
|
268
|
+
|
|
269
|
+
# multiprocess hook
|
|
270
|
+
if patch_multiprocess:
|
|
271
|
+
patch_multiprocessing(tracer, args)
|
|
272
|
+
patch_subprocess(args + ["--subprocess_child", "--dump_raw", "-o", tracer.output_file])
|
|
273
|
+
|
|
274
|
+
# If we want to hook fork correctly with file waiter, we need to
|
|
275
|
+
# use os.register_at_fork to write the file, and make sure
|
|
276
|
+
# os.exec won't clear viztracer so that the file lives forever.
|
|
277
|
+
# This is basically equivalent to py3.8 + Linux
|
|
278
|
+
if hasattr(sys, "addaudithook"):
|
|
279
|
+
if hasattr(os, "register_at_fork") and patch_multiprocess:
|
|
280
|
+
def audit_hook(event, _): # pragma: no cover
|
|
281
|
+
if event == "os.exec":
|
|
282
|
+
tracer.exit_routine()
|
|
283
|
+
sys.addaudithook(audit_hook) # type: ignore
|
|
284
|
+
|
|
285
|
+
def callback():
|
|
286
|
+
if "--patch_only" in args:
|
|
287
|
+
# We use --patch_only for forkserver process so we need to
|
|
288
|
+
# turn on tracer in the forked child process and register
|
|
289
|
+
# for exit routine
|
|
290
|
+
tracer.register_exit()
|
|
291
|
+
tracer.start()
|
|
292
|
+
else:
|
|
293
|
+
# otherwise we need to add a new exit_routine callback because the one
|
|
294
|
+
# from parent won't be executed as it has a different pid.
|
|
295
|
+
# Also make sure to label the file because it's a new process
|
|
296
|
+
import multiprocessing.util
|
|
297
|
+
multiprocessing.util.Finalize(tracer, tracer.exit_routine, exitpriority=-1)
|
|
298
|
+
tracer.label_file_to_write()
|
|
299
|
+
os.register_at_fork(after_in_child=callback) # type: ignore
|
|
300
|
+
|
|
301
|
+
if tracer.log_audit is not None:
|
|
302
|
+
audit_regex_list = [re.compile(regex) for regex in tracer.log_audit]
|
|
303
|
+
|
|
304
|
+
def audit_hook(event, _): # pragma: no cover
|
|
305
|
+
if len(audit_regex_list) == 0 or any((regex.fullmatch(event) for regex in audit_regex_list)):
|
|
306
|
+
tracer.log_instant(event, args={"args": [str(arg) for arg in args]})
|
|
307
|
+
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
|