Skip to content

Commit fe0dbfc

Browse files
committed
test(callgrind): add Python regression test for runtime obj-skip
End-to-end test for CALLGRIND_ADD_OBJ_SKIP using the canonical flow: start with --instr-atstart=no, register the libpython path via the trapdoor, then turn instrumentation on so the skip applies from the first instrumented BB.
1 parent 9aff0c3 commit fe0dbfc

6 files changed

Lines changed: 115 additions & 0 deletions

callgrind/tests/Makefile.am

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ EXTRA_DIST = \
1010
ann1.post.exp ann1.stderr.exp ann1.vgtest \
1111
ann2.post.exp ann2.stderr.exp ann2.vgtest \
1212
clreq.vgtest clreq.stderr.exp \
13+
runtime_obj_skip_py.vgtest runtime_obj_skip_py.stderr.exp runtime_obj_skip_py.post.exp \
14+
runtime_obj_skip_py.py runtime_obj_skip_py_shim.c \
1315
bug497723.stderr.exp bug497723.post.exp bug497723.vgtest \
1416
simwork1.vgtest simwork1.stdout.exp simwork1.stderr.exp \
1517
simwork2.vgtest simwork2.stdout.exp simwork2.stderr.exp \
@@ -38,3 +40,13 @@ inline_samefile_CFLAGS = $(AM_CFLAGS) -O2 -g
3840
inline_crossfile_CFLAGS = $(AM_CFLAGS) -O2 -g
3941

4042
threads_LDADD = -lpthread
43+
44+
# Shim loaded by runtime_obj_skip_py.py via ctypes. Built unconditionally;
45+
# the test's prereq skips it if the .so is missing.
46+
check_DATA = runtime_obj_skip_py_shim.so
47+
48+
runtime_obj_skip_py_shim.so: runtime_obj_skip_py_shim.c
49+
$(CC) -shared -fPIC -O2 -I$(top_srcdir) -I$(top_srcdir)/include \
50+
$< -o $@
51+
52+
CLEANFILES = runtime_obj_skip_py_shim.so
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
OK
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Resolve libpython at runtime via sysconfig (mirrors pytest-codspeed's
2+
approach in instruments/walltime.py), register it for obj-skip via the
3+
client-request shim, then turn instrumentation on and run a small
4+
integer workload.
5+
6+
We pass both the sysconfig path AND its os.path.realpath because
7+
callgrind stores the realpath in obj_node->name (after symlink
8+
resolution), and the runtime obj-skip check uses exact strcmp."""
9+
10+
import ctypes
11+
import os
12+
import sys
13+
import sysconfig
14+
15+
16+
def libpython_candidates() -> list[str]:
17+
ldlibrary = sysconfig.get_config_var("LDLIBRARY")
18+
libdir = sysconfig.get_config_var("LIBDIR")
19+
paths: list[str] = []
20+
if ldlibrary and libdir:
21+
paths.append(os.path.join(libdir, ldlibrary))
22+
if ldlibrary:
23+
paths.append(os.path.join(sys.prefix, "lib", ldlibrary))
24+
# Add realpath variants so the exact-match obj-skip finds the
25+
# file under whichever name the loader actually mapped.
26+
resolved: list[str] = []
27+
seen: set[str] = set()
28+
for p in paths:
29+
if not p:
30+
continue
31+
if p not in seen and os.path.exists(p):
32+
resolved.append(p)
33+
seen.add(p)
34+
try:
35+
r = os.path.realpath(p)
36+
except OSError:
37+
continue
38+
if r not in seen and os.path.exists(r):
39+
resolved.append(r)
40+
seen.add(r)
41+
return resolved
42+
43+
44+
def main() -> None:
45+
here = os.path.dirname(os.path.abspath(__file__))
46+
shim = ctypes.CDLL(os.path.join(here, "runtime_obj_skip_py_shim.so"))
47+
shim.add_obj_skip.argtypes = [ctypes.c_char_p]
48+
shim.add_obj_skip.restype = None
49+
shim.start_instr.argtypes = []
50+
shim.start_instr.restype = None
51+
shim.stop_instr.argtypes = []
52+
shim.stop_instr.restype = None
53+
54+
for path in libpython_candidates():
55+
shim.add_obj_skip(path.encode())
56+
if sys.executable:
57+
shim.add_obj_skip(sys.executable.encode())
58+
real = os.path.realpath(sys.executable)
59+
if real != sys.executable:
60+
shim.add_obj_skip(real.encode())
61+
62+
shim.start_instr()
63+
acc = 0
64+
for i in range(10_000):
65+
acc = (acc + i * i) ^ (i << 1)
66+
shim.stop_instr()
67+
68+
69+
if __name__ == "__main__":
70+
main()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
3+
Events : Ir
4+
Collected :
5+
6+
I refs:
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
prereq: command -v python3 >/dev/null && test -f runtime_obj_skip_py_shim.so
2+
prog-asis: python3
3+
args: runtime_obj_skip_py.py
4+
vgopts: --instr-atstart=no --compress-strings=no --callgrind-out-file=callgrind.out.runtime_obj_skip_py
5+
post: sh -c 'c=$(awk "/^ob=/{p=(\$0~/libpython/)} /^fn=/&&p{c++} END{print c+0}" callgrind.out.runtime_obj_skip_py); if [ "$c" -lt 100 ]; then echo OK; else echo "FAIL libpython fns=$c"; fi'
6+
cleanup: rm -f callgrind.out.runtime_obj_skip_py
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* Shim so Python (via ctypes) can issue callgrind client requests.
2+
The requests themselves are inline asm and unreachable from pure
3+
Python; this file just wraps them in regular C functions. */
4+
5+
#include "../callgrind.h"
6+
7+
void add_obj_skip(const char* path)
8+
{
9+
CALLGRIND_ADD_OBJ_SKIP(path);
10+
}
11+
12+
void start_instr(void)
13+
{
14+
CALLGRIND_START_INSTRUMENTATION;
15+
}
16+
17+
void stop_instr(void)
18+
{
19+
CALLGRIND_STOP_INSTRUMENTATION;
20+
}

0 commit comments

Comments
 (0)