Skip to content
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
48 changes: 48 additions & 0 deletions src/pytest_codspeed/instruments/hooks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Comment thread
not-matthias marked this conversation as resolved.


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)
19 changes: 19 additions & 0 deletions src/pytest_codspeed/instruments/hooks/instrument_hooks_module.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "core.h"
#include "valgrind.h"
Comment thread
art049 marked this conversation as resolved.

/* 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
Comment thread
not-matthias marked this conversation as resolved.

/* Capsule destructor for InstrumentHooks pointer */
static void instrument_hooks_capsule_destructor(PyObject *capsule) {
Expand Down Expand Up @@ -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;
}
Comment thread
not-matthias marked this conversation as resolved.
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,
Expand Down Expand Up @@ -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}
};

Expand Down
Loading