Skip to content

Commit f40890b

Browse files
authored
gh-103865: add monitoring support to LOAD_SUPER_ATTR (#103866)
1 parent febcc6c commit f40890b

10 files changed

+535
-236
lines changed

Include/internal/pycore_opcode.h

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/opcode.h

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/opcode.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,9 @@ def pseudo_op(name, op, real_ops):
233233
hasfree.append(176)
234234

235235
# Instrumented instructions
236-
MIN_INSTRUMENTED_OPCODE = 238
236+
MIN_INSTRUMENTED_OPCODE = 237
237237

238+
def_op('INSTRUMENTED_LOAD_SUPER_ATTR', 237)
238239
def_op('INSTRUMENTED_POP_JUMP_IF_NONE', 238)
239240
def_op('INSTRUMENTED_POP_JUMP_IF_NOT_NONE', 239)
240241
def_op('INSTRUMENTED_RESUME', 240)

Lib/test/test_monitoring.py

+220-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Test suite for the sys.monitoring."""
22

33
import collections
4+
import dis
45
import functools
56
import operator
67
import sys
8+
import textwrap
79
import types
810
import unittest
911

@@ -506,7 +508,7 @@ def test_lines_single(self):
506508
sys.monitoring.set_events(TEST_TOOL, 0)
507509
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
508510
start = LineMonitoringTest.test_lines_single.__code__.co_firstlineno
509-
self.assertEqual(events, [start+7, 14, start+8])
511+
self.assertEqual(events, [start+7, 16, start+8])
510512
finally:
511513
sys.monitoring.set_events(TEST_TOOL, 0)
512514
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
@@ -524,7 +526,7 @@ def test_lines_loop(self):
524526
sys.monitoring.set_events(TEST_TOOL, 0)
525527
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
526528
start = LineMonitoringTest.test_lines_loop.__code__.co_firstlineno
527-
self.assertEqual(events, [start+7, 21, 22, 21, 22, 21, start+8])
529+
self.assertEqual(events, [start+7, 23, 24, 23, 24, 23, start+8])
528530
finally:
529531
sys.monitoring.set_events(TEST_TOOL, 0)
530532
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
@@ -546,7 +548,7 @@ def test_lines_two(self):
546548
sys.monitoring.register_callback(TEST_TOOL, E.LINE, None)
547549
sys.monitoring.register_callback(TEST_TOOL2, E.LINE, None)
548550
start = LineMonitoringTest.test_lines_two.__code__.co_firstlineno
549-
expected = [start+10, 14, start+11]
551+
expected = [start+10, 16, start+11]
550552
self.assertEqual(events, expected)
551553
self.assertEqual(events2, expected)
552554
finally:
@@ -1177,6 +1179,221 @@ def func():
11771179
('return', None),
11781180
('line', 'check_events', 11)])
11791181

1182+
class TestLoadSuperAttr(CheckEvents):
1183+
RECORDERS = CallRecorder, LineRecorder, CRaiseRecorder, CReturnRecorder
1184+
1185+
def _exec(self, co):
1186+
d = {}
1187+
exec(co, d, d)
1188+
return d
1189+
1190+
def _exec_super(self, codestr, optimized=False):
1191+
# The compiler checks for statically visible shadowing of the name
1192+
# `super`, and declines to emit `LOAD_SUPER_ATTR` if shadowing is found.
1193+
# So inserting `super = super` prevents the compiler from emitting
1194+
# `LOAD_SUPER_ATTR`, and allows us to test that monitoring events for
1195+
# `LOAD_SUPER_ATTR` are equivalent to those we'd get from the
1196+
# un-optimized `LOAD_GLOBAL super; CALL; LOAD_ATTR` form.
1197+
assignment = "x = 1" if optimized else "super = super"
1198+
codestr = f"{assignment}\n{textwrap.dedent(codestr)}"
1199+
co = compile(codestr, "<string>", "exec")
1200+
# validate that we really do have a LOAD_SUPER_ATTR, only when optimized
1201+
self.assertEqual(self._has_load_super_attr(co), optimized)
1202+
return self._exec(co)
1203+
1204+
def _has_load_super_attr(self, co):
1205+
has = any(instr.opname == "LOAD_SUPER_ATTR" for instr in dis.get_instructions(co))
1206+
if not has:
1207+
has = any(
1208+
isinstance(c, types.CodeType) and self._has_load_super_attr(c)
1209+
for c in co.co_consts
1210+
)
1211+
return has
1212+
1213+
def _super_method_call(self, optimized=False):
1214+
codestr = """
1215+
class A:
1216+
def method(self, x):
1217+
return x
1218+
1219+
class B(A):
1220+
def method(self, x):
1221+
return super(
1222+
).method(
1223+
x
1224+
)
1225+
1226+
b = B()
1227+
def f():
1228+
return b.method(1)
1229+
"""
1230+
d = self._exec_super(codestr, optimized)
1231+
expected = [
1232+
('line', 'check_events', 10),
1233+
('call', 'f', sys.monitoring.MISSING),
1234+
('line', 'f', 1),
1235+
('call', 'method', d["b"]),
1236+
('line', 'method', 1),
1237+
('call', 'super', sys.monitoring.MISSING),
1238+
('C return', 'super', sys.monitoring.MISSING),
1239+
('line', 'method', 2),
1240+
('line', 'method', 3),
1241+
('line', 'method', 2),
1242+
('call', 'method', 1),
1243+
('line', 'method', 1),
1244+
('line', 'method', 1),
1245+
('line', 'check_events', 11),
1246+
('call', 'set_events', 2),
1247+
]
1248+
return d["f"], expected
1249+
1250+
def test_method_call(self):
1251+
nonopt_func, nonopt_expected = self._super_method_call(optimized=False)
1252+
opt_func, opt_expected = self._super_method_call(optimized=True)
1253+
1254+
self.check_events(nonopt_func, recorders=self.RECORDERS, expected=nonopt_expected)
1255+
self.check_events(opt_func, recorders=self.RECORDERS, expected=opt_expected)
1256+
1257+
def _super_method_call_error(self, optimized=False):
1258+
codestr = """
1259+
class A:
1260+
def method(self, x):
1261+
return x
1262+
1263+
class B(A):
1264+
def method(self, x):
1265+
return super(
1266+
x,
1267+
self,
1268+
).method(
1269+
x
1270+
)
1271+
1272+
b = B()
1273+
def f():
1274+
try:
1275+
return b.method(1)
1276+
except TypeError:
1277+
pass
1278+
else:
1279+
assert False, "should have raised TypeError"
1280+
"""
1281+
d = self._exec_super(codestr, optimized)
1282+
expected = [
1283+
('line', 'check_events', 10),
1284+
('call', 'f', sys.monitoring.MISSING),
1285+
('line', 'f', 1),
1286+
('line', 'f', 2),
1287+
('call', 'method', d["b"]),
1288+
('line', 'method', 1),
1289+
('line', 'method', 2),
1290+
('line', 'method', 3),
1291+
('line', 'method', 1),
1292+
('call', 'super', 1),
1293+
('C raise', 'super', 1),
1294+
('line', 'f', 3),
1295+
('line', 'f', 4),
1296+
('line', 'check_events', 11),
1297+
('call', 'set_events', 2),
1298+
]
1299+
return d["f"], expected
1300+
1301+
def test_method_call_error(self):
1302+
nonopt_func, nonopt_expected = self._super_method_call_error(optimized=False)
1303+
opt_func, opt_expected = self._super_method_call_error(optimized=True)
1304+
1305+
self.check_events(nonopt_func, recorders=self.RECORDERS, expected=nonopt_expected)
1306+
self.check_events(opt_func, recorders=self.RECORDERS, expected=opt_expected)
1307+
1308+
def _super_attr(self, optimized=False):
1309+
codestr = """
1310+
class A:
1311+
x = 1
1312+
1313+
class B(A):
1314+
def method(self):
1315+
return super(
1316+
).x
1317+
1318+
b = B()
1319+
def f():
1320+
return b.method()
1321+
"""
1322+
d = self._exec_super(codestr, optimized)
1323+
expected = [
1324+
('line', 'check_events', 10),
1325+
('call', 'f', sys.monitoring.MISSING),
1326+
('line', 'f', 1),
1327+
('call', 'method', d["b"]),
1328+
('line', 'method', 1),
1329+
('call', 'super', sys.monitoring.MISSING),
1330+
('C return', 'super', sys.monitoring.MISSING),
1331+
('line', 'method', 2),
1332+
('line', 'method', 1),
1333+
('line', 'check_events', 11),
1334+
('call', 'set_events', 2)
1335+
]
1336+
return d["f"], expected
1337+
1338+
def test_attr(self):
1339+
nonopt_func, nonopt_expected = self._super_attr(optimized=False)
1340+
opt_func, opt_expected = self._super_attr(optimized=True)
1341+
1342+
self.check_events(nonopt_func, recorders=self.RECORDERS, expected=nonopt_expected)
1343+
self.check_events(opt_func, recorders=self.RECORDERS, expected=opt_expected)
1344+
1345+
def test_vs_other_type_call(self):
1346+
code_template = textwrap.dedent("""
1347+
class C:
1348+
def method(self):
1349+
return {cls}().__repr__{call}
1350+
c = C()
1351+
def f():
1352+
return c.method()
1353+
""")
1354+
1355+
def get_expected(name, call_method, ns):
1356+
repr_arg = 0 if name == "int" else sys.monitoring.MISSING
1357+
return [
1358+
('line', 'check_events', 10),
1359+
('call', 'f', sys.monitoring.MISSING),
1360+
('line', 'f', 1),
1361+
('call', 'method', ns["c"]),
1362+
('line', 'method', 1),
1363+
('call', name, sys.monitoring.MISSING),
1364+
('C return', name, sys.monitoring.MISSING),
1365+
*(
1366+
[
1367+
('call', '__repr__', repr_arg),
1368+
('C return', '__repr__', repr_arg),
1369+
] if call_method else []
1370+
),
1371+
('line', 'check_events', 11),
1372+
('call', 'set_events', 2),
1373+
]
1374+
1375+
for call_method in [True, False]:
1376+
with self.subTest(call_method=call_method):
1377+
call_str = "()" if call_method else ""
1378+
code_super = code_template.format(cls="super", call=call_str)
1379+
code_int = code_template.format(cls="int", call=call_str)
1380+
co_super = compile(code_super, '<string>', 'exec')
1381+
self.assertTrue(self._has_load_super_attr(co_super))
1382+
ns_super = self._exec(co_super)
1383+
ns_int = self._exec(code_int)
1384+
1385+
self.check_events(
1386+
ns_super["f"],
1387+
recorders=self.RECORDERS,
1388+
expected=get_expected("super", call_method, ns_super)
1389+
)
1390+
self.check_events(
1391+
ns_int["f"],
1392+
recorders=self.RECORDERS,
1393+
expected=get_expected("int", call_method, ns_int)
1394+
)
1395+
1396+
11801397
class TestSetGetEvents(MonitoringTestBase, unittest.TestCase):
11811398

11821399
def test_global(self):

Python/bytecodes.c

+32
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,14 @@ dummy_func(
15821582
PREDICT(JUMP_BACKWARD);
15831583
}
15841584

1585+
inst(INSTRUMENTED_LOAD_SUPER_ATTR, (unused/9, unused, unused, unused -- unused if (oparg & 1), unused)) {
1586+
_PySuperAttrCache *cache = (_PySuperAttrCache *)next_instr;
1587+
// cancel out the decrement that will happen in LOAD_SUPER_ATTR; we
1588+
// don't want to specialize instrumented instructions
1589+
INCREMENT_ADAPTIVE_COUNTER(cache->counter);
1590+
GO_TO_INSTRUCTION(LOAD_SUPER_ATTR);
1591+
}
1592+
15851593
family(load_super_attr, INLINE_CACHE_ENTRIES_LOAD_SUPER_ATTR) = {
15861594
LOAD_SUPER_ATTR,
15871595
LOAD_SUPER_ATTR_ATTR,
@@ -1602,10 +1610,34 @@ dummy_func(
16021610
DECREMENT_ADAPTIVE_COUNTER(cache->counter);
16031611
#endif /* ENABLE_SPECIALIZATION */
16041612

1613+
if (opcode == INSTRUMENTED_LOAD_SUPER_ATTR) {
1614+
PyObject *arg = oparg & 2 ? class : &_PyInstrumentation_MISSING;
1615+
int err = _Py_call_instrumentation_2args(
1616+
tstate, PY_MONITORING_EVENT_CALL,
1617+
frame, next_instr-1, global_super, arg);
1618+
ERROR_IF(err, error);
1619+
}
1620+
16051621
// we make no attempt to optimize here; specializations should
16061622
// handle any case whose performance we care about
16071623
PyObject *stack[] = {class, self};
16081624
PyObject *super = PyObject_Vectorcall(global_super, stack, oparg & 2, NULL);
1625+
if (opcode == INSTRUMENTED_LOAD_SUPER_ATTR) {
1626+
PyObject *arg = oparg & 2 ? class : &_PyInstrumentation_MISSING;
1627+
if (super == NULL) {
1628+
_Py_call_instrumentation_exc2(
1629+
tstate, PY_MONITORING_EVENT_C_RAISE,
1630+
frame, next_instr-1, global_super, arg);
1631+
}
1632+
else {
1633+
int err = _Py_call_instrumentation_2args(
1634+
tstate, PY_MONITORING_EVENT_C_RETURN,
1635+
frame, next_instr-1, global_super, arg);
1636+
if (err < 0) {
1637+
Py_CLEAR(super);
1638+
}
1639+
}
1640+
}
16091641
DECREF_INPUTS();
16101642
ERROR_IF(super == NULL, error);
16111643
res = PyObject_GetAttr(super, name);

Python/compile.c

+4
Original file line numberDiff line numberDiff line change
@@ -4846,6 +4846,8 @@ maybe_optimize_method_call(struct compiler *c, expr_ty e)
48464846
int opcode = asdl_seq_LEN(meth->v.Attribute.value->v.Call.args) ?
48474847
LOAD_SUPER_METHOD : LOAD_ZERO_SUPER_METHOD;
48484848
ADDOP_NAME(c, loc, opcode, meth->v.Attribute.attr, names);
4849+
loc = update_start_location_to_match_attr(c, loc, meth);
4850+
ADDOP(c, loc, NOP);
48494851
} else {
48504852
VISIT(c, expr, meth->v.Attribute.value);
48514853
loc = update_start_location_to_match_attr(c, loc, meth);
@@ -6079,6 +6081,8 @@ compiler_visit_expr1(struct compiler *c, expr_ty e)
60796081
int opcode = asdl_seq_LEN(e->v.Attribute.value->v.Call.args) ?
60806082
LOAD_SUPER_ATTR : LOAD_ZERO_SUPER_ATTR;
60816083
ADDOP_NAME(c, loc, opcode, e->v.Attribute.attr, names);
6084+
loc = update_start_location_to_match_attr(c, loc, e);
6085+
ADDOP(c, loc, NOP);
60826086
return SUCCESS;
60836087
}
60846088
VISIT(c, expr, e->v.Attribute.value);

0 commit comments

Comments
 (0)