Skip to content

Commit

Permalink
pythongh-108753: Enhance pystats
Browse files Browse the repository at this point in the history
* Add PYTHONSTATS environment variable.
* Add PyConfig._pystats member.
* Add tests on sys functions and on setting PyConfig._pystats to 1.
* At exit, only dump statistics if PyConfig._pystats is non-zero.
* Add Include/cpython/pystats.h and Include/internal/pycore_pystats.h
  header files.
* Rename '_py_stats' variable to '_Py_stats'.
* Exclude Include/cpython/pystats.h from the Py_LIMITED_API.
* Move pystats.h include from object.h to Python.h.
* Add _Py_StatsOn() and _Py_StatsOff() functions. Remove
  '_py_stats_struct' variable from the API: make it static in
  specialize.c.
* Document API in Include/pystats.h and Include/cpython/pystats.h.
* Complete pystats documentation in Doc/using/configure.rst.
  • Loading branch information
vstinner committed Sep 1, 2023
1 parent e775601 commit aee6b33
Show file tree
Hide file tree
Showing 22 changed files with 347 additions and 171 deletions.
58 changes: 55 additions & 3 deletions Doc/using/configure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,66 @@ General Options

.. cmdoption:: --enable-pystats

Turn on internal statistics gathering.
Turn on internal Python performance statistics gathering.

By default, statistics gathering is off. Use ``python3 -X pystats`` command
or set ``PYTHONSTATS=1`` environment variable to turn on statistics
gathering at Python startup and to dump statistics at Python exit.

Effects:

* Add :option:`-X pystats <-X>` command line option.
* Add :envvar:`PYTHONSTATS` environment variable.
* Define the ``Py_STATS`` macro.
* Add functions to the :mod:`sys` module:

* :func:`sys._stats_on`: Turns on statistics gathering.
* :func:`sys._stats_off`: Turns off statistics gathering.
* :func:`sys._stats_clear`: Clears the statistics.
* :func:`sys._stats_dump`: Dump statistics to file, and clears the statistics.

The statistics will be dumped to a arbitrary (probably unique) file in
``/tmp/py_stats/``, or ``C:\temp\py_stats\`` on Windows. If that directory
does not exist, results will be printed on stdout.
``/tmp/py_stats/`` (Unix) or ``C:\temp\py_stats\`` (Windows). If that
directory does not exist, results will be printed on stdout.

Use ``Tools/scripts/summarize_stats.py`` to read the stats.

Statistics:

* Opcode:

* Specialization: success, failure, hit, deferred, miss, deopt, failures;
* Execution count;
* Pair count.

* Call:

* Inlined Python calls;
* PyEval calls;
* Frames pushed;
* Frame object created;
* Eval calls: vector, generator, legacy, function VECTORCALL, build class,
slot, function "ex", API, method.

* Object:

* incref and decref;
* interpreter incref and decref;
* allocations: all, 512 bytes, 4 kiB, big;
* free;
* to/from free lists;
* dictionary materialized/dematerialized;
* type cache;
* optimization attemps;
* optimization traces created/executed;
* uops executed.

* Garbage collector:

* Garbage collections;
* Objects visited;
* Objects collected.

.. versionadded:: 3.11

.. cmdoption:: --disable-gil
Expand Down
1 change: 1 addition & 0 deletions Include/Python.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "pymem.h"
#include "pytypedefs.h"
#include "pybuffer.h"
#include "pystats.h"
#include "object.h"
#include "objimpl.h"
#include "typeslots.h"
Expand Down
5 changes: 5 additions & 0 deletions Include/cpython/initconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ typedef struct PyConfig {

// If non-zero, we believe we're running from a source tree.
int _is_python_build;

#ifdef Py_STATS
// If non-zero, turns on statistics gathering.
int _pystats;
#endif
} PyConfig;

PyAPI_FUNC(void) PyConfig_InitPythonConfig(PyConfig *config);
Expand Down
120 changes: 120 additions & 0 deletions Include/cpython/pystats.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Statistics on Python performance.
//
// API:
//
// - _Py_INCREF_STAT_INC() and _Py_DECREF_STAT_INC() used by Py_INCREF()
// and Py_DECREF().
// - _Py_stats variable
//
// Functions of the sys module:
//
// - sys._stats_on()
// - sys._stats_off()
// - sys._stats_clear()
// - sys._stats_dump()
//
// Python must be built with ./configure --enable-pystats to define the
// Py_STATS macro.
//
// Define _PY_INTERPRETER macro to increment interpreter_increfs and
// interpreter_decrefs. Otherwise, increment increfs and decrefs.

#ifndef Py_CPYTHON_PYSTATS_H
# error "this header file must not be included directly"
#endif

#define SPECIALIZATION_FAILURE_KINDS 36

/* Stats for determining who is calling PyEval_EvalFrame */
#define EVAL_CALL_TOTAL 0
#define EVAL_CALL_VECTOR 1
#define EVAL_CALL_GENERATOR 2
#define EVAL_CALL_LEGACY 3
#define EVAL_CALL_FUNCTION_VECTORCALL 4
#define EVAL_CALL_BUILD_CLASS 5
#define EVAL_CALL_SLOT 6
#define EVAL_CALL_FUNCTION_EX 7
#define EVAL_CALL_API 8
#define EVAL_CALL_METHOD 9

#define EVAL_CALL_KINDS 10

typedef struct _specialization_stats {
uint64_t success;
uint64_t failure;
uint64_t hit;
uint64_t deferred;
uint64_t miss;
uint64_t deopt;
uint64_t failure_kinds[SPECIALIZATION_FAILURE_KINDS];
} SpecializationStats;

typedef struct _opcode_stats {
SpecializationStats specialization;
uint64_t execution_count;
uint64_t pair_count[256];
} OpcodeStats;

typedef struct _call_stats {
uint64_t inlined_py_calls;
uint64_t pyeval_calls;
uint64_t frames_pushed;
uint64_t frame_objects_created;
uint64_t eval_calls[EVAL_CALL_KINDS];
} CallStats;

typedef struct _object_stats {
uint64_t increfs;
uint64_t decrefs;
uint64_t interpreter_increfs;
uint64_t interpreter_decrefs;
uint64_t allocations;
uint64_t allocations512;
uint64_t allocations4k;
uint64_t allocations_big;
uint64_t frees;
uint64_t to_freelist;
uint64_t from_freelist;
uint64_t new_values;
uint64_t dict_materialized_on_request;
uint64_t dict_materialized_new_key;
uint64_t dict_materialized_too_big;
uint64_t dict_materialized_str_subclass;
uint64_t dict_dematerialized;
uint64_t type_cache_hits;
uint64_t type_cache_misses;
uint64_t type_cache_dunder_hits;
uint64_t type_cache_dunder_misses;
uint64_t type_cache_collisions;
uint64_t optimization_attempts;
uint64_t optimization_traces_created;
uint64_t optimization_traces_executed;
uint64_t optimization_uops_executed;
/* Temporary value used during GC */
uint64_t object_visits;
} ObjectStats;

typedef struct _gc_stats {
uint64_t collections;
uint64_t object_visits;
uint64_t objects_collected;
} GCStats;

typedef struct _stats {
OpcodeStats opcode_stats[256];
CallStats call_stats;
ObjectStats object_stats;
GCStats *gc_stats;
} PyStats;


// Export for shared extensions like 'math'
PyAPI_DATA(PyStats*) _Py_stats;

#ifdef _PY_INTERPRETER
# define _Py_INCREF_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.interpreter_increfs++; } while (0)
# define _Py_DECREF_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.interpreter_decrefs++; } while (0)
#else
# define _Py_INCREF_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.increfs++; } while (0)
# define _Py_DECREF_STAT_INC() do { if (_Py_stats) _Py_stats->object_stats.decrefs++; } while (0)
#endif
2 changes: 1 addition & 1 deletion Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ extern int _PyEval_SetAsyncGenFinalizer(PyObject *);
extern int _PyEval_GetCoroutineOriginTrackingDepth(void);
extern int _PyEval_SetCoroutineOriginTrackingDepth(int depth);

extern void _PyEval_Fini(void);
extern void _PyEval_Fini(PyThreadState *tstate);


extern PyObject* _PyEval_GetBuiltins(PyThreadState *tstate);
Expand Down
18 changes: 9 additions & 9 deletions Include/internal/pycore_code.h
Original file line number Diff line number Diff line change
Expand Up @@ -268,17 +268,17 @@ extern int _PyStaticCode_Init(PyCodeObject *co);

#ifdef Py_STATS

#define STAT_INC(opname, name) do { if (_py_stats) _py_stats->opcode_stats[opname].specialization.name++; } while (0)
#define STAT_DEC(opname, name) do { if (_py_stats) _py_stats->opcode_stats[opname].specialization.name--; } while (0)
#define OPCODE_EXE_INC(opname) do { if (_py_stats) _py_stats->opcode_stats[opname].execution_count++; } while (0)
#define CALL_STAT_INC(name) do { if (_py_stats) _py_stats->call_stats.name++; } while (0)
#define OBJECT_STAT_INC(name) do { if (_py_stats) _py_stats->object_stats.name++; } while (0)
#define STAT_INC(opname, name) do { if (_Py_stats) _Py_stats->opcode_stats[opname].specialization.name++; } while (0)
#define STAT_DEC(opname, name) do { if (_Py_stats) _Py_stats->opcode_stats[opname].specialization.name--; } while (0)
#define OPCODE_EXE_INC(opname) do { if (_Py_stats) _Py_stats->opcode_stats[opname].execution_count++; } while (0)
#define CALL_STAT_INC(name) do { if (_Py_stats) _Py_stats->call_stats.name++; } while (0)
#define OBJECT_STAT_INC(name) do { if (_Py_stats) _Py_stats->object_stats.name++; } while (0)
#define OBJECT_STAT_INC_COND(name, cond) \
do { if (_py_stats && cond) _py_stats->object_stats.name++; } while (0)
#define EVAL_CALL_STAT_INC(name) do { if (_py_stats) _py_stats->call_stats.eval_calls[name]++; } while (0)
do { if (_Py_stats && cond) _Py_stats->object_stats.name++; } while (0)
#define EVAL_CALL_STAT_INC(name) do { if (_Py_stats) _Py_stats->call_stats.eval_calls[name]++; } while (0)
#define EVAL_CALL_STAT_INC_IF_FUNCTION(name, callable) \
do { if (_py_stats && PyFunction_Check(callable)) _py_stats->call_stats.eval_calls[name]++; } while (0)
#define GC_STAT_ADD(gen, name, n) do { if (_py_stats) _py_stats->gc_stats[(gen)].name += (n); } while (0)
do { if (_Py_stats && PyFunction_Check(callable)) _Py_stats->call_stats.eval_calls[name]++; } while (0)
#define GC_STAT_ADD(gen, name, n) do { if (_Py_stats) _Py_stats->gc_stats[(gen)].name += (n); } while (0)

// Export for '_opcode' shared extension
PyAPI_FUNC(PyObject*) _Py_GetSpecializationStats(void);
Expand Down
21 changes: 21 additions & 0 deletions Include/internal/pycore_pystats.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef Py_INTERNAL_PYSTATS_H
#define Py_INTERNAL_PYSTATS_H
#ifdef __cplusplus
extern "C" {
#endif

#ifndef Py_BUILD_CORE
# error "this header requires Py_BUILD_CORE define"
#endif

#ifdef Py_STATS
extern void _Py_StatsOn(void);
extern void _Py_StatsOff(void);
extern void _Py_StatsClear(void);
extern void _Py_PrintSpecializationStats(int to_file);
#endif

#ifdef __cplusplus
}
#endif
#endif // !Py_INTERNAL_PYSTATS_H
2 changes: 0 additions & 2 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ A standard interface exists for objects that contain an array of items
whose size is determined when the object is allocated.
*/

#include "pystats.h"

/* Py_DEBUG implies Py_REF_DEBUG. */
#if defined(Py_DEBUG) && !defined(Py_REF_DEBUG)
# define Py_REF_DEBUG
Expand Down
Loading

0 comments on commit aee6b33

Please sign in to comment.