Skip to content

Commit

Permalink
gh-103865: add monitoring support to LOAD_SUPER_ATTR
Browse files Browse the repository at this point in the history
  • Loading branch information
carljm committed Apr 26, 2023
1 parent ef25feb commit 862c3dc
Show file tree
Hide file tree
Showing 10 changed files with 410 additions and 234 deletions.
4 changes: 2 additions & 2 deletions Include/internal/pycore_opcode.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Include/opcode.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Lib/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,9 @@ def pseudo_op(name, op, real_ops):
def_op('CALL_INTRINSIC_2', 174)

# Instrumented instructions
MIN_INSTRUMENTED_OPCODE = 238
MIN_INSTRUMENTED_OPCODE = 237

def_op('INSTRUMENTED_LOAD_SUPER_ATTR', 237)
def_op('INSTRUMENTED_POP_JUMP_IF_NONE', 238)
def_op('INSTRUMENTED_POP_JUMP_IF_NOT_NONE', 239)
def_op('INSTRUMENTED_RESUME', 240)
Expand Down
100 changes: 97 additions & 3 deletions Lib/test/test_monitoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import functools
import operator
import sys
import textwrap
import types
import unittest

Expand Down Expand Up @@ -506,7 +507,7 @@ def test_lines_single(self):
sys.monitoring.set_events(TEST_TOOL, 0)
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
start = LineMonitoringTest.test_lines_single.__code__.co_firstlineno
self.assertEqual(events, [start+7, 14, start+8])
self.assertEqual(events, [start+7, 15, start+8])
finally:
sys.monitoring.set_events(TEST_TOOL, 0)
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
Expand All @@ -524,7 +525,7 @@ def test_lines_loop(self):
sys.monitoring.set_events(TEST_TOOL, 0)
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
start = LineMonitoringTest.test_lines_loop.__code__.co_firstlineno
self.assertEqual(events, [start+7, 21, 22, 22, 21, start+8])
self.assertEqual(events, [start+7, 22, 23, 23, 22, start+8])
finally:
sys.monitoring.set_events(TEST_TOOL, 0)
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
Expand All @@ -546,7 +547,7 @@ def test_lines_two(self):
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
sys.monitoring.register_callback(TEST_TOOL2, E.LINE, None)
start = LineMonitoringTest.test_lines_two.__code__.co_firstlineno
expected = [start+10, 14, start+11]
expected = [start+10, 15, start+11]
self.assertEqual(events, expected)
self.assertEqual(events2, expected)
finally:
Expand Down Expand Up @@ -1082,6 +1083,99 @@ def func():
('line', 'check_events', 11)])


class TestLoadSuperAttr(CheckEvents):
def _super_method_call(self, optimized=False):
assignment = "x = 1" if optimized else "super = super"
codestr = textwrap.dedent(f"""
{assignment}
class A:
def method(self, x):
return x
class B(A):
def method(self, x):
return super(
).method(
x
)
b = B()
def f():
return b.method(1)
""")
d = {}
exec(codestr, d, d)
expected = [
('line', 'check_events', 10),
('call', 'f', sys.monitoring.MISSING),
('line', 'f', 1),
('call', 'method', d["b"]),
('line', 'method', 1),
('call', 'super', sys.monitoring.MISSING),
('C return', 'super', sys.monitoring.MISSING),
('line', 'method', 2),
('line', 'method', 3),
('line', 'method', 2),
('call', 'method', 1),
('line', 'method', 1),
('line', 'method', 1),
('line', 'check_events', 11),
('call', 'set_events', 2),
]
return d["f"], expected

def test_method_call(self):
nonopt_func, nonopt_expected = self._super_method_call(optimized=False)
opt_func, opt_expected = self._super_method_call(optimized=True)

recorders = CallRecorder, LineRecorder, CRaiseRecorder, CReturnRecorder

self.check_events(nonopt_func, recorders=recorders, expected=nonopt_expected)
self.check_events(opt_func, recorders=recorders, expected=opt_expected)

def _super_attr(self, optimized=False):
assignment = "x = 1" if optimized else "super = super"
codestr = textwrap.dedent(f"""
{assignment}
class A:
x = 1
class B(A):
def method(self):
return super(
).x
b = B()
def f():
return b.method()
""")
d = {}
exec(codestr, d, d)
expected = [
('line', 'check_events', 10),
('call', 'f', sys.monitoring.MISSING),
('line', 'f', 1),
('call', 'method', d["b"]),
('line', 'method', 1),
('call', 'super', sys.monitoring.MISSING),
('C return', 'super', sys.monitoring.MISSING),
('line', 'method', 2),
('line', 'method', 1),
('line', 'check_events', 11),
('call', 'set_events', 2)
]
return d["f"], expected

def test_attr(self):
nonopt_func, nonopt_expected = self._super_attr(optimized=False)
opt_func, opt_expected = self._super_attr(optimized=True)

recorders = CallRecorder, LineRecorder, CRaiseRecorder, CReturnRecorder

self.check_events(nonopt_func, recorders=recorders, expected=nonopt_expected)
self.check_events(opt_func, recorders=recorders, expected=opt_expected)


class TestSetGetEvents(MonitoringTestBase, unittest.TestCase):

def test_global(self):
Expand Down
32 changes: 32 additions & 0 deletions Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,14 @@ dummy_func(
PREDICT(JUMP_BACKWARD);
}

inst(INSTRUMENTED_LOAD_SUPER_ATTR, (unused/9, unused, unused, unused -- unused if (oparg & 1), unused)) {
_PySuperAttrCache *cache = (_PySuperAttrCache *)next_instr;
// cancel out the decrement that will happen in LOAD_SUPER_ATTR; we
// don't want to specialize instrumented instructions
INCREMENT_ADAPTIVE_COUNTER(cache->counter);
GO_TO_INSTRUCTION(LOAD_SUPER_ATTR);
}

family(load_super_attr, INLINE_CACHE_ENTRIES_LOAD_SUPER_ATTR) = {
LOAD_SUPER_ATTR,
LOAD_SUPER_ATTR_METHOD,
Expand All @@ -1573,6 +1581,14 @@ dummy_func(
DECREMENT_ADAPTIVE_COUNTER(cache->counter);
#endif /* ENABLE_SPECIALIZATION */

if (opcode == INSTRUMENTED_LOAD_SUPER_ATTR) {
PyObject *arg = oparg & 2 ? class : &_PyInstrumentation_MISSING;
int err = _Py_call_instrumentation_2args(
tstate, PY_MONITORING_EVENT_CALL,
frame, next_instr-1, global_super, arg);
ERROR_IF(err, error);
}

// we make no attempt to optimize here; specializations should
// handle any case whose performance we care about
PyObject *stack[] = {class, self};
Expand All @@ -1581,6 +1597,22 @@ dummy_func(
ERROR_IF(super == NULL, error);
res = PyObject_GetAttr(super, name);
Py_DECREF(super);
if (opcode == INSTRUMENTED_LOAD_SUPER_ATTR) {
PyObject *arg = oparg & 2 ? class : &_PyInstrumentation_MISSING;
if (res == NULL) {
_Py_call_instrumentation_exc2(
tstate, PY_MONITORING_EVENT_C_RAISE,
frame, next_instr-1, global_super, arg);
}
else {
int err = _Py_call_instrumentation_2args(
tstate, PY_MONITORING_EVENT_C_RETURN,
frame, next_instr-1, global_super, arg);
if (err < 0) {
Py_CLEAR(res);
}
}
}
ERROR_IF(res == NULL, error);
}

Expand Down
4 changes: 4 additions & 0 deletions Python/compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -4411,6 +4411,8 @@ maybe_optimize_method_call(struct compiler *c, expr_ty e)
int opcode = asdl_seq_LEN(meth->v.Attribute.value->v.Call.args) ?
LOAD_SUPER_METHOD : LOAD_ZERO_SUPER_METHOD;
ADDOP_NAME(c, loc, opcode, meth->v.Attribute.attr, names);
loc = update_start_location_to_match_attr(c, loc, meth);
ADDOP(c, loc, NOP);
} else {
VISIT(c, expr, meth->v.Attribute.value);
loc = update_start_location_to_match_attr(c, loc, meth);
Expand Down Expand Up @@ -5429,6 +5431,8 @@ compiler_visit_expr1(struct compiler *c, expr_ty e)
int opcode = asdl_seq_LEN(e->v.Attribute.value->v.Call.args) ?
LOAD_SUPER_ATTR : LOAD_ZERO_SUPER_ATTR;
ADDOP_NAME(c, loc, opcode, e->v.Attribute.attr, names);
loc = update_start_location_to_match_attr(c, loc, e);
ADDOP(c, loc, NOP);
return SUCCESS;
}
VISIT(c, expr, e->v.Attribute.value);
Expand Down

0 comments on commit 862c3dc

Please sign in to comment.