From: Ian Rogers <irogers@google.com>
To: Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@redhat.com>,
Arnaldo Carvalho de Melo <acme@kernel.org>,
Namhyung Kim <namhyung@kernel.org>, Jiri Olsa <jolsa@kernel.org>,
Ian Rogers <irogers@google.com>,
Adrian Hunter <adrian.hunter@intel.com>,
James Clark <james.clark@linaro.org>,
Thomas Falcon <thomas.falcon@intel.com>,
Alice Rogers <alice.mei.rogers@gmail.com>,
Changbin Du <changbin.du@huawei.com>,
Tengda Wu <wutengda@huaweicloud.com>, tanze <tanze@kylinos.cn>,
Athira Rajeev <atrajeev@linux.ibm.com>,
Dapeng Mi <dapeng1.mi@linux.intel.com>,
linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org
Subject: [PATCH v1 05/13] perf python: Release the GIL while processing session events
Date: Fri, 2 Oct 2026 11:26:14 -0700 [thread overview]
Message-ID: <20261002182624.3259797-6-irogers@google.com> (raw)
In-Reply-To: <20261002182624.3259797-1-irogers@google.com>
When a python session reads from a pipe, perf_session__new() and
perf_session__process_events() block waiting for data with the GIL held,
freezing any other python threads such as a TUI thread. Release the GIL
around both calls and acquire it with PyGILState_Ensure() in the tool and
call-return callbacks that invoke python.
Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/python.c | 258 ++++++++++++++++++++++++++++++++-------
1 file changed, 215 insertions(+), 43 deletions(-)
diff --git a/tools/perf/util/python.c b/tools/perf/util/python.c
index 251b7d517ad5..4eb17bdc5fd1 100644
--- a/tools/perf/util/python.c
+++ b/tools/perf/util/python.c
@@ -52,6 +52,7 @@
#include "srccode.h"
#include "srcline.h"
#include "strbuf.h"
+#include "strlist.h"
#include "symbol.h"
#include "stat.h"
#include "header.h"
@@ -4104,16 +4105,38 @@ static void pyrf_thread__delete(struct pyrf_thread *pthread)
static PyObject *pyrf_thread__comm(PyObject *obj)
{
struct pyrf_thread *pthread = (void *)obj;
- const char *str;
+ struct thread *thread = pthread->thread;
+ bool has_comm = false;
+ char *str = NULL;
+ PyObject *ret;
- CHECK_INITIALIZED(pthread->thread, "perf.thread");
+ CHECK_INITIALIZED(thread, "perf.thread");
- str = thread__comm_str(pthread->thread);
+ /*
+ * Copy the comm string under comm_lock so a concurrent PERF_RECORD_COMM
+ * in perf_session__process_events() (which runs with the GIL released)
+ * cannot free the old comm_str via comm__override() while it is being
+ * read, and drop comm_lock before calling into the Python allocator.
+ */
+ down_read(thread__comm_lock(thread));
+ if (!list_empty(thread__comm_list(thread))) {
+ const struct comm *comm;
+
+ comm = list_first_entry(thread__comm_list(thread), struct comm, list);
+ if (comm && comm__str(comm)) {
+ has_comm = true;
+ str = strdup(comm__str(comm));
+ }
+ }
+ up_read(thread__comm_lock(thread));
- if (!str)
+ if (!has_comm)
Py_RETURN_NONE;
-
- return PyUnicode_FromString(str);
+ if (!str)
+ return PyErr_NoMemory();
+ ret = PyUnicode_FromString(str);
+ free(str);
+ return ret;
}
static PyMethodDef pyrf_thread__methods[] = {
@@ -4211,6 +4234,8 @@ struct pyrf_session {
* that event processing stops, see pyrf_session__callback_raised().
*/
bool callback_raised;
+ unsigned int callback_depth;
+ unsigned long callback_thread;
char *vmlinux_name;
char *kallsyms_name;
char *symfs;
@@ -4579,11 +4604,10 @@ static bool pyrf_session__exception_pending(void)
return PyErr_Occurred() != NULL;
}
-static int pyrf_session__call_return_process(struct call_return *cr,
- u64 *parent_db_id,
- void *data)
+static int __pyrf_session__call_return_process(struct call_return *cr,
+ u64 *parent_db_id,
+ struct pyrf_session *psession)
{
- struct pyrf_session *psession = data;
PyObject *pyevent, *ret;
if (!psession->call_return || pyrf_session__exception_pending())
@@ -4610,6 +4634,38 @@ static int pyrf_session__call_return_process(struct call_return *cr,
return 0;
}
+static PyGILState_STATE pyrf_session__callback_enter(struct pyrf_session *psession)
+{
+ PyGILState_STATE gstate = PyGILState_Ensure();
+
+ psession->callback_thread = PyThread_get_thread_ident();
+ psession->callback_depth++;
+ return gstate;
+}
+
+static void pyrf_session__callback_leave(struct pyrf_session *psession,
+ PyGILState_STATE gstate)
+{
+ psession->callback_depth--;
+ PyGILState_Release(gstate);
+}
+
+/*
+ * Call returns may be processed during a sample callback, or when thread
+ * stacks are flushed, so acquire the GIL, see pyrf_session_tool__sample().
+ */
+static int pyrf_session__call_return_process(struct call_return *cr,
+ u64 *parent_db_id,
+ void *data)
+{
+ struct pyrf_session *psession = data;
+ PyGILState_STATE gstate = pyrf_session__callback_enter(psession);
+ int ret = __pyrf_session__call_return_process(cr, parent_db_id, psession);
+
+ pyrf_session__callback_leave(psession, gstate);
+ return ret;
+}
+
static int pyrf_session__call_event(struct pyrf_session *psession, PyObject *callback,
PyObject *pyevent, const char *extra_arg, bool has_extra_arg)
{
@@ -4645,10 +4701,10 @@ static int pyrf_session__call_event(struct pyrf_session *psession, PyObject *cal
return 0;
}
-static int pyrf_session_tool__sample(const struct perf_tool *tool,
- union perf_event *event,
- struct perf_sample *sample,
- struct machine *machine)
+static int __pyrf_session_tool__sample(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
{
struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
u64 sample_db_id = ++psession->sample_last_db_id;
@@ -4706,10 +4762,10 @@ static int pyrf_session_tool__sample(const struct perf_tool *tool,
return 0;
}
-static int pyrf_session_tool__context_switch(const struct perf_tool *tool,
- union perf_event *event,
- struct perf_sample *sample,
- struct machine *machine)
+static int __pyrf_session_tool__context_switch(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
{
struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
PyObject *pyevent;
@@ -4736,9 +4792,9 @@ static int pyrf_session_tool__context_switch(const struct perf_tool *tool,
/*extra_arg=*/NULL, /*has_extra_arg=*/false);
}
-static int pyrf_session_tool__stat(const struct perf_tool *tool,
- struct perf_session *session,
- union perf_event *event)
+static int __pyrf_session_tool__stat(const struct perf_tool *tool,
+ struct perf_session *session,
+ union perf_event *event)
{
struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
struct evsel *evsel = evlist__id2evsel(session->evlist, event->stat.id);
@@ -4757,9 +4813,9 @@ static int pyrf_session_tool__stat(const struct perf_tool *tool,
name, /*has_extra_arg=*/true);
}
-static int pyrf_session_tool__stat_round(const struct perf_tool *tool,
- struct perf_session *session __maybe_unused,
- union perf_event *event)
+static int __pyrf_session_tool__stat_round(const struct perf_tool *tool,
+ struct perf_session *session __maybe_unused,
+ union perf_event *event)
{
struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
PyObject *pyevent;
@@ -4776,6 +4832,84 @@ static int pyrf_session_tool__stat_round(const struct perf_tool *tool,
/*extra_arg=*/NULL, /*has_extra_arg=*/true);
}
+/*
+ * The session currently inside perf_session__new() or
+ * perf_session__process_events() with the GIL released. Because symbol_conf
+ * and session_done are process-globals and perf_session is not thread-safe,
+ * only one session may be active at a time. Modified with the GIL held.
+ */
+static struct pyrf_session *pyrf_session__active;
+
+static bool pyrf_session__check_not_busy(struct pyrf_session *psession)
+{
+ if (pyrf_session__active == psession &&
+ (!psession->callback_depth ||
+ psession->callback_thread != PyThread_get_thread_ident())) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "perf.session is busy processing events in another thread");
+ return false;
+ }
+ return true;
+}
+
+/*
+ * The GIL is released while events are processed, see
+ * pyrf_session__find_thread_events(), so that other python threads can run,
+ * such as a UI thread while a session reads from a pipe that may block. The
+ * tool callbacks that use python acquire the GIL, PyGILState_Ensure() is
+ * reentrant so nested callbacks, like call returns during a sample, work.
+ */
+
+static int pyrf_session_tool__sample(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
+ PyGILState_STATE gstate = pyrf_session__callback_enter(psession);
+ int ret = __pyrf_session_tool__sample(tool, event, sample, machine);
+
+ pyrf_session__callback_leave(psession, gstate);
+ return ret;
+}
+
+static int pyrf_session_tool__context_switch(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
+ PyGILState_STATE gstate = pyrf_session__callback_enter(psession);
+ int ret = __pyrf_session_tool__context_switch(tool, event, sample, machine);
+
+ pyrf_session__callback_leave(psession, gstate);
+ return ret;
+}
+
+static int pyrf_session_tool__stat(const struct perf_tool *tool,
+ struct perf_session *session,
+ union perf_event *event)
+{
+ struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
+ PyGILState_STATE gstate = pyrf_session__callback_enter(psession);
+ int ret = __pyrf_session_tool__stat(tool, session, event);
+
+ pyrf_session__callback_leave(psession, gstate);
+ return ret;
+}
+
+static int pyrf_session_tool__stat_round(const struct perf_tool *tool,
+ struct perf_session *session,
+ union perf_event *event)
+{
+ struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
+ PyGILState_STATE gstate = pyrf_session__callback_enter(psession);
+ int ret = __pyrf_session_tool__stat_round(tool, session, event);
+
+ pyrf_session__callback_leave(psession, gstate);
+ return ret;
+}
+
static PyObject *pyrf_session__find_thread(struct pyrf_session *psession, PyObject *args)
{
struct machine *machine;
@@ -4784,6 +4918,8 @@ static PyObject *pyrf_session__find_thread(struct pyrf_session *psession, PyObje
int pid, tid = -1;
CHECK_INITIALIZED(psession->session, "session");
+ if (!pyrf_session__check_not_busy(psession))
+ return NULL;
if (!PyArg_ParseTuple(args, "i|i", &pid, &tid))
return NULL;
@@ -4854,6 +4990,8 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject
psession->call_return_last_db_id = 0;
psession->sample_last_db_id = 0;
psession->callback_raised = false;
+ psession->callback_depth = 0;
+ psession->callback_thread = 0;
psession->vmlinux_name = NULL;
psession->kallsyms_name = NULL;
psession->symfs = NULL;
@@ -4936,7 +5074,6 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject
PyErr_NoMemory();
goto err_out;
}
- symbol_conf.vmlinux_name = psession->vmlinux_name;
}
if (!kallsyms_str)
@@ -4947,7 +5084,6 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject
PyErr_NoMemory();
goto err_out;
}
- symbol_conf.kallsyms_name = psession->kallsyms_name;
}
if (!symfs_str)
@@ -4958,16 +5094,7 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject
PyErr_NoMemory();
goto err_out;
}
- symbol_conf.symfs = psession->symfs;
- }
-
- session = perf_session__new(&pdata->data, &psession->tool);
- if (IS_ERR(session)) {
- PyErr_Format(PyExc_IOError, "failed to create session: %ld", PTR_ERR(session));
- goto err_out;
}
- psession->session = session;
- psession->session->itrace_synth_opts = &psession->itrace_opts;
if (!itrace_str)
itrace_str = getenv("PERF_ITRACE");
@@ -4975,18 +5102,43 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject
memset(&psession->itrace_opts, 0, sizeof(psession->itrace_opts));
if (itrace_do_parse_synth_opts(&psession->itrace_opts, itrace_str, 0) < 0) {
PyErr_SetString(PyExc_ValueError, "Failed to parse itrace options");
- Py_DECREF(psession);
- return NULL;
+ goto err_out;
}
}
+ if (pyrf_session__active) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Another perf.session is currently active");
+ goto err_out;
+ }
+
+ symbol_conf.vmlinux_name = psession->vmlinux_name;
+ symbol_conf.kallsyms_name = psession->kallsyms_name;
+ symbol_conf.symfs = psession->symfs ?: "";
symbol_conf.use_callchain = true;
symbol_conf.show_kernel_path = true;
symbol_conf.inline_name = false;
+
+ /* Reading the header of a pipe may block, let other python threads run. */
+ pyrf_session__active = psession;
+ Py_BEGIN_ALLOW_THREADS
+ session = perf_session__new(&pdata->data, &psession->tool);
+ Py_END_ALLOW_THREADS
+ pyrf_session__active = NULL;
+ if (!psession->kallsyms_name && symbol_conf.kallsyms_name)
+ psession->kallsyms_name = (char *)symbol_conf.kallsyms_name;
+ if (IS_ERR(session)) {
+ PyErr_Format(PyExc_IOError, "failed to create session: %ld", PTR_ERR(session));
+ goto err_out;
+ }
+ psession->session = session;
+ psession->session->itrace_synth_opts = &psession->itrace_opts;
if (symbol__init(perf_session__env(session)) < 0) {
PyErr_SetString(PyExc_OSError, "perf: symbol__init failed");
goto err_out;
}
+ if (psession->symfs && !symbol_conf.symfs[0])
+ zfree(&psession->symfs);
return (PyObject *)psession;
err_out:
@@ -4997,14 +5149,16 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject
static void pyrf_session__delete(struct pyrf_session *psession)
{
perf_session__delete(psession->session);
- if (symbol_conf.vmlinux_name == psession->vmlinux_name)
- symbol_conf.vmlinux_name = NULL;
+ if (!pyrf_session__active) {
+ if (symbol_conf.vmlinux_name == psession->vmlinux_name)
+ symbol_conf.vmlinux_name = NULL;
+ if (symbol_conf.kallsyms_name == psession->kallsyms_name)
+ symbol_conf.kallsyms_name = NULL;
+ if (symbol_conf.symfs == psession->symfs)
+ symbol_conf.symfs = "";
+ }
free(psession->vmlinux_name);
- if (symbol_conf.kallsyms_name == psession->kallsyms_name)
- symbol_conf.kallsyms_name = NULL;
free(psession->kallsyms_name);
- if (symbol_conf.symfs == psession->symfs)
- symbol_conf.symfs = "";
free(psession->symfs);
free(psession->itrace_opts.vm_tm_corr_args);
free(psession->itrace_opts.cpu_bitmap);
@@ -5025,9 +5179,23 @@ static PyObject *pyrf_session__find_thread_events(struct pyrf_session *psession)
int err;
CHECK_INITIALIZED(psession->session, "session");
+ if (pyrf_session__active) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Another perf.session is currently active");
+ return NULL;
+ }
+
+ symbol_conf.vmlinux_name = psession->vmlinux_name;
+ symbol_conf.kallsyms_name = psession->kallsyms_name;
+ symbol_conf.symfs = psession->symfs ?: "";
psession->callback_raised = false;
+ /* Callbacks that use python acquire the GIL, see pyrf_session_tool__sample(). */
+ pyrf_session__active = psession;
+ Py_BEGIN_ALLOW_THREADS
err = perf_session__process_events(psession->session);
+ Py_END_ALLOW_THREADS
+ pyrf_session__active = NULL;
if (psession->callback_raised) {
/*
* Clear the early stop requested by this session's callback, in
@@ -5089,6 +5257,8 @@ static PyObject *pyrf_session__getattro(struct pyrf_session *psession, PyObject
PyErr_SetString(PyExc_ValueError, "session not initialized");
return NULL;
}
+ if (!pyrf_session__check_not_busy(psession))
+ return NULL;
if (!strcmp(name_str, "e_machine"))
return PyLong_FromLong(perf_session__e_machine(psession->session,
/*e_flags=*/NULL));
@@ -5139,6 +5309,8 @@ static int pyrf_session__setattro(struct pyrf_session *psession, PyObject *attr_
PyErr_SetString(PyExc_ValueError, "session not initialized");
return -1;
}
+ if (!pyrf_session__check_not_busy(psession))
+ return -1;
return PyObject_GenericSetAttr((PyObject *) psession, attr_name, value);
}
--
2.56.0.rc1.315.gc6ed9934b7-goog
next prev parent reply other threads:[~2026-10-02 18:26 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-02 18:26 [PATCH v1 00/13] perf timechart/list/treport: Interactive Textual TUIs and perf python session improvements Ian Rogers
2026-10-02 18:26 ` [PATCH v1 01/13] perf session: Don't flush remaining events once processing is done Ian Rogers
2026-10-02 18:26 ` [PATCH v1 02/13] perf python: Quietly stop processing events when a callback raises Ian Rogers
2026-10-02 18:26 ` [PATCH v1 03/13] perf python: Lazily copy events and samples from process_events Ian Rogers
2026-10-02 18:26 ` [PATCH v1 04/13] perf python: Lazily resolve sample callchains Ian Rogers
2026-10-02 18:26 ` Ian Rogers [this message]
2026-10-02 18:26 ` [PATCH v1 06/13] perf list: Add a --tui option to launch ilist Ian Rogers
2026-10-02 18:26 ` [PATCH v1 07/13] perf test: Add a test for the ilist script Ian Rogers
2026-10-02 18:26 ` [PATCH v1 08/13] perf treport: Show the profile while it loads Ian Rogers
2026-10-02 18:26 ` [PATCH v1 09/13] perf test: Add a test for the treport script Ian Rogers
2026-10-02 18:26 ` [PATCH v1 10/13] perf timechart: Add an interactive --tui mode Ian Rogers
2026-10-02 18:26 ` [PATCH v1 11/13] perf test: Add a test for perf timechart --tui Ian Rogers
2026-10-02 18:26 ` [PATCH v1 12/13] perf timechart: Add a --live mode to the TUI Ian Rogers
2026-10-02 18:26 ` [PATCH v1 13/13] perf test: Test perf timechart --live Ian Rogers
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261002182624.3259797-6-irogers@google.com \
--to=irogers@google.com \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=alice.mei.rogers@gmail.com \
--cc=atrajeev@linux.ibm.com \
--cc=changbin.du@huawei.com \
--cc=dapeng1.mi@linux.intel.com \
--cc=james.clark@linaro.org \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=mingo@redhat.com \
--cc=namhyung@kernel.org \
--cc=peterz@infradead.org \
--cc=tanze@kylinos.cn \
--cc=thomas.falcon@intel.com \
--cc=wutengda@huaweicloud.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®