Skip to content

Commit ea3cd04

Browse files
authored
gh-114312: Collect stats for unlikely events (GH-114493)
1 parent c63c614 commit ea3cd04

File tree

11 files changed

+199
-1
lines changed

11 files changed

+199
-1
lines changed

Include/cpython/pystats.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,25 @@ typedef struct _optimization_stats {
122122
uint64_t optimized_trace_length_hist[_Py_UOP_HIST_SIZE];
123123
} OptimizationStats;
124124

125+
typedef struct _rare_event_stats {
126+
/* Setting an object's class, obj.__class__ = ... */
127+
uint64_t set_class;
128+
/* Setting the bases of a class, cls.__bases__ = ... */
129+
uint64_t set_bases;
130+
/* Setting the PEP 523 frame eval function, _PyInterpreterState_SetFrameEvalFunc() */
131+
uint64_t set_eval_frame_func;
132+
/* Modifying the builtins, __builtins__.__dict__[var] = ... */
133+
uint64_t builtin_dict;
134+
/* Modifying a function, e.g. func.__defaults__ = ..., etc. */
135+
uint64_t func_modification;
136+
} RareEventStats;
137+
125138
typedef struct _stats {
126139
OpcodeStats opcode_stats[256];
127140
CallStats call_stats;
128141
ObjectStats object_stats;
129142
OptimizationStats optimization_stats;
143+
RareEventStats rare_event_stats;
130144
GCStats *gc_stats;
131145
} PyStats;
132146

Include/internal/pycore_code.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ extern int _PyStaticCode_Init(PyCodeObject *co);
295295
_Py_stats->optimization_stats.name[bucket]++; \
296296
} \
297297
} while (0)
298+
#define RARE_EVENT_STAT_INC(name) do { if (_Py_stats) _Py_stats->rare_event_stats.name++; } while (0)
298299

299300
// Export for '_opcode' shared extension
300301
PyAPI_FUNC(PyObject*) _Py_GetSpecializationStats(void);
@@ -313,6 +314,7 @@ PyAPI_FUNC(PyObject*) _Py_GetSpecializationStats(void);
313314
#define UOP_STAT_INC(opname, name) ((void)0)
314315
#define OPT_UNSUPPORTED_OPCODE(opname) ((void)0)
315316
#define OPT_HIST(length, name) ((void)0)
317+
#define RARE_EVENT_STAT_INC(name) ((void)0)
316318
#endif // !Py_STATS
317319

318320
// Utility functions for reading/writing 32/64-bit values in the inline caches.

Include/internal/pycore_interp.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,21 @@ struct _stoptheworld_state {
6060

6161
/* cross-interpreter data registry */
6262

63+
/* Tracks some rare events per-interpreter, used by the optimizer to turn on/off
64+
specific optimizations. */
65+
typedef struct _rare_events {
66+
/* Setting an object's class, obj.__class__ = ... */
67+
uint8_t set_class;
68+
/* Setting the bases of a class, cls.__bases__ = ... */
69+
uint8_t set_bases;
70+
/* Setting the PEP 523 frame eval function, _PyInterpreterState_SetFrameEvalFunc() */
71+
uint8_t set_eval_frame_func;
72+
/* Modifying the builtins, __builtins__.__dict__[var] = ... */
73+
uint8_t builtin_dict;
74+
int builtins_dict_watcher_id;
75+
/* Modifying a function, e.g. func.__defaults__ = ..., etc. */
76+
uint8_t func_modification;
77+
} _rare_events;
6378

6479
/* interpreter state */
6580

@@ -217,6 +232,7 @@ struct _is {
217232
uint16_t optimizer_resume_threshold;
218233
uint16_t optimizer_backedge_threshold;
219234
uint32_t next_func_version;
235+
_rare_events rare_events;
220236

221237
_Py_GlobalMonitors monitors;
222238
bool sys_profile_initialized;
@@ -347,6 +363,19 @@ PyAPI_FUNC(PyStatus) _PyInterpreterState_New(
347363
PyInterpreterState **pinterp);
348364

349365

366+
#define RARE_EVENT_INTERP_INC(interp, name) \
367+
do { \
368+
/* saturating add */ \
369+
if (interp->rare_events.name < UINT8_MAX) interp->rare_events.name++; \
370+
RARE_EVENT_STAT_INC(name); \
371+
} while (0); \
372+
373+
#define RARE_EVENT_INC(name) \
374+
do { \
375+
PyInterpreterState *interp = PyInterpreterState_Get(); \
376+
RARE_EVENT_INTERP_INC(interp, name); \
377+
} while (0); \
378+
350379
#ifdef __cplusplus
351380
}
352381
#endif

Lib/test/test_optimizer.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import _testinternalcapi
2+
import unittest
3+
import types
4+
5+
6+
class TestRareEventCounters(unittest.TestCase):
7+
def test_set_class(self):
8+
class A:
9+
pass
10+
class B:
11+
pass
12+
a = A()
13+
14+
orig_counter = _testinternalcapi.get_rare_event_counters()["set_class"]
15+
a.__class__ = B
16+
self.assertEqual(
17+
orig_counter + 1,
18+
_testinternalcapi.get_rare_event_counters()["set_class"]
19+
)
20+
21+
def test_set_bases(self):
22+
class A:
23+
pass
24+
class B:
25+
pass
26+
class C(B):
27+
pass
28+
29+
orig_counter = _testinternalcapi.get_rare_event_counters()["set_bases"]
30+
C.__bases__ = (A,)
31+
self.assertEqual(
32+
orig_counter + 1,
33+
_testinternalcapi.get_rare_event_counters()["set_bases"]
34+
)
35+
36+
def test_set_eval_frame_func(self):
37+
orig_counter = _testinternalcapi.get_rare_event_counters()["set_eval_frame_func"]
38+
_testinternalcapi.set_eval_frame_record([])
39+
self.assertEqual(
40+
orig_counter + 1,
41+
_testinternalcapi.get_rare_event_counters()["set_eval_frame_func"]
42+
)
43+
_testinternalcapi.set_eval_frame_default()
44+
45+
def test_builtin_dict(self):
46+
orig_counter = _testinternalcapi.get_rare_event_counters()["builtin_dict"]
47+
if isinstance(__builtins__, types.ModuleType):
48+
builtins = __builtins__.__dict__
49+
else:
50+
builtins = __builtins__
51+
builtins["FOO"] = 42
52+
self.assertEqual(
53+
orig_counter + 1,
54+
_testinternalcapi.get_rare_event_counters()["builtin_dict"]
55+
)
56+
del builtins["FOO"]
57+
58+
def test_func_modification(self):
59+
def func(x=0):
60+
pass
61+
62+
for attribute in (
63+
"__code__",
64+
"__defaults__",
65+
"__kwdefaults__"
66+
):
67+
orig_counter = _testinternalcapi.get_rare_event_counters()["func_modification"]
68+
setattr(func, attribute, getattr(func, attribute))
69+
self.assertEqual(
70+
orig_counter + 1,
71+
_testinternalcapi.get_rare_event_counters()["func_modification"]
72+
)
73+
74+
if __name__ == "__main__":
75+
unittest.main()

Modules/_testinternalcapi.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,6 +1635,21 @@ get_type_module_name(PyObject *self, PyObject *type)
16351635
return _PyType_GetModuleName((PyTypeObject *)type);
16361636
}
16371637

1638+
static PyObject *
1639+
get_rare_event_counters(PyObject *self, PyObject *type)
1640+
{
1641+
PyInterpreterState *interp = PyInterpreterState_Get();
1642+
1643+
return Py_BuildValue(
1644+
"{sksksksksk}",
1645+
"set_class", interp->rare_events.set_class,
1646+
"set_bases", interp->rare_events.set_bases,
1647+
"set_eval_frame_func", interp->rare_events.set_eval_frame_func,
1648+
"builtin_dict", interp->rare_events.builtin_dict,
1649+
"func_modification", interp->rare_events.func_modification
1650+
);
1651+
}
1652+
16381653

16391654
#ifdef Py_GIL_DISABLED
16401655
static PyObject *
@@ -1711,6 +1726,7 @@ static PyMethodDef module_functions[] = {
17111726
{"restore_crossinterp_data", restore_crossinterp_data, METH_VARARGS},
17121727
_TESTINTERNALCAPI_TEST_LONG_NUMBITS_METHODDEF
17131728
{"get_type_module_name", get_type_module_name, METH_O},
1729+
{"get_rare_event_counters", get_rare_event_counters, METH_NOARGS},
17141730
#ifdef Py_GIL_DISABLED
17151731
{"py_thread_id", get_py_thread_id, METH_NOARGS},
17161732
#endif

Objects/funcobject.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ handle_func_event(PyFunction_WatchEvent event, PyFunctionObject *func,
5353
if (interp->active_func_watchers) {
5454
notify_func_watchers(interp, event, func, new_value);
5555
}
56+
switch (event) {
57+
case PyFunction_EVENT_MODIFY_CODE:
58+
case PyFunction_EVENT_MODIFY_DEFAULTS:
59+
case PyFunction_EVENT_MODIFY_KWDEFAULTS:
60+
RARE_EVENT_INTERP_INC(interp, func_modification);
61+
break;
62+
default:
63+
break;
64+
}
5665
}
5766

5867
int

Objects/typeobject.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,7 @@ type_set_bases(PyTypeObject *type, PyObject *new_bases, void *context)
13711371
res = 0;
13721372
}
13731373

1374+
RARE_EVENT_INC(set_bases);
13741375
Py_DECREF(old_bases);
13751376
Py_DECREF(old_base);
13761377

@@ -5842,6 +5843,8 @@ object_set_class(PyObject *self, PyObject *value, void *closure)
58425843
Py_SET_TYPE(self, newto);
58435844
if (oldto->tp_flags & Py_TPFLAGS_HEAPTYPE)
58445845
Py_DECREF(oldto);
5846+
5847+
RARE_EVENT_INC(set_class);
58455848
return 0;
58465849
}
58475850
else {

Python/pylifecycle.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,12 @@ init_interp_create_gil(PyThreadState *tstate, int gil)
605605
_PyEval_InitGIL(tstate, own_gil);
606606
}
607607

608+
static int
609+
builtins_dict_watcher(PyDict_WatchEvent event, PyObject *dict, PyObject *key, PyObject *new_value)
610+
{
611+
RARE_EVENT_INC(builtin_dict);
612+
return 0;
613+
}
608614

609615
static PyStatus
610616
pycore_create_interpreter(_PyRuntimeState *runtime,
@@ -1266,6 +1272,14 @@ init_interp_main(PyThreadState *tstate)
12661272
}
12671273
}
12681274

1275+
if ((interp->rare_events.builtins_dict_watcher_id = PyDict_AddWatcher(&builtins_dict_watcher)) == -1) {
1276+
return _PyStatus_ERR("failed to add builtin dict watcher");
1277+
}
1278+
1279+
if (PyDict_Watch(interp->rare_events.builtins_dict_watcher_id, interp->builtins) != 0) {
1280+
return _PyStatus_ERR("failed to set builtin dict watcher");
1281+
}
1282+
12691283
assert(!_PyErr_Occurred(tstate));
12701284

12711285
return _PyStatus_OK();
@@ -1592,6 +1606,10 @@ static void
15921606
finalize_modules(PyThreadState *tstate)
15931607
{
15941608
PyInterpreterState *interp = tstate->interp;
1609+
1610+
// Stop collecting stats on __builtin__ modifications during teardown
1611+
PyDict_Unwatch(interp->rare_events.builtins_dict_watcher_id, interp->builtins);
1612+
15951613
PyObject *modules = _PyImport_GetModules(interp);
15961614
if (modules == NULL) {
15971615
// Already done

Python/pystate.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2616,6 +2616,7 @@ _PyInterpreterState_SetEvalFrameFunc(PyInterpreterState *interp,
26162616
if (eval_frame != NULL) {
26172617
_Py_Executors_InvalidateAll(interp);
26182618
}
2619+
RARE_EVENT_INC(set_eval_frame_func);
26192620
interp->eval_frame = eval_frame;
26202621
}
26212622

Python/specialize.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,16 @@ print_optimization_stats(FILE *out, OptimizationStats *stats)
267267
}
268268
}
269269

270+
static void
271+
print_rare_event_stats(FILE *out, RareEventStats *stats)
272+
{
273+
fprintf(out, "Rare event (set_class): %" PRIu64 "\n", stats->set_class);
274+
fprintf(out, "Rare event (set_bases): %" PRIu64 "\n", stats->set_bases);
275+
fprintf(out, "Rare event (set_eval_frame_func): %" PRIu64 "\n", stats->set_eval_frame_func);
276+
fprintf(out, "Rare event (builtin_dict): %" PRIu64 "\n", stats->builtin_dict);
277+
fprintf(out, "Rare event (func_modification): %" PRIu64 "\n", stats->func_modification);
278+
}
279+
270280
static void
271281
print_stats(FILE *out, PyStats *stats)
272282
{
@@ -275,6 +285,7 @@ print_stats(FILE *out, PyStats *stats)
275285
print_object_stats(out, &stats->object_stats);
276286
print_gc_stats(out, stats->gc_stats);
277287
print_optimization_stats(out, &stats->optimization_stats);
288+
print_rare_event_stats(out, &stats->rare_event_stats);
278289
}
279290

280291
void

Tools/scripts/summarize_stats.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,14 @@ def get_histogram(self, prefix: str) -> list[tuple[int, int]]:
412412
rows.sort()
413413
return rows
414414

415+
def get_rare_events(self) -> list[tuple[str, int]]:
416+
prefix = "Rare event "
417+
return [
418+
(key[len(prefix) + 1:-1], val)
419+
for key, val in self._data.items()
420+
if key.startswith(prefix)
421+
]
422+
415423

416424
class Count(int):
417425
def markdown(self) -> str:
@@ -1064,6 +1072,17 @@ def iter_optimization_tables(base_stats: Stats, head_stats: Stats | None = None)
10641072
)
10651073

10661074

1075+
def rare_event_section() -> Section:
1076+
def calc_rare_event_table(stats: Stats) -> Table:
1077+
return [(x, Count(y)) for x, y in stats.get_rare_events()]
1078+
1079+
return Section(
1080+
"Rare events",
1081+
"Counts of rare/unlikely events",
1082+
[Table(("Event", "Count:"), calc_rare_event_table, JoinMode.CHANGE)],
1083+
)
1084+
1085+
10671086
def meta_stats_section() -> Section:
10681087
def calc_rows(stats: Stats) -> Rows:
10691088
return [("Number of data files", Count(stats.get("__nfiles__")))]
@@ -1085,6 +1104,7 @@ def calc_rows(stats: Stats) -> Rows:
10851104
object_stats_section(),
10861105
gc_stats_section(),
10871106
optimization_section(),
1107+
rare_event_section(),
10881108
meta_stats_section(),
10891109
]
10901110

@@ -1162,7 +1182,7 @@ def output_stats(inputs: list[Path], json_output=str | None):
11621182
case 1:
11631183
data = load_raw_data(Path(inputs[0]))
11641184
if json_output is not None:
1165-
with open(json_output, 'w', encoding='utf-8') as f:
1185+
with open(json_output, "w", encoding="utf-8") as f:
11661186
save_raw_data(data, f) # type: ignore
11671187
stats = Stats(data)
11681188
output_markdown(sys.stdout, LAYOUT, stats)

0 commit comments

Comments
 (0)