Skip to content

Commit 97cf4dc

Browse files
committed
wip: sysmon-branch-taken
1 parent de77e14 commit 97cf4dc

File tree

4 files changed

+95
-104
lines changed

4 files changed

+95
-104
lines changed

coverage/core.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ def __init__(self, warn: TWarnFn, config: CoverageConfig, metacov: bool) -> None
6464
warn("sys.monitoring isn't available, using default core", slug="no-sysmon")
6565
core_name = None
6666

67+
if core_name == "sysmon" and config.branch and not env.PYBEHAVIOR.branch_taken:
68+
warn(
69+
"sys.monitoring can't yet measure branches well, using default core",
70+
slug="no-sysmon",
71+
)
72+
core_name = None
73+
6774
if not core_name:
6875
# Once we're comfortable with sysmon as a default:
6976
# if env.PYBEHAVIOR.pep669 and self.should_start_context is None:

coverage/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ class PYBEHAVIOR:
162162
# Does sys.monitoring support BRANCH_TAKEN?
163163
branch_taken = (
164164
pep669 and
165-
hasattr(sys.monitoring.events, "BRANCH_TAKEN") # type:ignore[attr-defined]
165+
hasattr(sys.monitoring.events, "BRANCH_TAKEN") # type:ignore[attr-defined,unused-ignore]
166166
)
167167

168168

coverage/sysmon.py

Lines changed: 80 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@
1616
import traceback
1717

1818
from dataclasses import dataclass
19-
from types import CodeType, FrameType
19+
from types import CodeType
2020
from typing import (
2121
Any,
2222
Callable,
23-
TYPE_CHECKING,
23+
NewType,
24+
Optional,
2425
cast,
2526
)
2627

@@ -44,17 +45,16 @@
4445

4546
# pylint: disable=unused-argument
4647

47-
LOG = False
48+
# $set_env.py: COVERAGE_LOG_SYSMON - Log sys.monitoring activity
49+
LOG = bool(int(os.getenv("COVERAGE_LOG_SYSMON", 0)))
4850

4951
# This module will be imported in all versions of Python, but only used in 3.12+
5052
# It will be type-checked for 3.12, but not for earlier versions.
5153
sys_monitoring = getattr(sys, "monitoring", None)
5254

53-
if TYPE_CHECKING:
54-
assert sys_monitoring is not None
55-
# I want to say this but it's not allowed:
56-
# MonitorReturn = Literal[sys.monitoring.DISABLE] | None
57-
MonitorReturn = Any
55+
DISABLE_TYPE = NewType("DISABLE_TYPE", object)
56+
MonitorReturn = Optional[DISABLE_TYPE]
57+
DISABLE = cast(MonitorReturn, getattr(sys_monitoring, "DISABLE", None))
5858

5959

6060
if LOG: # pragma: debugging
@@ -77,7 +77,10 @@ def _wrapped(*args: Any, **kwargs: Any) -> Any:
7777
assert sys_monitoring is not None
7878

7979
short_stack = functools.partial(
80-
short_stack, full=True, short_filenames=True, frame_ids=True,
80+
short_stack,
81+
full=True,
82+
short_filenames=True,
83+
frame_ids=True,
8184
)
8285
seen_threads: set[int] = set()
8386

@@ -131,7 +134,9 @@ def _wrapped(self: Any, *args: Any) -> Any:
131134
return ret
132135
except Exception as exc:
133136
log(f"!!{exc.__class__.__name__}: {exc}")
134-
log("".join(traceback.format_exception(exc))) # pylint: disable=[no-value-for-parameter]
137+
if 1:
138+
# pylint: disable=no-value-for-parameter
139+
log("".join(traceback.format_exception(exc)))
135140
try:
136141
assert sys_monitoring is not None
137142
sys_monitoring.set_events(sys.monitoring.COVERAGE_ID, 0)
@@ -205,7 +210,6 @@ def __init__(self, tool_id: int) -> None:
205210
# A list of code_objects, just to keep them alive so that id's are
206211
# useful as identity.
207212
self.code_objects: list[CodeType] = []
208-
self.last_lines: dict[FrameType, int] = {}
209213
# Map id(code_object) -> code_object
210214
self.local_event_codes: dict[int, CodeType] = {}
211215
self.sysmon_on = False
@@ -231,20 +235,20 @@ def start(self) -> None:
231235
assert sys_monitoring is not None
232236
sys_monitoring.use_tool_id(self.myid, "coverage.py")
233237
register = functools.partial(sys_monitoring.register_callback, self.myid)
234-
events = sys_monitoring.events
238+
events = sys.monitoring.events
239+
import contextlib
240+
241+
with open("/tmp/foo.out", "a") as f:
242+
with contextlib.redirect_stdout(f):
243+
print(f"{events = }")
244+
sys_monitoring.set_events(self.myid, events.PY_START)
245+
register(events.PY_START, self.sysmon_py_start)
235246
if self.trace_arcs:
236-
sys_monitoring.set_events(
237-
self.myid,
238-
events.PY_START | events.PY_UNWIND,
239-
)
240-
register(events.PY_START, self.sysmon_py_start)
241-
register(events.PY_RESUME, self.sysmon_py_resume_arcs)
242-
register(events.PY_RETURN, self.sysmon_py_return_arcs)
243-
register(events.PY_UNWIND, self.sysmon_py_unwind_arcs)
247+
register(events.PY_RETURN, self.sysmon_py_return)
244248
register(events.LINE, self.sysmon_line_arcs)
249+
register(events.BRANCH_TAKEN, self.sysmon_branch_taken)
250+
register(events.BRANCH_NOT_TAKEN, self.sysmon_branch_not_taken)
245251
else:
246-
sys_monitoring.set_events(self.myid, events.PY_START)
247-
register(events.PY_START, self.sysmon_py_start)
248252
register(events.LINE, self.sysmon_line_lines)
249253
sys_monitoring.restart_events()
250254
self.sysmon_on = True
@@ -282,23 +286,10 @@ def get_stats(self) -> dict[str, int] | None:
282286
"""Return a dictionary of statistics, or None."""
283287
return None
284288

285-
# The number of frames in callers_frame takes @panopticon into account.
286-
if LOG:
287-
288-
def callers_frame(self) -> FrameType:
289-
"""Get the frame of the Python code we're monitoring."""
290-
return (
291-
inspect.currentframe().f_back.f_back.f_back # type: ignore[union-attr,return-value]
292-
)
293-
294-
else:
295-
296-
def callers_frame(self) -> FrameType:
297-
"""Get the frame of the Python code we're monitoring."""
298-
return inspect.currentframe().f_back.f_back # type: ignore[union-attr,return-value]
299-
300289
@panopticon("code", "@")
301-
def sysmon_py_start(self, code: CodeType, instruction_offset: int) -> MonitorReturn:
290+
def sysmon_py_start( # pylint: disable=useless-return
291+
self, code: CodeType, instruction_offset: int
292+
) -> MonitorReturn:
302293
"""Handle sys.monitoring.events.PY_START events."""
303294
# Entering a new frame. Decide if we should trace in this file.
304295
self._activity = True
@@ -350,91 +341,78 @@ def sysmon_py_start(self, code: CodeType, instruction_offset: int) -> MonitorRet
350341
with self.lock:
351342
if self.sysmon_on:
352343
assert sys_monitoring is not None
353-
sys_monitoring.set_local_events(
354-
self.myid,
355-
code,
356-
events.PY_RETURN
357-
#
358-
| events.PY_RESUME
359-
# | events.PY_YIELD
360-
| events.LINE
361-
| events.BRANCH_TAKEN
362-
| events.BRANCH_NOT_TAKEN
363-
# | events.JUMP
364-
)
344+
local_events = events.PY_RETURN | events.PY_RESUME | events.LINE
345+
if self.trace_arcs:
346+
assert env.PYBEHAVIOR.branch_taken
347+
local_events |= (
348+
events.BRANCH_TAKEN | events.BRANCH_NOT_TAKEN
349+
)
350+
sys_monitoring.set_local_events(self.myid, code, local_events)
365351
self.local_event_codes[id(code)] = code
366352

367-
if tracing_code and self.trace_arcs:
368-
frame = self.callers_frame()
369-
self.last_lines[frame] = -code.co_firstlineno
370-
return None
371-
else:
372-
return sys.monitoring.DISABLE
373-
374-
@panopticon("code", "@")
375-
def sysmon_py_resume_arcs(
376-
self, code: CodeType, instruction_offset: int,
377-
) -> MonitorReturn:
378-
"""Handle sys.monitoring.events.PY_RESUME events for branch coverage."""
379-
frame = self.callers_frame()
380-
self.last_lines[frame] = frame.f_lineno
353+
return None
381354

382355
@panopticon("code", "@", None)
383-
def sysmon_py_return_arcs(
384-
self, code: CodeType, instruction_offset: int, retval: object,
356+
def sysmon_py_return( # pylint: disable=useless-return
357+
self,
358+
code: CodeType,
359+
instruction_offset: int,
360+
retval: object,
385361
) -> MonitorReturn:
386362
"""Handle sys.monitoring.events.PY_RETURN events for branch coverage."""
387-
frame = self.callers_frame()
388363
code_info = self.code_infos.get(id(code))
389364
if code_info is not None and code_info.file_data is not None:
390-
last_line = self.last_lines.get(frame)
365+
assert code_info.byte_to_line is not None
366+
last_line = code_info.byte_to_line[instruction_offset]
391367
if last_line is not None:
392368
arc = (last_line, -code.co_firstlineno)
393-
# log(f"adding {arc=}")
394369
cast(set[TArc], code_info.file_data).add(arc)
395-
396-
# Leaving this function, no need for the frame any more.
397-
self.last_lines.pop(frame, None)
398-
399-
@panopticon("code", "@", "exc")
400-
def sysmon_py_unwind_arcs(
401-
self, code: CodeType, instruction_offset: int, exception: BaseException,
402-
) -> MonitorReturn:
403-
"""Handle sys.monitoring.events.PY_UNWIND events for branch coverage."""
404-
frame = self.callers_frame()
405-
# Leaving this function.
406-
last_line = self.last_lines.pop(frame, None)
407-
if isinstance(exception, GeneratorExit):
408-
# We don't want to count generator exits as arcs.
409-
return
410-
code_info = self.code_infos.get(id(code))
411-
if code_info is not None and code_info.file_data is not None:
412-
if last_line is not None:
413-
arc = (last_line, -code.co_firstlineno)
414-
# log(f"adding {arc=}")
415-
cast(set[TArc], code_info.file_data).add(arc)
416-
370+
log(f"adding {arc=}")
371+
return None
417372

418373
@panopticon("code", "line")
419374
def sysmon_line_lines(self, code: CodeType, line_number: int) -> MonitorReturn:
420375
"""Handle sys.monitoring.events.LINE events for line coverage."""
421376
code_info = self.code_infos[id(code)]
422377
if code_info.file_data is not None:
423378
cast(set[TLineNo], code_info.file_data).add(line_number)
424-
# log(f"adding {line_number=}")
425-
return sys.monitoring.DISABLE
379+
log(f"adding {line_number=}")
380+
return DISABLE
426381

427382
@panopticon("code", "line")
428383
def sysmon_line_arcs(self, code: CodeType, line_number: int) -> MonitorReturn:
429384
"""Handle sys.monitoring.events.LINE events for branch coverage."""
430385
code_info = self.code_infos[id(code)]
431-
ret = None
432386
if code_info.file_data is not None:
433-
frame = self.callers_frame()
434-
last_line = self.last_lines.get(frame)
435-
if last_line is not None:
436-
arc = (last_line, line_number)
437-
cast(set[TArc], code_info.file_data).add(arc)
438-
# log(f"adding {arc=}")
439-
self.last_lines[frame] = line_number
440-
return ret
387+
arc = (line_number, line_number)
388+
cast(set[TArc], code_info.file_data).add(arc)
389+
log(f"adding {arc=}")
390+
return DISABLE
391+
392+
@panopticon("code", "@", "@")
393+
def sysmon_branch_taken(
394+
self, code: CodeType, instruction_offset: int, destination_offset: int
395+
) -> MonitorReturn:
396+
"""Handed BRANCH_TAKEN and BRANCH_NOT_TAKEN events."""
397+
code_info = self.code_infos[id(code)]
398+
if code_info.file_data is not None:
399+
b2l = code_info.byte_to_line
400+
assert b2l is not None
401+
arc = (b2l[instruction_offset], b2l[destination_offset])
402+
cast(set[TArc], code_info.file_data).add(arc)
403+
log(f"adding {arc=}")
404+
return DISABLE
405+
406+
@panopticon("code", "@", "@")
407+
def sysmon_branch_not_taken(
408+
self, code: CodeType, instruction_offset: int, destination_offset: int
409+
) -> MonitorReturn:
410+
"""Handed BRANCH_TAKEN and BRANCH_NOT_TAKEN events."""
411+
code_info = self.code_infos[id(code)]
412+
if code_info.file_data is not None:
413+
b2l = code_info.byte_to_line
414+
assert b2l is not None
415+
arc = (b2l[instruction_offset], b2l[destination_offset])
416+
cast(set[TArc], code_info.file_data).add(arc)
417+
log(f"adding {arc=}")
418+
return DISABLE

tests/test_concurrency.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,13 +554,19 @@ def test_multiprocessing_with_branching(self, start_method: str) -> None:
554554
code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
555555
total = sum(x*x if x%2 else x*x*x for x in range(upto))
556556
expected_out = f"{nprocs} pids, total = {total}"
557+
expect_warn = (
558+
env.PYBEHAVIOR.pep669
559+
and (not env.PYBEHAVIOR.branch_taken)
560+
and testenv.SYS_MON
561+
)
557562
self.make_file("multi.py", code)
558563
self.make_file("multi.rc", """\
559564
[run]
560565
concurrency = multiprocessing
561566
branch = True
562567
omit = */site-packages/*
563-
""")
568+
""" + ("disable_warnings = no-sysmon" if expect_warn else "")
569+
)
564570

565571
out = self.run_command(f"coverage run --rcfile=multi.rc multi.py {start_method}")
566572
assert out.rstrip() == expected_out

0 commit comments

Comments
 (0)