Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f40890b

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

10 files changed

+535
-236
lines changed

‎Include/internal/pycore_opcode.h

Copy file name to clipboardExpand all lines: Include/internal/pycore_opcode.h
+2-2Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/opcode.h

Copy file name to clipboardExpand all lines: Include/opcode.h
+2-1Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Lib/opcode.py

Copy file name to clipboardExpand all lines: Lib/opcode.py
+2-1Lines changed: 2 additions & 1 deletion
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

Copy file name to clipboardExpand all lines: Lib/test/test_monitoring.py
+220-3Lines changed: 220 additions & 3 deletions
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

Copy file name to clipboardExpand all lines: Python/bytecodes.c
+32Lines changed: 32 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Python/compile.c
+4Lines changed: 4 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.