perf python: Add python session abstraction wrapping perf's session

Sessions are necessary to be able to use perf.data files within a
tool. Add a wrapper python type that incorporates the tool. Allow a
sample callback to be passed when creating the session. When
process_events is run this callback will be called, if supplied, for
sample events.

An example use looks like:
```
$ perf record -e cycles,instructions -a sleep 3
$ PYTHONPATH=..../perf/python python3
Python 3.13.7 (main, Aug 20 2025, 22:17:40) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import perf
>>> count=0
... def handle_sample(x):
...   global count
...   if count < 3:
...     print(dir(x))
...   count = count + 1
... perf.session(perf.data("perf.data"),sample=handle_sample).process_events()
...
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'sample_addr', 'sample_cpu', 'sample_id', 'sample_ip', 'sample_period', 'sample_pid', 'sample_stream_id', 'sample_tid', 'sample_time', 'type']
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'sample_addr', 'sample_cpu', 'sample_id', 'sample_ip', 'sample_period', 'sample_pid', 'sample_stream_id', 'sample_tid', 'sample_time', 'type']
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'sample_addr', 'sample_cpu', 'sample_id', 'sample_ip', 'sample_period', 'sample_pid', 'sample_stream_id', 'sample_tid', 'sample_time', 'type']
```

Also, add the ability to get the thread associated with a session. For
threads, allow the comm string to be retrieved. This can be useful for
filtering threads. Connect up some of the standard event handling in
psession->tool to better support queries of the machine. Also connect
up the symbols.

Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Gemini:gemini-3.1-pro-preview
Cc: Adrian Hunter <adrian.hunter@intel.com>
Cc: Alice Rogers <alice.mei.rogers@gmail.com>
Cc: Dapeng Mi <dapeng1.mi@linux.intel.com>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: James Clark <james.clark@linaro.org>
Cc: Leo Yan <leo.yan@linux.dev>
Cc: Namhyung Kim <namhyung@kernel.org>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Thomas Richter <tmricht@linux.ibm.com>
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
Signed-off-by: Namhyung Kim <namhyung@kernel.org>
This commit is contained in:
Ian Rogers
2026-06-15 18:15:32 -07:00
committed by Namhyung Kim
parent 27e59401c7
commit b4e3808006

View File

@@ -14,8 +14,10 @@
#include <structmember.h>
#include "callchain.h"
#include "comm.h"
#include "counts.h"
#include "data.h"
#include "debug.h"
#include "event.h"
#include "evlist.h"
#include "evsel.h"
@@ -25,8 +27,14 @@
#include "pmus.h"
#include "print_binary.h"
#include "record.h"
#include "session.h"
#include "strbuf.h"
#include "symbol.h"
#include "stat.h"
#include "header.h"
#include "thread.h"
#include "thread_map.h"
#include "tool.h"
#include "tp_pmu.h"
#include "trace-event.h"
#include "util/sample.h"
@@ -2579,6 +2587,332 @@ static int pyrf_data__setup_types(void)
return PyType_Ready(&pyrf_data__type);
}
struct pyrf_thread {
PyObject_HEAD
struct thread *thread;
};
static void pyrf_thread__delete(struct pyrf_thread *pthread)
{
thread__put(pthread->thread);
Py_TYPE(pthread)->tp_free((PyObject *)pthread);
}
static PyObject *pyrf_thread__comm(PyObject *obj)
{
struct pyrf_thread *pthread = (void *)obj;
const char *str;
CHECK_INITIALIZED(pthread->thread, "perf.thread");
str = thread__comm_str(pthread->thread);
if (!str)
Py_RETURN_NONE;
return PyUnicode_FromString(str);
}
static PyMethodDef pyrf_thread__methods[] = {
{
.ml_name = "comm",
.ml_meth = (PyCFunction)pyrf_thread__comm,
.ml_flags = METH_NOARGS,
.ml_doc = PyDoc_STR("Comm(and) associated with this thread.")
},
{ .ml_name = NULL, }
};
static PyObject *pyrf_thread__get_pid(struct pyrf_thread *pthread, void *closure __maybe_unused)
{
return PyLong_FromLong(thread__pid(pthread->thread));
}
static PyObject *pyrf_thread__get_tid(struct pyrf_thread *pthread, void *closure __maybe_unused)
{
return PyLong_FromLong(thread__tid(pthread->thread));
}
static PyObject *pyrf_thread__get_ppid(struct pyrf_thread *pthread, void *closure __maybe_unused)
{
return PyLong_FromLong(thread__ppid(pthread->thread));
}
static PyObject *pyrf_thread__get_cpu(struct pyrf_thread *pthread, void *closure __maybe_unused)
{
return PyLong_FromLong(thread__cpu(pthread->thread));
}
static PyGetSetDef pyrf_thread__getset[] = {
{ .name = "pid", .get = (getter)pyrf_thread__get_pid, .doc = "process ID" },
{ .name = "tid", .get = (getter)pyrf_thread__get_tid, .doc = "thread ID" },
{ .name = "ppid", .get = (getter)pyrf_thread__get_ppid, .doc = "parent process ID" },
{ .name = "cpu", .get = (getter)pyrf_thread__get_cpu, .doc = "cpu number" },
{ .name = NULL }
};
static const char pyrf_thread__doc[] = PyDoc_STR("perf thread object.");
static PyTypeObject pyrf_thread__type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "perf.thread",
.tp_basicsize = sizeof(struct pyrf_thread),
.tp_dealloc = (destructor)pyrf_thread__delete,
.tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE,
.tp_methods = pyrf_thread__methods,
.tp_getset = pyrf_thread__getset,
.tp_doc = pyrf_thread__doc,
};
static int pyrf_thread__setup_types(void)
{
return PyType_Ready(&pyrf_thread__type);
}
static PyObject *pyrf_thread__from_thread(struct thread *thread)
{
struct pyrf_thread *pthread = PyObject_New(struct pyrf_thread, &pyrf_thread__type);
if (!pthread)
return NULL;
pthread->thread = thread__get(thread);
return (PyObject *)pthread;
}
struct pyrf_session {
PyObject_HEAD
struct perf_session *session;
struct perf_tool tool;
struct pyrf_data *pdata;
PyObject *sample;
};
static int pyrf_session_tool__sample(const struct perf_tool *tool,
union perf_event *event,
struct perf_sample *sample,
struct machine *machine __maybe_unused)
{
struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool);
PyObject *pyevent = pyrf_event__new(event);
struct pyrf_event *pevent = (struct pyrf_event *)pyevent;
PyObject *ret;
bool needs_swap;
if (pyevent == NULL)
return -ENOMEM;
needs_swap = sample->evsel->needs_swap;
memcpy(&pevent->event, event, event->header.size);
/* The event is already swapped, disable swapping temporarily */
sample->evsel->needs_swap = false;
if (evsel__parse_sample(sample->evsel, &pevent->event, &pevent->sample) < 0) {
sample->evsel->needs_swap = needs_swap;
Py_DECREF(pyevent);
return -1;
}
sample->evsel->needs_swap = needs_swap;
ret = PyObject_CallFunction(psession->sample, "O", pyevent);
if (!ret) {
Py_DECREF(pyevent);
return -1;
}
Py_DECREF(ret);
Py_DECREF(pyevent);
return 0;
}
static PyObject *pyrf_session__find_thread(struct pyrf_session *psession, PyObject *args)
{
struct machine *machine;
struct thread *thread = NULL;
PyObject *result;
int pid;
CHECK_INITIALIZED(psession->session, "session");
if (!PyArg_ParseTuple(args, "i", &pid))
return NULL;
machine = &psession->session->machines.host;
thread = machine__find_thread(machine, pid, pid);
if (!thread) {
machine = perf_session__find_machine(psession->session, pid);
if (machine)
thread = machine__find_thread(machine, pid, pid);
}
if (!thread) {
PyErr_Format(PyExc_TypeError, "Failed to find thread %d", pid);
return NULL;
}
result = pyrf_thread__from_thread(thread);
thread__put(thread);
return result;
}
static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
struct pyrf_data *pdata;
PyObject *sample = NULL;
static char *kwlist[] = { "data", "sample", NULL };
struct pyrf_session *psession;
struct perf_session *session;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!|O", kwlist, &pyrf_data__type, &pdata,
&sample))
return NULL;
psession = PyObject_New(struct pyrf_session, type);
if (!psession)
return NULL;
psession->session = NULL;
psession->sample = NULL;
psession->pdata = NULL;
Py_INCREF(pdata);
psession->pdata = pdata;
perf_tool__init(&psession->tool, /*ordered_events=*/true);
psession->tool.ordering_requires_timestamps = true;
#define ADD_TOOL(name) \
do { \
if (name) { \
if (!PyCallable_Check(name)) { \
PyErr_SetString(PyExc_TypeError, #name " must be callable"); \
goto err_out; \
} \
psession->tool.name = pyrf_session_tool__##name; \
Py_INCREF(name); \
psession->name = name; \
} \
} while (0)
ADD_TOOL(sample);
#undef ADD_TOOL
psession->tool.comm = perf_event__process_comm;
psession->tool.mmap = perf_event__process_mmap;
psession->tool.mmap2 = perf_event__process_mmap2;
psession->tool.namespaces = perf_event__process_namespaces;
psession->tool.cgroup = perf_event__process_cgroup;
psession->tool.exit = perf_event__process_exit;
psession->tool.fork = perf_event__process_fork;
psession->tool.ksymbol = perf_event__process_ksymbol;
psession->tool.text_poke = perf_event__process_text_poke;
psession->tool.build_id = perf_event__process_build_id;
psession->tool.attr = perf_event__process_attr;
psession->tool.feature = perf_event__process_feature;
psession->tool.stat = perf_event__process_stat_event;
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;
if (symbol__init(perf_session__env(session)) < 0) {
PyErr_SetString(PyExc_OSError, "perf: symbol__init failed");
goto err_out;
}
return (PyObject *)psession;
err_out:
Py_DECREF(psession);
return NULL;
}
static void pyrf_session__delete(struct pyrf_session *psession)
{
perf_session__delete(psession->session);
Py_XDECREF(psession->pdata);
Py_XDECREF(psession->sample);
Py_TYPE(psession)->tp_free((PyObject *)psession);
}
static PyObject *pyrf_session__find_thread_events(struct pyrf_session *psession)
{
int err;
CHECK_INITIALIZED(psession->session, "session");
err = perf_session__process_events(psession->session);
if (PyErr_Occurred())
return NULL;
if (err < 0) {
PyErr_Format(PyExc_OSError, "Process events failed: %d", err);
return NULL;
}
Py_RETURN_NONE;
}
static PyMethodDef pyrf_session__methods[] = {
{
.ml_name = "process_events",
.ml_meth = (PyCFunction)pyrf_session__find_thread_events,
.ml_flags = METH_NOARGS,
.ml_doc = PyDoc_STR("Iterate and process events.")
},
{
.ml_name = "find_thread",
.ml_meth = (PyCFunction)pyrf_session__find_thread,
.ml_flags = METH_VARARGS,
.ml_doc = PyDoc_STR("Returns the thread associated with a pid.")
},
{ .ml_name = NULL, }
};
static const char pyrf_session__doc[] = PyDoc_STR("perf session object.");
static PyObject *pyrf_session__getattro(struct pyrf_session *psession, PyObject *attr_name)
{
if (!psession->session) {
PyErr_SetString(PyExc_ValueError, "session not initialized");
return NULL;
}
return PyObject_GenericGetAttr((PyObject *) psession, attr_name);
}
static int pyrf_session__setattro(struct pyrf_session *psession, PyObject *attr_name,
PyObject *value)
{
if (!psession->session) {
PyErr_SetString(PyExc_ValueError, "session not initialized");
return -1;
}
return PyObject_GenericSetAttr((PyObject *) psession, attr_name, value);
}
static PyTypeObject pyrf_session__type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "perf.session",
.tp_basicsize = sizeof(struct pyrf_session),
.tp_dealloc = (destructor)pyrf_session__delete,
.tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE,
.tp_methods = pyrf_session__methods,
.tp_doc = pyrf_session__doc,
.tp_new = pyrf_session__new,
.tp_getattro = (getattrofunc) pyrf_session__getattro,
.tp_setattro = (setattrofunc) pyrf_session__setattro,
};
static int pyrf_session__setup_types(void)
{
return PyType_Ready(&pyrf_session__type);
}
static PyMethodDef perf__methods[] = {
{
.ml_name = "metrics",
@@ -2633,8 +2967,10 @@ PyMODINIT_FUNC PyInit_perf(void)
};
PyObject *module = PyModule_Create(&moduledef);
if (module == NULL ||
pyrf_event__setup_types() < 0 ||
if (module == NULL)
return NULL;
if (pyrf_event__setup_types() < 0 ||
pyrf_evlist__setup_types() < 0 ||
pyrf_evsel__setup_types() < 0 ||
pyrf_thread_map__setup_types() < 0 ||
@@ -2642,8 +2978,12 @@ PyMODINIT_FUNC PyInit_perf(void)
pyrf_pmu_iterator__setup_types() < 0 ||
pyrf_pmu__setup_types() < 0 ||
pyrf_counts_values__setup_types() < 0 ||
pyrf_data__setup_types() < 0)
return module;
pyrf_data__setup_types() < 0 ||
pyrf_session__setup_types() < 0 ||
pyrf_thread__setup_types() < 0) {
Py_DECREF(module);
return NULL;
}
/* The page_size is placed in util object. */
page_size = sysconf(_SC_PAGE_SIZE);
@@ -2693,6 +3033,9 @@ PyMODINIT_FUNC PyInit_perf(void)
Py_INCREF(&pyrf_data__type);
PyModule_AddObject(module, "data", (PyObject *)&pyrf_data__type);
Py_INCREF(&pyrf_session__type);
PyModule_AddObject(module, "session", (PyObject *)&pyrf_session__type);
dict = PyModule_GetDict(module);
if (dict == NULL)
goto error;
@@ -2706,7 +3049,9 @@ PyMODINIT_FUNC PyInit_perf(void)
}
error:
if (PyErr_Occurred())
PyErr_SetString(PyExc_ImportError, "perf: Init failed!");
if (PyErr_Occurred()) {
Py_XDECREF(module);
return NULL;
}
return module;
}