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 02/13] perf python: Quietly stop processing events when a callback raises
Date: Fri, 2 Oct 2026 11:26:11 -0700 [thread overview]
Message-ID: <20261002182624.3259797-3-irogers@google.com> (raw)
In-Reply-To: <20261002182624.3259797-1-irogers@google.com>
When a session callback raises a python exception the callback returned
-1 causing reader__read_event() to print a "processing failed for event
of type: ..." error. This is redundant with the python exception that is
raised from process_events(), and corrupts the display of a TUI that
raises from a callback to deliberately stop processing, such as when
cancelling a background load.
Instead set session_done so that processing stops and return success,
the pending exception is then raised by process_events(). As a single
event may still cause further callbacks, for example multiple
call_return callbacks followed by a sample callback, skip calling python
while an exception is pending.
The session whose callback raised resets session_done after processing
so later sessions process all their events. As session_done is a global,
a session processed concurrently in another thread may be stopped early
too. Count the stop requests so that such a session raises an error
rather than silently dropping events.
Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/python.c | 149 ++++++++++++++++++++++++++++++++-------
1 file changed, 122 insertions(+), 27 deletions(-)
diff --git a/tools/perf/util/python.c b/tools/perf/util/python.c
index 95140dfaef9c..5b4963d5784d 100644
--- a/tools/perf/util/python.c
+++ b/tools/perf/util/python.c
@@ -4120,6 +4120,11 @@ struct pyrf_session {
*/
u64 call_return_last_db_id;
u64 sample_last_db_id;
+ /**
+ * @callback_raised: A python callback raised an exception and requested
+ * that event processing stops, see pyrf_session__callback_raised().
+ */
+ bool callback_raised;
char *vmlinux_name;
char *kallsyms_name;
char *symfs;
@@ -4439,6 +4444,55 @@ static PyObject *pyrf_call_return__new(struct call_return *cr)
return (PyObject *)pevent;
}
+/*
+ * Number of times a python callback has requested that event processing
+ * stops, see pyrf_session__callback_raised().
+ */
+static unsigned int pyrf_session__stop_requests;
+
+/*
+ * A python callback raised an exception.
+ *
+ * Returning an error to the session code would cause reader__read_event() to
+ * print a "processing failed for event of type: ..." error to stderr. This is
+ * noise, and in the case of a TUI corrupts the display, when a callback raises
+ * to deliberately stop processing (e.g. cancelling a background load) and the
+ * error message is redundant with the python exception that is raised anyway.
+ *
+ * Instead request that event processing stops by setting session_done and
+ * return success. The exception remains pending (PyErr_Occurred()) and
+ * pyrf_session__find_thread_events() returns NULL so python raises it.
+ *
+ * Note, session_done is a process-global rather than per-session. Were
+ * multiple sessions processing events concurrently in different python
+ * threads then an exception in one would stop all of them. Only the session
+ * whose callback raised clears session_done, and the other sessions detect
+ * the stop request, using pyrf_session__stop_requests, and raise an error
+ * rather than silently dropping events.
+ */
+static int pyrf_session__callback_raised(struct pyrf_session *psession)
+{
+ psession->callback_raised = true;
+ pyrf_session__stop_requests++;
+ session_done = 1;
+ return 0;
+}
+
+/*
+ * session_done stops the reader loops, the ordered_events and deferred sample
+ * flushes, and auxtrace__flush_events(), but some callbacks may still happen
+ * after a callback has raised. For example, a single sample may generate
+ * multiple call_return callbacks, and perf_session__flush_thread_stacks()
+ * delivers call returns for the remaining stack entries (skipping it would
+ * only defer those callbacks until the threads are deleted). Calling into
+ * python with an exception pending is an error, and the events would be
+ * discarded anyway, so callbacks check this and skip calling python.
+ */
+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)
@@ -4446,7 +4500,7 @@ static int pyrf_session__call_return_process(struct call_return *cr,
struct pyrf_session *psession = data;
PyObject *pyevent, *ret;
- if (!psession->call_return)
+ if (!psession->call_return || pyrf_session__exception_pending())
return 0;
if (!cr->db_id)
@@ -4463,12 +4517,10 @@ static int pyrf_session__call_return_process(struct call_return *cr,
return -1;
ret = PyObject_CallFunctionObjArgs(psession->call_return, pyevent, NULL);
- if (!ret) {
- Py_DECREF(pyevent);
- return -1;
- }
- Py_DECREF(ret);
Py_DECREF(pyevent);
+ if (!ret)
+ return pyrf_session__callback_raised(psession);
+ Py_DECREF(ret);
return 0;
}
@@ -4480,6 +4532,9 @@ static int pyrf_session_tool__sample(const struct perf_tool *tool,
struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
u64 sample_db_id = ++psession->sample_last_db_id;
+ if (pyrf_session__exception_pending())
+ return 0;
+
if (psession->crp) {
struct addr_location al, addr_al;
struct thread *thread = NULL;
@@ -4510,6 +4565,10 @@ static int pyrf_session_tool__sample(const struct perf_tool *tool,
addr_location__exit(&addr_al);
addr_location__exit(&al);
+
+ /* A call_return callback may have raised. */
+ if (pyrf_session__exception_pending())
+ return 0;
}
if (psession->sample) {
@@ -4524,7 +4583,7 @@ static int pyrf_session_tool__sample(const struct perf_tool *tool,
ret = PyObject_CallFunction(psession->sample, "O", pyevent);
Py_DECREF(pyevent);
if (!ret)
- return -1;
+ return pyrf_session__callback_raised(psession);
Py_DECREF(ret);
}
return 0;
@@ -4536,9 +4595,19 @@ static int pyrf_session_tool__context_switch(const struct perf_tool *tool,
struct machine *machine)
{
struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
- PyObject *pyevent = pyrf_event__new(event, sample->evsel, psession->session, machine, NULL);
+ PyObject *pyevent;
PyObject *ret;
+ if (pyrf_session__exception_pending()) {
+ /*
+ * Skip calling python, but keep the machine's thread state
+ * consistent as perf_event__process_switch() would have been
+ * called had no python callback been registered.
+ */
+ return perf_event__process_switch(tool, event, sample, machine);
+ }
+
+ pyevent = pyrf_event__new(event, sample->evsel, psession->session, machine, NULL);
if (perf_event__process_switch(tool, event, sample, machine) < 0) {
Py_XDECREF(pyevent);
return -1;
@@ -4548,12 +4617,10 @@ static int pyrf_session_tool__context_switch(const struct perf_tool *tool,
return -ENOMEM;
ret = PyObject_CallFunction(psession->context_switch, "O", pyevent);
- if (!ret) {
- Py_DECREF(pyevent);
- return -1;
- }
- Py_DECREF(ret);
Py_DECREF(pyevent);
+ if (!ret)
+ return pyrf_session__callback_raised(psession);
+ Py_DECREF(ret);
return 0;
}
@@ -4563,21 +4630,23 @@ static int pyrf_session_tool__stat(const struct perf_tool *tool,
{
struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
struct evsel *evsel = evlist__id2evsel(session->evlist, event->stat.id);
- PyObject *pyevent = pyrf_event__new(event, /*evsel=*/NULL, psession->session,
- /*machine=*/NULL, NULL);
const char *name = evsel ? evsel__name(evsel) : "unknown";
+ PyObject *pyevent;
PyObject *ret;
+ if (pyrf_session__exception_pending())
+ return 0;
+
+ pyevent = pyrf_event__new(event, /*evsel=*/NULL, psession->session,
+ /*machine=*/NULL, NULL);
if (pyevent == NULL)
return -ENOMEM;
ret = PyObject_CallFunction(psession->stat, "Oz", pyevent, name);
- if (!ret) {
- Py_DECREF(pyevent);
- return -1;
- }
- Py_DECREF(ret);
Py_DECREF(pyevent);
+ if (!ret)
+ return pyrf_session__callback_raised(psession);
+ Py_DECREF(ret);
return 0;
}
@@ -4586,20 +4655,22 @@ static int pyrf_session_tool__stat_round(const struct perf_tool *tool,
union perf_event *event)
{
struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
- PyObject *pyevent = pyrf_event__new(event, /*evsel=*/NULL, psession->session,
- /*machine=*/NULL, NULL);
+ PyObject *pyevent;
PyObject *ret;
+ if (pyrf_session__exception_pending())
+ return 0;
+
+ pyevent = pyrf_event__new(event, /*evsel=*/NULL, psession->session,
+ /*machine=*/NULL, NULL);
if (pyevent == NULL)
return -ENOMEM;
ret = PyObject_CallFunction(psession->stat, "Oz", pyevent, NULL);
- if (!ret) {
- Py_DECREF(pyevent);
- return -1;
- }
- Py_DECREF(ret);
Py_DECREF(pyevent);
+ if (!ret)
+ return pyrf_session__callback_raised(psession);
+ Py_DECREF(ret);
return 0;
}
@@ -4680,6 +4751,7 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject
psession->crp = NULL;
psession->call_return_last_db_id = 0;
psession->sample_last_db_id = 0;
+ psession->callback_raised = false;
psession->vmlinux_name = NULL;
psession->kallsyms_name = NULL;
psession->symfs = NULL;
@@ -4847,11 +4919,34 @@ static void pyrf_session__delete(struct pyrf_session *psession)
static PyObject *pyrf_session__find_thread_events(struct pyrf_session *psession)
{
+ unsigned int stop_requests = pyrf_session__stop_requests;
int err;
CHECK_INITIALIZED(psession->session, "session");
+ psession->callback_raised = false;
err = perf_session__process_events(psession->session);
+ if (psession->callback_raised) {
+ /*
+ * Clear the early stop requested by this session's callback, in
+ * pyrf_session__callback_raised(), so that later sessions process
+ * all their events. That function is the only writer of
+ * session_done in the python module, so resetting it here after
+ * processing is sufficient and there is no need to also reset it
+ * before processing.
+ */
+ session_done = 0;
+ } else if (stop_requests != pyrf_session__stop_requests && !PyErr_Occurred()) {
+ /*
+ * A callback of a session being processed concurrently, in
+ * another thread, raised and set session_done which may have
+ * stopped processing this session's events early. Raise an error
+ * rather than silently dropping events.
+ */
+ PyErr_SetString(PyExc_RuntimeError,
+ "Processing events may have stopped early as a callback of a concurrently processed session raised an exception");
+ return NULL;
+ }
if (PyErr_Occurred())
return NULL;
--
2.56.0.rc1.315.gc6ed9934b7-goog
next prev parent reply other threads:[~2026-10-02 18:26 UTC|newest]
Thread overview: 37+ 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-03 1:33 ` sashiko-bot
2026-10-02 18:26 ` Ian Rogers [this message]
2026-10-03 1:33 ` [PATCH v1 02/13] perf python: Quietly stop processing events when a callback raises sashiko-bot
2026-10-02 18:26 ` [PATCH v1 03/13] perf python: Lazily copy events and samples from process_events Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-02 18:26 ` [PATCH v1 04/13] perf python: Lazily resolve sample callchains Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-02 18:26 ` [PATCH v1 05/13] perf python: Release the GIL while processing session events Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-02 18:26 ` [PATCH v1 06/13] perf list: Add a --tui option to launch ilist Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-02 18:26 ` [PATCH v1 07/13] perf test: Add a test for the ilist script Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-02 18:26 ` [PATCH v1 08/13] perf treport: Show the profile while it loads Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-03 9:31 ` Arnaldo Carvalho de Melo
2026-10-02 18:26 ` [PATCH v1 09/13] perf test: Add a test for the treport script Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-03 9:35 ` Arnaldo Carvalho de Melo
2026-10-02 18:26 ` [PATCH v1 10/13] perf timechart: Add an interactive --tui mode Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-02 18:26 ` [PATCH v1 11/13] perf test: Add a test for perf timechart --tui Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-02 18:26 ` [PATCH v1 12/13] perf timechart: Add a --live mode to the TUI Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-02 18:26 ` [PATCH v1 13/13] perf test: Test perf timechart --live Ian Rogers
2026-10-03 1:33 ` sashiko-bot
2026-10-03 9:10 ` [PATCH v1 00/13] perf timechart/list/treport: Interactive Textual TUIs and perf python session improvements Arnaldo Carvalho de Melo
2026-10-03 9:12 ` Arnaldo Carvalho de Melo
2026-10-03 9:15 ` Arnaldo Carvalho de Melo
2026-10-14 4:22 ` [PATCH v2 0/2] perf timechart: Add --live mode to the TUI and test Ian Rogers
2026-10-14 4:22 ` [PATCH v2 1/2] perf timechart: Add a --live mode to the TUI Ian Rogers
2026-10-14 4:35 ` sashiko-bot
2026-10-14 4:22 ` [PATCH v2 2/2] perf test: Test perf timechart --live Ian Rogers
2026-10-14 4:28 ` sashiko-bot
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-3-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.