Skip to content

Commit bbcf424

Browse files
authored
GH-90230: Add stats to breakdown the origin of calls to PyEval_EvalFrame (GH-93284)
1 parent 8995177 commit bbcf424

File tree

13 files changed

+63
-11
lines changed

13 files changed

+63
-11
lines changed

Include/internal/pycore_call.h

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ _PyObject_CallNoArgsTstate(PyThreadState *tstate, PyObject *func) {
103103
// Private static inline function variant of public PyObject_CallNoArgs()
104104
static inline PyObject *
105105
_PyObject_CallNoArgs(PyObject *func) {
106+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, func);
106107
PyThreadState *tstate = _PyThreadState_GET();
107108
return _PyObject_VectorcallTstate(tstate, func, NULL, 0, NULL);
108109
}
@@ -111,6 +112,7 @@ _PyObject_CallNoArgs(PyObject *func) {
111112
static inline PyObject *
112113
_PyObject_FastCallTstate(PyThreadState *tstate, PyObject *func, PyObject *const *args, Py_ssize_t nargs)
113114
{
115+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, func);
114116
return _PyObject_VectorcallTstate(tstate, func, args, (size_t)nargs, NULL);
115117
}
116118

Include/internal/pycore_ceval.h

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ extern PyObject* _PyEval_BuiltinsFromGlobals(
6868
static inline PyObject*
6969
_PyEval_EvalFrame(PyThreadState *tstate, struct _PyInterpreterFrame *frame, int throwflag)
7070
{
71+
EVAL_CALL_STAT_INC(EVAL_CALL_TOTAL);
7172
if (tstate->interp->eval_frame == NULL) {
7273
return _PyEval_EvalFrameDefault(tstate, frame, throwflag);
7374
}

Include/internal/pycore_code.h

+5
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,9 @@ extern int _PyStaticCode_InternStrings(PyCodeObject *co);
265265
#define OBJECT_STAT_INC(name) _py_stats.object_stats.name++
266266
#define OBJECT_STAT_INC_COND(name, cond) \
267267
do { if (cond) _py_stats.object_stats.name++; } while (0)
268+
#define EVAL_CALL_STAT_INC(name) _py_stats.call_stats.eval_calls[name]++
269+
#define EVAL_CALL_STAT_INC_IF_FUNCTION(name, callable) \
270+
do { if (PyFunction_Check(callable)) _py_stats.call_stats.eval_calls[name]++; } while (0)
268271

269272
// Used by the _opcode extension which is built as a shared library
270273
PyAPI_FUNC(PyObject*) _Py_GetSpecializationStats(void);
@@ -276,6 +279,8 @@ PyAPI_FUNC(PyObject*) _Py_GetSpecializationStats(void);
276279
#define CALL_STAT_INC(name) ((void)0)
277280
#define OBJECT_STAT_INC(name) ((void)0)
278281
#define OBJECT_STAT_INC_COND(name, cond) ((void)0)
282+
#define EVAL_CALL_STAT_INC(name) ((void)0)
283+
#define EVAL_CALL_STAT_INC_IF_FUNCTION(name, callable) ((void)0)
279284
#endif // !Py_STATS
280285

281286
// Cache values are only valid in memory, so use native endianness.

Include/pystats.h

+15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ extern "C" {
1010

1111
#define SPECIALIZATION_FAILURE_KINDS 32
1212

13+
/* Stats for determining who is calling PyEval_EvalFrame */
14+
#define EVAL_CALL_TOTAL 0
15+
#define EVAL_CALL_VECTOR 1
16+
#define EVAL_CALL_GENERATOR 2
17+
#define EVAL_CALL_LEGACY 3
18+
#define EVAL_CALL_FUNCTION_VECTORCALL 4
19+
#define EVAL_CALL_BUILD_CLASS 5
20+
#define EVAL_CALL_SLOT 6
21+
#define EVAL_CALL_FUNCTION_EX 7
22+
#define EVAL_CALL_API 8
23+
#define EVAL_CALL_METHOD 9
24+
25+
#define EVAL_CALL_KINDS 10
26+
1327
typedef struct _specialization_stats {
1428
uint64_t success;
1529
uint64_t failure;
@@ -31,6 +45,7 @@ typedef struct _call_stats {
3145
uint64_t pyeval_calls;
3246
uint64_t frames_pushed;
3347
uint64_t frame_objects_created;
48+
uint64_t eval_calls[EVAL_CALL_KINDS];
3449
} CallStats;
3550

3651
typedef struct _object_stats {

Modules/_asynciomodule.c

+2-1
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ call_soon(PyObject *loop, PyObject *func, PyObject *arg, PyObject *ctx)
384384
nargs++;
385385
}
386386
stack[nargs] = (PyObject *)ctx;
387-
387+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, callable);
388388
handle = PyObject_Vectorcall(callable, stack, nargs, context_kwname);
389389
Py_DECREF(callable);
390390
}
@@ -2950,6 +2950,7 @@ task_step_impl(TaskObj *task, PyObject *exc)
29502950
PyObject *stack[2];
29512951
stack[0] = wrapper;
29522952
stack[1] = (PyObject *)task->task_context;
2953+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, add_cb);
29532954
tmp = PyObject_Vectorcall(add_cb, stack, 1, context_kwname);
29542955
Py_DECREF(add_cb);
29552956
Py_DECREF(wrapper);

Objects/call.c

+13-3
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ _Py_CheckSlotResult(PyObject *obj, const char *slot_name, int success)
109109
PyObject *
110110
PyObject_CallNoArgs(PyObject *func)
111111
{
112-
return _PyObject_CallNoArgs(func);
112+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, func);
113+
PyThreadState *tstate = _PyThreadState_GET();
114+
return _PyObject_VectorcallTstate(tstate, func, NULL, 0, NULL);
113115
}
114116

115117

@@ -322,7 +324,7 @@ _PyObject_Call(PyThreadState *tstate, PyObject *callable,
322324
assert(!_PyErr_Occurred(tstate));
323325
assert(PyTuple_Check(args));
324326
assert(kwargs == NULL || PyDict_Check(kwargs));
325-
327+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, callable);
326328
vectorcallfunc vector_func = _PyVectorcall_Function(callable);
327329
if (vector_func != NULL) {
328330
return _PyVectorcall_Call(tstate, vector_func, callable, args, kwargs);
@@ -367,6 +369,7 @@ PyCFunction_Call(PyObject *callable, PyObject *args, PyObject *kwargs)
367369
PyObject *
368370
PyObject_CallOneArg(PyObject *func, PyObject *arg)
369371
{
372+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, func);
370373
assert(arg != NULL);
371374
PyObject *_args[2];
372375
PyObject **args = _args + 1; // For PY_VECTORCALL_ARGUMENTS_OFFSET
@@ -389,6 +392,7 @@ _PyFunction_Vectorcall(PyObject *func, PyObject* const* stack,
389392
assert(nargs >= 0);
390393
PyThreadState *tstate = _PyThreadState_GET();
391394
assert(nargs == 0 || stack != NULL);
395+
EVAL_CALL_STAT_INC(EVAL_CALL_FUNCTION_VECTORCALL);
392396
if (((PyCodeObject *)f->func_code)->co_flags & CO_OPTIMIZED) {
393397
return _PyEval_Vector(tstate, f, NULL, stack, nargs, kwnames);
394398
}
@@ -520,7 +524,7 @@ _PyObject_CallFunctionVa(PyThreadState *tstate, PyObject *callable,
520524
if (stack == NULL) {
521525
return NULL;
522526
}
523-
527+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, callable);
524528
if (nargs == 1 && PyTuple_Check(stack[0])) {
525529
/* Special cases for backward compatibility:
526530
- PyObject_CallFunction(func, "O", tuple) calls func(*tuple)
@@ -815,6 +819,11 @@ object_vacall(PyThreadState *tstate, PyObject *base,
815819
stack[i] = va_arg(vargs, PyObject *);
816820
}
817821

822+
#ifdef Py_STATS
823+
if (PyFunction_Check(callable)) {
824+
EVAL_CALL_STAT_INC(EVAL_CALL_API);
825+
}
826+
#endif
818827
/* Call the function */
819828
result = _PyObject_VectorcallTstate(tstate, callable, stack, nargs, NULL);
820829

@@ -852,6 +861,7 @@ PyObject_VectorcallMethod(PyObject *name, PyObject *const *args,
852861
args++;
853862
nargsf--;
854863
}
864+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_METHOD, callable);
855865
PyObject *result = _PyObject_VectorcallTstate(tstate, callable,
856866
args, nargsf, kwnames);
857867
Py_DECREF(callable);

Objects/descrobject.c

+1
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,7 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value)
16771677
res = PyObject_CallOneArg(func, obj);
16781678
}
16791679
else {
1680+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, func);
16801681
PyObject *args[] = { obj, value };
16811682
res = PyObject_Vectorcall(func, args, 2, NULL);
16821683
}

Objects/genobject.c

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "pycore_pystate.h" // _PyThreadState_GET()
1414
#include "structmember.h" // PyMemberDef
1515
#include "opcode.h" // SEND
16+
#include "pystats.h"
1617

1718
static PyObject *gen_close(PyGenObject *, PyObject *);
1819
static PyObject *async_gen_asend_new(PyAsyncGenObject *, PyObject *);
@@ -218,6 +219,7 @@ gen_send_ex2(PyGenObject *gen, PyObject *arg, PyObject **presult,
218219
}
219220

220221
gen->gi_frame_state = FRAME_EXECUTING;
222+
EVAL_CALL_STAT_INC(EVAL_CALL_GENERATOR);
221223
result = _PyEval_EvalFrame(tstate, frame, exc);
222224
if (gen->gi_frame_state == FRAME_EXECUTING) {
223225
gen->gi_frame_state = FRAME_COMPLETED;

Objects/typeobject.c

+1
Original file line numberDiff line numberDiff line change
@@ -1653,6 +1653,7 @@ vectorcall_unbound(PyThreadState *tstate, int unbound, PyObject *func,
16531653
args++;
16541654
nargsf = nargsf - 1 + PY_VECTORCALL_ARGUMENTS_OFFSET;
16551655
}
1656+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_SLOT, func);
16561657
return _PyObject_VectorcallTstate(tstate, func, args, nargsf, NULL);
16571658
}
16581659

Python/bltinmodule.c

+1
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ builtin___build_class__(PyObject *self, PyObject *const *args, Py_ssize_t nargs,
198198
goto error;
199199
}
200200
PyThreadState *tstate = _PyThreadState_GET();
201+
EVAL_CALL_STAT_INC(EVAL_CALL_BUILD_CLASS);
201202
cell = _PyEval_Vector(tstate, (PyFunctionObject *)func, ns, NULL, 0, NULL);
202203
if (cell != NULL) {
203204
if (bases != orig_bases) {

Python/ceval.c

+4-1
Original file line numberDiff line numberDiff line change
@@ -1207,6 +1207,7 @@ PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals)
12071207
if (func == NULL) {
12081208
return NULL;
12091209
}
1210+
EVAL_CALL_STAT_INC(EVAL_CALL_LEGACY);
12101211
PyObject *res = _PyEval_Vector(tstate, func, locals, NULL, 0, NULL);
12111212
Py_DECREF(func);
12121213
return res;
@@ -6432,6 +6433,7 @@ _PyEval_Vector(PyThreadState *tstate, PyFunctionObject *func,
64326433
if (frame == NULL) {
64336434
return NULL;
64346435
}
6436+
EVAL_CALL_STAT_INC(EVAL_CALL_VECTOR);
64356437
PyObject *retval = _PyEval_EvalFrame(tstate, frame, 0);
64366438
assert(
64376439
_PyFrame_GetStackPointer(frame) == _PyFrame_Stackbase(frame) ||
@@ -6507,6 +6509,7 @@ PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
65076509
if (func == NULL) {
65086510
goto fail;
65096511
}
6512+
EVAL_CALL_STAT_INC(EVAL_CALL_LEGACY);
65106513
res = _PyEval_Vector(tstate, func, locals,
65116514
allargs, argcount,
65126515
kwnames);
@@ -7299,7 +7302,6 @@ do_call_core(PyThreadState *tstate,
72997302
)
73007303
{
73017304
PyObject *result;
7302-
73037305
if (PyCFunction_CheckExact(func) || PyCMethod_CheckExact(func)) {
73047306
C_TRACE(result, PyObject_Call(func, callargs, kwdict));
73057307
return result;
@@ -7329,6 +7331,7 @@ do_call_core(PyThreadState *tstate,
73297331
return result;
73307332
}
73317333
}
7334+
EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_FUNCTION_EX, func);
73327335
return PyObject_Call(func, callargs, kwdict);
73337336
}
73347337

Python/specialize.c

+4-1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ print_call_stats(FILE *out, CallStats *stats)
176176
fprintf(out, "Calls to Python functions inlined: %" PRIu64 "\n", stats->inlined_py_calls);
177177
fprintf(out, "Frames pushed: %" PRIu64 "\n", stats->frames_pushed);
178178
fprintf(out, "Frame objects created: %" PRIu64 "\n", stats->frame_objects_created);
179+
for (int i = 0; i < EVAL_CALL_KINDS; i++) {
180+
fprintf(out, "Calls via PyEval_EvalFrame[%d] : %" PRIu64 "\n", i, stats->eval_calls[i]);
181+
}
179182
}
180183

181184
static void
@@ -904,7 +907,7 @@ typedef enum {
904907
MANAGED_DICT = 2,
905908
OFFSET_DICT = 3,
906909
NO_DICT = 4,
907-
LAZY_DICT = 5,
910+
LAZY_DICT = 5,
908911
} ObjectDictKind;
909912

910913
// Please collect stats carefully before and after modifying. A subtle change

Tools/scripts/summarize_stats.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,14 @@ def extract_opcode_stats(stats):
108108
opcode_stats[int(n)][rest.strip(".")] = value
109109
return opcode_stats
110110

111-
def parse_kinds(spec_src):
111+
def parse_kinds(spec_src, prefix="SPEC_FAIL"):
112112
defines = collections.defaultdict(list)
113+
start = "#define " + prefix + "_"
113114
for line in spec_src:
114115
line = line.strip()
115-
if not line.startswith("#define SPEC_FAIL_"):
116+
if not line.startswith(start):
116117
continue
117-
line = line[len("#define SPEC_FAIL_"):]
118+
line = line[len(start):]
118119
name, val = line.split()
119120
defines[int(val.strip())].append(name.strip())
120121
return defines
@@ -129,8 +130,6 @@ def kind_to_text(kind, defines, opname):
129130
opname = "ATTR"
130131
if opname.endswith("SUBSCR"):
131132
opname = "SUBSCR"
132-
if opname.startswith("PRECALL"):
133-
opname = "CALL"
134133
for name in defines[kind]:
135134
if name.startswith(opname):
136135
return pretty(name[len(opname)+1:])
@@ -254,6 +253,9 @@ def emit_specialization_overview(opcode_stats, total):
254253
))
255254

256255
def emit_call_stats(stats):
256+
stats_path = os.path.join(os.path.dirname(__file__), "../../Include/pystats.h")
257+
with open(stats_path) as stats_src:
258+
defines = parse_kinds(stats_src, prefix="EVAL_CALL")
257259
with Section("Call stats", summary="Inlined calls and frame stats"):
258260
total = 0
259261
for key, value in stats.items():
@@ -263,6 +265,11 @@ def emit_call_stats(stats):
263265
for key, value in stats.items():
264266
if "Calls to" in key:
265267
rows.append((key, value, f"{100*value/total:0.1f}%"))
268+
elif key.startswith("Calls "):
269+
name, index = key[:-1].split("[")
270+
index = int(index)
271+
label = name + " (" + pretty(defines[index][0]) + ")"
272+
rows.append((label, value, f"{100*value/total:0.1f}%"))
266273
for key, value in stats.items():
267274
if key.startswith("Frame"):
268275
rows.append((key, value, f"{100*value/total:0.1f}%"))

0 commit comments

Comments
 (0)