viztracer 1.1.0__cp314-cp314t-win_amd64.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.cp314t-win_amd64.pyd +0 -0
- viztracer/snaptrace.pyi +77 -0
- viztracer/util.py +196 -0
- viztracer/vcompressor.cp314t-win_amd64.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/viewer.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
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
|
+
import argparse
|
|
5
|
+
import atexit
|
|
6
|
+
import contextlib
|
|
7
|
+
import functools
|
|
8
|
+
import html
|
|
9
|
+
import http.server
|
|
10
|
+
import gzip
|
|
11
|
+
import io
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import socket
|
|
15
|
+
import socketserver
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import traceback
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
import urllib.parse
|
|
22
|
+
from http import HTTPStatus
|
|
23
|
+
from typing import Any, Callable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
dir_lock = threading.Lock()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@contextlib.contextmanager
|
|
30
|
+
def chdir_temp(d: str):
|
|
31
|
+
with dir_lock:
|
|
32
|
+
curr_cwd = os.getcwd()
|
|
33
|
+
os.chdir(d)
|
|
34
|
+
try:
|
|
35
|
+
yield
|
|
36
|
+
finally:
|
|
37
|
+
os.chdir(curr_cwd)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HttpHandler(http.server.SimpleHTTPRequestHandler):
|
|
41
|
+
def end_headers(self):
|
|
42
|
+
self.send_header('Access-Control-Allow-Origin', '*')
|
|
43
|
+
self.send_header('Cache-Control', 'no-store')
|
|
44
|
+
return super().end_headers()
|
|
45
|
+
|
|
46
|
+
def log_message(self, format, *args):
|
|
47
|
+
# To quiet the http server
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ExternalProcessorHandler(HttpHandler):
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
server_thread: "ServerThread",
|
|
55
|
+
*args, **kwargs) -> None:
|
|
56
|
+
self.server_thread = server_thread
|
|
57
|
+
super().__init__(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
def do_GET(self):
|
|
60
|
+
self.server.last_request = self.path
|
|
61
|
+
self.server_thread.notify_active()
|
|
62
|
+
self.directory = os.path.join(os.path.dirname(__file__), "web_dist")
|
|
63
|
+
with chdir_temp(self.directory):
|
|
64
|
+
return super().do_GET()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PerfettoHandler(HttpHandler):
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
server_thread: "ServerThread",
|
|
71
|
+
*args, **kwargs) -> None:
|
|
72
|
+
self.server_thread = server_thread
|
|
73
|
+
super().__init__(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
def do_GET(self):
|
|
76
|
+
self.server.last_request = self.path
|
|
77
|
+
self.server_thread.notify_active()
|
|
78
|
+
if self.path.endswith("vizviewer_info"):
|
|
79
|
+
info = {}
|
|
80
|
+
self.send_response(200)
|
|
81
|
+
self.send_header("Content-type", "application/json")
|
|
82
|
+
self.end_headers()
|
|
83
|
+
self.wfile.write(json.dumps(info).encode("utf-8"))
|
|
84
|
+
self.wfile.flush()
|
|
85
|
+
elif self.path.endswith("file_info"):
|
|
86
|
+
self.send_response(200)
|
|
87
|
+
self.send_header("Content-type", "application/json")
|
|
88
|
+
self.end_headers()
|
|
89
|
+
self.wfile.write(json.dumps(self.server_thread.file_info).encode("utf-8"))
|
|
90
|
+
self.wfile.flush()
|
|
91
|
+
elif self.path.endswith("localtrace"):
|
|
92
|
+
# self.directory is used after 3.8
|
|
93
|
+
# os.getcwd() is used on 3.6
|
|
94
|
+
self.directory = os.path.dirname(self.server_thread.path)
|
|
95
|
+
with chdir_temp(self.directory):
|
|
96
|
+
filename = os.path.basename(self.server_thread.path)
|
|
97
|
+
self.path = f"/{filename}"
|
|
98
|
+
self.server.trace_served = True
|
|
99
|
+
return super().do_GET()
|
|
100
|
+
else:
|
|
101
|
+
self.directory = os.path.join(os.path.dirname(__file__), "web_dist")
|
|
102
|
+
with chdir_temp(self.directory):
|
|
103
|
+
return super().do_GET()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class HtmlHandler(HttpHandler):
|
|
107
|
+
def __init__(self, server_thread: "ServerThread", *args, **kwargs) -> None:
|
|
108
|
+
self.server_thread = server_thread
|
|
109
|
+
super().__init__(*args, **kwargs)
|
|
110
|
+
|
|
111
|
+
def do_GET(self):
|
|
112
|
+
self.directory = os.path.dirname(self.server_thread.path)
|
|
113
|
+
with chdir_temp(self.directory):
|
|
114
|
+
filename = os.path.basename(self.server_thread.path)
|
|
115
|
+
self.path = f"/{filename}"
|
|
116
|
+
self.server.trace_served = True
|
|
117
|
+
return super().do_GET()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class DirectoryHandler(HttpHandler):
|
|
121
|
+
def __init__(self, directory_viewer: "DirectoryViewer", *args, **kwargs) -> None:
|
|
122
|
+
self.directory_viewer = directory_viewer
|
|
123
|
+
kwargs["directory"] = directory_viewer.base_path
|
|
124
|
+
super().__init__(*args, **kwargs)
|
|
125
|
+
|
|
126
|
+
def do_GET(self):
|
|
127
|
+
if self.path.endswith("json"):
|
|
128
|
+
# self.path starts with '/', we need to remove it
|
|
129
|
+
self.send_response(302)
|
|
130
|
+
self.send_header("Location", self.directory_viewer.get_link(self.path[1:]))
|
|
131
|
+
self.end_headers()
|
|
132
|
+
else:
|
|
133
|
+
with chdir_temp(self.directory):
|
|
134
|
+
super().do_GET()
|
|
135
|
+
|
|
136
|
+
def send_head(self): # pragma: no cover
|
|
137
|
+
"""
|
|
138
|
+
Return list_directory even if there's an index.html in the dir
|
|
139
|
+
"""
|
|
140
|
+
path = self.translate_path(self.path)
|
|
141
|
+
if os.path.isdir(path):
|
|
142
|
+
parts = urllib.parse.urlsplit(self.path)
|
|
143
|
+
if not parts.path.endswith('/'):
|
|
144
|
+
# redirect browser - doing basically what apache does
|
|
145
|
+
self.send_response(HTTPStatus.MOVED_PERMANENTLY)
|
|
146
|
+
new_parts = (parts[0], parts[1], parts[2] + '/',
|
|
147
|
+
parts[3], parts[4])
|
|
148
|
+
new_url = urllib.parse.urlunsplit(new_parts)
|
|
149
|
+
self.send_header("Location", new_url)
|
|
150
|
+
self.send_header("Content-Length", "0")
|
|
151
|
+
self.end_headers()
|
|
152
|
+
return None
|
|
153
|
+
else:
|
|
154
|
+
return self.list_directory(path)
|
|
155
|
+
return super().send_head()
|
|
156
|
+
|
|
157
|
+
def list_directory(self, path): # pragma: no cover
|
|
158
|
+
"""
|
|
159
|
+
Almost the same as SimpleHTTPRequestHandler.list_directory, but
|
|
160
|
+
* Does not display file that does not end with json
|
|
161
|
+
* Created a new tab when click
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
list = os.listdir(path)
|
|
165
|
+
except OSError:
|
|
166
|
+
self.send_error(
|
|
167
|
+
HTTPStatus.NOT_FOUND,
|
|
168
|
+
"No permission to list directory")
|
|
169
|
+
return None
|
|
170
|
+
list.sort(key=lambda a: a.lower())
|
|
171
|
+
r = []
|
|
172
|
+
try:
|
|
173
|
+
displaypath = urllib.parse.unquote(self.path,
|
|
174
|
+
errors='surrogatepass')
|
|
175
|
+
except UnicodeDecodeError:
|
|
176
|
+
displaypath = urllib.parse.unquote(path)
|
|
177
|
+
displaypath = html.escape(displaypath, quote=False)
|
|
178
|
+
enc = sys.getfilesystemencoding()
|
|
179
|
+
title = f'Directory listing for {displaypath}'
|
|
180
|
+
r.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
|
|
181
|
+
'"http://www.w3.org/TR/html4/strict.dtd">')
|
|
182
|
+
r.append('<html>\n<head>')
|
|
183
|
+
r.append('<meta http-equiv="Content-Type" '
|
|
184
|
+
'content="text/html; charset=%s">' % enc)
|
|
185
|
+
r.append(f'<title>{title}</title>\n</head>')
|
|
186
|
+
r.append(f'<body>\n<h1>{title}</h1>')
|
|
187
|
+
r.append('<hr>\n<ul>')
|
|
188
|
+
for name in list:
|
|
189
|
+
fullname = os.path.join(path, name)
|
|
190
|
+
displayname = linkname = name
|
|
191
|
+
# Append / for directories or @ for symbolic links
|
|
192
|
+
if os.path.isdir(fullname):
|
|
193
|
+
displayname = name + "/"
|
|
194
|
+
linkname = name + "/"
|
|
195
|
+
elif not name.endswith("json") and not name.endswith("html"):
|
|
196
|
+
# Do not display files that we can't handle
|
|
197
|
+
continue
|
|
198
|
+
if os.path.islink(fullname):
|
|
199
|
+
displayname = name + "@"
|
|
200
|
+
# Note: a link to a directory displays with @ and links with /
|
|
201
|
+
if os.path.isdir(fullname):
|
|
202
|
+
r.append('<li><a href="%s">%s</a></li>'
|
|
203
|
+
% (urllib.parse.quote(linkname,
|
|
204
|
+
errors='surrogatepass'),
|
|
205
|
+
html.escape(displayname, quote=False)))
|
|
206
|
+
else:
|
|
207
|
+
# Open a new tab
|
|
208
|
+
r.append('<li><a href="%s" target="_blank">%s</a></li>'
|
|
209
|
+
% (urllib.parse.quote(linkname,
|
|
210
|
+
errors='surrogatepass'),
|
|
211
|
+
html.escape(displayname, quote=False)))
|
|
212
|
+
r.append('</ul>\n<hr>\n</body>\n</html>\n')
|
|
213
|
+
encoded = '\n'.join(r).encode(enc, 'surrogateescape')
|
|
214
|
+
f = io.BytesIO()
|
|
215
|
+
f.write(encoded)
|
|
216
|
+
f.seek(0)
|
|
217
|
+
self.send_response(HTTPStatus.OK)
|
|
218
|
+
self.send_header("Content-type", f"text/html; charset={enc}")
|
|
219
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
220
|
+
self.end_headers()
|
|
221
|
+
return f
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class ExternalProcessorProcess:
|
|
225
|
+
trace_processor_path = os.path.join(os.path.dirname(__file__), "web_dist", "trace_processor")
|
|
226
|
+
|
|
227
|
+
def __init__(self, path: str) -> None:
|
|
228
|
+
self.path = path
|
|
229
|
+
self._process = subprocess.Popen(
|
|
230
|
+
[
|
|
231
|
+
sys.executable,
|
|
232
|
+
self.trace_processor_path,
|
|
233
|
+
self.path,
|
|
234
|
+
"-D",
|
|
235
|
+
],
|
|
236
|
+
stderr=subprocess.PIPE,
|
|
237
|
+
)
|
|
238
|
+
atexit.register(self.stop)
|
|
239
|
+
self._wait_start()
|
|
240
|
+
|
|
241
|
+
def _wait_start(self):
|
|
242
|
+
print("Loading and parsing trace data, this could take a while...")
|
|
243
|
+
assert self._process.stderr is not None
|
|
244
|
+
while True:
|
|
245
|
+
line = self._process.stderr.readline().decode("utf-8")
|
|
246
|
+
if "This server can be used" in line:
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
def stop(self):
|
|
250
|
+
self._process.terminate()
|
|
251
|
+
try:
|
|
252
|
+
self._process.wait(timeout=2)
|
|
253
|
+
except subprocess.TimeoutExpired: # pragma: no cover
|
|
254
|
+
self._process.kill()
|
|
255
|
+
atexit.unregister(self.stop)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class VizViewerTCPServer(socketserver.TCPServer):
|
|
259
|
+
def handle_timeout(self) -> None:
|
|
260
|
+
self.trace_served = True
|
|
261
|
+
return super().handle_timeout()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ServerThread(threading.Thread):
|
|
265
|
+
def __init__(
|
|
266
|
+
self,
|
|
267
|
+
path: str,
|
|
268
|
+
port: int = 9001,
|
|
269
|
+
once: bool = False,
|
|
270
|
+
use_external_processor: bool = False,
|
|
271
|
+
timeout: float = 10,
|
|
272
|
+
quiet: bool = False) -> None:
|
|
273
|
+
self.path = path
|
|
274
|
+
self.port = port
|
|
275
|
+
self.once = once
|
|
276
|
+
self.timeout = timeout
|
|
277
|
+
self.quiet = quiet
|
|
278
|
+
self.link = f"http://127.0.0.1:{self.port}"
|
|
279
|
+
self.use_external_procesor = use_external_processor
|
|
280
|
+
self.externel_processor_process: ExternalProcessorProcess | None = None
|
|
281
|
+
self.fg_data: list[dict[str, Any]] | None = None
|
|
282
|
+
self.file_info = None
|
|
283
|
+
self.httpd: VizViewerTCPServer | None = None
|
|
284
|
+
self.last_active = time.time()
|
|
285
|
+
self.retcode: int | None = None
|
|
286
|
+
self.ready = threading.Event()
|
|
287
|
+
self.ready.clear()
|
|
288
|
+
super().__init__(daemon=True)
|
|
289
|
+
|
|
290
|
+
def run(self) -> None:
|
|
291
|
+
try:
|
|
292
|
+
self.retcode = self.view()
|
|
293
|
+
except Exception:
|
|
294
|
+
self.retcode = 1
|
|
295
|
+
traceback.print_exc()
|
|
296
|
+
finally:
|
|
297
|
+
# If it returns from view(), also set ready
|
|
298
|
+
self.ready.set()
|
|
299
|
+
|
|
300
|
+
def view(self) -> int:
|
|
301
|
+
# Get file data
|
|
302
|
+
filename = os.path.basename(self.path)
|
|
303
|
+
|
|
304
|
+
Handler: Callable[..., HttpHandler]
|
|
305
|
+
if filename.endswith("json") or filename.endswith("gz"):
|
|
306
|
+
trace_data = None
|
|
307
|
+
if self.use_external_procesor:
|
|
308
|
+
Handler = functools.partial(ExternalProcessorHandler, self)
|
|
309
|
+
self.externel_processor_process = ExternalProcessorProcess(self.path)
|
|
310
|
+
else:
|
|
311
|
+
if filename.endswith("gz"):
|
|
312
|
+
with gzip.open(self.path, "rt", encoding="utf-8", errors="ignore") as f:
|
|
313
|
+
trace_data = json.load(f)
|
|
314
|
+
else:
|
|
315
|
+
with open(self.path, encoding="utf-8", errors="ignore") as f:
|
|
316
|
+
trace_data = json.load(f)
|
|
317
|
+
self.file_info = trace_data.get("file_info", {})
|
|
318
|
+
Handler = functools.partial(PerfettoHandler, self)
|
|
319
|
+
elif filename.endswith("html"):
|
|
320
|
+
Handler = functools.partial(HtmlHandler, self)
|
|
321
|
+
else:
|
|
322
|
+
print(f"Do not support file type {filename}")
|
|
323
|
+
return 1
|
|
324
|
+
|
|
325
|
+
if self.is_port_in_use():
|
|
326
|
+
print(f'Error! Port {self.port} is already in use, try another port with "--port"')
|
|
327
|
+
return 1
|
|
328
|
+
|
|
329
|
+
socketserver.TCPServer.allow_reuse_address = True
|
|
330
|
+
with VizViewerTCPServer(('0.0.0.0', self.port), Handler) as self.httpd:
|
|
331
|
+
if not self.once and not self.quiet:
|
|
332
|
+
print("Running vizviewer")
|
|
333
|
+
print(f"You can also view your trace on http://localhost:{self.port}")
|
|
334
|
+
print("Press Ctrl+C to quit", flush=True)
|
|
335
|
+
self.ready.set()
|
|
336
|
+
if self.once:
|
|
337
|
+
self.httpd.timeout = self.timeout
|
|
338
|
+
while not self.httpd.__dict__.get("trace_served", False):
|
|
339
|
+
self.httpd.handle_request()
|
|
340
|
+
else:
|
|
341
|
+
self.httpd.serve_forever()
|
|
342
|
+
|
|
343
|
+
if self.externel_processor_process is not None:
|
|
344
|
+
self.externel_processor_process.stop()
|
|
345
|
+
|
|
346
|
+
return 0
|
|
347
|
+
|
|
348
|
+
def notify_active(self) -> None:
|
|
349
|
+
self.last_active = time.time()
|
|
350
|
+
|
|
351
|
+
def is_port_in_use(self) -> bool:
|
|
352
|
+
with contextlib.closing(
|
|
353
|
+
socket.socket(socket.AF_INET,
|
|
354
|
+
socket.SOCK_STREAM)) as sock:
|
|
355
|
+
return sock.connect_ex(('127.0.0.1', self.port)) == 0
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class DirectoryViewer:
|
|
359
|
+
def __init__(
|
|
360
|
+
self,
|
|
361
|
+
path: str,
|
|
362
|
+
port: int,
|
|
363
|
+
server_only: bool,
|
|
364
|
+
timeout: int,
|
|
365
|
+
use_external_processor: bool) -> None:
|
|
366
|
+
self.base_path = os.path.abspath(path)
|
|
367
|
+
self.port = port
|
|
368
|
+
self.server_only = server_only
|
|
369
|
+
self.timeout = timeout
|
|
370
|
+
self.use_external_processor = use_external_processor
|
|
371
|
+
self.max_port_number = 10
|
|
372
|
+
self.servers: dict[str, ServerThread] = {}
|
|
373
|
+
|
|
374
|
+
def get_link(self, path: str) -> str:
|
|
375
|
+
path = os.path.join(self.base_path, path)
|
|
376
|
+
if path not in self.servers:
|
|
377
|
+
self.servers[path] = self.create_server(path)
|
|
378
|
+
|
|
379
|
+
server = self.servers[path]
|
|
380
|
+
return server.link
|
|
381
|
+
|
|
382
|
+
def create_server(self, path: str) -> ServerThread:
|
|
383
|
+
max_port_number = self.max_port_number
|
|
384
|
+
ports_used = set((serv.port for serv in self.servers.values()))
|
|
385
|
+
if len(ports_used) == max_port_number:
|
|
386
|
+
self.clean_servers(force=True)
|
|
387
|
+
else:
|
|
388
|
+
self.clean_servers(force=False)
|
|
389
|
+
ports_used = set((serv.port for serv in self.servers.values()))
|
|
390
|
+
for port in range(self.port + 1, self.port + max_port_number + 1):
|
|
391
|
+
if port not in ports_used:
|
|
392
|
+
t = ServerThread(
|
|
393
|
+
path,
|
|
394
|
+
port=port,
|
|
395
|
+
use_external_processor=self.use_external_processor,
|
|
396
|
+
quiet=True)
|
|
397
|
+
t.start()
|
|
398
|
+
t.ready.wait()
|
|
399
|
+
return t
|
|
400
|
+
assert False, "Should always have a port available" # pragma: no cover
|
|
401
|
+
|
|
402
|
+
def clean_servers(self, force: bool = False) -> None:
|
|
403
|
+
curr_time = time.time()
|
|
404
|
+
removed_path = []
|
|
405
|
+
for path, server in self.servers.items():
|
|
406
|
+
if curr_time - server.last_active > self.timeout:
|
|
407
|
+
if server.httpd is not None:
|
|
408
|
+
server.httpd.shutdown()
|
|
409
|
+
removed_path.append(path)
|
|
410
|
+
server.join()
|
|
411
|
+
for path in removed_path:
|
|
412
|
+
self.servers.pop(path)
|
|
413
|
+
if len(removed_path) == 0 and force:
|
|
414
|
+
max_idle_time, max_idle_path = max(
|
|
415
|
+
(curr_time - server.last_active, path)
|
|
416
|
+
for path, server in self.servers.items()
|
|
417
|
+
)
|
|
418
|
+
server = self.servers.pop(max_idle_path)
|
|
419
|
+
if server.httpd:
|
|
420
|
+
server.httpd.shutdown()
|
|
421
|
+
server.join()
|
|
422
|
+
|
|
423
|
+
def run(self) -> int:
|
|
424
|
+
Handler = functools.partial(DirectoryHandler, self)
|
|
425
|
+
socketserver.TCPServer.allow_reuse_address = True
|
|
426
|
+
with VizViewerTCPServer(('0.0.0.0', self.port), Handler) as httpd:
|
|
427
|
+
print("Running vizviewer")
|
|
428
|
+
print(f"You can also view your trace on http://localhost:{self.port}")
|
|
429
|
+
print("Press Ctrl+C to quit", flush=True)
|
|
430
|
+
if not self.server_only:
|
|
431
|
+
# import webbrowser only if necessary
|
|
432
|
+
import webbrowser
|
|
433
|
+
webbrowser.open_new_tab(f'http://127.0.0.1:{self.port}')
|
|
434
|
+
try:
|
|
435
|
+
httpd.serve_forever()
|
|
436
|
+
except KeyboardInterrupt:
|
|
437
|
+
for server in self.servers.values():
|
|
438
|
+
if server.httpd:
|
|
439
|
+
server.httpd.shutdown()
|
|
440
|
+
server.join()
|
|
441
|
+
self.servers = {}
|
|
442
|
+
return 0
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def viewer_main() -> int:
|
|
446
|
+
parser = argparse.ArgumentParser()
|
|
447
|
+
parser.add_argument("file", nargs=1, help="html/json/gz file to open")
|
|
448
|
+
parser.add_argument("--server_only", "-s", default=False, action="store_true",
|
|
449
|
+
help="Only start the server, do not open webpage")
|
|
450
|
+
parser.add_argument("--port", "-p", nargs="?", type=int, default=9001,
|
|
451
|
+
help="Specify the port vizviewer will use")
|
|
452
|
+
parser.add_argument("--once", default=False, action="store_true",
|
|
453
|
+
help="Only serve trace data once, then exit.")
|
|
454
|
+
parser.add_argument("--timeout", nargs="?", type=int, default=10,
|
|
455
|
+
help="Timeout in seconds to stop the server without trace data requests")
|
|
456
|
+
parser.add_argument("--flamegraph", default=False, action="store_true",
|
|
457
|
+
help=argparse.SUPPRESS)
|
|
458
|
+
parser.add_argument("--use_external_processor", default=False, action="store_true",
|
|
459
|
+
help="Use the more powerful external trace processor instead of WASM")
|
|
460
|
+
|
|
461
|
+
options = parser.parse_args(sys.argv[1:])
|
|
462
|
+
f = options.file[0]
|
|
463
|
+
|
|
464
|
+
if options.flamegraph:
|
|
465
|
+
print("--flamegraph is removed because the front-end supports native flamegraph now.")
|
|
466
|
+
print("You can select slices in the UI and do 'Slice Flamegraph'.")
|
|
467
|
+
return 1
|
|
468
|
+
|
|
469
|
+
if options.use_external_processor:
|
|
470
|
+
# Perfetto trace processor only accepts requests from localhost:10000
|
|
471
|
+
options.port = 10000
|
|
472
|
+
# external trace process won't work with once or directory
|
|
473
|
+
if options.once:
|
|
474
|
+
print("You can't use --once with --use_external_processor")
|
|
475
|
+
return 1
|
|
476
|
+
if os.path.isdir(f):
|
|
477
|
+
print("You can't use --use_external_processor on a directory")
|
|
478
|
+
return 1
|
|
479
|
+
|
|
480
|
+
if os.path.isdir(f):
|
|
481
|
+
cwd = os.getcwd()
|
|
482
|
+
try:
|
|
483
|
+
directory_viewer = DirectoryViewer(
|
|
484
|
+
path=f,
|
|
485
|
+
port=options.port,
|
|
486
|
+
server_only=options.server_only,
|
|
487
|
+
timeout=options.timeout,
|
|
488
|
+
use_external_processor=options.use_external_processor,
|
|
489
|
+
)
|
|
490
|
+
directory_viewer.run()
|
|
491
|
+
finally:
|
|
492
|
+
os.chdir(cwd)
|
|
493
|
+
elif os.path.exists(f):
|
|
494
|
+
path = os.path.abspath(options.file[0])
|
|
495
|
+
cwd = os.getcwd()
|
|
496
|
+
try:
|
|
497
|
+
server = ServerThread(
|
|
498
|
+
path,
|
|
499
|
+
port=options.port,
|
|
500
|
+
once=options.once,
|
|
501
|
+
timeout=options.timeout,
|
|
502
|
+
use_external_processor=options.use_external_processor,
|
|
503
|
+
)
|
|
504
|
+
server.start()
|
|
505
|
+
server.ready.wait()
|
|
506
|
+
if server.retcode is not None:
|
|
507
|
+
return server.retcode
|
|
508
|
+
if not options.server_only:
|
|
509
|
+
# import webbrowser only if necessary
|
|
510
|
+
import webbrowser
|
|
511
|
+
webbrowser.open_new_tab(f'http://127.0.0.1:{options.port}')
|
|
512
|
+
while server.is_alive():
|
|
513
|
+
server.join(timeout=1)
|
|
514
|
+
except KeyboardInterrupt:
|
|
515
|
+
if server.httpd is not None:
|
|
516
|
+
server.httpd.shutdown()
|
|
517
|
+
server.join(timeout=2)
|
|
518
|
+
finally:
|
|
519
|
+
os.chdir(cwd)
|
|
520
|
+
else:
|
|
521
|
+
print(f"File {f} does not exist!")
|
|
522
|
+
return 1
|
|
523
|
+
|
|
524
|
+
return 0
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
if __name__ == "__main__":
|
|
528
|
+
sys.exit(viewer_main())
|
viztracer/vizcounter.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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 .event_base import _EventBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VizCounter(_EventBase):
|
|
8
|
+
def _viztracer_log(self) -> None:
|
|
9
|
+
if not self._viztracer_tracer:
|
|
10
|
+
return
|
|
11
|
+
d = {}
|
|
12
|
+
for attr in self._viztracer_get_attr_list():
|
|
13
|
+
if hasattr(self, attr):
|
|
14
|
+
val = self.__getattribute__(attr)
|
|
15
|
+
if not callable(val):
|
|
16
|
+
if type(val) is int or type(val) is float:
|
|
17
|
+
d[attr] = val
|
|
18
|
+
else:
|
|
19
|
+
raise Exception("Counter can only take numeric values")
|
|
20
|
+
self._viztracer_tracer.add_counter(self._viztracer_name, d)
|
viztracer/vizevent.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .viztracer import VizTracer # pragma: no cover
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VizEvent:
|
|
12
|
+
def __init__(self, tracer: "VizTracer", event_name: str, file_name: str, lineno: int) -> None:
|
|
13
|
+
self._tracer = tracer
|
|
14
|
+
self.event_name = event_name
|
|
15
|
+
self.file_name = file_name
|
|
16
|
+
self.lineno = lineno
|
|
17
|
+
self.start = 0.0
|
|
18
|
+
|
|
19
|
+
def __enter__(self) -> None:
|
|
20
|
+
self.start = self._tracer.getts()
|
|
21
|
+
|
|
22
|
+
def __exit__(self, type, value, trace) -> None:
|
|
23
|
+
dur = self._tracer.getts() - self.start
|
|
24
|
+
raw_data = {
|
|
25
|
+
"ph": "X",
|
|
26
|
+
"name": f"{self.event_name} ({self.file_name}:{self.lineno})",
|
|
27
|
+
"ts": self.start,
|
|
28
|
+
"dur": dur,
|
|
29
|
+
"cat": "FEE",
|
|
30
|
+
}
|
|
31
|
+
self._tracer.add_raw(raw_data)
|
viztracer/vizlogging.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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 logging import Handler, LogRecord
|
|
5
|
+
|
|
6
|
+
from .viztracer import VizTracer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VizLoggingHandler(Handler):
|
|
10
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
11
|
+
super().__init__(*args, **kwargs)
|
|
12
|
+
self._tracer: VizTracer | None = None
|
|
13
|
+
|
|
14
|
+
def emit(self, record: LogRecord) -> None:
|
|
15
|
+
if not self._tracer:
|
|
16
|
+
return
|
|
17
|
+
self._tracer.add_instant(f"logging - {self.format(record)}", scope="p")
|
|
18
|
+
|
|
19
|
+
def setTracer(self, tracer: VizTracer) -> None:
|
|
20
|
+
self._tracer = tracer
|
viztracer/vizobject.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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 .event_base import _EventBase
|
|
5
|
+
from .viztracer import VizTracer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VizObject(_EventBase):
|
|
9
|
+
def __init__(self, tracer: VizTracer, name: str, **kwargs) -> None:
|
|
10
|
+
super().__init__(tracer, name, **kwargs)
|
|
11
|
+
self._viztracer_id = str(id(self))
|
|
12
|
+
if self._viztracer_tracer:
|
|
13
|
+
self._viztracer_tracer.add_object("N", self._viztracer_id, self._viztracer_name)
|
|
14
|
+
|
|
15
|
+
def __del__(self) -> None:
|
|
16
|
+
if self._viztracer_tracer:
|
|
17
|
+
self._viztracer_tracer.add_object("D", self._viztracer_id, self._viztracer_name)
|
|
18
|
+
|
|
19
|
+
def _viztracer_log(self, ph: str = "O") -> None:
|
|
20
|
+
if not self._viztracer_tracer:
|
|
21
|
+
return
|
|
22
|
+
d = {}
|
|
23
|
+
for attr in self._viztracer_get_attr_list():
|
|
24
|
+
if hasattr(self, attr):
|
|
25
|
+
val = self.__getattribute__(attr)
|
|
26
|
+
if type(val) is list or type(val) is dict or type(val) is int or type(val) is float or type(val) is str:
|
|
27
|
+
d[attr] = val
|
|
28
|
+
self._viztracer_tracer.add_object(ph, self._viztracer_id, self._viztracer_name, {"snapshot": d})
|