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.

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 +699 -0
  21. viztracer/modules/eventnode.c +172 -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 +2205 -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 +307 -0
  36. viztracer/report_builder.py +311 -0
  37. viztracer/snaptrace.cp314-win32.pyd +0 -0
  38. viztracer/snaptrace.pyi +77 -0
  39. viztracer/util.py +196 -0
  40. viztracer/vcompressor.cp314-win32.pyd +0 -0
  41. viztracer/vcompressor.pyi +10 -0
  42. viztracer/viewer.py +528 -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.0.dist-info/METADATA +316 -0
  104. viztracer-1.1.0.dist-info/RECORD +109 -0
  105. viztracer-1.1.0.dist-info/WHEEL +5 -0
  106. viztracer-1.1.0.dist-info/entry_points.txt +3 -0
  107. viztracer-1.1.0.dist-info/licenses/LICENSE +222 -0
  108. viztracer-1.1.0.dist-info/licenses/NOTICE.txt +27 -0
  109. 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