Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
gh-98894: Fix dtrace tests in shared builds
Generate SystemTap probe definitions targeting libpython for shared
builds and use centralized USDT probe object discovery for readelf
and BPFTrace.
  • Loading branch information
stratakis committed Jul 9, 2026
commit 90f8699e08bac640074a678b01a3165648e9f95c
7 changes: 4 additions & 3 deletions Lib/test/dtracedata/call_stack.stp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function basename:string(path:string)
return last_token;
}

probe process.mark("function__entry")
probe @PYTHON_SYSTEMTAP_PROBE@("function__entry")
{
funcname = user_string($arg2);

Expand All @@ -19,7 +19,8 @@ probe process.mark("function__entry")
}
}

probe process.mark("function__entry"), process.mark("function__return")
probe @PYTHON_SYSTEMTAP_PROBE@("function__entry"),
@PYTHON_SYSTEMTAP_PROBE@("function__return")
{
filename = user_string($arg1);
funcname = user_string($arg2);
Expand All @@ -31,7 +32,7 @@ probe process.mark("function__entry"), process.mark("function__return")
}
}

probe process.mark("function__return")
probe @PYTHON_SYSTEMTAP_PROBE@("function__return")
{
funcname = user_string($arg2);

Expand Down
7 changes: 4 additions & 3 deletions Lib/test/dtracedata/gc.stp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
global tracing

probe process.mark("function__entry")
probe @PYTHON_SYSTEMTAP_PROBE@("function__entry")
{
funcname = user_string($arg2);

Expand All @@ -9,14 +9,15 @@ probe process.mark("function__entry")
}
}

probe process.mark("gc__start"), process.mark("gc__done")
probe @PYTHON_SYSTEMTAP_PROBE@("gc__start"),
@PYTHON_SYSTEMTAP_PROBE@("gc__done")
{
if (tracing) {
printf("%d\t%s:%ld\n", gettimeofday_us(), $$name, $arg1);
}
}

probe process.mark("function__return")
probe @PYTHON_SYSTEMTAP_PROBE@("function__return")
{
funcname = user_string($arg2);

Expand Down
90 changes: 66 additions & 24 deletions Lib/test/test_dtrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import subprocess
import sys
import sysconfig
import tempfile
import types
import unittest

Expand All @@ -25,6 +26,31 @@ def abspath(filename):
return os.path.abspath(findfile(filename, subdir="dtracedata"))


def get_probe_binary():
binary = sys.executable
if sysconfig.get_config_var("Py_ENABLE_SHARED"):
lib_dir = sysconfig.get_config_var("LIBDIR")
if not lib_dir or sysconfig.is_python_build():
lib_dir = os.path.abspath(os.path.dirname(sys.executable))

lib_names = []
for name in (
sysconfig.get_config_var("INSTSONAME"),
sysconfig.get_config_var("LDLIBRARY"),
):
if name and name not in lib_names:
lib_names.append(name)

if lib_dir:
for name in lib_names:
libpython_path = os.path.join(lib_dir, name)
if os.path.exists(libpython_path):
binary = libpython_path
break

return binary


def normalize_trace_output(output):
"""Normalize DTrace output for comparison.

Expand Down Expand Up @@ -180,6 +206,43 @@ class DTraceBackend(TraceBackend):
class SystemTapBackend(TraceBackend):
EXTENSION = ".stp"
COMMAND = ["stap", "-g"]
PROBE_PLACEHOLDER = "@PYTHON_SYSTEMTAP_PROBE@"

@staticmethod
def _quote_systemtap_string(value):
Comment thread
vstinner marked this conversation as resolved.
Outdated
return value.replace("\\", "\\\\").replace('"', '\\"')

def _python_probe(self):
executable = self._quote_systemtap_string(sys.executable)
probe_binary = get_probe_binary()
if probe_binary != sys.executable:
probe_binary = self._quote_systemtap_string(probe_binary)
return f'process("{executable}").library("{probe_binary}").mark'
return f'process("{executable}").mark'
Comment thread
vstinner marked this conversation as resolved.
Outdated

def _render_script(self, script_file):
with open(script_file) as script:
return script.read().replace(
self.PROBE_PLACEHOLDER, self._python_probe()
)
Comment thread
vstinner marked this conversation as resolved.
Outdated

def trace(self, script_file, subcommand=None, *, timeout=None,
check_returncode=False):
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", suffix=self.EXTENSION, delete=False
) as script:
script.write(self._render_script(script_file))
generated_script_file = script.name

try:
return super().trace(
generated_script_file,
subcommand,
timeout=timeout,
check_returncode=check_returncode,
)
finally:
os.unlink(generated_script_file)
Comment thread
vstinner marked this conversation as resolved.
Outdated


class BPFTraceBackend(TraceBackend):
Expand Down Expand Up @@ -273,7 +336,7 @@ def run_case(self, name, optimize_python=None):
python_flags.extend(["-O"] * optimize_python)

subcommand = [sys.executable] + python_flags + [python_file]
program = self.PROGRAMS[name].format(python=sys.executable)
program = self.PROGRAMS[name].format(python=get_probe_binary())

try:
proc = create_process_group(
Expand Down Expand Up @@ -312,7 +375,7 @@ def run_case(self, name, optimize_python=None):

def assert_usable(self):
# Check if bpftrace is available and can attach to USDT probes
program = f'usdt:{sys.executable}:python:function__entry {{ printf("probe: success\\n"); exit(); }}'
program = f'usdt:{get_probe_binary()}:python:function__entry {{ printf("probe: success\\n"); exit(); }}'
try:
proc = create_process_group(
["bpftrace", "-e", program, "-c",
Expand Down Expand Up @@ -455,28 +518,7 @@ def get_readelf_version():
return int(match.group(1)), int(match.group(2))

def get_readelf_output(self):
binary = sys.executable
if sysconfig.get_config_var("Py_ENABLE_SHARED"):
lib_dir = sysconfig.get_config_var("LIBDIR")
if not lib_dir or sysconfig.is_python_build():
lib_dir = os.path.abspath(os.path.dirname(sys.executable))

lib_names = []
for name in (
sysconfig.get_config_var("INSTSONAME"),
sysconfig.get_config_var("LDLIBRARY"),
):
if name and name not in lib_names:
lib_names.append(name)

if lib_dir:
for name in lib_names:
libpython_path = os.path.join(lib_dir, name)
if os.path.exists(libpython_path):
binary = libpython_path
break

return run_readelf(["readelf", "-n", binary])
return run_readelf(["readelf", "-n", get_probe_binary()])

def test_check_probes(self):
readelf_output = self.get_readelf_output()
Expand Down