gh-96143: Allow Linux perf profiler to see Python calls (GH-96123)

⚠️  ⚠️ Note for reviewers, hackers and fellow systems/low-level/compiler engineers ⚠️ ⚠️ 

If you have a lot of experience with this kind of shenanigans and want to improve the **first** version, **please make a PR against my branch** or **reach out by email** or **suggest code changes directly on GitHub**. 

If you have any **refinements or optimizations** please, wait until the first version is merged before starting hacking or proposing those so we can keep this PR productive.
This commit is contained in:
Pablo Galindo Salgado 2022-08-30 18:11:18 +01:00 committed by GitHub
parent 0f733fffe8
commit 6d791a9736
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1412 additions and 2 deletions

View file

@ -1151,6 +1151,79 @@ sys_getandroidapilevel(PyObject *module, PyObject *Py_UNUSED(ignored))
#endif /* defined(ANDROID_API_LEVEL) */
PyDoc_STRVAR(sys_activate_stack_trampoline__doc__,
"activate_stack_trampoline($module, backend, /)\n"
"--\n"
"\n"
"Activate the perf profiler trampoline.");
#define SYS_ACTIVATE_STACK_TRAMPOLINE_METHODDEF \
{"activate_stack_trampoline", (PyCFunction)sys_activate_stack_trampoline, METH_O, sys_activate_stack_trampoline__doc__},
static PyObject *
sys_activate_stack_trampoline_impl(PyObject *module, const char *backend);
static PyObject *
sys_activate_stack_trampoline(PyObject *module, PyObject *arg)
{
PyObject *return_value = NULL;
const char *backend;
if (!PyUnicode_Check(arg)) {
_PyArg_BadArgument("activate_stack_trampoline", "argument", "str", arg);
goto exit;
}
Py_ssize_t backend_length;
backend = PyUnicode_AsUTF8AndSize(arg, &backend_length);
if (backend == NULL) {
goto exit;
}
if (strlen(backend) != (size_t)backend_length) {
PyErr_SetString(PyExc_ValueError, "embedded null character");
goto exit;
}
return_value = sys_activate_stack_trampoline_impl(module, backend);
exit:
return return_value;
}
PyDoc_STRVAR(sys_deactivate_stack_trampoline__doc__,
"deactivate_stack_trampoline($module, /)\n"
"--\n"
"\n"
"Dectivate the perf profiler trampoline.");
#define SYS_DEACTIVATE_STACK_TRAMPOLINE_METHODDEF \
{"deactivate_stack_trampoline", (PyCFunction)sys_deactivate_stack_trampoline, METH_NOARGS, sys_deactivate_stack_trampoline__doc__},
static PyObject *
sys_deactivate_stack_trampoline_impl(PyObject *module);
static PyObject *
sys_deactivate_stack_trampoline(PyObject *module, PyObject *Py_UNUSED(ignored))
{
return sys_deactivate_stack_trampoline_impl(module);
}
PyDoc_STRVAR(sys_is_stack_trampoline_active__doc__,
"is_stack_trampoline_active($module, /)\n"
"--\n"
"\n"
"Returns *True* if the perf profiler trampoline is active.");
#define SYS_IS_STACK_TRAMPOLINE_ACTIVE_METHODDEF \
{"is_stack_trampoline_active", (PyCFunction)sys_is_stack_trampoline_active, METH_NOARGS, sys_is_stack_trampoline_active__doc__},
static PyObject *
sys_is_stack_trampoline_active_impl(PyObject *module);
static PyObject *
sys_is_stack_trampoline_active(PyObject *module, PyObject *Py_UNUSED(ignored))
{
return sys_is_stack_trampoline_active_impl(module);
}
#ifndef SYS_GETWINDOWSVERSION_METHODDEF
#define SYS_GETWINDOWSVERSION_METHODDEF
#endif /* !defined(SYS_GETWINDOWSVERSION_METHODDEF) */
@ -1194,4 +1267,4 @@ sys_getandroidapilevel(PyObject *module, PyObject *Py_UNUSED(ignored))
#ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
#define SYS_GETANDROIDAPILEVEL_METHODDEF
#endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
/*[clinic end generated code: output=322fb0409e376ad4 input=a9049054013a1b77]*/
/*[clinic end generated code: output=43b44240211afe95 input=a9049054013a1b77]*/

View file

@ -118,6 +118,11 @@ The following implementation-specific options are available:\n\
files are desired as well as suppressing the extra visual location indicators \n\
when the interpreter displays tracebacks.\n\
\n\
-X perf: activate support for the Linux \"perf\" profiler by activating the \"perf\"\n\
trampoline. When this option is activated, the Linux \"perf\" profiler will be \n\
able to report Python calls. This option is only available on some platforms and will \n\
do nothing if is not supported on the current system. The default value is \"off\".\n\
\n\
-X frozen_modules=[on|off]: whether or not frozen modules should be used.\n\
The default is \"on\" (or \"off\" if you are running a local build).";
@ -745,6 +750,7 @@ _PyConfig_InitCompatConfig(PyConfig *config)
config->use_hash_seed = -1;
config->faulthandler = -1;
config->tracemalloc = -1;
config->perf_profiling = -1;
config->module_search_paths_set = 0;
config->parse_argv = 0;
config->site_import = -1;
@ -829,6 +835,7 @@ PyConfig_InitIsolatedConfig(PyConfig *config)
config->use_hash_seed = 0;
config->faulthandler = 0;
config->tracemalloc = 0;
config->perf_profiling = 0;
config->safe_path = 1;
config->pathconfig_warnings = 0;
#ifdef MS_WINDOWS
@ -940,6 +947,7 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2)
COPY_ATTR(_install_importlib);
COPY_ATTR(faulthandler);
COPY_ATTR(tracemalloc);
COPY_ATTR(perf_profiling);
COPY_ATTR(import_time);
COPY_ATTR(code_debug_ranges);
COPY_ATTR(show_ref_count);
@ -1050,6 +1058,7 @@ _PyConfig_AsDict(const PyConfig *config)
SET_ITEM_UINT(hash_seed);
SET_ITEM_INT(faulthandler);
SET_ITEM_INT(tracemalloc);
SET_ITEM_INT(perf_profiling);
SET_ITEM_INT(import_time);
SET_ITEM_INT(code_debug_ranges);
SET_ITEM_INT(show_ref_count);
@ -1331,6 +1340,7 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict)
CHECK_VALUE("hash_seed", config->hash_seed <= MAX_HASH_SEED);
GET_UINT(faulthandler);
GET_UINT(tracemalloc);
GET_UINT(perf_profiling);
GET_UINT(import_time);
GET_UINT(code_debug_ranges);
GET_UINT(show_ref_count);
@ -1687,6 +1697,26 @@ config_read_env_vars(PyConfig *config)
return _PyStatus_OK();
}
static PyStatus
config_init_perf_profiling(PyConfig *config)
{
int active = 0;
const char *env = config_get_env(config, "PYTHONPERFSUPPORT");
if (env) {
if (_Py_str_to_int(env, &active) != 0) {
active = 0;
}
if (active) {
config->perf_profiling = 1;
}
}
const wchar_t *xoption = config_get_xoption(config, L"perf");
if (xoption) {
config->perf_profiling = 1;
}
return _PyStatus_OK();
}
static PyStatus
config_init_tracemalloc(PyConfig *config)
@ -1788,6 +1818,12 @@ config_read_complex_options(PyConfig *config)
return status;
}
}
if (config->perf_profiling < 0) {
status = config_init_perf_profiling(config);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
}
if (config->pycache_prefix == NULL) {
status = config_init_pycache_prefix(config);
@ -2104,6 +2140,9 @@ config_read(PyConfig *config, int compute_path_config)
if (config->tracemalloc < 0) {
config->tracemalloc = 0;
}
if (config->perf_profiling < 0) {
config->perf_profiling = 0;
}
if (config->use_hash_seed < 0) {
config->use_hash_seed = 0;
config->hash_seed = 0;

View file

@ -1149,6 +1149,16 @@ init_interp_main(PyThreadState *tstate)
if (_PyTraceMalloc_Init(config->tracemalloc) < 0) {
return _PyStatus_ERR("can't initialize tracemalloc");
}
#ifdef PY_HAVE_PERF_TRAMPOLINE
if (config->perf_profiling) {
if (_PyPerfTrampoline_SetCallbacks(&_Py_perfmap_callbacks) < 0 ||
_PyPerfTrampoline_Init(config->perf_profiling) < 0) {
return _PyStatus_ERR("can't initialize the perf trampoline");
}
}
#endif
}
status = init_sys_streams(tstate);
@ -1723,6 +1733,7 @@ finalize_interp_clear(PyThreadState *tstate)
_PyArg_Fini();
_Py_ClearFileSystemEncoding();
_Py_Deepfreeze_Fini();
_PyPerfTrampoline_Fini();
}
finalize_interp_types(tstate->interp);

View file

@ -2053,6 +2053,80 @@ sys_getandroidapilevel_impl(PyObject *module)
}
#endif /* ANDROID_API_LEVEL */
/*[clinic input]
sys.activate_stack_trampoline
backend: str
/
Activate the perf profiler trampoline.
[clinic start generated code]*/
static PyObject *
sys_activate_stack_trampoline_impl(PyObject *module, const char *backend)
/*[clinic end generated code: output=5783cdeb51874b43 input=b09020e3a17c78c5]*/
{
#ifdef PY_HAVE_PERF_TRAMPOLINE
if (strcmp(backend, "perf") == 0) {
_PyPerf_Callbacks cur_cb;
_PyPerfTrampoline_GetCallbacks(&cur_cb);
if (cur_cb.init_state != _Py_perfmap_callbacks.init_state) {
if (_PyPerfTrampoline_SetCallbacks(&_Py_perfmap_callbacks) < 0 ) {
PyErr_SetString(PyExc_ValueError, "can't activate perf trampoline");
return NULL;
}
}
}
else {
PyErr_Format(PyExc_ValueError, "invalid backend: %s", backend);
return NULL;
}
if (_PyPerfTrampoline_Init(1) < 0) {
return NULL;
}
Py_RETURN_NONE;
#else
PyErr_SetString(PyExc_ValueError, "perf trampoline not available");
return NULL;
#endif
}
/*[clinic input]
sys.deactivate_stack_trampoline
Dectivate the perf profiler trampoline.
[clinic start generated code]*/
static PyObject *
sys_deactivate_stack_trampoline_impl(PyObject *module)
/*[clinic end generated code: output=b50da25465df0ef1 input=491f4fc1ed615736]*/
{
if (_PyPerfTrampoline_Init(0) < 0) {
return NULL;
}
Py_RETURN_NONE;
}
/*[clinic input]
sys.is_stack_trampoline_active
Returns *True* if the perf profiler trampoline is active.
[clinic start generated code]*/
static PyObject *
sys_is_stack_trampoline_active_impl(PyObject *module)
/*[clinic end generated code: output=ab2746de0ad9d293 input=061fa5776ac9dd59]*/
{
#ifdef PY_HAVE_PERF_TRAMPOLINE
if (_PyIsPerfTrampolineActive()) {
Py_RETURN_TRUE;
}
#endif
Py_RETURN_FALSE;
}
static PyMethodDef sys_methods[] = {
/* Might as well keep this in alphabetic order */
SYS_ADDAUDITHOOK_METHODDEF
@ -2108,6 +2182,9 @@ static PyMethodDef sys_methods[] = {
METH_VARARGS | METH_KEYWORDS, set_asyncgen_hooks_doc},
SYS_GET_ASYNCGEN_HOOKS_METHODDEF
SYS_GETANDROIDAPILEVEL_METHODDEF
SYS_ACTIVATE_STACK_TRAMPOLINE_METHODDEF
SYS_DEACTIVATE_STACK_TRAMPOLINE_METHODDEF
SYS_IS_STACK_TRAMPOLINE_ACTIVE_METHODDEF
SYS_UNRAISABLEHOOK_METHODDEF
#ifdef Py_STATS
SYS__STATS_ON_METHODDEF