From 9dee211c27d6009764a689f46c4ba6bdfc464eae Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Wed, 23 Oct 2024 21:44:42 +0200 Subject: [PATCH] build(core): emulator valgrind support [no changelog] --- core/emu.py | 38 +++++++++++++++++------------- docs/SUMMARY.md | 1 + docs/core/emulator/valgrind.md | 43 ++++++++++++++++++++++++++++++++++ shell.nix | 1 + 4 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 docs/core/emulator/valgrind.md diff --git a/core/emu.py b/core/emu.py index dd183fe9c1..0cf88a6ca9 100755 --- a/core/emu.py +++ b/core/emu.py @@ -67,27 +67,31 @@ def watch_emulator(emulator: CoreEmulator) -> int: return 0 -def run_debugger(emulator: CoreEmulator, gdb_script_file: str | Path | None) -> None: +def run_debugger(emulator: CoreEmulator, gdb_script_file: str | Path | None, valgrind: bool = False, run_command: list[str] = []) -> None: os.chdir(emulator.workdir) env = emulator.make_env() - if platform.system() == "Darwin": + if valgrind: + dbg_command = ["valgrind", "-v", "--tool=callgrind", "--read-inline-info=yes", str(emulator.executable)] + emulator.make_args() + elif platform.system() == "Darwin": env["PATH"] = "/usr/bin" - os.execvpe( - "lldb", - ["lldb", "-f", str(emulator.executable), "--"] + emulator.make_args(), - env, - ) + dbg_command = ["lldb", "-f", str(emulator.executable), "--"] + emulator.make_args() else: # Optionally run a gdb script from a file if gdb_script_file is None: - gdb = ["gdb"] + dbg_command = ["gdb"] else: - gdb = ["gdb", "-x", str(HERE / gdb_script_file)] - os.execvpe( - "gdb", - gdb + ["--args", str(emulator.executable)] + emulator.make_args(), - env, - ) + dbg_command = ["gdb", "-x", str(HERE / gdb_script_file)] + dbg_command += ["--args", str(emulator.executable)] + dbg_command += emulator.make_args() + + if not run_command: + os.execvpe(dbg_command[0], dbg_command, env) + else: + dbg_process = subprocess.Popen(dbg_command, env=env) + run_process = subprocess.Popen(run_command, env=env, shell=True) + rc = run_process.wait() + dbg_process.send_signal(signal.SIGINT) + sys.exit(rc) def _from_env(name: str) -> bool: @@ -118,6 +122,7 @@ def _from_env(name: str) -> bool: @click.option("-r", "--record-dir", help="Directory where to record screen changes") @click.option("-s", "--slip0014", is_flag=True, help="Initialize device with SLIP-14 seed (all all all...)") @click.option("-S", "--script-gdb-file", type=click.Path(exists=True, dir_okay=False), help="Run gdb with an init file") +@click.option("-V", "--valgrind", is_flag=True, help="Use valgrind instead of debugger (-D)") @click.option("-t", "--temporary-profile", is_flag=True, help="Create an empty temporary profile") @click.option("-w", "--watch", is_flag=True, help="Restart emulator if sources change") @click.option("-X", "--extra-arg", "extra_args", multiple=True, help="Extra argument to pass to micropython") @@ -144,6 +149,7 @@ def cli( record_dir: Optional[str], slip0014: bool, script_gdb_file: str | Path | None, + valgrind: bool, temporary_profile: bool, watch: bool, extra_args: list[str], @@ -261,8 +267,8 @@ def cli( if alloc_profiling: os.environ["TREZOR_MEMPERF"] = "1" - if debugger: - run_debugger(emulator, script_gdb_file) + if debugger or valgrind: + run_debugger(emulator, script_gdb_file, valgrind, command) raise RuntimeError("run_debugger should not return") emulator.start() diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b4dee6579c..9e2f77059c 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -6,6 +6,7 @@ - [Embedded](core/build/embedded.md) - [Emulator](core/build/emulator.md) - [Emulator](core/emulator/index.md) + - [Valgrind profiling](core/emulator/valgrind.md) - [Event Loop](core/src/event-loop.md) - [Apps](core/src/apps.md) - [Tests](core/tests/index.md) diff --git a/docs/core/emulator/valgrind.md b/docs/core/emulator/valgrind.md new file mode 100644 index 0000000000..05ad6b3a23 --- /dev/null +++ b/docs/core/emulator/valgrind.md @@ -0,0 +1,43 @@ +# Profiling emulator with Valgrind + +Sometimes, it can be helpful to know which parts of your code take most of the CPU time. +[Callgrind](https://valgrind.org/docs/manual/cl-manual.html) tool from the [Valgrind](https://valgrind.org/) +instrumentation framework can generate profiling data for a run of Trezor emulator. These can then be visualized +with [KCachegrind](https://kcachegrind.github.io/). + +Bear in mind that profiling the emulator is of very limited usefulness due to: +* different CPU architecture, +* different/mocked drivers, +* & other differences from actual hardware. +Still, it might be a way to get *some* insight without a [hardware debugger](../systemview/index.md) +and a development board. + +Valgrind also currently doesn't understand MicroPython call stack so it won't help you when your code is spending +a lot of time in pure python functions that don't call out to C. It might be possible to instrument trezor-core +so that Valgrind is aware of MicroPython stack frames. + +## Build + +``` +make build_unix_frozen TREZOR_EMULATOR_DEBUGGABLE=1 ADDRESS_SANITIZER=0 +``` + +With `PYOPT=0`, most of the execution time is spent formatting and writing logs, so it is recommended to use +`PYOPT=1` (and lose DebugLink) or get rid of logging manually. + +## Run + +If you're using Nix, you can use Valgrind and KCachegrind packages from our `shell.nix`: +``` +nix-shell --args devTools true --run "poetry shell" +``` + +Record profiling data on some device tests: +``` +./emu.py -a --debugger --valgrind -c 'sleep 10; pytest ../../tests/device_tests/ -v --other-pytest-args...' +``` + +Open profiling data in KCachegrind (file suffix is different for each emulator process): +``` +kcachegrind src/callgrind.out.$PID +``` diff --git a/shell.nix b/shell.nix index c9478c8e74..2c9f5cd238 100644 --- a/shell.nix +++ b/shell.nix @@ -164,6 +164,7 @@ stdenvNoCC.mkDerivation ({ ] ++ lib.optionals devTools [ shellcheck openocd-stm + kcachegrind ] ++ lib.optionals (devTools && !stdenv.isDarwin) [ gdb ] ++ lib.optionals (devTools && acceptJlink) [