Skip to content
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

Use sys.monitoring for scrutineer on 3.12+ #3776

Merged
merged 14 commits into from
Nov 4, 2023
6 changes: 6 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
RELEASE_TYPE: patch

This patch improves the speed of the explain phase on python 3.12+, by using the new
:mod:`sys.monitoring` module to collect coverage, instead of :obj:`sys.settrace`.

Thanks to Liam DeVoe for :pull:`3776`!
16 changes: 7 additions & 9 deletions hypothesis-python/src/hypothesis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,15 +915,13 @@ def _execute_once_for_engine(self, data):
): # pragma: no cover
# This is in fact covered by our *non-coverage* tests, but due to the
# settrace() contention *not* by our coverage tests. Ah well.
tracer = Tracer()
try:
sys.settrace(tracer.trace)
result = self.execute_once(data)
if data.status == Status.VALID:
self.explain_traces[None].add(frozenset(tracer.branches))
finally:
sys.settrace(None)
trace = frozenset(tracer.branches)
with Tracer() as tracer:
try:
result = self.execute_once(data)
if data.status == Status.VALID:
self.explain_traces[None].add(frozenset(tracer.branches))
finally:
trace = frozenset(tracer.branches)
else:
result = self.execute_once(data)
if result is not None:
Expand Down
48 changes: 46 additions & 2 deletions hypothesis-python/src/hypothesis/internal/scrutineer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# obtain one at https://mozilla.org/MPL/2.0/.

import sys
import types
from collections import defaultdict
from functools import lru_cache, reduce
from os import sep
Expand All @@ -25,6 +26,16 @@ def should_trace_file(fname):
return not (is_hypothesis_file(fname) or fname.startswith("<"))


# where possible, we'll use 3.12's new sys.monitoring module for low-overhead
# coverage instrumentation. Otherwise, we'll default to sys.settrace.
# This can be simplified once we drop 3.11.
# tool_id = 2 is designated for coverage, but we intentionally choose a
# non-reserved tool id so we can co-exist with coverage tools.
MONITORING_TOOL_ID = 3
if sys.version_info[:2] >= (3, 12):
MONITORING_EVENTS = {sys.monitoring.events.LINE: "trace_line"}


class Tracer:
"""A super-simple branch coverage tracer."""

Expand All @@ -38,12 +49,45 @@ def trace(self, frame, event, arg):
if event == "call":
return self.trace
elif event == "line":
fname = frame.f_code.co_filename
code = frame.f_code
line_number = frame.f_lineno

# manual inlining of self.trace_line for performance.
fname = code.co_filename
if should_trace_file(fname):
current_location = (fname, frame.f_lineno)
current_location = (fname, line_number)
self.branches.add((self._previous_location, current_location))
self._previous_location = current_location

def trace_line(self, code: types.CodeType, line_number: int) -> None:
fname = code.co_filename
if should_trace_file(fname):
current_location = (fname, line_number)
self.branches.add((self._previous_location, current_location))
self._previous_location = current_location

def __enter__(self):
if sys.version_info[:2] < (3, 12):
sys.settrace(self.trace)
return self

sys.monitoring.use_tool_id(MONITORING_TOOL_ID, "scrutineer")
for event, callback_name in MONITORING_EVENTS.items():
sys.monitoring.set_events(MONITORING_TOOL_ID, event)
callback = getattr(self, callback_name)
sys.monitoring.register_callback(MONITORING_TOOL_ID, event, callback)

return self

def __exit__(self, *args, **kwargs):
if sys.version_info[:2] < (3, 12):
sys.settrace(None)
return

sys.monitoring.free_tool_id(MONITORING_TOOL_ID)
for event in MONITORING_EVENTS:
sys.monitoring.register_callback(MONITORING_TOOL_ID, event, None)


UNHELPFUL_LOCATIONS = (
# There's a branch which is only taken when an exception is active while exiting
Expand Down
Loading