From 3bbabe482458518555fa9939902410ae8cf1baa8 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Thu, 14 May 2026 11:05:52 -0700 Subject: [PATCH] feat: skip Python runtime objects in callgrind Adds a callgrind_add_obj_skip C extension function that emits the Valgrind client request VG_USERREQ__ADD_OBJ_SKIP (0x43540006), and a callgrind_skip_python_runtime() helper that skips libpython and the python executable. Called from InstrumentHooks.__init__ so Python runtime frames stop polluting the callgrind flamegraph (COD-2654). --- .../instruments/hooks/__init__.py | 48 +++++++++++++++++++ .../hooks/instrument_hooks_module.c | 19 ++++++++ 2 files changed, 67 insertions(+) diff --git a/src/pytest_codspeed/instruments/hooks/__init__.py b/src/pytest_codspeed/instruments/hooks/__init__.py index 2e6ffd5..be8e665 100644 --- a/src/pytest_codspeed/instruments/hooks/__init__.py +++ b/src/pytest_codspeed/instruments/hooks/__init__.py @@ -50,6 +50,10 @@ def __init__(self) -> None: if SUPPORTS_PERF_TRAMPOLINE and not sys.is_stack_trampoline_active(): sys.activate_stack_trampoline("perf") # type: ignore + # Ignore libpython and python executable frames in callgrind so they + # don't obfuscate the flamegraph. + callgrind_skip_python_runtime() + def __del__(self): # Don't manually deinit - let the capsule destructor handle it pass @@ -223,3 +227,47 @@ def collect_and_write_python_environment(self) -> None: self.set_environment_list(section, "config", config_items) self.write_environment() + + +def callgrind_add_obj_skip(path: str) -> None: + """Tell callgrind to skip the given object file (and its realpath). + + The actual Valgrind client-request trapdoor lives in the C extension; this + just resolves the realpath so callgrind's strcmp matches either form. + """ + if not path or not os.path.exists(path): + return + try: + from . import dist_instrument_hooks # type: ignore + except ImportError: + return + + dist_instrument_hooks.callgrind_add_obj_skip(path.encode()) + + # The dynamic loader maps the realpath (e.g. libpython3.12.so.1.0), and + # callgrind stores that in obj_node->name. Skip both so the exact strcmp + # matches regardless of which path callgrind sees. + real = os.path.realpath(path) + if real != path: + dist_instrument_hooks.callgrind_add_obj_skip(real.encode()) + + +def callgrind_skip_python_runtime() -> None: + """Skip libpython and the python executable from callgrind measurement.""" + ldlibrary = sysconfig.get_config_var("LDLIBRARY") + libdir = sysconfig.get_config_var("LIBDIR") + libpython = next( + ( + p + for p in ( + os.path.join(libdir, ldlibrary) if ldlibrary and libdir else None, + os.path.join(sys.prefix, "lib", ldlibrary) if ldlibrary else None, + ) + if p and os.path.exists(p) + ), + None, + ) + if libpython: + callgrind_add_obj_skip(libpython) + + callgrind_add_obj_skip(sys.executable) diff --git a/src/pytest_codspeed/instruments/hooks/instrument_hooks_module.c b/src/pytest_codspeed/instruments/hooks/instrument_hooks_module.c index ce205ea..a358d62 100644 --- a/src/pytest_codspeed/instruments/hooks/instrument_hooks_module.c +++ b/src/pytest_codspeed/instruments/hooks/instrument_hooks_module.c @@ -1,6 +1,12 @@ #define PY_SSIZE_T_CLEAN #include #include "core.h" +#include "valgrind.h" + +/* CodSpeed-specific Valgrind client request: tell callgrind to skip an + * object file by path. Not present in upstream callgrind.h. */ +// TODO(COD-2654): Move this to instrument-hooks and just call it here +#define VG_USERREQ__ADD_OBJ_SKIP 0x43540006 /* Capsule destructor for InstrumentHooks pointer */ static void instrument_hooks_capsule_destructor(PyObject *capsule) { @@ -242,6 +248,17 @@ static PyObject *py_instrument_hooks_write_environment(PyObject *self, PyObject return PyLong_FromLong(result); } +/* callgrind_add_obj_skip(path: bytes) -> None + * Outside Valgrind this expands to a nop, so it's safe on bare metal. */ +static PyObject *py_callgrind_add_obj_skip(PyObject *self, PyObject *args) { + const char *path; + if (!PyArg_ParseTuple(args, "y", &path)) { + return NULL; + } + VALGRIND_DO_CLIENT_REQUEST_STMT(VG_USERREQ__ADD_OBJ_SKIP, path, 0, 0, 0, 0); + Py_RETURN_NONE; +} + /* Method definitions */ static PyMethodDef InstrumentHooksMethods[] = { {"instrument_hooks_init", py_instrument_hooks_init, METH_NOARGS, @@ -272,6 +289,8 @@ static PyMethodDef InstrumentHooksMethods[] = { "Register a list of values under a named section for environment collection."}, {"instrument_hooks_write_environment", py_instrument_hooks_write_environment, METH_VARARGS, "Flush all registered environment sections to disk."}, + {"callgrind_add_obj_skip", py_callgrind_add_obj_skip, METH_VARARGS, + "Tell callgrind to skip the given object file path."}, {NULL, NULL, 0, NULL} };