Skip to content

gh-103615: Use local events for opcode tracing #109472

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

Merged
merged 8 commits into from
Nov 3, 2023
Merged
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
2 changes: 2 additions & 0 deletions Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ PyAPI_FUNC(int) _PyEval_SetProfile(PyThreadState *tstate, Py_tracefunc func, PyO

extern int _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg);

extern int _PyEval_SetOpcodeTrace(PyFrameObject *f, bool enable);

// Helper to look up a builtin object
// Export for 'array' shared extension
PyAPI_FUNC(PyObject*) _PyEval_GetBuiltin(PyObject *);
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_instruments.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ typedef uint32_t _PyMonitoringEventSet;
PyObject *_PyMonitoring_RegisterCallback(int tool_id, int event_id, PyObject *obj);

int _PyMonitoring_SetEvents(int tool_id, _PyMonitoringEventSet events);
int _PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEventSet events);
int _PyMonitoring_GetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEventSet *events);

extern int
_Py_call_instrumentation(PyThreadState *tstate, int event,
Expand Down
1 change: 0 additions & 1 deletion Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ struct _is {
uint32_t next_func_version;

_Py_GlobalMonitors monitors;
bool f_opcode_trace_set;
bool sys_profile_initialized;
bool sys_trace_initialized;
Py_ssize_t sys_profiling_threads; /* Count of threads with c_profilefunc set */
Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_sys_settrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
import asyncio
from test.support import import_helper
import contextlib
import os
import tempfile
import textwrap
import subprocess
import warnings

support.requires_working_socket(module=True)
Expand Down Expand Up @@ -1802,6 +1806,39 @@ def compare_events(self, line_offset, events, expected_events):
def make_tracer():
return Tracer(trace_opcode_events=True)

def test_trace_opcodes_after_settrace(self):
"""Make sure setting f_trace_opcodes after starting trace works even
if it's the first time f_trace_opcodes is being set. GH-103615"""

code = textwrap.dedent("""
import sys

def opcode_trace_func(frame, event, arg):
if event == "opcode":
print("opcode trace triggered")
return opcode_trace_func

sys.settrace(opcode_trace_func)
sys._getframe().f_trace = opcode_trace_func
sys._getframe().f_trace_opcodes = True
a = 1
""")

# We can't use context manager because Windows can't execute a file while
# it's being written
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.py')
tmp.write(code.encode('utf-8'))
tmp.close()
try:
p = subprocess.Popen([sys.executable, tmp.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait()
out = p.stdout.read()
finally:
os.remove(tmp.name)
p.stdout.close()
p.stderr.close()
self.assertIn(b"opcode trace triggered", out)


class RaisingTraceFuncTestCase(unittest.TestCase):
def setUp(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use local events for opcode tracing
8 changes: 7 additions & 1 deletion Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,13 @@ frame_settrace_opcodes(PyFrameObject *f, PyObject* value, void *Py_UNUSED(ignore
}
if (value == Py_True) {
f->f_trace_opcodes = 1;
_PyInterpreterState_GET()->f_opcode_trace_set = true;
if (f->f_trace) {
return _PyEval_SetOpcodeTrace(f, true);
}
}
else {
f->f_trace_opcodes = 0;
return _PyEval_SetOpcodeTrace(f, false);
}
return 0;
}
Expand Down Expand Up @@ -842,6 +845,9 @@ frame_settrace(PyFrameObject *f, PyObject* v, void *closure)
}
if (v != f->f_trace) {
Py_XSETREF(f->f_trace, Py_XNewRef(v));
if (v != NULL && f->f_trace_opcodes) {
return _PyEval_SetOpcodeTrace(f, true);
}
}
return 0;
}
Expand Down
17 changes: 17 additions & 0 deletions Python/instrumentation.c
Original file line number Diff line number Diff line change
Expand Up @@ -1833,6 +1833,23 @@ _PyMonitoring_SetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEvent
return 0;
}

int
_PyMonitoring_GetLocalEvents(PyCodeObject *code, int tool_id, _PyMonitoringEventSet *events)
{
assert(0 <= tool_id && tool_id < PY_MONITORING_TOOL_IDS);
PyInterpreterState *interp = _PyInterpreterState_GET();
if (check_tool(interp, tool_id)) {
return -1;
}
if (code->_co_monitoring == NULL) {
*events = 0;
return 0;
}
_Py_LocalMonitors *local = &code->_co_monitoring->local_monitors;
*events = get_local_events(local, tool_id);
return 0;
}

/*[clinic input]
module monitoring
[clinic start generated code]*/
Expand Down
52 changes: 48 additions & 4 deletions Python/legacy_tracing.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ sys_profile_call_or_return(
Py_RETURN_NONE;
}

int
_PyEval_SetOpcodeTrace(
PyFrameObject *frame,
bool enable
) {
assert(frame != NULL);
assert(PyCode_Check(frame->f_frame->f_executable));

PyCodeObject *code = (PyCodeObject *)frame->f_frame->f_executable;
_PyMonitoringEventSet events = 0;

if (_PyMonitoring_GetLocalEvents(code, PY_MONITORING_SYS_TRACE_ID, &events) < 0) {
return -1;
}

if (enable) {
if (events & (1 << PY_MONITORING_EVENT_INSTRUCTION)) {
return 0;
}
events |= (1 << PY_MONITORING_EVENT_INSTRUCTION);
} else {
if (!(events & (1 << PY_MONITORING_EVENT_INSTRUCTION))) {
return 0;
}
events &= (~(1 << PY_MONITORING_EVENT_INSTRUCTION));
}
return _PyMonitoring_SetLocalEvents(code, PY_MONITORING_SYS_TRACE_ID, events);
}

static PyObject *
call_trace_func(_PyLegacyEventHandler *self, PyObject *arg)
{
Expand All @@ -130,6 +159,12 @@ call_trace_func(_PyLegacyEventHandler *self, PyObject *arg)
"Missing frame when calling trace function.");
return NULL;
}
if (frame->f_trace_opcodes) {
if (_PyEval_SetOpcodeTrace(frame, true) != 0) {
return NULL;
}
}

Py_INCREF(frame);
int err = tstate->c_tracefunc(tstate->c_traceobj, frame, self->event, arg);
Py_DECREF(frame);
Expand Down Expand Up @@ -230,11 +265,14 @@ sys_trace_instruction_func(
"Missing frame when calling trace function.");
return NULL;
}
if (!frame->f_trace_opcodes) {
PyThreadState *tstate = _PyThreadState_GET();
if (!tstate->c_tracefunc || !frame->f_trace_opcodes) {
if (_PyEval_SetOpcodeTrace(frame, false) != 0) {
return NULL;
}
Py_RETURN_NONE;
}
Py_INCREF(frame);
PyThreadState *tstate = _PyThreadState_GET();
int err = tstate->c_tracefunc(tstate->c_traceobj, frame, self->event, Py_None);
frame->f_lineno = 0;
Py_DECREF(frame);
Expand Down Expand Up @@ -531,9 +569,15 @@ _PyEval_SetTrace(PyThreadState *tstate, Py_tracefunc func, PyObject *arg)
(1 << PY_MONITORING_EVENT_PY_UNWIND) | (1 << PY_MONITORING_EVENT_PY_THROW) |
(1 << PY_MONITORING_EVENT_STOP_ITERATION) |
(1 << PY_MONITORING_EVENT_EXCEPTION_HANDLED);
if (tstate->interp->f_opcode_trace_set) {
events |= (1 << PY_MONITORING_EVENT_INSTRUCTION);

PyFrameObject* frame = PyEval_GetFrame();
if (frame->f_trace_opcodes) {
int ret = _PyEval_SetOpcodeTrace(frame, true);
if (ret != 0) {
return ret;
}
}
}

return _PyMonitoring_SetEvents(PY_MONITORING_SYS_TRACE_ID, events);
}
2 changes: 0 additions & 2 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,6 @@ init_interpreter(PyInterpreterState *interp,
/* Fix the self-referential, statically initialized fields. */
interp->dtoa = (struct _dtoa_state)_dtoa_state_INIT(interp);
}
interp->f_opcode_trace_set = false;

interp->_initialized = 1;
return _PyStatus_OK();
Expand Down Expand Up @@ -958,7 +957,6 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
interp->code_watchers[i] = NULL;
}
interp->active_code_watchers = 0;
interp->f_opcode_trace_set = false;
// XXX Once we have one allocator per interpreter (i.e.
// per-interpreter GC) we must ensure that all of the interpreter's
// objects have been cleaned up at the point.
Expand Down