diff --git a/README.md b/README.md index 3ec77e9c..514de7f0 100644 --- a/README.md +++ b/README.md @@ -212,11 +212,7 @@ Refer to [async docs](https://viztracer.readthedocs.io/en/stable/concurrency.htm ### Flamegraph -VizTracer can show flamegraph of traced data. - -```sh -vizviewer --flamegraph result.json -``` +Perfetto supports native flamegraph, just select slices on the UI and choose "Slice Flamegraph". [![example_img](https://github.com/gaogaotiantian/viztracer/blob/master/img/flamegraph.png)](https://github.com/gaogaotiantian/viztracer/blob/master/img/flamegraph.png) diff --git a/img/flamegraph.png b/img/flamegraph.png index 5810cff9..44688e2a 100755 Binary files a/img/flamegraph.png and b/img/flamegraph.png differ diff --git a/src/viztracer/flamegraph.py b/src/viztracer/flamegraph.py deleted file mode 100644 index 5a0f9356..00000000 --- a/src/viztracer/flamegraph.py +++ /dev/null @@ -1,115 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/gaogaotiantian/viztracer/blob/master/NOTICE.txt - -import queue -from typing import Any, Dict, List, Optional, Tuple - -from .functree import FuncTree, FuncTreeNode - - -class _FlameNode: - def __init__(self, parent: Optional["_FlameNode"], name: str) -> None: - self.name: str = name - self.value: float = 0 - self.count: int = 0 - self.parent: Optional["_FlameNode"] = parent - self.children: Dict[str, "_FlameNode"] = {} - - def get_child(self, child: FuncTreeNode) -> None: - if child.fullname not in self.children: - self.children[child.fullname] = _FlameNode(self, child.fullname) - self.children[child.fullname].value += child.end - child.start - self.children[child.fullname].count += 1 - for grandchild in child.children: - self.children[child.fullname].get_child(grandchild) - - -class _FlameTree: - def __init__(self, func_tree: FuncTree) -> None: - self.root: _FlameNode = _FlameNode(None, "__root__") - self.parse(func_tree) - - def parse(self, func_tree: FuncTree) -> None: - self.root = _FlameNode(None, "__root__") - for child in func_tree.root.children: - self.root.get_child(child) - - -class FlameGraph: - def __init__(self, trace_data: Optional[Dict[str, Any]] = None) -> None: - self.trees: Dict[str, _FlameTree] = {} - if trace_data: - self.parse(trace_data) - - def parse(self, trace_data: Dict[str, Any]) -> None: - func_trees: Dict[str, FuncTree] = {} - for data in trace_data["traceEvents"]: - key = f"p{data['pid']}_t{data['tid']}" - if key in func_trees: - tree = func_trees[key] - else: - tree = FuncTree(data["pid"], data["tid"]) - func_trees[key] = tree - - if data["ph"] == "X": - tree.add_event(data) - - for key, tree in func_trees.items(): - self.trees[key] = _FlameTree(tree) - - def dump_to_perfetto(self) -> List[Dict[str, Any]]: - """ - Reformat data to what perfetto likes - private _functionProfileDetails?: FunctionProfileDetails[] - export interface FunctionProfileDetails { - name?: string; - flamegraph?: CallsiteInfo[]; - expandedCallsite?: CallsiteInfo; - expandedId?: number; - } - export interface CallsiteInfo { - id: number; - parentId: number; - depth: number; - name?: string; - totalSize: number; - selfSize: number; - mapping: string; - merged: boolean; - highlighted: boolean; - } - """ - ret = [] - for name, tree in self.trees.items(): - q: queue.Queue[Tuple[_FlameNode, int, int]] = queue.Queue() - for child in tree.root.children.values(): - q.put((child, -1, 0)) - - if q.empty(): - continue - - flamegraph = [] - idx = 0 - while not q.empty(): - node, parent, depth = q.get() - flamegraph.append({ - "id": idx, - "parentId": parent, - "depth": depth, - "name": node.name, - "totalSize": node.value, - "selfSize": node.value - sum((n.value for n in node.children.values())), - "mapping": f"{node.count}", - "merged": False, - "highlighted": False, - }) - for n in node.children.values(): - q.put((n, idx, depth + 1)) - idx += 1 - - detail = { - "name": name, - "flamegraph": flamegraph, - } - ret.append(detail) - return ret diff --git a/src/viztracer/viewer.py b/src/viztracer/viewer.py index f66c904f..3e43def7 100644 --- a/src/viztracer/viewer.py +++ b/src/viztracer/viewer.py @@ -21,8 +21,6 @@ from http import HTTPStatus from typing import Any, Callable, Dict, List, Optional -from .flamegraph import FlameGraph - dir_lock = threading.Lock() @@ -77,9 +75,7 @@ def do_GET(self): self.server.last_request = self.path self.server_thread.notify_active() if self.path.endswith("vizviewer_info"): - info = { - "is_flamegraph": self.server_thread.flamegraph, - } + info = {} self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() @@ -100,13 +96,6 @@ def do_GET(self): self.path = f"/{filename}" self.server.trace_served = True return super().do_GET() - elif self.path.endswith("flamegraph"): - self.send_response(200) - self.send_header("Content-type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(self.server_thread.fg_data).encode("utf-8")) - self.wfile.flush() - self.server.trace_served = True else: self.directory = os.path.join(os.path.dirname(__file__), "web_dist") with chdir_temp(self.directory): @@ -276,7 +265,6 @@ def __init__( path: str, port: int = 9001, once: bool = False, - flamegraph: bool = False, use_external_processor: bool = False, timeout: float = 10, quiet: bool = False) -> None: @@ -286,7 +274,6 @@ def __init__( self.timeout = timeout self.quiet = quiet self.link = f"http://127.0.0.1:{self.port}" - self.flamegraph = flamegraph self.use_external_procesor = use_external_processor self.externel_processor_process: Optional[ExternalProcessorProcess] = None self.fg_data: Optional[List[Dict[str, Any]]] = None @@ -313,17 +300,7 @@ def view(self) -> int: filename = os.path.basename(self.path) Handler: Callable[..., HttpHandler] - if self.flamegraph: - if filename.endswith("json"): - with open(self.path, encoding="utf-8", errors="ignore") as f: - trace_data = json.load(f) - fg = FlameGraph(trace_data) - self.fg_data = fg.dump_to_perfetto() - Handler = functools.partial(PerfettoHandler, self) - else: - print(f"Do not support flamegraph for file type {filename}") - return 1 - elif filename.endswith("json"): + if filename.endswith("json"): trace_data = None if self.use_external_procesor: Handler = functools.partial(ExternalProcessorHandler, self) @@ -378,13 +355,11 @@ def __init__( path: str, port: int, server_only: bool, - flamegraph: bool, timeout: int, use_external_processor: bool) -> None: self.base_path = os.path.abspath(path) self.port = port self.server_only = server_only - self.flamegraph = flamegraph self.timeout = timeout self.use_external_processor = use_external_processor self.max_port_number = 10 @@ -411,7 +386,6 @@ def create_server(self, path: str) -> ServerThread: t = ServerThread( path, port=port, - flamegraph=self.flamegraph, use_external_processor=self.use_external_processor, quiet=True) t.start() @@ -474,23 +448,25 @@ def viewer_main() -> int: parser.add_argument("--timeout", nargs="?", type=int, default=10, help="Timeout in seconds to stop the server without trace data requests") parser.add_argument("--flamegraph", default=False, action="store_true", - help="Show flamegraph of data") + help=argparse.SUPPRESS) parser.add_argument("--use_external_processor", default=False, action="store_true", help="Use the more powerful external trace processor instead of WASM") options = parser.parse_args(sys.argv[1:]) f = options.file[0] + if options.flamegraph: + print("--flamegraph is removed because the front-end supports native flamegraph now.") + print("You can select slices in the UI and do 'Slice Flamegraph'.") + return 1 + if options.use_external_processor: # Perfetto trace processor only accepts requests from localhost:10000 options.port = 10000 - # external trace process won't work with once or flamegraph or directory + # external trace process won't work with once or directory if options.once: print("You can't use --once with --use_external_processor") return 1 - if options.flamegraph: - print("You can't use --flamegraph with --use_external_processor") - return 1 if os.path.isdir(f): print("You can't use --use_external_processor on a directory") return 1 @@ -502,7 +478,6 @@ def viewer_main() -> int: path=f, port=options.port, server_only=options.server_only, - flamegraph=options.flamegraph, timeout=options.timeout, use_external_processor=options.use_external_processor, ) @@ -517,7 +492,6 @@ def viewer_main() -> int: path, port=options.port, once=options.once, - flamegraph=options.flamegraph, timeout=options.timeout, use_external_processor=options.use_external_processor, ) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index bdf2a457..6e4bf8e3 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -344,14 +344,14 @@ def test_tracer_entries(self): def test_trace_self(self): def check_func(data): - self.assertGreater(len(data["traceEvents"]), 10000) + self.assertGreater(len(data["traceEvents"]), 1000) example_json_dir = os.path.join(os.path.dirname(__file__), "../", "example/json") if sys.platform == "win32": - self.template(["viztracer", "--trace_self", "vizviewer", "--flamegraph", "--server_only", + self.template(["viztracer", "--trace_self", "vizviewer", "--server_only", os.path.join(example_json_dir, "multithread.json")], success=False) else: - self.template(["viztracer", "--trace_self", "vizviewer", "--flamegraph", "--server_only", + self.template(["viztracer", "--trace_self", "vizviewer", "--server_only", os.path.join(example_json_dir, "multithread.json")], send_sig=(signal.SIGTERM, "Ctrl+C"), expected_output_file="result.json", check_func=check_func) diff --git a/tests/test_flamegraph.py b/tests/test_flamegraph.py deleted file mode 100644 index b25a58d4..00000000 --- a/tests/test_flamegraph.py +++ /dev/null @@ -1,29 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/gaogaotiantian/viztracer/blob/master/NOTICE.txt - -import json -import os - -from viztracer.flamegraph import FlameGraph - -from .base_tmpl import BaseTmpl - - -class TestFlameGraph(BaseTmpl): - def test_dump_perfetto(self): - with open(os.path.join(os.path.dirname(__file__), "data/multithread.json")) as f: - sample_data = json.loads(f.read()) - fg = FlameGraph(sample_data) - data = fg.dump_to_perfetto() - self.assertEqual(len(data), 5) - for callsite_info in data: - self.assertIn("name", callsite_info) - self.assertIn("flamegraph", callsite_info) - - # Test if FlameGraph can handle empty tree - sample_data["traceEvents"].append( - {"ph": "M", "pid": 1, "tid": 1, "name": "thread_name", "args": {"name": "MainThread"}}) - fg = FlameGraph(sample_data) - self.assertEqual(len(fg.trees), 6) - data = fg.dump_to_perfetto() - self.assertEqual(len(data), 5) diff --git a/tests/test_viewer.py b/tests/test_viewer.py index c626b632..12b9d88b 100644 --- a/tests/test_viewer.py +++ b/tests/test_viewer.py @@ -27,7 +27,6 @@ def __init__( self, file_path, once=False, - flamegraph=False, timeout=None, use_external_processor=None, expect_success=True, @@ -42,9 +41,6 @@ def __init__( if once: self.cmd.append("--once") - if flamegraph: - self.cmd.append("--flamegraph") - if timeout is not None: self.cmd.append("--timeout") self.cmd.append(f"{timeout}") @@ -329,25 +325,35 @@ def test_once_timeout(self): finally: os.remove(f.name) - def test_flamegraph(self): + @unittest.skipIf(sys.platform == "darwin", "MacOS has a high security check for multiprocessing") + def test_browser(self): + html = '' + try: + with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f: + f.write(html) + with unittest.mock.patch.object(sys, "argv", ["vizviewer", "--once", f.name]): + with unittest.mock.patch.object(webbrowser, "open_new_tab", MockOpen(html)) as mock_obj: + viewer_main() + mock_obj.p.join() + self.assertEqual(mock_obj.p.exitcode, 0) + finally: + os.remove(f.name) + + def test_vizviewer_info(self): json_script = '{"file_info": {}, "traceEvents": []}' try: with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: f.write(json_script) - # --once won't work with --use_external_processor - self.template(["vizviewer", "--flamegraph", "--use_external_processor", f.name], - success=False, expected_output_file=None) - - v = Viewer(f.name, once=True, flamegraph=True) + v = Viewer(f.name, once=True) v.run() try: time.sleep(0.5) resp = urllib.request.urlopen(f"{v.url()}/vizviewer_info") self.assertTrue(resp.code == 200) - self.assertTrue(json.loads(resp.read().decode("utf-8"))["is_flamegraph"], True) - resp = urllib.request.urlopen(f"{v.url()}/flamegraph") - self.assertEqual(json.loads(resp.read().decode("utf-8")), []) + self.assertEqual(json.loads(resp.read().decode("utf-8")), {}) + resp = urllib.request.urlopen(f"{v.url()}/localtrace") + self.assertEqual(json.loads(resp.read().decode("utf-8")), json.loads(json_script)) except Exception: v.stop() raise @@ -361,20 +367,6 @@ def test_flamegraph(self): finally: os.remove(f.name) - @unittest.skipIf(sys.platform == "darwin", "MacOS has a high security check for multiprocessing") - def test_browser(self): - html = '' - try: - with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f: - f.write(html) - with unittest.mock.patch.object(sys, "argv", ["vizviewer", "--once", f.name]): - with unittest.mock.patch.object(webbrowser, "open_new_tab", MockOpen(html)) as mock_obj: - viewer_main() - mock_obj.p.join() - self.assertEqual(mock_obj.p.exitcode, 0) - finally: - os.remove(f.name) - @unittest.skipIf(sys.platform == "win32", "Can't send Ctrl+C reliably on Windows") def test_directory(self): test_data_dir = os.path.join(os.path.dirname(__file__), "data") @@ -409,22 +401,6 @@ def test_directory_browser(self): finally: os.remove(f.name) - @unittest.skipIf(sys.platform == "win32", "Can't send Ctrl+C reliably on Windows") - def test_directory_flamegraph(self): - test_data_dir = os.path.join(os.path.dirname(__file__), "data") - with Viewer(test_data_dir, flamegraph=True) as v: - time.sleep(0.5) - resp = urllib.request.urlopen(v.url()) - self.assertEqual(resp.code, 200) - self.assertIn("fib.json", resp.read().decode("utf-8")) - resp = urllib.request.urlopen(f"{v.url()}/fib.json") - self.assertEqual(resp.url, f"{v.url(1)}/") - resp = urllib.request.urlopen(f"{v.url(1)}/vizviewer_info") - self.assertTrue(resp.code == 200) - self.assertTrue(json.loads(resp.read().decode("utf-8"))["is_flamegraph"], True) - resp = urllib.request.urlopen(f"{v.url(1)}/flamegraph") - self.assertEqual(len(json.loads(resp.read().decode("utf-8"))[0]["flamegraph"]), 2) - @unittest.skipIf(sys.platform == "win32", "Can't send Ctrl+C reliably on Windows") def test_directory_timeout(self): test_data_dir = os.path.join(os.path.dirname(__file__), "data") @@ -468,3 +444,5 @@ def test_invalid(self): self.template(["vizviewer", "do_not_exist.json"], success=False, expected_output_file=None) self.template(["vizviewer", "README.md"], success=False, expected_output_file=None) self.template(["vizviewer", "--flamegraph", "README.md"], success=False, expected_output_file=None) + self.template(["vizviewer", "--flamegraph", "example/json/multithread.md"], + success=False, expected_output_file=None, expected_stdout="--flamegraph is removed.*")