Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a script to translate jittied function names in flamegraph #2906

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion doc/perf_tune.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ $ perf report --input=perf.data
>
> For example, with EMCC, you can add `-g2`.
>
> If not able to get the context of the custom name section, WAMR will use `aot_func#N` to represent the function name. `N` is from 0. `aot_func#0` represents the first *not imported wasm function*.
> If not able to get the context of the custom name section, WAMR will use `aot_func#N` to represent the function name. `N` is from 0. `aot_func#0` represents the first _not imported wasm function_.

### 7.1 Flamegraph

Expand Down Expand Up @@ -177,3 +177,16 @@ $ ./FlameGraph/flamegraph.pl out.folded > perf.foo.wasm.svg
> # only jitted functions
> $ grep "wasm_runtime_invoke_native" out.folded | ./FlameGraph/flamegraph.pl > perf.foo.wasm.only.svg
> ```

> [!TIP]
> use [trans_wasm_func_name.py](../test-tools/trans-jitted-func-name/trans_wasm_func_name.py) to translate jitted function
> names to its original wasm function names. It requires _wasm-objdump_ in _wabt_ and _name section_ in the .wasm file
>
> The input file is the output of `./FlameGraph/stackcollapse-perf.pl`.
>
> ```bash
> python trans_wasm_func_name.py --wabt_home <wabt-installation> --folded out.folded <.wasm>
> ```
>
> Then you will see a new file named _out.folded.translated_ which contains the translated folded stacks.
> All wasm functions are translated to its original names with a prefix like "[Wasm]"
196 changes: 196 additions & 0 deletions test-tools/trans-jitted-func-name/trans_wasm_func_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019 Intel Corporation. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
"""
It is used to translate jitted functions' names(in out.folded) to coorespond name in name section in .wasm

Usage:

After
```
$ perf script -i perf.data > out.perf

# fold call stacks
$ ./FlameGraph/stackcollapse-perf.pl out.perf > out.folded
```

Add a step:
```
# translate jitted functions' names
$ python translate_wasm_function_name.py --wabt_home <wabt-installation> --folded out.folded <.wasm>
# out.folded -> out.folded.translated
$ ls out.folded.translated
```

Then
```
# generate flamegraph
$ ./FlameGraph/flamegraph.pl out.folded.translated > perf.wasm.svg
```

"""

import argparse
import os
from pathlib import Path
import re
import shlex
import subprocess


def preflight_check(wabt_home: Path) -> Path:
"""
if wasm-objdump exists in wabt_home
"""
wasm_objdump_bin = wabt_home.joinpath("bin", "wasm-objdump")
if not wasm_objdump_bin.exists():
raise RuntimeError(f"wasm-objdump not found in {wabt_home}")

return wasm_objdump_bin


def collect_import_section_content(wasm_objdump_bin: Path, wasm_file: Path) -> dict:
"""
execute "wasm_objdump_bin -j Import -x <wasm_file>" and return a dict like {function: X, global: Y, memory: Z, table: N}
"""
assert wasm_objdump_bin.exists()
assert wasm_file.exists()

command = f"{wasm_objdump_bin} -j Import -x {wasm_file}"
p = subprocess.run(
shlex.split(command),
capture_output=True,
check=False,
text=True,
universal_newlines=True,
)

if p.stderr:
return {}

import_section = {}
for line in p.stdout.split(os.linesep):
line = line.strip()

if not line:
continue

if line.startswith(" - func"):
import_section.update("function", import_section.get("function", 0) + 1)
else:
pass

return import_section


def collect_name_section_content(wasm_objdump_bin: Path, wasm_file: Path) -> dict:
"""
execute "wasm_objdump_bin -j name -x wasm_file" and store the output in a list
"""
assert wasm_objdump_bin.exists()
assert wasm_file.exists()

command = f"{wasm_objdump_bin} -j name -x {wasm_file}"
p = subprocess.run(
shlex.split(command),
capture_output=True,
check=False,
text=True,
universal_newlines=True,
)

if p.stderr:
raise RuntimeError(f"not found name section in {wasm_file}")

name_section = {}
for line in p.stdout.split(os.linesep):
line = line.strip()

if not line:
continue

# - func[0] <__imported_wasi_snapshot_preview1_fd_close>
if line.startswith("- func"):
m = re.match(r"- func\[(\d+)\] <(.+)>", line)
assert m

func_index, func_name = m.groups()
name_section.update({func_index: func_name})

assert name_section
return name_section


def replace_function_name(
import_section: dict, name_section: dict, folded_in: str, folded_out: str
) -> None:
"""
read content in <folded_in>. each line will be like:

quiche::BalsaFrame::ProcessHeaders;non-virtual thunk to Envoy::Http::Http1::BalsaParser::MessageDone;Envoy::Http::Http1::ConnectionImpl::onMessageComplete;Envoy::Http::Http1::ConnectionImpl::onMessageCompleteImpl;Envoy::Http::Http1::ServerConnectionImpl::onMessageCompleteBase;Envoy::Http::ConnectionManagerImpl::ActiveStream::decodeHeaders;Envoy::Http::FilterManager::decodeHeaders;virtual thunk to Envoy::Extensions::Common::Wasm::Context::decodeHeaders;proxy_wasm::ContextBase::onRequestHeaders;proxy_wasm::wamr::Wamr::getModuleFunctionImpl<proxy_wasm::Word, proxy_wasm::Word, proxy_wasm::Word, proxy_wasm::Word>;wasm_func_call;wasm_runtime_call_wasm;wasm_call_function;call_wasm_with_hw_bound_check;wasm_interp_call_wasm;llvm_jit_call_func_bytecode;wasm_runtime_invoke_native;push_args_end;aot_func_internal#3302;aot_func_internal#3308;asm_sysvec_apic_timer_interrupt;sysvec_apic_timer_interrupt;__sysvec_apic_timer_interrupt;hrtimer_interrupt;__hrtimer_run_queues;__remove_hrtimer;rb_next 1110899

symbol names are spearated by ";"

if there is a symbol named like "aot_func#XXX" or "aot_func_internal#XXX", it will be replaced with the function name in name section by index
"""
folded_in = Path(folded_in)
assert folded_in.exists()
folded_out = Path(folded_out)

import_function_count = import_section.get("function", 0)
with folded_in.open("rt", encoding="utf-8") as f_in, folded_out.open(
"wt", encoding="utf-8"
) as f_out:
for line in f_in:
new_line = []
line = line.strip()

m = re.match(r"(.*) (\d+)", line)
syms, samples = m.groups()
for sym in syms.split(";"):
m = re.match(r"aot_func(_internal)?#(\d+)", sym)
if not m:
new_line.append(sym)
continue

func_idx = m.groups()[-1]
if func_idx in name_section:
wasm_func_name = f"[Wasm] {name_section[func_idx]}"
else:
wasm_func_name = (
f"[Wasm] function[{func_idx + import_function_count}]"
)
# aot_func_internal
wasm_func_name += "_precheck" if not m.groups()[0] else ""
new_line.append(wasm_func_name)

line = ";".join(new_line)
line += f" {samples}"
f_out.write(line + os.linesep)

print(f"⚙️ {folded_in} -> {folded_out}")


def main(wabt_home: str, wasm_file: str, folded: str) -> None:
wabt_home = Path(wabt_home)
wasm_file = Path(wasm_file)

wasm_objdump_bin = preflight_check(wabt_home)
import_section = collect_import_section_content(wasm_objdump_bin, wasm_file)
name_section = collect_name_section_content(wasm_objdump_bin, wasm_file)

replace_function_name(import_section, name_section, folded, folded + ".translated")


if __name__ == "__main__":
argparse = argparse.ArgumentParser()
argparse.add_argument(
"--folded", help="stackcollapse-perf.pl generated, like out.folded"
)
argparse.add_argument("wasm_file", help="wasm file")
argparse.add_argument("--wabt_home", help="wabt home, like /opt/wabt-1.0.33")

args = argparse.parse_args()
main(args.wabt_home, args.wasm_file, args.folded)