Skip to content

Cod 732 support perf profiling in python walltime #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -44,6 +46,8 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: astral-sh/setup-uv@v4
with:
version: "0.5.20"
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "tests/benchmarks/TheAlgorithms"]
path = tests/benchmarks/TheAlgorithms
url = [email protected]:TheAlgorithms/Python.git
[submodule "src/pytest_codspeed/instruments/hooks/instrument-hooks"]
path = src/pytest_codspeed/instruments/hooks/instrument-hooks
url = https://github.com/CodSpeedHQ/instrument-hooks
8 changes: 3 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@

from setuptools import setup

build_path = (
Path(__file__).parent / "src/pytest_codspeed/instruments/valgrind/_wrapper/build.py"
)
build_path = Path(__file__).parent / "src/pytest_codspeed/instruments/hooks/build.py"

spec = importlib.util.spec_from_file_location("build", build_path)
assert spec is not None, "The spec should be initialized"
Expand Down Expand Up @@ -52,8 +50,8 @@
setup(
package_data={
"pytest_codspeed": [
"instruments/valgrind/_wrapper/*.h",
"instruments/valgrind/_wrapper/*.c",
"instruments/hooks/instrument-hooks/includes/*.h",
"instruments/hooks/instrument-hooks/dist/*.c",
]
},
ext_modules=(
Expand Down
83 changes: 83 additions & 0 deletions src/pytest_codspeed/instruments/hooks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from __future__ import annotations

import os
import sys
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .dist_instrument_hooks import lib as LibType

SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12)


class InstrumentHooks:
"""Zig library wrapper class providing benchmark measurement functionality."""

lib: LibType
instance: int

def __init__(self) -> None:
if os.environ.get("CODSPEED_ENV") is None:
raise RuntimeError(
"Can't run benchmarks outside of CodSpeed environment."
"Please set the CODSPEED_ENV environment variable."
)

try:
from .dist_instrument_hooks import lib # type: ignore
except ImportError as e:
raise RuntimeError(f"Failed to load instrument hooks library: {e}") from e

instance = lib.instrument_hooks_init()
if instance == 0:
raise RuntimeError("Failed to initialize CodSpeed instrumentation library.")

if SUPPORTS_PERF_TRAMPOLINE:
sys.activate_stack_trampoline("perf") # type: ignore

self.lib = lib
self.instance = instance

def __del__(self):
if hasattr(self, "lib") and hasattr(self, "instance"):
self.lib.instrument_hooks_deinit(self.instance)

def start_benchmark(self) -> None:
"""Start a new benchmark measurement."""
ret = self.lib.instrument_hooks_start_benchmark(self.instance)
if ret != 0:
raise RuntimeError("Failed to start benchmark measurement")

def stop_benchmark(self) -> None:
"""Stop the current benchmark measurement."""
ret = self.lib.instrument_hooks_stop_benchmark(self.instance)
if ret != 0:
raise RuntimeError("Failed to stop benchmark measurement")

def set_executed_benchmark(self, uri: str, pid: int | None = None) -> None:
"""Set the executed benchmark URI and process ID.

Args:
uri: The benchmark URI string identifier
pid: Optional process ID (defaults to current process)
"""
if pid is None:
pid = os.getpid()

ret = self.lib.instrument_hooks_executed_benchmark(
self.instance, pid, uri.encode("ascii")
)
if ret != 0:
raise RuntimeError("Failed to set executed benchmark")

def set_integration(self, name: str, version: str) -> None:
"""Set the integration name and version."""
ret = self.lib.instrument_hooks_set_integration(
self.instance, name.encode("ascii"), version.encode("ascii")
)
if ret != 0:
raise RuntimeError("Failed to set integration name and version")

def is_instrumented(self) -> bool:
"""Check if instrumentation is active."""
return self.lib.instrument_hooks_is_instrumented(self.instance)
26 changes: 26 additions & 0 deletions src/pytest_codspeed/instruments/hooks/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pathlib import Path

from cffi import FFI # type: ignore

ffibuilder = FFI()

includes_dir = Path(__file__).parent.joinpath("instrument-hooks/includes")
header_text = (includes_dir / "core.h").read_text()
filtered_header = "\n".join(
line for line in header_text.splitlines() if not line.strip().startswith("#")
)
ffibuilder.cdef(filtered_header)

ffibuilder.set_source(
"pytest_codspeed.instruments.hooks.dist_instrument_hooks",
"""
#include "core.h"
""",
sources=[
"src/pytest_codspeed/instruments/hooks/instrument-hooks/dist/core.c",
],
include_dirs=[str(includes_dir)],
)

if __name__ == "__main__":
ffibuilder.compile(verbose=True)
1 change: 1 addition & 0 deletions src/pytest_codspeed/instruments/hooks/instrument-hooks
Submodule instrument-hooks added at b003e5
Original file line number Diff line number Diff line change
@@ -1,41 +1,36 @@
from __future__ import annotations

import os
import sys
from typing import TYPE_CHECKING

from pytest_codspeed import __semver_version__
from pytest_codspeed.instruments import Instrument
from pytest_codspeed.instruments.valgrind._wrapper import get_lib
from pytest_codspeed.instruments.hooks import InstrumentHooks

if TYPE_CHECKING:
from typing import Any, Callable

from pytest import Session

from pytest_codspeed.instruments import P, T
from pytest_codspeed.instruments.valgrind._wrapper import LibType
from pytest_codspeed.plugin import CodSpeedConfig

SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12)


class ValgrindInstrument(Instrument):
instrument = "valgrind"
lib: LibType | None
instrument_hooks: InstrumentHooks | None

def __init__(self, config: CodSpeedConfig) -> None:
self.benchmark_count = 0
self.should_measure = os.environ.get("CODSPEED_ENV") is not None
if self.should_measure:
self.lib = get_lib()
self.lib.dump_stats_at(
f"Metadata: pytest-codspeed {__semver_version__}".encode("ascii")
)
if SUPPORTS_PERF_TRAMPOLINE:
sys.activate_stack_trampoline("perf") # type: ignore
else:
self.lib = None
try:
self.instrument_hooks = InstrumentHooks()
self.instrument_hooks.set_integration("pytest-codspeed", __semver_version__)
except RuntimeError:
self.instrument_hooks = None

self.should_measure = self.instrument_hooks is not None

def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]:
config = (
Expand All @@ -61,7 +56,8 @@ def measure(
**kwargs: P.kwargs,
) -> T:
self.benchmark_count += 1
if self.lib is None: # Thus should_measure is False

if not self.instrument_hooks:
return fn(*args, **kwargs)

def __codspeed_root_frame__() -> T:
Expand All @@ -71,14 +67,15 @@ def __codspeed_root_frame__() -> T:
# Warmup CPython performance map cache
__codspeed_root_frame__()

self.lib.zero_stats()
self.lib.start_instrumentation()
# Manually call the library function to avoid an extra stack frame. Also
# call the callgrind markers directly to avoid extra overhead.
self.instrument_hooks.lib.callgrind_start_instrumentation()
try:
return __codspeed_root_frame__()
finally:
# Ensure instrumentation is stopped even if the test failed
self.lib.stop_instrumentation()
self.lib.dump_stats_at(uri.encode("ascii"))
self.instrument_hooks.lib.callgrind_stop_instrumentation()
self.instrument_hooks.set_executed_benchmark(uri)

def report(self, session: Session) -> None:
reporter = session.config.pluginmanager.get_plugin("terminalreporter")
Expand Down
15 changes: 0 additions & 15 deletions src/pytest_codspeed/instruments/valgrind/_wrapper/__init__.py

This file was deleted.

19 changes: 0 additions & 19 deletions src/pytest_codspeed/instruments/valgrind/_wrapper/build.py

This file was deleted.

25 changes: 0 additions & 25 deletions src/pytest_codspeed/instruments/valgrind/_wrapper/wrapper.c

This file was deleted.

6 changes: 0 additions & 6 deletions src/pytest_codspeed/instruments/valgrind/_wrapper/wrapper.h

This file was deleted.

13 changes: 0 additions & 13 deletions src/pytest_codspeed/instruments/valgrind/_wrapper/wrapper.pyi

This file was deleted.

Loading