From 598e905931380f0714d5fd50cecfac615a1ec742 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Mon, 15 Jun 2026 19:49:45 -0300 Subject: [PATCH 001/425] perf jitdump: Fix extended header read that always fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jit_open() sets bsz = bs before the fread() that uses bs - bsz as the read size, making the expression always evaluate to zero. fread() with size 0 returns 0, which triggers the ret != 1 error path — so extended jitdump headers (total_size > sizeof(header)) have been silently broken since the original implementation. Additionally, when 0 < bs <= bsz the if (bs > bsz) block is skipped entirely, leaving extended header bytes unread in the stream. Subsequent jit_get_next_entry() calls then parse those leftover bytes as a jr_prefix, corrupting the record stream. Fix by separating the buffer growth from the read: realloc only when bs > bsz, then unconditionally fread bs bytes when bs > 0. Fixes: 9b07e27f88b9cd78 ("perf inject: Add jitdump mmap injection support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Cc: Ian Rogers Cc: Namhyung Kim Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index 83005b30b9bf..4b7c7ba7cd95 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -224,10 +224,12 @@ jit_open(struct jit_buf_desc *jd, const char *name) n = realloc(buf, bs); if (!n) goto error; - bsz = bs; buf = n; - /* read extra we do not know about */ - ret = fread(buf, bs - bsz, 1, jd->in); + bsz = bs; + } + if (bs > 0) { + /* consume extended header bytes from the stream */ + ret = fread(buf, bs, 1, jd->in); if (ret != 1) goto error; } From 2bd967f3adcba1d3e54f10b0d8b25f950e4f03fd Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Mon, 15 Jun 2026 20:40:42 -0300 Subject: [PATCH 002/425] perf jitdump: Fix debug_data and unwinding_data leaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jit_repipe_debug_info() overwrites jd->debug_data without freeing the previous allocation. If two consecutive JIT_CODE_DEBUG_INFO records appear without an intervening LOAD record consuming the data, the first allocation leaks. The sibling jit_repipe_unwinding_info() already frees the old jd->unwinding_data before reassignment — add the same pattern to jit_repipe_debug_info() using zfree(). Also add cleanup of both buffers in jit_close() so they are freed when the jitdump session ends, even if no LOAD record consumed them. Fixes: 9b07e27f88b9 ("perf inject: Add jitdump mmap injection support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index 4b7c7ba7cd95..dcb26d9c6c8f 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -117,6 +117,8 @@ jit_close(struct jit_buf_desc *jd) funlockfile(jd->in); fclose(jd->in); jd->in = NULL; + zfree(&jd->debug_data); + zfree(&jd->unwinding_data); } static int @@ -665,6 +667,7 @@ static int jit_repipe_debug_info(struct jit_buf_desc *jd, union jr_entry *jr) memcpy(data, &jr->info.entries, sz); + zfree(&jd->debug_data); jd->debug_data = data; /* From 06c7cf7a072fcd52e68471628c56630d2bdcc650 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Mon, 15 Jun 2026 20:22:12 -0300 Subject: [PATCH 003/425] perf jitdump: Validate code_size against total_size in code load jit_repipe_code_load() reads code_size from the jitdump record and uses it to compute a pointer to the code blob: code = (unsigned long)jr + jr->load.p.total_size - csize; An oversized code_size underflows the pointer arithmetic, causing OOB reads into earlier heap memory. Validate that code_size fits within the record (total_size - sizeof(jr->load)) before the pointer computation. code_size is uint64_t but csize is int; values above INT_MAX wrap negative when narrowed into csize, which defeats the bounds check and sends the code pointer past the end of the record. Reject those too. Fixes: 9b07e27f88b9 ("perf inject: Add jitdump mmap injection support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index dcb26d9c6c8f..14bd23c8d196 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -452,6 +453,16 @@ static int jit_repipe_code_load(struct jit_buf_desc *jd, union jr_entry *jr) csize = jr->load.code_size; usize = jd->unwinding_mapped_size; addr = jr->load.code_addr; + + /* code blob lives at the end of the record, validate it fits */ + if (jr->load.p.total_size < sizeof(jr->load) || + jr->load.code_size > jr->load.p.total_size - sizeof(jr->load) || + jr->load.code_size > INT_MAX) { + pr_warning("jitdump: invalid code_size %" PRIu64 " (total_size=%u) in code_load record\n", + (uint64_t)jr->load.code_size, jr->load.p.total_size); + return -1; + } + sym = (void *)((unsigned long)jr + sizeof(jr->load)); code = (unsigned long)jr + jr->load.p.total_size - csize; count = jr->load.code_index; From dfe8fe96efca3c32979dc0859f88e15b677d25f1 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Mon, 15 Jun 2026 20:23:36 -0300 Subject: [PATCH 004/425] perf jitdump: Prevent integer underflow in debug info size calculation jit_repipe_debug_info() and jit_repipe_unwinding_info() compute payload sizes by subtracting the fixed header size from total_size: sz = jr->prefix.total_size - sizeof(jr->info); When total_size is smaller than the header struct (from a truncated or corrupted jitdump record), the subtraction underflows to a massive value, causing an oversized allocation followed by an OOB memcpy. Validate that total_size covers at least the fixed header before the subtraction in both functions. Fixes: 598b7c6919c7 ("perf jit: add source line info support") Fixes: 0284fecd13b6 ("perf jit: Add unwinding support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Cc: Stefano Sanfilippo Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index 14bd23c8d196..f79e9420c6bd 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -671,6 +671,10 @@ static int jit_repipe_debug_info(struct jit_buf_desc *jd, union jr_entry *jr) if (!(jd && jr)) return -1; + /* total_size must cover at least the fixed header */ + if (jr->prefix.total_size < sizeof(jr->info)) + return -1; + sz = jr->prefix.total_size - sizeof(jr->info); data = malloc(sz); if (!data) @@ -699,6 +703,10 @@ jit_repipe_unwinding_info(struct jit_buf_desc *jd, union jr_entry *jr) if (!(jd && jr)) return -1; + /* total_size must cover at least the fixed header */ + if (jr->prefix.total_size < sizeof(jr->unwinding)) + return -1; + unwinding_data_size = jr->prefix.total_size - sizeof(jr->unwinding); unwinding_data = malloc(unwinding_data_size); if (!unwinding_data) From ede3cd8151675c2983cd49104b1e9b25a3712ed8 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Mon, 15 Jun 2026 20:26:31 -0300 Subject: [PATCH 005/425] perf jitdump: Bounds-check debug entry byte-swap loop The byte-swap loop for JIT_CODE_DEBUG_INFO uses array indexing (jr->info.entries[n]) to iterate debug entries. struct debug_entry has a flexible array member name[], so each entry has a different size. Array indexing computes offsets assuming fixed-size elements, landing inside variable-length name strings after the first entry and byte-swapping garbage. Additionally, nr_entry is read from untrusted jitdump input without validation against total_size, so a crafted value causes OOB reads. Replace the array indexing with debug_entry_next() pointer arithmetic (which correctly accounts for the variable-length name) and bounds-check each entry against the record's total_size before byte-swapping. Fixes: 9b07e27f88b9 ("perf inject: Add jitdump mmap injection support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index f79e9420c6bd..7efbaa07f1ba 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -320,14 +320,32 @@ jit_get_next_entry(struct jit_buf_desc *jd) switch(id) { case JIT_CODE_DEBUG_INFO: if (jd->needs_bswap) { + void *end = (void *)jr + jr->prefix.total_size; + struct debug_entry *ent; uint64_t n; + jr->info.code_addr = bswap_64(jr->info.code_addr); jr->info.nr_entry = bswap_64(jr->info.nr_entry); - for (n = 0 ; n < jr->info.nr_entry; n++) { - jr->info.entries[n].addr = bswap_64(jr->info.entries[n].addr); - jr->info.entries[n].lineno = bswap_32(jr->info.entries[n].lineno); - jr->info.entries[n].discrim = bswap_32(jr->info.entries[n].discrim); + + /* + * debug_entry has a variable-length name[], so array + * indexing would compute wrong offsets — use + * debug_entry_next() and bounds-check each entry. + */ + ent = &jr->info.entries[0]; + for (n = 0; n < jr->info.nr_entry; n++) { + if ((void *)ent + sizeof(*ent) > end) + break; + /* name must be NUL-terminated within the record */ + if (!memchr(ent->name, '\0', (char *)end - ent->name)) + break; + ent->addr = bswap_64(ent->addr); + ent->lineno = bswap_32(ent->lineno); + ent->discrim = bswap_32(ent->discrim); + ent = debug_entry_next(ent); } + /* clamp so downstream consumers don't overrun */ + jr->info.nr_entry = n; } break; case JIT_CODE_UNWINDING_INFO: From 0155f05ea6e133c18a04b8b5945cac59870da156 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Mon, 15 Jun 2026 20:27:43 -0300 Subject: [PATCH 006/425] perf jitdump: Check snprintf return before computing header size snprintf() returns the would-have-been length on truncation. When the jitted filename exceeds PATH_MAX, the unclamped 'size' value inflates sizeof(event->mmap2.filename) - size into a massive underflow, causing the header.size computation to write an oversized header. The subsequent write to 'id = event + header.size - idr_size' then corrupts the heap. Clamp size to PATH_MAX - 1 after snprintf in both jit_repipe_code_load() and jit_repipe_code_move(). Fixes: 9b07e27f88b9 ("perf inject: Add jitdump mmap injection support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index 7efbaa07f1ba..45f0e21b0e78 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -495,6 +495,9 @@ static int jit_repipe_code_load(struct jit_buf_desc *jd, union jr_entry *jr) jd->dir, nspid, count); + /* snprintf returns would-be length on truncation, clamp to buffer */ + if (size >= sizeof(event->mmap2.filename)) + size = sizeof(event->mmap2.filename) - 1; size++; /* for \0 */ @@ -625,6 +628,9 @@ static int jit_repipe_code_move(struct jit_buf_desc *jd, union jr_entry *jr) jd->dir, nspid, jr->move.code_index); + /* snprintf returns would-be length on truncation, clamp to buffer */ + if (size >= sizeof(event->mmap2.filename)) + size = sizeof(event->mmap2.filename) - 1; size++; /* for \0 */ From bfde6848c1e24fd979d0138c57c1cc28c35093c3 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Mon, 15 Jun 2026 20:35:55 -0300 Subject: [PATCH 007/425] perf jitdump: Fix funlockfile on unlocked stream in jit_open() error path If the malloc() for the initial read buffer fails, jit_open() jumps to the error label which calls funlockfile(jd->in). However, flockfile() is called later in the function, so at this point the stream was never locked. Calling funlockfile() on an unlocked stream is undefined behavior per POSIX. Split the error path into two labels: 'error' (after flockfile) calls funlockfile before cleanup, 'error_noflock' (before flockfile) skips the unlock. Fixes: 9b07e27f88b9 ("perf inject: Add jitdump mmap injection support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index 45f0e21b0e78..ae63366b86c6 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -157,7 +157,7 @@ jit_open(struct jit_buf_desc *jd, const char *name) buf = malloc(bsz); if (!buf) - goto error; + goto error_noflock; /* * protect from writer modifying the file while we are reading it @@ -246,8 +246,9 @@ jit_open(struct jit_buf_desc *jd, const char *name) return 0; error: - free(buf); funlockfile(jd->in); +error_noflock: + free(buf); fclose(jd->in); return retval; } From 7fc5851aac606b9e4143f6857710cc5f346770c5 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Mon, 15 Jun 2026 20:39:32 -0300 Subject: [PATCH 008/425] perf jitdump: Free event in jit_repipe_code_move() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jit_repipe_code_move() allocates a perf_event with calloc but never frees it — the 'out' label exits with only perf_sample__exit(). The sibling function jit_repipe_code_load() correctly calls free(event) at its out label. Add the same free(event) to jit_repipe_code_move(). Fixes: 9b07e27f88b9 ("perf inject: Add jitdump mmap injection support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index ae63366b86c6..91aa1eea8229 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -685,6 +685,7 @@ static int jit_repipe_code_move(struct jit_buf_desc *jd, union jr_entry *jr) build_id__mark_dso_hit(tool, event, &sample, jd->machine); out: perf_sample__exit(&sample); + free(event); return ret; } From 4eddf7d9e341eb8c41704b8aa27b4a91f4e2c7c9 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Mon, 15 Jun 2026 20:41:33 -0300 Subject: [PATCH 009/425] perf jitdump: Use dirname() return value in jit_open() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jit_open() calls dirname(jd->dir) but ignores the return value. POSIX says dirname() may return a pointer to internal static storage — glibc does this when the path has no '/', returning "." from a static buffer and leaving jd->dir unchanged with the original filename. Capture the return value and copy it back to jd->dir when dirname() returns a different pointer. Fixes: 9b07e27f88b9 ("perf inject: Add jitdump mmap injection support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index 91aa1eea8229..d3de307532d5 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -146,6 +146,7 @@ jit_open(struct jit_buf_desc *jd, const char *name) ssize_t bs, bsz = 0; void *n, *buf = NULL; int ret, retval = -1; + char *dname; nsinfo__mountns_enter(jd->nsi, &nsc); jd->in = fopen(name, "r"); @@ -241,7 +242,9 @@ jit_open(struct jit_buf_desc *jd, const char *name) */ strncpy(jd->dir, name, PATH_MAX - 1); jd->dir[PATH_MAX - 1] = '\0'; - dirname(jd->dir); + dname = dirname(jd->dir); + if (dname != jd->dir) + strlcpy(jd->dir, dname, sizeof(jd->dir)); free(buf); return 0; From 51e14e495f0cea061bbad33e7635f7fd857113ad Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Wed, 17 Jun 2026 13:52:15 -0300 Subject: [PATCH 010/425] perf jitdump: Validate debug entries on native (non-swap) path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bounds-checking and nr_entry clamping added for the byte-swap path only runs when jd->needs_bswap is true. On native-endian files, nr_entry passes through unvalidated to jit_repipe_debug_info(), which stores it as jd->nr_debug_entries. Downstream, jit_process_debug_info() in genelf_debug.c iterates nr_debug_entries times via debug_entry_next(), which calls strlen() on each entry's name field — a crafted nr_entry causes OOB reads and writes. Add bounds-checked iteration in jit_repipe_debug_info() that validates each debug_entry fits in the payload and its name is NUL-terminated before calling debug_entry_next(). Clamp nr_debug_entries to the count of valid entries. Fixes: 598b7c6919c7bbcc ("perf jit: add source line info support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index d3de307532d5..5a3ea2681fb3 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -694,8 +694,10 @@ static int jit_repipe_code_move(struct jit_buf_desc *jd, union jr_entry *jr) static int jit_repipe_debug_info(struct jit_buf_desc *jd, union jr_entry *jr) { - void *data; - size_t sz; + struct debug_entry *ent; + void *data, *end; + size_t sz, valid; + uint64_t i; if (!(jd && jr)) return -1; @@ -715,10 +717,25 @@ static int jit_repipe_debug_info(struct jit_buf_desc *jd, union jr_entry *jr) jd->debug_data = data; /* - * we must use nr_entry instead of size here because - * we cannot distinguish actual entry from padding otherwise + * Clamp nr_debug_entries to entries that actually fit in the + * payload. The byte-swap path already does this for cross-endian + * files; validate on the native path too, since downstream + * jit_process_debug_info() iterates via debug_entry_next() which + * calls strlen() on each entry's name field. */ - jd->nr_debug_entries = jr->info.nr_entry; + end = data + sz; + ent = data; + valid = 0; + for (i = 0; i < jr->info.nr_entry; i++) { + if ((void *)ent + sizeof(*ent) > end) + break; + /* name must be NUL-terminated within the payload */ + if (!memchr(ent->name, '\0', (char *)end - ent->name)) + break; + ent = debug_entry_next(ent); + valid++; + } + jd->nr_debug_entries = valid; return 0; } From 98bfe1142ccbaa5ffdca5dcdddd26f238ad33585 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Wed, 17 Jun 2026 13:59:06 -0300 Subject: [PATCH 011/425] perf jitdump: Validate sym string NUL-termination in code load jit_repipe_code_load() computes sym = (void *)jr + sizeof(jr->load) and passes it to jit_emit_elf() which calls strlen(sym) via jit_write_elf(). If code_size equals total_size - sizeof(jr->load), the sym pointer aliases the code blob with no NUL terminator, and strlen() scans past the buffer into adjacent heap memory. Add a memchr() check to verify the symbol name is NUL-terminated within the region between the load header and the code blob before use. Fixes: 598b7c6919c7bbcc ("perf jit: add source line info support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index 5a3ea2681fb3..5898a7d8eb96 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -487,6 +487,13 @@ static int jit_repipe_code_load(struct jit_buf_desc *jd, union jr_entry *jr) sym = (void *)((unsigned long)jr + sizeof(jr->load)); code = (unsigned long)jr + jr->load.p.total_size - csize; + + /* sym string lives between the load header and the code blob */ + if (!memchr(sym, '\0', code - (unsigned long)sym)) { + pr_warning("jitdump: unterminated symbol name in code_load record\n"); + return -1; + } + count = jr->load.code_index; idr_size = jd->machine->id_hdr_size; From 92d50319b4f0c0bbee8a236a09063272cd22faab Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Wed, 17 Jun 2026 14:00:22 -0300 Subject: [PATCH 012/425] perf jitdump: Validate unwinding sizes against record payload jit_repipe_unwinding_info() copies unwinding_size and eh_frame_hdr_size from the jitdump record into jd-> fields without checking them against the actual payload size. Downstream, jit_add_eh_frame_info() in genelf.c computes unwinding_table_size = unwinding_size - eh_frame_hdr_size, which underflows when eh_frame_hdr_size > unwinding_size. The result is passed as d->d_size to libelf, causing an OOB heap read into the output ELF file. Validate that unwinding_size fits within the record payload and that eh_frame_hdr_size does not exceed unwinding_size before allocating or storing the values, so a bogus record cannot force a large allocation that is then discarded. mapped_size is likewise taken from the record and was narrowed into an int for the mmap2 len computation in jit_repipe_code_load() and jit_repipe_code_move(); values above INT_MAX would turn negative, producing a wrong mmap2 length. Use uint64_t for usize so the value cannot truncate. Fixes: 0284fecd13b6db3e ("perf jit: Add unwinding support") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stefano Sanfilippo Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index 5898a7d8eb96..d25a9fe9b020 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -462,7 +462,8 @@ static int jit_repipe_code_load(struct jit_buf_desc *jd, union jr_entry *jr) u16 idr_size; const char *sym; uint64_t count; - int ret, csize, usize; + int ret, csize; + uint64_t usize; pid_t nspid, pid, tid; struct { u32 pid, tid; @@ -543,7 +544,7 @@ static int jit_repipe_code_load(struct jit_buf_desc *jd, union jr_entry *jr) event->mmap2.pgoff = GEN_ELF_TEXT_OFFSET; event->mmap2.start = addr; - event->mmap2.len = usize ? ALIGN_8(csize) + usize : csize; + event->mmap2.len = usize ? ALIGN_8((uint64_t)csize) + usize : (uint64_t)csize; event->mmap2.pid = pid; event->mmap2.tid = tid; event->mmap2.ino = st.st_ino; @@ -612,7 +613,7 @@ static int jit_repipe_code_move(struct jit_buf_desc *jd, union jr_entry *jr) char *filename; size_t size; struct stat st; - int usize; + uint64_t usize; u16 idr_size; int ret; pid_t nspid, pid, tid; @@ -761,6 +762,18 @@ jit_repipe_unwinding_info(struct jit_buf_desc *jd, union jr_entry *jr) return -1; unwinding_data_size = jr->prefix.total_size - sizeof(jr->unwinding); + + /* + * Validate sizes before allocating — jit_add_eh_frame_info() + * computes unwinding_size - eh_frame_hdr_size and uses the + * result as a buffer length for libelf. + */ + if (jr->unwinding.unwinding_size > unwinding_data_size || + jr->unwinding.eh_frame_hdr_size > jr->unwinding.unwinding_size) { + pr_warning("jitdump: invalid unwinding sizes in unwinding_info record\n"); + return -1; + } + unwinding_data = malloc(unwinding_data_size); if (!unwinding_data) return -1; From 0cb3024d577460959edd3042f8ac2b9192ce6243 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Wed, 5 Aug 2026 16:03:18 -0300 Subject: [PATCH 013/425] perf jitdump: Byte-swap debug entries via unaligned-safe accessors debug_entry records are packed with a variable-length name[] field, so entries after the first may start at addresses that are not naturally aligned for their u64 addr and int lineno/discrim fields. On strict alignment architectures the byte-swap loop in jit_get_next_entry() performed misaligned 64-bit loads and stores through struct member access, which is undefined behavior. Use get_unaligned()/put_unaligned() for the byte-swap of each field. Reported-by: sashiko-bot Cc: Stephane Eranian Assisted-by: Opencode:DeepSeek-V4-Flash-free Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index d25a9fe9b020..e0d5cc9a8281 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include "event.h" #include "debug.h" @@ -343,9 +345,14 @@ jit_get_next_entry(struct jit_buf_desc *jd) /* name must be NUL-terminated within the record */ if (!memchr(ent->name, '\0', (char *)end - ent->name)) break; - ent->addr = bswap_64(ent->addr); - ent->lineno = bswap_32(ent->lineno); - ent->discrim = bswap_32(ent->discrim); + /* + * debug entries are packed with a variable-length + * name[], so entries after the first may be + * unaligned: byte-swap via unaligned-safe accessors. + */ + put_unaligned(bswap_64(get_unaligned(&ent->addr)), &ent->addr); + put_unaligned(bswap_32(get_unaligned(&ent->lineno)), &ent->lineno); + put_unaligned(bswap_32(get_unaligned(&ent->discrim)), &ent->discrim); ent = debug_entry_next(ent); } /* clamp so downstream consumers don't overrun */ From 9c2827b53120bea43995741fffc7a67c4d80905c Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Wed, 5 Aug 2026 16:08:21 -0300 Subject: [PATCH 014/425] perf genelf: Use unaligned-safe accessors for debug entries debug_entry records are packed with a variable-length name[] field, so entries after the first may start at addresses that are not naturally aligned. jit_process_debug_info(), get_special_opcode() and emit_lineno_info() read and write the u64 addr and int lineno fields through struct member access, which is undefined behavior on strict-alignment architectures. Use get_unaligned()/put_unaligned() to read and update each field, matching the layout the jitdump writers (LLVM, JVM agents) emit, which packs entries without padding. struct debug_entry.lineno is signed and emit_advance_lineno() takes a long line delta that relies on sign extension, so the field is read into an int: reading it into an unsigned int would turn a backward line jump into a huge forward one and corrupt the line number program. Reported-by: sashiko-bot Cc: Stephane Eranian Assisted-by: Opencode:DeepSeek-V4-Flash-free Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/genelf_debug.c | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tools/perf/util/genelf_debug.c b/tools/perf/util/genelf_debug.c index 8588b3e35e00..7d9ef43aa6ac 100644 --- a/tools/perf/util/genelf_debug.c +++ b/tools/perf/util/genelf_debug.c @@ -12,6 +12,8 @@ */ #include #include +#include +#include #include #include #include @@ -303,11 +305,13 @@ static ubyte get_special_opcode(struct debug_entry *ent, { unsigned int temp; unsigned long delta_addr; + int lineno = get_unaligned(&ent->lineno); + uint64_t addr = get_unaligned(&ent->addr); /* * delta from line_base */ - temp = (ent->lineno - last_line) - default_debug_line_header.line_base; + temp = (lineno - last_line) - default_debug_line_header.line_base; if (temp >= default_debug_line_header.line_range) return 0; @@ -315,7 +319,7 @@ static ubyte get_special_opcode(struct debug_entry *ent, /* * delta of addresses */ - delta_addr = (ent->addr - last_vma) / default_debug_line_header.minimum_instruction_length; + delta_addr = (addr - last_vma) / default_debug_line_header.minimum_instruction_length; /* This is not sufficient to ensure opcode will be in [0-256] but * sufficient to ensure when summing with the delta lineno we will @@ -362,6 +366,8 @@ static void emit_lineno_info(struct buffer_ext *be, for (i = 0; i < nr_entry; i++, ent = debug_entry_next(ent)) { int need_copy = 0; ubyte special_opcode; + int lineno = get_unaligned(&ent->lineno); + uint64_t addr = get_unaligned(&ent->addr); /* * check if filename changed, if so add it @@ -376,24 +382,24 @@ static void emit_lineno_info(struct buffer_ext *be, special_opcode = get_special_opcode(ent, last_line, last_vma); if (special_opcode != 0) { - last_line = ent->lineno; - last_vma = ent->addr; + last_line = lineno; + last_vma = addr; emit_opcode(be, special_opcode); } else { /* * lines differ, emit line delta */ - if (last_line != ent->lineno) { - emit_advance_lineno(be, ent->lineno - last_line); - last_line = ent->lineno; + if (last_line != lineno) { + emit_advance_lineno(be, lineno - last_line); + last_line = lineno; need_copy = 1; } /* * addresses differ, emit address delta */ - if (last_vma != ent->addr) { - emit_advance_pc(be, ent->addr - last_vma); - last_vma = ent->addr; + if (last_vma != addr) { + emit_advance_pc(be, addr - last_vma); + last_vma = addr; need_copy = 1; } /* @@ -480,7 +486,7 @@ jit_process_debug_info(uint64_t code_addr, int i; for (i = 0; i < nr_debug_entries; i++) { - ent->addr = ent->addr - code_addr; + put_unaligned(get_unaligned(&ent->addr) - code_addr, &ent->addr); ent = debug_entry_next(ent); } add_compilation_unit(di, buffer_ext_size(dl)); From cd9cd72b8036c0c4164cfb5f44258bfd69df9a19 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Wed, 5 Aug 2026 16:09:14 -0300 Subject: [PATCH 015/425] perf jitdump: Free unwinding data even when eh_frame_hdr_size is zero jit_repipe_code_load() only cleared the unwinding state when both unwinding_data and eh_frame_hdr_size were set. When a record carries unwinding data but eh_frame_hdr_size is 0, the cleanup condition fails and the unwinding state persists in jd, being applied to all subsequent JIT_CODE_LOAD and JIT_CODE_MOVE records, duplicating unwinding sections in the generated ELF files and inflating their event->mmap2.len. The record is validated upstream so eh_frame_hdr_size <= unwinding_size always holds. Free the unwinding data based on the data pointer alone. Reported-by: sashiko-bot Reviewed-by: Ian Rogers Cc: Stephane Eranian Assisted-by: Opencode:DeepSeek-V4-Flash-free Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index e0d5cc9a8281..efb40d93e33a 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -530,7 +530,7 @@ static int jit_repipe_code_load(struct jit_buf_desc *jd, union jr_entry *jr) jd->nr_debug_entries = 0; } - if (jd->unwinding_data && jd->eh_frame_hdr_size) { + if (jd->unwinding_data) { zfree(&jd->unwinding_data); jd->eh_frame_hdr_size = 0; jd->unwinding_mapped_size = 0; From adeae6235c83af852f926314b2135b012cb32bd3 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Thu, 3 Sep 2026 23:07:48 -0300 Subject: [PATCH 016/425] perf jitdump: Write sample id fields in the order used by evsel__id_hdr_size() jit_repipe_code_load() and jit_repipe_code_move() cast the sample id area appended to the synthesized mmap2 record to a fixed: struct { u32 pid, tid; u64 time; }; and store the timestamp at offset 8 whenever PERF_SAMPLE_TIME is set. That matches what evsel__id_hdr_size() accounts for only when PERF_SAMPLE_TID is set as well: the fields are appended in a fixed order, skipping the ones not requested by sample_type, so with PERF_SAMPLE_TID unset PERF_SAMPLE_TIME starts at offset 0 and idr_size is 8. Storing the timestamp at offset 8 then lands 8 bytes past the end of the id area, which for an event allocated as sizeof(*event) + idr_size is past the end of the heap allocation, besides corrupting the record the tooling reading it back expects. Walk the id area in the order used by evsel__id_hdr_size(), advancing past each field only when its sample_type bit is set, and keep the computed timestamp in a local variable instead of reading it back from the event buffer. Reported-by: sashiko-bot Cc: Stephane Eranian Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 64 +++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index efb40d93e33a..f8b937a95fe8 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -472,10 +472,8 @@ static int jit_repipe_code_load(struct jit_buf_desc *jd, union jr_entry *jr) int ret, csize; uint64_t usize; pid_t nspid, pid, tid; - struct { - u32 pid, tid; - u64 time; - } *id; + uint64_t timestamp = 0; + unsigned long id; nspid = jr->load.pid; pid = jr_entry_pid(jd, jr); @@ -561,13 +559,27 @@ static int jit_repipe_code_load(struct jit_buf_desc *jd, union jr_entry *jr) event->mmap2.flags = MAP_SHARED; event->mmap2.ino_generation = 1; - id = (void *)((unsigned long)event + event->mmap.header.size - idr_size); + /* + * The sample id fields are appended in the order accounted for by + * evsel__id_hdr_size(), skipping the ones not requested in + * sample_type, so they cannot be written through a fixed struct: + * with PERF_SAMPLE_TID unset, PERF_SAMPLE_TIME starts at offset 0 + * and idr_size is 8, so storing it at offset 8 runs past the end of + * the event allocation. + */ + id = (unsigned long)event + event->mmap.header.size - idr_size; if (jd->sample_type & PERF_SAMPLE_TID) { - id->pid = pid; - id->tid = tid; + struct { u32 pid, tid; } *id_tid = (void *)id; + + id_tid->pid = pid; + id_tid->tid = tid; + id += sizeof(u64); + } + if (jd->sample_type & PERF_SAMPLE_TIME) { + timestamp = convert_timestamp(jd, jr->load.p.timestamp); + *(u64 *)id = timestamp; + id += sizeof(u64); } - if (jd->sample_type & PERF_SAMPLE_TIME) - id->time = convert_timestamp(jd, jr->load.p.timestamp); /* * create pseudo sample to induce dso hit increment @@ -577,7 +589,7 @@ static int jit_repipe_code_load(struct jit_buf_desc *jd, union jr_entry *jr) sample.cpumode = PERF_RECORD_MISC_USER; sample.pid = pid; sample.tid = tid; - sample.time = id->time; + sample.time = timestamp; sample.ip = addr; ret = perf_event__process_mmap2(tool, event, &sample, jd->machine); @@ -624,10 +636,8 @@ static int jit_repipe_code_move(struct jit_buf_desc *jd, union jr_entry *jr) u16 idr_size; int ret; pid_t nspid, pid, tid; - struct { - u32 pid, tid; - u64 time; - } *id; + uint64_t timestamp = 0; + unsigned long id; nspid = jr->load.pid; pid = jr_entry_pid(jd, jr); @@ -675,13 +685,27 @@ static int jit_repipe_code_move(struct jit_buf_desc *jd, union jr_entry *jr) event->mmap2.flags = MAP_SHARED; event->mmap2.ino_generation = 1; - id = (void *)((unsigned long)event + event->mmap.header.size - idr_size); + /* + * The sample id fields are appended in the order accounted for by + * evsel__id_hdr_size(), skipping the ones not requested in + * sample_type, so they cannot be written through a fixed struct: + * with PERF_SAMPLE_TID unset, PERF_SAMPLE_TIME starts at offset 0 + * and idr_size is 8, so storing it at offset 8 runs past the end of + * the event allocation. + */ + id = (unsigned long)event + event->mmap.header.size - idr_size; if (jd->sample_type & PERF_SAMPLE_TID) { - id->pid = pid; - id->tid = tid; + struct { u32 pid, tid; } *id_tid = (void *)id; + + id_tid->pid = pid; + id_tid->tid = tid; + id += sizeof(u64); + } + if (jd->sample_type & PERF_SAMPLE_TIME) { + timestamp = convert_timestamp(jd, jr->load.p.timestamp); + *(u64 *)id = timestamp; + id += sizeof(u64); } - if (jd->sample_type & PERF_SAMPLE_TIME) - id->time = convert_timestamp(jd, jr->load.p.timestamp); /* * create pseudo sample to induce dso hit increment @@ -691,7 +715,7 @@ static int jit_repipe_code_move(struct jit_buf_desc *jd, union jr_entry *jr) sample.cpumode = PERF_RECORD_MISC_USER; sample.pid = pid; sample.tid = tid; - sample.time = id->time; + sample.time = timestamp; sample.ip = jr->move.new_code_addr; ret = perf_event__process_mmap2(tool, event, &sample, jd->machine); From ba8763e37e95215386b28762a747b8225d6042c4 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Wed, 5 Aug 2026 16:09:42 -0300 Subject: [PATCH 017/425] perf jitdump: Size code_move event allocation with idr_size jit_repipe_code_move() allocated the mmap2 event with a hardcoded +16, but computes event->mmap2.header.size as sizeof(event->mmap2) minus unused filename bytes plus idr_size. When idr_size is larger than 16, header.size exceeds the allocation, so perf_data__write() reads past the heap allocation, leaking adjacent heap memory into the generated perf.data file. Size the allocation with idr_size like jit_repipe_code_load() does. Reported-by: sashiko-bot Cc: Stephane Eranian Assisted-by: Opencode:DeepSeek-V4-Flash-free Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/jitdump.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/perf/util/jitdump.c b/tools/perf/util/jitdump.c index f8b937a95fe8..45a05316b3ca 100644 --- a/tools/perf/util/jitdump.c +++ b/tools/perf/util/jitdump.c @@ -646,9 +646,10 @@ static int jit_repipe_code_move(struct jit_buf_desc *jd, union jr_entry *jr) idr_size = jd->machine->id_hdr_size; /* - * +16 to account for sample_id_all (hack) + * Sample ID is written past the end of the mmap2 record; size + * the allocation to account for it instead of a hardcoded +16. */ - event = calloc(1, sizeof(*event) + 16); + event = calloc(1, sizeof(*event) + idr_size); if (!event) return -1; From 02241f791aa28802cc05223a740e163e14cd5ea2 Mon Sep 17 00:00:00 2001 From: Aaron Tomlin Date: Mon, 24 Aug 2026 09:31:18 -0400 Subject: [PATCH 018/425] perf trace: Fix error checking in btf_struct_scnprintf() btf_dump__dump_type_data() returns the positive number of bytes dumped on success, or a negative error code (e.g., -EINVAL) on failure. Currently, btf_struct_scnprintf() checks if btf_dump__dump_type_data() returns 0. When a negative error code is returned on failure, this check evaluates to false, causing the function to proceed down the success path and advance arg->augmented.args past the unprinted struct data before returning 0. Consequently, when syscall__scnprintf_args() falls back to the default argument beautifier upon receiving 0 from trace__btf_scnprintf(), the fallback beautifier or subsequent arguments read from an erroneously advanced augmented arguments buffer. Fix this by checking for '<= 0' ensuring that on any failure or 0-byte dump, 0 is returned without modifying the augmented arguments pointer. Fixes: cb32035214b9a09d ("perf trace: Pretty print struct data") Reported-by: sashiko-bot Reviewed-by: Ian Rogers Signed-off-by: Aaron Tomlin Tested-by: Ian Rogers Cc: Howard Chu [ Restored that call to be a single line ] Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-trace.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index c3c7f1f85c53..4c8f91f0f854 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -1066,7 +1066,7 @@ static size_t btf_struct_scnprintf(const struct btf_type *type, struct btf *btf, return 0; /* pretty print the struct data here */ - if (btf_dump__dump_type_data(btf_dump, type_id, arg->augmented.args->value, type->size, &dump_data_opts) == 0) + if (btf_dump__dump_type_data(btf_dump, type_id, arg->augmented.args->value, type->size, &dump_data_opts) <= 0) return 0; consumed = sizeof(*augmented_arg) + augmented_arg->size; From fc076b5f4807a36aeec32d25138eaa947ad6bc1f Mon Sep 17 00:00:00 2001 From: Aaron Tomlin Date: Mon, 24 Aug 2026 09:31:19 -0400 Subject: [PATCH 019/425] perf trace: Introduce kernel symbol beautifier for virtual addresses Currently, when 'perf trace' formats tracepoint payloads or system call arguments containing raw kernel virtual addresses (e.g., a work item function pointer work_func_t in workqueue:workqueue_execute_start), it prints them as raw hexadecimal values (e.g., 0xffffffff81234567). This impairs readability when tracing kernel execution flows. Introduce a dedicated kernel symbol beautifier, syscall_arg__scnprintf_ksym (i.e., SCA_KSYM), to resolve kernel virtual addresses to human-readable symbol names and offsets (e.g., "flush_to_ldisc"). The beautifier looks up the virtual address in the machine kernel maps via machine__find_kernel_symbol(). If a valid kernel symbol is found, the symbol name and offset are printed without requiring --libtraceevent; if the address is zero, "NULL" is rendered; otherwise, it gracefully falls back to hexadecimal formatting. Signed-off-by: Aaron Tomlin Reviewed-by: Ian Rogers Tested-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-trace.c | 29 +++++++++++++++++++++++++++++ tools/perf/trace/beauty/beauty.h | 3 +++ 2 files changed, 32 insertions(+) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 4c8f91f0f854..19132b6ea8fb 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -750,6 +750,35 @@ size_t syscall_arg__scnprintf_ptr(char *bf, size_t size, struct syscall_arg *arg return syscall_arg__scnprintf_hex(bf, size, arg); } +size_t syscall_arg__scnprintf_ksym(char *bf, size_t size, struct syscall_arg *arg) +{ + if (arg->val == 0) + return scnprintf(bf, size, "NULL"); + + if (arg->trace && arg->trace->host) { + struct map *map = NULL; + struct symbol *sym = machine__find_kernel_symbol(arg->trace->host, + arg->val, &map); + + if (sym) { + u64 start = map__unmap_ip(map, sym->start); + u64 offset = arg->val - start; + size_t printed; + + if (offset == 0) + printed = scnprintf(bf, size, "%s", sym->name); + else + printed = scnprintf(bf, size, "%s+0x%" PRIx64, + sym->name, offset); + map__put(map); + return printed; + } + map__put(map); + } + + return syscall_arg__scnprintf_hex(bf, size, arg); +} + size_t syscall_arg__scnprintf_int(char *bf, size_t size, struct syscall_arg *arg) { return scnprintf(bf, size, "%d", arg->val); diff --git a/tools/perf/trace/beauty/beauty.h b/tools/perf/trace/beauty/beauty.h index 58a3206481ae..0f4801c61a5b 100644 --- a/tools/perf/trace/beauty/beauty.h +++ b/tools/perf/trace/beauty/beauty.h @@ -160,6 +160,9 @@ size_t syscall_arg__scnprintf_hex(char *bf, size_t size, struct syscall_arg *arg size_t syscall_arg__scnprintf_ptr(char *bf, size_t size, struct syscall_arg *arg); #define SCA_PTR syscall_arg__scnprintf_ptr +size_t syscall_arg__scnprintf_ksym(char *bf, size_t size, struct syscall_arg *arg); +#define SCA_KSYM syscall_arg__scnprintf_ksym + size_t syscall_arg__scnprintf_int(char *bf, size_t size, struct syscall_arg *arg); #define SCA_INT syscall_arg__scnprintf_int From d02cd80fb6d73244e5bf783988d3e8ea4fb9554f Mon Sep 17 00:00:00 2001 From: Aaron Tomlin Date: Mon, 24 Aug 2026 09:31:20 -0400 Subject: [PATCH 020/425] perf trace: Auto-assign kernel symbol beautifier to function pointer fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracepoint fields that convey kernel function pointers, callbacks, and call sites, such as "function", "func", "fn", "callback", "action", "handler", "caller", "caller_ip", "location", "callsite", and "call_site" are currently formatted as generic hexadecimal pointers by default. Enhance syscall_arg_fmt__init_array() to automatically detect non-array function pointer fields by type signature (e.g., typedefs ending with "_func_t" or "_fn", or C function pointer types containing "(*)") and assign SCA_KSYM as their default beautifier. Additionally, register common function pointer and callback field names within the sorted syscall_arg_fmts__by_name lookup table. To prevent misclassifying dynamic string arrays, enums, or standard non-pointer integers sharing generic names, guard SCA_KSYM assignment to true pointer fields and pointer-sized non-array scalars (via field_is_ptr_sized(), field_is_enum(), and field_is_plain_int()). This ensures enum fields continue falling through to BTF pretty-printing. To support cross-architecture analysis (e.g., analyzing 32-bit trace data on a 64-bit host) and 64-bit instruction pointers, determine target pointer size via tep_get_long_size() while also supporting 64-bit scalars. This ensures tracepoint arguments such as workqueue:workqueue_execute_start.function, csd:csd_function.func, and xfs:xfs_bunmapi.caller_ip are symbolised automatically without requiring explicit per-event configuration. For example: ❯ sudo tools/perf/perf trace --event workqueue:workqueue_execute_end --max-events 2 --show-cpu 0.000 [000] kworker/u32:15/236682 workqueue:workqueue_execute_end(work: 0xffffffffab2f1420, function: toggle_allocation_gate) 0.132 [000] kworker/u32:15/236682 workqueue:workqueue_execute_end(work: 0xffff8ac2c1adc010, function: flush_to_ldisc) Signed-off-by: Aaron Tomlin Reviewed-by: Ian Rogers Tested-by: Arnaldo Carvalho de Melo Tested-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-trace.c | 113 ++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 38 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 19132b6ea8fb..f872c7c1d8dd 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2096,6 +2096,18 @@ static int syscall__alloc_arg_fmts(struct syscall *sc, int nr_args) } static const struct syscall_arg_fmt syscall_arg_fmts__by_name[] = { + { .name = "action", .scnprintf = SCA_KSYM, }, + { .name = "call_site", .scnprintf = SCA_KSYM, }, + { .name = "callback", .scnprintf = SCA_KSYM, }, + { .name = "caller", .scnprintf = SCA_KSYM, }, + { .name = "caller_ip", .scnprintf = SCA_KSYM, }, + { .name = "callsite", .scnprintf = SCA_KSYM, }, + { .name = "cb", .scnprintf = SCA_KSYM, }, + { .name = "fn", .scnprintf = SCA_KSYM, }, + { .name = "func", .scnprintf = SCA_KSYM, }, + { .name = "function", .scnprintf = SCA_KSYM, }, + { .name = "handler", .scnprintf = SCA_KSYM, }, + { .name = "location", .scnprintf = SCA_KSYM, }, { .name = "msr", .scnprintf = SCA_X86_MSR, .strtoul = STUL_X86_MSR, }, { .name = "vector", .scnprintf = SCA_X86_IRQ_VECTORS, .strtoul = STUL_X86_IRQ_VECTORS, }, }; @@ -2171,6 +2183,29 @@ static bool field_has_hex_fmt(struct tep_format_field *field, int len) return false; } +static bool field_is_enum(const struct tep_format_field *field) +{ + return field->type && strstr(field->type, "enum") != NULL; +} + +static bool field_is_plain_int(const struct tep_format_field *field) +{ + if (!field->type) + return false; + + return !strcmp(field->type, "int") || + !strcmp(field->type, "unsigned int") || + !strcmp(field->type, "u32") || + !strcmp(field->type, "s32"); +} + +static bool field_is_ptr_sized(const struct tep_format_field *field) +{ + int ptr_size = tep_get_long_size(field->event->tep); + + return field->size == ptr_size || field->size == sizeof(u64); +} + static struct tep_format_field * syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field *field, bool *use_btf) @@ -2198,38 +2233,49 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field ((len >= 4 && strcmp(field->name + len - 4, "name") == 0) || strstr(field->name, "path") != NULL)) { arg->scnprintf = SCA_FILENAME; - } else if ((field->flags & TEP_FIELD_IS_POINTER) || strstr(field->name, "addr") || - field_has_hex_fmt(field, len)) - arg->scnprintf = SCA_PTR; - else if (strcmp(field->type, "pid_t") == 0) - arg->scnprintf = SCA_PID; - else if (strcmp(field->type, "umode_t") == 0) - arg->scnprintf = SCA_MODE_T; - else if ((field->flags & TEP_FIELD_IS_ARRAY) && strstr(field->type, "char")) { - arg->scnprintf = SCA_CHAR_ARRAY; - arg->nr_entries = field->arraylen; - } else if ((strcmp(field->type, "int") == 0 || - strcmp(field->type, "unsigned int") == 0 || - strcmp(field->type, "long") == 0) && - len >= 2 && strcmp(field->name + len - 2, "fd") == 0) { - /* - * /sys/kernel/tracing/events/syscalls/sys_enter* - * grep -E 'field:.*fd;' .../format|sed -r 's/.*field:([a-z ]+) [a-z_]*fd.+/\1/g'|sort|uniq -c - * 65 int - * 23 unsigned int - * 7 unsigned long - */ - arg->scnprintf = SCA_FD; - } else if (strstr(field->type, "enum") && use_btf != NULL) { - *use_btf = true; - arg->strtoul = STUL_BTF_TYPE; + } else if (field->type && !(field->flags & TEP_FIELD_IS_ARRAY) && + (strstr(field->type, "(*)") != NULL || + strstr(field->type, "_func_t") != NULL || + strstr(field->type, "_fn") != NULL)) { + arg->scnprintf = SCA_KSYM; } else { const struct syscall_arg_fmt *fmt = syscall_arg_fmt__find_by_name(field->name); if (fmt) { - arg->scnprintf = fmt->scnprintf; - arg->strtoul = fmt->strtoul; + if (fmt->scnprintf == SCA_KSYM) { + if ((field->flags & TEP_FIELD_IS_POINTER) || + (!field_is_enum(field) && !field_is_plain_int(field) && + field_is_ptr_sized(field) && !(field->flags & TEP_FIELD_IS_ARRAY))) { + arg->scnprintf = fmt->scnprintf; + arg->strtoul = fmt->strtoul; + } + } else { + arg->scnprintf = fmt->scnprintf; + arg->strtoul = fmt->strtoul; + } + } + + if (arg->scnprintf == NULL) { + if ((field->flags & TEP_FIELD_IS_POINTER) || strstr(field->name, "addr") || + field_has_hex_fmt(field, len)) { + arg->scnprintf = SCA_PTR; + } else if (strcmp(field->type, "pid_t") == 0) { + arg->scnprintf = SCA_PID; + } else if (strcmp(field->type, "umode_t") == 0) { + arg->scnprintf = SCA_MODE_T; + } else if ((field->flags & TEP_FIELD_IS_ARRAY) && strstr(field->type, "char")) { + arg->scnprintf = SCA_CHAR_ARRAY; + arg->nr_entries = field->arraylen; + } else if ((strcmp(field->type, "int") == 0 || + strcmp(field->type, "unsigned int") == 0 || + strcmp(field->type, "long") == 0) && + len >= 2 && strcmp(field->name + len - 2, "fd") == 0) { + arg->scnprintf = SCA_FD; + } else if (field_is_enum(field) && use_btf != NULL) { + *use_btf = true; + arg->strtoul = STUL_BTF_TYPE; + } } } } @@ -3297,12 +3343,6 @@ static unsigned char bitmap_byte(const unsigned long *mask, int byte_idx) return b_val; } -static bool trace__field_is_ip(const char *name) -{ - return !strcmp(name, "__probe_ip") || - !strcmp(name, "caller_ip") || - !strcmp(name, "call_site"); -} static size_t trace__fprintf_tp_fields(struct trace *trace, struct perf_sample *sample, struct thread *thread, void *augmented_args, int augmented_args_size) @@ -3404,14 +3444,11 @@ static size_t trace__fprintf_tp_fields(struct trace *trace, struct perf_sample * * Suppress it by default to avoid cluttering the output. * If verbose mode is enabled, ensure it is formatted as a * hexadecimal memory address rather than a signed integer. - * - * caller_ip and call_site are also expected to be instruction - * pointers and should always be represented in hexadecimal. */ is_probe_ip = evsel__is_probe(evsel) && !strcmp(field->name, "__probe_ip"); - if (is_probe_ip || trace__field_is_ip(field->name)) { - if (is_probe_ip && !verbose) + if (is_probe_ip) { + if (!verbose) continue; printed += scnprintf(bf + printed, size - printed, From c6dd6161815d0c10ed24ad13fdad847bec63d961 Mon Sep 17 00:00:00 2001 From: Aaron Tomlin Date: Mon, 24 Aug 2026 09:31:21 -0400 Subject: [PATCH 021/425] perf trace: Enhance BTF type formatting to symbolise kernel function pointers When BTF (BPF Type Format) metadata is loaded from vmlinux, 'perf trace' can inspect the precise C types of tracepoint and system call parameters. However, function pointer arguments are currently not recognised during BTF pretty-printing and default to hexadecimal output. Introduce btf_is_func_ptr() to inspect BTF type hierarchies (i.e., traversing pointers, typedefs, and type modifiers) to determine whether a parameter resolves to a function prototype (BTF_KIND_FUNC_PROTO). Generalise BTF type caching via syscall_arg_fmt__cache_btf_type() to handle structs, unions, enums, and function pointers alike. When a field is identified as a kernel function pointer, trace__btf_scnprintf() routes its value to syscall_arg__scnprintf_ksym(), enabling automatic zero-config symbolisation of kernel function pointers whenever BTF is available. Signed-off-by: Aaron Tomlin Reviewed-by: Ian Rogers Tested-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-trace.c | 106 +++++++++++++++++++++++++++++-------- 1 file changed, 85 insertions(+), 21 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f872c7c1d8dd..be19d70eba09 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -122,6 +122,7 @@ struct syscall_arg_fmt { #ifdef HAVE_LIBBPF_SUPPORT const struct btf_type *type; int type_id; /* used in btf_dump */ + bool btf_type_cached; #endif }; @@ -979,21 +980,68 @@ static size_t syscall_arg__scnprintf_getrandom_flags(char *bf, size_t size, #define SCA_GETRANDOM_FLAGS syscall_arg__scnprintf_getrandom_flags #ifdef HAVE_LIBBPF_SUPPORT -static void syscall_arg_fmt__cache_btf_enum(struct syscall_arg_fmt *arg_fmt, struct btf *btf, char *type) +static bool btf_is_func_ptr(const struct btf *btf, const struct btf_type *type) { + int nr_ptrs = 0; + + while (type) { + if (btf_is_ptr(type)) { + if (++nr_ptrs > 1) + return false; + type = btf__type_by_id(btf, type->type); + } else if (btf_is_typedef(type) || btf_is_mod(type)) { + type = btf__type_by_id(btf, type->type); + } else { + break; + } + } + return nr_ptrs == 1 && type && btf_is_func_proto(type); +} + +static void syscall_arg_fmt__cache_btf_type(struct syscall_arg_fmt *arg_fmt, + struct btf *btf, const char *type) +{ + char name[128]; + const char *pos; + size_t len = 0; int id; - type = strstr(type, "enum "); + arg_fmt->btf_type_cached = true; + if (type == NULL) return; - type += 5; // skip "enum " to get the enumeration name + /* Pointers to enums are memory addresses, not scalar enums */ + if (strstr(type, "enum ") && strchr(type, '*')) + return; - id = btf__find_by_name(btf, type); + if ((pos = strstr(type, "enum ")) != NULL) + pos += 5; + else if ((pos = strstr(type, "struct ")) != NULL) + pos += 7; + else if ((pos = strstr(type, "union ")) != NULL) + pos += 6; + else + pos = type; + + while (isspace(*pos)) + pos++; + + while ((isalnum(pos[len]) || pos[len] == '_') && len < sizeof(name) - 1) { + name[len] = pos[len]; + len++; + } + name[len] = '\0'; + + if (len == 0) + return; + + id = btf__find_by_name(btf, name); if (id < 0) return; arg_fmt->type = btf__type_by_id(btf, id); + arg_fmt->type_id = id; } static bool syscall_arg__strtoul_btf_enum(char *bf, size_t size, struct syscall_arg *arg, u64 *val) @@ -1027,10 +1075,8 @@ static bool syscall_arg__strtoul_btf_type(char *bf, size_t size, struct syscall_ if (btf == NULL) return false; - if (arg->fmt->type == NULL) { - // See if this is an enum - syscall_arg_fmt__cache_btf_enum(arg->fmt, btf, type); - } + if (!arg->fmt->btf_type_cached) + syscall_arg_fmt__cache_btf_type(arg->fmt, btf, type); // Now let's see if we have a BTF type resolved bt = arg->fmt->type; @@ -1038,19 +1084,22 @@ static bool syscall_arg__strtoul_btf_type(char *bf, size_t size, struct syscall_ return false; // If it is an enum: - if (btf_is_enum(arg->fmt->type)) + if (btf_is_enum(arg->fmt->type)) { + if (type && strchr(type, '*')) + return false; return syscall_arg__strtoul_btf_enum(bf, size, arg, val); + } return false; } -static size_t btf_enum_scnprintf(const struct btf_type *type, struct btf *btf, char *bf, size_t size, int val) +static size_t btf_enum_scnprintf(const struct btf_type *type, struct btf *btf, char *bf, size_t size, unsigned long val) { struct btf_enum *be = btf_enum(type); const unsigned int nr_entries = btf_vlen(type); for (unsigned int i = 0; i < nr_entries; ++i, ++be) { - if (be->val == val) { + if ((unsigned long)(__u32)be->val == val || (unsigned long)be->val == val) { return scnprintf(bf, size, "%s", btf__name_by_offset(btf, be->name_off)); } @@ -1077,14 +1126,19 @@ static size_t btf_struct_scnprintf(const struct btf_type *type, struct btf *btf, .bf = bf, .size = size, }; - struct augmented_arg *augmented_arg = arg->augmented.args; + struct augmented_arg *augmented_arg; int type_id = arg->fmt->type_id, consumed; struct btf_dump *btf_dump; LIBBPF_OPTS(btf_dump_opts, dump_opts); LIBBPF_OPTS(btf_dump_type_data_opts, dump_data_opts); - if (arg == NULL || arg->augmented.args == NULL) + if (arg == NULL || arg->augmented.args == NULL || arg->augmented.size <= 0 || + arg->fmt == NULL || !arg->fmt->from_user) + return 0; + + augmented_arg = arg->augmented.args; + if (augmented_arg->size <= 0) return 0; dump_data_opts.compact = true; @@ -1095,8 +1149,10 @@ static size_t btf_struct_scnprintf(const struct btf_type *type, struct btf *btf, return 0; /* pretty print the struct data here */ - if (btf_dump__dump_type_data(btf_dump, type_id, arg->augmented.args->value, type->size, &dump_data_opts) <= 0) + if (btf_dump__dump_type_data(btf_dump, type_id, arg->augmented.args->value, type->size, &dump_data_opts) <= 0) { + btf_dump__free(btf_dump); return 0; + } consumed = sizeof(*augmented_arg) + augmented_arg->size; arg->augmented.args = ((void *)arg->augmented.args) + consumed; @@ -1108,33 +1164,40 @@ static size_t btf_struct_scnprintf(const struct btf_type *type, struct btf *btf, } static size_t trace__btf_scnprintf(struct trace *trace, struct syscall_arg *arg, char *bf, - size_t size, int val, char *type) + size_t size, unsigned long val, char *type) { struct syscall_arg_fmt *arg_fmt = arg->fmt; if (trace->btf == NULL) return 0; - if (arg_fmt->type == NULL) { - // Check if this is an enum and if we have the BTF type for it. - syscall_arg_fmt__cache_btf_enum(arg_fmt, trace->btf, type); - } + if (!arg_fmt->btf_type_cached) + syscall_arg_fmt__cache_btf_type(arg_fmt, trace->btf, type); // Did we manage to find a BTF type for the syscall/tracepoint argument? if (arg_fmt->type == NULL) return 0; + if (type && strchr(type, '*')) { + if (btf_is_enum(arg_fmt->type) || btf_is_func_ptr(trace->btf, arg_fmt->type)) + return 0; + } + if (btf_is_enum(arg_fmt->type)) return btf_enum_scnprintf(arg_fmt->type, trace->btf, bf, size, val); else if (btf_is_struct(arg_fmt->type) || btf_is_union(arg_fmt->type)) return btf_struct_scnprintf(arg_fmt->type, trace->btf, bf, size, arg); + else if (btf_is_func_ptr(trace->btf, arg_fmt->type)) { + arg->val = val; + return syscall_arg__scnprintf_ksym(bf, size, arg); + } return 0; } #else // HAVE_LIBBPF_SUPPORT static size_t trace__btf_scnprintf(struct trace *trace __maybe_unused, struct syscall_arg *arg __maybe_unused, - char *bf __maybe_unused, size_t size __maybe_unused, int val __maybe_unused, + char *bf __maybe_unused, size_t size __maybe_unused, unsigned long val __maybe_unused, char *type __maybe_unused) { return 0; @@ -2604,7 +2667,8 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, default_scnprintf = sc->arg_fmt[arg.idx].scnprintf; - if (trace->force_btf || default_scnprintf == NULL || default_scnprintf == SCA_PTR) { + if (trace->force_btf || default_scnprintf == NULL || + default_scnprintf == SCA_PTR || default_scnprintf == SCA_KSYM) { btf_printed = trace__btf_scnprintf(trace, &arg, bf + printed, size - printed, val, field->type); if (btf_printed) { From 35770d96fc707be3989ab7c2d9650759d8475c17 Mon Sep 17 00:00:00 2001 From: Aaron Tomlin Date: Mon, 24 Aug 2026 09:31:22 -0400 Subject: [PATCH 022/425] perf tests: Add shell test for kernel symbol beautifier Add a dedicated shell test script, trace_ksym_beautifier.sh, to verify that 'perf trace' properly symbolises kernel virtual addresses (e.g. call_site in kmem:kmalloc) and function pointer fields (e.g. function in timer:hrtimer_start) using the kernel symbol beautifier (SCA_KSYM). Committer testing: root@number:~# perf test "kernel sym" 145: perf trace kernel symbol beautifier tests : Ok === Test Summary === Passed main tests : 1 Passed subtests : 0 Skipped tests : 0 Failed tests : 0 root@number:~# perf test -vv "kernel sym" 145: perf trace kernel symbol beautifier tests: ---- start ---- test child forked, pid 920749 Testing perf trace kernel symbol beautifier (call_site) Testing perf trace kernel symbol beautifier (function pointer) ---- end(0) ---- 145: perf trace kernel symbol beautifier tests : Ok === Test Summary === Passed main tests : 1 Passed subtests : 0 Skipped tests : 0 Failed tests : 0 root@number:~# perf test -vvv "kernel sym" 145: perf trace kernel symbol beautifier tests: ---- start ---- test child forked, pid 920784 Testing perf trace kernel symbol beautifier (call_site) Testing perf trace kernel symbol beautifier (function pointer) ---- end(0) ---- 145: perf trace kernel symbol beautifier tests : Ok === Test Summary === Passed main tests : 1 Passed subtests : 0 Skipped tests : 0 Failed tests : 0 root@number:~# Signed-off-by: Aaron Tomlin Reviewed-by: Ian Rogers Tested-by: Arnaldo Carvalho de Melo Tested-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- .../perf/tests/shell/trace_ksym_beautifier.sh | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100755 tools/perf/tests/shell/trace_ksym_beautifier.sh diff --git a/tools/perf/tests/shell/trace_ksym_beautifier.sh b/tools/perf/tests/shell/trace_ksym_beautifier.sh new file mode 100755 index 000000000000..7c441f8551a2 --- /dev/null +++ b/tools/perf/tests/shell/trace_ksym_beautifier.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# perf trace kernel symbol beautifier tests +# SPDX-License-Identifier: GPL-2.0 + +err=0 + +# shellcheck source=lib/probe.sh +. "$(dirname "$0")"/lib/probe.sh +skip_if_no_perf_trace || exit 2 +[ "$(id -u)" = 0 ] || exit 2 + +test_ksym_call_site() { + echo "Testing perf trace kernel symbol beautifier (call_site)" + output="$(perf trace -e kmem:kmalloc --max-events=1 -- true 2>&1)" + if ! echo "$output" | grep -q -E "call_site: [a-zA-Z_][a-zA-Z0-9_]*" || echo "$output" | grep -q -E "call_site: 0x[0-9a-fA-F]+" + then + printf "Call site kernel symbol beautification failed, output:\n%s\n" "$output" + err=1 + fi +} + +test_ksym_function_ptr() { + echo "Testing perf trace kernel symbol beautifier (function pointer)" + output="$(perf trace -e timer:hrtimer_start --max-events=1 -- sleep 0.01 2>&1)" + if ! echo "$output" | grep -q -E "function: [a-zA-Z_][a-zA-Z0-9_]*" || echo "$output" | grep -q -E "function: 0x[0-9a-fA-F]+" + then + printf "Function pointer kernel symbol beautification failed, output:\n%s\n" "$output" + err=1 + fi +} + +test_ksym_call_site + +if [ $err = 0 ]; then + test_ksym_function_ptr +fi + +exit $err From 3b12692c678f70b997a8aaf5f01e48e4b37b7b3c Mon Sep 17 00:00:00 2001 From: Andi Kleen Date: Sun, 30 Aug 2026 12:34:37 -0700 Subject: [PATCH 023/425] perf tools record: Modernize -W man page Modernize the -W / --weight description in the manpage to cover more cases that are supported now. Reviewed-by: Namhyung Kim Signed-off-by: Andi Kleen Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-record.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/perf/Documentation/perf-record.txt b/tools/perf/Documentation/perf-record.txt index 178f483140ed..2c6510c5a87b 100644 --- a/tools/perf/Documentation/perf-record.txt +++ b/tools/perf/Documentation/perf-record.txt @@ -480,9 +480,11 @@ Note that this feature may not be available on all processors. -W:: --weight:: -Enable weightened sampling. An additional weight is recorded per sample and can be -displayed with the weight and local_weight sort keys. This currently works for TSX -abort events and some memory events in precise mode on modern Intel CPUs. +Enable weighted sampling. One or more additional weights can be recorded per sample +and can be displayed with the weight, weight2, weight3 and local_weight sort keys. +Possible uses for weights are memory or instruction retirement latencies. +Support depends on the PMU and the event, as well as its configuration. Typically +it requires use of 'p'. --namespaces:: Record events of type PERF_RECORD_NAMESPACES. This enables 'cgroup_id' sort key. From ee5e07b6c2cbd2930c34082cba831f1161de34d4 Mon Sep 17 00:00:00 2001 From: Andi Kleen Date: Sun, 30 Aug 2026 12:34:38 -0700 Subject: [PATCH 024/425] perf tools top: Add --weight option Add a -W/--weight option to perf top to collect weights too. Useful with follow on patches. Reviewed-by: Namhyung Kim Assisted-by: omp:GPT-5.6-Luna Signed-off-by: Andi Kleen Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-top.txt | 6 ++++++ tools/perf/builtin-top.c | 2 ++ 2 files changed, 8 insertions(+) diff --git a/tools/perf/Documentation/perf-top.txt b/tools/perf/Documentation/perf-top.txt index af3e4230c72f..2da2a16bbf26 100644 --- a/tools/perf/Documentation/perf-top.txt +++ b/tools/perf/Documentation/perf-top.txt @@ -25,6 +25,12 @@ OPTIONS --count=:: Event period to sample. +-W:: +--weight:: +Enable weighted sampling. One or more additional weights can be recorded per sample +and can be displayed with the weight, weight2, weight3 and local_weight sort keys. +Possible uses for weights are memory or instruction retirement latencies. + -C :: --cpu=:: Monitor only on the list of CPUs provided. Multiple CPUs can be provided as a diff --git a/tools/perf/builtin-top.c b/tools/perf/builtin-top.c index 570410599f1b..2888ac3b7b92 100644 --- a/tools/perf/builtin-top.c +++ b/tools/perf/builtin-top.c @@ -1493,6 +1493,8 @@ int cmd_top(int argc, const char **argv) OPT_CALLBACK(0, "filter", &top.evlist, "filter", "event filter", parse_filter), OPT_U64('c', "count", &opts->user_interval, "event period to sample"), + OPT_BOOLEAN('W', "weight", &opts->sample_weight, + "sample by weight (on special events only)"), OPT_STRING('p', "pid", &target->pid, "pid", "profile events on existing process id"), OPT_STRING('t', "tid", &target->tid, "tid", From fe2a382dd9fa1f5dfdec3bd7dcf82c12a9f91734 Mon Sep 17 00:00:00 2001 From: Andi Kleen Date: Sun, 30 Aug 2026 12:34:39 -0700 Subject: [PATCH 025/425] perf tools: Add support for displaying weights in annotate Add support for showing all the three possible per IP weights in annotate. The weights are shown by defaults if any are non zero. This is useful, especially with the new insn lat statistics, but also for all the existing weights. Add a hotkey to the interactive browser to turn them off (w), as well as a perf annotate command line option. The weights are stored unconditionally in the sym_hist_entry, which will increase memory consumption somewhat. Reviewed-by: Namhyung Kim Assisted-by: omp:GPT-5.6-Luna Signed-off-by: Andi Kleen Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-annotate.txt | 3 + tools/perf/Documentation/perf-report.txt | 6 + tools/perf/builtin-annotate.c | 3 + tools/perf/builtin-report.c | 3 + tools/perf/ui/browsers/annotate.c | 20 +++- tools/perf/util/annotate.c | 121 +++++++++++++++++++-- tools/perf/util/annotate.h | 44 +++++++- tools/perf/util/symbol_conf.h | 13 ++- 8 files changed, 194 insertions(+), 19 deletions(-) diff --git a/tools/perf/Documentation/perf-annotate.txt b/tools/perf/Documentation/perf-annotate.txt index a688738809c4..1a90b09a12d5 100644 --- a/tools/perf/Documentation/perf-annotate.txt +++ b/tools/perf/Documentation/perf-annotate.txt @@ -95,6 +95,9 @@ include::itrace.txt[] --gtk:: Use the GTK interface. +--weights:: Show or hide (with `--no-weights`) weight columns in annotation output. + By default, weight columns are shown when samples contain non-zero weights. + -C:: --cpu=:: Only report samples for the list of CPUs provided. Multiple CPUs can be provided as a comma-separated list with no space: 0,1. Ranges of diff --git a/tools/perf/Documentation/perf-report.txt b/tools/perf/Documentation/perf-report.txt index 22f87eaa3279..ae68ca402d0b 100644 --- a/tools/perf/Documentation/perf-report.txt +++ b/tools/perf/Documentation/perf-report.txt @@ -64,6 +64,9 @@ OPTIONS --symbol-filter=:: Only show symbols that match (partially) with this filter. +--weights:: Show or hide (with `--no-weights`) weight columns in annotation output. + By default, weight columns are shown when samples contain non-zero weights. + -U:: --hide-unresolved:: Only display entries resolved to a symbol. @@ -353,6 +356,9 @@ OPTIONS --gtk:: Use the GTK2 interface. +--weights:: Show or hide (with `--no-weights`) weight columns in annotation output. + By default, weight columns are shown when samples contain non-zero weights. + -k:: --vmlinux=:: vmlinux pathname diff --git a/tools/perf/builtin-annotate.c b/tools/perf/builtin-annotate.c index 69cb72b2082a..9e0b704c31f4 100644 --- a/tools/perf/builtin-annotate.c +++ b/tools/perf/builtin-annotate.c @@ -719,6 +719,8 @@ int cmd_annotate(int argc, const char **argv) OPT_BOOLEAN(0, "tui", &annotate.use_tui, "Use the TUI interface"), #endif OPT_BOOLEAN(0, "stdio", &annotate.use_stdio, "Use the stdio interface"), + OPT_BOOLEAN(0, "weights", &symbol_conf.annotate_weight, + "Show or hide weight columns in annotation. Default show if non zero."), OPT_BOOLEAN(0, "stdio2", &annotate.use_stdio2, "Use the stdio interface"), OPT_BOOLEAN(0, "ignore-vmlinux", &symbol_conf.ignore_vmlinux, "don't load vmlinux even if found"), @@ -788,6 +790,7 @@ int cmd_annotate(int argc, const char **argv) set_option_flag(options, 0, "show-total-period", PARSE_OPT_EXCLUSIVE); set_option_flag(options, 0, "show-nr-samples", PARSE_OPT_EXCLUSIVE); + symbol_conf.annotate_weight = true; annotation_options__init(); diff --git a/tools/perf/builtin-report.c b/tools/perf/builtin-report.c index 60d1f166629e..b55c01bb45b4 100644 --- a/tools/perf/builtin-report.c +++ b/tools/perf/builtin-report.c @@ -1364,6 +1364,8 @@ int cmd_report(int argc, const char **argv) #endif OPT_BOOLEAN(0, "stdio", &report.use_stdio, "Use the stdio interface"), + OPT_BOOLEAN(0, "weights", &symbol_conf.annotate_weight, + "Show or hide weight columns in annotation. Default show if non-zero."), OPT_BOOLEAN(0, "header", &report.header, "Show data header."), OPT_BOOLEAN(0, "header-only", &report.header_only, "Show only data header."), @@ -1525,6 +1527,7 @@ int cmd_report(int argc, const char **argv) * reference exited threads. */ symbol_conf.keep_exited_threads = true; + symbol_conf.annotate_weight = true; annotation_options__init(); diff --git a/tools/perf/ui/browsers/annotate.c b/tools/perf/ui/browsers/annotate.c index e47a46775089..e8de644c0e9d 100644 --- a/tools/perf/ui/browsers/annotate.c +++ b/tools/perf/ui/browsers/annotate.c @@ -189,7 +189,7 @@ static void annotate_browser__draw_current_jump(struct ui_browser *browser) struct map_symbol *ms = ab->b.priv; struct symbol *sym = ms->sym; struct annotation *notes = symbol__annotation(sym); - u8 pcnt_width = annotation__pcnt_width(notes); + u8 pcnt_width = annotation__pcnt_width(notes, ab->evsel); u8 cntr_width = annotation__br_cntr_width(); int width; int diff = 0; @@ -255,7 +255,8 @@ static unsigned int annotate_browser__refresh(struct ui_browser *browser) { struct annotation *notes = browser__annotation(browser); int ret = ui_browser__list_head_refresh(browser); - int pcnt_width = annotation__pcnt_width(notes); + int pcnt_width = annotation__pcnt_width(notes, + container_of(browser, struct annotate_browser, b)->evsel); if (annotate_opts.jump_arrows) annotate_browser__draw_current_jump(browser); @@ -972,6 +973,7 @@ static int annotate_browser__run(struct annotate_browser *browser, "O Bump offset level (jump targets -> +call -> all -> cycle thru)\n" "s Toggle source code view\n" "t Circulate percent, total period, samples view\n" + "w Toggle weight columns\n" "c Show min/max cycle\n" "/ Search string\n" "k Toggle line numbers\n" @@ -1091,6 +1093,14 @@ static int annotate_browser__run(struct annotate_browser *browser, symbol_conf.show_total_period = true; annotation__update_column_widths(notes); continue; + case 'w': + symbol_conf.annotate_weight = !symbol_conf.annotate_weight; + browser->b.width = notes->src->widths.max_line_len + + annotation__pcnt_width(notes, evsel) + + annotation__cycles_width(notes) + + annotation__br_cntr_width(); + ui_browser__refresh_dimensions(&browser->b); + continue; case 'c': if (annotate_opts.show_minmax_cycle) annotate_opts.show_minmax_cycle = false; @@ -1225,10 +1235,12 @@ int __hist_entry__tui_annotate(struct hist_entry *he, struct map_symbol *ms, browser.type_hash = hashmap__new(type_hash, type_equal, /*ctx=*/NULL); } - browser.b.width = notes->src->widths.max_line_len; + browser.b.width = notes->src->widths.max_line_len + + annotation__pcnt_width(notes, evsel) + + annotation__cycles_width(notes) + + annotation__br_cntr_width(); browser.b.nr_entries = notes->src->nr_entries; browser.b.entries = ¬es->src->source; - browser.b.width += 18; /* Percentage */ if (annotate_opts.hide_src_code) ui_browser__init_asm_mode(&browser.b); diff --git a/tools/perf/util/annotate.c b/tools/perf/util/annotate.c index df70e95a8470..9304c21c686a 100644 --- a/tools/perf/util/annotate.c +++ b/tools/perf/util/annotate.c @@ -222,6 +222,7 @@ static int __symbol__inc_addr_samples(struct map_symbol *ms, u64 offset; struct sym_hist *h; struct sym_hist_entry *entry; + u64 weight = sample->weight ?: sample->ins_lat ?: sample->weight3; pr_debug3("%s: addr=%#" PRIx64 "\n", __func__, map__unmap_ip(ms->map, addr)); @@ -256,10 +257,28 @@ static int __symbol__inc_addr_samples(struct map_symbol *ms, entry->nr_samples++; entry->period += sample->period; + if (sample->evsel->core.attr.sample_type & PERF_SAMPLE_WEIGHT_TYPE) { + entry->weight_sum[WEIGHT_WEIGHT] += sample->weight; + entry->weight_num[WEIGHT_WEIGHT]++; + if (sample->weight) + sym_hist__set_weight_mask(h, BIT(WEIGHT_WEIGHT)); + } + if (sample->evsel->core.attr.sample_type & PERF_SAMPLE_WEIGHT_STRUCT) { + entry->weight_sum[WEIGHT_INSNLAT] += sample->ins_lat; + entry->weight_num[WEIGHT_INSNLAT]++; + entry->weight_sum[WEIGHT_WEIGHT3] += sample->weight3; + entry->weight_num[WEIGHT_WEIGHT3]++; + if (sample->ins_lat) + sym_hist__set_weight_mask(h, BIT(WEIGHT_INSNLAT)); + if (sample->weight3) + sym_hist__set_weight_mask(h, BIT(WEIGHT_WEIGHT3)); + } + pr_debug3("%#" PRIx64 " %s: period++ [addr: %#" PRIx64 ", %#" PRIx64 - ", evidx=%d] => nr_samples: %" PRIu64 ", period: %" PRIu64 "\n", + ", evidx=%d] => nr_samples: %" PRIu64 ", period: %" PRIu64 + " weight %" PRIu64 "\n", sym->start, sym->name, addr, addr - sym->start, evsel->core.idx, - entry->nr_samples, entry->period); + entry->nr_samples, entry->period, weight); return 0; } @@ -778,6 +797,50 @@ static bool needs_type_info(struct annotated_data_type *data_type) return (data_type != &stackop_type) && (data_type != &canary_type); } +static const char *annotation__weight_mode_str(enum symbol__weight_mode mode, + const struct evsel *evsel) +{ + switch (mode) { + case WEIGHT_NONE: + return ""; + case WEIGHT_WEIGHT: + return "Weight"; + case WEIGHT_INSNLAT: + return "InsnLat"; + case WEIGHT_WEIGHT3: + switch (evsel__e_machine((struct evsel *)evsel, NULL)) { + case EM_PPC: + case EM_PPC64: + return "PCycleLat"; + case EM_X86_64: + return "RetireLat"; + default: + return "Weight3"; + } + default: + return ""; + } +} + +static void annotation__column_title(char *buf, size_t size, + struct annotation *notes, + const struct evsel *evsel) +{ + const char *base = symbol_conf.show_total_period ? "Period" : + symbol_conf.show_nr_samples ? "Samples" : "Percent"; + int weight; + u8 weight_mask = annotation__weight_mask(notes, evsel); + + scnprintf(buf, size, "%s", base); + for_each_weight(weight) { + if (weight_mask & BIT(weight)) + scnprintf(buf + strlen(buf), + size - strlen(buf), + " %s", + annotation__weight_mode_str(weight, evsel)); + } +} + static int annotation_line__print(struct annotation_line *al, struct annotation_print_data *apd, struct annotation_options *opts, int printed, @@ -833,6 +896,8 @@ annotation_line__print(struct annotation_line *al, struct annotation_print_data for (i = 0; i < nr_percent; i++) { struct annotation_data *data = &al->data[i]; double percent; + int weight; + u8 weight_mask = annotation__weight_mask(notes, apd->evsel); percent = annotation_data__percent(data, percent_type); color = get_percent_color(percent); @@ -845,6 +910,14 @@ annotation_line__print(struct annotation_line *al, struct annotation_print_data data->he.nr_samples); else color_fprintf(stdout, color, " %7.2f", percent); + for_each_weight(weight) { + if (weight_mask & BIT(weight)) + color_fprintf(stdout, color, " %7" PRIu64, + data->he.weight_num[weight] ? + data->he.weight_sum[weight] / + data->he.weight_num[weight] + : 0); + } } printf(" : "); @@ -891,7 +964,7 @@ annotation_line__print(struct annotation_line *al, struct annotation_print_data } else if (max_lines && printed >= max_lines) return 1; else { - int width = annotation__pcnt_width(notes); + int width = annotation__pcnt_width(notes, apd->evsel); if (queue) return -1; @@ -915,6 +988,9 @@ static void calc_percent(struct annotation *notes, struct sym_hist *sym_hist = annotation__histogram(notes, evsel); unsigned int hits = 0; u64 period = 0; + int i; + u64 weight_sum[WEIGHT_WEIGHT3 + 1] = { 0 }; + u64 weight_num[WEIGHT_WEIGHT3 + 1] = { 0 }; while (offset < end) { struct sym_hist_entry *entry; @@ -923,6 +999,10 @@ static void calc_percent(struct annotation *notes, if (entry) { hits += entry->nr_samples; period += entry->period; + for_each_weight(i) { + weight_sum[i] += entry->weight_sum[i]; + weight_num[i] += entry->weight_num[i]; + } } ++offset; } @@ -930,6 +1010,10 @@ static void calc_percent(struct annotation *notes, if (sym_hist->nr_samples) { data->he.period = period; data->he.nr_samples = hits; + for_each_weight(i) { + data->he.weight_sum[i] = weight_sum[i]; + data->he.weight_num[i] = weight_num[i]; + } data->percent[PERCENT_HITS_LOCAL] = 100.0 * hits / sym_hist->nr_samples; } @@ -1252,8 +1336,9 @@ int hist_entry__annotate_printf(struct hist_entry *he, struct evsel *evsel) int printed = 2, queue_len = 0; int more = 0; bool context = opts->context; - int width = annotation__pcnt_width(notes); + int width = annotation__pcnt_width(notes, evsel); int graph_dotted_len; + char title[64]; char buf[512]; filename = strdup(dso__long_name(dso)); @@ -1275,10 +1360,10 @@ int hist_entry__annotate_printf(struct hist_entry *he, struct evsel *evsel) return ENOTSUP; } - graph_dotted_len = printf(" %-*.*s| Source code & Disassembly of %s for %s (%" PRIu64 " samples, " + annotation__column_title(title, sizeof(title), notes, evsel); + graph_dotted_len = printf(" %-*.*s|\tSource code & Disassembly of %s for %s (%" PRIu64 " samples, " "percent: %s)\n", - width, width, symbol_conf.show_total_period ? "Period" : - symbol_conf.show_nr_samples ? "Samples" : "Percent", + width, width, title, d_filename, evsel_name, h->nr_samples, percent_type_str(opts->percent_type)); @@ -2029,7 +2114,8 @@ static int disasm_line__snprint_type_info(struct disasm_line *dl, return printed; } -void annotation_line__write(struct annotation_line *al, struct annotation *notes, +void annotation_line__write(struct annotation_line *al, + struct annotation *notes, const struct annotation_write_ops *wops, struct annotation_print_data *apd) { @@ -2037,7 +2123,8 @@ void annotation_line__write(struct annotation_line *al, struct annotation *notes bool change_color = wops->change_color; double percent_max = annotation_line__max_percent(al, annotate_opts.percent_type); int width = wops->width; - int pcnt_width = annotation__pcnt_width(notes); + int pcnt_width = annotation__pcnt_width(notes, apd->evsel); + u8 weight_mask = annotation__weight_mask(notes, apd->evsel); int cycles_width = annotation__cycles_width(notes); bool show_title = false; char bf[256]; @@ -2062,6 +2149,7 @@ void annotation_line__write(struct annotation_line *al, struct annotation *notes for (i = 0; i < al->data_nr; i++) { double percent; + int weight; percent = annotation_data__percent(&al->data[i], annotate_opts.percent_type); @@ -2075,6 +2163,15 @@ void annotation_line__write(struct annotation_line *al, struct annotation *notes } else { obj__printf(obj, "%7.2f ", percent); } + + for_each_weight(weight) { + if (weight_mask & BIT(weight)) + obj__printf(obj, "%7" PRIu64 " ", + al->data[i].he.weight_num[weight] ? + al->data[i].he.weight_sum[weight] / + al->data[i].he.weight_num[weight] : + 0); + } } } else { obj__set_percent_color(obj, 0, current_entry); @@ -2082,9 +2179,9 @@ void annotation_line__write(struct annotation_line *al, struct annotation *notes if (!show_title) obj__printf(obj, "%-*s", pcnt_width, " "); else { - obj__printf(obj, "%-*s", pcnt_width, - symbol_conf.show_total_period ? "Period" : - symbol_conf.show_nr_samples ? "Samples" : "Percent"); + char buf[64]; + annotation__column_title(buf, sizeof(buf), notes, apd->evsel); + obj__printf(obj, "%-*s", pcnt_width, buf); } } width -= pcnt_width; diff --git a/tools/perf/util/annotate.h b/tools/perf/util/annotate.h index fa08d09b80f7..d7807df6667f 100644 --- a/tools/perf/util/annotate.h +++ b/tools/perf/util/annotate.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -86,6 +87,8 @@ struct annotation; struct sym_hist_entry { u64 nr_samples; u64 period; + u64 weight_sum[WEIGHT_WEIGHT3 + 1]; + u64 weight_num[WEIGHT_WEIGHT3 + 1]; }; enum { @@ -231,8 +234,20 @@ void symbol__calc_percent(struct symbol *sym, struct evsel *evsel); struct sym_hist { u64 nr_samples; u64 period; + u8 weight_mask; }; +/* Can be set asynchronously by top. */ +static inline u8 sym_hist__weight_mask(const struct sym_hist *hist) +{ + return __atomic_load_n(&hist->weight_mask, __ATOMIC_RELAXED); +} + +static inline void sym_hist__set_weight_mask(struct sym_hist *hist, u8 mask) +{ + __atomic_fetch_or(&hist->weight_mask, mask, __ATOMIC_RELAXED); +} + /** * struct cyc_hist - (CPU) cycle histogram for a basic block * @@ -376,9 +391,34 @@ static inline int annotation__cycles_width(struct annotation *notes) return notes->branch ? ANNOTATION__IPC_WIDTH + ANNOTATION__CYCLES_WIDTH : 0; } -static inline int annotation__pcnt_width(struct annotation *notes) +static inline u8 annotation__weight_mask(struct annotation *notes, + const struct evsel *evsel) { - return (symbol_conf.show_total_period ? 12 : 8) * notes->src->nr_events; + u8 mask = 0; + struct evsel *pos; + int i; + + if (!symbol_conf.annotate_weight) + return 0; + + if (evsel__is_group_event((struct evsel *)evsel)) { + pos = (struct evsel *)evsel; + for (i = 0; i < evsel->core.nr_members; i++) { + mask |= sym_hist__weight_mask(¬es->src->histograms[pos->core.idx]); + pos = evsel__next(pos); + } + return mask; + } + + return sym_hist__weight_mask(¬es->src->histograms[evsel->core.idx]); +} + +static inline int annotation__pcnt_width(struct annotation *notes, + const struct evsel *evsel) +{ + int extra = hweight8(annotation__weight_mask(notes, evsel)) * 8; + return ((symbol_conf.show_total_period ? 12 : 8) + extra) * + notes->src->nr_events; } static inline bool annotation_line__filter(struct annotation_line *al) diff --git a/tools/perf/util/symbol_conf.h b/tools/perf/util/symbol_conf.h index 0dee5aa6a534..71f60081a85b 100644 --- a/tools/perf/util/symbol_conf.h +++ b/tools/perf/util/symbol_conf.h @@ -27,6 +27,16 @@ enum a2l_style { }; #define MAX_A2L_STYLE (A2L_STYLE_CMD + 1) +enum symbol__weight_mode { + WEIGHT_NONE = 0, + WEIGHT_WEIGHT, + WEIGHT_INSNLAT, + WEIGHT_WEIGHT3, +}; + +#define for_each_weight(_weight) \ + for ((_weight) = WEIGHT_WEIGHT; (_weight) <= WEIGHT_WEIGHT3; (_weight)++) + struct symbol_conf { bool nanosecs; unsigned short priv_size; @@ -69,7 +79,8 @@ struct symbol_conf { annotate_data_sample, skip_empty, enable_latency, - prefer_latency; + prefer_latency, + annotate_weight; const char *vmlinux_name, *kallsyms_name, *source_prefix, From 64de6b64b62115258b1368109433514605fba2a5 Mon Sep 17 00:00:00 2001 From: Andi Kleen Date: Sun, 30 Aug 2026 12:34:40 -0700 Subject: [PATCH 026/425] perf tools: Add test for weight annotation Add a test case to check if --stdio weight annotation works Reviewed-by: Namhyung Kim Assisted-by: omp:GPT-5.6-Luna Signed-off-by: Andi Kleen Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/annotate_weight.sh | 63 +++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100755 tools/perf/tests/shell/annotate_weight.sh diff --git a/tools/perf/tests/shell/annotate_weight.sh b/tools/perf/tests/shell/annotate_weight.sh new file mode 100755 index 000000000000..6b8c105c048b --- /dev/null +++ b/tools/perf/tests/shell/annotate_weight.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# perf annotate weight regression test +# SPDX-License-Identifier: GPL-2.0 + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=tools/perf/tests/shell/lib/perf_has_symbol.sh +. "${shelldir}"/lib/perf_has_symbol.sh + +testsym="test_loop" +skip_test_missing_symbol "${testsym}" + +perfdata=$(mktemp /tmp/__perf_test.annotate_weight.XXXXX) +record_log=$(mktemp /tmp/__perf_test.annotate_weight.XXXXX.log) +report_out=$(mktemp /tmp/__perf_test.annotate_weight.XXXXX.report) +annotate_out=$(mktemp /tmp/__perf_test.annotate_weight.XXXXX.annotate) + +cleanup() { + rm -f "${perfdata}" "${record_log}" "${report_out}" "${annotate_out}" + trap - EXIT TERM INT +} + +trap 'cleanup; exit 1' TERM INT +trap cleanup EXIT + +# mem-loads:pu requests a precise user PEBS event whose sample weight should +# be populated by -W. Unsupported PEBS/weight PMUs are skipped below. +if ! perf record -W -e mem-loads:pu -o "${perfdata}" -- perf test -w thloop \ + > /dev/null 2> "${record_log}"; then + echo "[SKIP] precise PEBS weight sampling is unavailable" + exit 2 +fi + +# Confirm the PMU actually produced nonzero weights. A successful record alone +# is insufficient: some PMUs accept the event but provide no weight payload. +if ! perf report --stdio -i "${perfdata}" --fields=weight1,weight2,weight3,symbol --percent-limit 0 \ + > "${report_out}" 2> "${record_log}"; then + echo "[SKIP] weighted samples cannot be decoded" + exit 2 +fi + +if ! awk '$1 ~ /^[0-9]/ && ($1 + 0) > 0 { found = 1 } END { exit !found }' \ + "${report_out}"; then + echo "[SKIP] PEBS weight sampling produced no nonzero weights" + exit 2 +fi + +perf annotate --stdio -i "${perfdata}" --symbol "${testsym}" \ + > "${annotate_out}" 2> "${record_log}" + +grep -q 'Percent Weight' "${annotate_out}" + +# The second numeric column is the rendered average weight. This assertion +# fails on the original regression because calc_percent() left it at zero. +if ! awk '$1 ~ /^[0-9]/ && $2 ~ /^[0-9]/ && ($2 + 0) > 0 { found = 1 } END { exit !found }' \ + "${annotate_out}"; then + echo "Annotation output contained no nonzero weight" + cat "${annotate_out}" + exit 1 +fi + +echo "PEBS annotation weights: PASS" From a202e928b282495b4acf68764817dbdadc9ed14e Mon Sep 17 00:00:00 2001 From: Chun-Tse Shao Date: Wed, 27 May 2026 15:19:33 -0700 Subject: [PATCH 027/425] perf pmu intel: Generalize SNC cpumask adjustment for multiple platforms Prepare for supporting more Intel platforms with sub-NUMA clustering by generalizing the GNR specific logic. Reviewed-by: Zide Chen Acked-by: Namhyung Kim Signed-off-by: Chun-Tse Shao Assisted-by: Gemini:gemini-3.1-pro-preview Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/arch/x86/util/pmu.c | 207 +++++++++++++++++++++------------ 1 file changed, 134 insertions(+), 73 deletions(-) diff --git a/tools/perf/arch/x86/util/pmu.c b/tools/perf/arch/x86/util/pmu.c index 7c9d238922a6..9b00d5720fb7 100644 --- a/tools/perf/arch/x86/util/pmu.c +++ b/tools/perf/arch/x86/util/pmu.c @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-2.0 +#include #include #include #include @@ -22,20 +23,31 @@ #include "util/env.h" #include "util/header.h" -static bool x86__is_intel_graniterapids(void) +#define GENUINE_INTEL_GNR "GenuineIntel-6-A[DE]" + +static bool cached_snc_supported; +static pthread_once_t snc_support_once = PTHREAD_ONCE_INIT; + +static void init_snc_support(void) { - static bool checked_if_graniterapids; - static bool is_graniterapids; + /* Graniterapids supports SNC configuration. */ + static const char *const supported_cpuids[] = { + GENUINE_INTEL_GNR, /* Graniterapids */ + }; + char *cpuid = get_cpuid_str((struct perf_cpu){0}); - if (!checked_if_graniterapids) { - const char *graniterapids_cpuid = "GenuineIntel-6-A[DE]"; - char *cpuid = get_cpuid_str((struct perf_cpu){0}); - - is_graniterapids = cpuid && strcmp_cpuid_str(graniterapids_cpuid, cpuid) == 0; - free(cpuid); - checked_if_graniterapids = true; + for (size_t i = 0; i < ARRAY_SIZE(supported_cpuids); i++) { + cached_snc_supported = cpuid && strcmp_cpuid_str(supported_cpuids[i], cpuid) == 0; + if (cached_snc_supported) + break; } - return is_graniterapids; + free(cpuid); +} + +static bool x86__is_snc_supported(void) +{ + pthread_once(&snc_support_once, init_snc_support); + return cached_snc_supported; } static struct perf_cpu_map *read_sysfs_cpu_map(const char *sysfs_path) @@ -52,49 +64,58 @@ static struct perf_cpu_map *read_sysfs_cpu_map(const char *sysfs_path) return cpus; } +static int cached_snc_nodes; +static pthread_once_t snc_nodes_once = PTHREAD_ONCE_INIT; + +static void init_snc_nodes(void) +{ + struct perf_cpu_map *node_cpus = + read_sysfs_cpu_map("devices/system/node/node0/cpulist"); + struct perf_cpu_map *cache_cpus = + read_sysfs_cpu_map("devices/system/cpu/cpu0/cache/index3/shared_cpu_list"); + + if (node_cpus && cache_cpus) + cached_snc_nodes = perf_cpu_map__nr(cache_cpus) / perf_cpu_map__nr(node_cpus); + else + cached_snc_nodes = 0; + perf_cpu_map__put(cache_cpus); + perf_cpu_map__put(node_cpus); +} + static int snc_nodes_per_l3_cache(void) { - static bool checked_snc; - static int snc_nodes; + pthread_once(&snc_nodes_once, init_snc_nodes); + return cached_snc_nodes; +} - if (!checked_snc) { - struct perf_cpu_map *node_cpus = - read_sysfs_cpu_map("devices/system/node/node0/cpulist"); - struct perf_cpu_map *cache_cpus = - read_sysfs_cpu_map("devices/system/cpu/cpu0/cache/index3/shared_cpu_list"); +static int cached_num_chas; +static pthread_once_t num_chas_once = PTHREAD_ONCE_INIT; - snc_nodes = perf_cpu_map__nr(cache_cpus) / perf_cpu_map__nr(node_cpus); - perf_cpu_map__put(cache_cpus); - perf_cpu_map__put(node_cpus); - checked_snc = true; +static void init_num_chas(void) +{ + int fd = perf_pmu__event_source_devices_fd(); + struct io_dir dir; + struct io_dirent64 *dent; + + if (fd < 0) { + cached_num_chas = -1; + return; } - return snc_nodes; + + io_dir__init(&dir, fd); + + while ((dent = io_dir__readdir(&dir)) != NULL) { + /* Note, dent->d_type will be DT_LNK and so isn't a useful filter. */ + if (strstarts(dent->d_name, "uncore_cha_")) + cached_num_chas++; + } + close(fd); } static int num_chas(void) { - static bool checked_chas; - static int num_chas; - - if (!checked_chas) { - int fd = perf_pmu__event_source_devices_fd(); - struct io_dir dir; - struct io_dirent64 *dent; - - if (fd < 0) - return -1; - - io_dir__init(&dir, fd); - - while ((dent = io_dir__readdir(&dir)) != NULL) { - /* Note, dent->d_type will be DT_LNK and so isn't a useful filter. */ - if (strstarts(dent->d_name, "uncore_cha_")) - num_chas++; - } - close(fd); - checked_chas = true; - } - return num_chas; + pthread_once(&num_chas_once, init_num_chas); + return cached_num_chas; } #define MAX_SNCS 6 @@ -121,46 +142,73 @@ static int uncore_cha_snc(struct perf_pmu *pmu) return 0; } chas_per_node = num_cha / snc_nodes; + if (chas_per_node == 0) { + pr_warning("Unexpected: chas_per_node is 0 (num_cha=%d, snc_nodes=%d)\n", + num_cha, snc_nodes); + return 0; + } cha_snc = cha_num / chas_per_node; /* Range check cha_snc. for unexpected out of bounds. */ return cha_snc >= MAX_SNCS ? 0 : cha_snc; } +static const u8 *cached_imc_snc_map; +static size_t cached_imc_snc_map_len; +static pthread_once_t imc_snc_map_once = PTHREAD_ONCE_INIT; + +static void init_snc_map(void) +{ + int snc_nodes = snc_nodes_per_l3_cache(); + char *cpuid; + static const u8 gnr_snc2_map[] = { 1, 1, 0, 0 }; + static const u8 snc3_map[] = { 1, 1, 0, 0, 2, 2 }; + + switch (snc_nodes) { + case 2: + cpuid = get_cpuid_str((struct perf_cpu){ 0 }); + if (cpuid) { + if (strcmp_cpuid_str(GENUINE_INTEL_GNR, cpuid) == 0) { + cached_imc_snc_map = gnr_snc2_map; + cached_imc_snc_map_len = ARRAY_SIZE(gnr_snc2_map); + } + free(cpuid); + } + break; + case 3: + cached_imc_snc_map = snc3_map; + cached_imc_snc_map_len = ARRAY_SIZE(snc3_map); + break; + default: + /* Error or no lookup support for SNC with >3 nodes. */ + break; + } + + if (!cached_imc_snc_map) + pr_warning("Unexpected: can not find snc map config\n"); +} + static int uncore_imc_snc(struct perf_pmu *pmu) { // Compute the IMC SNC using lookup tables. unsigned int imc_num; int snc_nodes = snc_nodes_per_l3_cache(); - const u8 snc2_map[] = {1, 1, 0, 0, 1, 1, 0, 0}; - const u8 snc3_map[] = {1, 1, 0, 0, 2, 2, 1, 1, 0, 0, 2, 2}; - const u8 *snc_map; - size_t snc_map_len; - switch (snc_nodes) { - case 2: - snc_map = snc2_map; - snc_map_len = ARRAY_SIZE(snc2_map); - break; - case 3: - snc_map = snc3_map; - snc_map_len = ARRAY_SIZE(snc3_map); - break; - default: - /* Error or no lookup support for SNC with >3 nodes. */ + if (snc_nodes <= 1) return 0; - } + + pthread_once(&imc_snc_map_once, init_snc_map); /* Compute SNC for PMU. */ if (sscanf(pmu->name, "uncore_imc_%u", &imc_num) != 1) { pr_warning("Unexpected: unable to compute IMC number '%s'\n", pmu->name); return 0; } - if (imc_num >= snc_map_len) { - pr_warning("Unexpected IMC %d for SNC%d mapping\n", imc_num, snc_nodes); + + if (!cached_imc_snc_map) return 0; - } - return snc_map[imc_num]; + + return cached_imc_snc_map[imc_num % cached_imc_snc_map_len]; } static int uncore_cha_imc_compute_cpu_adjust(int pmu_snc) @@ -200,7 +248,9 @@ static int uncore_cha_imc_compute_cpu_adjust(int pmu_snc) return cpu_adjust[pmu_snc]; } -static void gnr_uncore_cha_imc_adjust_cpumask_for_snc(struct perf_pmu *pmu, bool cha) +static pthread_mutex_t pmu_adjust_mutex = PTHREAD_MUTEX_INITIALIZER; + +static void uncore_cha_imc_adjust_cpumask_for_snc(struct perf_pmu *pmu, bool cha) { // With sub-NUMA clustering (SNC) there is a NUMA node per SNC in the // topology. For example, a two socket graniterapids machine may be set @@ -231,9 +281,11 @@ static void gnr_uncore_cha_imc_adjust_cpumask_for_snc(struct perf_pmu *pmu, bool return; } + pthread_mutex_lock(&pmu_adjust_mutex); + pmu_snc = cha ? uncore_cha_snc(pmu) : uncore_imc_snc(pmu); if (pmu_snc == 0) { - // No adjustment necessary for the first SNC. + pthread_mutex_unlock(&pmu_adjust_mutex); return; } @@ -242,8 +294,10 @@ static void gnr_uncore_cha_imc_adjust_cpumask_for_snc(struct perf_pmu *pmu, bool // Hold onto the perf_cpu_map globally to avoid recomputation. cpu_adjust = uncore_cha_imc_compute_cpu_adjust(pmu_snc); adjusted[pmu_snc] = perf_cpu_map__empty_new(perf_cpu_map__nr(pmu->cpus)); - if (!adjusted[pmu_snc]) + if (!adjusted[pmu_snc]) { + pthread_mutex_unlock(&pmu_adjust_mutex); return; + } } perf_cpu_map__for_each_cpu(cpu, idx, pmu->cpus) { @@ -263,6 +317,8 @@ static void gnr_uncore_cha_imc_adjust_cpumask_for_snc(struct perf_pmu *pmu, bool perf_cpu_map__put(pmu->cpus); pmu->cpus = perf_cpu_map__get(adjusted[pmu_snc]); + + pthread_mutex_unlock(&pmu_adjust_mutex); } void perf_pmu__arch_init(struct perf_pmu *pmu) @@ -300,11 +356,16 @@ void perf_pmu__arch_init(struct perf_pmu *pmu) pmu->mem_events = perf_mem_events_intel_aux; else pmu->mem_events = perf_mem_events_intel; - } else if (x86__is_intel_graniterapids()) { - if (strstarts(pmu->name, "uncore_cha_")) - gnr_uncore_cha_imc_adjust_cpumask_for_snc(pmu, /*cha=*/true); - else if (strstarts(pmu->name, "uncore_imc_")) - gnr_uncore_cha_imc_adjust_cpumask_for_snc(pmu, /*cha=*/false); + } else if (x86__is_snc_supported()) { + int snc_nodes = snc_nodes_per_l3_cache(); + + if (snc_nodes == 2 || snc_nodes == 3) { + if (strstarts(pmu->name, "uncore_cha_")) + uncore_cha_imc_adjust_cpumask_for_snc(pmu, /*cha=*/true); + else if (strstarts(pmu->name, "uncore_imc_") && + !strstarts(pmu->name, "uncore_imc_free_running")) + uncore_cha_imc_adjust_cpumask_for_snc(pmu, /*cha=*/false); + } } } } From 24dac8f0ec7924e7493e48f6020e556d40ff52e2 Mon Sep 17 00:00:00 2001 From: Chun-Tse Shao Date: Wed, 27 May 2026 15:19:34 -0700 Subject: [PATCH 028/425] perf pmu intel: Adjust cpumasks for sub-NUMA clusters on Sapphire Rapids and Emerald Rapids Similar to GNR [1], Sapphire Rapids and Emerald Rapids support sub-NUMA clusters as well. Adjust cpumasks using the same logic as GNR in [1]. Tested on Emerald Rapids with SNC2 enabled: $ perf stat --per-node -e 'UNC_CHA_CLOCKTICKS,UNC_M_CLOCKTICKS' -a -- sleep 1 Performance counter stats for 'system wide': N0 30 72125876670 UNC_CHA_CLOCKTICKS N0 4 8815163648 UNC_M_CLOCKTICKS N1 30 72124958844 UNC_CHA_CLOCKTICKS N1 4 8815014974 UNC_M_CLOCKTICKS N2 30 72121049022 UNC_CHA_CLOCKTICKS N2 4 8814592626 UNC_M_CLOCKTICKS N3 30 72117133854 UNC_CHA_CLOCKTICKS N3 4 8814012840 UNC_M_CLOCKTICKS 1.001574118 seconds time elapsed [1] lore.kernel.org/20250515181417.491401-1-irogers@google.com Reviewed-by: Zide Chen Reviewed-by: Ian Rogers Acked-by: Namhyung Kim Signed-off-by: Chun-Tse Shao Assisted-by: Gemini:gemini-3.1-pro-preview Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/arch/x86/util/pmu.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tools/perf/arch/x86/util/pmu.c b/tools/perf/arch/x86/util/pmu.c index 9b00d5720fb7..2c24ef3140da 100644 --- a/tools/perf/arch/x86/util/pmu.c +++ b/tools/perf/arch/x86/util/pmu.c @@ -23,6 +23,8 @@ #include "util/env.h" #include "util/header.h" +#define GENUINE_INTEL_SPR "GenuineIntel-6-8F" +#define GENUINE_INTEL_EMR "GenuineIntel-6-CF" #define GENUINE_INTEL_GNR "GenuineIntel-6-A[DE]" static bool cached_snc_supported; @@ -30,8 +32,10 @@ static pthread_once_t snc_support_once = PTHREAD_ONCE_INIT; static void init_snc_support(void) { - /* Graniterapids supports SNC configuration. */ + /* Sapphirerapids Emeraldrapids Graniterapids support SNC configuration. */ static const char *const supported_cpuids[] = { + GENUINE_INTEL_SPR, /* Sapphirerapids */ + GENUINE_INTEL_EMR, /* Emeraldrapids */ GENUINE_INTEL_GNR, /* Graniterapids */ }; char *cpuid = get_cpuid_str((struct perf_cpu){0}); @@ -161,6 +165,7 @@ static void init_snc_map(void) { int snc_nodes = snc_nodes_per_l3_cache(); char *cpuid; + static const u8 spr_emr_snc2_map[] = { 0, 0, 1, 1 }; static const u8 gnr_snc2_map[] = { 1, 1, 0, 0 }; static const u8 snc3_map[] = { 1, 1, 0, 0, 2, 2 }; @@ -168,7 +173,11 @@ static void init_snc_map(void) case 2: cpuid = get_cpuid_str((struct perf_cpu){ 0 }); if (cpuid) { - if (strcmp_cpuid_str(GENUINE_INTEL_GNR, cpuid) == 0) { + if (strcmp_cpuid_str(GENUINE_INTEL_SPR, cpuid) == 0 || + strcmp_cpuid_str(GENUINE_INTEL_EMR, cpuid) == 0) { + cached_imc_snc_map = spr_emr_snc2_map; + cached_imc_snc_map_len = ARRAY_SIZE(spr_emr_snc2_map); + } else if (strcmp_cpuid_str(GENUINE_INTEL_GNR, cpuid) == 0) { cached_imc_snc_map = gnr_snc2_map; cached_imc_snc_map_len = ARRAY_SIZE(gnr_snc2_map); } From 2148e094671b0f6a44f033787b7da4ed14e96c27 Mon Sep 17 00:00:00 2001 From: Mark Amirkan Date: Thu, 3 Sep 2026 04:04:20 -0700 Subject: [PATCH 029/425] perf evsel: Validate branch stack before byte swapping When perf reads an opposite-endian branch stack, __evsel__parse_sample() swaps each entry before checking whether all entries fit in the event. A truncated sample can therefore make the swap loop read and write past the event boundary. A truncated perf.data file makes perf report crash with SIGSEGV. ASan reports an out-of-bounds read. A regression test puts an entry just past the declared end and shows that its flags are changed before the parser returns -EFAULT. Move the bounds check before the byte-swap loop. Valid samples are handled as before. Fixes: 63c12ae2f246dcdc ("perf evsel: Add bitfield_swap() to handle branch_stack endian issue") Reviewed-by: Ian Rogers Assisted-by: Symbolic Signed-off-by: Mark Amirkan Cc: Madhavan Srinivasan Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/sample-parsing.c | 48 +++++++++++++++++++++++++++++++ tools/perf/util/evsel.c | 3 +- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/tools/perf/tests/sample-parsing.c b/tools/perf/tests/sample-parsing.c index 32dbc484487a..583951534937 100644 --- a/tools/perf/tests/sample-parsing.c +++ b/tools/perf/tests/sample-parsing.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 #include +#include #include #include #include @@ -417,6 +418,49 @@ static int do_test(u64 sample_type, u64 sample_regs, u64 read_format) return ret; } +static int test_truncated_branch_stack(void) +{ + struct perf_event_attr attr = { + .sample_type = PERF_SAMPLE_BRANCH_STACK, + }; + struct { + struct perf_event_header header; + u64 nr; + struct branch_entry entry; + } input = { + .header = { + .type = PERF_RECORD_SAMPLE, + .size = sizeof(input.header) + sizeof(input.nr), + }, + .nr = 1, + }; + struct perf_sample sample; + struct evsel *evsel; + u64 flags = 1; + int err; + + input.entry.flags.value = flags; + evsel = evsel__new(&attr); + if (!evsel) + return -1; + + evsel->sample_size = __evsel__sample_size(attr.sample_type); + err = __evsel__parse_sample(evsel, (union perf_event *)&input, + &sample, /*needs_swap=*/true); + perf_sample__exit(&sample); + evsel__put(evsel); + + if (err != -EFAULT) { + pr_debug("truncated branch stack returned %d, expected -EFAULT\n", err); + return -1; + } + if (input.entry.flags.value != flags) { + pr_debug("truncated branch stack modified data past the event\n"); + return -1; + } + return 0; +} + /** * test__sample_parsing - test sample parsing. * @@ -433,6 +477,10 @@ static int test__sample_parsing(struct test_suite *test __maybe_unused, int subt size_t i; int err; + err = test_truncated_branch_stack(); + if (err) + return err; + /* * Fail the test if it has not been updated when new sample format bits * were added. Please actually update the test rather than just change diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index d4cb455f4a7d..cc0bc0857754 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -3639,6 +3639,8 @@ int __evsel__parse_sample(struct evsel *evsel, union perf_event *event, e = (struct branch_entry *)&data->branch_stack->hw_idx; } + OVERFLOW_CHECK(array, sz, max_size); + if (swapped) { /* * struct branch_flag does not have endian @@ -3654,7 +3656,6 @@ int __evsel__parse_sample(struct evsel *evsel, union perf_event *event, e->flags.value = evsel__bitfield_swap_branch_flags(e->flags.value); } - OVERFLOW_CHECK(array, sz, max_size); array = (void *)array + sz; if (evsel__has_branch_counters(evsel)) { From 02f6847e1822714a4201b87e42f92b0d43e8549d Mon Sep 17 00:00:00 2001 From: Mark Amirkan Date: Thu, 3 Sep 2026 04:04:21 -0700 Subject: [PATCH 030/425] perf evsel: Validate RAW sample before byte swapping For an opposite-endian RAW sample, __evsel__parse_sample() passes the input-controlled size to mem_bswap_64() before checking whether the payload fits in the event. A truncated record can therefore make the helper read and write past the event boundary. A crafted perf.data file makes perf report crash with SIGSEGV. ASan reports the out-of-bounds access. A regression test puts backed data past the declared end and shows that it is changed before the parser returns -EFAULT. Move the bounds checks before mem_bswap_64(). Check the rounded length too, because the helper accesses complete 64-bit words. Complete records are handled as before. Fixes: f9d8adb345d7adbb ("perf evsel: Fix swap for samples with raw data") Reviewed-by: Ian Rogers Assisted-by: Symbolic Signed-off-by: Mark Amirkan Cc: Jiri Olsa Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/sample-parsing.c | 61 +++++++++++++++++++++++++++++++ tools/perf/util/evsel.c | 17 +++++---- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/tools/perf/tests/sample-parsing.c b/tools/perf/tests/sample-parsing.c index 583951534937..bd30f6d4c31b 100644 --- a/tools/perf/tests/sample-parsing.c +++ b/tools/perf/tests/sample-parsing.c @@ -461,6 +461,55 @@ static int test_truncated_branch_stack(void) return 0; } +static int test_truncated_swapped_raw(u16 event_size, u32 raw_size) +{ + struct perf_event_attr attr = { + .sample_type = PERF_SAMPLE_RAW, + }; + struct { + struct perf_event_header header; + union { + u64 value; + u32 words[2]; + } raw; + u64 canary; + } input = { + .header = { + .type = PERF_RECORD_SAMPLE, + .size = event_size, + }, + /* Parsing a pre-swapped word exchanges these two u32 values. */ + .raw.words = { 0x12345678, raw_size }, + .canary = 0x8877665544332211ULL, + }; + struct perf_sample sample; + struct evsel *evsel; + u64 raw = input.raw.value; + u64 canary = input.canary; + int err; + + evsel = evsel__new(&attr); + if (!evsel) + return -1; + + evsel->sample_size = __evsel__sample_size(attr.sample_type); + err = __evsel__parse_sample(evsel, (union perf_event *)&input, + &sample, /*needs_swap=*/true); + perf_sample__exit(&sample); + evsel__put(evsel); + + if (err != -EFAULT) { + pr_debug("truncated swapped RAW sample (size %u, raw %u) returned %d, expected -EFAULT\n", + event_size, raw_size, err); + return -1; + } + if (input.raw.value != raw || input.canary != canary) { + pr_debug("truncated swapped RAW sample modified data before validation\n"); + return -1; + } + return 0; +} + /** * test__sample_parsing - test sample parsing. * @@ -481,6 +530,18 @@ static int test__sample_parsing(struct test_suite *test __maybe_unused, int subt if (err) return err; + /* The declared RAW payload extends past an otherwise aligned event. */ + err = test_truncated_swapped_raw(sizeof(struct perf_event_header) + + sizeof(u64), 16); + if (err) + return err; + + /* The final complete word touched by mem_bswap_64() extends past it. */ + err = test_truncated_swapped_raw(sizeof(struct perf_event_header) + + sizeof(u32) + 9, 9); + if (err) + return err; + /* * Fail the test if it has not been updated when new sample format bits * were added. Please actually update the test rather than just change diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index cc0bc0857754..ce429eb247b6 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -3584,7 +3584,10 @@ int __evsel__parse_sample(struct evsel *evsel, union perf_event *event, } if (type & PERF_SAMPLE_RAW) { + const __u64 *raw; + OVERFLOW_CHECK_u64(array); + raw = array; u.val64 = *array; /* @@ -3600,16 +3603,14 @@ int __evsel__parse_sample(struct evsel *evsel, union perf_event *event, } data->raw_size = u.val32[0]; - /* - * The raw data is aligned on 64bits including the - * u32 size, so it's safe to use mem_bswap_64. - */ - if (swapped) - mem_bswap_64((void *) array, data->raw_size); - array = (void *)array + sizeof(u32); - OVERFLOW_CHECK(array, data->raw_size, max_size); + if (swapped) { + /* mem_bswap_64() accesses complete 64-bit words. */ + sz = roundup((u64)data->raw_size, sizeof(u64)); + OVERFLOW_CHECK(raw, sz, max_size); + mem_bswap_64((void *)raw, data->raw_size); + } data->raw_data = (void *)array; array = (void *)array + data->raw_size; } From 7071a2871c765038eb6da6f17f3149e6ba255eee Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 2 Sep 2026 08:48:53 -0700 Subject: [PATCH 031/425] perf tool: Remove util/cache.h Move definitions to places they are used, or path.h in the case of path.c's mkpath function. Remove unused definitions. Fix transitive include dependencies. Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-annotate.c | 6 +++++- tools/perf/builtin-config.c | 4 +++- tools/perf/builtin-help.c | 3 ++- tools/perf/perf.c | 11 ++++++++++- tools/perf/util/cache.h | 31 ------------------------------ tools/perf/util/config.c | 5 ++++- tools/perf/util/help-unknown-cmd.c | 6 +++++- tools/perf/util/path.c | 2 +- tools/perf/util/path.h | 3 +++ tools/perf/util/strbuf.c | 5 ++++- 10 files changed, 37 insertions(+), 39 deletions(-) delete mode 100644 tools/perf/util/cache.h diff --git a/tools/perf/builtin-annotate.c b/tools/perf/builtin-annotate.c index 9e0b704c31f4..88de3bd577e2 100644 --- a/tools/perf/builtin-annotate.c +++ b/tools/perf/builtin-annotate.c @@ -8,10 +8,10 @@ */ #include "builtin.h" #include "perf.h" +#include #include "util/color.h" #include -#include "util/cache.h" #include #include "util/symbol.h" @@ -38,12 +38,16 @@ #include "util/branch.h" #include "util/util.h" #include "ui/progress.h" +#include "ui/ui.h" #include #include #include #include #include +#include +#include +#include struct perf_annotate { struct perf_tool tool; diff --git a/tools/perf/builtin-config.c b/tools/perf/builtin-config.c index 237600643bbd..845a8af67fc5 100644 --- a/tools/perf/builtin-config.c +++ b/tools/perf/builtin-config.c @@ -7,14 +7,16 @@ */ #include "builtin.h" -#include "util/cache.h" #include #include "util/debug.h" #include "util/config.h" +#include "util/path.h" #include #include #include #include +#include +#include static bool use_system_config, use_user_config; diff --git a/tools/perf/builtin-help.c b/tools/perf/builtin-help.c index 2692b2e40a23..f0e95d741892 100644 --- a/tools/perf/builtin-help.c +++ b/tools/perf/builtin-help.c @@ -4,8 +4,8 @@ * * Builtin help command */ -#include "util/cache.h" #include "util/config.h" +#include "util/path.h" #include "util/strbuf.h" #include "builtin.h" #include @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include diff --git a/tools/perf/perf.c b/tools/perf/perf.c index 1f51e8de6b1b..8ad01a78fdd5 100644 --- a/tools/perf/perf.c +++ b/tools/perf/perf.c @@ -10,13 +10,13 @@ #include "perf.h" #include "util/build-id.h" -#include "util/cache.h" #include "util/env.h" #include // page_size #include #include "util/config.h" #include #include "util/parse-events.h" +#include #include #include #include "util/debug.h" @@ -30,7 +30,10 @@ #include #include #include +#include #include +#include +#include #include #include #include @@ -39,6 +42,12 @@ #include #include +#define CMD_EXEC_PATH "--exec-path" +#define CMD_DEBUGFS_DIR "--debugfs-dir=" + +#define EXEC_PATH_ENVIRONMENT "PERF_EXEC_PATH" +#define PERF_PAGER_ENVIRONMENT "PERF_PAGER" + static int use_pager = -1; static FILE *debug_fp = NULL; diff --git a/tools/perf/util/cache.h b/tools/perf/util/cache.h deleted file mode 100644 index 0b61840d4226..000000000000 --- a/tools/perf/util/cache.h +++ /dev/null @@ -1,31 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0 */ -#ifndef __PERF_CACHE_H -#define __PERF_CACHE_H - -#include "strbuf.h" -#include -#include "../ui/ui.h" - -#include -#include - -#define CMD_EXEC_PATH "--exec-path" -#define CMD_DEBUGFS_DIR "--debugfs-dir=" - -#define EXEC_PATH_ENVIRONMENT "PERF_EXEC_PATH" -#define PERF_DEBUGFS_ENVIRONMENT "PERF_DEBUGFS_DIR" -#define PERF_TRACEFS_ENVIRONMENT "PERF_TRACEFS_DIR" -#define PERF_PAGER_ENVIRONMENT "PERF_PAGER" - -int split_cmdline(char *cmdline, const char ***argv); - -#define alloc_nr(x) (((x)+16)*3/2) - -static inline int is_absolute_path(const char *path) -{ - return path[0] == '/'; -} - -char *mkpath(char *path_buf, size_t sz, const char *fmt, ...) __printf(3, 4); - -#endif /* __PERF_CACHE_H */ diff --git a/tools/perf/util/config.c b/tools/perf/util/config.c index 7988149dc7ed..5b696139f655 100644 --- a/tools/perf/util/config.c +++ b/tools/perf/util/config.c @@ -11,7 +11,6 @@ */ #include #include -#include "cache.h" #include "callchain.h" #include "header.h" #include @@ -19,6 +18,7 @@ #include "util/hist.h" /* perf_hist_config */ #include "util/stat.h" /* perf_stat__set_big_num */ #include "util/evsel.h" /* evsel__hw_names, evsel__use_bpf_counters */ +#include "path.h" #include "srcline.h" #include "build-id.h" #include "debug.h" @@ -26,7 +26,10 @@ #include "unwind.h" #include #include +#include #include +#include +#include #include #include #include diff --git a/tools/perf/util/help-unknown-cmd.c b/tools/perf/util/help-unknown-cmd.c index a0a46e34f8d1..f1cd9d4746f3 100644 --- a/tools/perf/util/help-unknown-cmd.c +++ b/tools/perf/util/help-unknown-cmd.c @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-2.0 -#include "cache.h" #include "config.h" #include #include #include +#include #include #include "../builtin.h" #include "levenshtein.h" @@ -36,10 +36,14 @@ static int add_cmd_list(struct cmdnames *cmds, struct cmdnames *old) if (nr > cmds->alloc) { /* Choose bigger one to alloc */ +#define alloc_nr(x) (((x) + 16) * 3 / 2) + if (alloc_nr(cmds->alloc) < nr) cmds->alloc = nr; else cmds->alloc = alloc_nr(cmds->alloc); + +#undef alloc_nr tmp = realloc(cmds->names, cmds->alloc * sizeof(*cmds->names)); if (!tmp) return -1; diff --git a/tools/perf/util/path.c b/tools/perf/util/path.c index 2e62f272fda8..5ba7861a3e65 100644 --- a/tools/perf/util/path.c +++ b/tools/perf/util/path.c @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-2.0 #include "path.h" -#include "cache.h" #include #include +#include #include #include #include diff --git a/tools/perf/util/path.h b/tools/perf/util/path.h index fb850fb55c60..e7cdb0cb1097 100644 --- a/tools/perf/util/path.h +++ b/tools/perf/util/path.h @@ -4,9 +4,12 @@ #include #include +#include struct dirent; +char *mkpath(char *path_buf, size_t sz, const char *fmt, ...) __printf(3, 4); + int path__join(char *bf, size_t size, const char *path1, const char *path2); int path__join3(char *bf, size_t size, const char *path1, const char *path2, const char *path3); diff --git a/tools/perf/util/strbuf.c b/tools/perf/util/strbuf.c index a64a37628f12..df3f9c4123fd 100644 --- a/tools/perf/util/strbuf.c +++ b/tools/perf/util/strbuf.c @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-2.0 -#include "cache.h" #include "debug.h" #include "strbuf.h" #include @@ -8,6 +7,7 @@ #include #include #include +#include #include /* @@ -54,9 +54,12 @@ int strbuf_grow(struct strbuf *sb, size_t extra) if (nr <= sb->len) return -E2BIG; +#define alloc_nr(x) (((x) + 16) * 3 / 2) + if (alloc_nr(sb->alloc) > nr) nr = alloc_nr(sb->alloc); +#undef alloc_nr /* * Note that sb->buf == strbuf_slopbuf if sb->alloc == 0, and it is * a static variable. Thus we have to avoid passing it to realloc. From 583ba92ef0ecd95c9b27e2574fa44f3e6fcb9848 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 2 Sep 2026 08:48:54 -0700 Subject: [PATCH 032/425] perf tool: Tidy up util/cache.h header file users Use git clang-format to sort header files. Review header file includes removing those that were unnecessary or adding explicit includes in cases where transitive dependencies were be using. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-annotate.c | 71 ++++++++++++++---------------- tools/perf/builtin-config.c | 18 ++++---- tools/perf/builtin-help.c | 31 ++++++------- tools/perf/perf.c | 45 +++++++++---------- tools/perf/util/config.c | 41 +++++++++-------- tools/perf/util/help-unknown-cmd.c | 13 +++--- tools/perf/util/path.c | 8 ++-- tools/perf/util/path.h | 3 +- tools/perf/util/strbuf.c | 9 ++-- 9 files changed, 121 insertions(+), 118 deletions(-) diff --git a/tools/perf/builtin-annotate.c b/tools/perf/builtin-annotate.c index 88de3bd577e2..1466b5dca5f2 100644 --- a/tools/perf/builtin-annotate.c +++ b/tools/perf/builtin-annotate.c @@ -6,49 +6,46 @@ * look up and read DSOs and symbol information and display * a histogram of results, along various sorting keys. */ -#include "builtin.h" -#include "perf.h" -#include - -#include "util/color.h" -#include -#include -#include "util/symbol.h" - -#include "util/debug.h" - -#include "util/evlist.h" -#include "util/evsel.h" -#include "util/annotate.h" -#include "util/annotate-data.h" -#include "util/event.h" -#include -#include "util/parse-events.h" -#include "util/sort.h" -#include "util/hist.h" -#include "util/dso.h" -#include "util/machine.h" -#include "util/map.h" -#include "util/session.h" -#include "util/tool.h" -#include "util/data.h" -#include "arch/common.h" -#include "util/block-range.h" -#include "util/map_symbol.h" -#include "util/branch.h" -#include "util/util.h" -#include "ui/progress.h" -#include "ui/ui.h" - -#include #include -#include -#include #include +#include #include #include #include +#include +#include +#include +#include +#include + +#include + +#include "arch/common.h" +#include "builtin.h" +#include "perf.h" +#include "ui/progress.h" +#include "ui/ui.h" +#include "util/annotate-data.h" +#include "util/annotate.h" +#include "util/block-range.h" +#include "util/branch.h" +#include "util/data.h" +#include "util/debug.h" +#include "util/dso.h" +#include "util/event.h" +#include "util/evlist.h" +#include "util/evsel.h" +#include "util/hist.h" +#include "util/machine.h" +#include "util/map.h" +#include "util/map_symbol.h" +#include "util/session.h" +#include "util/sort.h" +#include "util/symbol.h" +#include "util/tool.h" +#include "util/util.h" + struct perf_annotate { struct perf_tool tool; struct perf_session *session; diff --git a/tools/perf/builtin-config.c b/tools/perf/builtin-config.c index 845a8af67fc5..cefd042e4f85 100644 --- a/tools/perf/builtin-config.c +++ b/tools/perf/builtin-config.c @@ -5,19 +5,21 @@ * Copyright (C) 2015, Taeung Song * */ -#include "builtin.h" - -#include -#include "util/debug.h" -#include "util/config.h" -#include "util/path.h" -#include #include +#include #include #include -#include #include +#include + +#include + +#include "builtin.h" +#include "util/config.h" +#include "util/debug.h" +#include "util/path.h" + static bool use_system_config, use_user_config; static const char * const config_usage[] = { diff --git a/tools/perf/builtin-help.c b/tools/perf/builtin-help.c index f0e95d741892..5bbdb4d9b9c0 100644 --- a/tools/perf/builtin-help.c +++ b/tools/perf/builtin-help.c @@ -4,29 +4,30 @@ * * Builtin help command */ -#include "util/config.h" -#include "util/path.h" -#include "util/strbuf.h" -#include "builtin.h" -#include -#include -#include -#include -#include "util/debug.h" -#include "util/util.h" -#include -#include -#include #include #include +#include #include #include -#include #include -#include + +#include +#include #include #include +#include +#include +#include +#include + +#include "builtin.h" +#include "util/config.h" +#include "util/debug.h" +#include "util/path.h" +#include "util/strbuf.h" +#include "util/util.h" + static struct man_viewer_list { struct man_viewer_list *next; char name[0]; diff --git a/tools/perf/perf.c b/tools/perf/perf.c index 8ad01a78fdd5..53c50cfd92d6 100644 --- a/tools/perf/perf.c +++ b/tools/perf/perf.c @@ -6,41 +6,36 @@ * This is the main hub from which the sub-commands (perf stat, * perf top, perf record, perf report, etc.) are started. */ -#include "builtin.h" #include "perf.h" -#include "util/build-id.h" -#include "util/env.h" -#include // page_size -#include -#include "util/config.h" -#include -#include "util/parse-events.h" -#include -#include -#include -#include "util/debug.h" -#include "util/event.h" -#include "util/util.h" // usage() -#include "ui/ui.h" -#include "perf-sys.h" -#include -#include -#include #include -#include #include +#include #include #include #include -#include #include -#include -#include -#include -#include + #include #include +#include + +#include +#include // page_size +#include +#include +#include +#include +#include +#include + +#include "builtin.h" +#include "perf-sys.h" +#include "ui/ui.h" +#include "util/build-id.h" +#include "util/config.h" +#include "util/debug.h" +#include "util/util.h" // usage() #define CMD_EXEC_PATH "--exec-path" #define CMD_DEBUGFS_DIR "--debugfs-dir=" diff --git a/tools/perf/util/config.c b/tools/perf/util/config.c index 5b696139f655..b2972c35c1ec 100644 --- a/tools/perf/util/config.c +++ b/tools/perf/util/config.c @@ -9,31 +9,34 @@ * Copyright (C) Johannes Schindelin, 2005 * */ -#include -#include -#include "callchain.h" -#include "header.h" -#include -#include "util/event.h" /* proc_map_timeout */ -#include "util/hist.h" /* perf_hist_config */ -#include "util/stat.h" /* perf_stat__set_big_num */ -#include "util/evsel.h" /* evsel__hw_names, evsel__use_bpf_counters */ -#include "path.h" -#include "srcline.h" -#include "build-id.h" -#include "debug.h" #include "config.h" -#include "unwind.h" -#include -#include + +#include +#include #include #include -#include #include -#include + +#include #include #include -#include +#include +#include +#include + +#include + +#include "build-id.h" +#include "callchain.h" +#include "debug.h" +#include "header.h" +#include "path.h" +#include "srcline.h" +#include "unwind.h" +#include "util/event.h" /* proc_map_timeout */ +#include "util/evsel.h" /* evsel__hw_names, evsel__use_bpf_counters */ +#include "util/hist.h" /* perf_hist_config */ +#include "util/stat.h" /* perf_stat__set_big_num */ #define MAXNAME (256) diff --git a/tools/perf/util/help-unknown-cmd.c b/tools/perf/util/help-unknown-cmd.c index f1cd9d4746f3..64dbe6e514f5 100644 --- a/tools/perf/util/help-unknown-cmd.c +++ b/tools/perf/util/help-unknown-cmd.c @@ -1,13 +1,16 @@ // SPDX-License-Identifier: GPL-2.0 -#include "config.h" -#include #include #include #include -#include -#include "../builtin.h" -#include "levenshtein.h" + #include +#include + +#include + +#include "../builtin.h" +#include "config.h" +#include "levenshtein.h" static int autocorrect; diff --git a/tools/perf/util/path.c b/tools/perf/util/path.c index 5ba7861a3e65..b7592e16718d 100644 --- a/tools/perf/util/path.c +++ b/tools/perf/util/path.c @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-2.0 #include "path.h" -#include + #include #include #include #include -#include -#include + #include -#include +#include +#include static char *cleanup_path(char *path) { diff --git a/tools/perf/util/path.h b/tools/perf/util/path.h index e7cdb0cb1097..62e86d390589 100644 --- a/tools/perf/util/path.h +++ b/tools/perf/util/path.h @@ -2,8 +2,9 @@ #ifndef _PERF_PATH_H #define _PERF_PATH_H -#include #include +#include + #include struct dirent; diff --git a/tools/perf/util/strbuf.c b/tools/perf/util/strbuf.c index df3f9c4123fd..4080bc02a533 100644 --- a/tools/perf/util/strbuf.c +++ b/tools/perf/util/strbuf.c @@ -1,15 +1,16 @@ // SPDX-License-Identifier: GPL-2.0 -#include "debug.h" #include "strbuf.h" -#include -#include -#include + #include #include #include #include + +#include #include +#include "debug.h" + /* * Used as the default ->buf value, so that people can always assume * buf is non NULL and ->buf is NUL terminated even for a freshly From bc8af6295ad205edad12837bf34860fa1720f96c Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 2 Sep 2026 08:48:55 -0700 Subject: [PATCH 033/425] perf tool: Remove unused includes in perf.c Remove perf-sys.h, internal/lib.h (page_size), signal.h, and perf.h since their exports are not used directly in perf.c. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/perf.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/perf/perf.c b/tools/perf/perf.c index 53c50cfd92d6..7e2e667b011f 100644 --- a/tools/perf/perf.c +++ b/tools/perf/perf.c @@ -6,10 +6,8 @@ * This is the main hub from which the sub-commands (perf stat, * perf top, perf record, perf report, etc.) are started. */ -#include "perf.h" #include -#include #include #include #include @@ -21,7 +19,6 @@ #include #include -#include // page_size #include #include #include @@ -30,7 +27,6 @@ #include #include "builtin.h" -#include "perf-sys.h" #include "ui/ui.h" #include "util/build-id.h" #include "util/config.h" From c6b40a642563c7b9ddaea19880c0c36659cc7743 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 2 Sep 2026 08:48:56 -0700 Subject: [PATCH 034/425] perf tool: Move usage strings and functions to perf.c The code is mostly used by perf.c with additional unused functionality such as function pointers derived from early git code. Moving it into perf.c directly reduces the code footprint, drops the util/util.h dependence from perf.c, and allows us to remove util/usage.c entirely. The string constants are exposed in builtin.h, as they are used in builtin-help.c. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin.h | 3 +++ tools/perf/perf.c | 13 ++++++++++++- tools/perf/util/Build | 1 - tools/perf/util/usage.c | 34 ---------------------------------- tools/perf/util/util.h | 4 ---- 5 files changed, 15 insertions(+), 40 deletions(-) delete mode 100644 tools/perf/util/usage.c diff --git a/tools/perf/builtin.h b/tools/perf/builtin.h index 40c4078c295f..e0b430821abc 100644 --- a/tools/perf/builtin.h +++ b/tools/perf/builtin.h @@ -2,6 +2,9 @@ #ifndef BUILTIN_H #define BUILTIN_H +extern const char perf_usage_string[]; +extern const char perf_more_info_string[]; + struct feature_status { const char *name; const char *macro; diff --git a/tools/perf/perf.c b/tools/perf/perf.c index 7e2e667b011f..65bdfd71f626 100644 --- a/tools/perf/perf.c +++ b/tools/perf/perf.c @@ -31,7 +31,18 @@ #include "util/build-id.h" #include "util/config.h" #include "util/debug.h" -#include "util/util.h" // usage() + +const char perf_usage_string[] = + "perf [--version] [--help] [OPTIONS] COMMAND [ARGS]"; + +const char perf_more_info_string[] = + "See 'perf help COMMAND' for more information on a specific command."; + +static void __noreturn usage(const char *err) +{ + fprintf(stderr, "\n Usage: %s\n", err); + exit(129); +} #define CMD_EXEC_PATH "--exec-path" #define CMD_DEBUGFS_DIR "--debugfs-dir=" diff --git a/tools/perf/util/Build b/tools/perf/util/Build index b26a0b1ddfa3..d08f2af7d970 100644 --- a/tools/perf/util/Build +++ b/tools/perf/util/Build @@ -54,7 +54,6 @@ perf-util-y += string.o perf-util-y += strlist.o perf-util-y += strfilter.o perf-util-y += top.o -perf-util-y += usage.o perf-util-y += dso.o perf-util-y += dsos.o perf-util-y += symbol.o diff --git a/tools/perf/util/usage.c b/tools/perf/util/usage.c deleted file mode 100644 index 4c8ffbad2323..000000000000 --- a/tools/perf/util/usage.c +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -/* - * usage.c - * - * Various reporting routines. - * Originally copied from GIT source. - * - * Copyright (C) Linus Torvalds, 2005 - */ -#include "util.h" -#include -#include -#include - -const char perf_usage_string[] = - "perf [--version] [--help] [OPTIONS] COMMAND [ARGS]"; - -const char perf_more_info_string[] = - "See 'perf help COMMAND' for more information on a specific command."; - -static __noreturn void usage_builtin(const char *err) -{ - fprintf(stderr, "\n Usage: %s\n", err); - exit(129); -} - -/* If we are in a dlopen()ed .so write to a global variable would segfault - * (ugh), so keep things static. */ -static void (*usage_routine)(const char *err) __noreturn = usage_builtin; - -void usage(const char *err) -{ - usage_routine(err); -} diff --git a/tools/perf/util/util.h b/tools/perf/util/util.h index 87a0818a8c76..f8f1ff603c72 100644 --- a/tools/perf/util/util.h +++ b/tools/perf/util/util.h @@ -17,9 +17,6 @@ #include #endif -extern const char perf_usage_string[]; -extern const char perf_more_info_string[]; - extern const char *input_name; /* This will control if perf_{host,guest} will set attr.exclude_{host,guest}. */ @@ -29,7 +26,6 @@ extern bool perf_host; extern bool perf_guest; /* General helper functions */ -void usage(const char *err) __noreturn; struct dirent; struct strlist; From d845187d47e84111882121d96d3969d40913d5e0 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 2 Sep 2026 08:48:57 -0700 Subject: [PATCH 035/425] perf tool: Add fallback stubs for missing dependencies Declare stubs directly inside perf.c for commands that have library dependencies sometimes omitted at build time, such as libtraceevent (timechart, sched, kmem, lock, trace, kwork) and libelf (probe). By unconditionally including these commands in the commands[] array, users will receive a clear, unified warning indicating the missing dependency instead of the command silently being absent. Running `perf trace` with libtraceevent missing. Before: ``` perf: 'trace' is not a perf-command. See 'perf --help'. Did you mean this? ftrace ``` After: ``` 'trace' command not available: missing libtraceevent devel package at build time. ``` Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/perf.c | 51 ++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/tools/perf/perf.c b/tools/perf/perf.c index 65bdfd71f626..6c5baa285b13 100644 --- a/tools/perf/perf.c +++ b/tools/perf/perf.c @@ -7,6 +7,7 @@ * perf top, perf record, perf report, etc.) are started. */ +#include #include #include #include @@ -53,6 +54,38 @@ static void __noreturn usage(const char *err) static int use_pager = -1; static FILE *debug_fp = NULL; + +#ifndef HAVE_LIBTRACEEVENT +#define DECLARE_LIBTRACEEVENT_STUB(_cmd) \ + int cmd_##_cmd(int argc __always_unused, const char **argv __always_unused) \ + { \ + fprintf(stderr, \ + "'" #_cmd "' command not available: missing libtraceevent " \ + "devel package at build time.\n"); \ + return -1; \ + } + +DECLARE_LIBTRACEEVENT_STUB(timechart) +DECLARE_LIBTRACEEVENT_STUB(sched) +DECLARE_LIBTRACEEVENT_STUB(kmem) +DECLARE_LIBTRACEEVENT_STUB(lock) +DECLARE_LIBTRACEEVENT_STUB(trace) +DECLARE_LIBTRACEEVENT_STUB(kwork) +#endif + +#ifndef HAVE_LIBELF_SUPPORT +#define DECLARE_LIBELF_SUPPORT_STUB(_cmd) \ + int cmd_##_cmd(int argc __always_unused, const char **argv __always_unused) \ + { \ + fprintf(stderr, \ + "'" #_cmd "' command not available: missing libelf " \ + "devel package at build time.\n"); \ + return -1; \ + } + +DECLARE_LIBELF_SUPPORT_STUB(probe) +#endif + struct cmd_struct { const char *cmd; int (*fn)(int, const char **); @@ -76,36 +109,24 @@ static const struct cmd_struct commands[] = { { "report", cmd_report, 0 }, { "bench", cmd_bench, 0 }, { "stat", cmd_stat, 0 }, -#ifdef HAVE_LIBTRACEEVENT { "timechart", cmd_timechart, 0 }, -#endif { "top", cmd_top, 0 }, { "annotate", cmd_annotate, 0 }, { "version", cmd_version, 0 }, { "script", cmd_script, 0 }, -#ifdef HAVE_LIBTRACEEVENT { "sched", cmd_sched, 0 }, -#endif -#ifdef HAVE_LIBELF_SUPPORT { "probe", cmd_probe, 0 }, -#endif -#ifdef HAVE_LIBTRACEEVENT { "kmem", cmd_kmem, 0 }, { "lock", cmd_lock, 0 }, -#endif { "kvm", cmd_kvm, 0 }, { "test", cmd_test, 0 }, -#if defined(HAVE_LIBTRACEEVENT) { "trace", cmd_trace, 0 }, -#endif { "inject", cmd_inject, 0 }, { "mem", cmd_mem, 0 }, { "data", cmd_data, 0 }, { "ftrace", cmd_ftrace, 0 }, { "daemon", cmd_daemon, 0 }, -#ifdef HAVE_LIBTRACEEVENT { "kwork", cmd_kwork, 0 }, -#endif }; struct pager_config { @@ -514,15 +535,9 @@ int main(int argc, const char **argv) argv[0] = cmd; } if (strstarts(cmd, "trace")) { -#ifndef HAVE_LIBTRACEEVENT - fprintf(stderr, - "trace command not available: missing libtraceevent devel package at build time.\n"); - goto out; -#else setup_path(); argv[0] = "trace"; return cmd_trace(argc, argv); -#endif } /* Look for flags.. */ argv++; From 537ff9c4271d0903f3652886aa29371a104ea649 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 2 Sep 2026 08:48:58 -0700 Subject: [PATCH 036/425] perf hist: Remove keysym.h include from hist.h hist.h isn't related to UI and so remove the UI inclusion. Fix the transitive dependency issues this exposes. Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-annotate.c | 8 ++++++++ tools/perf/builtin-c2c.c | 1 + tools/perf/builtin-kvm.c | 1 + tools/perf/builtin-report.c | 1 + tools/perf/builtin-top.c | 1 + tools/perf/ui/browsers/annotate.c | 1 + tools/perf/ui/browsers/hists.c | 1 + tools/perf/ui/browsers/scripts.c | 1 + tools/perf/ui/libslang.h | 2 ++ tools/perf/util/hist.h | 6 ------ 10 files changed, 17 insertions(+), 6 deletions(-) diff --git a/tools/perf/builtin-annotate.c b/tools/perf/builtin-annotate.c index 1466b5dca5f2..4638e6fdc39b 100644 --- a/tools/perf/builtin-annotate.c +++ b/tools/perf/builtin-annotate.c @@ -24,6 +24,7 @@ #include "arch/common.h" #include "builtin.h" #include "perf.h" +#include "ui/keysyms.h" #include "ui/progress.h" #include "ui/ui.h" #include "util/annotate-data.h" @@ -46,6 +47,13 @@ #include "util/tool.h" #include "util/util.h" +#ifndef HAVE_SLANG_SUPPORT +#undef K_LEFT +#define K_LEFT -1000 +#undef K_RIGHT +#define K_RIGHT -2000 +#endif + struct perf_annotate { struct perf_tool tool; struct perf_session *session; diff --git a/tools/perf/builtin-c2c.c b/tools/perf/builtin-c2c.c index 715b75d42f2a..6b64c0d6b24f 100644 --- a/tools/perf/builtin-c2c.c +++ b/tools/perf/builtin-c2c.c @@ -49,6 +49,7 @@ #include "thread.h" #include "tool.h" #include "ui/browsers/hists.h" +#include "ui/keysyms.h" #include "ui/progress.h" #include "ui/ui.h" #include "util/annotate.h" diff --git a/tools/perf/builtin-kvm.c b/tools/perf/builtin-kvm.c index b9f6cb71c0a0..c5e82c078848 100644 --- a/tools/perf/builtin-kvm.c +++ b/tools/perf/builtin-kvm.c @@ -27,6 +27,7 @@ #include "util/kvm-stat.h" #include "util/util.h" #include "ui/browsers/hists.h" +#include "ui/keysyms.h" #include "ui/progress.h" #include "ui/ui.h" #include "util/string2.h" diff --git a/tools/perf/builtin-report.c b/tools/perf/builtin-report.c index b55c01bb45b4..b280ff9ff45e 100644 --- a/tools/perf/builtin-report.c +++ b/tools/perf/builtin-report.c @@ -51,6 +51,7 @@ #include "util/units.h" #include "util/unwind.h" #include "util/util.h" // perf_tip() +#include "ui/keysyms.h" #include "ui/ui.h" #include "ui/progress.h" #include "util/block-info.h" diff --git a/tools/perf/builtin-top.c b/tools/perf/builtin-top.c index 2888ac3b7b92..c2562d49be46 100644 --- a/tools/perf/builtin-top.c +++ b/tools/perf/builtin-top.c @@ -51,6 +51,7 @@ #include "util/intlist.h" #include "util/parse-branch-options.h" #include "arch/common.h" +#include "ui/keysyms.h" #include "ui/ui.h" #include "util/debug.h" diff --git a/tools/perf/ui/browsers/annotate.c b/tools/perf/ui/browsers/annotate.c index e8de644c0e9d..cf36b5347f72 100644 --- a/tools/perf/ui/browsers/annotate.c +++ b/tools/perf/ui/browsers/annotate.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 #include "../browser.h" #include "../helpline.h" +#include "../keysyms.h" #include "../ui.h" #include "../../util/annotate.h" #include "../../util/debug.h" diff --git a/tools/perf/ui/browsers/hists.c b/tools/perf/ui/browsers/hists.c index c15874a491b4..f62cb2d534ed 100644 --- a/tools/perf/ui/browsers/hists.c +++ b/tools/perf/ui/browsers/hists.c @@ -35,6 +35,7 @@ #include "../browsers/hists.h" #include "../helpline.h" +#include "../keysyms.h" #include "../util.h" #include "../ui.h" #include "map.h" diff --git a/tools/perf/ui/browsers/scripts.c b/tools/perf/ui/browsers/scripts.c index 1e8c2c2f952d..94cc1f427c96 100644 --- a/tools/perf/ui/browsers/scripts.c +++ b/tools/perf/ui/browsers/scripts.c @@ -7,6 +7,7 @@ #include "../../util/session.h" #include "../../util/symbol.h" #include "../browser.h" +#include "../keysyms.h" #include "../libslang.h" #include "config.h" #include diff --git a/tools/perf/ui/libslang.h b/tools/perf/ui/libslang.h index 6722561e0458..dd6b9f192155 100644 --- a/tools/perf/ui/libslang.h +++ b/tools/perf/ui/libslang.h @@ -15,7 +15,9 @@ #define ENABLE_SLFUTURE_CONST 1 #define ENABLE_SLFUTURE_VOID 1 +#ifdef HAVE_SLANG_SUPPORT #include +#endif #define SL_KEY_UNTAB 0x1000 diff --git a/tools/perf/util/hist.h b/tools/perf/util/hist.h index b830cbe7f95b..b30375a203e7 100644 --- a/tools/perf/util/hist.h +++ b/tools/perf/util/hist.h @@ -714,7 +714,6 @@ struct block_hist { #define NO_ADDR 0 #ifdef HAVE_SLANG_SUPPORT -#include "../ui/keysyms.h" void attr_to_script(char *buf, struct perf_event_attr *attr); int __hist_entry__tui_annotate(struct hist_entry *he, struct map_symbol *ms, @@ -787,11 +786,6 @@ static inline int block_hists_tui_browse(struct block_hist *bh __maybe_unused, { return 0; } - -#define K_LEFT -1000 -#define K_RIGHT -2000 -#define K_SWITCH_INPUT_DATA -3000 -#define K_RELOAD -4000 #endif unsigned int hists__sort_list_width(struct hists *hists); From d9c7e2ede92ce258a5d4a6cbc55f75acd379c97d Mon Sep 17 00:00:00 2001 From: Wang Yan Date: Mon, 24 Aug 2026 10:55:53 +0800 Subject: [PATCH 037/425] perf powerpc-vpadtl: Fix off-by-one in auxtrace_info minimum size check min_sz is set to sizeof(u64) * POWERPC_VPADTL_TYPE, but the code reads auxtrace_info->priv[POWERPC_VPADTL_TYPE], which needs at least POWERPC_VPADTL_TYPE + 1 elements. POWERPC_VPADTL_TYPE is the first enumerator of the priv index enum (0), so min_sz evaluates to 0 and the check validates only the perf_record_auxtrace_info header itself. A PERF_RECORD_AUXTRACE_INFO event carrying a zero-length priv array then passes the size check, and the subsequent priv[POWERPC_VPADTL_TYPE] read runs one u64 past the validated region. This is the same off-by-one fixed for Intel PT by commit c4362d5e1a5e ("perf intel-pt: Fix off-by-one in auxtrace_info minimum size check") and for Intel BTS by commit b9fb8225951c ("perf intel-bts: Fix off-by-one in auxtrace_info minimum size check"). Use sizeof(u64) * (POWERPC_VPADTL_TYPE + 1) so the highest accessed priv index is covered by the minimum-size validation. Fixes: c4bbd4ec2e50a9ed ("perf powerpc: Process auxtrace events and display in 'perf report -D'") Reviewed-by: Adrian Hunter Signed-off-by: Wang Yan Cc: Athira Rajeev Cc: stable@vger.kernel.org Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/powerpc-vpadtl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/util/powerpc-vpadtl.c b/tools/perf/util/powerpc-vpadtl.c index af6783cfdb53..032864e3884a 100644 --- a/tools/perf/util/powerpc-vpadtl.c +++ b/tools/perf/util/powerpc-vpadtl.c @@ -683,7 +683,7 @@ int powerpc_vpadtl_process_auxtrace_info(union perf_event *event, struct perf_session *session) { struct perf_record_auxtrace_info *auxtrace_info = &event->auxtrace_info; - size_t min_sz = sizeof(u64) * POWERPC_VPADTL_TYPE; + size_t min_sz = sizeof(u64) * (POWERPC_VPADTL_TYPE + 1); struct powerpc_vpadtl *vpa; int err; From 79e07ac60869cc95895c5f74424fe5cf32c5dac3 Mon Sep 17 00:00:00 2001 From: Nylon Chen Date: Wed, 2 Sep 2026 02:32:10 -0700 Subject: [PATCH 038/425] perf bench futex: Define SYS_futex on 32-bit arches with 64-bit time_t The kernel does not provide sys_futex() on 32-bit architectures that lack a 32-bit time representation, such as riscv32. Consequently glibc does not define SYS_futex there, only SYS_futex_time64, and perf bench's futex benchmarks fail to build: bench/futex.h: In function 'futex_syscall': bench/futex.h:77:18: error: 'SYS_futex' undeclared (first use in this function) Define SYS_futex as SYS_futex_time64 when only the latter is available. The guard and its comment are taken verbatim from tools/testing/selftests/futex/include/futextest.h, where they were added by commit 04850819c65c ("selftests/futex: Define SYS_futex on 32-bit architectures with 64-bit time_t"), keeping the two futex userspace headers in sync. No timespec conversion helper is needed on top of this. glibc lays out struct timespec on 32-bit architectures with 64-bit time_t as { int64 tv_sec; int32 tv_nsec; 32-bit pad }, which is compatible with struct __kernel_timespec, and every futex_wait()/futex_lock_pi()/ futex_wait_requeue_pi() call site in perf bench passes a NULL timeout anyway, so no timespec ever crosses the syscall boundary here. Note that an earlier and more ambitious attempt at this was commit c1ff12dac465 ("perf bench futex: Add support for 32-bit systems with 64-bit time_t"), reverted by commit ba4026b09d83 ("Revert "perf bench futex: Add support for 32-bit systems with 64-bit time_t"") because it included linux/time_types.h, which is unavailable on older distributions. This change deliberately avoids that dependency and adds only the one guard needed to fix compilation. Reviewed-by: Ian Rogers Signed-off-by: Nylon Chen Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/bench/futex.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/perf/bench/futex.h b/tools/perf/bench/futex.h index fcb72d682cf8..6df8837b365b 100644 --- a/tools/perf/bench/futex.h +++ b/tools/perf/bench/futex.h @@ -14,6 +14,17 @@ #include #include +/* + * SYS_futex is expected from system C library, in glibc some 32-bit + * architectures (e.g. RV32) are using 64-bit time_t, therefore it doesn't have + * SYS_futex defined but just SYS_futex_time64. Define SYS_futex as + * SYS_futex_time64 in this situation to ensure the compilation and the + * compatibility. + */ +#if !defined(SYS_futex) && defined(SYS_futex_time64) +#define SYS_futex SYS_futex_time64 +#endif + struct bench_futex_parameters { bool silent; bool fshared; From 454e049a3f6528a04f96afb4cbef1fcbb49c2d0f Mon Sep 17 00:00:00 2001 From: Thomas Richter Date: Thu, 3 Sep 2026 09:35:53 +0200 Subject: [PATCH 039/425] perf test: perf stat tests fails on s390 The test case 'perf stat tests' fails on s390 z/VM machines. The root cause is the unsupported CPU measurement facility on z/VM. Therefore the events cycles and instructions do not exist and are unsupported. This cause the subtest Hide zero events stat test to fail. Output before: # perf test 116 116: perf stat tests : FAILED! === Test Summary === Passed main tests : 0 Passed subtests : 0 Skipped tests : 0 Failed tests : 1 List of failed tests: 116: perf stat tests # Output after: # ./perf test 116 116: perf stat tests : Ok === Test Summary === Passed main tests : 1 Passed subtests : 0 Skipped tests : 0 Failed tests : 0 # Fixes: 64724095a33fbee8 ("perf stat: Add --hide-zero-events option to suppress zero-count events") Reviewed-by: Aaron Tomlin Reviewed-by: Ian Rogers Signed-off-by: Thomas Richter Cc: Aaron Tomlin Cc: Ian Rogers Cc: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/stat.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/perf/tests/shell/stat.sh b/tools/perf/tests/shell/stat.sh index 298ce454b45a..1f5d4cc05807 100755 --- a/tools/perf/tests/shell/stat.sh +++ b/tools/perf/tests/shell/stat.sh @@ -583,6 +583,11 @@ test_hide_zero_events_stat() { # Check that --metric-only works with --hide-zero-events if ! perf stat --hide-zero-events --metric-only -e instructions,cycles true > "${stat_output}" 2>&1 then + if grep -sq 'No supported events found' "${stat_output}" + then + echo "Hide zero events stat test [Skipped - events not supported]" + return + fi echo "Hide zero events stat test [Failed - metric-only command failed]" err=1 return From 9e8c2fbadfa44d3cadfeaaefae1423259982bed5 Mon Sep 17 00:00:00 2001 From: Liu Jing Date: Thu, 3 Sep 2026 16:13:50 +0800 Subject: [PATCH 040/425] perf python: Fix redundant self-assignment in get_argument_count() In get_argument_count(), the variable code_obj is assigned to itself before being assigned the result of PyObject_GetAttrString(). This is a redundant self-assignment that appears to be a typo. Fix it by removing the redundant self-assignment. Signed-off-by: Liu Jing Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/scripting-engines/trace-event-python.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/util/scripting-engines/trace-event-python.c b/tools/perf/util/scripting-engines/trace-event-python.c index 8f832ae316ca..7e8e3a7df1b1 100644 --- a/tools/perf/util/scripting-engines/trace-event-python.c +++ b/tools/perf/util/scripting-engines/trace-event-python.c @@ -165,7 +165,7 @@ static int get_argument_count(PyObject *handler) { int arg_count = 0; - PyObject *code_obj = code_obj = PyObject_GetAttrString(handler, "__code__"); + PyObject *code_obj = PyObject_GetAttrString(handler, "__code__"); PyErr_Clear(); if (code_obj) { PyObject *arg_count_obj = PyObject_GetAttrString(code_obj, From 87923c8f4a0e7c49d13e684eb523fff273080b9f Mon Sep 17 00:00:00 2001 From: Namhyung Kim Date: Sun, 30 Aug 2026 23:39:38 -0700 Subject: [PATCH 041/425] perf ftrace latency: Do not read trace files when BPF is used I've realized that it didn't set up the tracing files when BPF is used so poll() just returns immediately. It ends up with calling poll() unnecessarily in a loop. BPF still needs the loop to wait for the target process exiting or a signal from users. Let's use a semaphore instead. Suggested-by: Ian Rogers Reviewed-by: Ian Rogers Signed-off-by: Namhyung Kim Cc: Masami Hiramatsu Cc: Steven Rostedt (VMware) Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-ftrace.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/perf/builtin-ftrace.c b/tools/perf/builtin-ftrace.c index 4f881a40c311..6017493ff179 100644 --- a/tools/perf/builtin-ftrace.c +++ b/tools/perf/builtin-ftrace.c @@ -6,23 +6,23 @@ * Copyright (c) 2020 Changbin Du , significant enhancement. */ -#include "builtin.h" - +#include #include -#include -#include -#include #include #include #include #include -#include +#include +#include +#include +#include #include #include #include #include #include +#include "builtin.h" #include "debug.h" #include #include @@ -45,6 +45,7 @@ static volatile sig_atomic_t workload_exec_errno; static volatile sig_atomic_t done; +static sem_t sig_sem; static struct stats latency_stats; /* for tracepoints */ @@ -53,6 +54,7 @@ static char tracing_instance[PATH_MAX]; /* Trace instance directory */ static void sig_handler(int sig __maybe_unused) { done = true; + sem_post(&sig_sem); } /* @@ -68,6 +70,7 @@ static void ftrace__workload_exec_failed_signal(int signo __maybe_unused, { workload_exec_errno = info->si_value.sival_int; done = true; + sem_post(&sig_sem); } static bool check_ftrace_capable(void) @@ -1146,6 +1149,11 @@ static int __cmd_latency(struct perf_ftrace *ftrace) line[0] = '\0'; while (!done) { + if (ftrace->target.use_bpf) { + sem_wait(&sig_sem); + break; + } + if (poll(&pollfd, 1, -1) < 0) break; @@ -1852,6 +1860,8 @@ int cmd_ftrace(int argc, const char **argv) INIT_LIST_HEAD(&ftrace.nograph_funcs); INIT_LIST_HEAD(&ftrace.event_pair); + sem_init(&sig_sem, 0, 0); + signal(SIGINT, sig_handler); signal(SIGUSR1, sig_handler); signal(SIGCHLD, sig_handler); @@ -2015,5 +2025,11 @@ int cmd_ftrace(int argc, const char **argv) delete_filter_func(&ftrace.nograph_funcs); delete_filter_func(&ftrace.event_pair); + signal(SIGINT, SIG_DFL); + signal(SIGUSR1, SIG_DFL); + signal(SIGCHLD, SIG_DFL); + signal(SIGPIPE, SIG_DFL); + + sem_destroy(&sig_sem); return ret; } From ee32ee73cc78a5068c571da2a71dd5f34b818058 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Tue, 8 Sep 2026 18:20:53 +0300 Subject: [PATCH 042/425] perf test waiting.sh: Replace timestamp polling with sleep The waiting helpers implement timeouts using: date +%s%1N This relies on GNU coreutils date truncating %N to the specified width, so %1N yields tenths of a second. Rust coreutils (uutils) interprets the width differently and does not truncate the nanoseconds field. Consequently "date +%1N" returns all nine nanosecond digits, causing the elapsed-time calculation to be done in nanoseconds while timeout values remain in tenths of a second. As a result, timeout comparisons succeed immediately and the waiting helpers time out on their first iteration. This causes test_intel_pt.sh to fail on systems using uutils "date". Avoid implementation-specific date formatting entirely. Instead, wait for 100 ms on each iteration and count the timeout down. Besides fixing the portability issue, this removes the busy-waiting behaviour in wait_for_perf_to_start(), which could otherwise consume CPU while waiting for perf record to start. Since the timeout is now based on repeated sleeps, it is only approximate. Update the comments accordingly. Also make is_running() wait for exactly the documented number of tenths by changing its timeout test from -gt to the new logic, and quote tm_out in the modified code. Reviewed-by: Ian Rogers Signed-off-by: Adrian Hunter Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/lib/waiting.sh | 34 +++++++++++++-------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/tools/perf/tests/shell/lib/waiting.sh b/tools/perf/tests/shell/lib/waiting.sh index 3a152892e077..43f4322dbe9e 100644 --- a/tools/perf/tests/shell/lib/waiting.sh +++ b/tools/perf/tests/shell/lib/waiting.sh @@ -1,77 +1,75 @@ #!/bin/bash # SPDX-License-Identifier: GPL-2.0 -tenths=date\ +%s%1N - # Wait for PID $1 to have $2 number of threads started -# Time out after $3 tenths of a second or 5 seconds if $3 is "" +# Time out after approx. $3 tenths of a second or 5 seconds if $3 is "" wait_for_threads() { tm_out=$3 ; [ -n "${tm_out}" ] || tm_out=50 - start_time=$($tenths) while [ -e "/proc/$1/task" ] ; do th_cnt=$(find "/proc/$1/task" -mindepth 1 -maxdepth 1 -printf x | wc -c) if [ "${th_cnt}" -ge "$2" ] ; then return 0 fi - # Wait at most tm_out tenths of a second - if [ $(($($tenths) - start_time)) -ge $tm_out ] ; then + if [ "${tm_out}" -le 0 ] ; then echo "PID $1 does not have $2 threads" return 1 fi + sleep 0.1 + tm_out=$((tm_out - 1)) done return 1 } # Wait for perf record -vvv 2>$2 with PID $1 to start by looking at file $2 # It depends on capturing perf record debug message "perf record has started" -# Time out after $3 tenths of a second or 5 seconds if $3 is "" +# Time out after approx. $3 tenths of a second or 5 seconds if $3 is "" wait_for_perf_to_start() { tm_out=$3 ; [ -n "${tm_out}" ] || tm_out=50 echo "Waiting for \"perf record has started\" message" - start_time=$($tenths) while [ -e "/proc/$1" ] ; do if grep -q "perf record has started" "$2" ; then echo OK break fi - # Wait at most tm_out tenths of a second - if [ $(($($tenths) - start_time)) -ge $tm_out ] ; then + if [ "${tm_out}" -le 0 ] ; then echo "perf recording did not start" return 1 fi + sleep 0.1 + tm_out=$((tm_out - 1)) done return 0 } # Wait for process PID %1 to exit -# Time out after $2 tenths of a second or 5 seconds if $2 is "" +# Time out after approx. $2 tenths of a second or 5 seconds if $2 is "" wait_for_process_to_exit() { tm_out=$2 ; [ -n "${tm_out}" ] || tm_out=50 - start_time=$($tenths) while [ -e "/proc/$1" ] ; do - # Wait at most tm_out tenths of a second - if [ $(($($tenths) - start_time)) -ge $tm_out ] ; then + if [ "${tm_out}" -le 0 ] ; then echo "PID $1 did not exit as expected" return 1 fi + sleep 0.1 + tm_out=$((tm_out - 1)) done return 0 } -# Check if PID $1 is still running after $2 tenths of a second +# Check if PID $1 is still running after approx. $2 tenths of a second # or 0.3 seconds if $2 is "" is_running() { tm_out=$2 ; [ -n "${tm_out}" ] || tm_out=3 - start_time=$($tenths) while [ -e "/proc/$1" ] ; do - # Check for at least tm_out tenths of a second - if [ $(($($tenths) - start_time)) -gt $tm_out ] ; then + if [ "${tm_out}" -le 0 ] ; then return 0 fi + sleep 0.1 + tm_out=$((tm_out - 1)) done echo "PID $1 exited prematurely" return 1 From 15917a3a1ce0695f824892c8f9d0dc51408ee580 Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Sat, 5 Sep 2026 18:54:11 -0400 Subject: [PATCH 043/425] perf thread: Fix live-session detection in thread__e_machine() When the per-thread e_machine cannot be determined from the DSOs in the thread's maps, thread__e_machine_endian() decides between reading /proc//exe and falling back to the recorded session environment: bool is_live = machine->machines == NULL; if (!is_live) { /* Check if the session has a data file. */ struct perf_session *session = container_of(...); is_live = !!session->data; } Neither half of that works. The back pointer added by commit a088031c4998 ("perf tools: Add machine to machines back pointer") is set by machines__add(), which only ever adds guests; the host machine never gets one. Host-machine threads, which is to say almost all of them, therefore see machine->machines == NULL and are declared live before the session is consulted at all. The session test is also inverted. A session with a perf_data attached is one being read from a perf.data file, i.e. exactly the case that is not live, while a live session such as 'perf top' passes data=NULL to __perf_session__new(). So a file-based session takes the live path and reads /proc//exe on the analysing host, which at best describes an unrelated process that has since been given the recorded pid, and normally just fails, leaving e_machine as EM_NONE. The perf_env fallback that would have supplied the recorded architecture is never reached, and thread__e_machine() returns EM_HOST. For a same-architecture recording this is invisible, since EM_HOST is the right answer anyway. Cross-architecture it is not: annotating an Alpha perf.data on an x86_64 host selects the x86 struct arch, so the Alpha disassembly is matched against the x86 instruction table. Alpha's 'ret' collides with x86's and gets ret_ops, while its calls and branches match nothing and are left unparsed, so no call target is resolved and no jump arrows are drawn. Set the back pointer for the host machine and correct the session test. The new back pointer does not disturb the other reader of the field, machine__findnew_guest_code(), which machine__resolve() only calls when !machine__is_host(machine). Fixes: 70351029b55677eb ("perf thread: Add support for reading the e_machine type for a thread") Reviewed-by: Ian Rogers Signed-off-by: Matt Turner Cc: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/machine.c | 1 + tools/perf/util/thread.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/perf/util/machine.c b/tools/perf/util/machine.c index f86b3b7df742..a1288fbed833 100644 --- a/tools/perf/util/machine.c +++ b/tools/perf/util/machine.c @@ -237,6 +237,7 @@ int machines__init(struct machines *machines) { int err = machine__init(&machines->host, "", HOST_KERNEL_ID); + machines->host.machines = machines; machines->guests = RB_ROOT_CACHED; return err; } diff --git a/tools/perf/util/thread.c b/tools/perf/util/thread.c index e483ffcb5d93..f0d3773d87db 100644 --- a/tools/perf/util/thread.c +++ b/tools/perf/util/thread.c @@ -536,7 +536,7 @@ uint16_t thread__e_machine_endian(struct thread *thread, struct machine *machine struct perf_session, machines); - is_live = !!session->data; + is_live = !session->data; } /* Read from /proc/pid/exe if live. */ if (is_live) { From c749aacf7378973d6cd65bcbf21361079918ec0a Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Sat, 5 Sep 2026 18:54:12 -0400 Subject: [PATCH 044/425] perf annotate: add Alpha instruction support Teach perf annotate about the Alpha control-transfer instructions, so that an Alpha perf.data gets call and jump arrows and resolved call targets, whether it is read on Alpha or on another host. Add tools/perf/util/annotate-arch/annotate-alpha.c with arch__new_alpha() and an associate_instruction_ops() that classifies: call: bsr, plus jsr and jcr as indirect calls ret: ret jump: br, the conditional branches beq/bne/blt/ble/bgt/bge/blbc/blbs and fbeq/fbne/fblt/fble/fbgt/fbge, plus jmp as an indirect jump mov: mov, fmov (objdump pseudos) That is every mnemonic binutils can print for the branch and JSR formats. jcr rather than jsr_coroutine, because both name the same MBR(0x1a,3) encoding and print_insn_alpha() takes the first match in the table, where the jcr alias has come first since the sources were imported in 1999. bsr needs an Alpha-specific parse routine. The generic call__parse() expects the operand string to begin with the target address, but a bsr prints its return-address register first: bsr t0,fffffc0001031dc0 strtoull() then stops on the leading register name, leaving the target address as 0, which makes call__scnprintf() fall back to printing the raw operands and leaves target.sym unresolved so the browser cannot follow the call. alpha_call__parse() takes the address from after the comma instead, as s390_call__parse() does for the same reason. The PC-relative branches need no such handling, as jump__parse() already skips up to two operands. jsr and jmp get ins_ops that resolve no target at all. They transfer control to a register, and their trailing operand is only a branch prediction hint: jsr ra,(t12),fffffc0001014ee8 <_printk> binutils extracts that hint as a 14-bit signed field scaled by four and prints it relative to the next instruction (extract_jhint() in alpha-opc.c, print_insn_alpha() in alpha-dis.c), so it can name the callee only when the callee lies within the resulting +-32KB. It also defaults to zero, which prints as the next instruction. Of the 213750 jsr in a vmlinux built from this tree, only 23093 hints land on a symbol; 157204 point into the middle of an unrelated function and 33453 are that default. Parsing the hint would therefore invent a call target for the majority of calls, so these keep their operands, as an indirect call does elsewhere. EM_ALPHA is 0x9026, far too large to index the e_machine-keyed arch_new_fn[] table in arch__find(), so select arch__new_alpha explicitly before the table lookup. Declare it in disasm.h and add the object to the annotate-arch Build. Disassembly itself comes from objdump/binutils, which already supports Alpha; this provides perf's instruction-class metadata for annotation. Tested on an EV7 Marvel, both natively and by annotating its perf.data on an x86_64 host, over bsr to a local function, jsr through the PLT and kernel-mode jsr; the two hosts produce identical output. Reviewed-by: Ian Rogers Signed-off-by: Matt Turner Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/annotate-arch/Build | 1 + .../perf/util/annotate-arch/annotate-alpha.c | 185 ++++++++++++++++++ tools/perf/util/disasm.c | 15 +- tools/perf/util/disasm.h | 1 + 4 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 tools/perf/util/annotate-arch/annotate-alpha.c diff --git a/tools/perf/util/annotate-arch/Build b/tools/perf/util/annotate-arch/Build index 23316743fdc5..0a74e1a9f17b 100644 --- a/tools/perf/util/annotate-arch/Build +++ b/tools/perf/util/annotate-arch/Build @@ -1,3 +1,4 @@ +perf-util-y += annotate-alpha.o perf-util-y += annotate-arc.o perf-util-y += annotate-arm.o perf-util-y += annotate-arm64.o diff --git a/tools/perf/util/annotate-arch/annotate-alpha.c b/tools/perf/util/annotate-arch/annotate-alpha.c new file mode 100644 index 000000000000..ccb13e2338a2 --- /dev/null +++ b/tools/perf/util/annotate-arch/annotate-alpha.c @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include +#include +#include "../../util/disasm.h" +#include "../../util/map.h" +#include "../../util/maps.h" +#include "../../util/symbol.h" +#include "../../util/thread.h" + +/* + * Alpha control-transfer instructions, as printed by objdump: + * + * PC-relative (opcode group 0x30-0x3f), 21-bit displacement: + * br, bsr unconditional / to-subroutine + * beq bne blt ble bgt bge blbc blbs integer conditional + * fbeq fbne fblt fble fbgt fbge floating conditional + * + * Register-indirect (JSR group, opcode 0x1a): + * jmp, jsr, ret, jcr + * + * bsr/jsr (and jcr, the coroutine form, which binutils prints in preference to + * the jsr_coroutine spelling) save a return address, so they are calls; ret + * returns; everything else that transfers control is a jump. + * + * Alpha has no machine "mov"; objdump prints "mov"/"fmov" as pseudos for + * bis/cpys, so map them to mov_ops when present. The no-ops are deliberately + * left alone: nop_ops would let delete_last_nop() trim the padding gcc leaves + * at the end of a function, but its scnprintf() prints the literal "nop", and + * Alpha pads with unop (ldq_u $31) rather than nop. + */ + +/* + * The generic call__parse() expects the target address to be the first thing + * in the operand string, but a bsr prints its return-address register first: + * + * bsr t0,fffffc0001031dc0 + * + * so take the address from after the comma. Without this the address comes + * out as 0, and neither the callee symbol nor the annotation browser's + * "go to target" work. + */ +static int alpha_call__parse(const struct arch *arch, struct ins_operands *ops, + struct map_symbol *ms, + struct disasm_line *dl __maybe_unused) +{ + char *endptr, *tok, *name; + struct map *map = ms->map; + struct addr_map_symbol target; + + tok = strchr(ops->raw, ','); + if (tok == NULL) + return -1; + + ops->target.addr = strtoull(tok + 1, &endptr, 16); + if (endptr == tok + 1) + return -1; + + /* A stripped object has no "" to name the target with. */ + name = strchr(endptr, '<'); + if (name == NULL) + goto find_target; + + name++; + + if (arch->objdump.skip_functions_char && + strchr(name, arch->objdump.skip_functions_char)) + return -1; + + tok = strchr(name, '>'); + if (tok == NULL) + return -1; + + *tok = '\0'; + ops->target.name = strdup(name); + *tok = '>'; + + if (ops->target.name == NULL) + return -1; + +find_target: + target = (struct addr_map_symbol) { + .ms = { .map = map__get(map), }, + .addr = map__objdump_2mem(map, ops->target.addr), + }; + + if (maps__find_ams(thread__maps(ms->thread), &target) == 0 && + map__rip_2objdump(target.ms.map, + map__map_ip(target.ms.map, target.addr)) == ops->target.addr) + ops->target.sym = target.ms.sym; + + addr_map_symbol__exit(&target); + return 0; +} + +static const struct ins_ops alpha_call_ops = { + .parse = alpha_call__parse, + .scnprintf = call__scnprintf, + .is_call = true, +}; + +/* + * jsr and jmp transfer control to a register, and their trailing operand is + * only a branch prediction hint: + * + * jsr ra,(t12),fffffc0001014ee8 <_printk> + * + * binutils extracts that hint as a 14-bit signed field scaled by four and + * prints it relative to the next instruction (extract_jhint() in alpha-opc.c, + * print_insn_alpha() in alpha-dis.c), so it can name the callee only when the + * callee lies within the resulting +-32KB. It also defaults to zero, which + * prints as the next instruction. Most hints are therefore not the callee at + * all, and parsing one would invent a call target, so these resolve no target + * and keep their operands, as an indirect call does elsewhere. The hint on + * jcr is not even an address. + */ +static const struct ins_ops alpha_indirect_call_ops = { + .scnprintf = ins__raw_scnprintf, + .is_call = true, +}; + +static const struct ins_ops alpha_indirect_jump_ops = { + .scnprintf = ins__raw_scnprintf, + .is_jump = true, +}; + +static int is_alpha_cond_branch(const char *name) +{ + static const char *const branches[] = { + "beq", "bne", "blt", "ble", "bgt", "bge", "blbc", "blbs", + "fbeq", "fbne", "fblt", "fble", "fbgt", "fbge", + }; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(branches); i++) { + if (!strcmp(name, branches[i])) + return 1; + } + return 0; +} + +static const struct ins_ops *alpha__associate_instruction_ops(struct arch *arch, const char *name) +{ + const struct ins_ops *ops = NULL; + + if (!strcmp(name, "bsr")) { + ops = &alpha_call_ops; + } else if (!strcmp(name, "jsr") || + !strcmp(name, "jcr")) { + ops = &alpha_indirect_call_ops; + } else if (!strcmp(name, "ret")) { + ops = &ret_ops; + } else if (!strcmp(name, "jmp")) { + ops = &alpha_indirect_jump_ops; + } else if (!strcmp(name, "br") || + is_alpha_cond_branch(name)) { + ops = &jump_ops; + } else if (!strcmp(name, "mov") || + !strcmp(name, "fmov")) { + ops = &mov_ops; + } + + if (ops) + arch__associate_ins_ops(arch, name, ops); + + return ops; +} + +const struct arch *arch__new_alpha(const struct e_machine_and_e_flags *id, + const char *cpuid __maybe_unused) +{ + struct arch *arch = zalloc(sizeof(*arch)); + + if (!arch) + return NULL; + + arch->name = "alpha"; + arch->id = *id; + arch->associate_instruction_ops = alpha__associate_instruction_ops; + /* objdump emits no comments for Alpha; '#' is what the assembler uses. */ + arch->objdump.comment_char = '#'; + return arch; +} diff --git a/tools/perf/util/disasm.c b/tools/perf/util/disasm.c index 6cfdbabbb8c7..49c206a88eec 100644 --- a/tools/perf/util/disasm.c +++ b/tools/perf/util/disasm.c @@ -161,6 +161,8 @@ const struct arch *arch__find(uint16_t e_machine, uint32_t e_flags, const char * .e_flags = e_flags, }; const struct arch *result = NULL, **tmp; + const struct arch *(*new_fn)(const struct e_machine_and_e_flags *id, + const char *cpuid) = NULL; if (num_archs > 0) { tmp = bsearch(&key, archs, num_archs, sizeof(*archs), arch__key_cmp); @@ -171,7 +173,16 @@ const struct arch *arch__find(uint16_t e_machine, uint32_t e_flags, const char * if (result) return result; - if (e_machine >= ARRAY_SIZE(arch_new_fn) || arch_new_fn[e_machine] == NULL) { + /* + * EM_ALPHA (0x9026) is far too large to index arch_new_fn[], so it is + * selected explicitly; everything else uses the e_machine-indexed table. + */ + if (e_machine == EM_ALPHA) + new_fn = arch__new_alpha; + else if (e_machine < ARRAY_SIZE(arch_new_fn)) + new_fn = arch_new_fn[e_machine]; + + if (new_fn == NULL) { errno = ENOTSUP; return NULL; } @@ -182,7 +193,7 @@ const struct arch *arch__find(uint16_t e_machine, uint32_t e_flags, const char * archs = tmp; - result = arch_new_fn[e_machine](&key, cpuid); + result = new_fn(&key, cpuid); if (!result) { pr_err("%s: failed to initialize %u arch priv area\n", __func__, e_machine); diff --git a/tools/perf/util/disasm.h b/tools/perf/util/disasm.h index 25756e3f47e4..adbdbf8f1f35 100644 --- a/tools/perf/util/disasm.h +++ b/tools/perf/util/disasm.h @@ -122,6 +122,7 @@ extern const struct ins_ops ret_ops; int arch__associate_ins_ops(struct arch *arch, const char *name, const struct ins_ops *ops); +const struct arch *arch__new_alpha(const struct e_machine_and_e_flags *id, const char *cpuid); const struct arch *arch__new_arc(const struct e_machine_and_e_flags *id, const char *cpuid); const struct arch *arch__new_arm(const struct e_machine_and_e_flags *id, const char *cpuid); const struct arch *arch__new_arm64(const struct e_machine_and_e_flags *id, const char *cpuid); From 4bbcb0fb9b866d05a2dd2db8bccd78be9d0476a9 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 4 Sep 2026 12:49:54 -0700 Subject: [PATCH 045/425] perf test: Avoid call to missing cleanup function Cleanup function was removed by writing to /dev/null rather than temporary files. The exit path still had a call. Fixes: 44f6b44027910eb9 ("perf test: Fix hybrid testing of event fallback test") Reviewed-by: Dapeng Mi Signed-off-by: Ian Rogers Cc: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/test_event_open_fallback.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/perf/tests/shell/test_event_open_fallback.sh b/tools/perf/tests/shell/test_event_open_fallback.sh index 9420a7557c13..27d440a5355d 100755 --- a/tools/perf/tests/shell/test_event_open_fallback.sh +++ b/tools/perf/tests/shell/test_event_open_fallback.sh @@ -57,8 +57,6 @@ ret=0 test_decrease_precise_ip || ret=$? ; count_result $ret ; ret=0 test_decrease_precise_ip_complicated || ret=$? ; count_result $ret ; ret=0 -cleanup - if [ ${err_cnt} -gt 0 ] ; then exit 1 fi From 024b219744a949419b3c9e8aa23031c04428d9d6 Mon Sep 17 00:00:00 2001 From: Aaron Tomlin Date: Sat, 5 Sep 2026 21:11:32 -0400 Subject: [PATCH 046/425] perf trace: Add upper bound checks for augmented BTF struct printing When pretty-printing augmented struct payloads using BTF via btf_struct_scnprintf(), augmented_arg->size is currently only validated against values <= 0. However, several edge cases can result in size mismatches or buffer over-reads: 1. If arg->augmented.size is smaller than sizeof(*augmented_arg), dereferencing augmented_arg->size reads past the available buffer. 2. If augmented_arg->size exceeds arg->augmented.size - sizeof(*augmented_arg), calculating consumed = sizeof(*augmented_arg) + augmented_arg->size can overflow signed integer limits (e.g., with crafted INT_MAX values in an untrusted perf.data file) or cause arg->augmented.size to underflow. This advances arg->augmented.args out of bounds, corrupting the parsing state for subsequent arguments in multi-argument syscalls. 3. If the captured payload is truncated (e.g., short reads in BPF, or during cross-architecture analysis such as replaying a 32-bit perf.data on a 64-bit host where host BTF type->size exceeds the 32-bit target payload), passing type->size to btf_dump__dump_type_data() causes libbpf to read past the end of the payload buffer. Enforce an upper bound on augmented_arg->size against the remaining buffer (arg->augmented.size - sizeof(*augmented_arg)) and verify that the captured payload contains at least type->size bytes before passing it to btf_dump__dump_type_data(). Fixes: cb32035214b9a09d ("perf trace: Pretty print struct data") Reported-by: sashiko-bot Signed-off-by: Aaron Tomlin Cc: Howard Chu Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-trace.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index be19d70eba09..20fffc24507b 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -1133,12 +1133,13 @@ static size_t btf_struct_scnprintf(const struct btf_type *type, struct btf *btf, LIBBPF_OPTS(btf_dump_opts, dump_opts); LIBBPF_OPTS(btf_dump_type_data_opts, dump_data_opts); - if (arg == NULL || arg->augmented.args == NULL || arg->augmented.size <= 0 || + if (arg == NULL || arg->augmented.args == NULL || arg->augmented.size < (int)sizeof(*augmented_arg) || arg->fmt == NULL || !arg->fmt->from_user) return 0; augmented_arg = arg->augmented.args; - if (augmented_arg->size <= 0) + if (augmented_arg->size <= 0 || augmented_arg->size > arg->augmented.size - (int)sizeof(*augmented_arg) || + (size_t)augmented_arg->size < type->size) return 0; dump_data_opts.compact = true; From aa18964dd64511305de0711fed912054da6f5d18 Mon Sep 17 00:00:00 2001 From: Triet Hoang Date: Tue, 18 Aug 2026 14:48:37 +0000 Subject: [PATCH 047/425] perf unwind: Handle allocation failure in libdw__get_entries() Check the return value of zalloc() before dereferencing the allocated dwfl_ui_ti structure. Return -ENOMEM when the allocation fails to avoid a NULL pointer dereference. Signed-off-by: Triet Hoang Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/unwind-libdw.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tools/perf/util/unwind-libdw.c b/tools/perf/util/unwind-libdw.c index 63a5c2253174..8d9fa6fd81c3 100644 --- a/tools/perf/util/unwind-libdw.c +++ b/tools/perf/util/unwind-libdw.c @@ -397,6 +397,11 @@ int libdw__get_entries(unwind_entry_cb_t cb, void *arg, dwfl = dwfl_ui_ti->dwfl; } else { dwfl_ui_ti = zalloc(sizeof(*dwfl_ui_ti)); + if (!dwfl_ui_ti) { + free(ui); + return -ENOMEM; + } + dwfl = dwfl_begin(&offline_callbacks); if (!dwfl) goto out; From 93fd7adf1a47c61ef81b33c8fdd9cb4e4ea0bdcb Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 8 Sep 2026 15:07:20 -0700 Subject: [PATCH 048/425] perf stat: Avoid uninitialized memory read on error If has_supported_counters fails then msg was passed to ui__error regardless of whether an error was written into msg leading to reading uninitialized memory. Avoid this by always terminating msg at initialization. Opportunistically reduce the scope of msg to make its use clearer. Fixes: c9a8c343ef2f ("perf stat: When no events, don't report an error if there is none") Signed-off-by: Ian Rogers Link: https://lore.kernel.org/r/20260908220720.564614-1-irogers@google.com Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-stat.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/perf/builtin-stat.c b/tools/perf/builtin-stat.c index c50cf230f16e..37752d6bfef4 100644 --- a/tools/perf/builtin-stat.c +++ b/tools/perf/builtin-stat.c @@ -777,7 +777,6 @@ static int __run_perf_stat(int argc, const char **argv, int run_idx) int interval = stat_config.interval; int times = stat_config.times; int timeout = stat_config.timeout; - char msg[BUFSIZ]; unsigned long long t0, t1; struct evsel *counter; size_t l; @@ -908,6 +907,9 @@ static int __run_perf_stat(int argc, const char **argv, int run_idx) } } if (!has_supported_counters && !stat_config.null_run) { + char msg[BUFSIZ]; + + msg[0] = '\0'; if (open_err) { evsel__open_strerror(evlist__first(evsel_list), &target, open_err, msg, sizeof(msg)); From b245a18823027c372b4aebb866129368c2ecd3f5 Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Tue, 8 Sep 2026 22:50:34 -0400 Subject: [PATCH 049/425] tools: Port perf ui from GTK 2 to GTK 4 Port straight to GTK 4 rather than GTK 3, since GTK 4 is where new development happens and GTK 3 is old itself now. GTK 4 drops GtkContainer, GdkScreen, and the gtk_main()/ gtk_dialog_run() family perf's GTK UI relied on. Containers get per-widget setters (gtk_box_append() and friends), monitor geometry comes from GdkMonitor instead of GdkScreen, and the main and error-dialog loops become explicit GMainLoops quit from the "close-request" and "response" signals. Widgets are visible by default now, so gtk_widget_show_all()/set_no_show_all() go away, and the remaining gtk_widget_show()/gtk_widget_hide() calls become gtk_widget_set_visible() (with a small wrapper where "response" needs to pass gtk_widget_hide() as a callback, since it no longer exists as a plain function). gtk_ui_progress__finish() skips destroying a progress dialog that was never created, since gtk_window_destroy() asserts on NULL where the old widget destroy tolerated it. Two spots the GTK 2 to GTK 3 port had missed (builtin-annotate.c, ui/gtk/setup.c still using HAVE_GTK2_SUPPORT and gtk_main_quit()) are fixed to match. Runtime fallout from the new signal-driven loops: the error dialog's nested loop hung if the parent window closed (GTK_DIALOG_DESTROY_WITH_PARENT destroys without emitting "response"); gtk_info_bar_get_content_area() is gone, breaking GTK_INFO_BAR_SUPPORT; the progress dialog's static widget pointers dangled after a manual close; perf_gtk__error() and the warning functions reused an exhausted va_list when vasprintf() failed. The error loop is tracked in a list instead of a single pointer, since perf_gtk__error() can be called re-entrantly (the dialog isn't modal) and a lone global leaked the outer loop when that happened. The list is only ever touched from the main thread: perf_gtk__error() updates it while handling a dialog, and SIGINT/SIGQUIT/SIGTERM are deferred to a GLib source via g_unix_signal_add() rather than calling perf_gtk__exit() straight out of a real signal handler, so quitting on those signals is serialized with the list update instead of racing it from signal-handler context. SIGSEGV/SIGFPE keep a real handler, since they're synchronous faults with no "later" to defer to, but it's pared down to reporting and reraising the default disposition (perf_gtk__fatal_signal()): there's no safe way to run GTK/GLib code from the faulting context. stdarg.h, stdio.h, and string.h are now included explicitly where used (util.c, hists.c, annotate.c) rather than relying on transitive includes, which musl doesn't guarantee. The gtk4-infobar feature check is dropped: GtkInfoBar has existed unconditionally since GTK 3.10, so the check can only ever pass, and it was failing outright here anyway since gtk_info_bar_new() is deprecated and the check treats deprecation warnings as errors. HAVE_GTK_INFO_BAR_SUPPORT and its statusbar-only fallback go away; the info bar is now built unconditionally. Signed-off-by: Matt Turner Link: https://lore.kernel.org/r/20260908-perf-gtk2-v8-1-e90d5d155f0d@gmail.com [ Fixed up some patch fuzz ] [ Removed gtk2 from FEATURES_DISPLAY, it is opt-in use 'make VF=1' to see if it was detected ] Signed-off-by: Arnaldo Carvalho de Melo --- tools/build/Makefile.feature | 3 +- tools/build/feature/Makefile | 10 +- tools/build/feature/test-gtk2-infobar.c | 12 --- .../feature/{test-gtk2.c => test-gtk4.c} | 4 +- tools/perf/Documentation/perf-report.txt | 2 +- tools/perf/Makefile | 2 +- tools/perf/Makefile.config | 22 ++--- tools/perf/Makefile.perf | 6 +- tools/perf/builtin-annotate.c | 8 +- tools/perf/builtin-report.c | 8 +- tools/perf/scripts/install-build-deps.sh | 4 +- tools/perf/tests/make | 4 +- tools/perf/ui/gtk/annotate.c | 36 +++---- tools/perf/ui/gtk/browser.c | 96 ++++++++++++++---- tools/perf/ui/gtk/gtk.h | 16 +-- tools/perf/ui/gtk/hists.c | 43 ++++---- tools/perf/ui/gtk/progress.c | 40 +++++--- tools/perf/ui/gtk/setup.c | 5 +- tools/perf/ui/gtk/util.c | 97 +++++++++++-------- tools/perf/ui/setup.c | 2 +- 20 files changed, 238 insertions(+), 182 deletions(-) delete mode 100644 tools/build/feature/test-gtk2-infobar.c rename tools/build/feature/{test-gtk2.c => test-gtk4.c} (76%) diff --git a/tools/build/Makefile.feature b/tools/build/Makefile.feature index 99eb0ea09537..331f5cdfc34b 100644 --- a/tools/build/Makefile.feature +++ b/tools/build/Makefile.feature @@ -113,8 +113,7 @@ FEATURE_TESTS_EXTRA := \ compile-x32 \ cplus-demangle \ cxa-demangle \ - gtk2 \ - gtk2-infobar \ + gtk4 \ hello \ babeltrace2-ctf-writer \ libcapstone \ diff --git a/tools/build/feature/Makefile b/tools/build/feature/Makefile index 7d165018116a..01c48e6ef021 100644 --- a/tools/build/feature/Makefile +++ b/tools/build/feature/Makefile @@ -11,8 +11,7 @@ FILES= \ test-eventfd.bin \ test-fortify-source.bin \ test-glibc.bin \ - test-gtk2.bin \ - test-gtk2-infobar.bin \ + test-gtk4.bin \ test-hello.bin \ test-libbfd.bin \ test-libbfd-threadsafe.bin \ @@ -240,11 +239,8 @@ $(OUTPUT)test-libcpupower.bin: $(OUTPUT)test-libtracefs.bin: $(BUILD) $(shell $(PKG_CONFIG) --cflags libtracefs 2>/dev/null) -ltracefs -$(OUTPUT)test-gtk2.bin: - $(BUILD) $(shell $(PKG_CONFIG) --libs --cflags gtk+-2.0 2>/dev/null) -Wno-deprecated-declarations - -$(OUTPUT)test-gtk2-infobar.bin: - $(BUILD) $(shell $(PKG_CONFIG) --libs --cflags gtk+-2.0 2>/dev/null) +$(OUTPUT)test-gtk4.bin: + $(BUILD) $(shell $(PKG_CONFIG) --libs --cflags gtk4 2>/dev/null) grep-libs = $(filter -l%,$(1)) strip-libs = $(filter-out -l%,$(1)) diff --git a/tools/build/feature/test-gtk2-infobar.c b/tools/build/feature/test-gtk2-infobar.c deleted file mode 100644 index b1b716dd5733..000000000000 --- a/tools/build/feature/test-gtk2-infobar.c +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -#pragma GCC diagnostic ignored "-Wstrict-prototypes" -#include -#pragma GCC diagnostic error "-Wstrict-prototypes" - -int main(int argc, char *argv[]) -{ - gtk_init(&argc, &argv); - gtk_info_bar_new(); - - return 0; -} diff --git a/tools/build/feature/test-gtk2.c b/tools/build/feature/test-gtk4.c similarity index 76% rename from tools/build/feature/test-gtk2.c rename to tools/build/feature/test-gtk4.c index 2aaf4bfc2055..b9520e7408b9 100644 --- a/tools/build/feature/test-gtk2.c +++ b/tools/build/feature/test-gtk4.c @@ -3,9 +3,9 @@ #include #pragma GCC diagnostic error "-Wstrict-prototypes" -int main(int argc, char *argv[]) +int main(void) { - gtk_init(&argc, &argv); + gtk_init(); return 0; } diff --git a/tools/perf/Documentation/perf-report.txt b/tools/perf/Documentation/perf-report.txt index ae68ca402d0b..1a4706329c6c 100644 --- a/tools/perf/Documentation/perf-report.txt +++ b/tools/perf/Documentation/perf-report.txt @@ -354,7 +354,7 @@ OPTIONS requires a tty, if one is not present, as when piping to other commands, the stdio interface is used. ---gtk:: Use the GTK2 interface. +--gtk:: Use the GTK4 interface. --weights:: Show or hide (with `--no-weights`) weight columns in annotation output. By default, weight columns are shown when samples contain non-zero weights. diff --git a/tools/perf/Makefile b/tools/perf/Makefile index 5b713837eede..56014106479a 100644 --- a/tools/perf/Makefile +++ b/tools/perf/Makefile @@ -106,7 +106,7 @@ clean: # make -C tools/perf -f tests/make # build-test: - @$(MAKE) SHUF=1 -f tests/make REUSE_FEATURES_DUMP=1 MK=Makefile SET_PARALLEL=1 --no-print-directory tarpkg make_static make_with_gtk2 out + @$(MAKE) SHUF=1 -f tests/make REUSE_FEATURES_DUMP=1 MK=Makefile SET_PARALLEL=1 --no-print-directory tarpkg make_static make_with_gtk4 out build-test-tarball: @$(MAKE) -f tests/make REUSE_FEATURES_DUMP=1 MK=Makefile SET_PARALLEL=1 --no-print-directory out diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config index 4d5993da9f94..3e59e2b7eaec 100644 --- a/tools/perf/Makefile.config +++ b/tools/perf/Makefile.config @@ -774,20 +774,16 @@ ifndef NO_SLANG endif endif -ifdef GTK2 - FLAGS_GTK2=$(CFLAGS) $(LDFLAGS) $(EXTLIBS) $(shell $(PKG_CONFIG) --libs --cflags gtk+-2.0 2>/dev/null) - $(call feature_check,gtk2) - ifneq ($(feature-gtk2), 1) - $(warning GTK2 not found, disables GTK2 support. Please install gtk2-devel or libgtk2.0-dev) - NO_GTK2 := 1 +ifdef GTK4 + FLAGS_GTK4=$(CFLAGS) $(LDFLAGS) $(EXTLIBS) $(shell $(PKG_CONFIG) --libs --cflags gtk4 2>/dev/null) + $(call feature_check,gtk4) + ifneq ($(feature-gtk4), 1) + $(warning GTK4 not found, disables GTK4 support. Please install gtk4-devel or libgtk-4-dev) + NO_GTK4 := 1 else - $(call feature_check,gtk2-infobar) - ifeq ($(feature-gtk2-infobar), 1) - GTK_CFLAGS := -DHAVE_GTK_INFO_BAR_SUPPORT - endif - CFLAGS += -DHAVE_GTK2_SUPPORT - GTK_CFLAGS += $(shell $(PKG_CONFIG) --cflags gtk+-2.0 2>/dev/null) - GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk+-2.0 2>/dev/null) + CFLAGS += -DHAVE_GTK4_SUPPORT + GTK_CFLAGS += $(shell $(PKG_CONFIG) --cflags gtk4 2>/dev/null) + GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4 2>/dev/null) EXTLIBS += -ldl endif endif diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf index 29cfd44c427f..2438b40eaaec 100644 --- a/tools/perf/Makefile.perf +++ b/tools/perf/Makefile.perf @@ -49,7 +49,7 @@ include ../scripts/utilities.mak # # Define NO_SLANG if you do not want TUI support. # -# Define GTK2 if you want GTK+ GUI support. +# Define GTK4 if you want GTK+ GUI support. # # Define NO_DEMANGLE if you do not want C++ symbol demangling. # @@ -473,7 +473,7 @@ ifneq ($(OUTPUT),) CFLAGS += -I$(OUTPUT) endif -ifdef GTK2 +ifdef GTK4 ALL_PROGRAMS += $(OUTPUT)libperf-gtk.so GTK_IN := $(OUTPUT)gtk-in.o endif @@ -811,7 +811,7 @@ check: prepare ### Installation rules -ifdef GTK2 +ifdef GTK4 install-gtk: $(OUTPUT)libperf-gtk.so $(call QUIET_INSTALL, 'GTK UI') \ $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(libdir_SQ)'; \ diff --git a/tools/perf/builtin-annotate.c b/tools/perf/builtin-annotate.c index 4638e6fdc39b..850fc72fa75f 100644 --- a/tools/perf/builtin-annotate.c +++ b/tools/perf/builtin-annotate.c @@ -61,7 +61,7 @@ struct perf_annotate { bool use_tui; #endif bool use_stdio, use_stdio2; -#ifdef HAVE_GTK2_SUPPORT +#ifdef HAVE_GTK4_SUPPORT bool use_gtk; #endif bool skip_missing; @@ -721,7 +721,7 @@ int cmd_annotate(int argc, const char **argv) OPT_BOOLEAN('q', "quiet", &quiet, "do now show any warnings or messages"), OPT_BOOLEAN('D', "dump-raw-trace", &dump_trace, "dump raw trace in ASCII"), -#ifdef HAVE_GTK2_SUPPORT +#ifdef HAVE_GTK4_SUPPORT OPT_BOOLEAN(0, "gtk", &annotate.use_gtk, "Use the GTK interface"), #endif #ifdef HAVE_SLANG_SUPPORT @@ -840,7 +840,7 @@ int cmd_annotate(int argc, const char **argv) if (annotate_check_args() < 0) return -EINVAL; -#ifdef HAVE_GTK2_SUPPORT +#ifdef HAVE_GTK4_SUPPORT if (symbol_conf.show_nr_samples && annotate.use_gtk) { pr_err("--show-nr-samples is not available in --gtk mode at this time\n"); return ret; @@ -910,7 +910,7 @@ int cmd_annotate(int argc, const char **argv) else if (annotate.use_tui) use_browser = 1; #endif -#ifdef HAVE_GTK2_SUPPORT +#ifdef HAVE_GTK4_SUPPORT else if (annotate.use_gtk) use_browser = 2; #endif diff --git a/tools/perf/builtin-report.c b/tools/perf/builtin-report.c index b280ff9ff45e..279e61c2366c 100644 --- a/tools/perf/builtin-report.c +++ b/tools/perf/builtin-report.c @@ -83,7 +83,7 @@ struct report { #ifdef HAVE_SLANG_SUPPORT bool use_tui; #endif -#ifdef HAVE_GTK2_SUPPORT +#ifdef HAVE_GTK4_SUPPORT bool use_gtk; #endif bool use_stdio; @@ -1360,8 +1360,8 @@ int cmd_report(int argc, const char **argv) #ifdef HAVE_SLANG_SUPPORT OPT_BOOLEAN(0, "tui", &report.use_tui, "Use the TUI interface"), #endif -#ifdef HAVE_GTK2_SUPPORT - OPT_BOOLEAN(0, "gtk", &report.use_gtk, "Use the GTK2 interface"), +#ifdef HAVE_GTK4_SUPPORT + OPT_BOOLEAN(0, "gtk", &report.use_gtk, "Use the GTK4 interface"), #endif OPT_BOOLEAN(0, "stdio", &report.use_stdio, "Use the stdio interface"), @@ -1714,7 +1714,7 @@ int cmd_report(int argc, const char **argv) else if (report.use_tui) use_browser = 1; #endif -#ifdef HAVE_GTK2_SUPPORT +#ifdef HAVE_GTK4_SUPPORT else if (report.use_gtk) use_browser = 2; #endif diff --git a/tools/perf/scripts/install-build-deps.sh b/tools/perf/scripts/install-build-deps.sh index d003e7fab2be..a601a5260c17 100755 --- a/tools/perf/scripts/install-build-deps.sh +++ b/tools/perf/scripts/install-build-deps.sh @@ -199,8 +199,8 @@ fedora_pkg_for() { # opt-in features, which a default build does not enable: the libbfd # disassembler family (libbfd, libbfd-threadsafe, libbfd-liberty, # disassembler-*, cplus-demangle), only linked on BUILD_NONDISTRO - # builds and deprecated in favor of capstone, GTK2, LIBPERL and - # LIBUNWIND support (ifdef GTK2 / ifdef LIBPERL / LIBUNWIND=1), + # builds and deprecated in favor of capstone, GTK4, LIBPERL and + # LIBUNWIND support (ifdef GTK4 / ifdef LIBPERL / LIBUNWIND=1), # and CoreSight (ifdef CORESIGHT), are deliberately not mapped. # libaio is not mapped either: its # test uses the POSIX AIO API (aio.h, aio_*, -lrt), provided by diff --git a/tools/perf/tests/make b/tools/perf/tests/make index d2c2f526e1db..202ab5501916 100644 --- a/tools/perf/tests/make +++ b/tools/perf/tests/make @@ -95,7 +95,7 @@ make_no_babeltrace2 := NO_BABELTRACE2=1 make_with_coresight := CORESIGHT=1 make_no_sdt := NO_SDT=1 make_no_libpfm4 := NO_LIBPFM4=1 -make_with_gtk2 := GTK2=1 +make_with_gtk4 := GTK4=1 make_refcnt_check := EXTRA_CFLAGS="-DREFCNT_CHECKING=1" make_tags := tags make_cscope := cscope @@ -318,7 +318,7 @@ $(run): $(call test,$@) && \ rm -rf $@ $$TMP_DEST || (cat $@ ; false) -make_with_gtk2: +make_with_gtk4: $(call clean) @TMP_DEST=$$(mktemp -d); \ cmd="cd $(PERF) && $(MAKE_F) $($@) $(PARALLEL_OPT) $(O_OPT) DESTDIR=$$TMP_DEST"; \ diff --git a/tools/perf/ui/gtk/annotate.c b/tools/perf/ui/gtk/annotate.c index 8920e298420a..094dfa045b77 100644 --- a/tools/perf/ui/gtk/annotate.c +++ b/tools/perf/ui/gtk/annotate.c @@ -11,6 +11,7 @@ #include "ui/helpline.h" #include #include +#include enum { ANN_COL__PERCENT, @@ -161,7 +162,7 @@ static int perf_gtk__annotate_symbol(GtkWidget *window, struct map_symbol *ms, gtk_list_store_set(store, &iter, ANN_COL__LINE, s, -1); } - gtk_container_add(GTK_CONTAINER(window), view); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(window), view); list_for_each_entry_safe(pos, n, ¬es->src->source, al.node) { list_del_init(&pos->al.node); @@ -205,40 +206,34 @@ static int symbol__gtk_annotate(struct map_symbol *ms, struct evsel *evsel, GtkWidget *infobar; GtkWidget *statbar; - signal(SIGSEGV, perf_gtk__signal); - signal(SIGFPE, perf_gtk__signal); - signal(SIGINT, perf_gtk__signal); - signal(SIGQUIT, perf_gtk__signal); - signal(SIGTERM, perf_gtk__signal); + signal(SIGSEGV, perf_gtk__fatal_signal); + signal(SIGFPE, perf_gtk__fatal_signal); + perf_gtk__install_quit_signals(); - window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + window = gtk_window_new(); gtk_window_set_title(GTK_WINDOW(window), "perf annotate"); - g_signal_connect(window, "delete_event", gtk_main_quit, NULL); - pgctx = perf_gtk__activate_context(window); if (!pgctx) return -1; - vbox = gtk_vbox_new(FALSE, 0); + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); notebook = gtk_notebook_new(); pgctx->notebook = notebook; - gtk_box_pack_start(GTK_BOX(vbox), notebook, TRUE, TRUE, 0); + gtk_widget_set_vexpand(notebook, TRUE); + gtk_box_append(GTK_BOX(vbox), notebook); infobar = perf_gtk__setup_info_bar(); - if (infobar) { - gtk_box_pack_start(GTK_BOX(vbox), infobar, - FALSE, FALSE, 0); - } + gtk_box_append(GTK_BOX(vbox), infobar); statbar = perf_gtk__setup_statusbar(); - gtk_box_pack_start(GTK_BOX(vbox), statbar, FALSE, FALSE, 0); + gtk_box_append(GTK_BOX(vbox), statbar); - gtk_container_add(GTK_CONTAINER(window), vbox); + gtk_window_set_child(GTK_WINDOW(window), vbox); } - scrolled_window = gtk_scrolled_window_new(NULL, NULL); + scrolled_window = gtk_scrolled_window_new(); tab_label = gtk_label_new(sym->name); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), @@ -267,12 +262,11 @@ void perf_gtk__show_annotations(void) return; window = pgctx->main_window; - gtk_widget_show_all(window); perf_gtk__resize_window(window); - gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); + gtk_widget_set_visible(window, TRUE); - gtk_main(); + perf_gtk__run_main_loop(window); perf_gtk__deactivate_context(&pgctx); } diff --git a/tools/perf/ui/gtk/browser.c b/tools/perf/ui/gtk/browser.c index d2dadf3873fb..98f1d6bfcb24 100644 --- a/tools/perf/ui/gtk/browser.c +++ b/tools/perf/ui/gtk/browser.c @@ -5,32 +5,92 @@ #include "../hist.h" #include "../helpline.h" +#include #include -void perf_gtk__signal(int sig) +/* + * SIGINT/SIGQUIT/SIGTERM are asynchronous: deferring the actual exit to a + * GLib source dispatched from the main loop means it always runs on the + * main thread, serialized with everything else the main loop does + * (including perf_gtk__error()'s updates to perf_gtk__error_loops), instead + * of racing them from arbitrary signal-handler context. + */ +static gboolean perf_gtk__quit_signal(gpointer data __maybe_unused) { perf_gtk__exit(false); + return G_SOURCE_REMOVE; +} + +void perf_gtk__install_quit_signals(void) +{ + g_unix_signal_add(SIGINT, perf_gtk__quit_signal, NULL); + g_unix_signal_add(SIGQUIT, perf_gtk__quit_signal, NULL); + g_unix_signal_add(SIGTERM, perf_gtk__quit_signal, NULL); +} + +/* + * SIGSEGV/SIGFPE are synchronous faults: there's no "later" to defer to, + * and no safe way to run GTK/GLib code (or anything else non-async-signal- + * safe) from the faulting context. Report and let the default disposition + * (core dump) happen instead of trying to tear down GTK state here. + */ +void perf_gtk__fatal_signal(int sig) +{ psignal(sig, "perf"); + signal(sig, SIG_DFL); + raise(sig); } void perf_gtk__resize_window(GtkWidget *window) { GdkRectangle rect; - GdkScreen *screen; - int monitor; + GdkMonitor *monitor; + GdkDisplay *display; + GListModel *monitors; int height; int width; - screen = gtk_widget_get_screen(window); + display = gtk_widget_get_display(window); + monitors = gdk_display_get_monitors(display); + monitor = g_list_model_get_item(monitors, 0); + if (!monitor) { + gtk_window_set_default_size(GTK_WINDOW(window), 800, 600); + return; + } - monitor = gdk_screen_get_monitor_at_window(screen, window->window); - - gdk_screen_get_monitor_geometry(screen, monitor, &rect); + gdk_monitor_get_geometry(monitor, &rect); + g_object_unref(monitor); width = rect.width * 3 / 4; height = rect.height * 3 / 4; - gtk_window_resize(GTK_WINDOW(window), width, height); + gtk_window_set_default_size(GTK_WINDOW(window), width, height); +} + +static GMainLoop *perf_gtk__main_loop; + +void perf_gtk__quit_main_loop(void) +{ + if (perf_gtk__main_loop) + g_main_loop_quit(perf_gtk__main_loop); +} + +static gboolean perf_gtk__close_request(GtkWidget *widget __maybe_unused, + gpointer data __maybe_unused) +{ + perf_gtk__quit_main_loop(); + + return FALSE; +} + +void perf_gtk__run_main_loop(GtkWidget *window) +{ + g_signal_connect(window, "close-request", + G_CALLBACK(perf_gtk__close_request), NULL); + + perf_gtk__main_loop = g_main_loop_new(NULL, FALSE); + g_main_loop_run(perf_gtk__main_loop); + g_clear_pointer(&perf_gtk__main_loop, g_main_loop_unref); } const char *perf_gtk__get_percent_color(double percent) @@ -42,33 +102,35 @@ const char *perf_gtk__get_percent_color(double percent) return NULL; } -#ifdef HAVE_GTK_INFO_BAR_SUPPORT +static void perf_gtk__hide_widget(GtkWidget *widget, gint response_id __maybe_unused, + gpointer data __maybe_unused) +{ + gtk_widget_set_visible(widget, FALSE); +} + GtkWidget *perf_gtk__setup_info_bar(void) { GtkWidget *info_bar; GtkWidget *label; - GtkWidget *content_area; info_bar = gtk_info_bar_new(); - gtk_widget_set_no_show_all(info_bar, TRUE); + gtk_widget_set_visible(info_bar, FALSE); label = gtk_label_new(""); - gtk_widget_show(label); + gtk_widget_set_visible(label, TRUE); - content_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_bar)); - gtk_container_add(GTK_CONTAINER(content_area), label); + gtk_info_bar_add_child(GTK_INFO_BAR(info_bar), label); - gtk_info_bar_add_button(GTK_INFO_BAR(info_bar), GTK_STOCK_OK, + gtk_info_bar_add_button(GTK_INFO_BAR(info_bar), "_OK", GTK_RESPONSE_OK); g_signal_connect(info_bar, "response", - G_CALLBACK(gtk_widget_hide), NULL); + G_CALLBACK(perf_gtk__hide_widget), NULL); pgctx->info_bar = info_bar; pgctx->message_label = label; return info_bar; } -#endif GtkWidget *perf_gtk__setup_statusbar(void) { diff --git a/tools/perf/ui/gtk/gtk.h b/tools/perf/ui/gtk/gtk.h index a2b497f03fd6..6b37fd9943c9 100644 --- a/tools/perf/ui/gtk/gtk.h +++ b/tools/perf/ui/gtk/gtk.h @@ -13,10 +13,8 @@ struct perf_gtk_context { GtkWidget *main_window; GtkWidget *notebook; -#ifdef HAVE_GTK_INFO_BAR_SUPPORT GtkWidget *info_bar; GtkWidget *message_label; -#endif GtkWidget *statbar; guint statbar_ctx_id; }; @@ -38,19 +36,15 @@ void perf_gtk__init_helpline(void); void gtk_ui_progress__init(void); void perf_gtk__init_hpp(void); -void perf_gtk__signal(int sig); +void perf_gtk__install_quit_signals(void); +void perf_gtk__fatal_signal(int sig); void perf_gtk__resize_window(GtkWidget *window); +void perf_gtk__run_main_loop(GtkWidget *window); +void perf_gtk__quit_main_loop(void); +void perf_gtk__quit_error_dialog(void); const char *perf_gtk__get_percent_color(double percent); GtkWidget *perf_gtk__setup_statusbar(void); - -#ifdef HAVE_GTK_INFO_BAR_SUPPORT GtkWidget *perf_gtk__setup_info_bar(void); -#else -static inline GtkWidget *perf_gtk__setup_info_bar(void) -{ - return NULL; -} -#endif struct evsel; struct evlist; diff --git a/tools/perf/ui/gtk/hists.c b/tools/perf/ui/gtk/hists.c index bae21f336ae6..716dcf02bd0e 100644 --- a/tools/perf/ui/gtk/hists.c +++ b/tools/perf/ui/gtk/hists.c @@ -8,6 +8,8 @@ #include "../helpline.h" #include "../string2.h" #include +#include +#include #include #include @@ -395,11 +397,9 @@ static void perf_gtk__show_hists(GtkWidget *window, struct hists *hists, } } - gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(view), TRUE); - g_signal_connect(view, "row-activated", G_CALLBACK(on_row_activated), NULL); - gtk_container_add(GTK_CONTAINER(window), view); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(window), view); } static void perf_gtk__add_hierarchy_entries(struct hists *hists, @@ -583,11 +583,9 @@ static void perf_gtk__show_hierarchy(GtkWidget *window, struct hists *hists, perf_gtk__add_hierarchy_entries(hists, &hists->entries, store, NULL, &hpp, min_pcnt); - gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(view), TRUE); - g_signal_connect(view, "row-activated", G_CALLBACK(on_row_activated), NULL); - gtk_container_add(GTK_CONTAINER(window), view); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(window), view); } int evlist__gtk_browse_hists(struct evlist *evlist, const char *help, @@ -600,36 +598,32 @@ int evlist__gtk_browse_hists(struct evlist *evlist, const char *help, GtkWidget *statbar; GtkWidget *window; - signal(SIGSEGV, perf_gtk__signal); - signal(SIGFPE, perf_gtk__signal); - signal(SIGINT, perf_gtk__signal); - signal(SIGQUIT, perf_gtk__signal); - signal(SIGTERM, perf_gtk__signal); + signal(SIGSEGV, perf_gtk__fatal_signal); + signal(SIGFPE, perf_gtk__fatal_signal); + perf_gtk__install_quit_signals(); - window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + window = gtk_window_new(); gtk_window_set_title(GTK_WINDOW(window), "perf report"); - g_signal_connect(window, "delete_event", gtk_main_quit, NULL); - pgctx = perf_gtk__activate_context(window); if (!pgctx) return -1; - vbox = gtk_vbox_new(FALSE, 0); + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); notebook = gtk_notebook_new(); - gtk_box_pack_start(GTK_BOX(vbox), notebook, TRUE, TRUE, 0); + gtk_widget_set_vexpand(notebook, TRUE); + gtk_box_append(GTK_BOX(vbox), notebook); info_bar = perf_gtk__setup_info_bar(); - if (info_bar) - gtk_box_pack_start(GTK_BOX(vbox), info_bar, FALSE, FALSE, 0); + gtk_box_append(GTK_BOX(vbox), info_bar); statbar = perf_gtk__setup_statusbar(); - gtk_box_pack_start(GTK_BOX(vbox), statbar, FALSE, FALSE, 0); + gtk_box_append(GTK_BOX(vbox), statbar); - gtk_container_add(GTK_CONTAINER(window), vbox); + gtk_window_set_child(GTK_WINDOW(window), vbox); evlist__for_each_entry(evlist, pos) { struct hists *hists = evsel__hists(pos); @@ -649,7 +643,7 @@ int evlist__gtk_browse_hists(struct evlist *evlist, const char *help, } } - scrolled_window = gtk_scrolled_window_new(NULL, NULL); + scrolled_window = gtk_scrolled_window_new(); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), GTK_POLICY_AUTOMATIC, @@ -665,15 +659,12 @@ int evlist__gtk_browse_hists(struct evlist *evlist, const char *help, gtk_notebook_append_page(GTK_NOTEBOOK(notebook), scrolled_window, tab_label); } - gtk_widget_show_all(window); - perf_gtk__resize_window(window); - - gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); + gtk_widget_set_visible(window, TRUE); ui_helpline__push(help); - gtk_main(); + perf_gtk__run_main_loop(window); perf_gtk__deactivate_context(&pgctx); diff --git a/tools/perf/ui/gtk/progress.c b/tools/perf/ui/gtk/progress.c index eea6fcde518a..770f9251b54b 100644 --- a/tools/perf/ui/gtk/progress.c +++ b/tools/perf/ui/gtk/progress.c @@ -1,49 +1,65 @@ // SPDX-License-Identifier: GPL-2.0 #include +#include #include "gtk.h" #include "../progress.h" +#include static GtkWidget *dialog; static GtkWidget *progress; +static void gtk_ui_progress__destroyed(GtkWidget *widget __maybe_unused, + gpointer data __maybe_unused) +{ + dialog = NULL; + progress = NULL; +} + static void gtk_ui_progress__update(struct ui_progress *p) { double fraction = p->total ? 1.0 * p->curr / p->total : 0.0; char buf[1024]; if (dialog == NULL) { - GtkWidget *vbox = gtk_vbox_new(TRUE, 5); + GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); GtkWidget *label = gtk_label_new(p->title); - dialog = gtk_window_new(GTK_WINDOW_TOPLEVEL); + dialog = gtk_window_new(); progress = gtk_progress_bar_new(); - gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, FALSE, 3); - gtk_box_pack_start(GTK_BOX(vbox), progress, TRUE, TRUE, 3); + gtk_widget_set_vexpand(label, TRUE); + gtk_box_append(GTK_BOX(vbox), label); + gtk_widget_set_vexpand(progress, TRUE); + gtk_box_append(GTK_BOX(vbox), progress); - gtk_container_add(GTK_CONTAINER(dialog), vbox); + gtk_window_set_child(GTK_WINDOW(dialog), vbox); + + g_signal_connect(dialog, "destroy", + G_CALLBACK(gtk_ui_progress__destroyed), NULL); gtk_window_set_title(GTK_WINDOW(dialog), "perf"); - gtk_window_resize(GTK_WINDOW(dialog), 300, 80); - gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER); + gtk_window_set_default_size(GTK_WINDOW(dialog), 300, 80); - gtk_widget_show_all(dialog); + gtk_widget_set_visible(dialog, TRUE); } gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(progress), fraction); snprintf(buf, sizeof(buf), "%"PRIu64" / %"PRIu64, p->curr, p->total); gtk_progress_bar_set_text(GTK_PROGRESS_BAR(progress), buf); - /* we didn't call gtk_main yet, so do it manually */ - while (gtk_events_pending()) - gtk_main_iteration(); + /* we didn't start a main loop yet, so pump events manually */ + while (g_main_context_pending(NULL)) + g_main_context_iteration(NULL, FALSE); } static void gtk_ui_progress__finish(void) { + if (dialog == NULL) + return; + /* this will also destroy all of its children */ - gtk_widget_destroy(dialog); + gtk_window_destroy(GTK_WINDOW(dialog)); dialog = NULL; } diff --git a/tools/perf/ui/gtk/setup.c b/tools/perf/ui/gtk/setup.c index f5eee4d66873..9b44f3719747 100644 --- a/tools/perf/ui/gtk/setup.c +++ b/tools/perf/ui/gtk/setup.c @@ -12,7 +12,7 @@ int perf_gtk__init(void) gtk_ui_progress__init(); perf_gtk__init_hpp(); - return gtk_init_check(NULL, NULL) ? 0 : -1; + return gtk_init_check() ? 0 : -1; } void perf_gtk__exit(bool wait_for_ok __maybe_unused) @@ -20,5 +20,6 @@ void perf_gtk__exit(bool wait_for_ok __maybe_unused) if (!perf_gtk__is_active_context(pgctx)) return; perf_error__unregister(&perf_gtk_eops); - gtk_main_quit(); + perf_gtk__quit_error_dialog(); + perf_gtk__quit_main_loop(); } diff --git a/tools/perf/ui/gtk/util.c b/tools/perf/ui/gtk/util.c index c47f5c387838..5823ff0d7c2a 100644 --- a/tools/perf/ui/gtk/util.c +++ b/tools/perf/ui/gtk/util.c @@ -2,8 +2,10 @@ #include "../util.h" #include "gtk.h" +#include +#include #include -#include +#include #include struct perf_gtk_context *pgctx; @@ -28,86 +30,103 @@ int perf_gtk__deactivate_context(struct perf_gtk_context **ctx) return 0; } +/* + * perf_gtk__error() can be called re-entrantly, since the dialog isn't + * modal and its nested loop still pumps events for the main window. + * Track every currently running loop instead of a single pointer, so a + * nested call can't clobber an outer call's loop and leak it. + */ +static GSList *perf_gtk__error_loops; + +static void perf_gtk__quit_loop(gpointer data, gpointer user_data __maybe_unused) +{ + g_main_loop_quit(data); +} + +void perf_gtk__quit_error_dialog(void) +{ + g_slist_foreach(perf_gtk__error_loops, perf_gtk__quit_loop, NULL); +} + +static void perf_gtk__dialog_response(GtkDialog *dialog, + gint response_id __maybe_unused, + gpointer data __maybe_unused) +{ + gtk_window_destroy(GTK_WINDOW(dialog)); +} + static int perf_gtk__error(const char *format, va_list args) { char *msg; GtkWidget *dialog; + GMainLoop *loop; + va_list args_copy; + va_copy(args_copy, args); if (!perf_gtk__is_active_context(pgctx) || - vasprintf(&msg, format, args) < 0) { + vasprintf(&msg, format, args_copy) < 0) { + va_end(args_copy); fprintf(stderr, "Error:\n"); vfprintf(stderr, format, args); fprintf(stderr, "\n"); return -1; } + va_end(args_copy); dialog = gtk_message_dialog_new_with_markup(GTK_WINDOW(pgctx->main_window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "Error\n\n%s", msg); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); + /* + * "response" only fires when a button is clicked; DESTROY_WITH_PARENT + * destroys the dialog directly without it. Quit from "destroy" + * instead, which fires either way, so the nested loop below can't + * outlive the dialog and hang. + */ + loop = g_main_loop_new(NULL, FALSE); + perf_gtk__error_loops = g_slist_prepend(perf_gtk__error_loops, loop); + g_signal_connect(dialog, "response", + G_CALLBACK(perf_gtk__dialog_response), NULL); + g_signal_connect_swapped(dialog, "destroy", + G_CALLBACK(g_main_loop_quit), loop); + + gtk_widget_set_visible(dialog, TRUE); + g_main_loop_run(loop); + perf_gtk__error_loops = g_slist_remove(perf_gtk__error_loops, loop); + g_main_loop_unref(loop); + free(msg); return 0; } -#ifdef HAVE_GTK_INFO_BAR_SUPPORT static int perf_gtk__warning_info_bar(const char *format, va_list args) { char *msg; + va_list args_copy; + va_copy(args_copy, args); if (!perf_gtk__is_active_context(pgctx) || - vasprintf(&msg, format, args) < 0) { + vasprintf(&msg, format, args_copy) < 0) { + va_end(args_copy); fprintf(stderr, "Warning:\n"); vfprintf(stderr, format, args); fprintf(stderr, "\n"); return -1; } + va_end(args_copy); gtk_label_set_text(GTK_LABEL(pgctx->message_label), msg); gtk_info_bar_set_message_type(GTK_INFO_BAR(pgctx->info_bar), GTK_MESSAGE_WARNING); - gtk_widget_show(pgctx->info_bar); + gtk_widget_set_visible(pgctx->info_bar, TRUE); free(msg); return 0; } -#else -static int perf_gtk__warning_statusbar(const char *format, va_list args) -{ - char *msg, *p; - - if (!perf_gtk__is_active_context(pgctx) || - vasprintf(&msg, format, args) < 0) { - fprintf(stderr, "Warning:\n"); - vfprintf(stderr, format, args); - fprintf(stderr, "\n"); - return -1; - } - - gtk_statusbar_pop(GTK_STATUSBAR(pgctx->statbar), - pgctx->statbar_ctx_id); - - /* Only first line can be displayed */ - p = strchr(msg, '\n'); - if (p) - *p = '\0'; - - gtk_statusbar_push(GTK_STATUSBAR(pgctx->statbar), - pgctx->statbar_ctx_id, msg); - - free(msg); - return 0; -} -#endif struct perf_error_ops perf_gtk_eops = { .error = perf_gtk__error, -#ifdef HAVE_GTK_INFO_BAR_SUPPORT .warning = perf_gtk__warning_info_bar, -#else - .warning = perf_gtk__warning_statusbar, -#endif }; diff --git a/tools/perf/ui/setup.c b/tools/perf/ui/setup.c index ff800047e697..d887346c7a63 100644 --- a/tools/perf/ui/setup.c +++ b/tools/perf/ui/setup.c @@ -14,7 +14,7 @@ int use_browser = -1; #define PERF_GTK_DSO "libperf-gtk.so" -#ifdef HAVE_GTK2_SUPPORT +#ifdef HAVE_GTK4_SUPPORT static int setup_gtk_browser(void) { From 0b568c865fac195e90f4669bde5af0e236830af2 Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Tue, 8 Sep 2026 22:50:35 -0400 Subject: [PATCH 050/425] perf tools: make the GTK4 report browser actually loadable at runtime perf report --gtk dlopen()s libperf-gtk.so, which expects to resolve symbols back against the running perf binary (callchain_param, symbol_conf, evsel__name, and friends live in perf, not the plugin). Two things broke that after the GTK 4 port: perf never passed -rdynamic, so none of its symbols were in its dynamic symbol table for a dlopen()ed plugin to find. Add -rdynamic to LDFLAGS when GTK4 support is enabled. annotated_source__hist_entry() was a static inline in annotate.h, so ui/gtk/annotate.c calling it pulled hashmap__find()'s expansion, hashmap_find(), into libperf-gtk.so as an undefined symbol. The only hashmap_find perf links against normally is libbpf's internal one (tools/lib/bpf/hashmap.c), built with -fvisibility=hidden, so it can never be exported to a dlopen()ed plugin regardless of LDFLAGS. Move annotated_source__hist_entry() into annotate.c as an ordinary exported function, so the plugin depends on it the same way it already depends on evsel__group_desc() and friends. With both fixes, a default 'make GTK4=1' build (libbpf statically linked) can dlopen() libperf-gtk.so and open the report browser without NO_LIBBPF=1 or manual LDFLAGS. Verified with perf report --gtk against real perf.data on a GTK4 desktop. Tested-by: Arnaldo Carvalho de Melo Signed-off-by: Matt Turner Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Makefile.config | 5 +++++ tools/perf/util/annotate.c | 11 +++++++++++ tools/perf/util/annotate.h | 12 ++---------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config index 3e59e2b7eaec..4ee7393a39f9 100644 --- a/tools/perf/Makefile.config +++ b/tools/perf/Makefile.config @@ -785,6 +785,11 @@ ifdef GTK4 GTK_CFLAGS += $(shell $(PKG_CONFIG) --cflags gtk4 2>/dev/null) GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4 2>/dev/null) EXTLIBS += -ldl + # libperf-gtk.so is dlopen()ed at runtime and calls back into + # symbols defined in the perf binary itself (callchain_param, + # symbol_conf, evsel__name, ...): perf needs to export those + # dynamically for the plugin to resolve them. + LDFLAGS += -rdynamic endif endif diff --git a/tools/perf/util/annotate.c b/tools/perf/util/annotate.c index 9304c21c686a..fd789eb53e74 100644 --- a/tools/perf/util/annotate.c +++ b/tools/perf/util/annotate.c @@ -145,6 +145,17 @@ static int annotated_source__alloc_histograms(struct annotated_source *src, return src->histograms ? 0 : -1; } +struct sym_hist_entry * +annotated_source__hist_entry(struct annotated_source *src, const struct evsel *evsel, u64 offset) +{ + struct sym_hist_entry *entry; + long key = offset << 16 | evsel->core.idx; + + if (!hashmap__find(src->samples, key, &entry)) + return NULL; + return entry; +} + void symbol__annotate_zero_histograms(struct symbol *sym) { struct annotation *notes = symbol__annotation(sym); diff --git a/tools/perf/util/annotate.h b/tools/perf/util/annotate.h index d7807df6667f..4c49d41e73a4 100644 --- a/tools/perf/util/annotate.h +++ b/tools/perf/util/annotate.h @@ -446,16 +446,8 @@ static inline struct sym_hist *annotation__histogram(struct annotation *notes, return annotated_source__histogram(notes->src, evsel); } -static inline struct sym_hist_entry * -annotated_source__hist_entry(struct annotated_source *src, const struct evsel *evsel, u64 offset) -{ - struct sym_hist_entry *entry; - long key = offset << 16 | evsel->core.idx; - - if (!hashmap__find(src->samples, key, &entry)) - return NULL; - return entry; -} +struct sym_hist_entry * +annotated_source__hist_entry(struct annotated_source *src, const struct evsel *evsel, u64 offset); static inline struct annotation *symbol__annotation(struct symbol *sym) { From 116f95a0e299fd914787206bd2c22b13b276f9d3 Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Tue, 8 Sep 2026 22:50:36 -0400 Subject: [PATCH 051/425] perf tools gtk: Fix two hierarchy-view stack buffer overflows perf_gtk__show_hierarchy() builds a merged column header for the hierarchy view with unbounded strcat() calls into a 512-byte stack buffer. The pieces being appended come from tracepoint field names and sort-key headers in perf.data, so a file with enough dynamic sort keys or long enough field names overflows the buffer. perf_gtk__add_hierarchy_entries() has a related bug in the loop that formats each entry's value columns. fmt->entry()/fmt->color() return via scnprintf(), so ret is clamped to at most hpp->size - 1, but advance_hpp(hpp, ret + 2) doesn't clamp: when ret hits that maximum, ret + 2 exceeds hpp->size by one, and hpp->size (size_t) underflows to roughly SIZE_MAX. The next iteration's fmt->entry() then writes into the caller's stack buffer using that bogus size, a second overflow. That same loop also saves bf/size at the top of each iteration but only restored hpp->buf/hpp->size to them before recursing into non-leaf children. Leaf entries left the buffer state advanced from the format loop, so the next sibling in the traversal inherited a shrunk hpp->size and an already-advanced hpp->buf, eventually running hpp->size down to 0 and pointing bf past the end of the stack buffer for the strim(bf) call. Fix the header builder by tracking the write offset and using scnprintf() for each append, same pattern already used elsewhere in this file. Fix the entry loop by clamping the amount passed to advance_hpp() to what's actually left in the buffer, and by restoring hpp->buf/hpp->size unconditionally after formatting each entry instead of only before recursing. Both bugs predate the perf GTK UI's move to GTK 4; neither function is touched by that port. Tested-by: Arnaldo Carvalho de Melo Signed-off-by: Matt Turner Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/ui/gtk/hists.c | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/tools/perf/ui/gtk/hists.c b/tools/perf/ui/gtk/hists.c index 716dcf02bd0e..80df3fec8ea1 100644 --- a/tools/perf/ui/gtk/hists.c +++ b/tools/perf/ui/gtk/hists.c @@ -449,7 +449,7 @@ static void perf_gtk__add_hierarchy_entries(struct hists *hists, bf = hpp->buf; size = hpp->size; perf_hpp_list__for_each_format(he->hpp_list, fmt) { - int ret; + int ret, inc; if (fmt->color) ret = fmt->color(fmt, hpp, he); @@ -457,15 +457,26 @@ static void perf_gtk__add_hierarchy_entries(struct hists *hists, ret = fmt->entry(fmt, hpp, he); snprintf(hpp->buf + ret, hpp->size - ret, " "); - advance_hpp(hpp, ret + 2); + /* + * ret can be as large as hpp->size - 1, so ret + 2 + * can exceed hpp->size. advance_hpp() doesn't clamp, + * so passing that through would underflow the + * size_t hpp->size and let a later fmt->entry() in + * this loop write past the end of the caller's + * stack buffer. + */ + inc = ret + 2; + if (inc > (int)hpp->size) + inc = hpp->size; + advance_hpp(hpp, inc); } gtk_tree_store_set(store, &iter, col_idx, strim(bf), -1); - if (!he->leaf) { - hpp->buf = bf; - hpp->size = size; + hpp->buf = bf; + hpp->size = size; + if (!he->leaf) { perf_gtk__add_hierarchy_entries(hists, &he->hroot_out, store, &iter, hpp, min_pcnt); @@ -505,6 +516,7 @@ static void perf_gtk__show_hierarchy(GtkWidget *window, struct hists *hists, GtkWidget *view; int col_idx; int nr_cols = 0; + int ret; char s[512]; char buf[512]; bool first_node, first_col; @@ -541,9 +553,10 @@ static void perf_gtk__show_hierarchy(GtkWidget *window, struct hists *hists, /* construct merged column header since sort keys share single column */ buf[0] = '\0'; first_node = true; + ret = 0; list_for_each_entry_continue(fmt_node, &hists->hpp_formats, list) { if (!first_node) - strcat(buf, " / "); + ret += scnprintf(buf + ret, sizeof(buf) - ret, " / "); first_node = false; first_col = true; @@ -552,11 +565,11 @@ static void perf_gtk__show_hierarchy(GtkWidget *window, struct hists *hists, continue; if (!first_col) - strcat(buf, "+"); + ret += scnprintf(buf + ret, sizeof(buf) - ret, "+"); first_col = false; fmt->header(fmt, &hpp, hists, 0, NULL); - strcat(buf, strim(hpp.buf)); + ret += scnprintf(buf + ret, sizeof(buf) - ret, "%s", strim(hpp.buf)); } } From 89d001d48ec792b972ae2d09e59d8b8a5f2124db Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Wed, 9 Sep 2026 16:49:17 -0300 Subject: [PATCH 052/425] tools build: Add zstd to the binutils library feature tests Recently zstd became needed with the binutils libraries, add it to the mix. Signed-off-by: Arnaldo Carvalho de Melo --- tools/build/feature/Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/build/feature/Makefile b/tools/build/feature/Makefile index 01c48e6ef021..cdf89f132074 100644 --- a/tools/build/feature/Makefile +++ b/tools/build/feature/Makefile @@ -265,17 +265,17 @@ $(OUTPUT)test-libpython.bin: $(BUILD) $(FLAGS_PYTHON_EMBED) $(OUTPUT)test-libbfd.bin: - $(BUILD_BFD) + $(BUILD_BFD) || $(BUILD_BFD) -lzstd $(OUTPUT)test-libbfd-threadsafe.bin: - $(BUILD_BFD) || $(BUILD_BFD) -liberty || $(BUILD_BFD) -liberty -lz + $(BUILD_BFD) || $(BUILD_BFD) -lzstd || $(BUILD_BFD) -liberty || $(BUILD_BFD) -liberty -lz $(OUTPUT)test-disassembler-four-args.bin: - $(BUILD_BFD) -lopcodes || $(BUILD_BFD) -lopcodes -liberty || \ + $(BUILD_BFD) -lopcodes || $(BUILD_BFD) -lopcodes -lzstd || $(BUILD_BFD) -lopcodes -liberty || \ $(BUILD_BFD) -lopcodes -liberty -lz $(OUTPUT)test-disassembler-init-styled.bin: - $(BUILD_BFD) -lopcodes || $(BUILD_BFD) -lopcodes -liberty || \ + $(BUILD_BFD) -lopcodes || $(BUILD_BFD) -lopcodes -liberty || \ $(BUILD_BFD) -lopcodes -liberty -lz $(OUTPUT)test-reallocarray.bin: From 1572ba36fbca95835dc17b35a70c4524301205c6 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 9 Sep 2026 09:10:40 -0700 Subject: [PATCH 053/425] perf tests: Improvements to build-test performance Replace hardcoded 'make' calls with '$(MAKE)' within tests/make to ensure GNU Make jobserver file descriptors are properly inherited by nested builds. Additionally, remove the notorious 'unexport MAKEFLAGS' from tools/perf/Makefile. This hack was originally added to prevent the wrapper's internal '-j$(JOBS)' override from crashing when parent jobserver tokens were present. Instead, use proper GNU Make conditionals to check for '-j' or 'jobserver' strings within MAKEFLAGS. If a jobserver is already orchestrating the build, gracefully back off and let GNU Make manage parallelism natively. Finally, optimize the 'out' target (which processes 'run_O' out-of-tree builds) by stripping sequential bottlenecks like $(call clean) from isolated tmp directories, and wrapping the target list in a parallel sub-make call. These combined changes enable 'build-test' out-of-tree targets to build completely concurrently without severing jobserver tokens. Unscientifically measured the change lowered the build-test from around 15 minutes to 10 minutes on a 28 threaded Alderlake CPU. Fixing the jobserver issues meant the machine remained somewhat usable while running the test. Tested-by: Arnaldo Carvalho de Melo Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Makefile | 35 +++++++++++++++++++----------- tools/perf/tests/make | 50 +++++++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/tools/perf/Makefile b/tools/perf/Makefile index 56014106479a..b8d38728306a 100644 --- a/tools/perf/Makefile +++ b/tools/perf/Makefile @@ -14,23 +14,29 @@ .SUFFIXES: # -# We don't want to pass along options like -j: +# If no parallel build was requested, do a parallel build with multiple jobs, +# based on the number of CPUs online in this system: 'make -j8' on an 8-CPU +# system, etc. # -unexport MAKEFLAGS - -# -# Do a parallel build with multiple jobs, based on the number of CPUs online -# in this system: 'make -j8' on a 8-CPU system, etc. +# If MAKEFLAGS contains '-j' or 'jobserver', JOBS is intentionally left +# uninitialized so the native GNU Make jobserver can seamlessly control the +# parallel bounds of the build. # # (To override it, run 'make JOBS=1' and similar.) # -ifeq ($(JOBS),) - JOBS := $(shell (getconf _NPROCESSORS_ONLN || grep -E -c '^processor|^CPU[0-9]' /proc/cpuinfo) 2>/dev/null) - ifeq ($(JOBS),0) - JOBS := 1 +ifeq ($(findstring -j,$(MAKEFLAGS)),) + ifeq ($(findstring jobserver,$(MAKEFLAGS)),) + ifeq ($(JOBS),) + JOBS := $(shell (getconf _NPROCESSORS_ONLN || grep -E -c '^processor|^CPU[0-9]' /proc/cpuinfo) 2>/dev/null) + ifeq ($(JOBS),0) + JOBS := 1 + endif + endif + PARALLEL_OPT := -j$(JOBS) endif endif + # # Only pass canonical directory names as the output directory: # @@ -58,11 +64,11 @@ else endif define print_msg - @printf ' BUILD: Doing '\''make \033[33m-j'$(JOBS)'\033[m'\'' $(BUILD_TYPE) build\n' + @printf ' BUILD: Doing '\''make\033[33m%s\033[m'\'' $(BUILD_TYPE) build\n' "$(if $(JOBS), -j$(JOBS),)" endef define make - @$(MAKE) -f Makefile.perf --no-print-directory -j$(JOBS) O=$(FULL_O) $(SET_DEBUG) $@ + @$(MAKE) -f Makefile.perf --no-print-directory $(PARALLEL_OPT) O=$(FULL_O) $(SET_DEBUG) $@ endef # @@ -106,7 +112,10 @@ clean: # make -C tools/perf -f tests/make # build-test: - @$(MAKE) SHUF=1 -f tests/make REUSE_FEATURES_DUMP=1 MK=Makefile SET_PARALLEL=1 --no-print-directory tarpkg make_static make_with_gtk4 out + @$(MAKE) SHUF=1 -f tests/make REUSE_FEATURES_DUMP=1 MK=Makefile SET_PARALLEL=1 --no-print-directory tarpkg + @$(MAKE) SHUF=1 -f tests/make REUSE_FEATURES_DUMP=1 MK=Makefile SET_PARALLEL=1 --no-print-directory make_static + @$(MAKE) SHUF=1 -f tests/make REUSE_FEATURES_DUMP=1 MK=Makefile SET_PARALLEL=1 --no-print-directory make_with_gtk4 + @$(MAKE) SHUF=1 -f tests/make REUSE_FEATURES_DUMP=1 MK=Makefile SET_PARALLEL=1 --no-print-directory out build-test-tarball: @$(MAKE) -f tests/make REUSE_FEATURES_DUMP=1 MK=Makefile SET_PARALLEL=1 --no-print-directory out diff --git a/tools/perf/tests/make b/tools/perf/tests/make index 202ab5501916..f879f8109072 100644 --- a/tools/perf/tests/make +++ b/tools/perf/tests/make @@ -28,15 +28,17 @@ endif PARALLEL_OPT= ifeq ($(SET_PARALLEL),1) - ifeq ($(JOBS),) - cores := $(shell (getconf _NPROCESSORS_ONLN || grep -E -c '^processor|^CPU[0-9]' /proc/cpuinfo) 2>/dev/null) - ifeq ($(cores),0) - cores := 1 + ifeq ($(findstring jobserver,$(MAKEFLAGS))$(findstring -j,$(MAKEFLAGS)),) + ifeq ($(JOBS),) + cores := $(shell (getconf _NPROCESSORS_ONLN || grep -E -c '^processor|^CPU[0-9]' /proc/cpuinfo) 2>/dev/null) + ifeq ($(cores),0) + cores := 1 + endif + else + cores=$(JOBS) endif - else - cores=$(JOBS) + PARALLEL_OPT="-j$(cores)" endif - PARALLEL_OPT="-j$(cores)" endif # As per kernel Makefile, avoid funny character set dependencies @@ -230,7 +232,7 @@ test_make_util_pmu_bison_o := test -f $(PERF_O)/util/pmu-bison.o define test_dest_files for file in $(1); do \ - if [ ! -x $$TMP_DEST/$$file ]; then \ + if [ ! -f $$TMP_DEST/$$file ]; then \ echo " failed to find: $$file"; \ fi \ done @@ -310,7 +312,8 @@ clean := @(cd $(PERF); $(MAKE_F) -s $(O_OPT) clean >/dev/null && $(MAKE) -s $(O_ $(run): $(call clean) - @TMP_DEST=$$(mktemp -d); \ + +@TMP_DEST=$$(mktemp -d); \ + trap 'rm -rf $@ $$TMP_DEST; exit 130' INT TERM; \ cmd="cd $(PERF) && $(MAKE_F) $($@) $(PARALLEL_OPT) $(O_OPT) DESTDIR=$$TMP_DEST"; \ printf "%*.*s: %s\n" $(max_width) $(max_width) "$@" "$$cmd" && echo $$cmd > $@ && \ ( eval $$cmd ) >> $@ 2>&1; \ @@ -320,7 +323,8 @@ $(run): make_with_gtk4: $(call clean) - @TMP_DEST=$$(mktemp -d); \ + +@TMP_DEST=$$(mktemp -d); \ + trap 'rm -rf $@ $$TMP_DEST; exit 130' INT TERM; \ cmd="cd $(PERF) && $(MAKE_F) $($@) $(PARALLEL_OPT) $(O_OPT) DESTDIR=$$TMP_DEST"; \ printf "%*.*s: %s\n" $(max_width) $(max_width) "$@" "$$cmd" && echo $$cmd > $@ && \ ( eval $$cmd ) >> $@ 2>&1; \ @@ -330,7 +334,8 @@ make_with_gtk4: make_static: $(call clean) - @TMP_DEST=$$(mktemp -d); \ + +@TMP_DEST=$$(mktemp -d); \ + trap 'rm -rf $@ $$TMP_DEST; exit 130' INT TERM; \ cmd="cd $(PERF) && $(MAKE_F) $($@) $(PARALLEL_OPT) $(O_OPT) DESTDIR=$$TMP_DEST"; \ printf "%*.*s: %s\n" $(max_width) $(max_width) "$@" "$$cmd" && echo $$cmd > $@ && \ ( eval $$cmd ) >> $@ 2>&1; \ @@ -339,9 +344,12 @@ make_static: rm -rf $@ $$TMP_DEST || (cat $@ ; false) $(run_O): +ifneq ($(O_CLEAN),0) $(call clean) - @TMP_O=$$(mktemp -d); \ +endif + +@TMP_O=$$(mktemp -d); \ TMP_DEST=$$(mktemp -d); \ + trap 'rm -rf $@ $$TMP_O $$TMP_DEST; exit 130' INT TERM; \ cmd="cd $(PERF) && $(MAKE_F) $($(patsubst %_O,%,$@)) $(PARALLEL_OPT) O=$$TMP_O DESTDIR=$$TMP_DEST"; \ printf "%*.*s: %s\n" $(max_width) $(max_width) "$@" "$$cmd" && echo $$cmd > $@ && \ ( eval $$cmd ) >> $@ 2>&1 && \ @@ -363,18 +371,18 @@ endif make_kernelsrc: @echo "- make -C $(PARALLEL_OPT) $(K_O_OPT) tools/perf" $(call clean); \ - (make -C ../.. $(PARALLEL_OPT) $(K_O_OPT) tools/perf) > $@ 2>&1 && \ + ($(MAKE) -C ../.. $(PARALLEL_OPT) $(K_O_OPT) tools/perf) > $@ 2>&1 && \ test -x $(KERNEL_O)/tools/perf/perf && rm -f $@ || (cat $@ ; false) make_kernelsrc_tools: @echo "- make -C /tools $(PARALLEL_OPT) $(K_O_OPT) perf" $(call clean); \ - (make -C ../../tools $(PARALLEL_OPT) $(K_O_OPT) perf) > $@ 2>&1 && \ + ($(MAKE) -C ../../tools $(PARALLEL_OPT) $(K_O_OPT) perf) > $@ 2>&1 && \ test -x $(KERNEL_O)/tools/perf/perf && rm -f $@ || (cat $@ ; false) make_libperf: @echo "- make -C lib"; - make -C lib clean >$@ 2>&1; make -C lib >>$@ 2>&1 && rm $@ + $(MAKE) -C lib clean >$@ 2>&1; $(MAKE) -C lib >>$@ 2>&1 && rm $@ FEATURES_DUMP_FILE := $(FULL_O)/BUILD_TEST_FEATURE_DUMP FEATURES_DUMP_FILE_STATIC := $(FULL_O)/BUILD_TEST_FEATURE_DUMP_STATIC @@ -383,20 +391,22 @@ all: $(run) $(run_O) tarpkg make_kernelsrc make_kernelsrc_tools @echo OK @rm -f $(FEATURES_DUMP_FILE) $(FEATURES_DUMP_FILE_STATIC) -out: $(run_O) +out: + $(call clean) + @$(MAKE) -f tests/make $(run_O) PARALLEL_OPT= O_CLEAN=0 $(if $(findstring jobserver,$(MAKEFLAGS))$(findstring -j,$(MAKEFLAGS)),,$(if $(cores),-j$(cores))) @echo OK @rm -f $(FEATURES_DUMP_FILE) $(FEATURES_DUMP_FILE_STATIC) ifeq ($(REUSE_FEATURES_DUMP),1) $(FEATURES_DUMP_FILE): $(call clean) - @cmd="cd $(PERF) && make FEATURE_DUMP_COPY=$@ $(O_OPT) feature-dump"; \ + @cmd="cd $(PERF) && $(MAKE) FEATURE_DUMP_COPY=$@ $(O_OPT) feature-dump"; \ echo "- $@: $$cmd" && echo $$cmd && \ ( eval $$cmd ) > /dev/null 2>&1 $(FEATURES_DUMP_FILE_STATIC): $(call clean) - @cmd="cd $(PERF) && make FEATURE_DUMP_COPY=$@ $(O_OPT) LDFLAGS='-static' feature-dump"; \ + @cmd="cd $(PERF) && $(MAKE) FEATURE_DUMP_COPY=$@ $(O_OPT) LDFLAGS='-static' feature-dump"; \ echo "- $@: $$cmd" && echo $$cmd && \ ( eval $$cmd ) > /dev/null 2>&1 @@ -416,3 +426,7 @@ endif .PHONY: all $(run) $(run_O) tarpkg clean make_kernelsrc make_kernelsrc_tools make_libperf endif # ifndef MK + +# Serialize execution of tasks writing to shared in-tree files +make_tags_O: | make_clean_all_O +make_cscope_O: | make_clean_all_O From 26c137421fd59a0e7410d1148b42b2d4f4870daa Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 2 Sep 2026 08:34:20 -0700 Subject: [PATCH 054/425] perf tool: Align structs in arrays in .clang-format Configure clang-format to ensure structs within arrays maintain left alignment inside tools/perf/.clang-format. This prevents standard formatting runs from arbitrarily squashing tabbed initialization pillars. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/.clang-format | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/perf/.clang-format b/tools/perf/.clang-format index e4cff363a603..7cc11f84da88 100644 --- a/tools/perf/.clang-format +++ b/tools/perf/.clang-format @@ -21,3 +21,4 @@ IncludeCategories: # Your Project's Other Headers - Regex: '^".*"' Priority: 4 +AlignArrayOfStructures: Left From 591a97eb0c241db5646d308353620f9eaaf8500c Mon Sep 17 00:00:00 2001 From: Alice Rogers Date: Sat, 8 Aug 2026 22:18:18 -0700 Subject: [PATCH 055/425] perf python: New treport script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A textual app that displays the results of processing samples similar to perf report. The app displays a tree of first processes and then functions which drop down to show more detail on the functions they call. The functions with the largest number of samples are sorted first, after each function the percentage of time spent within it is highlighted. If more than one event is recorded then each event appears first, with the processes and functions sorted for that event beneath it. The app is written in python and requires the textual framework. Using the textual framework means that in most modern terminals the mouse is supported, the color scheme can be configured, and screenshots made. With perf report: ``` $ perf record -e cycles:u -g perf test -w brstack [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.008 MB perf.data (57 samples) ] $ perf report ... Samples: 57 of event 'cycles:u', Event count (approx.): 15907831 Children Self Command Shared Object Symbol - 88.86% 0.00% perf libc.so.6 [.] 0x000079b17df69ca8 0x79b17df69ca8 main handle_internal_command cmd_test - brstack - 84.27% brstack_bench - 40.69% brstack_foo brstack_bar 3.32% brstack_bar 3.31% brstack_bar 1.27% brstack_foo + 88.86% 0.00% perf perf [.] main + 88.86% 0.00% perf perf [.] handle_internal_command + 88.86% 0.00% perf perf [.] cmd_test + 88.86% 0.00% perf perf [.] brstack + 84.27% 40.26% perf perf [.] brstack_bench + 41.96% 30.71% perf perf [.] brstack_foo + 17.89% 17.89% perf perf [.] brstack_bar ``` With the treport script: ``` $ perf record -e cycles:u -g perf test -w brstack [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.008 MB perf.data (57 samples) ] $ tools/perf/python/treport.py O ReportApp Report ╸━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▼ Profile └── ▼ cycles:u 100% └── ▼ perf (32963) 100% ├── ▼ /usr/lib/x86_64-linux-gnu/libc.so.6 0x79b17df6 │ └── ▼ main 56.1% │ └── ▼ handle_internal_command 56.1% │ └── ▼ cmd_test 56.1% │ └── ▼ brstack 56.1% ▁▁ │ ├── ▼ brstack_bench 52.6% │ │ ├── ▶ brstack_foo 24.6% │ │ └── brstack_bar 1.75% │ ├── brstack_foo 1.75% │ └── brstack_bar 1.75% ├── ▶ /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ▌ ^q Quit ▏^p palette ``` Committer testing: One needs to install 'python-textual' to have this working. Tested-by: Arnaldo Carvalho de Melo Co-developed-by: Ian Rogers Signed-off-by: Ian Rogers Signed-off-by: Alice Rogers Link: https://lore.kernel.org/r/20260809051820.2436530-2-irogers@google.com Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/treport.py | 207 +++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100755 tools/perf/python/treport.py diff --git a/tools/perf/python/treport.py b/tools/perf/python/treport.py new file mode 100755 index 000000000000..71a91775bd6a --- /dev/null +++ b/tools/perf/python/treport.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""treport.py - perf report like tool written using textual.""" +from typing import Dict, Optional +import argparse +import os +import sys +import perf +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.widgets import Footer, Header, TabbedContent, TabPane, Tree +from textual.widgets.tree import TreeNode + +# Global session. +session :Optional[perf.session] = None + +class ProfileNode: + """Represents a single node in a call stack tree. + + Generally a ProfileNode corresponds to a symbol in a call stack. + The root is special, its children are events and the events + children are process names. After the process name come the + samples. + + Attributes: + name (str): The name of the function, process or event. + value (int): The sample count for this node including counts from its + children. + parent (ProfileNode): The parent of this node, this node belongs to its + children. + children (Dict[str, ProfileNode]): A dictionary of child nodes, keyed by + their names. + """ + def __init__(self, name: str, parent: "ProfileNode"): + """Initializes a ProfileNode.""" + self.name = name + self.value: int = 0 + self.parent = parent if parent else self + self.children: Dict[str, ProfileNode] = {} + + def find_or_create_node(self, name: str) -> "ProfileNode": + """Finds a child node by name or creates it if it doesn't exist.""" + if name in self.children: + return self.children[name] + child = ProfileNode(name, self) + self.children[name] = child + return child + + def depth(self) -> int: + """The maximum depth of the call stack tree from this node down.""" + if not self.children: + return 1 + return max(child.depth() for child in self.children.values()) + 1 + + def process_event(self, sample) -> None: + """Processes a single profiling event to update the call stack tree. + + Args: + sample: a single profiling sample. + """ + pid = sample.sample_pid + try: + assert session + thread = session.find_thread(sample.sample_tid) + comm = thread.comm() + except Exception: + comm = f"unknown ({pid})" + + period = sample.sample_period + self.value += period + + node = self.find_or_create_node(comm) + node.value += period + + if sample.callchain: + for entry in reversed(sample.callchain): + name = entry.symbol + if not name or name == "[unknown]": + name = entry.dso or "unknown" + if entry.ip: + name += f" 0x{entry.ip:x}" + node = node.find_or_create_node(name) + node.value += period + else: + name = sample.symbol + if not name or name == "[unknown]": + name = sample.dso or "unknown" + if sample.sample_ip: + name += f" 0x{sample.sample_ip:x}" + node = node.find_or_create_node(name) + node.value += period + + def add_to_tree(self, node: TreeNode, root_value: int) -> None: + """Recursively adds this node and its children to a textual TreeNode. + + Args: + node (TreeNode): The textual `TreeNode` object to which this + ProfileNode should be added. + root_value (int): Value at the root of the tree. + """ + if root_value == 0: + root_value = self.value + + # Calculate the percentage for the node, highlighting the + # percentage with reversed colors. + if root_value != 0: + percent = self.value / root_value * 100 + label = f"{self.name} [r]{percent:.3g}%[/]" + else: + label = self.name + + # Add a standalone leaf. + if not self.children: + node.add_leaf(label) + return + + # Recursively add children. + new_node = node.add(label) + for pnode in sorted(self.children.values(), + key=lambda pnode: pnode.value, reverse=True): + pnode.add_to_tree(new_node, root_value) + + +class ReportApp(App): + """A Textual application to display profiling data.""" + + # The ^q binding is implied but having it here adds it in the Footer. + BINDINGS = [ + Binding(key="^q", action="quit", description="Quit", + tooltip="Quit the app"), + ] + + def __init__(self, root: ProfileNode): + """Initialize the application.""" + super().__init__() + self.root = root + + def make_report_tree(self) -> Tree: + """Make a Tree widget from the profile data.""" + tree: Tree[None] = Tree("Profile") + # Add events to tree skipping the root. + for pnode in sorted(self.root.children.values(), + key=lambda node: node.value, reverse=True): + pnode.add_to_tree(tree.root, root_value=0) + + # Expand the root tree (shows all events) and the largest of the children + # for each event. + def expand_first_child(tnode: TreeNode) -> None: + """Recursively expand the first child node""" + if not tnode.children: + return + first = tnode.children[0] + first.expand() + expand_first_child(first) + tree.root.expand() + for tnode in tree.root.children: + expand_first_child(tnode) + + # If there is only one event, expand it also. + if len(tree.root.children) == 1: + tree.root.children[0].expand() + + return tree + + def compose(self) -> ComposeResult: + """Composes the user interface of the application.""" + yield Header() + with TabbedContent(initial="report"): + with TabPane("Report", id="report"): + yield self.make_report_tree() + yield Footer() + + +class ProfileBuilder: + """Constructs a profile tree from a stream of events.""" + def __init__(self): + self.root = ProfileNode("root", parent=None) + + def process_event(self, sample) -> None: + """Called by session.process_events to update the profile tree.""" + ev_name = str(sample.evsel)[6:-1] + ev_root = self.root.find_or_create_node(ev_name) + ev_root.process_event(sample) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="TUI report using perf python module.") + parser.add_argument("-i", "--input", help="input perf.data file") + args = parser.parse_args() + + input_file = args.input or "perf.data" + if not os.path.exists(input_file): + print(f"Error: {input_file} not found. (try 'perf record' first)", file=sys.stderr) + sys.exit(1) + + profile = ProfileBuilder() + try: + session = perf.session(perf.data(input_file), sample=profile.process_event) + except Exception as e: + print(f"Error opening session: {e}", file=sys.stderr) + sys.exit(1) + + # profile.process_event is called for each perf event to build the profile. + session.process_events() + + # Visualize data. + app = ReportApp(profile.root) + app.run() From 96276ee99f81195bae4d6d7559774cbf41362584 Mon Sep 17 00:00:00 2001 From: Alice Rogers Date: Sat, 8 Aug 2026 22:18:19 -0700 Subject: [PATCH 056/425] perf python: Add flamegraph support to treport Implement a flamegraph widget that recursively walks down a tree splitting line segments based on their value (summed up periods across call chains). A visitor pattern is used so that the same logic can both draw the line segments and locate which segment had a mouse click. Add a tab for the flame graph widget. Tested-by: Arnaldo Carvalho de Melo Co-developed-by: Ian Rogers Signed-off-by: Ian Rogers Signed-off-by: Alice Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/treport.py | 356 ++++++++++++++++++++++++++++++++++- 1 file changed, 355 insertions(+), 1 deletion(-) diff --git a/tools/perf/python/treport.py b/tools/perf/python/treport.py index 71a91775bd6a..43542599a884 100755 --- a/tools/perf/python/treport.py +++ b/tools/perf/python/treport.py @@ -1,19 +1,49 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: MIT """treport.py - perf report like tool written using textual.""" +from abc import ABC, abstractmethod from typing import Dict, Optional import argparse import os import sys import perf +from rich.segment import Segment +from rich.style import Style +from textual import events from textual.app import App, ComposeResult from textual.binding import Binding +from textual.color import Color +from textual.scroll_view import ScrollView +from textual.strip import Strip from textual.widgets import Footer, Header, TabbedContent, TabPane, Tree from textual.widgets.tree import TreeNode # Global session. session :Optional[perf.session] = None +def make_fixed_length_string(s: str, length: int, pad_char=' '): + """Make the string s a fixed length. + + Increases or decreases the length of s to be length. If the length is + increased then pad_char is inserted on the right. + """ + return s[:length] if len(s) > length else s.ljust(length, pad_char) + + +class FlameVisitor(ABC): + """Parent for visitor used by ProfileNode.flame_walk""" + @abstractmethod + def visit(self, node: Optional["ProfileNode"], width: int) -> None: + """Visit a profile node width the specified flame graph width. + + Args: + node: The `ProfileNode` for the current segment. This may be `None` + to represent a gap or an unknown portion of the stack. + width: The calculated width of the flame graph rectangle for this + node, which is proportional to its sample count. + """ + + class ProfileNode: """Represents a single node in a call stack tree. @@ -120,6 +150,327 @@ class ProfileNode: key=lambda pnode: pnode.value, reverse=True): pnode.add_to_tree(new_node, root_value) + def largest_child(self) -> "ProfileNode": + """Finds the child with the highest value (sample count).""" + if self.children: + return max(self.children.values(), key=lambda node: node.value) + return self + + def child_after(self, sought: "ProfileNode") -> "ProfileNode": + """Finds the next sibling after the given node, sorted by value.""" + found = False + for child in sorted(self.children.values(), key=lambda node: node.value, + reverse=True): + if child == sought: + found = True + elif found: + return child + return sought + + def child_before(self, sought: "ProfileNode") -> "ProfileNode": + """Finds the previous sibling before the given node, sorted by value.""" + last = None + for child in sorted(self.children.values(), key=lambda node: node.value, + reverse=True): + if child == sought: + return last if last else sought + last = child + return sought + + def has_parent(self, parent: "ProfileNode") -> bool: + """Checks if the parent node is an ancestor of this node.""" + p = self.parent + while True: + if p == parent: + return True + new_p = p.parent + if new_p == p: + break + p = new_p + return False + + def has_child(self, sought: "ProfileNode") -> bool: + """Checks if the sought node is a descendant of this node.""" + return sought.has_parent(self) + + def flame_walk(self, wanted_strip: int, cur_strip: int, parent_width: int, + selected: "ProfileNode", visitor: FlameVisitor) -> None: + """Recursively walks the tree to visit a single flame graph row. + + This method calculates the proportional width for each child + based on its value (sample count) relative to its parent. It + then invokes a `visitor` to process each segment of the flame + graph row. + + Args: + wanted_strip (int): The target depth (Y-axis) of the flame graph row + to generate. + cur_strip (int): The current depth of the traversal. + parent_width (int): The width of the parent of this node. + selected (ProfileNode): The currently selected node in the UI, used + to adjust rendering to highlight the + selected path. + visitor (FlameVisitor): A visitor object whose `visit` method is + called for each segment of the flame graph + row. + """ + if parent_width == 0: + return + + parent_selected = selected == self or self.has_parent(selected) + child_selected = not parent_selected and self.has_child(selected) + if not parent_selected and not child_selected: + # Branches of the tree with no node selected aren't drawn. + return + + # left_over is used to check for a gap after the children due + # to samples being in the parent. + left_over = parent_width + for child in sorted(self.children.values(), key=lambda node: node.value, + reverse=True): + if parent_selected: + if self.value: + desired_width = int((parent_width * child.value) / self.value) + else: + desired_width = parent_width // len(self.children) + if desired_width == 0: + # Nothing can be drawn for this node or later smaller children. + break + elif child == selected or child.has_child(selected): + desired_width = parent_width + else: + # A sibling or its child are selected, but not this branch. + continue + + # Either visit the wanted_strip or recurse to the next level. + if wanted_strip == cur_strip: + visitor.visit(child, desired_width) + else: + child.flame_walk(wanted_strip, cur_strip + 1, desired_width, + selected, visitor) + left_over -= desired_width + if left_over == 0: + # No space left to draw in. + break + + # Always visit the left_over regardless of the wanted_strip as there + # may be additional gap added to a line by a parent. + if left_over: + visitor.visit(None, left_over) + + def make_flame_strip(self, wanted_strip: int, parent_width: int, + cursor: "ProfileNode", selected: "ProfileNode", + theme_variables: Dict[str, str]) -> Strip: + """Creates a renderable 'Strip' for a single row of a flame graph. + + This method orchestrates the `flame_walk` traversal with a specialized + visitor to generate a list of segments. The segments are used by a`Strip` + object for rendering in the terminal. + + Args: + wanted_strip (int): The target depth (Y-axis) of the flame graph row. + parent_width (int): The total width (in characters) of the display + area. + cursor (ProfileNode): The node currently under the cursor, for + highlighting. + selected (ProfileNode): The node that is actively selected. + theme_variables(Dict): Values of colors for the textual theme. + + Returns: + Strip: A renderable strip of segments for the specified row. + """ + primary = Color.parse(theme_variables["primary"]) + secondary = Color.parse(theme_variables["secondary"]) + surface = Color.parse(theme_variables["surface"]) + def luminance(color: Color) -> float: + """Computes the luminance of a color from the rgb""" + return color.r * 0.299 + color.g * 0.587 + color.b * 0.114 + + # Set of styles for different flamegraph segments, the styles are + # cycled through to provide contrast. + normal_styles = [] + for x in range(0, 125, 25): + fgcolor = secondary.blend(primary, x/100) + if luminance(fgcolor) > luminance(surface): + bgcolor = surface.lighten(0.05+x/500) + else: + bgcolor = surface.darken(0.05+x/500) + normal_styles.append(Style(color=fgcolor.rich_color, + bgcolor=bgcolor.rich_color)) + + # Style for the selected flame graph node. + accent = Color.parse(theme_variables["accent"]) + accent_muted = Color.parse(theme_variables["accent-muted"]) + cursor_style = Style(color=accent.rich_color, bgcolor=accent_muted.rich_color) + + class StripVisitor(FlameVisitor): + """Visitor creating textual flame graph segments. + + Attributes: + segments (list): The textual segments that will be placed in a + `Strip`. + gap_width (int): The width of any outstanding gap between the + last and next node. + ctr (int): Used to adjust the flame graph segment's color. + """ + def __init__(self): + self.segments = [] + self.gap_width = 0 + self.ctr = wanted_strip + + def visit(self, node: Optional[ProfileNode], width: int) -> None: + if node: + if self.gap_width > 0: + self.segments.append(Segment( + make_fixed_length_string(" ", self.gap_width))) + self.gap_width = 0 + style = cursor_style + if node != cursor: + style = normal_styles[self.ctr % len(normal_styles)] + self.segments.append(Segment( + make_fixed_length_string(node.name, width), style)) + else: + self.gap_width += width + self.ctr += 1 + + visitor = StripVisitor() + self.flame_walk(wanted_strip, 0, parent_width, selected, visitor) + return Strip(visitor.segments) if visitor.segments else Strip.blank(parent_width) + + def find_node(self, sought_x: int, sought_y: int, parent_width: int, + selected: "ProfileNode") -> "ProfileNode": + """Finds the ProfileNode corresponding to specific X, Y coordinates. + + This translates a mouse click on a flame graph back to the + `ProfileNode` that it represents. + + Args: + sought_x (int): The X coordinate (character column). + sought_y (int): The Y coordinate (row or depth). + parent_width (int): The total width of the display area. + selected (ProfileNode): The currently selected node, which affects + layout. + + Returns: + Optional[ProfileNode]: The node found at the coordinates, or None. + + """ + class FindVisitor(FlameVisitor): + """Visitor locating a `ProfileNode`. + + Attributes: + x (int): offset within line. + found (Optional[ProfileNode]): located node + gap_width (int): The width of any outstanding gap between the + last and next node. + ctr (int): Used to adjust the flame graph segment's color. + """ + def __init__(self): + self.x = 0 + self.found = None + + def visit(self, node: Optional[ProfileNode], width: int) -> None: + if self.x <= sought_x < self.x + width: + self.found = node + self.x += width + + visitor = FindVisitor() + self.flame_walk(sought_y, 0, parent_width, selected, visitor) + return visitor.found + + +class FlameGraph(ScrollView): + """A scrollable widget to display a flame graph from a profile. + + Attributes: + root (ProfileNode): Root of the profile tree. + cursor (ProfileNode): Currently highlighted cursor node. + selected (ProfileNode): The currently selected node for zooming. + """ + + # Define key bindings for navigating the flame graph. + # Allows movement with vim-style keys (h,j,k,l) and arrow keys. + BINDINGS = [ + Binding("j,down", "move_down", "Down", key_display="↓", + tooltip="Move cursor down to largest child"), + Binding("k,up", "move_up", "Up", key_display="↑", + tooltip="Move cursor up to parent"), + Binding("l,right", "move_right", "Right", key_display="→", + tooltip="Move cursor to the right sibling"), + Binding("h,left", "move_left", "Left", key_display="←", + tooltip="Move cursor to the left sibling"), + Binding("enter", "zoom_in", "Zoom In", + tooltip="Expand the cursor's node to be screen width"), + Binding("escape", "zoom_out", "Zoom Out", + tooltip="Zoom out to initial view."), + ] + + # Default CSS for the widget to ensure it fills its container's width. + DEFAULT_CSS = """ + FlameGraph { + width: 100%; + } + """ + + def __init__(self, root: ProfileNode, *args, **kwargs): + """Initialize the FlameGraph widget.""" + super().__init__(*args, **kwargs) + self.root = root + self.cursor = root + self.selected = root + + def action_move_down(self) -> None: + """Handle key press down.""" + self.cursor = self.cursor.largest_child() + self.refresh() + + def action_move_up(self) -> None: + """Handle key press up.""" + if self.cursor.parent != self.cursor.parent.parent: + self.cursor = self.cursor.parent + self.refresh() + + def action_move_right(self) -> None: + """Handle key press right.""" + self.cursor = self.cursor.parent.child_after(self.cursor) + self.refresh() + + def action_move_left(self) -> None: + """Handle key press left.""" + self.cursor = self.cursor.parent.child_before(self.cursor) + self.refresh() + + def action_zoom_in(self) -> None: + """Handle key press zoom in.""" + self.selected = self.cursor + self.refresh() + + def action_zoom_out(self) -> None: + """Handle key press zoom out.""" + self.selected = self.root + self.refresh() + + def render_line(self, y: int) -> Strip: + """Render a single line (row) of the flame graph.""" + _, scroll_y = self.scroll_offset + y += scroll_y + return self.root.make_flame_strip(y, self.size.width, self.cursor, + self.selected, self.app.theme_variables) + + def on_mount(self) -> None: + """Set the height of the widget when it is displayed.""" + self.styles.height = self.root.depth() + + def on_click(self, click: events.Click) -> None: + """Handles a mouse click and update the cursor position.""" + _, scroll_y = self.scroll_offset + y = scroll_y + click.y + clicked_node = self.root.find_node(click.x, y, self.size.width, + self.selected) + if clicked_node: + self.cursor = clicked_node + self.refresh() + class ReportApp(App): """A Textual application to display profiling data.""" @@ -168,6 +519,8 @@ class ReportApp(App): with TabbedContent(initial="report"): with TabPane("Report", id="report"): yield self.make_report_tree() + with TabPane("Flame Graph", id="flame"): + yield FlameGraph(self.root) yield Footer() @@ -183,7 +536,8 @@ class ProfileBuilder: ev_root.process_event(sample) if __name__ == "__main__": - parser = argparse.ArgumentParser(description="TUI report using perf python module.") + parser = argparse.ArgumentParser( + description="TUI report and flame graph using perf python module.") parser.add_argument("-i", "--input", help="input perf.data file") args = parser.parse_args() From 9d527dc79e6bac6b984f53c2e9a64ab5412ca725 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 9 Sep 2026 15:20:48 -0700 Subject: [PATCH 057/425] perf test x86: Fix missing __msan_unpoison __msan_unpoison may need the sanitizer header including for its definition. Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/arch/x86/tests/dwarf-unwind.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/perf/arch/x86/tests/dwarf-unwind.c b/tools/perf/arch/x86/tests/dwarf-unwind.c index 99d2b7ed016f..3c66dadcab75 100644 --- a/tools/perf/arch/x86/tests/dwarf-unwind.c +++ b/tools/perf/arch/x86/tests/dwarf-unwind.c @@ -8,6 +8,10 @@ #include "debug.h" #include "tests/tests.h" +#if defined(MEMORY_SANITIZER) && !defined(__msan_unpoison) +# include +#endif + #define STACK_SIZE 8192 static int sample_ustack(struct perf_sample *sample, From c5429ac2d1f03b91dcb3dd8ba0a970eac433414f Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 10 Sep 2026 14:39:07 -0700 Subject: [PATCH 058/425] tools build: Fix timerfd feature detection with clang Clang's uninitialized-const-pointer warning causes a feature test to fail when building perf with clang. This disables support for 'perf kvm stat live'. Add default initialization to new_value as a workaround. test-timerfd.c:15:30: error: variable 'new_value' is uninitialized when passed as a const pointer argument here [-Werror,-Wuninitialized-const-pointer] 15 | if (timerfd_settime(fd, 0, &new_value, NULL) != 0) | ^~~~~~~~~ 1 error generated. Signed-off-by: Ian Rogers Link: https://lore.kernel.org/r/20260910213907.3206529-1-irogers@google.com Signed-off-by: Arnaldo Carvalho de Melo --- tools/build/feature/test-timerfd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/build/feature/test-timerfd.c b/tools/build/feature/test-timerfd.c index 9c72c697a9df..f88e8cc63ef3 100644 --- a/tools/build/feature/test-timerfd.c +++ b/tools/build/feature/test-timerfd.c @@ -6,7 +6,7 @@ int main(void) { - struct itimerspec new_value; + struct itimerspec new_value = {}; int fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); if (fd < 0) From dd93c3b939f17d4c8d9c3802a88eb6ca357b7cfe Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 10 Sep 2026 14:15:39 -0700 Subject: [PATCH 059/425] perf python: Fix ilist TreeNode string type drift Updates Tree type hints to correctly use TreeNode[TreeValue] across all assignments preventing mypy strictness mismatches. Address some other minor stylistic issues reported by mypy, pylint and Sashiko. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/ilist.py | 78 ++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/tools/perf/python/ilist.py b/tools/perf/python/ilist.py index 0d757ddb4795..ebff0a843b7a 100755 --- a/tools/perf/python/ilist.py +++ b/tools/perf/python/ilist.py @@ -2,11 +2,13 @@ # SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) """Interactive perf list.""" -from abc import ABC, abstractmethod +from __future__ import annotations + import argparse +from abc import ABC, abstractmethod from dataclasses import dataclass import math -from typing import Any, Dict, Optional, Tuple +from typing import Any, Optional import perf from textual import on from textual.app import App, ComposeResult @@ -19,7 +21,7 @@ from textual.widgets import Button, Footer, Header, Input, Label, Sparkline, Sta from textual.widgets.tree import TreeNode -def get_info(info: Dict[str, str], key: str): +def get_info(info: dict[str, Any], key: str): return (info[key] + "\n") if key in info else "" @@ -140,7 +142,7 @@ class ErrorScreen(ModalScreen[bool]): def compose(self) -> ComposeResult: yield Button(f"Error: {self.error}", variant="primary", id="error") - def on_button_pressed(self, event: Button.Pressed) -> None: + def on_button_pressed(self) -> None: self.dismiss(True) @@ -234,7 +236,7 @@ class IListApp(App): def __init__(self, interval: float) -> None: self.interval = interval - self.evlist = None + self.evlist: Optional[perf.evlist] = None self.selected: Optional[TreeValue] = None self.search_results: list[TreeNode[TreeValue]] = [] self.cur_search_result: TreeNode[TreeValue] | None = None @@ -284,18 +286,18 @@ class IListApp(App): """Sets the focus after the SearchScreen is dismissed.""" search_label = self.query_one("#active_search", Label) - search_label.display = True if event else False + search_label.display = bool(event) if not event: return event = event.lower() search_label.update(f'Searching for events matching "{event}"') - tree: Tree[str] = self.query_one("#root", Tree) + tree: Tree[TreeValue] = self.query_one("#root", Tree) - def find_search_results(event: str, node: TreeNode[str], + def find_search_results(event: str, node: TreeNode[TreeValue], cursor_seen: bool = False, - match_after_cursor: Optional[TreeNode[str]] = None - ) -> Tuple[bool, Optional[TreeNode[str]]]: + match_after_cursor: Optional[TreeNode[TreeValue]] = None + ) -> tuple[bool, Optional[TreeNode[TreeValue]]]: """Find nodes that match the search remembering the one after the cursor.""" if not cursor_seen and node == tree.cursor_node: cursor_seen = True @@ -332,7 +334,7 @@ class IListApp(App): def action_collapse(self) -> None: """Collapse the part of the tree currently on.""" - tree: Tree[str] = self.query_one("#root", Tree) + tree: Tree[TreeValue] = self.query_one("#root", Tree) node = tree.cursor_node if node and node.parent: node.parent.collapse_all() @@ -343,31 +345,34 @@ class IListApp(App): if not self.selected or not self.evlist: return - def update_count(cpu: int, count: int): + def update_count(cpu: int, count: int | float): # Update the raw count display. - counter: Label = self.query(f"#counter_cpu{cpu}" if cpu >= 0 else "#counter_total") - if not counter: + counter_query = self.query(f"#counter_cpu{cpu}" if cpu >= 0 else "#counter_total") + if not counter_query: return - counter = counter.first(Label) + counter: Label = counter_query.first(Label) counter.update(str(count)) # Update the sparkline. - line: Sparkline = self.query(f"#sparkline_cpu{cpu}" if cpu >= 0 else "#sparkline_total") - if not line: + line_query = self.query(f"#sparkline_cpu{cpu}" if cpu >= 0 else "#sparkline_total") + if not line_query: return - line = line.first(Sparkline) + line: Sparkline = line_query.first(Sparkline) # If there are more events than the width, remove the front event. - if len(line.data) > line.size.width: - line.data.pop(0) - line.data.append(count) - line.mutate_reactive(Sparkline.data) + if line.data is not None: + if len(line.data) > line.size.width: + line.data = line.data[1:] + line.data = list(line.data) + [float(count)] + else: + line.data = [float(count)] + line.refresh() # Update the total and each CPU counts, assume there's just 1 evsel. - total = 0 + total: float = 0.0 self.evlist.disable() for evsel in self.evlist: for cpu in evsel.cpus(): - aggr = 0 + aggr: float = 0.0 for thread in evsel.threads(): aggr += self.selected.value(self.evlist, evsel, cpu, thread) update_count(cpu, aggr) @@ -424,16 +429,16 @@ class IListApp(App): # Add spark lines for all the CPUs. Note, must be done after # open so that the evlist CPUs have been computed by propagate # maps. - line = CounterSparkline(cpu=-1) - lines.mount(line) + line_sp = CounterSparkline(cpu=-1) + lines.mount(line_sp) for cpu in self.evlist.all_cpus(): - line = CounterSparkline(cpu) - lines.mount(line) - line = Counter(cpu=-1) - lines.mount(line) + c_sp = CounterSparkline(cpu) + lines.mount(c_sp) + c_val = Counter(cpu=-1) + lines.mount(c_val) for cpu in self.evlist.all_cpus(): - line = Counter(cpu) - lines.mount(line) + c_val2 = Counter(cpu) + lines.mount(c_val2) def compose(self) -> ComposeResult: """Draws the app.""" @@ -459,17 +464,18 @@ class IListApp(App): # Reading events may fail with EPERM, ignore. pass metrics = tree.root.add("Metrics") - groups = set() + groups: set[str] = set() for metric in perf.metrics(): groups.update(metric["MetricGroup"]) - def add_metrics_to_tree(node: TreeNode[TreeValue], parent: str, pmu: str = None): + def add_metrics_to_tree(node: TreeNode[TreeValue], parent: str, pmu: Optional[str] = None): for metric in sorted(perf.metrics(), key=lambda x: x["MetricName"]): - metric_pmu = metric.get('PMU') + metric_pmu_raw = metric.get('PMU') + metric_pmu = str(metric_pmu_raw) if metric_pmu_raw else '' if pmu and metric_pmu and metric_pmu != pmu: continue if parent in metric["MetricGroup"]: - name = metric["MetricName"] + name = str(metric["MetricName"]) display_name = name if metric_pmu: display_name += f" ({metric_pmu})" From 811d1ddf976048e85a41c4a5234ccf73f23b3050 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 10 Sep 2026 14:12:18 -0700 Subject: [PATCH 060/425] perf header: Fix HEADER_COMPRESSED validation for comp_mmap_len This commit updates process_compressed() to ignore comp_mmap_len == 0 when validating the compressed header. A zero size is valid and may occur during testing. Sashiko review flagged string.h was missing (preexisting problem) and so I opportunistically fixed this and sorted the header files. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/header.c | 85 +++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/tools/perf/util/header.c b/tools/perf/util/header.c index 7db7da090a1e..974ce1c926cd 100644 --- a/tools/perf/util/header.c +++ b/tools/perf/util/header.c @@ -1,65 +1,69 @@ // SPDX-License-Identifier: GPL-2.0 +#include "header.h" + #include #include #include -#include "string2.h" -#include -#include -#include -#include -#include #include #include -#include -#include -#include +#include + +#include +#include +#include #include +#include +#include +#include +#include #include #include -#include -#include -#include #include -#include -#ifdef HAVE_LIBBPF_SUPPORT -#include -#endif +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include #include #include // reallocarray +#include "../perf.h" +#include "bpf-event.h" +#include "bpf-utils.h" +#include "build-id.h" +#include "cacheline.h" +#include "clockid.h" +#include "cpumap.h" +#include "cputopo.h" +#include "data.h" +#include "debug.h" #include "dso.h" #include "evlist.h" #include "evsel.h" -#include "util/evsel_fprintf.h" -#include "header.h" +#include "evsel_fprintf.h" #include "memswap.h" -#include "trace-event.h" -#include "session.h" -#include "symbol.h" -#include "debug.h" -#include "cpumap.h" #include "pmu.h" #include "pmus.h" -#include "vdso.h" +#include "session.h" #include "strbuf.h" -#include "build-id.h" -#include "data.h" -#include -#include -#include "asm/bug.h" -#include "tool.h" -#include "../perf.h" +#include "string2.h" +#include "symbol.h" #include "time-utils.h" +#include "tool.h" +#include "trace-event.h" #include "units.h" -#include "util/util.h" // perf_exe() -#include "cputopo.h" -#include "bpf-event.h" -#include "bpf-utils.h" -#include "clockid.h" -#include "cacheline.h" +#include "util.h" // perf_exe() +#include "vdso.h" -#include -#include +#ifdef HAVE_LIBBPF_SUPPORT +#include +#endif #ifdef HAVE_LIBTRACEEVENT #include @@ -3903,7 +3907,8 @@ static int process_compressed(struct feat_fd *ff, * checks decomp_len + sizeof(struct decomp) against SIZE_MAX * before allocating, which handles 32-bit safety. */ - if (env->comp_mmap_len < 4096 || env->comp_mmap_len % 4096) { + if (env->comp_mmap_len && + (env->comp_mmap_len < 4096 || env->comp_mmap_len % 4096)) { pr_err("Invalid HEADER_COMPRESSED: comp_mmap_len (%u) must be a 4K-aligned value >= 4096\n", env->comp_mmap_len); return -1; From d964ffba01f5d0935dd6bc850029958a62c74491 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 10 Sep 2026 14:12:19 -0700 Subject: [PATCH 061/425] perf header: Fix potential memory corruption in cpu_cache_level__read strim() may advance the pointer assigned to cache->size which causes later frees to crash. Fix by performing the strim and then memmove-ing the potentially shifted string back over the original string. The bug was introduced by the transition from rtrim() to strim(), as rtrim() wouldn't move on the left. Fixes: 13c230ab6e56c6ae ("perf tools: Ditch rtrim(), use strim() from tools/lib") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/header.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tools/perf/util/header.c b/tools/perf/util/header.c index 974ce1c926cd..0a4236fc0293 100644 --- a/tools/perf/util/header.c +++ b/tools/perf/util/header.c @@ -1268,7 +1268,11 @@ static int cpu_cache_level__read(struct cpu_cache_level *cache, u32 cpu, u16 lev return -1; cache->type[len] = 0; - cache->type = strim(cache->type); + { + char *trimmed = strim(cache->type); + + memmove(cache->type, trimmed, strlen(trimmed) + 1); + } scnprintf(file, PATH_MAX, "%s/size", path); if (sysfs__read_str(file, &cache->size, &len)) { @@ -1277,7 +1281,11 @@ static int cpu_cache_level__read(struct cpu_cache_level *cache, u32 cpu, u16 lev } cache->size[len] = 0; - cache->size = strim(cache->size); + { + char *trimmed = strim(cache->size); + + memmove(cache->size, trimmed, strlen(trimmed) + 1); + } scnprintf(file, PATH_MAX, "%s/shared_cpu_list", path); if (sysfs__read_str(file, &cache->map, &len)) { @@ -1287,7 +1295,11 @@ static int cpu_cache_level__read(struct cpu_cache_level *cache, u32 cpu, u16 lev } cache->map[len] = 0; - cache->map = strim(cache->map); + { + char *trimmed = strim(cache->map); + + memmove(cache->map, trimmed, strlen(trimmed) + 1); + } return 0; } From f945859da4430396a97ec6bc01769460e865ad20 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 10 Sep 2026 14:12:20 -0700 Subject: [PATCH 062/425] perf header: Transition WARN macros to debug.h equivalents Avoid a checkpatch.pl warning on the use of asm/bug.h by switching the use of WARN macros to explicit tests and then using pr_warning from debug.h. Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/header.c | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tools/perf/util/header.c b/tools/perf/util/header.c index 0a4236fc0293..c7f2c29f02b0 100644 --- a/tools/perf/util/header.c +++ b/tools/perf/util/header.c @@ -8,7 +8,6 @@ #include #include -#include #include #include #include @@ -385,8 +384,10 @@ static int do_read_bitmap(struct feat_fd *ff, unsigned long **pset, u64 *psize) static int write_tracing_data(struct feat_fd *ff, struct evlist *evlist __maybe_unused) { - if (WARN(ff->buf, "Error: calling %s in pipe-mode.\n", __func__)) + if (ff->buf) { + pr_warning("Error: calling %s in pipe-mode.\n", __func__); return -1; + } #ifdef HAVE_LIBTRACEEVENT return read_tracing_data(ff->fd, &evlist__core(evlist)->entries); @@ -407,8 +408,10 @@ static int write_build_id(struct feat_fd *ff, if (!perf_session__read_build_ids(session, true)) return -1; - if (WARN(ff->buf, "Error: calling %s in pipe-mode.\n", __func__)) + if (ff->buf) { + pr_warning("Error: calling %s in pipe-mode.\n", __func__); return -1; + } err = perf_session__write_buildid_table(session, ff); if (err < 0) { @@ -1014,8 +1017,10 @@ static int write_auxtrace(struct feat_fd *ff, struct perf_session *session; int err; - if (WARN(ff->buf, "Error: calling %s in pipe-mode.\n", __func__)) + if (ff->buf) { + pr_warning("Error: calling %s in pipe-mode.\n", __func__); return -1; + } session = container_of(ff->ph, struct perf_session, header); @@ -1109,9 +1114,10 @@ static int write_dir_format(struct feat_fd *ff, session = container_of(ff->ph, struct perf_session, header); data = session->data; - if (WARN_ON(!perf_data__is_dir(data))) + if (!perf_data__is_dir(data)) { + pr_warning("Expected data to be a directory\n"); return -1; - + } return do_write(ff, &data->dir.version, sizeof(data->dir.version)); } @@ -3715,9 +3721,10 @@ static int process_dir_format(struct feat_fd *ff, session = container_of(ff->ph, struct perf_session, header); data = session->data; - if (WARN_ON(!perf_data__is_dir(data))) + if (!perf_data__is_dir(data)) { + pr_warning("Expected data to be a directory\n"); return -1; - + } return do_read_u64(ff, &data->dir.version); } @@ -4395,8 +4402,10 @@ static int do_write_feat(struct feat_fd *ff, int type, if (!feat_ops[type].write) return -1; - if (WARN(ff->buf, "Error: calling %s in pipe-mode.\n", __func__)) + if (ff->buf) { + pr_warning("Error: calling %s in pipe-mode.\n", __func__); return -1; + } (*p)->offset = lseek(ff->fd, 0, SEEK_CUR); From 1f065103499e1572fafb4c607c52bd5c62faac20 Mon Sep 17 00:00:00 2001 From: Qerogram Date: Thu, 10 Sep 2026 21:38:39 +0900 Subject: [PATCH 063/425] perf list: Avoid repeated strlen() when escaping JSON strings The %S conversion in fix_escape_fprintf() calls strlen(s) in the loop condition while escaping each character. GCC 14.2.0 retains this call inside the loop in a normal DEBUG=0 (-O3) perf build, repeatedly scanning the whole string. Test the current character for NUL instead. This processes the same bytes without rescanning the string. Escaping and buffer growth are unchanged. Full-command wall-clock measurements on an Apple M5 Pro host running an aarch64 Linux container with GCC 14.2.0 and glibc 2.41 gave these medians: perf list --json metrics Before (ms) After (ms) Default catalog 0.5969 0.5855 PERF_CPUID=0x00000000410fd830 0.7818 0.7033 Each workload used 50 randomized before/after pairs. Each sample averages the time per invocation across five fresh processes, with stdout sent to /dev/null and no single-CPU pinning. PERF_CPUID selects the shipped V3 catalog on the same host; these are not measurements on V3 hardware. The measurements include process startup and do not establish a gain on other machines or perf commands. These observations come from one short run. Relinking also changes code layout: .text grows from 2,013,172 to 2,017,236 bytes in this build. The timings do not isolate scan removal from the changed layout. The existing list shell test passes for both binaries. Source-extracted functions linked to perf's strbuf implementation pass 906 equivalence cases with ASan and UBSan. Full-command outputs are byte-identical. Codex assisted with identifying the redundant scan, preparing the change, and running the comparisons. Assisted-by: LLM Signed-off-by: Qerogram Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-list.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/builtin-list.c b/tools/perf/builtin-list.c index 50f69c2c0d51..4118424cb364 100644 --- a/tools/perf/builtin-list.c +++ b/tools/perf/builtin-list.c @@ -325,7 +325,7 @@ static void fix_escape_fprintf(FILE *fp, struct strbuf *buf, const char *fmt, .. case 'S': { const char *s = va_arg(args, const char*); - for (size_t s_pos = 0; s_pos < strlen(s); s_pos++) { + for (size_t s_pos = 0; s[s_pos] != '\0'; s_pos++) { switch (s[s_pos]) { case '\n': strbuf_addstr(buf, "\\n"); From 5a8e536e5f698f36250edd8a3c0f14056fd8a719 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 9 Sep 2026 15:04:45 -0700 Subject: [PATCH 064/425] perf jevents: Add mesh bandwidth saturation for CWF, EMR, GRR, SNR, SPR, SRF Extend the mesh bandwidth saturation's list of events to include events from clearwaterforest, emeraldrapids, grandridge, graniterapids, sapphirerapids and sierraforest. This makes the lpm_mem_sat metric available for those CPU models. Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/pmu-events/intel_metrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/perf/pmu-events/intel_metrics.py b/tools/perf/pmu-events/intel_metrics.py index bc2b920d3a0d..9780bf978a41 100755 --- a/tools/perf/pmu-events/intel_metrics.py +++ b/tools/perf/pmu-events/intel_metrics.py @@ -1099,7 +1099,8 @@ def UncoreMemSat() -> Optional[Metric]: try: clocks = Event("UNC_CHA_CLOCKTICKS", "UNC_C_CLOCKTICKS") sat = Event("UNC_CHA_DISTRESS_ASSERTED.VERT", "UNC_CHA_FAST_ASSERTED.VERT", - "UNC_C_FAST_ASSERTED") + "UNC_C_FAST_ASSERTED", "UNC_CHA_DISTRESS_ASSERTED.DPT_ANY", + "UNC_CHA_DISTRESS_ASSERTED.DPT_NONLOCAL") except: return None From 3792e2922d1b11cd9a7d3c34743a84eddf49a885 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:40 +0800 Subject: [PATCH 065/425] perf dwarf-regs: Fix DWARF register index bounds check Tighten bounds validation in __get_dwarf_regnum_for_perf_regnum_xxx() helpers by changing perf_regnum > ARRAY_SIZE() to perf_regnum >= ARRAY_SIZE(). This fixes an off-by-one condition where perf_regnum == ARRAY_SIZE() could pass validation and cause out-of-bounds access for dwarf_xxx_regnums[]. Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/dwarf-regs-arch/dwarf-regs-csky.c | 2 +- tools/perf/util/dwarf-regs-arch/dwarf-regs-powerpc.c | 2 +- tools/perf/util/dwarf-regs-arch/dwarf-regs-s390.c | 2 +- tools/perf/util/dwarf-regs-arch/dwarf-regs-x86.c | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/perf/util/dwarf-regs-arch/dwarf-regs-csky.c b/tools/perf/util/dwarf-regs-arch/dwarf-regs-csky.c index cb44b774f8d9..7f976aa3cfa2 100644 --- a/tools/perf/util/dwarf-regs-arch/dwarf-regs-csky.c +++ b/tools/perf/util/dwarf-regs-arch/dwarf-regs-csky.c @@ -118,7 +118,7 @@ int __get_dwarf_regnum_for_perf_regnum_csky(int perf_regnum, unsigned int flags) if (flags & EF_CSKY_ABIV2) idx++; - if (perf_regnum < 0 || perf_regnum > (int)ARRAY_SIZE(dwarf_csky_regnums) || + if (perf_regnum < 0 || perf_regnum >= (int)ARRAY_SIZE(dwarf_csky_regnums) || dwarf_csky_regnums[perf_regnum][idx] == 0) return -ENOENT; diff --git a/tools/perf/util/dwarf-regs-arch/dwarf-regs-powerpc.c b/tools/perf/util/dwarf-regs-arch/dwarf-regs-powerpc.c index 51892a09725b..98e7f2b2791c 100644 --- a/tools/perf/util/dwarf-regs-arch/dwarf-regs-powerpc.c +++ b/tools/perf/util/dwarf-regs-arch/dwarf-regs-powerpc.c @@ -128,7 +128,7 @@ int __get_dwarf_regnum_for_perf_regnum_powerpc(int perf_regnum) if (perf_regnum == 0) return 0; - if (perf_regnum < 0 || perf_regnum > (int)ARRAY_SIZE(dwarf_powerpc_regnums) || + if (perf_regnum < 0 || perf_regnum >= (int)ARRAY_SIZE(dwarf_powerpc_regnums) || dwarf_powerpc_regnums[perf_regnum] == 0) return -ENOENT; diff --git a/tools/perf/util/dwarf-regs-arch/dwarf-regs-s390.c b/tools/perf/util/dwarf-regs-arch/dwarf-regs-s390.c index 310a37451bdc..9cef846ef9c0 100644 --- a/tools/perf/util/dwarf-regs-arch/dwarf-regs-s390.c +++ b/tools/perf/util/dwarf-regs-arch/dwarf-regs-s390.c @@ -45,7 +45,7 @@ int __get_dwarf_regnum_for_perf_regnum_s390(int perf_regnum) if (perf_regnum == 0) return 0; - if (perf_regnum < 0 || perf_regnum > (int)ARRAY_SIZE(dwarf_s390_regnums) || + if (perf_regnum < 0 || perf_regnum >= (int)ARRAY_SIZE(dwarf_s390_regnums) || dwarf_s390_regnums[perf_regnum] == 0) return -ENOENT; diff --git a/tools/perf/util/dwarf-regs-arch/dwarf-regs-x86.c b/tools/perf/util/dwarf-regs-arch/dwarf-regs-x86.c index cadef120aeb4..c2a70e03b1a9 100644 --- a/tools/perf/util/dwarf-regs-arch/dwarf-regs-x86.c +++ b/tools/perf/util/dwarf-regs-arch/dwarf-regs-x86.c @@ -197,7 +197,7 @@ int __get_dwarf_regnum_for_perf_regnum_i386(int perf_regnum) if (perf_regnum == 0) return 0; - if (perf_regnum < 0 || perf_regnum > (int)ARRAY_SIZE(dwarf_i386_regnums) || + if (perf_regnum < 0 || perf_regnum >= (int)ARRAY_SIZE(dwarf_i386_regnums) || dwarf_i386_regnums[perf_regnum] == 0) return -ENOENT; @@ -252,7 +252,7 @@ int __get_dwarf_regnum_for_perf_regnum_x86_64(int perf_regnum) if (perf_regnum == 0) return 0; - if (perf_regnum < 0 || perf_regnum > (int)ARRAY_SIZE(dwarf_x86_64_regnums) || + if (perf_regnum < 0 || perf_regnum >= (int)ARRAY_SIZE(dwarf_x86_64_regnums) || dwarf_x86_64_regnums[perf_regnum] == 0) return -ENOENT; From 2b7e273af0cdd6242d7dd2827fe79918d1558f94 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:41 +0800 Subject: [PATCH 066/425] perf util: Add missed fields in _attr__fprintf() and __attr_swap() Some new introduced perf_event_attr fields, like config3 and config4, are forgotten to add into perf_event_attr__fprintf() and perf_event__attr_swap() helpers. Add these missed fields into these 2 helpers. The aux_action is an exception. aux_action aliases bitfields (e.g. aux_start_paused) in a union. Bitfields and plain u32 fields follow different endian rules, so both views cannot be swapped correctly at the same time. Leave aux_action unswapped until the ABI is represented in a byte-order-safe form. Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/perf_event_attr_fprintf.c | 4 ++++ tools/perf/util/session.c | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/tools/perf/util/perf_event_attr_fprintf.c b/tools/perf/util/perf_event_attr_fprintf.c index 3933639d76c5..cc817294ad3d 100644 --- a/tools/perf/util/perf_event_attr_fprintf.c +++ b/tools/perf/util/perf_event_attr_fprintf.c @@ -405,6 +405,10 @@ int perf_event_attr__fprintf(FILE *fp, struct perf_event_attr *attr, PRINT_ATTRf_bf(aux_pause, p_unsigned); PRINT_ATTRf_bf(aux_resume, p_unsigned); } + if (attr_size >= offsetof(struct perf_event_attr, config4)) + PRINT_ATTRf(config3, p_hex); + if (attr_size > offsetof(struct perf_event_attr, config4)) + PRINT_ATTRf(config4, p_hex); return ret; } diff --git a/tools/perf/util/session.c b/tools/perf/util/session.c index 3237870a1a34..212506aa8baa 100644 --- a/tools/perf/util/session.c +++ b/tools/perf/util/session.c @@ -629,6 +629,16 @@ do { \ bswap_field_32(aux_watermark); bswap_field_16(sample_max_stack); bswap_field_32(aux_sample_size); + /* + * aux_action aliases bitfields (e.g. aux_start_paused) in a union. + * Bitfields and plain u32 fields follow different endian rules, so + * both views cannot be swapped correctly at the same time. + * Leave aux_action unswapped until the ABI is represented in a + * byte-order-safe form. + */ + bswap_field_64(sig_data); + bswap_field_64(config3); + bswap_field_64(config4); /* * After read_format are bitfields. Check read_format because From 7846bf499da3fe5da834630cab693955fe147d66 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:42 +0800 Subject: [PATCH 067/425] tools headers: Sync x86 headers with kernel sources Sync newly introduced ARCH_PEBS_VECR_xxx macros from kernel. Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/arch/x86/include/asm/msr-index.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/arch/x86/include/asm/msr-index.h b/tools/arch/x86/include/asm/msr-index.h index 18c4be75e927..9c26d4075c5f 100644 --- a/tools/arch/x86/include/asm/msr-index.h +++ b/tools/arch/x86/include/asm/msr-index.h @@ -350,6 +350,13 @@ #define ARCH_PEBS_LBR_SHIFT 40 #define ARCH_PEBS_LBR (0x3ull << ARCH_PEBS_LBR_SHIFT) #define ARCH_PEBS_VECR_XMM BIT_ULL(49) +#define ARCH_PEBS_VECR_YMMH BIT_ULL(50) +#define ARCH_PEBS_VECR_EGPRS BIT_ULL(51) +#define ARCH_PEBS_VECR_OPMASK BIT_ULL(53) +#define ARCH_PEBS_VECR_ZMMH BIT_ULL(54) +#define ARCH_PEBS_VECR_H16ZMM BIT_ULL(55) +#define ARCH_PEBS_VECR_EXT_SHIFT 49 +#define ARCH_PEBS_VECR_EXT (0x7full << ARCH_PEBS_VECR_EXT_SHIFT) #define ARCH_PEBS_GPR BIT_ULL(61) #define ARCH_PEBS_AUX BIT_ULL(62) #define ARCH_PEBS_EN BIT_ULL(63) From eb89aef367e4701038869e1ddfa719463524e3e3 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:43 +0800 Subject: [PATCH 068/425] perf headers: Sync perf_event.h/perf_regs.h with the kernel headers Sync the UAPI header changes of supporting SIMD/eGPRs/SSP sampling into corresponding tools UAPI headers. Additionally, support the new introduced perf_event_attr fields in the perf_event_attr__fprintf and perf_event__attr_swap() helpers, and add sanity check for the new introduced __reserved_4 field in perf_attr_check(). Co-developed-by: Kan Liang Signed-off-by: Kan Liang Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/arch/x86/include/uapi/asm/perf_regs.h | 53 +++++++++++++++++++++ tools/include/uapi/linux/perf_event.h | 49 +++++++++++++++++-- tools/perf/util/header.c | 3 +- tools/perf/util/perf_event_attr_fprintf.c | 11 ++++- tools/perf/util/session.c | 7 +++ 5 files changed, 117 insertions(+), 6 deletions(-) diff --git a/tools/arch/x86/include/uapi/asm/perf_regs.h b/tools/arch/x86/include/uapi/asm/perf_regs.h index 7c9d2bb3833b..faaa82df688d 100644 --- a/tools/arch/x86/include/uapi/asm/perf_regs.h +++ b/tools/arch/x86/include/uapi/asm/perf_regs.h @@ -2,6 +2,8 @@ #ifndef _ASM_X86_PERF_REGS_H #define _ASM_X86_PERF_REGS_H +#include + enum perf_event_x86_regs { PERF_REG_X86_AX, PERF_REG_X86_BX, @@ -27,9 +29,35 @@ enum perf_event_x86_regs { PERF_REG_X86_R13, PERF_REG_X86_R14, PERF_REG_X86_R15, + /* + * The eGPRs/SSP and XMM have overlaps. Only one can be used + * at a time. The ABI PERF_SAMPLE_REGS_ABI_SIMD is used to + * distinguish which one is used. If PERF_SAMPLE_REGS_ABI_SIMD + * is set, then eGPRs/SSP is used, otherwise, XMM is used. + * + * Extended GPRs (eGPRs) + */ + PERF_REG_X86_R16, + PERF_REG_X86_R17, + PERF_REG_X86_R18, + PERF_REG_X86_R19, + PERF_REG_X86_R20, + PERF_REG_X86_R21, + PERF_REG_X86_R22, + PERF_REG_X86_R23, + PERF_REG_X86_R24, + PERF_REG_X86_R25, + PERF_REG_X86_R26, + PERF_REG_X86_R27, + PERF_REG_X86_R28, + PERF_REG_X86_R29, + PERF_REG_X86_R30, + PERF_REG_X86_R31, + PERF_REG_X86_SSP, /* These are the limits for the GPRs. */ PERF_REG_X86_32_MAX = PERF_REG_X86_GS + 1, PERF_REG_X86_64_MAX = PERF_REG_X86_R15 + 1, + PERF_REG_MISC_MAX = PERF_REG_X86_SSP + 1, /* These all need two bits set because they are 128bit */ PERF_REG_X86_XMM0 = 32, @@ -54,5 +82,30 @@ enum perf_event_x86_regs { }; #define PERF_REG_EXTENDED_MASK (~((1ULL << PERF_REG_X86_XMM0) - 1)) +#define PERF_X86_EGPRS_MASK __GENMASK_ULL(PERF_REG_X86_R31, PERF_REG_X86_R16) + +enum { + PERF_X86_SIMD_XMM_REGS = 16, + PERF_X86_SIMD_YMM_REGS = 16, + PERF_X86_SIMD_ZMM_REGS = 32, + PERF_X86_SIMD_VEC_REGS_MAX = PERF_X86_SIMD_ZMM_REGS, + + PERF_X86_SIMD_OPMASK_REGS = 8, + PERF_X86_SIMD_PRED_REGS_MAX = PERF_X86_SIMD_OPMASK_REGS, +}; + +#define PERF_X86_SIMD_PRED_MASK __GENMASK(PERF_X86_SIMD_PRED_REGS_MAX - 1, 0) +#define PERF_X86_SIMD_VEC_MASK __GENMASK_ULL(PERF_X86_SIMD_VEC_REGS_MAX - 1, 0) + +#define PERF_X86_H16ZMM_BASE 16 + +enum { + /* 1 qword = 8 bytes */ + PERF_X86_OPMASK_QWORDS = 1, + PERF_X86_XMM_QWORDS = 2, + PERF_X86_YMM_QWORDS = 4, + PERF_X86_ZMM_QWORDS = 8, + PERF_X86_SIMD_QWORDS_MAX = PERF_X86_ZMM_QWORDS, +}; #endif /* _ASM_X86_PERF_REGS_H */ diff --git a/tools/include/uapi/linux/perf_event.h b/tools/include/uapi/linux/perf_event.h index fd10aa8d697f..c49fc76292f7 100644 --- a/tools/include/uapi/linux/perf_event.h +++ b/tools/include/uapi/linux/perf_event.h @@ -314,8 +314,9 @@ enum { */ enum perf_sample_regs_abi { PERF_SAMPLE_REGS_ABI_NONE = 0, - PERF_SAMPLE_REGS_ABI_32 = 1, - PERF_SAMPLE_REGS_ABI_64 = 2, + PERF_SAMPLE_REGS_ABI_32 = (1 << 0), + PERF_SAMPLE_REGS_ABI_64 = (1 << 1), + PERF_SAMPLE_REGS_ABI_SIMD = (1 << 2), }; /* @@ -383,6 +384,7 @@ enum perf_event_read_format { #define PERF_ATTR_SIZE_VER7 128 /* Add: sig_data */ #define PERF_ATTR_SIZE_VER8 136 /* Add: config3 */ #define PERF_ATTR_SIZE_VER9 144 /* add: config4 */ +#define PERF_ATTR_SIZE_VER10 176 /* Add: sample_simd_{vec|pred}_reg_* */ /* * 'struct perf_event_attr' contains various attributes that define @@ -547,6 +549,29 @@ struct perf_event_attr { __u64 config3; /* extension of config2 */ __u64 config4; /* extension of config3 */ + + /* + * Defines the sampling SIMD/PRED(predicate) registers bitmap and + * qwords (8 bytes) length. + * + * sample_simd_regs_enabled != 0 indicates there are SIMD/PRED + * registers to be sampled, the SIMD/PRED registers bitmap and + * qwords length are represented in + * sample_simd_{vec|pred}_reg_{intr|user} and + * sample_simd_{vec|pred}_reg_qwords fields separately. + * + * sample_simd_regs_enabled == 0 indicates no SIMD/PRED registers + * are sampled. + */ + __u16 sample_simd_regs_enabled; + __u16 sample_simd_pred_reg_qwords; + __u16 sample_simd_vec_reg_qwords; + __u16 __reserved_4; + + __u32 sample_simd_pred_reg_intr; + __u32 sample_simd_pred_reg_user; + __u64 sample_simd_vec_reg_intr; + __u64 sample_simd_vec_reg_user; }; /* @@ -1020,7 +1045,15 @@ enum perf_event_type { * } && PERF_SAMPLE_BRANCH_STACK * * { u64 abi; # enum perf_sample_regs_abi - * u64 regs[weight(mask)]; } && PERF_SAMPLE_REGS_USER + * u64 regs[weight(mask)]; + * struct { + * u64 nr_vectors; # 0 ... weight(sample_simd_vec_reg_user) + * u64 vector_qwords; # 0 ... sample_simd_vec_reg_qwords + * u64 nr_pred; # 0 ... weight(sample_simd_pred_reg_user) + * u64 pred_qwords; # 0 ... sample_simd_pred_reg_qwords + * u64 data[nr_vectors * vector_qwords + nr_pred * pred_qwords]; + * } && (abi & PERF_SAMPLE_REGS_ABI_SIMD) + * } && PERF_SAMPLE_REGS_USER * * { u64 size; * char data[size]; @@ -1047,7 +1080,15 @@ enum perf_event_type { * { u64 data_src; } && PERF_SAMPLE_DATA_SRC * { u64 transaction; } && PERF_SAMPLE_TRANSACTION * { u64 abi; # enum perf_sample_regs_abi - * u64 regs[weight(mask)]; } && PERF_SAMPLE_REGS_INTR + * u64 regs[weight(mask)]; + * struct { + * u64 nr_vectors; # 0 ... weight(sample_simd_vec_reg_intr) + * u64 vector_qwords; # 0 ... sample_simd_vec_reg_qwords + * u64 nr_pred; # 0 ... weight(sample_simd_pred_reg_intr) + * u64 pred_qwords; # 0 ... sample_simd_pred_reg_qwords + * u64 data[nr_vectors * vector_qwords + nr_pred * pred_qwords]; + * } && (abi & PERF_SAMPLE_REGS_ABI_SIMD) + * } && PERF_SAMPLE_REGS_INTR * { u64 phys_addr;} && PERF_SAMPLE_PHYS_ADDR * { u64 cgroup;} && PERF_SAMPLE_CGROUP * { u64 data_page_size;} && PERF_SAMPLE_DATA_PAGE_SIZE diff --git a/tools/perf/util/header.c b/tools/perf/util/header.c index c7f2c29f02b0..ec2935c0ed04 100644 --- a/tools/perf/util/header.c +++ b/tools/perf/util/header.c @@ -2185,7 +2185,8 @@ static void free_event_desc(struct evsel *events) static bool perf_attr_check(struct perf_event_attr *attr) { - if (attr->__reserved_1 || attr->__reserved_2 || attr->__reserved_3) { + if (attr->__reserved_1 || attr->__reserved_2 || + attr->__reserved_3 || attr->__reserved_4) { pr_warning("Reserved bits are set unexpectedly. " "Please update perf tool.\n"); return false; diff --git a/tools/perf/util/perf_event_attr_fprintf.c b/tools/perf/util/perf_event_attr_fprintf.c index cc817294ad3d..2e8be9a357dc 100644 --- a/tools/perf/util/perf_event_attr_fprintf.c +++ b/tools/perf/util/perf_event_attr_fprintf.c @@ -407,8 +407,17 @@ int perf_event_attr__fprintf(FILE *fp, struct perf_event_attr *attr, } if (attr_size >= offsetof(struct perf_event_attr, config4)) PRINT_ATTRf(config3, p_hex); - if (attr_size > offsetof(struct perf_event_attr, config4)) + if (attr_size >= offsetof(struct perf_event_attr, sample_simd_regs_enabled)) PRINT_ATTRf(config4, p_hex); + if (attr_size > offsetof(struct perf_event_attr, sample_simd_vec_reg_user)) { + PRINT_ATTRf(sample_simd_regs_enabled, p_unsigned); + PRINT_ATTRf(sample_simd_pred_reg_qwords, p_unsigned); + PRINT_ATTRf(sample_simd_pred_reg_intr, p_hex); + PRINT_ATTRf(sample_simd_pred_reg_user, p_hex); + PRINT_ATTRf(sample_simd_vec_reg_qwords, p_unsigned); + PRINT_ATTRf(sample_simd_vec_reg_intr, p_hex); + PRINT_ATTRf(sample_simd_vec_reg_user, p_hex); + } return ret; } diff --git a/tools/perf/util/session.c b/tools/perf/util/session.c index 212506aa8baa..c7ac0e7c5c04 100644 --- a/tools/perf/util/session.c +++ b/tools/perf/util/session.c @@ -639,6 +639,13 @@ do { \ bswap_field_64(sig_data); bswap_field_64(config3); bswap_field_64(config4); + bswap_field_16(sample_simd_regs_enabled); + bswap_field_16(sample_simd_pred_reg_qwords); + bswap_field_16(sample_simd_vec_reg_qwords); + bswap_field_32(sample_simd_pred_reg_intr); + bswap_field_32(sample_simd_pred_reg_user); + bswap_field_64(sample_simd_vec_reg_intr); + bswap_field_64(sample_simd_vec_reg_user); /* * After read_format are bitfields. Check read_format because From adc6aa9a289273d5ff9cf79cfac0bcf6ea997bd0 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:44 +0800 Subject: [PATCH 069/425] perf regs: Support x86 eGPRs/SSP sampling Add support for sampling x86 extended GP registers (R16-R31) and the shadow stack pointer (SSP) register. The original XMM registers space in sample_regs_user/sample_regs_intr is reclaimed to represent the eGPRs and SSP when SIMD registers sampling is supported with the new SIMD sampling fields in the perf_event_attr structure. This necessitates a way to distinguish which register layout is used for the sample_regs_user/sample_regs_intr bitmap. To address this, a new "abi" argument is added to the helpers perf_intr_reg_mask(), perf_user_reg_mask(), and perf_reg_name(). When "abi & PERF_SAMPLE_REGS_ABI_SIMD" is true, it indicates the eGPRs and SSP layout is represented; otherwise, the legacy XMM registers are represented. Please note that PERF_SAMPLE_REGS_ABI_SIMD is set by default on platforms that support SIMD register sampling, even when no eGPR or SSP register is requested (for example, -Iax). As a result, sample_regs_intr and sample_regs_usr always use the new GPR layout on platforms with SIMD register sampling support. The patch only supports eGPRs and SSP sampling, the complete SIMD registers sampling would be supported in the next patch. Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-script.c | 2 +- tools/perf/util/evsel.c | 27 +++- tools/perf/util/evsel.h | 7 + tools/perf/util/parse-regs-options.c | 35 +++-- .../perf/util/perf-regs-arch/perf_regs_x86.c | 124 +++++++++++++++--- tools/perf/util/perf_regs.c | 12 +- tools/perf/util/perf_regs.h | 10 +- tools/perf/util/record.h | 7 + .../scripting-engines/trace-event-python.c | 2 +- tools/perf/util/session.c | 13 +- tools/perf/util/synthetic-events.c | 4 + 11 files changed, 198 insertions(+), 45 deletions(-) diff --git a/tools/perf/builtin-script.c b/tools/perf/builtin-script.c index ad8ca08ceb5f..ec3ce841b33e 100644 --- a/tools/perf/builtin-script.c +++ b/tools/perf/builtin-script.c @@ -734,7 +734,7 @@ static int perf_sample__fprintf_regs(struct regs_dump *regs, uint64_t mask, for_each_set_bit(r, (unsigned long *) &mask, sizeof(mask) * 8) { u64 val = regs->regs[i++]; printed += fprintf(fp, "%5s:0x%"PRIx64" ", - perf_reg_name(r, e_machine, e_flags), + perf_reg_name(r, e_machine, e_flags, regs->abi), val); } diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index ce429eb247b6..bcd919c6bde8 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -1125,19 +1125,22 @@ static void __evsel__config_callchain(struct evsel *evsel, const struct record_o } if (param->record_mode == CALLCHAIN_DWARF) { + int abi = -1; /* -1 indicates only basic GPRs are needed. */ + if (!function) { uint16_t e_machine = evsel__e_machine(evsel, /*e_flags=*/NULL); evsel__set_sample_bit(evsel, REGS_USER); evsel__set_sample_bit(evsel, STACK_USER); if (opts->sample_user_regs && - DWARF_MINIMAL_REGS(e_machine) != perf_user_reg_mask(EM_HOST)) { + DWARF_MINIMAL_REGS(e_machine) != perf_user_reg_mask(EM_HOST, &abi)) { attr->sample_regs_user |= DWARF_MINIMAL_REGS(e_machine); pr_warning("WARNING: The use of --call-graph=dwarf may require all the user registers, " "specifying a subset with --user-regs may render DWARF unwinding unreliable, " "so the minimal registers set (IP, SP) is explicitly forced.\n"); } else { - attr->sample_regs_user |= perf_user_reg_mask(EM_HOST); + abi = -1; + attr->sample_regs_user |= perf_user_reg_mask(EM_HOST, &abi); } attr->sample_stack_user = param->dump_size; attr->exclude_callchain_user = 1; @@ -1663,12 +1666,18 @@ void evsel__config(struct evsel *evsel, const struct record_opts *opts, if (opts->sample_intr_regs && !evsel->no_aux_samples && !evsel__is_dummy_event(evsel)) { attr->sample_regs_intr = opts->sample_intr_regs; + attr->sample_simd_regs_enabled = + evsel__is_non_software_event(evsel) ? + !!opts->sample_simd_regs_enabled : 0; evsel__set_sample_bit(evsel, REGS_INTR); } if (opts->sample_user_regs && !evsel->no_aux_samples && !evsel__is_dummy_event(evsel)) { attr->sample_regs_user |= opts->sample_user_regs; + attr->sample_simd_regs_enabled = + evsel__is_non_software_event(evsel) ? + !!opts->sample_simd_regs_enabled : 0; evsel__set_sample_bit(evsel, REGS_USER); } @@ -3683,6 +3692,13 @@ int __evsel__parse_sample(struct evsel *evsel, union perf_event *event, regs->mask = mask; regs->regs = (u64 *)array; array = (void *)array + sz; + + if (regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) { + /* Skip SIMD-regs header. */ + sz = 4 * sizeof(u64); + OVERFLOW_CHECK(array, sz, max_size); + array = (void *)array + sz; + } } } @@ -3740,6 +3756,13 @@ int __evsel__parse_sample(struct evsel *evsel, union perf_event *event, regs->mask = mask; regs->regs = (u64 *)array; array = (void *)array + sz; + + if (regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) { + /* Skip SIMD-regs header. */ + sz = 4 * sizeof(u64); + OVERFLOW_CHECK(array, sz, max_size); + array = (void *)array + sz; + } } } diff --git a/tools/perf/util/evsel.h b/tools/perf/util/evsel.h index d9ecc6628217..6f759b0e86e9 100644 --- a/tools/perf/util/evsel.h +++ b/tools/perf/util/evsel.h @@ -587,6 +587,13 @@ static inline bool evsel__is_dummy_event(struct evsel *evsel) (evsel->core.attr.config == PERF_COUNT_SW_DUMMY); } +static inline bool evsel__is_non_software_event(struct evsel *evsel) +{ + return (evsel->core.attr.type != PERF_TYPE_SOFTWARE) && + (evsel->core.attr.type != PERF_TYPE_TRACEPOINT) && + (evsel->core.attr.type != PERF_TYPE_BREAKPOINT); +} + struct perf_session *evsel__session(struct evsel *evsel); struct perf_env *evsel__env(struct evsel *evsel); uint16_t evsel__e_machine(struct evsel *evsel, uint32_t *e_flags); diff --git a/tools/perf/util/parse-regs-options.c b/tools/perf/util/parse-regs-options.c index c93c2f0c8105..70a1cc90b2c1 100644 --- a/tools/perf/util/parse-regs-options.c +++ b/tools/perf/util/parse-regs-options.c @@ -6,11 +6,14 @@ #include #include "util/debug.h" #include +#include #include #include "util/perf_regs.h" #include "util/parse-regs-options.h" +#include "record.h" -static void list_perf_regs(FILE *fp, uint64_t mask) +static void +list_perf_regs(FILE *fp, uint64_t mask, int abi) { const char *last_name = NULL; @@ -21,7 +24,7 @@ static void list_perf_regs(FILE *fp, uint64_t mask) if (((1ULL << reg) & mask) == 0) continue; - name = perf_reg_name(reg, EM_HOST, EF_HOST); + name = perf_reg_name(reg, EM_HOST, EF_HOST, abi); if (name && (!last_name || strcmp(last_name, name))) fprintf(fp, "%s%s", reg > 0 ? " " : "", name); last_name = name; @@ -29,7 +32,8 @@ static void list_perf_regs(FILE *fp, uint64_t mask) fputc('\n', fp); } -static uint64_t name_to_perf_reg_mask(const char *to_match, uint64_t mask) +static uint64_t +name_to_perf_reg_mask(const char *to_match, uint64_t mask, int abi) { uint64_t reg_mask = 0; @@ -39,7 +43,7 @@ static uint64_t name_to_perf_reg_mask(const char *to_match, uint64_t mask) if (((1ULL << reg) & mask) == 0) continue; - name = perf_reg_name(reg, EM_HOST, EF_HOST); + name = perf_reg_name(reg, EM_HOST, EF_HOST, abi); if (!name) continue; @@ -53,9 +57,12 @@ static int __parse_regs(const struct option *opt, const char *str, int unset, bool intr) { uint64_t *mode = (uint64_t *)opt->value; + struct record_opts *opts; char *s, *os = NULL, *p; + const char *warn; int ret = -1; uint64_t mask; + int abi = 0; if (unset) return 0; @@ -66,11 +73,16 @@ __parse_regs(const struct option *opt, const char *str, int unset, bool intr) if (*mode) return -1; - mask = intr ? perf_intr_reg_mask(EM_HOST) : perf_user_reg_mask(EM_HOST); + mask = intr ? perf_intr_reg_mask(EM_HOST, &abi) : + perf_user_reg_mask(EM_HOST, &abi); + opts = intr ? container_of(opt->value, struct record_opts, sample_intr_regs) : + container_of(opt->value, struct record_opts, sample_user_regs); /* str may be NULL in case no arg is passed to -I */ if (!str) { *mode = mask; + if (abi & PERF_SAMPLE_REGS_ABI_SIMD) + opts->sample_simd_regs_enabled = 1; return 0; } @@ -79,6 +91,7 @@ __parse_regs(const struct option *opt, const char *str, int unset, bool intr) if (!s) return -1; + warn = "Unknown register \"%s\", check man page or run \"perf record %s?\"\n"; for (;;) { uint64_t reg_mask; @@ -87,14 +100,16 @@ __parse_regs(const struct option *opt, const char *str, int unset, bool intr) *p = '\0'; if (!strcmp(s, "?")) { - list_perf_regs(stderr, mask); + list_perf_regs(stderr, mask, abi); goto error; } - reg_mask = name_to_perf_reg_mask(s, mask); - if (reg_mask == 0) { - ui__warning("Unknown register \"%s\", check man page or run \"perf record %s?\"\n", - s, intr ? "-I" : "--user-regs="); + reg_mask = name_to_perf_reg_mask(s, mask, abi); + if (reg_mask) { + if (abi & PERF_SAMPLE_REGS_ABI_SIMD) + opts->sample_simd_regs_enabled = 1; + } else { + ui__warning(warn, s, intr ? "-I" : "--user-regs="); goto error; } *mode |= reg_mask; diff --git a/tools/perf/util/perf-regs-arch/perf_regs_x86.c b/tools/perf/util/perf-regs-arch/perf_regs_x86.c index b6d20522b4e8..ae26d991cdc9 100644 --- a/tools/perf/util/perf-regs-arch/perf_regs_x86.c +++ b/tools/perf/util/perf-regs-arch/perf_regs_x86.c @@ -235,26 +235,26 @@ int __perf_sdt_arg_parse_op_x86(char *old_op, char **new_op) return SDT_ARG_VALID; } -uint64_t __perf_reg_mask_x86(bool intr) +static uint64_t __arch__reg_mask(u64 sample_type, u64 mask, bool has_simd_regs) { struct perf_event_attr attr = { - .type = PERF_TYPE_HARDWARE, - .config = PERF_COUNT_HW_CPU_CYCLES, - .sample_type = PERF_SAMPLE_REGS_INTR, - .sample_regs_intr = PERF_REG_EXTENDED_MASK, - .precise_ip = 1, - .disabled = 1, - .exclude_kernel = 1, + .type = PERF_TYPE_HARDWARE, + .config = PERF_COUNT_HW_CPU_CYCLES, + .sample_type = sample_type, + .precise_ip = 1, + .disabled = 1, + .exclude_kernel = 1, + .sample_simd_regs_enabled = has_simd_regs, }; int fd; - - if (!intr) - return PERF_REGS_MASK; - /* * In an unnamed union, init it here to build on older gcc versions */ attr.sample_period = 1; + if (sample_type == PERF_SAMPLE_REGS_INTR) + attr.sample_regs_intr = mask; + else + attr.sample_regs_user = mask; if (perf_pmus__num_core_pmus() > 1) { struct perf_pmu *pmu = NULL; @@ -276,13 +276,38 @@ uint64_t __perf_reg_mask_x86(bool intr) /*group_fd=*/-1, /*flags=*/0); if (fd != -1) { close(fd); - return (PERF_REG_EXTENDED_MASK | PERF_REGS_MASK); + return mask; } - return PERF_REGS_MASK; + return 0; } -const char *__perf_reg_name_x86(int id) +uint64_t __perf_reg_mask_x86(bool intr, int *abi) +{ + u64 sample_type = intr ? PERF_SAMPLE_REGS_INTR : PERF_SAMPLE_REGS_USER; + uint64_t mask = PERF_REGS_MASK; + + /* -1 indicates only basic GPRs are needed. */ + if (*abi < 0) + return PERF_REGS_MASK; + + *abi = 0; + mask |= __arch__reg_mask(sample_type, + GENMASK_ULL(PERF_REG_X86_R31, PERF_REG_X86_R16), + true); + mask |= __arch__reg_mask(sample_type, BIT_ULL(PERF_REG_X86_SSP), true); + + if (mask != PERF_REGS_MASK) { + *abi |= PERF_SAMPLE_REGS_ABI_SIMD; + } else { + mask |= __arch__reg_mask(sample_type, PERF_REG_EXTENDED_MASK, + false); + } + + return mask; +} + +static const char *__arch_reg_gpr_name(int id) { switch (id) { case PERF_REG_X86_AX: @@ -333,7 +358,60 @@ const char *__perf_reg_name_x86(int id) return "R14"; case PERF_REG_X86_R15: return "R15"; + default: + return NULL; + } + return NULL; +} + +static const char *__arch_reg_egpr_name(int id) +{ + switch (id) { + case PERF_REG_X86_R16: + return "R16"; + case PERF_REG_X86_R17: + return "R17"; + case PERF_REG_X86_R18: + return "R18"; + case PERF_REG_X86_R19: + return "R19"; + case PERF_REG_X86_R20: + return "R20"; + case PERF_REG_X86_R21: + return "R21"; + case PERF_REG_X86_R22: + return "R22"; + case PERF_REG_X86_R23: + return "R23"; + case PERF_REG_X86_R24: + return "R24"; + case PERF_REG_X86_R25: + return "R25"; + case PERF_REG_X86_R26: + return "R26"; + case PERF_REG_X86_R27: + return "R27"; + case PERF_REG_X86_R28: + return "R28"; + case PERF_REG_X86_R29: + return "R29"; + case PERF_REG_X86_R30: + return "R30"; + case PERF_REG_X86_R31: + return "R31"; + case PERF_REG_X86_SSP: + return "SSP"; + default: + return NULL; + } + + return NULL; +} + +static const char *__arch_reg_xmm_name(int id) +{ + switch (id) { #define XMM(x) \ case PERF_REG_X86_XMM ## x: \ case PERF_REG_X86_XMM ## x + 1: \ @@ -362,6 +440,22 @@ const char *__perf_reg_name_x86(int id) return NULL; } +const char *__perf_reg_name_x86(int id, int abi) +{ + const char *name; + + name = __arch_reg_gpr_name(id); + if (name) + return name; + + if (abi & PERF_SAMPLE_REGS_ABI_SIMD) + name = __arch_reg_egpr_name(id); + else + name = __arch_reg_xmm_name(id); + + return name; +} + uint64_t __perf_reg_ip_x86(void) { return PERF_REG_X86_IP; diff --git a/tools/perf/util/perf_regs.c b/tools/perf/util/perf_regs.c index 558c143abbab..089ededbae94 100644 --- a/tools/perf/util/perf_regs.c +++ b/tools/perf/util/perf_regs.c @@ -38,7 +38,7 @@ int perf_sdt_arg_parse_op(uint16_t e_machine, char *old_op, char **new_op) return ret; } -uint64_t perf_intr_reg_mask(uint16_t e_machine) +uint64_t perf_intr_reg_mask(uint16_t e_machine, int *abi /*inout*/) { uint64_t mask = 0; @@ -70,7 +70,7 @@ uint64_t perf_intr_reg_mask(uint16_t e_machine) break; case EM_386: case EM_X86_64: - mask = __perf_reg_mask_x86(/*intr=*/true); + mask = __perf_reg_mask_x86(/*intr=*/true, abi); break; default: pr_debug("Unknown ELF machine %d, interrupt sampling register mask will be empty.\n", @@ -81,7 +81,7 @@ uint64_t perf_intr_reg_mask(uint16_t e_machine) return mask; } -uint64_t perf_user_reg_mask(uint16_t e_machine) +uint64_t perf_user_reg_mask(uint16_t e_machine, int *abi /*inout*/) { uint64_t mask = 0; @@ -113,7 +113,7 @@ uint64_t perf_user_reg_mask(uint16_t e_machine) break; case EM_386: case EM_X86_64: - mask = __perf_reg_mask_x86(/*intr=*/false); + mask = __perf_reg_mask_x86(/*intr=*/false, abi); break; default: pr_debug("Unknown ELF machine %d, user sampling register mask will be empty.\n", @@ -124,7 +124,7 @@ uint64_t perf_user_reg_mask(uint16_t e_machine) return mask; } -const char *perf_reg_name(int id, uint16_t e_machine, uint32_t e_flags) +const char *perf_reg_name(int id, uint16_t e_machine, uint32_t e_flags, int abi) { const char *reg_name = NULL; @@ -156,7 +156,7 @@ const char *perf_reg_name(int id, uint16_t e_machine, uint32_t e_flags) break; case EM_386: case EM_X86_64: - reg_name = __perf_reg_name_x86(id); + reg_name = __perf_reg_name_x86(id, abi); break; default: break; diff --git a/tools/perf/util/perf_regs.h b/tools/perf/util/perf_regs.h index 79be2b791509..1c78549127cc 100644 --- a/tools/perf/util/perf_regs.h +++ b/tools/perf/util/perf_regs.h @@ -13,10 +13,10 @@ enum { }; int perf_sdt_arg_parse_op(uint16_t e_machine, char *old_op, char **new_op); -uint64_t perf_intr_reg_mask(uint16_t e_machine); -uint64_t perf_user_reg_mask(uint16_t e_machine); +uint64_t perf_intr_reg_mask(uint16_t e_machine, int *abi /*inout*/); +uint64_t perf_user_reg_mask(uint16_t e_machine, int *abi /*inout*/); -const char *perf_reg_name(int id, uint16_t e_machine, uint32_t e_flags); +const char *perf_reg_name(int id, uint16_t e_machine, uint32_t e_flags, int abi); int perf_reg_value(u64 *valp, struct regs_dump *regs, int id); uint64_t perf_arch_reg_ip(uint16_t e_machine); uint64_t perf_arch_reg_sp(uint16_t e_machine); @@ -66,8 +66,8 @@ uint64_t __perf_reg_sp_s390(void); int __perf_sdt_arg_parse_op_s390(char *old_op, char **new_op); int __perf_sdt_arg_parse_op_x86(char *old_op, char **new_op); -uint64_t __perf_reg_mask_x86(bool intr); -const char *__perf_reg_name_x86(int id); +uint64_t __perf_reg_mask_x86(bool intr, int *abi); +const char *__perf_reg_name_x86(int id, int abi); uint64_t __perf_reg_ip_x86(void); uint64_t __perf_reg_sp_x86(void); diff --git a/tools/perf/util/record.h b/tools/perf/util/record.h index 93627c9a7338..411bb7276ad7 100644 --- a/tools/perf/util/record.h +++ b/tools/perf/util/record.h @@ -62,6 +62,13 @@ struct record_opts { u64 branch_stack; u64 sample_intr_regs; u64 sample_user_regs; + u16 sample_simd_regs_enabled; + u16 sample_vec_reg_qwords; + u16 sample_pred_reg_qwords; + u32 sample_intr_pred_regs; + u32 sample_user_pred_regs; + u64 sample_intr_vec_regs; + u64 sample_user_vec_regs; u64 default_interval; u64 user_interval; size_t auxtrace_snapshot_size; diff --git a/tools/perf/util/scripting-engines/trace-event-python.c b/tools/perf/util/scripting-engines/trace-event-python.c index 7e8e3a7df1b1..11877b56548f 100644 --- a/tools/perf/util/scripting-engines/trace-event-python.c +++ b/tools/perf/util/scripting-engines/trace-event-python.c @@ -730,7 +730,7 @@ static void regs_map(struct regs_dump *regs, uint64_t mask, uint16_t e_machine, printed += scnprintf(bf + printed, size - printed, "%5s:0x%" PRIx64 " ", - perf_reg_name(r, e_machine, e_flags), val); + perf_reg_name(r, e_machine, e_flags, regs->abi), val); } } diff --git a/tools/perf/util/session.c b/tools/perf/util/session.c index c7ac0e7c5c04..90d4e45f09f0 100644 --- a/tools/perf/util/session.c +++ b/tools/perf/util/session.c @@ -1433,15 +1433,16 @@ static void branch_stack__printf(struct perf_sample *sample, } } -static void regs_dump__printf(u64 mask, u64 *regs, uint16_t e_machine, uint32_t e_flags) +static void regs_dump__printf(u64 mask, struct regs_dump *regs, + uint16_t e_machine, uint32_t e_flags) { unsigned rid, i = 0; for_each_set_bit(rid, (unsigned long *) &mask, sizeof(mask) * 8) { - u64 val = regs[i++]; + u64 val = regs->regs[i++]; printf(".... %-5s 0x%016" PRIx64 "\n", - perf_reg_name(rid, e_machine, e_flags), val); + perf_reg_name(rid, e_machine, e_flags, regs->abi), val); } } @@ -1449,11 +1450,13 @@ static const char *regs_abi[] = { [PERF_SAMPLE_REGS_ABI_NONE] = "none", [PERF_SAMPLE_REGS_ABI_32] = "32-bit", [PERF_SAMPLE_REGS_ABI_64] = "64-bit", + [PERF_SAMPLE_REGS_ABI_SIMD | PERF_SAMPLE_REGS_ABI_32] = "32-bit SIMD", + [PERF_SAMPLE_REGS_ABI_SIMD | PERF_SAMPLE_REGS_ABI_64] = "64-bit SIMD", }; static inline const char *regs_dump_abi(struct regs_dump *d) { - if (d->abi > PERF_SAMPLE_REGS_ABI_64) + if (d->abi >= ARRAY_SIZE(regs_abi) || !regs_abi[d->abi]) return "unknown"; return regs_abi[d->abi]; @@ -1469,7 +1472,7 @@ static void regs__printf(const char *type, struct regs_dump *regs, mask, regs_dump_abi(regs)); - regs_dump__printf(mask, regs->regs, e_machine, e_flags); + regs_dump__printf(mask, regs, e_machine, e_flags); } static void regs_user__printf(struct perf_sample *sample, uint16_t e_machine, uint32_t e_flags) diff --git a/tools/perf/util/synthetic-events.c b/tools/perf/util/synthetic-events.c index 0c150193cca8..2779f63a442c 100644 --- a/tools/perf/util/synthetic-events.c +++ b/tools/perf/util/synthetic-events.c @@ -1644,6 +1644,8 @@ size_t perf_event__sample_event_size(const struct perf_sample *sample, u64 type, if (sample->user_regs && sample->user_regs->abi) { result += sizeof(u64); sz = hweight64(sample->user_regs->mask) * sizeof(u64); + if (sample->user_regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) + sz += 4 * sizeof(u64); result += sz; } else { result += sizeof(u64); @@ -1672,6 +1674,8 @@ size_t perf_event__sample_event_size(const struct perf_sample *sample, u64 type, if (sample->intr_regs && sample->intr_regs->abi) { result += sizeof(u64); sz = hweight64(sample->intr_regs->mask) * sizeof(u64); + if (sample->intr_regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) + sz += 4 * sizeof(u64); result += sz; } else { result += sizeof(u64); From 6e9c063d8fd5d7b5403088b0e0e9986863dfc4e2 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:45 +0800 Subject: [PATCH 070/425] perf regs: Support x86 SIMD registers sampling Add support for the newly introduced SIMD register sampling format by adding the following 5 functions: uint64_t perf_intr_simd_reg_class_mask(uint16_t e_machine, bool pred); uint64_t perf_user_simd_reg_class_mask(uint16_t e_machine, bool pred); uint64_t perf_intr_simd_reg_class_bitmap_qwords(uint16_t e_machine, int reg_c, uint16_t *qwords, bool pred); uint64_t perf_user_simd_reg_class_bitmap_qwords(uint16_t e_machine, int reg_c, uint16_t *qwords, bool pred); const char *perf_simd_reg_class_name(uint16_t e_machine, int id, bool pred); The perf_{intr|user}_simd_reg_class_mask() functions retrieve the bitmap of kernel supported SIMD/PRED register classes on current platform for intr-regs and user-regs sampling, such as OPMASK/XMM/YMM/ZMM on x86 platforms. The perf_{intr|user}_simd_reg_class_bitmap_qwords() functions retrieve the bitmap and qwords length of a certain class of SIMD/PRED register on current platform for intr-regs and user-regs sampling. For example, for the XMM registers on x86 platforms, the returned bitmap is 0xffff (XMM0 ~ XMM15) and the qwords length is 2 (128 bits for each XMM register). The perf_simd_reg_class_name() function gets the register class name for a certain register class index. Additionally, the function __parse_regs() is enhanced to support parsing these newly introduced SIMD/PRED registers. Currently, each class of register can only be sampled collectively; sampling a specific SIMD register is not supported. For example, all XMM registers are sampled together rather than sampling only XMM0. When multiple overlapping register types, such as XMM and YMM, are sampled simultaneously, only the superset (YMM registers) is sampled. With this patch, all supported sampling registers on x86 platforms are displayed as follows. $perf record --intr-regs=? available registers: AX BX CX DX SI DI BP SP IP FLAGS CS SS R8 R9 R10 R11 R12 R13 R14 R15 R16 R17 R18 R19 R20 R21 R22 R23 R24 R25 R26 R27 R28 R29 R30 R31 SSP XMM0-15 YMM0-15 ZMM0-31 OPMASK0-7 $perf record --user-regs=? available registers: AX BX CX DX SI DI BP SP IP FLAGS CS SS R8 R9 R10 R11 R12 R13 R14 R15 R16 R17 R18 R19 R20 R21 R22 R23 R24 R25 R26 R27 R28 R29 R30 R31 SSP XMM0-15 YMM0-15 ZMM0-31 OPMASK0-7 Signed-off-by: Dapeng Mi Reviewed-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-record.txt | 10 +- tools/perf/util/evsel.c | 21 ++ tools/perf/util/parse-regs-options.c | 187 ++++++++++- .../perf/util/perf-regs-arch/perf_regs_x86.c | 316 +++++++++++++++++- tools/perf/util/perf_regs.c | 72 ++++ tools/perf/util/perf_regs.h | 11 + 6 files changed, 597 insertions(+), 20 deletions(-) diff --git a/tools/perf/Documentation/perf-record.txt b/tools/perf/Documentation/perf-record.txt index 2c6510c5a87b..388455f9b07b 100644 --- a/tools/perf/Documentation/perf-record.txt +++ b/tools/perf/Documentation/perf-record.txt @@ -515,12 +515,16 @@ Capture machine state (registers) at interrupt, i.e., on counter overflows for each sample. List of captured registers depends on the architecture. This option is off by default. It is possible to select the registers to sample using their symbolic names, e.g. on x86, ax, si. To list the available registers use ---intr-regs=\?. To name registers, pass a comma separated list such as ---intr-regs=ax,bx. The list of register is architecture dependent. +--intr-regs=\?. On supported architectures, SIMD registers are displayed as +groups (e.g., on x86: XMM0-15,YMM0-15,ZMM0-31). To name registers, pass a comma +separated list such as --intr-regs=ax,bx,zmm. Please notice SIMD registers must +be assigned as a complete set, sampling individual SIMD registers (e.g., zmm0) +is not supported. The list of register is architecture dependent. --user-regs:: Similar to -I, but capture user registers at sample time. To list the available -user registers use --user-regs=\?. +user registers use --user-regs=\?. For SIMD registers, only complete register +sets are allowed like -I. --running-time:: Record running and enabled time for read events (:S) diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index bcd919c6bde8..df16780bf887 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -1672,6 +1673,16 @@ void evsel__config(struct evsel *evsel, const struct record_opts *opts, evsel__set_sample_bit(evsel, REGS_INTR); } + if ((opts->sample_intr_vec_regs || opts->sample_intr_pred_regs) && + !evsel->no_aux_samples && !evsel__is_dummy_event(evsel)) { + attr->sample_simd_regs_enabled = !!opts->sample_simd_regs_enabled; + attr->sample_simd_vec_reg_intr = opts->sample_intr_vec_regs; + attr->sample_simd_vec_reg_qwords = opts->sample_vec_reg_qwords; + attr->sample_simd_pred_reg_intr = opts->sample_intr_pred_regs; + attr->sample_simd_pred_reg_qwords = opts->sample_pred_reg_qwords; + evsel__set_sample_bit(evsel, REGS_INTR); + } + if (opts->sample_user_regs && !evsel->no_aux_samples && !evsel__is_dummy_event(evsel)) { attr->sample_regs_user |= opts->sample_user_regs; @@ -1681,6 +1692,16 @@ void evsel__config(struct evsel *evsel, const struct record_opts *opts, evsel__set_sample_bit(evsel, REGS_USER); } + if ((opts->sample_user_vec_regs || opts->sample_user_pred_regs) && + !evsel->no_aux_samples && !evsel__is_dummy_event(evsel)) { + attr->sample_simd_regs_enabled = !!opts->sample_simd_regs_enabled; + attr->sample_simd_vec_reg_user = opts->sample_user_vec_regs; + attr->sample_simd_vec_reg_qwords = opts->sample_vec_reg_qwords; + attr->sample_simd_pred_reg_user = opts->sample_user_pred_regs; + attr->sample_simd_pred_reg_qwords = opts->sample_pred_reg_qwords; + evsel__set_sample_bit(evsel, REGS_USER); + } + if (target__has_cpu(&opts->target) || opts->sample_cpu) evsel__set_sample_bit(evsel, CPU); diff --git a/tools/perf/util/parse-regs-options.c b/tools/perf/util/parse-regs-options.c index 70a1cc90b2c1..709fcfb077a2 100644 --- a/tools/perf/util/parse-regs-options.c +++ b/tools/perf/util/parse-regs-options.c @@ -13,11 +13,10 @@ #include "record.h" static void -list_perf_regs(FILE *fp, uint64_t mask, int abi) +__list_gp_regs(FILE *fp, uint64_t mask, int abi) { const char *last_name = NULL; - fprintf(fp, "available registers: "); for (int reg = 0; reg < 64; reg++) { const char *name; @@ -29,14 +28,68 @@ list_perf_regs(FILE *fp, uint64_t mask, int abi) fprintf(fp, "%s%s", reg > 0 ? " " : "", name); last_name = name; } +} + +static void +__list_simd_regs(FILE *fp, uint64_t mask, bool intr, bool pred) +{ + uint64_t bitmap = 0; + uint16_t qwords = 0; + const char *name; + int i = 0; + + for (int reg_c = 0; reg_c < 64; reg_c++) { + if (((1ULL << reg_c) & mask) == 0) + continue; + + name = perf_simd_reg_class_name(EM_HOST, reg_c, pred); + bitmap = intr ? + perf_intr_simd_reg_class_bitmap_qwords(EM_HOST, reg_c, &qwords, pred) : + perf_user_simd_reg_class_bitmap_qwords(EM_HOST, reg_c, &qwords, pred); + if (name && bitmap) + fprintf(fp, "%s%s0-%d", i++ > 0 ? " " : "", + name, fls64(bitmap) - 1); + } +} + +static void +list_perf_regs(FILE *fp, uint64_t mask, uint64_t simd_mask, + uint64_t pred_mask, int abi, bool intr) +{ + bool printed = false; + + fprintf(fp, "available registers: "); + + if (mask) { + __list_gp_regs(fp, mask, abi); + printed = true; + } + + if (simd_mask) { + if (printed) + fprintf(fp, " "); + __list_simd_regs(fp, simd_mask, intr, /*pred=*/false); + printed = true; + } + + if (pred_mask) { + if (printed) + fprintf(fp, " "); + __list_simd_regs(fp, pred_mask, intr, /*pred=*/true); + printed = true; + } + fputc('\n', fp); } static uint64_t -name_to_perf_reg_mask(const char *to_match, uint64_t mask, int abi) +name_to_gp_reg_mask(const char *to_match, uint64_t mask, int abi) { uint64_t reg_mask = 0; + if (!mask) + return reg_mask; + for (int reg = 0; reg < 64; reg++) { const char *name; @@ -53,26 +106,116 @@ name_to_perf_reg_mask(const char *to_match, uint64_t mask, int abi) return reg_mask; } +static bool +name_to_simd_reg_mask(struct record_opts *opts, const char *to_match, + uint64_t mask, bool intr, bool pred) +{ + bool matched = false; + uint64_t bitmap; + uint16_t qwords; + int reg_c; + + if (!mask) + return false; + + for (reg_c = 0; reg_c < 64; reg_c++) { + const char *name; + + if (((1ULL << reg_c) & mask) == 0) + continue; + + name = perf_simd_reg_class_name(EM_HOST, reg_c, pred); + if (!name) + continue; + + if (!strcasecmp(to_match, name)) { + matched = true; + break; + } + } + + if (!matched) + return false; + + if (intr) { + bitmap = perf_intr_simd_reg_class_bitmap_qwords(EM_HOST, + reg_c, &qwords, pred); + } else { + bitmap = perf_user_simd_reg_class_bitmap_qwords(EM_HOST, + reg_c, &qwords, pred); + } + + /* + * Assume higher width SIMD registers are always the superset of lower + * width SIMD registers. So only pick the largest qwords and bitmap. + */ + if (pred) { + opts->sample_pred_reg_qwords = + MAX(qwords, opts->sample_pred_reg_qwords); + if (intr && + hweight64(bitmap) > hweight32(opts->sample_intr_pred_regs)) + opts->sample_intr_pred_regs = bitmap; + if (!intr && + hweight64(bitmap) > hweight32(opts->sample_user_pred_regs)) + opts->sample_user_pred_regs = bitmap; + + /* + * sample_intr_pred_regs and sample_user_pred_regs share + * sample_pred_reg_qwords. If both are set, keep their bitmaps identical. + */ + if (opts->sample_intr_pred_regs && opts->sample_user_pred_regs) { + uint64_t max_pred_regs = MAX(opts->sample_intr_pred_regs, + opts->sample_user_pred_regs); + + opts->sample_intr_pred_regs = max_pred_regs; + opts->sample_user_pred_regs = max_pred_regs; + } + } else { + opts->sample_vec_reg_qwords = + MAX(qwords, opts->sample_vec_reg_qwords); + if (intr && + hweight64(bitmap) > hweight64(opts->sample_intr_vec_regs)) + opts->sample_intr_vec_regs = bitmap; + if (!intr && + hweight64(bitmap) > hweight64(opts->sample_user_vec_regs)) + opts->sample_user_vec_regs = bitmap; + + /* + * sample_intr_vec_regs and sample_user_vec_regs share + * sample_vec_reg_qwords. If both are set, keep their bitmaps identical. + */ + if (opts->sample_intr_vec_regs && opts->sample_user_vec_regs) { + uint64_t max_vec_regs = MAX(opts->sample_intr_vec_regs, + opts->sample_user_vec_regs); + + opts->sample_intr_vec_regs = max_vec_regs; + opts->sample_user_vec_regs = max_vec_regs; + } + } + + if (opts->sample_pred_reg_qwords || opts->sample_vec_reg_qwords) + opts->sample_simd_regs_enabled = 1; + + return true; +} + static int __parse_regs(const struct option *opt, const char *str, int unset, bool intr) { uint64_t *mode = (uint64_t *)opt->value; struct record_opts *opts; char *s, *os = NULL, *p; - const char *warn; - int ret = -1; + uint64_t simd_mask; + uint64_t pred_mask; uint64_t mask; + const char *warn; + bool matched; + int ret = -1; int abi = 0; if (unset) return 0; - /* - * cannot set it twice - */ - if (*mode) - return -1; - mask = intr ? perf_intr_reg_mask(EM_HOST, &abi) : perf_user_reg_mask(EM_HOST, &abi); opts = intr ? container_of(opt->value, struct record_opts, sample_intr_regs) : @@ -91,6 +234,14 @@ __parse_regs(const struct option *opt, const char *str, int unset, bool intr) if (!s) return -1; + if (intr) { + simd_mask = perf_intr_simd_reg_class_mask(EM_HOST, /*pred=*/false); + pred_mask = perf_intr_simd_reg_class_mask(EM_HOST, /*pred=*/true); + } else { + simd_mask = perf_user_simd_reg_class_mask(EM_HOST, /*pred=*/false); + pred_mask = perf_user_simd_reg_class_mask(EM_HOST, /*pred=*/true); + } + warn = "Unknown register \"%s\", check man page or run \"perf record %s?\"\n"; for (;;) { uint64_t reg_mask; @@ -100,17 +251,23 @@ __parse_regs(const struct option *opt, const char *str, int unset, bool intr) *p = '\0'; if (!strcmp(s, "?")) { - list_perf_regs(stderr, mask, abi); + list_perf_regs(stderr, mask, simd_mask, pred_mask, abi, intr); goto error; } - reg_mask = name_to_perf_reg_mask(s, mask, abi); + reg_mask = name_to_gp_reg_mask(s, mask, abi); if (reg_mask) { if (abi & PERF_SAMPLE_REGS_ABI_SIMD) opts->sample_simd_regs_enabled = 1; } else { - ui__warning(warn, s, intr ? "-I" : "--user-regs="); - goto error; + matched = name_to_simd_reg_mask(opts, s, simd_mask, + intr, /*pred=*/false) || + name_to_simd_reg_mask(opts, s, pred_mask, + intr, /*pred=*/true); + if (!matched) { + ui__warning(warn, s, intr ? "-I" : "--user-regs="); + goto error; + } } *mode |= reg_mask; diff --git a/tools/perf/util/perf-regs-arch/perf_regs_x86.c b/tools/perf/util/perf-regs-arch/perf_regs_x86.c index ae26d991cdc9..fb6cf34f42f2 100644 --- a/tools/perf/util/perf-regs-arch/perf_regs_x86.c +++ b/tools/perf/util/perf-regs-arch/perf_regs_x86.c @@ -282,6 +282,7 @@ static uint64_t __arch__reg_mask(u64 sample_type, u64 mask, bool has_simd_regs) return 0; } +static bool __support_simd_sampling(bool intr); uint64_t __perf_reg_mask_x86(bool intr, int *abi) { u64 sample_type = intr ? PERF_SAMPLE_REGS_INTR : PERF_SAMPLE_REGS_USER; @@ -300,8 +301,14 @@ uint64_t __perf_reg_mask_x86(bool intr, int *abi) if (mask != PERF_REGS_MASK) { *abi |= PERF_SAMPLE_REGS_ABI_SIMD; } else { - mask |= __arch__reg_mask(sample_type, PERF_REG_EXTENDED_MASK, - false); + /* + * Use the SIMD sampling interface for XMM registers + * when available. + */ + if (!__support_simd_sampling(intr)) { + mask |= __arch__reg_mask(sample_type, + PERF_REG_EXTENDED_MASK, false); + } } return mask; @@ -465,3 +472,308 @@ uint64_t __perf_reg_sp_x86(void) { return PERF_REG_X86_SP; } + +enum { + PERF_REG_CLASS_X86_OPMASK = 0, + PERF_REG_CLASS_X86_XMM, + PERF_REG_CLASS_X86_YMM, + PERF_REG_CLASS_X86_ZMM, + PERF_REG_X86_MAX_SIMD_CLASSES, +}; + +#define PERF_REG_CLASS_X86_PRED_MASK (BIT(PERF_REG_CLASS_X86_OPMASK)) +#define PERF_REG_CLASS_X86_SIMD_MASK (BIT(PERF_REG_CLASS_X86_XMM) | \ + BIT(PERF_REG_CLASS_X86_YMM) | \ + BIT(PERF_REG_CLASS_X86_ZMM)) + +/* + * This function is used to determine whether kernel perf subsystem + * supports which kinds of SIMD registers (OPMASK/XMM/YMM/ZMM) sampling. + * + * @sample_type: PERF_SAMPLE_REGS_INTR or PERF_SAMPLE_REGS_USER + * @qwords: the length of SIMD register, like 1/2/4/8 qwords for + * OPMASK/XMM/YMM/ZMM registers. + * @mask: the bitmask of SIMD register, like 0xffff for XMM0 ~ XMM15 + * @pred: whether It's a predicate SIMD register, like OPMASK register. + * + * Return value: true indicates support, otherwise no support. + */ +static bool +__support_simd_reg_class(uint64_t sample_type, uint16_t qwords, + uint64_t mask, bool pred) +{ + struct perf_event_attr attr = { + .type = PERF_TYPE_HARDWARE, + .config = PERF_COUNT_HW_CPU_CYCLES, + .sample_type = sample_type, + .disabled = 1, + .exclude_kernel = 1, + .sample_simd_regs_enabled = 1, + }; + int fd; + + attr.sample_period = 1; + + if (!pred) { + attr.sample_simd_vec_reg_qwords = qwords; + if (sample_type == PERF_SAMPLE_REGS_INTR) + attr.sample_simd_vec_reg_intr = mask; + else + attr.sample_simd_vec_reg_user = mask; + } else { + attr.sample_simd_pred_reg_qwords = PERF_X86_OPMASK_QWORDS; + if (sample_type == PERF_SAMPLE_REGS_INTR) + attr.sample_simd_pred_reg_intr = PERF_X86_SIMD_PRED_MASK; + else + attr.sample_simd_pred_reg_user = PERF_X86_SIMD_PRED_MASK; + } + + if (perf_pmus__num_core_pmus() > 1) { + __u64 type = perf_pmus__find_core_pmu()->type; + + attr.config |= type << PERF_PMU_TYPE_SHIFT; + } + + event_attr_init(&attr); + + fd = sys_perf_event_open(&attr, 0, -1, -1, 0); + if (fd != -1) { + close(fd); + return true; + } + + return false; +} + +#define PERF_X86_SIMD_ZMM_LOW_REGS (PERF_X86_SIMD_ZMM_REGS / 2) + +static bool __arch_has_simd_reg_class(uint64_t sample_type, int reg_class, + uint64_t *mask, uint16_t *qwords) +{ + bool supported = false; + uint64_t bits; + + *mask = 0; + *qwords = 0; + + switch (reg_class) { + case PERF_REG_CLASS_X86_OPMASK: + bits = BIT_ULL(PERF_X86_SIMD_OPMASK_REGS) - 1; + supported = __support_simd_reg_class(sample_type, + PERF_X86_OPMASK_QWORDS, + bits, true); + if (supported) { + *mask = bits; + *qwords = PERF_X86_OPMASK_QWORDS; + } + break; + case PERF_REG_CLASS_X86_XMM: + bits = BIT_ULL(PERF_X86_SIMD_XMM_REGS) - 1; + supported = __support_simd_reg_class(sample_type, + PERF_X86_XMM_QWORDS, + bits, false); + if (supported) { + *mask = bits; + *qwords = PERF_X86_XMM_QWORDS; + } + break; + case PERF_REG_CLASS_X86_YMM: + bits = BIT_ULL(PERF_X86_SIMD_YMM_REGS) - 1; + supported = __support_simd_reg_class(sample_type, + PERF_X86_YMM_QWORDS, + bits, false); + if (supported) { + *mask = bits; + *qwords = PERF_X86_YMM_QWORDS; + } + break; + case PERF_REG_CLASS_X86_ZMM: + bits = BIT_ULL(PERF_X86_SIMD_ZMM_REGS) - 1; + supported = __support_simd_reg_class(sample_type, + PERF_X86_ZMM_QWORDS, + bits, false); + if (supported) { + *mask = bits; + *qwords = PERF_X86_ZMM_QWORDS; + break; + } + + bits = BIT_ULL(PERF_X86_SIMD_ZMM_LOW_REGS) - 1; + supported = __support_simd_reg_class(sample_type, + PERF_X86_ZMM_QWORDS, + bits, false); + if (supported) { + *mask = bits; + *qwords = PERF_X86_ZMM_QWORDS; + } + break; + default: + break; + } + + return supported; +} + +static bool __support_simd_sampling(bool intr) +{ + static bool intr_simd_sampling_supported; + static bool user_simd_sampling_supported; + static bool intr_cached; + static bool user_cached; + uint64_t mask; + uint16_t qwords; + + if (intr && intr_cached) + return intr_simd_sampling_supported; + if (!intr && user_cached) + return user_simd_sampling_supported; + + if (intr) { + intr_simd_sampling_supported = + __arch_has_simd_reg_class(PERF_SAMPLE_REGS_INTR, + PERF_REG_CLASS_X86_XMM, + &mask, &qwords); + intr_cached = true; + } else { + user_simd_sampling_supported = + __arch_has_simd_reg_class(PERF_SAMPLE_REGS_USER, + PERF_REG_CLASS_X86_XMM, + &mask, &qwords); + user_cached = true; + } + + return intr ? intr_simd_sampling_supported : + user_simd_sampling_supported; +} + +/* + * @x86_intr_simd_cached: indicates the data of below 3 + * x86_intr_simd_* items has been retrieved from kernel and cached. + * @x86_intr_simd_reg_class_mask: indicates which kinds of PRED/SIMD + * registers are supported for intr-regs option. Assume kernel perf + * subsystem supports XMM/YMM sampling, then the mask is + * PERF_REG_CLASS_X86_XMM|PERF_REG_CLASS_X86_YMM. + * @x86_intr_simd_mask: indicates register bitmask for each kind of + * supported PRED/SIMD register, like + * x86_intr_simd_mask[PERF_REG_CLASS_X86_XMM] = 0xffff. + * @x86_intr_simd_qwords: indicates the register length (qwords unit) + * for each kind of supported PRED/SIMD register, like + * x86_intr_simd_qwords[PERF_REG_CLASS_X86_XMM] = 2. + */ +static bool x86_intr_simd_cached; +static uint64_t x86_intr_simd_reg_class_mask; +static uint64_t x86_intr_simd_mask[PERF_REG_X86_MAX_SIMD_CLASSES]; +static uint16_t x86_intr_simd_qwords[PERF_REG_X86_MAX_SIMD_CLASSES]; + +/* + * Similar with above x86_intr_simd_* items, the difference is these + * items are used for user-regs option. + */ +static bool x86_user_simd_cached; +static uint64_t x86_user_simd_reg_class_mask; +static uint64_t x86_user_simd_mask[PERF_REG_X86_MAX_SIMD_CLASSES]; +static uint16_t x86_user_simd_qwords[PERF_REG_X86_MAX_SIMD_CLASSES]; + +static uint64_t __arch__simd_reg_class_mask(bool intr) +{ + uint64_t mask = 0; + bool supported; + int reg_c; + + if (!__support_simd_sampling(intr)) + goto done; + + if (intr && x86_intr_simd_cached) + return x86_intr_simd_reg_class_mask; + + if (!intr && x86_user_simd_cached) + return x86_user_simd_reg_class_mask; + + for (reg_c = 0; reg_c < PERF_REG_X86_MAX_SIMD_CLASSES; reg_c++) { + supported = false; + + if (intr) { + supported = __arch_has_simd_reg_class( + PERF_SAMPLE_REGS_INTR, + reg_c, + &x86_intr_simd_mask[reg_c], + &x86_intr_simd_qwords[reg_c]); + } else { + supported = __arch_has_simd_reg_class( + PERF_SAMPLE_REGS_USER, + reg_c, + &x86_user_simd_mask[reg_c], + &x86_user_simd_qwords[reg_c]); + } + if (supported) + mask |= BIT_ULL(reg_c); + } + +done: + if (intr) { + x86_intr_simd_reg_class_mask = mask; + x86_intr_simd_cached = true; + } else { + x86_user_simd_reg_class_mask = mask; + x86_user_simd_cached = true; + } + + return mask; +} + +static uint64_t +__arch__simd_reg_class_bitmap_qwords(bool intr, int reg_c, uint16_t *qwords) +{ + uint64_t mask = 0; + uint64_t class_mask; + + *qwords = 0; + class_mask = intr ? x86_intr_simd_reg_class_mask : + x86_user_simd_reg_class_mask; + if (!(class_mask & BIT_ULL(reg_c))) + return 0; + + if (intr) { + mask = x86_intr_simd_mask[reg_c]; + *qwords = x86_intr_simd_qwords[reg_c]; + } else { + mask = x86_user_simd_mask[reg_c]; + *qwords = x86_user_simd_qwords[reg_c]; + } + + return mask; +} + +uint64_t __perf_simd_reg_class_mask_x86(bool intr, bool pred) +{ + uint64_t mask = __arch__simd_reg_class_mask(intr); + + return pred ? mask & PERF_REG_CLASS_X86_PRED_MASK : + mask & PERF_REG_CLASS_X86_SIMD_MASK; +} + +uint64_t __perf_simd_reg_class_bitmap_qwords_x86(int reg_c, uint16_t *qwords, + bool intr, bool pred) +{ + if (intr ? !x86_intr_simd_cached : !x86_user_simd_cached) + __perf_simd_reg_class_mask_x86(intr, pred); + return __arch__simd_reg_class_bitmap_qwords(intr, reg_c, qwords); +} + +const char *__perf_simd_reg_class_name_x86(int id, bool pred __maybe_unused) +{ + switch (id) { + case PERF_REG_CLASS_X86_OPMASK: + return "OPMASK"; + case PERF_REG_CLASS_X86_XMM: + return "XMM"; + case PERF_REG_CLASS_X86_YMM: + return "YMM"; + case PERF_REG_CLASS_X86_ZMM: + return "ZMM"; + default: + return NULL; + } + + return NULL; +} diff --git a/tools/perf/util/perf_regs.c b/tools/perf/util/perf_regs.c index 089ededbae94..c8242b7e1db5 100644 --- a/tools/perf/util/perf_regs.c +++ b/tools/perf/util/perf_regs.c @@ -252,3 +252,75 @@ uint64_t perf_arch_reg_sp(uint16_t e_machine) return 0; } } + +uint64_t perf_intr_simd_reg_class_mask(uint16_t e_machine, bool pred) +{ + switch (e_machine) { + case EM_386: + case EM_X86_64: + return __perf_simd_reg_class_mask_x86(/*intr=*/true, pred); + default: + return 0; + } +} + +uint64_t perf_user_simd_reg_class_mask(uint16_t e_machine, bool pred) +{ + switch (e_machine) { + case EM_386: + case EM_X86_64: + return __perf_simd_reg_class_mask_x86(/*intr=*/false, pred); + default: + return 0; + } +} + +uint64_t perf_intr_simd_reg_class_bitmap_qwords(uint16_t e_machine, int reg_c, + uint16_t *qwords, bool pred) +{ + switch (e_machine) { + case EM_386: + case EM_X86_64: + return __perf_simd_reg_class_bitmap_qwords_x86(reg_c, qwords, + /*intr=*/true, + pred); + default: + *qwords = 0; + return 0; + } +} + +uint64_t perf_user_simd_reg_class_bitmap_qwords(uint16_t e_machine, int reg_c, + uint16_t *qwords, bool pred) +{ + switch (e_machine) { + case EM_386: + case EM_X86_64: + return __perf_simd_reg_class_bitmap_qwords_x86(reg_c, qwords, + /*intr=*/false, + pred); + default: + *qwords = 0; + return 0; + } +} + +const char *perf_simd_reg_class_name(uint16_t e_machine, int id, bool pred) +{ + const char *name = NULL; + + switch (e_machine) { + case EM_386: + case EM_X86_64: + name = __perf_simd_reg_class_name_x86(id, pred); + break; + default: + break; + } + if (name) + return name; + + pr_debug("Failed to find %s register %d for ELF machine type %u\n", + pred ? "PRED" : "SIMD", id, e_machine); + return "unknown"; +} diff --git a/tools/perf/util/perf_regs.h b/tools/perf/util/perf_regs.h index 1c78549127cc..c9f461b6f11f 100644 --- a/tools/perf/util/perf_regs.h +++ b/tools/perf/util/perf_regs.h @@ -20,6 +20,13 @@ const char *perf_reg_name(int id, uint16_t e_machine, uint32_t e_flags, int abi) int perf_reg_value(u64 *valp, struct regs_dump *regs, int id); uint64_t perf_arch_reg_ip(uint16_t e_machine); uint64_t perf_arch_reg_sp(uint16_t e_machine); +uint64_t perf_intr_simd_reg_class_mask(uint16_t e_machine, bool pred); +uint64_t perf_user_simd_reg_class_mask(uint16_t e_machine, bool pred); +uint64_t perf_intr_simd_reg_class_bitmap_qwords(uint16_t e_machine, int reg_c, + uint16_t *qwords, bool pred); +uint64_t perf_user_simd_reg_class_bitmap_qwords(uint16_t e_machine, int reg_c, + uint16_t *qwords, bool pred); +const char *perf_simd_reg_class_name(uint16_t e_machine, int id, bool pred); int __perf_sdt_arg_parse_op_arm64(char *old_op, char **new_op); uint64_t __perf_reg_mask_arm64(bool intr); @@ -70,6 +77,10 @@ uint64_t __perf_reg_mask_x86(bool intr, int *abi); const char *__perf_reg_name_x86(int id, int abi); uint64_t __perf_reg_ip_x86(void); uint64_t __perf_reg_sp_x86(void); +uint64_t __perf_simd_reg_class_mask_x86(bool intr, bool pred); +uint64_t __perf_simd_reg_class_bitmap_qwords_x86(int reg_c, uint16_t *qwords, + bool intr, bool pred); +const char *__perf_simd_reg_class_name_x86(int id, bool pred); static inline uint64_t DWARF_MINIMAL_REGS(uint16_t e_machine) { From 01ee8c14354f1fb90ed7ac81998b863a9d42e285 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:46 +0800 Subject: [PATCH 071/425] perf regs: Enable dumping of SIMD registers Support dumping of SIMD registers with the new PERF_SAMPLE_REGS_ABI_SIMD ABI. Currently, the XMM, YMM, ZMM, OPMASK, eGPRs, and SSP registers on x86 platforms are supported with the PERF_SAMPLE_REGS_ABI_SIMD ABI. An example of the raw format output is displayed below. Example: $perf record -e cycles:p -Iax,bx,r8,r16,r31,ssp,xmm,ymm,zmm,opmask ./test $perf report -D ... ... 3342715685845 0x3afe8 [0xbc8]: PERF_RECORD_SAMPLE(IP, 0x1): 27776/27776: 0xffffffff91d7c18f period: 10000 addr: 0 ... intr regs: mask 0x18001010003 ABI 64-bit SIMD .... AX 0xffffed102de1a606 .... BX 0xffffed102de1a606 .... R8 0x0000000000000001 .... R16 0x0000000000000000 .... R31 0x0000000000000000 .... SSP 0x0000000000000000 ... SIMD ABI nr_vectors 32 vector_qwords 8 nr_pred 8 pred_qwords 1 .... ZMM[0][0] 0x616c2f656d6f682f .... ZMM[0][1] 0x696c2f7265737562 .... ZMM[0][2] 0x0000000000000000 .... ZMM[0][3] 0x0000000000000000 .... ZMM[0][4] 0x0000000000000000 .... ZMM[0][5] 0x0000000000000000 .... ZMM[0][6] 0x0000000000000000 .... ZMM[0][7] 0x0000000000000000 .... ZMM[1][0] 0x702f636578656269 .... ZMM[1][1] 0x65726f632d667265 .... ZMM[1][2] 0x0000000000000000 .... ZMM[1][3] 0x0000000000000000 .... ZMM[1][4] 0x0000000000000000 .... ZMM[1][5] 0x0000000000000000 .... ZMM[1][6] 0x0000000000000000 .... ZMM[1][7] 0x0000000000000000 ... ... .... ZMM[31][0] 0x0000000000000000 .... ZMM[31][1] 0x0000000000000000 .... ZMM[31][2] 0x0000000000000000 .... ZMM[31][3] 0x0000000000000000 .... ZMM[31][4] 0x0000000000000000 .... ZMM[31][5] 0x0000000000000000 .... ZMM[31][6] 0x0000000000000000 .... ZMM[31][7] 0x0000000000000000 .... OPMASK[0] 0x0000000000100221 .... OPMASK[1] 0x0000000000000020 .... OPMASK[2] 0x000000007fffffff .... OPMASK[3] 0x0000000000000000 .... OPMASK[4] 0x0000000000000000 .... OPMASK[5] 0x0000000000000000 .... OPMASK[6] 0x0000000000000000 .... OPMASK[7] 0x0000000000000000 ... ... Co-developed-by: Kan Liang Signed-off-by: Kan Liang Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-script.c | 91 +++++++++++-- tools/perf/util/evsel.c | 68 +++++++++- tools/perf/util/sample.h | 5 + .../scripting-engines/trace-event-python.c | 125 ++++++++++++++++-- tools/perf/util/session.c | 82 ++++++++++++ tools/perf/util/synthetic-events.c | 38 +++++- 6 files changed, 387 insertions(+), 22 deletions(-) diff --git a/tools/perf/builtin-script.c b/tools/perf/builtin-script.c index ec3ce841b33e..0174489d1c0f 100644 --- a/tools/perf/builtin-script.c +++ b/tools/perf/builtin-script.c @@ -719,23 +719,96 @@ static int perf_session__check_output_opt(struct perf_session *session) return 0; } -static int perf_sample__fprintf_regs(struct regs_dump *regs, uint64_t mask, +static int perf_sample__fprintf_regs(struct regs_dump *regs, + struct perf_event_attr *attr, uint16_t e_machine, uint32_t e_flags, - FILE *fp) + FILE *fp, bool intr) { - unsigned i = 0, r; + const char *name = "unknown"; + int reg_c, idx, pred_base; + unsigned int i = 0, j, r; + uint16_t qwords; int printed = 0; + uint64_t mask; - if (!regs || !regs->regs) + if (!regs || (!regs->regs && !regs->simd_data)) return 0; printed += fprintf(fp, " ABI:%" PRIu64 " ", regs->abi); + mask = intr ? attr->sample_regs_intr : attr->sample_regs_user; for_each_set_bit(r, (unsigned long *) &mask, sizeof(mask) * 8) { - u64 val = regs->regs[i++]; + if (!regs->regs) + break; printed += fprintf(fp, "%5s:0x%"PRIx64" ", perf_reg_name(r, e_machine, e_flags, regs->abi), - val); + regs->regs[i++]); + } + + if (!regs->simd_data) + return printed; + + if (!regs->nr_vectors && !regs->nr_pred) + return printed; + + for (reg_c = 0; reg_c < 64; reg_c++) { + if (!regs->nr_vectors) + break; + if (intr) { + perf_intr_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/false); + } else { + perf_user_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/false); + } + if (regs->vector_qwords == qwords) { + name = perf_simd_reg_class_name(e_machine, reg_c, /*pred=*/false); + break; + } + } + + for (i = 0; i < regs->nr_vectors; i++) { + for (j = 0; j < regs->vector_qwords; j++) { + idx = i * regs->vector_qwords + j; + if (regs->vector_qwords > 1) { + printed += fprintf(fp, "%5s[%d][%d]:0x%" PRIx64 " ", + name, i, j, regs->simd_data[idx]); + } else { + printed += fprintf(fp, "%5s[%d]:0x%" PRIx64 " ", + name, i, regs->simd_data[idx]); + } + } + } + + name = "unknown"; + for (reg_c = 0; reg_c < 64; reg_c++) { + if (!regs->nr_pred) + break; + if (intr) { + perf_intr_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/true); + } else { + perf_user_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/true); + } + if (regs->pred_qwords == qwords) { + name = perf_simd_reg_class_name(e_machine, reg_c, /*pred=*/true); + break; + } + } + + pred_base = regs->nr_vectors * regs->vector_qwords; + for (i = 0; i < regs->nr_pred; i++) { + for (j = 0; j < regs->pred_qwords; j++) { + idx = pred_base + i * regs->pred_qwords + j; + if (regs->pred_qwords > 1) { + printed += fprintf(fp, "%5s[%d][%d]:0x%" PRIx64 " ", + name, i, j, regs->simd_data[idx]); + } else { + printed += fprintf(fp, "%5s[%d]:0x%" PRIx64 " ", + name, i, regs->simd_data[idx]); + } + } } return printed; @@ -801,7 +874,8 @@ static int perf_sample__fprintf_iregs(struct perf_sample *sample, return 0; return perf_sample__fprintf_regs(perf_sample__intr_regs(sample), - attr->sample_regs_intr, e_machine, e_flags, fp); + attr, e_machine, e_flags, fp, + /*intr=*/true); } static int perf_sample__fprintf_uregs(struct perf_sample *sample, @@ -814,7 +888,8 @@ static int perf_sample__fprintf_uregs(struct perf_sample *sample, return 0; return perf_sample__fprintf_regs(perf_sample__user_regs(sample), - attr->sample_regs_user, e_machine, e_flags, fp); + attr, e_machine, e_flags, fp, + /*intr=*/false); } static int perf_sample__fprintf_start(struct perf_script *script, diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index df16780bf887..b44fd880d2d5 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -3715,9 +3715,39 @@ int __evsel__parse_sample(struct evsel *evsel, union perf_event *event, array = (void *)array + sz; if (regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) { - /* Skip SIMD-regs header. */ - sz = 4 * sizeof(u64); + u64 attr_nr_vectors = + hweight64(evsel->core.attr.sample_simd_vec_reg_user); + u64 attr_vec_qwords = + evsel->core.attr.sample_simd_vec_reg_qwords; + u64 attr_nr_pred = + hweight32(evsel->core.attr.sample_simd_pred_reg_user); + u64 attr_pred_qwords = + evsel->core.attr.sample_simd_pred_reg_qwords; + + OVERFLOW_CHECK_u64(array); + regs->nr_vectors = *(u64 *)array; + array = (void *)array + sizeof(u64); + OVERFLOW_CHECK_u64(array); + regs->vector_qwords = *(u64 *)array; + array = (void *)array + sizeof(u64); + OVERFLOW_CHECK_u64(array); + regs->nr_pred = *(u64 *)array; + array = (void *)array + sizeof(u64); + OVERFLOW_CHECK_u64(array); + regs->pred_qwords = *(u64 *)array; + array = (void *)array + sizeof(u64); + + if (regs->nr_vectors > attr_nr_vectors || + regs->vector_qwords > attr_vec_qwords || + regs->nr_pred > attr_nr_pred || + regs->pred_qwords > attr_pred_qwords) + goto out_efault; + + sz = (regs->nr_vectors * regs->vector_qwords + + regs->nr_pred * regs->pred_qwords) * sizeof(u64); OVERFLOW_CHECK(array, sz, max_size); + + regs->simd_data = (u64 *)array; array = (void *)array + sz; } } @@ -3779,9 +3809,39 @@ int __evsel__parse_sample(struct evsel *evsel, union perf_event *event, array = (void *)array + sz; if (regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) { - /* Skip SIMD-regs header. */ - sz = 4 * sizeof(u64); + u64 attr_nr_vectors = + hweight64(evsel->core.attr.sample_simd_vec_reg_intr); + u64 attr_vec_qwords = + evsel->core.attr.sample_simd_vec_reg_qwords; + u64 attr_nr_pred = + hweight32(evsel->core.attr.sample_simd_pred_reg_intr); + u64 attr_pred_qwords = + evsel->core.attr.sample_simd_pred_reg_qwords; + + OVERFLOW_CHECK_u64(array); + regs->nr_vectors = *(u64 *)array; + array = (void *)array + sizeof(u64); + OVERFLOW_CHECK_u64(array); + regs->vector_qwords = *(u64 *)array; + array = (void *)array + sizeof(u64); + OVERFLOW_CHECK_u64(array); + regs->nr_pred = *(u64 *)array; + array = (void *)array + sizeof(u64); + OVERFLOW_CHECK_u64(array); + regs->pred_qwords = *(u64 *)array; + array = (void *)array + sizeof(u64); + + if (regs->nr_vectors > attr_nr_vectors || + regs->vector_qwords > attr_vec_qwords || + regs->nr_pred > attr_nr_pred || + regs->pred_qwords > attr_pred_qwords) + goto out_efault; + + sz = (regs->nr_vectors * regs->vector_qwords + + regs->nr_pred * regs->pred_qwords) * sizeof(u64); OVERFLOW_CHECK(array, sz, max_size); + + regs->simd_data = (u64 *)array; array = (void *)array + sz; } } diff --git a/tools/perf/util/sample.h b/tools/perf/util/sample.h index c4eae8b2fd06..cb4b16654876 100644 --- a/tools/perf/util/sample.h +++ b/tools/perf/util/sample.h @@ -16,6 +16,11 @@ struct regs_dump { u64 abi; u64 mask; u64 *regs; + u64 nr_vectors; + u64 vector_qwords; + u64 nr_pred; + u64 pred_qwords; + u64 *simd_data; /* Cached values/mask filled by first register access. */ u64 cache_regs[PERF_SAMPLE_REGS_CACHE_SIZE]; diff --git a/tools/perf/util/scripting-engines/trace-event-python.c b/tools/perf/util/scripting-engines/trace-event-python.c index 11877b56548f..239104b0337d 100644 --- a/tools/perf/util/scripting-engines/trace-event-python.c +++ b/tools/perf/util/scripting-engines/trace-event-python.c @@ -711,8 +711,8 @@ static void set_sample_datasrc_in_dict(PyObject *dict, _PyUnicode_FromString(decode)); } -static void regs_map(struct regs_dump *regs, uint64_t mask, uint16_t e_machine, uint32_t e_flags, - char *bf, int size) +static int regs_map(struct regs_dump *regs, uint64_t mask, uint16_t e_machine, + uint32_t e_flags, char *bf, int size) { unsigned int i = 0, r; int printed = 0; @@ -720,10 +720,10 @@ static void regs_map(struct regs_dump *regs, uint64_t mask, uint16_t e_machine, bf[0] = 0; if (size <= 0) - return; + return 0; if (!regs || !regs->regs) - return; + return 0; for_each_set_bit(r, (unsigned long *) &mask, sizeof(mask) * 8) { u64 val = regs->regs[i++]; @@ -732,6 +732,90 @@ static void regs_map(struct regs_dump *regs, uint64_t mask, uint16_t e_machine, "%5s:0x%" PRIx64 " ", perf_reg_name(r, e_machine, e_flags, regs->abi), val); } + + return printed; +} + +static void simd_regs_map(struct regs_dump *regs, struct perf_event_attr *attr, + uint16_t e_machine, char *bf, int size, int printed, bool intr) +{ + const char *name = "unknown"; + int reg_c, idx, pred_base; + unsigned int i = 0, j; + uint16_t qwords; + + if (size <= 0 || printed >= size) + return; + + if (!regs || !regs->simd_data) + return; + + if (!regs->nr_vectors && !regs->nr_pred) + return; + + for (reg_c = 0; reg_c < 64; reg_c++) { + if (!regs->nr_vectors) + break; + if (intr) { + perf_intr_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/false); + } else { + perf_user_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/false); + } + if (regs->vector_qwords == qwords) { + name = perf_simd_reg_class_name(e_machine, reg_c, /*pred=*/false); + break; + } + } + + for (i = 0; i < regs->nr_vectors; i++) { + for (j = 0; j < regs->vector_qwords; j++) { + idx = i * regs->vector_qwords + j; + if (regs->vector_qwords > 1) { + printed += scnprintf(bf + printed, size - printed, + "%5s[%d][%d]:0x%" PRIx64 " ", + name, i, j, regs->simd_data[idx]); + } else { + printed += scnprintf(bf + printed, size - printed, + "%5s[%d]:0x%" PRIx64 " ", + name, i, regs->simd_data[idx]); + } + } + } + + name = "unknown"; + for (reg_c = 0; reg_c < 64; reg_c++) { + if (!regs->nr_pred) + break; + if (intr) { + perf_intr_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/true); + } else { + perf_user_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/true); + } + if (regs->pred_qwords == qwords) { + name = perf_simd_reg_class_name(e_machine, reg_c, /*pred=*/true); + break; + } + } + + pred_base = regs->nr_vectors * regs->vector_qwords; + for (i = 0; i < regs->nr_pred; i++) { + for (j = 0; j < regs->pred_qwords; j++) { + idx = pred_base + i * regs->pred_qwords + j; + if (regs->pred_qwords > 1) { + printed += scnprintf(bf + printed, size - printed, + "%5s[%d][%d]:0x%" PRIx64 " ", + name, i, j, regs->simd_data[idx]); + } else { + printed += scnprintf(bf + printed, size - printed, + "%5s[%d]:0x%" PRIx64 " ", + name, i, regs->simd_data[idx]); + } + } + } } #define MAX_REG_SIZE 128 @@ -742,16 +826,36 @@ static int set_regs_in_dict(PyObject *dict, uint32_t e_flags) { struct perf_event_attr *attr = &sample->evsel->core.attr; - - int size = (__sw_hweight64(attr->sample_regs_intr) * MAX_REG_SIZE) + 1; + int intr_size, user_size, size; + struct regs_dump *regs; char *bf = NULL; + int printed; + + intr_size = (__sw_hweight64(attr->sample_regs_intr) * MAX_REG_SIZE) + 1; + user_size = (__sw_hweight64(attr->sample_regs_user) * MAX_REG_SIZE) + 1; + if (sample->intr_regs && attr->sample_simd_regs_enabled) { + regs = sample->intr_regs; + intr_size += (regs->nr_vectors * regs->vector_qwords + + regs->nr_pred * regs->pred_qwords) * MAX_REG_SIZE; + } + if (sample->user_regs && attr->sample_simd_regs_enabled) { + regs = sample->user_regs; + user_size += (regs->nr_vectors * regs->vector_qwords + + regs->nr_pred * regs->pred_qwords) * MAX_REG_SIZE; + } + size = intr_size > user_size ? intr_size : user_size; if (sample->intr_regs) { bf = malloc(size); if (!bf) return -1; - regs_map(sample->intr_regs, attr->sample_regs_intr, e_machine, e_flags, bf, size); + printed = regs_map(sample->intr_regs, attr->sample_regs_intr, + e_machine, e_flags, bf, size); + if (attr->sample_simd_regs_enabled) { + simd_regs_map(sample->intr_regs, attr, e_machine, bf, + size, printed, true); + } pydict_set_item_string_decref(dict, "iregs", _PyUnicode_FromString(bf)); @@ -763,7 +867,12 @@ static int set_regs_in_dict(PyObject *dict, if (!bf) return -1; } - regs_map(sample->user_regs, attr->sample_regs_user, e_machine, e_flags, bf, size); + printed = regs_map(sample->user_regs, attr->sample_regs_user, + e_machine, e_flags, bf, size); + if (attr->sample_simd_regs_enabled) { + simd_regs_map(sample->user_regs, attr, e_machine, bf, + size, printed, false); + } pydict_set_item_string_decref(dict, "uregs", _PyUnicode_FromString(bf)); diff --git a/tools/perf/util/session.c b/tools/perf/util/session.c index 90d4e45f09f0..a5b596cd14be 100644 --- a/tools/perf/util/session.c +++ b/tools/perf/util/session.c @@ -1446,6 +1446,86 @@ static void regs_dump__printf(u64 mask, struct regs_dump *regs, } } +static void simd_regs_dump__printf(uint16_t e_machine, struct regs_dump *regs, bool intr) +{ + const char *name = "unknown"; + const char *simd_header; + u32 i, j, idx, pred_base; + uint16_t qwords; + int reg_c; + + if (!(regs->abi & PERF_SAMPLE_REGS_ABI_SIMD)) + return; + + if (!regs->nr_vectors && !regs->nr_pred) + return; + + simd_header = "... SIMD ABI nr_vectors %" PRIu64 " vector_qwords %" PRIu64 \ + " nr_pred %" PRIu64 " pred_qwords %" PRIu64 "\n"; + printf(simd_header, regs->nr_vectors, regs->vector_qwords, + regs->nr_pred, regs->pred_qwords); + + for (reg_c = 0; reg_c < 64; reg_c++) { + if (!regs->nr_vectors) + break; + if (intr) { + perf_intr_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/false); + } else { + perf_user_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/false); + } + if (regs->vector_qwords == qwords) { + name = perf_simd_reg_class_name(e_machine, reg_c, /*pred=*/false); + break; + } + } + + for (i = 0; i < regs->nr_vectors; i++) { + for (j = 0; j < regs->vector_qwords; j++) { + idx = i * regs->vector_qwords + j; + if (regs->vector_qwords > 1) { + printf(".... %3s[%d][%d] 0x%016" PRIx64 "\n", + name, i, j, regs->simd_data[idx]); + } else { + printf(".... %3s[%d] 0x%016" PRIx64 "\n", + name, i, regs->simd_data[idx]); + } + } + } + + name = "unknown"; + for (reg_c = 0; reg_c < 64; reg_c++) { + if (!regs->nr_pred) + break; + if (intr) { + perf_intr_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/true); + } else { + perf_user_simd_reg_class_bitmap_qwords(e_machine, reg_c, + &qwords, /*pred=*/true); + } + if (regs->pred_qwords == qwords) { + name = perf_simd_reg_class_name(e_machine, reg_c, /*pred=*/true); + break; + } + } + + pred_base = regs->nr_vectors * regs->vector_qwords; + for (i = 0; i < regs->nr_pred; i++) { + for (j = 0; j < regs->pred_qwords; j++) { + idx = pred_base + i * regs->pred_qwords + j; + if (regs->pred_qwords > 1) { + printf(".... %3s[%d][%d] 0x%016" PRIx64 "\n", + name, i, j, regs->simd_data[idx]); + } else { + printf(".... %3s[%d] 0x%016" PRIx64 "\n", + name, i, regs->simd_data[idx]); + } + } + } +} + static const char *regs_abi[] = { [PERF_SAMPLE_REGS_ABI_NONE] = "none", [PERF_SAMPLE_REGS_ABI_32] = "32-bit", @@ -1486,6 +1566,7 @@ static void regs_user__printf(struct perf_sample *sample, uint16_t e_machine, ui if (user_regs->regs) regs__printf("user", user_regs, e_machine, e_flags); + simd_regs_dump__printf(e_machine, user_regs, /*intr=*/false); } static void regs_intr__printf(struct perf_sample *sample, uint16_t e_machine, uint32_t e_flags) @@ -1499,6 +1580,7 @@ static void regs_intr__printf(struct perf_sample *sample, uint16_t e_machine, ui if (intr_regs->regs) regs__printf("intr", intr_regs, e_machine, e_flags); + simd_regs_dump__printf(e_machine, intr_regs, /*intr=*/true); } static void stack_user__printf(struct stack_dump *dump) diff --git a/tools/perf/util/synthetic-events.c b/tools/perf/util/synthetic-events.c index 2779f63a442c..6c5e2327fdfa 100644 --- a/tools/perf/util/synthetic-events.c +++ b/tools/perf/util/synthetic-events.c @@ -1644,8 +1644,13 @@ size_t perf_event__sample_event_size(const struct perf_sample *sample, u64 type, if (sample->user_regs && sample->user_regs->abi) { result += sizeof(u64); sz = hweight64(sample->user_regs->mask) * sizeof(u64); - if (sample->user_regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) + if (sample->user_regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) { sz += 4 * sizeof(u64); + sz += (sample->user_regs->nr_vectors * + sample->user_regs->vector_qwords + + sample->user_regs->nr_pred * + sample->user_regs->pred_qwords) * sizeof(u64); + } result += sz; } else { result += sizeof(u64); @@ -1674,8 +1679,13 @@ size_t perf_event__sample_event_size(const struct perf_sample *sample, u64 type, if (sample->intr_regs && sample->intr_regs->abi) { result += sizeof(u64); sz = hweight64(sample->intr_regs->mask) * sizeof(u64); - if (sample->intr_regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) + if (sample->intr_regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) { sz += 4 * sizeof(u64); + sz += (sample->intr_regs->nr_vectors * + sample->intr_regs->vector_qwords + + sample->intr_regs->nr_pred * + sample->intr_regs->pred_qwords) * sizeof(u64); + } result += sz; } else { result += sizeof(u64); @@ -1863,6 +1873,18 @@ int perf_event__synthesize_sample(union perf_event *event, u64 type, u64 read_fo sz = hweight64(sample->user_regs->mask) * sizeof(u64); memcpy(array, sample->user_regs->regs, sz); array = (void *)array + sz; + if (sample->user_regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) { + *array++ = sample->user_regs->nr_vectors; + *array++ = sample->user_regs->vector_qwords; + *array++ = sample->user_regs->nr_pred; + *array++ = sample->user_regs->pred_qwords; + sz = (sample->user_regs->nr_vectors * + sample->user_regs->vector_qwords + + sample->user_regs->nr_pred * + sample->user_regs->pred_qwords) * sizeof(u64); + memcpy(array, sample->user_regs->simd_data, sz); + array = (void *)array + sz; + } } else { *array++ = 0; } @@ -1899,6 +1921,18 @@ int perf_event__synthesize_sample(union perf_event *event, u64 type, u64 read_fo sz = hweight64(sample->intr_regs->mask) * sizeof(u64); memcpy(array, sample->intr_regs->regs, sz); array = (void *)array + sz; + if (sample->intr_regs->abi & PERF_SAMPLE_REGS_ABI_SIMD) { + *array++ = sample->intr_regs->nr_vectors; + *array++ = sample->intr_regs->vector_qwords; + *array++ = sample->intr_regs->nr_pred; + *array++ = sample->intr_regs->pred_qwords; + sz = (sample->intr_regs->nr_vectors * + sample->intr_regs->vector_qwords + + sample->intr_regs->nr_pred * + sample->intr_regs->pred_qwords) * sizeof(u64); + memcpy(array, sample->intr_regs->simd_data, sz); + array = (void *)array + sz; + } } else { *array++ = 0; } From 1f2177e9e05a19fe50b4ce41ad136c8d906349d4 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:47 +0800 Subject: [PATCH 072/425] perf dwarf-regs: Add SIMD/eGPRs support for x86 DWARF registers Enhance the x86-specific DWARF register handling by adding support for SIMD and eGPRs registers. This update is based on the "DWARF Register Number Mapping" table from the "System V Application Binary Interface AMD64 Architecture Processor Supplement" (version 1.0). Modifications include: - Updating the x86_64_regidx_table[] array to incorporate SIMD and eGPRs registers. - Enhancing the __get_dwarf_regnum_for_perf_regnum_x86_64() function to retrieve the DWARF register index for eGPRs. Since currently libdw doesn't support SIMD and APX eGPRs registers yet, still keep the original return value for get_libdw_frame_nregs() helper. Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- .../util/dwarf-regs-arch/dwarf-regs-x86.c | 138 +++++++++++++++--- tools/perf/util/dwarf-regs.c | 9 +- tools/perf/util/include/dwarf-regs.h | 7 +- tools/perf/util/unwind-libdw.c | 6 +- 4 files changed, 132 insertions(+), 28 deletions(-) diff --git a/tools/perf/util/dwarf-regs-arch/dwarf-regs-x86.c b/tools/perf/util/dwarf-regs-arch/dwarf-regs-x86.c index c2a70e03b1a9..bb17d779036a 100644 --- a/tools/perf/util/dwarf-regs-arch/dwarf-regs-x86.c +++ b/tools/perf/util/dwarf-regs-arch/dwarf-regs-x86.c @@ -90,22 +90,22 @@ static const struct dwarf_regs_idx x86_64_regidx_table[] = { { "r14", 14 }, { "r14d", 14 }, { "r14w", 14 }, { "r14b", 14 }, { "r15", 15 }, { "r15d", 15 }, { "r15w", 15 }, { "r15b", 15 }, // 16 - Return Address RA - { "xmm0", 17}, - { "xmm1", 18}, - { "xmm2", 19}, - { "xmm3", 20}, - { "xmm4", 21}, - { "xmm5", 22}, - { "xmm6", 23}, - { "xmm7", 24}, - { "xmm8", 25}, - { "xmm9", 26}, - { "xmm10", 27}, - { "xmm11", 28}, - { "xmm12", 29}, - { "xmm13", 30}, - { "xmm14", 31}, - { "xmm15", 32}, + { "zmm0", 17 }, { "ymm0", 17 }, { "xmm0", 17 }, + { "zmm1", 18 }, { "ymm1", 18 }, { "xmm1", 18 }, + { "zmm2", 19 }, { "ymm2", 19 }, { "xmm2", 19 }, + { "zmm3", 20 }, { "ymm3", 20 }, { "xmm3", 20 }, + { "zmm4", 21 }, { "ymm4", 21 }, { "xmm4", 21 }, + { "zmm5", 22 }, { "ymm5", 22 }, { "xmm5", 22 }, + { "zmm6", 23 }, { "ymm6", 23 }, { "xmm6", 23 }, + { "zmm7", 24 }, { "ymm7", 24 }, { "xmm7", 24 }, + { "zmm8", 25 }, { "ymm8", 25 }, { "xmm8", 25 }, + { "zmm9", 26 }, { "ymm9", 26 }, { "xmm9", 26 }, + { "zmm10", 27 }, { "ymm10", 27 }, { "xmm10", 27 }, + { "zmm11", 28 }, { "ymm11", 28 }, { "xmm11", 28 }, + { "zmm12", 29 }, { "ymm12", 29 }, { "xmm12", 29 }, + { "zmm13", 30 }, { "ymm13", 30 }, { "xmm13", 30 }, + { "zmm14", 31 }, { "ymm14", 31 }, { "xmm14", 31 }, + { "zmm15", 32 }, { "ymm15", 32 }, { "xmm15", 32 }, { "st0", 33}, { "st1", 34}, { "st2", 35}, @@ -129,7 +129,7 @@ static const struct dwarf_regs_idx x86_64_regidx_table[] = { { "ds", 53}, { "fs", 54}, { "gs", 55}, - // 56-47 - reserved + // 56-57 - reserved { "fs.base", 58}, { "gs.base", 59}, // 60-61 - reserved @@ -138,6 +138,49 @@ static const struct dwarf_regs_idx x86_64_regidx_table[] = { { "mxcsr", 64}, // 128-bit Media Control and Status { "fcw", 65}, // x87 Control Word { "fsw", 66}, // x87 Status Word + // 67-82 - Upper Vector Registers 16–31 + { "zmm16", 67 }, { "ymm16", 67 }, { "xmm16", 67 }, + { "zmm17", 68 }, { "ymm17", 68 }, { "xmm17", 68 }, + { "zmm18", 69 }, { "ymm18", 69 }, { "xmm18", 69 }, + { "zmm19", 70 }, { "ymm19", 70 }, { "xmm19", 70 }, + { "zmm20", 71 }, { "ymm20", 71 }, { "xmm20", 71 }, + { "zmm21", 72 }, { "ymm21", 72 }, { "xmm21", 72 }, + { "zmm22", 73 }, { "ymm22", 73 }, { "xmm22", 73 }, + { "zmm23", 74 }, { "ymm23", 74 }, { "xmm23", 74 }, + { "zmm24", 75 }, { "ymm24", 75 }, { "xmm24", 75 }, + { "zmm25", 76 }, { "ymm25", 76 }, { "xmm25", 76 }, + { "zmm26", 77 }, { "ymm26", 77 }, { "xmm26", 77 }, + { "zmm27", 78 }, { "ymm27", 78 }, { "xmm27", 78 }, + { "zmm28", 79 }, { "ymm28", 79 }, { "xmm28", 79 }, + { "zmm29", 80 }, { "ymm29", 80 }, { "xmm29", 80 }, + { "zmm30", 81 }, { "ymm30", 81 }, { "xmm30", 81 }, + { "zmm31", 82 }, { "ymm31", 82 }, { "xmm31", 82 }, + // 118-125 - Vector Mask Registers 0–7 + { "k0", 118 }, + { "k1", 119 }, + { "k2", 120 }, + { "k3", 121 }, + { "k4", 122 }, + { "k5", 123 }, + { "k6", 124 }, + { "k7", 125 }, + // 130-145 - APX Integer Registers 16-31 + { "r16", 130 }, { "r16d", 130 }, { "r16w", 130 }, { "r16b", 130 }, + { "r17", 131 }, { "r17d", 131 }, { "r17w", 131 }, { "r17b", 131 }, + { "r18", 132 }, { "r18d", 132 }, { "r18w", 132 }, { "r18b", 132 }, + { "r19", 133 }, { "r19d", 133 }, { "r19w", 133 }, { "r19b", 133 }, + { "r20", 134 }, { "r20d", 134 }, { "r20w", 134 }, { "r20b", 134 }, + { "r21", 135 }, { "r21d", 135 }, { "r21w", 135 }, { "r21b", 135 }, + { "r22", 136 }, { "r22d", 136 }, { "r22w", 136 }, { "r22b", 136 }, + { "r23", 137 }, { "r23d", 137 }, { "r23w", 137 }, { "r23b", 137 }, + { "r24", 138 }, { "r24d", 138 }, { "r24w", 138 }, { "r24b", 138 }, + { "r25", 139 }, { "r25d", 139 }, { "r25w", 139 }, { "r25b", 139 }, + { "r26", 140 }, { "r26d", 140 }, { "r26w", 140 }, { "r26b", 140 }, + { "r27", 141 }, { "r27d", 141 }, { "r27w", 141 }, { "r27b", 141 }, + { "r28", 142 }, { "r28d", 142 }, { "r28w", 142 }, { "r28b", 142 }, + { "r29", 143 }, { "r29d", 143 }, { "r29w", 143 }, { "r29b", 143 }, + { "r30", 144 }, { "r30d", 144 }, { "r30w", 144 }, { "r30b", 144 }, + { "r31", 145 }, { "r31d", 145 }, { "r31w", 145 }, { "r31b", 145 }, // End of regular dwarf registers. { "rip", DWARF_REG_PC }, { "eip", DWARF_REG_PC }, { "ip", DWARF_REG_PC }, }; @@ -204,7 +247,7 @@ int __get_dwarf_regnum_for_perf_regnum_i386(int perf_regnum) return dwarf_i386_regnums[perf_regnum]; } -int __get_dwarf_regnum_for_perf_regnum_x86_64(int perf_regnum) +int __get_dwarf_regnum_for_perf_regnum_x86_64(int perf_regnum, int abi) { static const int dwarf_x86_64_regnums[] = { [PERF_REG_X86_AX] = 0, @@ -248,13 +291,66 @@ int __get_dwarf_regnum_for_perf_regnum_x86_64(int perf_regnum) [PERF_REG_X86_XMM14] = 31, [PERF_REG_X86_XMM15] = 32, }; + static const int dwarf_x86_64_regnums_apx[] = { + [PERF_REG_X86_AX] = 0, + [PERF_REG_X86_BX] = 3, + [PERF_REG_X86_CX] = 2, + [PERF_REG_X86_DX] = 1, + [PERF_REG_X86_SI] = 4, + [PERF_REG_X86_DI] = 5, + [PERF_REG_X86_BP] = 6, + [PERF_REG_X86_SP] = 7, + [PERF_REG_X86_IP] = 16, + [PERF_REG_X86_FLAGS] = 49, + [PERF_REG_X86_CS] = 51, + [PERF_REG_X86_SS] = 52, + [PERF_REG_X86_DS] = 53, + [PERF_REG_X86_ES] = 50, + [PERF_REG_X86_FS] = 54, + [PERF_REG_X86_GS] = 55, + [PERF_REG_X86_R8] = 8, + [PERF_REG_X86_R9] = 9, + [PERF_REG_X86_R10] = 10, + [PERF_REG_X86_R11] = 11, + [PERF_REG_X86_R12] = 12, + [PERF_REG_X86_R13] = 13, + [PERF_REG_X86_R14] = 14, + [PERF_REG_X86_R15] = 15, + [PERF_REG_X86_R16] = 130, + [PERF_REG_X86_R17] = 131, + [PERF_REG_X86_R18] = 132, + [PERF_REG_X86_R19] = 133, + [PERF_REG_X86_R20] = 134, + [PERF_REG_X86_R21] = 135, + [PERF_REG_X86_R22] = 136, + [PERF_REG_X86_R23] = 137, + [PERF_REG_X86_R24] = 138, + [PERF_REG_X86_R25] = 139, + [PERF_REG_X86_R26] = 140, + [PERF_REG_X86_R27] = 141, + [PERF_REG_X86_R28] = 142, + [PERF_REG_X86_R29] = 143, + [PERF_REG_X86_R30] = 144, + [PERF_REG_X86_R31] = 145, + }; if (perf_regnum == 0) return 0; - if (perf_regnum < 0 || perf_regnum >= (int)ARRAY_SIZE(dwarf_x86_64_regnums) || - dwarf_x86_64_regnums[perf_regnum] == 0) + if (perf_regnum < 0) return -ENOENT; - return dwarf_x86_64_regnums[perf_regnum]; + if (!(abi & PERF_SAMPLE_REGS_ABI_SIMD) && + (perf_regnum >= (int)ARRAY_SIZE(dwarf_x86_64_regnums) || + dwarf_x86_64_regnums[perf_regnum] == 0)) + return -ENOENT; + + if ((abi & PERF_SAMPLE_REGS_ABI_SIMD) && + (perf_regnum >= (int)ARRAY_SIZE(dwarf_x86_64_regnums_apx) || + dwarf_x86_64_regnums_apx[perf_regnum] == 0)) + return -ENOENT; + + return abi & PERF_SAMPLE_REGS_ABI_SIMD ? + dwarf_x86_64_regnums_apx[perf_regnum] : + dwarf_x86_64_regnums[perf_regnum]; } diff --git a/tools/perf/util/dwarf-regs.c b/tools/perf/util/dwarf-regs.c index 797f455eba0d..67f38342247e 100644 --- a/tools/perf/util/dwarf-regs.c +++ b/tools/perf/util/dwarf-regs.c @@ -158,6 +158,10 @@ static int get_libdw_frame_nregs(unsigned int machine, unsigned int flags __mayb { switch (machine) { case EM_X86_64: + /* + * Since libdw doesn't support SIMD and APX eGPR register yet, + * still keep nregs to 17. + */ return 17; case EM_386: return 9; @@ -187,13 +191,14 @@ static int get_libdw_frame_nregs(unsigned int machine, unsigned int flags __mayb } int get_dwarf_regnum_for_perf_regnum(int perf_regnum, unsigned int machine, - unsigned int flags, bool only_libdw_supported) + unsigned int flags, + bool only_libdw_supported, int abi) { int reg; switch (machine) { case EM_X86_64: - reg = __get_dwarf_regnum_for_perf_regnum_x86_64(perf_regnum); + reg = __get_dwarf_regnum_for_perf_regnum_x86_64(perf_regnum, abi); break; case EM_386: reg = __get_dwarf_regnum_for_perf_regnum_i386(perf_regnum); diff --git a/tools/perf/util/include/dwarf-regs.h b/tools/perf/util/include/dwarf-regs.h index 46a764cf322f..92cf0af93e9e 100644 --- a/tools/perf/util/include/dwarf-regs.h +++ b/tools/perf/util/include/dwarf-regs.h @@ -103,7 +103,7 @@ int __get_csky_regnum(const char *name, unsigned int flags); int __get_dwarf_regnum_i386(const char *name); int __get_dwarf_regnum_x86_64(const char *name); int __get_dwarf_regnum_for_perf_regnum_i386(int perf_regnum); -int __get_dwarf_regnum_for_perf_regnum_x86_64(int perf_regnum); +int __get_dwarf_regnum_for_perf_regnum_x86_64(int perf_regnum, int abi); int __get_dwarf_regnum_for_perf_regnum_arm(int perf_regnum); int __get_dwarf_regnum_for_perf_regnum_arm64(int perf_regnum); @@ -125,8 +125,9 @@ int get_dwarf_regnum(const char *name, unsigned int machine, unsigned int flags) /* * get_dwarf_regnum - Returns DWARF regnum from perf register number. */ -int get_dwarf_regnum_for_perf_regnum(int perf_regnum, unsigned int machine, unsigned int flags, - bool only_libdw_supported); +int get_dwarf_regnum_for_perf_regnum(int perf_regnum, unsigned int machine, + unsigned int flags, + bool only_libdw_supported, int abi); void get_powerpc_regs(u32 raw_insn, int is_source, struct annotated_op_loc *op_loc); diff --git a/tools/perf/util/unwind-libdw.c b/tools/perf/util/unwind-libdw.c index 8d9fa6fd81c3..ebea101e1001 100644 --- a/tools/perf/util/unwind-libdw.c +++ b/tools/perf/util/unwind-libdw.c @@ -290,7 +290,8 @@ static bool libdw_set_initial_registers(Dwfl_Thread *thread, void *arg) int dwarf_reg = get_dwarf_regnum_for_perf_regnum(perf_reg, e_machine, e_flags, - /*only_libdw_supported=*/true); + /*only_libdw_supported=*/true, + user_regs->abi); if (dwarf_reg > max_dwarf_reg) max_dwarf_reg = dwarf_reg; } @@ -305,7 +306,8 @@ static bool libdw_set_initial_registers(Dwfl_Thread *thread, void *arg) int dwarf_reg = get_dwarf_regnum_for_perf_regnum(perf_reg, e_machine, e_flags, - /*only_libdw_supported=*/true); + /*only_libdw_supported=*/true, + user_regs->abi); if (dwarf_reg >= 0) { val = 0; if (perf_reg_value(&val, user_regs, perf_reg) == 0) From 7db989086ac94e334c675d3dc7c2f342d8fdb935 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:48 +0800 Subject: [PATCH 073/425] perf tests: Add x86 eGPRs/SSP registers sampling test Validate that eGPRs (extended general purpose registers) and SSP (shadow stack pointer) registers can be sampled correctly on x86 platforms. Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/record.sh | 119 +++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tools/perf/tests/shell/record.sh b/tools/perf/tests/shell/record.sh index 625240809fce..770a546f8d45 100755 --- a/tools/perf/tests/shell/record.sh +++ b/tools/perf/tests/shell/record.sh @@ -131,6 +131,124 @@ check_system_wide() { perf report -i "${perfdata}" -q | grep -q "${testsym}" } +check_ext_reg_capture() { + local script_field="$1" + local ext_reg="$2" + + perf script -F ip,sym,"${script_field}" -i "${perfdata}" 2>/dev/null | \ + grep -q -i "${ext_reg}:" +} + +get_x86_extended_regs() { + local advertised_ext_regs="$1" + local ext_regs="" + + if echo "${advertised_ext_regs}" | grep -q -i R16 + then + ext_regs="${ext_regs} R16" + fi + if echo "${advertised_ext_regs}" | grep -q -i R31 + then + ext_regs="${ext_regs} R31" + fi + if echo "${advertised_ext_regs}" | grep -q -i SSP + then + ext_regs="${ext_regs} SSP" + fi + + echo "${ext_regs}" | xargs +} + +validate_extd_regs_sampling() { + local regs_opt="$1" + local extd_regs="$2" + local script_field="$3" + local ret=0 + local reg + + for reg in ${extd_regs} + do + perf_record_with_retry "${perfdata}" \ + "check_ext_reg_capture ${script_field} ${reg}" "perf test -w thloop" \ + -e br_inst_retired.near_call ${regs_opt}=${reg} -c 1000 \ + --per-thread || ret=$? + + if [ $ret -ne 0 ] + then + echo "Extended register capture test [Failed record ${regs_opt}=${reg}]" + return 1 + fi + done + + return 0 +} + +test_extd_register_capture() { + local arch + local intr_regs + local user_regs + local intr_ext_regs + local user_ext_regs + local tested=0 + + echo "Extended register capture test" + if ! perf list pmu | grep -q 'br_inst_retired.near_call' + then + echo "Extended register capture test [Skipped missing event]" + return + fi + + intr_regs=$(perf record --intr-regs=\? 2>&1 || true) + user_regs=$(perf record --user-regs=\? 2>&1 || true) + + intr_ext_regs="" + user_ext_regs="" + + arch=$(uname -m) + case ${arch} in + x86_64|i386) + intr_ext_regs=$(get_x86_extended_regs "${intr_regs}") + user_ext_regs=$(get_x86_extended_regs "${user_regs}") + ;; + *) + echo "Extended register capture test [Skipped non-x86 platform]" + return + ;; + esac + + if [ -z "${intr_ext_regs}" ] + then + echo "Extended register capture test [Skipped missing intr extended registers]" + elif ! validate_extd_regs_sampling "--intr-regs" "${intr_ext_regs}" "iregs" + then + echo "Extended register capture test [Failed intr extended register sampling]" + err=1 + return + else + tested=1 + fi + + if [ -z "${user_ext_regs}" ] + then + echo "Extended register capture test [Skipped missing user extended registers]" + elif ! validate_extd_regs_sampling "--user-regs" "${user_ext_regs}" "uregs" + then + echo "Extended register capture test [Failed user extended register sampling]" + err=1 + return + else + tested=1 + fi + + if [ ${tested} -eq 0 ] + then + echo "Extended register capture test [Skipped missing extended registers]" + return + fi + + echo "Extended register capture test [Success]" +} + test_system_wide() { echo "Basic --system-wide mode test" local ret=0 @@ -491,6 +609,7 @@ fi test_per_thread test_register_capture +test_extd_register_capture test_system_wide test_workload test_branch_counter From 193e66b9e2d29d485ba25fa3231dd424c699e7c5 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Mon, 14 Sep 2026 09:40:49 +0800 Subject: [PATCH 074/425] perf tests: Add SIMD registers sampling test Validate that SIMD registers (XMM/YMM/ZMM/OPMASK on x86) can be sampled correctly with hardware support. Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/record.sh | 138 +++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/tools/perf/tests/shell/record.sh b/tools/perf/tests/shell/record.sh index 770a546f8d45..4a4931cb5ba9 100755 --- a/tools/perf/tests/shell/record.sh +++ b/tools/perf/tests/shell/record.sh @@ -249,6 +249,143 @@ test_extd_register_capture() { echo "Extended register capture test [Success]" } +extract_x86_advertised_simd_classes() { + local regs_output="$1" + + echo "${regs_output}" \ + | grep -oE '(ZMM|YMM|XMM|OPMASK)[0-9]+-[0-9]+' \ + | sed -E 's/[0-9]+-[0-9]+$//' \ + | tr '[:upper:]' '[:lower:]' \ + | sort -u +} + +ordered_x86_simd_classes() { + local advertised_classes="$1" + local simd_classes="" + + if echo "${advertised_classes}" | grep -qw zmm + then + simd_classes="${simd_classes} zmm" + fi + if echo "${advertised_classes}" | grep -qw ymm + then + simd_classes="${simd_classes} ymm" + fi + if echo "${advertised_classes}" | grep -qw xmm + then + simd_classes="${simd_classes} xmm" + fi + if echo "${advertised_classes}" | grep -qw opmask + then + simd_classes="${simd_classes} opmask" + fi + + echo "${simd_classes}" | xargs +} + +check_simd_reg_capture() { + local script_field="$1" + local simd_reg="$2" + + perf script -F ip,sym,"${script_field}" -i "${perfdata}" 2>/dev/null | \ + grep -q -i "${simd_reg}\[" +} + +validate_simd_regs_sampling() { + local regs_opt="$1" + local simd_classes="$2" + local script_field="$3" + local simd_class + local ret=0 + + for simd_class in ${simd_classes} + do + perf_record_with_retry "${perfdata}" \ + "check_simd_reg_capture ${script_field} ${simd_class}" "perf test -w thloop" \ + -e br_inst_retired.near_call ${regs_opt}=${simd_class} -c 1000 \ + --per-thread || ret=$? + + if [ $ret -ne 0 ] + then + echo "SIMD register capture test [Failed record ${regs_opt}=${simd_class}]" + return 1 + fi + done + + return 0 +} + +test_simd_register_capture() { + local arch + local intr_regs + local user_regs + local simd_classes + local user_simd_classes + local advertised_intr_classes + local advertised_user_classes + local tested=0 + + echo "SIMD register capture test" + if ! perf list pmu | grep -q 'br_inst_retired.near_call' + then + echo "SIMD register capture test [Skipped missing event]" + return + fi + + intr_regs=$(perf record --intr-regs=\? 2>&1 || true) + user_regs=$(perf record --user-regs=\? 2>&1 || true) + + simd_classes="" + user_simd_classes="" + + arch=$(uname -m) + case ${arch} in + x86_64|i386) + advertised_intr_classes=$(extract_x86_advertised_simd_classes "${intr_regs}") + advertised_user_classes=$(extract_x86_advertised_simd_classes "${user_regs}") + + simd_classes=$(ordered_x86_simd_classes "${advertised_intr_classes}") + user_simd_classes=$(ordered_x86_simd_classes "${advertised_user_classes}") + ;; + *) + echo "SIMD register capture test [Skipped non-x86 platform]" + return + ;; + esac + + if [ -z "${simd_classes}" ] + then + echo "SIMD register capture test [Skipped missing intr SIMD registers]" + elif ! validate_simd_regs_sampling "--intr-regs" "${simd_classes}" "iregs" + then + echo "SIMD register capture test [Failed intr SIMD register sampling]" + err=1 + return + else + tested=1 + fi + + if [ -z "${user_simd_classes}" ] + then + echo "SIMD register capture test [Skipped missing user SIMD registers]" + elif ! validate_simd_regs_sampling "--user-regs" "${user_simd_classes}" "uregs" + then + echo "SIMD register capture test [Failed user SIMD register sampling]" + err=1 + return + else + tested=1 + fi + + if [ ${tested} -eq 0 ] + then + echo "SIMD register capture test [Skipped missing SIMD registers]" + return + fi + + echo "SIMD register capture test [Success]" +} + test_system_wide() { echo "Basic --system-wide mode test" local ret=0 @@ -610,6 +747,7 @@ fi test_per_thread test_register_capture test_extd_register_capture +test_simd_register_capture test_system_wide test_workload test_branch_counter From 91b0782fc9e9d2f0a40b5256146e014802fdbb36 Mon Sep 17 00:00:00 2001 From: Namhyung Kim Date: Sun, 13 Sep 2026 23:45:32 -0700 Subject: [PATCH 075/425] perf annotate-data: Convert type histogram to hashmap The type histogram maintains sample counts and periods per offset. Use a hashmap instead of an array to reduce the memory overhead. No functional changes intended. Signed-off-by: Namhyung Kim Reviewed-by: Tengda Wu Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/ui/browsers/annotate-data.c | 10 ++-- tools/perf/util/annotate-data.c | 72 ++++++++++++++++---------- tools/perf/util/annotate-data.h | 8 +-- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/tools/perf/ui/browsers/annotate-data.c b/tools/perf/ui/browsers/annotate-data.c index aa8c89fe2e82..c6e07a9b6408 100644 --- a/tools/perf/ui/browsers/annotate-data.c +++ b/tools/perf/ui/browsers/annotate-data.c @@ -62,12 +62,16 @@ static int get_member_overhead(struct annotated_data_type *adt, k = 0; for_each_group_evsel(evsel, leader) { + struct type_hist_entry *hist; + if (symbol_conf.skip_empty && evsel__hists(evsel)->stats.nr_samples == 0) continue; - h = adt->histograms[evsel->core.idx]; - update_hist_entry(&entry->hists[k++], &h->addr[offset]); + h = &adt->histograms[evsel->core.idx]; + if (hashmap__find(&h->samples, offset, &hist)) + update_hist_entry(&entry->hists[k], hist); + k++; } } return 0; @@ -416,7 +420,7 @@ static void browser__write(struct ui_browser *uib, void *entry, int row) /* print the number */ for_each_group_evsel(evsel, leader) { - struct type_hist *h = adt->histograms[evsel->core.idx]; + struct type_hist *h = &adt->histograms[evsel->core.idx]; if (symbol_conf.skip_empty && evsel__hists(evsel)->stats.nr_samples == 0) diff --git a/tools/perf/util/annotate-data.c b/tools/perf/util/annotate-data.c index 4e4c58764082..aff60a630fd0 100644 --- a/tools/perf/util/annotate-data.c +++ b/tools/perf/util/annotate-data.c @@ -1750,42 +1750,45 @@ struct annotated_data_type *find_data_type(struct data_loc_info *dloc) return dso__findnew_data_type(dso, &type_die); } +static size_t data_type_hash(long key, void *ctx __maybe_unused) +{ + return key; +} + +static bool data_type_equal(long key1, long key2, void *ctx __maybe_unused) +{ + return key1 == key2; +} + static int alloc_data_type_histograms(struct annotated_data_type *adt, int nr_entries) { int i; - size_t sz = sizeof(struct type_hist); - sz += sizeof(struct type_hist_entry) * adt->self.size; - - /* Allocate a table of pointers for each event */ + /* Allocate a histogram for each event */ adt->histograms = calloc(nr_entries, sizeof(*adt->histograms)); if (adt->histograms == NULL) return -ENOMEM; - /* - * Each histogram is allocated for the whole size of the type. - * TODO: Probably we can move the histogram to members. - */ for (i = 0; i < nr_entries; i++) { - adt->histograms[i] = zalloc(sz); - if (adt->histograms[i] == NULL) - goto err; + hashmap__init(&adt->histograms[i].samples, data_type_hash, + data_type_equal, /*ctx=*/NULL); } adt->nr_histograms = nr_entries; return 0; - -err: - while (--i >= 0) - zfree(&(adt->histograms[i])); - zfree(&adt->histograms); - return -ENOMEM; } static void delete_data_type_histograms(struct annotated_data_type *adt) { - for (int i = 0; i < adt->nr_histograms; i++) - zfree(&(adt->histograms[i])); + for (int i = 0; i < adt->nr_histograms; i++) { + struct hashmap *map = &adt->histograms[i].samples; + struct hashmap_entry *pos, *tmp; + size_t bkt; + + hashmap__for_each_entry_safe(map, pos, tmp, bkt) + free(pos->pvalue); + hashmap__clear(map); + } zfree(&adt->histograms); adt->nr_histograms = 0; @@ -1824,6 +1827,7 @@ int annotated_data_type__update_samples(struct annotated_data_type *adt, int nr_samples, u64 period) { struct type_hist *h; + struct type_hist_entry *entry; if (adt == NULL) return 0; @@ -1838,12 +1842,23 @@ int annotated_data_type__update_samples(struct annotated_data_type *adt, if (offset < 0 || offset >= adt->self.size) return -1; - h = adt->histograms[evsel->core.idx]; + h = &adt->histograms[evsel->core.idx]; h->nr_samples += nr_samples; - h->addr[offset].nr_samples += nr_samples; h->period += period; - h->addr[offset].period += period; + + if (!hashmap__find(&h->samples, offset, &entry)) { + entry = zalloc(sizeof(*entry)); + if (entry == NULL) + return -1; + + if (hashmap__append(&h->samples, offset, entry) < 0) { + free(entry); + return -1; + } + } + entry->nr_samples += nr_samples; + entry->period += period; return 0; } @@ -1911,14 +1926,14 @@ static void print_annotated_data_type(struct annotated_data_type *mem_type, struct evsel *evsel, int indent) { struct annotated_member *child; - struct type_hist *h = mem_type->histograms[evsel->core.idx]; + struct type_hist *h; int i, nr_events = 0, samples = 0; u64 period = 0; int width = symbol_conf.show_total_period ? 11 : 7; struct evsel *pos; for_each_group_evsel(pos, evsel) { - h = mem_type->histograms[pos->core.idx]; + h = &mem_type->histograms[pos->core.idx]; if (symbol_conf.skip_empty && evsel__hists(pos)->stats.nr_samples == 0) @@ -1927,8 +1942,13 @@ static void print_annotated_data_type(struct annotated_data_type *mem_type, samples = 0; period = 0; for (i = 0; i < member->size; i++) { - samples += h->addr[member->offset + i].nr_samples; - period += h->addr[member->offset + i].period; + struct type_hist_entry *entry; + + if (!hashmap__find(&h->samples, member->offset + i, &entry)) + continue; + + samples += entry->nr_samples; + period += entry->period; } print_annotated_data_value(h, period, samples); nr_events++; diff --git a/tools/perf/util/annotate-data.h b/tools/perf/util/annotate-data.h index c26130744260..ca2096a9ee62 100644 --- a/tools/perf/util/annotate-data.h +++ b/tools/perf/util/annotate-data.h @@ -73,12 +73,12 @@ struct type_hist_entry { * struct type_hist - Type histogram for each event * @nr_samples: Total number of samples in this data type * @period: Total count of the event in this data type - * @offset: Array of histogram entry + * @samples: Hashmap of (offset, type_hist_entry) */ struct type_hist { u64 nr_samples; u64 period; - struct type_hist_entry addr[]; + struct hashmap samples; }; /** @@ -86,7 +86,7 @@ struct type_hist { * @node: RB-tree node for dso->type_tree * @self: Actual type information * @nr_histogram: Number of histogram entries - * @histograms: An array of pointers to histograms + * @histograms: An array of histograms * * This represents a data type accessed by samples in the profile data. */ @@ -94,7 +94,7 @@ struct annotated_data_type { struct rb_node node; struct annotated_member self; int nr_histograms; - struct type_hist **histograms; + struct type_hist *histograms; }; extern struct annotated_data_type unknown_type; From 29f320d221c1c4c082c7eafb2251fedf8a4868ec Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Tue, 8 Sep 2026 11:03:44 -0300 Subject: [PATCH 076/425] perf test: Skip data_type_profiling when the PMU cannot record memory events The test's only guard matches "failed: no PMU supports the memory events", but a PMU that has memory events and refuses them per-thread, such as AMD IBS on kernels without the swfilt filter, falls through: the script runs under 'set -e' and the bare 'perf mem record' aborts it through the EXIT trap, reporting a signal that never happened and turning "this PMU cannot record these events" into a test failure. Skip when a trivial per-thread 'perf mem record' fails, and put the record and annotate calls in the condition of an 'if', where 'set -e' leaves them alone, so the remaining cases report their own failures instead of the first one aborting the run. Fixes: f60a5c22967b ("perf tests: Test annotate with data type profiling and rust") Acked-by: Namhyung Kim Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/data_type_profiling.sh | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/tools/perf/tests/shell/data_type_profiling.sh b/tools/perf/tests/shell/data_type_profiling.sh index eca694600a04..a916c410274a 100755 --- a/tools/perf/tests/shell/data_type_profiling.sh +++ b/tools/perf/tests/shell/data_type_profiling.sh @@ -19,6 +19,15 @@ perfout=$(mktemp /tmp/__perf_test.perf.out.XXXXX) perf mem record -o /dev/null -- true 2>&1 | \ grep -q "failed: no PMU supports the memory events" && exit 2 +# Skip if per-thread mem record is not supported on this PMU (e.g. AMD IBS +# needs system-wide '-a'): it is what the test records with below, and a +# failing record must not be reported as a test failure. +if ! perf mem record -o /dev/null -- true 2>/dev/null +then + echo "Skip: cannot record memory events on this PMU" + exit 2 +fi + cleanup() { rm -rf "${perfdata}" "${perfout}" rm -rf "${perfdata}".old @@ -52,25 +61,42 @@ test_basic_annotate() { index=1 ;; esac + # Under 'set -e' a bare failing command aborts the script through the EXIT + # trap, so the commands that report a failure have to be the condition of + # an 'if' for that reporting to ever happen. if [ "x${mode}" == "xBasic" ] then - perf mem record -o "${perfdata}" ${testprogs[$index]} 2> /dev/null + if ! perf mem record -o "${perfdata}" ${testprogs[$index]} 2> /dev/null + then + echo "${mode} annotate [Failed: perf record]" + err=1 + return + fi else - perf mem record -o - ${testprogs[$index]} 2> /dev/null > "${perfdata}" - fi - if [ "x$?" != "x0" ] - then - echo "${mode} annotate [Failed: perf record]" - err=1 - return + if ! perf mem record -o - ${testprogs[$index]} 2> /dev/null > "${perfdata}" + then + echo "${mode} annotate [Failed: perf record]" + err=1 + return + fi fi # Generate the annotated output file if [ "x${mode}" == "xBasic" ] then - perf annotate --code-with-type -i "${perfdata}" --stdio --percent-limit 1 2> /dev/null > "${perfout}" + if ! perf annotate --code-with-type -i "${perfdata}" --stdio --percent-limit 1 2> /dev/null > "${perfout}" + then + echo "${mode} annotate [Failed: perf annotate]" + err=1 + return + fi else - perf annotate --code-with-type -i - --stdio 2> /dev/null --percent-limit 1 < "${perfdata}" > "${perfout}" + if ! perf annotate --code-with-type -i - --stdio 2> /dev/null --percent-limit 1 < "${perfdata}" > "${perfout}" + then + echo "${mode} annotate [Failed: perf annotate]" + err=1 + return + fi fi # check if it has the target data type From 3cbb16ccd7d8c7377058195b48c721e99ee158aa Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 20 Aug 2026 14:37:57 -0700 Subject: [PATCH 077/425] perf record: Fix unhandled POLLHUP on non_perf_event descriptors When processing POLLHUP on a non_perf_event control descriptor, evlist__finalize_ctlfd() is invoked to finalize the setup, correctly setting the core evlist's poll array file descriptor to -1. However, this finalized teardown state is never propagated back to the individual thread's local replica of the pollfd array. Consequently, on the next iteration of the main recording loop, record__update_evlist_pollfd_from_thread() performs a strict equivalence check between the core evlist's array and the thread's localized poll array, detecting that the fd values no longer match. This causes an immediate -EINVAL abort and a premature teardown. Fix the underlying logic within record__update_evlist_pollfd_from_thread() to sustainably propagate the finalized teardown statuses (-1) originating from the core evlist back to the thread's localized poll structure. This correctly maintains synchronization and entirely prevents the unhandled index mismatch crashes. Additionally, add a unit test that explicitly validates that fdarray__filter() preserves its invariants regarding fdarray_flag__nonfilterable items to guard against regressions. Fixes: fb4751e79c45cb48 ("perf record: Fix teardown hang on system-wide multi-threaded sessions") Signed-off-by: Ian Rogers Assisted-by: Gemini:gemini-3.1-pro Cc: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/lib/api/fd/array.c | 4 ++++ tools/perf/builtin-record.c | 15 +++++++++++++++ tools/perf/tests/fdarray.c | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/tools/lib/api/fd/array.c b/tools/lib/api/fd/array.c index 67b73481df27..3200746d1657 100644 --- a/tools/lib/api/fd/array.c +++ b/tools/lib/api/fd/array.c @@ -116,6 +116,10 @@ int fdarray__filter(struct fdarray *fda, short revents, return 0; for (fd = 0; fd < fda->nr; ++fd) { + /* + * Explicitly bypass nonfilterable items (e.g. control descriptors). + * This ensures their fd, events, and revents remain completely untouched. + */ if (fda->priv[fd].flags & fdarray_flag__nonfilterable) continue; diff --git a/tools/perf/builtin-record.c b/tools/perf/builtin-record.c index a57987851cf0..ad81458989b5 100644 --- a/tools/perf/builtin-record.c +++ b/tools/perf/builtin-record.c @@ -1169,6 +1169,21 @@ static int record__update_evlist_pollfd_from_thread(struct record *rec, int e_pos = rec->index_map[i].evlist_pollfd_index; int t_pos = rec->index_map[i].thread_pollfd_index; + if (e_entries[e_pos].fd == -1 || e_entries[e_pos].events == 0) { + /* + * If the control file descriptor was closed, then evlist__ctlfd_process() + * will have called evlist__finalize_ctlfd() on the PREVIOUS loop iteration + * to cleanly set the core evlist's e_entries[e_pos].fd to -1. + * + * Since nonfilterable items are skipped by fdarray__filter(), the + * thread's local t_entries[t_pos] retains its original state. + * We must explicitly propagate the finalized -1 state to t_entries + * BEFORE evaluating the strict equivalence check below. + */ + t_entries[t_pos].fd = -1; + t_entries[t_pos].events = 0; + } + if (e_entries[e_pos].fd != t_entries[t_pos].fd || e_entries[e_pos].events != t_entries[t_pos].events) { pr_err("Thread and evlist pollfd index mismatch\n"); diff --git a/tools/perf/tests/fdarray.c b/tools/perf/tests/fdarray.c index 40983c3574b1..23860edb8ef0 100644 --- a/tools/perf/tests/fdarray.c +++ b/tools/perf/tests/fdarray.c @@ -80,6 +80,38 @@ static int test__fdarray__filter(struct test_suite *test __maybe_unused, int sub goto out_delete; } + fdarray__init_revents(fda, POLLHUP); + fda->priv[2].flags = fdarray_flag__nonfilterable; + + pr_debug("\nfiltering all but fda->entries[2] (nonfilterable):"); + fdarray__fprintf_prefix(fda, "before", stderr); + + /* + * Note: fdarray__filter() in tools/lib/api/fd/array.c evaluates the + * fdarray_flag__nonfilterable flag at the very top of its loop via an + * early continue. Therefore, it completely skips all processing for this + * descriptor, guaranteeing its fd, events, and revents fields remain + * entirely untouched by the filter mask evaluation below it. + */ + nr_fds = fdarray__filter(fda, POLLHUP, NULL, NULL); + fdarray__fprintf_prefix(fda, " after", stderr); + + if (nr_fds != 0) { + pr_debug("\nfdarray__filter()=%d != 0, should be 0\n", + nr_fds); + goto out_delete; + } + if (fda->entries[2].fd == -1) { + pr_debug("\nfdarray__filter() illegally modified nonfilterable fd!"); + goto out_delete; + } + if (fda->entries[2].revents != POLLHUP) { + pr_debug("\nfdarray__filter() illegally modified nonfilterable revents!"); + goto out_delete; + } + + fda->priv[2].flags = 0; /* reset flags */ + pr_debug("\n"); err = 0; From 63c2df4cfb1a8d1eb34f19db78f51cc87c10f369 Mon Sep 17 00:00:00 2001 From: liujing Date: Tue, 15 Sep 2026 15:11:22 -0700 Subject: [PATCH 078/425] perf bench messaging: Fix the formatting issue when printing. The unsigned int type should use the %u format specifier when printing with printf or related functions. Signed-off-by: liujing Reviewed-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/bench/sched-messaging.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/perf/bench/sched-messaging.c b/tools/perf/bench/sched-messaging.c index 4fb6657fc826..c5c9a7a1d7db 100644 --- a/tools/perf/bench/sched-messaging.c +++ b/tools/perf/bench/sched-messaging.c @@ -338,9 +338,9 @@ int bench_sched_messaging(int argc, const char **argv) switch (bench_format) { case BENCH_FORMAT_DEFAULT: - printf("# %d sender and receiver %s per group\n", + printf("# %u sender and receiver %s per group\n", num_fds, thread_mode ? "threads" : "processes"); - printf("# %d groups == %d %s run\n\n", + printf("# %u groups == %u %s run\n\n", num_groups, num_groups * 2 * num_fds, thread_mode ? "threads" : "processes"); printf(" %14s: %lu.%03lu [sec]\n", "Total time", From 86a27811675a415bd351efca1a194a1a94c082dd Mon Sep 17 00:00:00 2001 From: Chun-Tse Shao Date: Tue, 15 Sep 2026 13:49:51 -0700 Subject: [PATCH 079/425] perf evsel: Find process with busy PMUs for EBUSY It parses fdinfo with PMU type, comparing with the event which failed to open, and report the processes causing EBUSY error. Testing cycles and intel_pt// $ ./perf stat -e cycles & [1] 55569 $ ./perf stat -e intel_pt// & [2] 55683 $ ./perf stat -e intel_pt// Error: The PMU intel_pt counters are busy and in use by another process. Possible processes: 55683 ./perf stat -e intel_pt// Only perf with intel_pt was reported. Reviewed-by: Ian Rogers Signed-off-by: Chun-Tse Shao Assisted-by: Gemini:gemini-3.1-pro-preview [ Added a few missing includes as per Sashiko's review ] Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/evsel.c | 72 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index b44fd880d2d5..c663aafa88b2 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -14,9 +14,12 @@ #include "evsel.h" #include +#include #include +#include #include #include +#include #include #include @@ -4354,8 +4357,11 @@ static bool find_process(const char *name) return ret ? false : true; } -static int dump_perf_event_processes(char *msg, size_t size) +static int dump_perf_event_processes(const struct evsel *evsel, + char *msg, size_t size) { + const struct perf_event_attr *failed_attr = &evsel->core.attr; + u32 target_pmu_type = evsel->pmu ? evsel->pmu->type : UINT32_MAX; DIR *proc_dir; struct dirent *proc_entry; int printed = 0; @@ -4386,6 +4392,15 @@ static int dump_perf_event_processes(char *msg, size_t size) continue; } while ((fd_entry = readdir(fd_dir)) != NULL) { + const char *target_lnk = "anon_inode:[perf_event]"; + size_t target_lnk_len = sizeof("anon_inode:[perf_event]") - 1; + char fdinfo_buf[1024]; + int fdinfo_fd; + ssize_t fdinfo_size; + char *line; + char *saveptr; + u32 perf_event_type = UINT32_MAX; + u32 pmu_type = UINT32_MAX; ssize_t link_size; if (fd_entry->d_type != DT_LNK) @@ -4394,12 +4409,51 @@ static int dump_perf_event_processes(char *msg, size_t size) if (link_size < 0) continue; /* Take care as readlink doesn't null terminate the string. */ - if (!strncmp(buf, "anon_inode:[perf_event]", link_size)) { + if (link_size != (ssize_t)target_lnk_len || + strncmp(buf, target_lnk, target_lnk_len)) + continue; + + /* Let's check the PMU type reserved by this process */ + scnprintf(buf, sizeof(buf), "%s/fdinfo/%s", + proc_entry->d_name, fd_entry->d_name); + fdinfo_fd = openat(dirfd(proc_dir), buf, O_RDONLY | O_NONBLOCK); + if (fdinfo_fd == -1) + continue; + fdinfo_size = read(fdinfo_fd, fdinfo_buf, sizeof(fdinfo_buf) - 1); + close(fdinfo_fd); + if (fdinfo_size < 0) + continue; + fdinfo_buf[fdinfo_size] = '\0'; + + line = strtok_r(fdinfo_buf, "\n", &saveptr); + while (line) { + if (sscanf(line, + "perf_event_attr.type:\t%u", + &perf_event_type) == 1) { + /* continue parsing */ + } else if (sscanf(line, + "pmu_type:\t%u", + &pmu_type) == 1) { + /* continue parsing */ + } + line = strtok_r(NULL, "\n", &saveptr); + } + + /* Report the process which reserves the conflicted PMU. */ + /* If fdinfo does not contain PMU type, report it too. */ + if (perf_event_type == failed_attr->type || + pmu_type == failed_attr->type || + (target_pmu_type != UINT32_MAX && + pmu_type == target_pmu_type) || + (perf_event_type == UINT32_MAX && + pmu_type == UINT32_MAX)) { int cmdline_fd; ssize_t cmdline_size; - scnprintf(buf, sizeof(buf), "%s/cmdline", proc_entry->d_name); - cmdline_fd = openat(dirfd(proc_dir), buf, O_RDONLY); + scnprintf(buf, sizeof(buf), + "%s/cmdline", + proc_entry->d_name); + cmdline_fd = openat(dirfd(proc_dir), buf, O_RDONLY | O_NONBLOCK); if (cmdline_fd == -1) continue; cmdline_size = read(cmdline_fd, buf, sizeof(buf) - 1); @@ -4410,10 +4464,12 @@ static int dump_perf_event_processes(char *msg, size_t size) for (ssize_t i = 0; i < cmdline_size; i++) { if (buf[i] == '\0') buf[i] = ' '; + else if (!isprint((unsigned char)buf[i])) + buf[i] = '.'; } - if (printed == 0) - printed += scnprintf(msg, size, "Possible processes:\n"); + printed += scnprintf(msg, size, + "Possible processes:\n"); printed += scnprintf(msg + printed, size - printed, "%s %s\n", proc_entry->d_name, buf); @@ -4535,7 +4591,9 @@ int evsel__open_strerror(struct evsel *evsel, struct target *target, msg, size, "The PMU %s counters are busy and in use by another process.\n", evsel->pmu ? evsel->pmu->name : ""); - return printed + dump_perf_event_processes(msg + printed, size - printed); + return printed + dump_perf_event_processes(evsel, + msg + printed, + size - printed); break; case EINVAL: if (evsel->core.attr.sample_type & PERF_SAMPLE_CODE_PAGE_SIZE && perf_missing_features.code_page_size) From 0824af00a8d2ede0d8bc70bdc0b493ecfd167900 Mon Sep 17 00:00:00 2001 From: Zide Chen Date: Wed, 16 Sep 2026 13:27:40 -0700 Subject: [PATCH 080/425] perf topdown: Don't require config1==0 for a slots event arch_is_topdown_slots() requires config1 == 0, so it misidentified "cpu/slots,metrics_clear=1/" as not being the slots event, and an additional slots event gets inserted, which fails event scheduling since the group now needs two fixed counter 3. $ perf stat -e "{cpu/slots,metrics_clear=1/,cpu/topdown-retiring/}" -- sleep 1 WARNING: events were regrouped to match PMUs slots cpu/slots,metrics_clear=1/ cpu/topdown-retiring/ Drop the config1 == 0 check from arch_is_topdown_slots(). This is safe: config1 has no bearing on the event's identifying config value (TOPDOWN_SLOTS, 0x0400). The attr.type == PERF_TYPE_RAW check is what actually matters: only the core PMU is registered with PERF_TYPE_RAW, and on x86, 0x400 is guaranteed to be slots event on core CPU. arch_is_topdown_metrics() keeps its own config1 == 0 check, since genuine metrics events have no "metrics_clear" and config1 remains 0. Add a test verifying that an explicit "slots,metrics_clear=1" event in a group does not trigger an extra slots event injection. Fixes: 5b546de9cc177936 ("perf topdown: Use attribute to see an event is a topdown metic or slots") Reviewed-by: Dapeng Mi Reviewed-by: Ian Rogers Signed-off-by: Zide Chen Cc: Namhyung Kim [ Add missing stdio.h and string.h as noticed by Sashiko ] Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/arch/x86/tests/topdown.c | 30 +++++++++++++++++++++++++++++ tools/perf/arch/x86/util/topdown.c | 3 +-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/tools/perf/arch/x86/tests/topdown.c b/tools/perf/arch/x86/tests/topdown.c index 2b6f47ce4932..520075329c37 100644 --- a/tools/perf/arch/x86/tests/topdown.c +++ b/tools/perf/arch/x86/tests/topdown.c @@ -1,5 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 #include +#include +#include #include "arch-tests.h" #include "../util/topdown.h" #include "debug.h" @@ -229,10 +231,38 @@ static int test__x86_topdown_slots_injection(struct test_suite *test __maybe_unu return TEST_OK; } +/* + * An explicit "slots,metrics_clear=1" event is still the slots event and + * must not cause an extra slots event to be injected into the group. + */ +static int test__x86_topdown_metrics_clear(struct test_suite *test __maybe_unused, + int subtest __maybe_unused) +{ + struct perf_pmu *pmu; + char event_str[128]; + int ret; + + if (!topdown_sys_has_perf_metrics()) + return TEST_OK; + + pmu = perf_pmus__find_by_type(PERF_TYPE_RAW); + if (!pmu || !perf_pmu__has_format(pmu, "metrics_clear")) + return TEST_OK; + + snprintf(event_str, sizeof(event_str), + "{%s/slots,metrics_clear=1/,%s/topdown-retiring/}", + pmu->name, pmu->name); + ret = test_sort(event_str, 2, 1); + TEST_ASSERT_EQUAL("explicit metrics_clear slots event isn't duplicated", ret, TEST_OK); + + return TEST_OK; +} + static struct test_case x86_topdown_tests[] = { TEST_CASE("topdown events", x86_topdown), TEST_CASE("topdown sorting", x86_topdown_sorting), TEST_CASE("topdown slots injection", x86_topdown_slots_injection), + TEST_CASE("topdown metrics_clear no extra slots", x86_topdown_metrics_clear), { .name = NULL, } }; diff --git a/tools/perf/arch/x86/util/topdown.c b/tools/perf/arch/x86/util/topdown.c index bafd285119d7..64bc64e335a2 100644 --- a/tools/perf/arch/x86/util/topdown.c +++ b/tools/perf/arch/x86/util/topdown.c @@ -37,8 +37,7 @@ bool topdown_sys_has_perf_metrics(void) bool arch_is_topdown_slots(const struct evsel *evsel) { return evsel->core.attr.type == PERF_TYPE_RAW && - evsel->core.attr.config == TOPDOWN_SLOTS && - evsel->core.attr.config1 == 0; + evsel->core.attr.config == TOPDOWN_SLOTS; } bool arch_is_topdown_metrics(const struct evsel *evsel) From bf96e296d52d74a1ae5254a341175afede8a1963 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:04:48 -0700 Subject: [PATCH 081/425] perf libdw: Fix Dwfl discovery with split files dso__libdw_dwfl() opens the Dwfl with dso__long_name(), the file the samples came from. When the debug information lives in a separate file, symbol loading finds it and records it as the dso's symsrc filename, but the Dwfl still refers to the original file, so libdw has no DWARF to resolve addresses against. Open the Dwfl with the symsrc filename when one is known and fall back to the long name when it isn't. The symsrc file is found while symbols are loaded, which can happen after the Dwfl has been created and cached, so drop the cached Dwfl and the addr2line cache built from it when the symsrc filename is set. That teardown can run while another thread is using the cache, so take the dso lock across both the teardown and the Dwfl accessors, and re-read dso__symsrc_filename() under the dso lock in libbfd__addr2line() and cmd__addr2line() to prevent a TOCTOU race with cache invalidation. Fixes: b7a2b011e9627ff3 ("perf powerpc: Unify the skip-callchain-idx libdw with that for addr2line") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- .../arch/powerpc/util/skip-callchain-idx.c | 4 +- tools/perf/util/addr2line.c | 42 +++++++++--- tools/perf/util/dso.c | 9 +++ tools/perf/util/dso.h | 5 +- tools/perf/util/libbfd.c | 29 ++++++--- tools/perf/util/libdw.c | 39 +++++++---- tools/perf/util/srcline.c | 64 +++++++++++++------ tools/perf/util/unwind-libdw.c | 2 + tools/perf/util/unwind-libunwind.c | 62 +++++++++++++----- 9 files changed, 187 insertions(+), 69 deletions(-) diff --git a/tools/perf/arch/powerpc/util/skip-callchain-idx.c b/tools/perf/arch/powerpc/util/skip-callchain-idx.c index e57f10798fa6..472714cfad38 100644 --- a/tools/perf/arch/powerpc/util/skip-callchain-idx.c +++ b/tools/perf/arch/powerpc/util/skip-callchain-idx.c @@ -152,9 +152,10 @@ static int check_return_addr(struct dso *dso, Dwarf_Addr mapped_pc) Dwarf_Addr end = mapped_pc; bool signalp; + mutex_lock(dso__lock(dso)); dwfl = dso__libdw_dwfl(dso); if (!dwfl) - return -1; + goto out; mod = dwfl_addrmodule(dwfl, mapped_pc); if (!mod) { @@ -183,6 +184,7 @@ static int check_return_addr(struct dso *dso, Dwarf_Addr mapped_pc) rc = check_return_reg(ra_regno, frame); out: + mutex_unlock(dso__lock(dso)); return rc; } diff --git a/tools/perf/util/addr2line.c b/tools/perf/util/addr2line.c index 4b0d349ed334..6f64f6b42442 100644 --- a/tools/perf/util/addr2line.c +++ b/tools/perf/util/addr2line.c @@ -15,6 +15,7 @@ #include #include #include +#include #define MAX_INLINE_NEST 1024 @@ -284,7 +285,7 @@ int cmd__addr2line(const char *dso_name, u64 addr, struct inline_node *node, struct symbol *sym __maybe_unused) { - struct child_process *a2l = dso__a2l(dso); + struct child_process *a2l; char *record_function = NULL; char *record_filename = NULL; unsigned int record_line_nr = 0; @@ -296,24 +297,38 @@ int cmd__addr2line(const char *dso_name, u64 addr, ssize_t written; struct io io = { .eof = false }; enum cmd_a2l_style cmd_a2l_style; + const char *current_dso_name; + + mutex_lock(dso__lock(dso)); + current_dso_name = dso__symsrc_filename(dso) ?: dso_name; + a2l = dso__a2l(dso); if (!a2l) { - if (!filename__has_section(dso_name, ".debug_line")) - goto out; + if (!filename__has_section(current_dso_name, ".debug_line")) + goto out_unlock; dso__set_a2l(dso, - addr2line_subprocess_init(symbol_conf.addr2line_path, dso_name)); + addr2line_subprocess_init(symbol_conf.addr2line_path, + current_dso_name)); a2l = dso__a2l(dso); } if (a2l == NULL) { if (!symbol_conf.addr2line_disable_warn) pr_warning("%s %s: addr2line_subprocess_init failed\n", __func__, dso_name); - goto out; + goto out_unlock; } cmd_a2l_style = cmd_addr2line_configure(a2l, dso_name); if (cmd_a2l_style == BROKEN) - goto out; + goto out_unlock; + + /* + * Take ownership of the a2l subprocess so we can safely perform + * blocking IPC without holding the dso lock. If another thread + * resolves a symbol concurrently, it will spawn a new a2l process. + */ + dso__set_a2l(dso, NULL); + mutex_unlock(dso__lock(dso)); /* * Send our request and then *deliberately* send something that can't be @@ -414,12 +429,21 @@ int cmd__addr2line(const char *dso_name, u64 addr, } out: + mutex_lock(dso__lock(dso)); free(record_function); free(record_filename); - if (io.eof) { - dso__set_a2l(dso, NULL); + + current_dso_name = dso__symsrc_filename(dso) ?: dso__long_name(dso); + if (!io.eof && dso__a2l(dso) == NULL && current_dso_name && + !strcmp(current_dso_name, dso_name)) + dso__set_a2l(dso, a2l); + else addr2line_subprocess_cleanup(a2l); - } + mutex_unlock(dso__lock(dso)); + return ret; + +out_unlock: + mutex_unlock(dso__lock(dso)); return ret; } diff --git a/tools/perf/util/dso.c b/tools/perf/util/dso.c index 42bfe30a3b51..df39e6ca88e6 100644 --- a/tools/perf/util/dso.c +++ b/tools/perf/util/dso.c @@ -2075,3 +2075,12 @@ struct debuginfo *dso__debuginfo(struct dso *dso) free(name); return dinfo; } + +void dso__set_symsrc_filename(struct dso *dso, char *val) +{ + RC_CHK_ACCESS(dso)->symsrc_filename = val; + dso__free_libdw(dso); + dso__free_a2l(dso); + dso__set_has_srcline(dso, true); + dso__set_a2l_fails(dso, 0); +} diff --git a/tools/perf/util/dso.h b/tools/perf/util/dso.h index 55c4aaa53c38..7966c7048c85 100644 --- a/tools/perf/util/dso.h +++ b/tools/perf/util/dso.h @@ -735,10 +735,7 @@ static inline const char *dso__symsrc_filename(const struct dso *dso) return RC_CHK_ACCESS(dso)->symsrc_filename; } -static inline void dso__set_symsrc_filename(struct dso *dso, char *val) -{ - RC_CHK_ACCESS(dso)->symsrc_filename = val; -} +void dso__set_symsrc_filename(struct dso *dso, char *val); static inline void dso__free_symsrc_filename(struct dso *dso) { diff --git a/tools/perf/util/libbfd.c b/tools/perf/util/libbfd.c index 33dc6158b2b1..35aa9d98f2f8 100644 --- a/tools/perf/util/libbfd.c +++ b/tools/perf/util/libbfd.c @@ -226,7 +226,11 @@ int libbfd__addr2line(const char *dso_name, u64 addr, struct symbol *sym) { int ret = 0; - struct a2l_data *a2l = dso__a2l(dso); + struct a2l_data *a2l; + + mutex_lock(dso__lock(dso)); + dso_name = dso__symsrc_filename(dso) ?: dso_name; + a2l = dso__a2l(dso); if (!a2l) { a2l = addr2line_init(dso_name); @@ -236,7 +240,8 @@ int libbfd__addr2line(const char *dso_name, u64 addr, if (a2l == NULL) { if (!symbol_conf.addr2line_disable_warn) pr_warning("addr2line_init failed for %s\n", dso_name); - return 0; + ret = -1; + goto out; } a2l->addr = addr; @@ -244,14 +249,18 @@ int libbfd__addr2line(const char *dso_name, u64 addr, bfd_map_over_sections(a2l->abfd, find_address_in_section, a2l); - if (!a2l->found) - return 0; + if (!a2l->found) { + ret = 0; + goto out; + } if (unwind_inlines) { int cnt = 0; - if (node && inline_list__append_dso_a2l(dso, node, sym)) - return 0; + if (node && inline_list__append_dso_a2l(dso, node, sym)) { + ret = 0; + goto out; + } while (bfd_find_inliner_info(a2l->abfd, &a2l->filename, &a2l->funcname, &a2l->line) && @@ -261,8 +270,10 @@ int libbfd__addr2line(const char *dso_name, u64 addr, a2l->filename = NULL; if (node != NULL) { - if (inline_list__append_dso_a2l(dso, node, sym)) - return 0; + if (inline_list__append_dso_a2l(dso, node, sym)) { + ret = 0; + goto out; + } // found at least one inline frame ret = 1; } @@ -277,6 +288,8 @@ int libbfd__addr2line(const char *dso_name, u64 addr, if (line) *line = a2l->line; +out: + mutex_unlock(dso__lock(dso)); return ret; } diff --git a/tools/perf/util/libdw.c b/tools/perf/util/libdw.c index 4ca7e7e4fbe9..c4504cceb313 100644 --- a/tools/perf/util/libdw.c +++ b/tools/perf/util/libdw.c @@ -35,7 +35,7 @@ struct Dwfl *dso__libdw_dwfl(struct dso *dso) if (dwfl) return dwfl; - dso_name = dso__long_name(dso); + dso_name = dso__symsrc_filename(dso) ?: dso__long_name(dso); /* * Initialize Dwfl session. * We need to open the DSO file to report it to libdw. @@ -167,32 +167,41 @@ int libdw__addr2line(u64 addr, char **file, unsigned int *line_nr, struct dso *dso, bool unwind_inlines, struct inline_node *node, struct symbol *sym) { - Dwfl *dwfl = dso__libdw_dwfl(dso); + Dwfl *dwfl; Dwfl_Module *mod; Dwfl_Line *dwline; Dwarf_Addr bias; const char *src; int lineno = 0; + int ret = 0; + mutex_lock(dso__lock(dso)); + dwfl = dso__libdw_dwfl(dso); if (!dwfl) - return 0; + goto out; mod = dwfl_addrmodule(dwfl, addr); - if (!mod) - return 0; + if (!mod) { + ret = 0; + goto out; + } /* * Get/ignore the dwarf information. Determine the bias, difference * between the regular ELF addr2line addresses and those to use with * libdw. */ - if (!dwfl_module_getdwarf(mod, &bias)) - return 0; + if (!dwfl_module_getdwarf(mod, &bias)) { + ret = -1; + goto out; + } /* Find source line information for the address. */ dwline = dwfl_module_getsrc(mod, addr + bias); - if (!dwline) - return 0; + if (!dwline) { + ret = -1; + goto out; + } /* Get line information. */ src = dwfl_lineinfo(dwline, /*addr=*/NULL, &lineno, /*col=*/NULL, /*mtime=*/NULL, @@ -219,7 +228,8 @@ int libdw__addr2line(u64 addr, char **file, unsigned int *line_nr, free(*file); *file = NULL; } - return 0; + ret = 0; + goto out; } /* Walk from the parent down to the leaf. */ @@ -235,8 +245,13 @@ int libdw__addr2line(u64 addr, char **file, unsigned int *line_nr, *file = NULL; } inline_node__clear_frames(node); - return 0; + ret = 0; + goto out; } } - return 1; + ret = 1; + +out: + mutex_unlock(dso__lock(dso)); + return ret; } diff --git a/tools/perf/util/srcline.c b/tools/perf/util/srcline.c index b082178c279b..5ed89f97ff11 100644 --- a/tools/perf/util/srcline.c +++ b/tools/perf/util/srcline.c @@ -1,40 +1,49 @@ // SPDX-License-Identifier: GPL-2.0 #include "srcline.h" -#include "addr2line.h" -#include "dso.h" -#include "callchain.h" -#include "libbfd.h" -#include "llvm.h" -#include "symbol.h" -#include "libdw.h" -#include "debug.h" -#include "util.h" #include +#include +#include #include + #include #include +#include "addr2line.h" +#include "callchain.h" +#include "debug.h" +#include "dso.h" +#include "libbfd.h" +#include "libdw.h" +#include "llvm.h" +#include "symbol.h" +#include "util.h" + bool srcline_full_filename; char *srcline__unknown = (char *)"??:0"; -static const char *srcline_dso_name(struct dso *dso) +static char *srcline_dso_name(struct dso *dso) { const char *dso_name; + char *ret = NULL; + mutex_lock(dso__lock(dso)); if (dso__symsrc_filename(dso)) dso_name = dso__symsrc_filename(dso); else dso_name = dso__long_name(dso); if (dso_name[0] == '[') - return NULL; + goto out; if (is_perf_pid_map_name(dso_name)) - return NULL; + goto out; - return dso_name; + ret = strdup(dso_name); +out: + mutex_unlock(dso__lock(dso)); + return ret; } int inline_list__append(struct symbol *symbol, char *srcline, struct inline_node *node) @@ -258,7 +267,7 @@ char *__get_srcline(struct dso *dso, u64 addr, struct symbol *sym, char *file = NULL; unsigned line = 0; char *srcline; - const char *dso_name; + char *dso_name; if (!dso__has_srcline(dso)) goto out; @@ -268,8 +277,11 @@ char *__get_srcline(struct dso *dso, u64 addr, struct symbol *sym, goto out_err; if (!addr2line(dso_name, addr, &file, &line, dso, - unwind_inlines, /*node=*/NULL, sym)) + unwind_inlines, /*node=*/NULL, sym)) { + free(dso_name); goto out_err; + } + free(dso_name); srcline = srcline_from_fileline(file, line); free(file); @@ -277,16 +289,20 @@ char *__get_srcline(struct dso *dso, u64 addr, struct symbol *sym, if (!srcline) goto out_err; + mutex_lock(dso__lock(dso)); dso__set_a2l_fails(dso, 0); + mutex_unlock(dso__lock(dso)); return srcline; out_err: + mutex_lock(dso__lock(dso)); dso__set_a2l_fails(dso, dso__a2l_fails(dso) + 1); if (dso__a2l_fails(dso) > A2L_FAIL_LIMIT) { dso__set_has_srcline(dso, false); dso__free_a2l(dso); } + mutex_unlock(dso__lock(dso)); out: if (!show_addr) return (show_sym && sym) ? @@ -305,7 +321,7 @@ char *__get_srcline(struct dso *dso, u64 addr, struct symbol *sym, char *get_srcline_split(struct dso *dso, u64 addr, unsigned *line) { char *file = NULL; - const char *dso_name; + char *dso_name; if (!dso__has_srcline(dso)) return NULL; @@ -315,18 +331,25 @@ char *get_srcline_split(struct dso *dso, u64 addr, unsigned *line) goto out_err; if (!addr2line(dso_name, addr, &file, line, dso, /*unwind_inlines=*/true, - /*node=*/NULL, /*sym=*/NULL)) + /*node=*/NULL, /*sym=*/NULL)) { + free(dso_name); goto out_err; + } + free(dso_name); + mutex_lock(dso__lock(dso)); dso__set_a2l_fails(dso, 0); + mutex_unlock(dso__lock(dso)); return file; out_err: + mutex_lock(dso__lock(dso)); dso__set_a2l_fails(dso, dso__a2l_fails(dso) + 1); if (dso__a2l_fails(dso) > A2L_FAIL_LIMIT) { dso__set_has_srcline(dso, false); dso__free_a2l(dso); } + mutex_unlock(dso__lock(dso)); return NULL; } @@ -420,13 +443,16 @@ void srcline__tree_delete(struct rb_root_cached *tree) struct inline_node *dso__parse_addr_inlines(struct dso *dso, u64 addr, struct symbol *sym) { - const char *dso_name; + char *dso_name; + struct inline_node *node; dso_name = srcline_dso_name(dso); if (dso_name == NULL) return NULL; - return addr2inlines(dso_name, addr, dso, sym); + node = addr2inlines(dso_name, addr, dso, sym); + free(dso_name); + return node; } void inline_node__clear_frames(struct inline_node *node) diff --git a/tools/perf/util/unwind-libdw.c b/tools/perf/util/unwind-libdw.c index ebea101e1001..4ccfcc7c2dfc 100644 --- a/tools/perf/util/unwind-libdw.c +++ b/tools/perf/util/unwind-libdw.c @@ -42,8 +42,10 @@ static int __find_debuginfo(Dwfl_Module *mod __maybe_unused, void **userdata, const struct dso *dso = *userdata; assert(dso); + mutex_lock(dso__lock((struct dso *)dso)); if (dso__symsrc_filename(dso) && strcmp(file_name, dso__symsrc_filename(dso))) *debuginfo_file_name = strdup(dso__symsrc_filename(dso)); + mutex_unlock(dso__lock((struct dso *)dso)); return -1; } diff --git a/tools/perf/util/unwind-libunwind.c b/tools/perf/util/unwind-libunwind.c index 73d191ce51a5..6052383e862b 100644 --- a/tools/perf/util/unwind-libunwind.c +++ b/tools/perf/util/unwind-libunwind.c @@ -1,20 +1,28 @@ // SPDX-License-Identifier: GPL-2.0 +#include "unwind.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + #include "callchain.h" #include "debug.h" #include "dso.h" #include "env.h" +#include "libunwind-arch/libunwind-arch.h" #include "map.h" #include "perf_regs.h" #include "session.h" #include "symbol.h" #include "thread.h" -#include "unwind.h" -#include "libunwind-arch/libunwind-arch.h" -#include -#include -#include -#include -#include #define DW_EH_PE_FORMAT_MASK 0x0f /* format of the encoded value */ #define DW_EH_PE_APPL_MASK 0x70 /* how the value is to be applied */ @@ -293,10 +301,19 @@ static int read_unwind_spec_debug_frame(struct dso *dso, } if (ofs <= 0) { - fd = open(dso__symsrc_filename(dso), O_RDONLY); - if (fd >= 0) { - ofs = elf_section_offset(fd, ".debug_frame"); - close(fd); + char *alloc_name; + + mutex_lock(dso__lock(dso)); + alloc_name = dso__symsrc_filename(dso) ? + strdup(dso__symsrc_filename(dso)) : NULL; + mutex_unlock(dso__lock(dso)); + if (alloc_name) { + fd = open(alloc_name, O_RDONLY); + if (fd >= 0) { + ofs = elf_section_offset(fd, ".debug_frame"); + close(fd); + } + free(alloc_name); } } @@ -321,6 +338,7 @@ static int read_unwind_spec_debug_frame(struct dso *dso, } } if (ofs > 0) { + mutex_lock(dso__lock(dso)); if (dso__symsrc_filename(dso) != NULL) { pr_warning( "%s: overwrite symsrc(%s,%s)\n", @@ -330,6 +348,7 @@ static int read_unwind_spec_debug_frame(struct dso *dso, dso__free_symsrc_filename(dso); } dso__set_symsrc_filename(dso, debuglink); + mutex_unlock(dso__lock(dso)); } else { free(debuglink); } @@ -429,12 +448,23 @@ int __libunwind__find_proc_info(void *as, uint64_t ip, void *pi, int need_unwind dso__data_put_fd(dso); } - symfile = dso__symsrc_filename(dso) ?: dso__name(dso); + mutex_lock(dso__lock(dso)); + symfile = dso__symsrc_filename(dso) ? + strdup(dso__symsrc_filename(dso)) : + strdup(dso__name(dso)); + mutex_unlock(dso__lock(dso)); - if (libunwind_arch__dwarf_find_debug_frame(ui->e_machine, /*found=*/0, &di, ip, - base, symfile, start, map__end(map))) { - ret = libunwind_arch__dwarf_search_unwind_table(ui->e_machine, as, ip, &di, pi, - need_unwind_info, arg); + if (symfile) { + if (libunwind_arch__dwarf_find_debug_frame(ui->e_machine, + /*found=*/0, &di, ip, + base, symfile, start, + map__end(map))) { + ret = libunwind_arch__dwarf_search_unwind_table(ui->e_machine, as, + ip, &di, pi, + need_unwind_info, + arg); + } + free((char *)symfile); } } map__put(map); From f651c02755af85324ad029cd948a8e570d0769fb Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:04:49 -0700 Subject: [PATCH 082/425] perf dso: Separate libbfd and cmd addr2line caches to fix confusion When a dso executes addr2line via libbfd it instantiates an a2l_data pointer. If the DSO later executes via the command-line fallback due to an inlined bug, the a2l pointer is unconditionally populated with a child_process process wrapper. If libbfd is queried again, the offline reader attempts to cast struct a2l_data into child_process, coercing random arbitrary instruction addresses and causing silent aborts. This cleanly splits the caches. Fixes: 257046a36750a6db ("perf srcline: Fallback between addr2line implementations") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/dso.c | 52 ++++++++++++++++++++++----------------- tools/perf/util/dso.h | 21 ++++++++++++++-- tools/perf/util/libbfd.c | 10 ++++---- tools/perf/util/srcline.c | 2 ++ 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/tools/perf/util/dso.c b/tools/perf/util/dso.c index df39e6ca88e6..9e90de92fcfa 100644 --- a/tools/perf/util/dso.c +++ b/tools/perf/util/dso.c @@ -1,38 +1,44 @@ // SPDX-License-Identifier: GPL-2.0 +#include "dso.h" + +#include +#include + #include +#include #include #include #include -#include #include -#include #include +#include +#include #include -#include -#include -#include + +#include "annotate-data.h" +#include "auxtrace.h" +#include "compress.h" +#include "debug.h" +#include "dsos.h" +#include "env.h" +#include "libbfd.h" +#include "libdw.h" +#include "machine.h" +#include "map.h" +#include "namespaces.h" +#include "path.h" +#include "srcline.h" +#include "string2.h" +#include "symbol.h" +#include "util.h" /* O_CLOEXEC for older systems */ +#include "vdso.h" + #ifdef HAVE_LIBBPF_SUPPORT #include + #include "bpf-event.h" #include "bpf-utils.h" #endif -#include "compress.h" -#include "env.h" -#include "namespaces.h" -#include "path.h" -#include "map.h" -#include "symbol.h" -#include "srcline.h" -#include "dso.h" -#include "dsos.h" -#include "machine.h" -#include "auxtrace.h" -#include "util.h" /* O_CLOEXEC for older systems */ -#include "debug.h" -#include "string2.h" -#include "vdso.h" -#include "annotate-data.h" -#include "libdw.h" static const char * const debuglink_paths[] = { "%.0s%s", @@ -1757,6 +1763,7 @@ void dso__delete(struct dso *dso) auxtrace_cache__free(RC_CHK_ACCESS(dso)->auxtrace_cache); dso_cache__free(dso); dso__free_a2l(dso); + dso__free_a2l_libbfd(dso); dso__free_libdw(dso); dso__free_symsrc_filename(dso); nsinfo__zput(RC_CHK_ACCESS(dso)->nsinfo); @@ -2081,6 +2088,7 @@ void dso__set_symsrc_filename(struct dso *dso, char *val) RC_CHK_ACCESS(dso)->symsrc_filename = val; dso__free_libdw(dso); dso__free_a2l(dso); + dso__free_a2l_libbfd(dso); dso__set_has_srcline(dso, true); dso__set_a2l_fails(dso, 0); } diff --git a/tools/perf/util/dso.h b/tools/perf/util/dso.h index 7966c7048c85..e7d5f4bbf894 100644 --- a/tools/perf/util/dso.h +++ b/tools/perf/util/dso.h @@ -304,7 +304,12 @@ DECLARE_RC_STRUCT(dso) { const char *short_name; const char *long_name; void *a2l; +#ifdef HAVE_LIBBFD_SUPPORT + void *a2l_libbfd; +#endif +#ifdef HAVE_LIBDW_SUPPORT void *libdw; +#endif char *symsrc_filename; struct nsinfo *nsinfo; struct auxtrace_cache *auxtrace_cache; @@ -368,6 +373,20 @@ static inline void dso__set_a2l(struct dso *dso, void *val) RC_CHK_ACCESS(dso)->a2l = val; } +#ifdef HAVE_LIBBFD_SUPPORT +static inline void *dso__a2l_libbfd(const struct dso *dso) +{ + return RC_CHK_ACCESS(dso)->a2l_libbfd; +} + +static inline void dso__set_a2l_libbfd(struct dso *dso, void *val) +{ + RC_CHK_ACCESS(dso)->a2l_libbfd = val; +} +#endif + +struct Dwfl; +#ifdef HAVE_LIBDW_SUPPORT static inline void *dso__libdw(const struct dso *dso) { return RC_CHK_ACCESS(dso)->libdw; @@ -378,8 +397,6 @@ static inline void dso__set_libdw(struct dso *dso, void *val) RC_CHK_ACCESS(dso)->libdw = val; } -struct Dwfl; -#ifdef HAVE_LIBDW_SUPPORT struct Dwfl *dso__libdw_dwfl(struct dso *dso); #else static inline struct Dwfl *dso__libdw_dwfl(struct dso *dso __maybe_unused) diff --git a/tools/perf/util/libbfd.c b/tools/perf/util/libbfd.c index 35aa9d98f2f8..2a73124b76b8 100644 --- a/tools/perf/util/libbfd.c +++ b/tools/perf/util/libbfd.c @@ -210,7 +210,7 @@ static int inline_list__append_dso_a2l(struct dso *dso, struct inline_node *node, struct symbol *sym) { - struct a2l_data *a2l = dso__a2l(dso); + struct a2l_data *a2l = dso__a2l_libbfd(dso); struct symbol *inline_sym = new_inline_sym(dso, sym, a2l->funcname); char *srcline = NULL; @@ -230,11 +230,11 @@ int libbfd__addr2line(const char *dso_name, u64 addr, mutex_lock(dso__lock(dso)); dso_name = dso__symsrc_filename(dso) ?: dso_name; - a2l = dso__a2l(dso); + a2l = dso__a2l_libbfd(dso); if (!a2l) { a2l = addr2line_init(dso_name); - dso__set_a2l(dso, a2l); + dso__set_a2l_libbfd(dso, a2l); } if (a2l == NULL) { @@ -295,14 +295,14 @@ int libbfd__addr2line(const char *dso_name, u64 addr, void dso__free_a2l_libbfd(struct dso *dso) { - struct a2l_data *a2l = dso__a2l(dso); + struct a2l_data *a2l = dso__a2l_libbfd(dso); if (!a2l) return; addr2line_cleanup(a2l); - dso__set_a2l(dso, NULL); + dso__set_a2l_libbfd(dso, NULL); } static int bfd_symbols__cmpvalue(const void *a, const void *b) diff --git a/tools/perf/util/srcline.c b/tools/perf/util/srcline.c index 5ed89f97ff11..a8d16106f619 100644 --- a/tools/perf/util/srcline.c +++ b/tools/perf/util/srcline.c @@ -301,6 +301,7 @@ char *__get_srcline(struct dso *dso, u64 addr, struct symbol *sym, if (dso__a2l_fails(dso) > A2L_FAIL_LIMIT) { dso__set_has_srcline(dso, false); dso__free_a2l(dso); + dso__free_a2l_libbfd(dso); } mutex_unlock(dso__lock(dso)); out: @@ -348,6 +349,7 @@ char *get_srcline_split(struct dso *dso, u64 addr, unsigned *line) if (dso__a2l_fails(dso) > A2L_FAIL_LIMIT) { dso__set_has_srcline(dso, false); dso__free_a2l(dso); + dso__free_a2l_libbfd(dso); } mutex_unlock(dso__lock(dso)); From 3d8fb5a4a1d793f6afdfe6df6868f82e610e447e Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:04:50 -0700 Subject: [PATCH 083/425] perf libbfd: Report success when an address is found libbfd__addr2line() only reports success when the caller asked for a file name. addr2inlines() passes a NULL file as it just wants the inline_node populating, so libbfd__addr2line() returns 0 for it unless bfd_find_inliner_info() happened to find an inline frame. addr2line() in srcline.c treats 0 as a failure and tries the next addr2line implementation, which appends its own frames to the inline_node libbfd already appended to. Every frame that isn't inlined is then reported twice, which happens by default when perf is built with libbfd but without libdw as the fallback order is then libbfd followed by the addr2line command: $ perf record --call-graph dwarf -- perf test -w inlineloop 1 $ perf script --fields +srcline ... 56051a994f8e parent+0x2e (perf) inlineloop.c:32 56051a99503a inlineloop+0x8a (perf) inlineloop.c:47 56051a99503a inlineloop+0x8a (perf) inlineloop.c:47 56051a95841a cmd_test+0xb7a (perf) ??:0 56051a95841a cmd_test+0xb7a (perf) ??:0 ... Report success whenever the address is found, like libdw__addr2line() does, and clear the frames appended so far when appending fails so that a following implementation starts from an empty node. Fixes: 257046a36750a6db ("perf srcline: Fallback between addr2line implementations") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Cc: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/libbfd.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tools/perf/util/libbfd.c b/tools/perf/util/libbfd.c index 2a73124b76b8..936555217e81 100644 --- a/tools/perf/util/libbfd.c +++ b/tools/perf/util/libbfd.c @@ -258,6 +258,7 @@ int libbfd__addr2line(const char *dso_name, u64 addr, int cnt = 0; if (node && inline_list__append_dso_a2l(dso, node, sym)) { + inline_node__clear_frames(node); ret = 0; goto out; } @@ -271,23 +272,32 @@ int libbfd__addr2line(const char *dso_name, u64 addr, if (node != NULL) { if (inline_list__append_dso_a2l(dso, node, sym)) { + inline_node__clear_frames(node); ret = 0; goto out; } - // found at least one inline frame - ret = 1; } } } if (file) { *file = a2l->filename ? strdup(a2l->filename) : NULL; - ret = *file ? 1 : 0; + if (!*file) { + /* Leave ret as 0 so that another addr2line is tried. */ + goto out; + } } if (line) *line = a2l->line; + /* + * The address was found, report success so that the caller doesn't try + * another addr2line implementation that would append the inline frames + * above a second time. + */ + ret = 1; + out: mutex_unlock(dso__lock(dso)); return ret; From ce7948448d99fcb5e8a564c46fbc96a9282b78eb Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:07:00 -0700 Subject: [PATCH 084/425] perf evlist: Implement evlist__can_merge_hybrid using first_wildcard_match Add logic to dynamically identify mergeable events spawned from the same wildcard alias via first_wildcard_match. first_wildcard_match is populated when events are parsed, in `perf report` the events come from a file and so the first_wildcard_match is computed by matching event names. evlist__can_merge_hybrid only tests whether merging is possible and doesn't modify the evlist, so that it may be used as a predicate, for example, to decide whether to display a hint. The linking of all the matching events is done by evlist__merge_hybrid. is_pmu_core_len identifies core PMUs using the hybrid topology of the perf.data file. Should the file lack the topology, the PMUs of this machine are scanned but only when the file was recorded on the same architecture, as PMU names like "cpu_core" are architecture specific. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/evlist.c | 183 +++++++++++++++++++++++++++++++++ tools/perf/util/evlist.h | 3 + tools/perf/util/evsel.h | 1 + tools/perf/util/hist.h | 1 + tools/perf/util/parse-events.c | 4 +- tools/perf/util/pmu.c | 43 +++++++- tools/perf/util/pmu.h | 3 + tools/perf/util/symbol_conf.h | 1 + 8 files changed, 236 insertions(+), 3 deletions(-) diff --git a/tools/perf/util/evlist.c b/tools/perf/util/evlist.c index c3d784727810..930efbb66bc8 100644 --- a/tools/perf/util/evlist.c +++ b/tools/perf/util/evlist.c @@ -7,6 +7,7 @@ */ #include "evlist.h" +#include #include #include #include @@ -53,6 +54,7 @@ #include "event.h" #include "evsel.h" #include "evsel_fprintf.h" +#include "hist.h" #include "intel-tpebs.h" #include "metricgroup.h" #include "mmap.h" @@ -125,12 +127,24 @@ struct evlist *evlist__new_default(const struct target *target, bool sample_call if (err) goto out_err; } else { + struct evsel *leader = NULL; + + /* Fallback for cross-platform file analysis missing the topology header */ while ((pmu = perf_pmus__scan_core(pmu)) != NULL) { snprintf(buf, sizeof(buf), "%s/cycles/%s", pmu->name, can_profile_kernel ? "P" : "Pu"); err = parse_event(evlist, buf); if (err) goto out_err; + + if (!leader) { + leader = evlist__last(evlist); + } else { + struct evsel *last = evlist__last(evlist); + + if (last != leader) + last->first_wildcard_match = leader; + } } } @@ -148,6 +162,175 @@ struct evlist *evlist__new_default(const struct target *target, bool sample_call return NULL; } +/** + * evlist__hybrid_matches - find events that may be merged across core PMUs. + * @evlist: The evlist to check. + * @env: The perf_env containing the PMU mapping information, NULL for the + * current machine. + * @link: Should the matches be recorded in first_wildcard_match? + * + * perf record, top, etc. set first_wildcard_match in the event parsing. The + * perf.data case (e.g. perf report) recomputes the first_wildcard_match using + * string matches for core events on hybrid systems. + */ +static bool evlist__hybrid_matches(struct evlist *evlist, struct perf_env *env, bool link) +{ + struct evsel *pos; + unsigned int nr = 0; + bool found = false; + + evlist__for_each_entry(evlist, pos) { + if (evsel__is_dummy_event(pos)) + continue; + + if (pos->core.leader != &pos->core || pos->core.nr_members > 1) + return false; + + if (pos->first_wildcard_match) + found = true; + nr++; + } + + /* No events found. */ + if (nr <= 1) + return false; + + /* Found wildcard events from parsing. */ + if (found) + return true; + + /* Try to find matching core events and set the first_wildcard_match. */ + evlist__for_each_entry(evlist, pos) { + const char *pos_name; + const char *pos_match; + struct evsel *peer; + + if (evsel__is_dummy_event(pos) || pos->first_wildcard_match) + continue; + + pos_name = evsel__name(pos); + pos_match = pos_name ? strchr(pos_name, '/') : NULL; + + if (!pos_match) + continue; + + /* If evsel->core.is_pmu_core missing in report, fallback to perf_env */ + if (!evsel__is_hybrid(pos)) { + if (!is_pmu_core_len(env, pos_name, pos_match - pos_name)) + continue; + } + + peer = pos; + list_for_each_entry_continue(peer, &evlist__core(evlist)->entries, core.node) { + const char *peer_name; + const char *peer_match; + + if (evsel__is_dummy_event(peer) || peer->first_wildcard_match) + continue; + + peer_name = evsel__name(peer); + peer_match = peer_name ? strchr(peer_name, '/') : NULL; + if (!peer_match) + continue; + + if (!evsel__is_hybrid(peer)) { + if (!is_pmu_core_len(env, peer_name, + peer_match - peer_name)) + continue; + } + + if (strcmp(pos_match, peer_match)) + continue; + + found = true; + if (!link) { + /* Just a test, don't modify the evlist. */ + return true; + } + /* + * Keep looking so that all the events of this name are + * linked, there may be more than 2 core PMUs. + */ + peer->first_wildcard_match = pos; + } + } + return found; +} + +/** + * evlist__can_merge_hybrid - can events in the evlist be merged across core + * PMUs? The evlist isn't modified. + * @evlist: The evlist to check. + * @env: The perf_env containing the PMU mapping information, NULL for the + * current machine. + */ +bool evlist__can_merge_hybrid(struct evlist *evlist, struct perf_env *env) +{ + return evlist__hybrid_matches(evlist, env, /*link=*/false); +} + +/* + * evlist__merge_hybrid - group hybrid events logically together. + * @evlist: The evlist containing events to merge. + * @env: The perf_env containing the PMU mapping information, NULL for the + * current machine. + * + * Iterates through the evlist and merges associated hybrid events by assigning + * their first_wildcard_match as their core group leader, modifying their + * presentation for a single merged histogram view. + */ +void evlist__merge_hybrid(struct evlist *evlist, struct perf_env *env) +{ + struct list_head new_list; + struct evsel *member, *mtmp; + struct evsel *pos, *tmp; + int idx = 0; + + /* Compute first_wildcard_match for events that lack it, say from a file. */ + if (!evlist__hybrid_matches(evlist, env, /*link=*/true)) + return; + + evlist__for_each_entry_safe(evlist, tmp, pos) { + struct evsel *leader, *old_leader; + + if (evsel__is_dummy_event(pos)) + continue; + + if (!pos->first_wildcard_match) + continue; + + leader = evsel__leader(pos->first_wildcard_match); + old_leader = evsel__leader(pos); + if (old_leader == leader) + continue; + + if (old_leader != pos) + old_leader->core.nr_members--; + pos->core.leader = &leader->core; + pos->merged_hybrid_group = true; + /* Base is 1 to natively represent the leader */ + if (leader->core.nr_members == 0) + leader->core.nr_members = 1; + leader->core.nr_members++; + } + + INIT_LIST_HEAD(&new_list); + + while (!list_empty(&evlist__core(evlist)->entries)) { + pos = list_first_entry(&evlist__core(evlist)->entries, struct evsel, core.node); + list_move_tail(&pos->core.node, &new_list); + + list_for_each_entry_safe(member, mtmp, &evlist__core(evlist)->entries, core.node) { + if (member->core.leader == &pos->core) + list_move_tail(&member->core.node, &new_list); + } + } + list_splice_init(&new_list, &evlist__core(evlist)->entries); + + evlist__for_each_entry(evlist, pos) + pos->core.idx = idx++; +} + struct evlist *evlist__new_dummy(void) { struct evlist *evlist = evlist__new(); diff --git a/tools/perf/util/evlist.h b/tools/perf/util/evlist.h index 838e263b76f3..0f380421e03d 100644 --- a/tools/perf/util/evlist.h +++ b/tools/perf/util/evlist.h @@ -331,6 +331,9 @@ static inline void evlist__set_selected(struct evlist *evlist, struct evsel *evs struct evlist *evlist__new(void); struct evlist *evlist__new_default(const struct target *target, bool sample_callchains); +struct perf_env; +bool evlist__can_merge_hybrid(struct evlist *evlist, struct perf_env *env); +void evlist__merge_hybrid(struct evlist *evlist, struct perf_env *env); struct evlist *evlist__new_dummy(void); struct evlist *evlist__get(struct evlist *evlist); void evlist__put(struct evlist *evlist); diff --git a/tools/perf/util/evsel.h b/tools/perf/util/evsel.h index 6f759b0e86e9..5c5799cee601 100644 --- a/tools/perf/util/evsel.h +++ b/tools/perf/util/evsel.h @@ -53,6 +53,7 @@ struct evsel { int id_pos; int is_pos; unsigned int sample_size; + bool merged_hybrid_group; /* * These fields can be set in the parse-events code or similar. diff --git a/tools/perf/util/hist.h b/tools/perf/util/hist.h index b30375a203e7..d0ed43807cf6 100644 --- a/tools/perf/util/hist.h +++ b/tools/perf/util/hist.h @@ -130,6 +130,7 @@ struct hists { struct hists_stats stats; u64 event_stream; u16 col_len[HISTC_NR_COLS]; + bool merge_entries; bool has_callchains; int socket_filter; struct perf_hpp_list *hpp_list; diff --git a/tools/perf/util/parse-events.c b/tools/perf/util/parse-events.c index cc7ad331a49f..e40c16cbef01 100644 --- a/tools/perf/util/parse-events.c +++ b/tools/perf/util/parse-events.c @@ -1683,7 +1683,7 @@ int parse_events_multi_pmu_add(struct parse_events_state *parse_state, strbuf_release(&sb); ok++; } - if (first_wildcard_match == NULL) + if (first_wildcard_match == NULL && !list_empty(list)) first_wildcard_match = container_of(list->prev, struct evsel, core.node); } @@ -1754,7 +1754,7 @@ int parse_events_multi_pmu_add_or_add_pmu(struct parse_events_state *parse_state ok++; parse_state->wild_card_pmus = true; } - if (first_wildcard_match == NULL) { + if (first_wildcard_match == NULL && !list_empty(*listp)) { first_wildcard_match = container_of((*listp)->prev, struct evsel, core.node); } diff --git a/tools/perf/util/pmu.c b/tools/perf/util/pmu.c index 836e3b5615cd..825c0801c86a 100644 --- a/tools/perf/util/pmu.c +++ b/tools/perf/util/pmu.c @@ -2036,7 +2036,7 @@ int perf_pmu__for_each_format(struct perf_pmu *pmu, void *state, pmu_format_call } /** - * is_pmu_core() - Check if the given PMU name corresponds to a core CPU PMU. + * is_pmu_core() - Runtime check if the given PMU name corresponds to a core CPU PMU. * @name: The PMU name to check. * * Core PMUs can be identified by: @@ -2057,6 +2057,47 @@ bool is_pmu_core(const char *name) is_sysfs_pmu_core(name); } +/** + * is_pmu_core_len - Perf env based check if a given string prefix matches a core PMU name. + * @env: The perf_env to check within, NULL for the current machine. + * @name: The PMU name to check. + * @len: The length of the PMU name prefix in the string. + */ +bool is_pmu_core_len(struct perf_env *env, const char *name, size_t len) +{ + struct perf_pmu *pmu = NULL; + + if (env && env->hybrid_nodes) { + for (int i = 0; i < env->nr_hybrid_nodes; i++) { + const char *pmu_name = env->hybrid_nodes[i].pmu_name; + + if (strlen(pmu_name) == len && !strncmp(name, pmu_name, len)) + return true; + } + return false; + } + + /* + * No hybrid topology, either the machine isn't hybrid or the perf.data + * file was written by a perf lacking HEADER_HYBRID_TOPOLOGY. Fall back + * to the PMUs of this machine, which is only meaningful when the data + * was recorded on this architecture. Guessing PMU names from a prefix + * isn't portable, for example, big.LITTLE ARM and Apple core PMUs are + * named after their CPU rather than "cpu_core" and "cpu_atom". + */ + if (env && strcmp(perf_env__arch(env), perf_env__arch(/*env=*/NULL))) { + pr_debug("Can't identify core PMUs of a %s perf.data file recorded without hybrid topology\n", + perf_env__arch(env)); + return false; + } + + while ((pmu = perf_pmus__scan_core(pmu)) != NULL) { + if (strlen(pmu->name) == len && !strncmp(name, pmu->name, len)) + return true; + } + return false; +} + bool perf_pmu__supports_legacy_cache(const struct perf_pmu *pmu) { return pmu->is_core; diff --git a/tools/perf/util/pmu.h b/tools/perf/util/pmu.h index 0d9f3c57e8e8..2c8916ad073e 100644 --- a/tools/perf/util/pmu.h +++ b/tools/perf/util/pmu.h @@ -16,6 +16,7 @@ struct evsel_config_term; struct hashmap; struct perf_cpu_map; +struct perf_env; struct print_callbacks; enum { @@ -289,6 +290,8 @@ int perf_pmu__for_each_format(struct perf_pmu *pmu, void *state, pmu_format_call u64 perf_pmu__format_unpack(unsigned long *format, u64 config_val); bool is_pmu_core(const char *name); +bool is_pmu_core_len(struct perf_env *env, const char *name, size_t len); + bool perf_pmu__supports_legacy_cache(const struct perf_pmu *pmu); bool perf_pmu__auto_merge_stats(const struct perf_pmu *pmu); bool perf_pmu__have_event(struct perf_pmu *pmu, const char *name); diff --git a/tools/perf/util/symbol_conf.h b/tools/perf/util/symbol_conf.h index 71f60081a85b..50317bdaf2b6 100644 --- a/tools/perf/util/symbol_conf.h +++ b/tools/perf/util/symbol_conf.h @@ -38,6 +38,7 @@ enum symbol__weight_mode { for ((_weight) = WEIGHT_WEIGHT; (_weight) <= WEIGHT_WEIGHT3; (_weight)++) struct symbol_conf { + bool hybrid_merge; bool nanosecs; unsigned short priv_size; bool try_vmlinux_path, From bcb89e6320c7b86bcfad7278be21bab54e68c378 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:07:01 -0700 Subject: [PATCH 085/425] perf ui hist: Add support for aggregated total_period and merging entries Add the histogram side of hybrid event merging. When events are merged the entries of the other events are linked to the leader's entries as pairs, and a row is then displayed as the sum over an entry and its pairs. Teach the hpp code to work on that sum: - The formatting and sorting routines add up the value of an entry and of each of its pairs, so a row shows, and is sorted by, the total across the core PMUs rather than the leader's value alone. - Percentages are taken against the summed total_period of the merged hists. Computing each event's share against its own PMU's total would leave the column not adding up to 100%. hist_entry__get_percent_limit_merged() does the same for --percent-limit, which would otherwise compare against the wrong total and filter out the wrong entries. - A header and column width are reserved for the "Total" the merged value is shown under. evlist__merge_hists_hybrid() links the hists together and resorts the result so that it is ordered by the merged total. hists__link() is run once for every event merged into the leader, so an entry can be offered as a pair more than once. hist_entry__add_pair() used list_add_tail() unconditionally, which corrupts the list when the entry is already on one. Return early if the entry is already paired with this one, and use list_move_tail() so that an entry linked elsewhere is moved rather than added a second time. Merging is never combined with --hierarchy, which the merge_entries tests rely on. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/ui/hist.c | 246 +++++++++++++++++++++++++++++++++++++++-- tools/perf/util/hist.h | 14 ++- 2 files changed, 252 insertions(+), 8 deletions(-) diff --git a/tools/perf/ui/hist.c b/tools/perf/ui/hist.c index e58327595d37..09da45c80889 100644 --- a/tools/perf/ui/hist.c +++ b/tools/perf/ui/hist.c @@ -57,8 +57,9 @@ struct hpp_fmt_value { }; static int __hpp__fmt(struct perf_hpp *hpp, struct hist_entry *he, - hpp_field_fn get_field, const char *fmt, int len, - hpp_snprint_fn print_fn, enum perf_hpp_fmt_type fmtype) + hpp_field_fn get_field, const char *fmtstr, int len, + hpp_snprint_fn print_fn, enum perf_hpp_fmt_type fmtype, + struct perf_hpp_fmt *fmt __maybe_unused) { int ret = 0; struct hists *hists = he->hists; @@ -98,13 +99,50 @@ static int __hpp__fmt(struct perf_hpp *hpp, struct hist_entry *he, } } + /* Note, merge_entries implies !symbol_conf.report_hierarchy. */ + if (he->hists->merge_entries) { + u64 total_val = 0; + u64 total_samples = 0; + u64 total_period = 0; + + for (i = 0; i < nr_members; i++) { + struct evsel *member_evsel = hists_to_evsel(values[i].hists); + + struct evsel *he_evsel = hists_to_evsel(he->hists); + + if (member_evsel != he_evsel && + member_evsel->first_wildcard_match != he_evsel) + continue; + + total_val += values[i].val; + total_samples += values[i].samples; + total_period += fmtype == PERF_HPP_FMT_TYPE__PERCENT ? + hists__total_period(values[i].hists) : + hists__total_latency(values[i].hists); + } + + if (fmtype == PERF_HPP_FMT_TYPE__PERCENT || fmtype == PERF_HPP_FMT_TYPE__LATENCY) { + double percent = 0.0; + + if (total_period) + percent = 100.0 * total_val / total_period; + ret += hpp__call_print_fn(hpp, print_fn, fmtstr, len, percent); + } else if (fmtype == PERF_HPP_FMT_TYPE__AVERAGE) { + double avg = total_samples ? (1.0 * total_val / total_samples) : 0; + + ret += hpp__call_print_fn(hpp, print_fn, fmtstr, len, avg); + } else { + ret += hpp__call_print_fn(hpp, print_fn, fmtstr, len, total_val); + } + } + for (i = 0; i < nr_members; i++) { if (symbol_conf.skip_empty && values[i].hists->stats.nr_samples == 0) continue; ret += __hpp__fmt_print(hpp, values[i].hists, values[i].val, - values[i].samples, fmt, len, + values[i].samples, fmtstr, len, print_fn, fmtype); } @@ -129,7 +167,7 @@ int hpp__fmt(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp, if (symbol_conf.field_sep) { return __hpp__fmt(hpp, he, get_field, fmtstr, 1, - print_fn, fmtype); + print_fn, fmtype, fmt); } if (fmtype == PERF_HPP_FMT_TYPE__PERCENT || fmtype == PERF_HPP_FMT_TYPE__LATENCY) @@ -137,7 +175,7 @@ int hpp__fmt(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp, else len -= 1; - return __hpp__fmt(hpp, he, get_field, fmtstr, len, print_fn, fmtype); + return __hpp__fmt(hpp, he, get_field, fmtstr, len, print_fn, fmtype, fmt); } int hpp__fmt_acc(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp, @@ -287,6 +325,35 @@ static int __hpp__sort(struct hist_entry *a, struct hist_entry *b, return __hpp__group_sort_idx(a, b, get_field, symbol_conf.group_sort_idx); } + /* + * Relies on merge_entries being only enabled if there are + * only matching events. If that is ever relaxed will need + * more logic here. merge_entries also implies that + * symbol_conf.report_hierarchy is false. + */ + if (a->hists->merge_entries && b->hists->merge_entries) { + u64 val_a = get_field(a), val_b = get_field(b); + struct hist_entry *pair; + struct evsel *evsel_a = hists_to_evsel(a->hists); + struct evsel *evsel_b = hists_to_evsel(b->hists); + + list_for_each_entry(pair, &a->pairs.head, pairs.node) { + struct evsel *pair_evsel = hists_to_evsel(pair->hists); + + if (pair_evsel->first_wildcard_match == evsel_a) + val_a += get_field(pair); + } + list_for_each_entry(pair, &b->pairs.head, pairs.node) { + struct evsel *pair_evsel = hists_to_evsel(pair->hists); + + if (pair_evsel->first_wildcard_match == evsel_b) + val_b += get_field(pair); + } + + ret = field_cmp(val_a, val_b); + if (ret) + return ret; + } ret = field_cmp(get_field(a), get_field(b)); if (ret || !symbol_conf.event_group) @@ -323,7 +390,29 @@ static int __hpp__sort_acc(struct hist_entry *a, struct hist_entry *b, /* * Put caller above callee when they have equal period. */ - ret = field_cmp(get_field(a), get_field(b)); + if (a->hists->merge_entries && b->hists->merge_entries) { + u64 val_a = get_field(a), val_b = get_field(b); + struct hist_entry *pair; + struct evsel *evsel_a = hists_to_evsel(a->hists); + struct evsel *evsel_b = hists_to_evsel(b->hists); + + list_for_each_entry(pair, &a->pairs.head, pairs.node) { + struct evsel *pair_evsel = hists_to_evsel(pair->hists); + + if (pair_evsel->first_wildcard_match == evsel_a) + val_a += get_field(pair); + } + list_for_each_entry(pair, &b->pairs.head, pairs.node) { + struct evsel *pair_evsel = hists_to_evsel(pair->hists); + + if (pair_evsel->first_wildcard_match == evsel_b) + val_b += get_field(pair); + } + + ret = field_cmp(val_a, val_b); + } else { + ret = field_cmp(get_field(a), get_field(b)); + } if (ret) return ret; @@ -386,6 +475,8 @@ static int hpp__width_fn(struct perf_hpp_fmt *fmt, evsel__hists(pos)->stats.nr_samples) nr++; } + if (hists->merge_entries) + nr++; /* Add 1 extra unit of width generically for the 'Total' */ len = max(len, nr * fmt->len); } @@ -403,8 +494,38 @@ static int hpp__header_fn(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp, int len = hpp__width_fn(fmt, hpp, hists); const char *hdr = ""; - if (line == hists->hpp_list->nr_header_lines - 1) + if (line == hists->hpp_list->nr_header_lines - 1) { hdr = fmt->name; + if (hists->merge_entries) { + int w = 0; + int f_len = fmt->user_len ?: fmt->len; + struct evsel *pos, *evsel = hists_to_evsel(hists); + int string_len = f_len; + + for_each_group_evsel(pos, evsel) { + if (symbol_conf.skip_empty && + evsel__hists(pos)->stats.nr_samples == 0) + continue; + string_len += f_len; + } + + if (len > string_len) { + w += scnprintf(hpp->buf + w, hpp->size - w, "%*s", + len - string_len, ""); + } + + w += scnprintf(hpp->buf + w, hpp->size - w, "%*.*s", + f_len, f_len, fmt->name); + for_each_group_evsel(pos, evsel) { + if (symbol_conf.skip_empty && + evsel__hists(pos)->stats.nr_samples == 0) + continue; + w += scnprintf(hpp->buf + w, hpp->size - w, " %*.*s", + f_len - 1, f_len - 1, evsel__name(pos)); + } + return w; + } + } return scnprintf(hpp->buf, hpp->size, "%*s", len, hdr); } @@ -1271,3 +1392,114 @@ int perf_hpp__alloc_mem_stats(struct perf_hpp_list *list, struct evlist *evlist) } return 0; } + +float hist_entry__get_percent_limit_merged(struct hist_entry *he) +{ + struct hist_entry *pair; + u64 period = he->stat.period; + u64 total_period = hists__total_period(he->hists); + struct evsel *evsel = hists_to_evsel(he->hists); + struct evsel *pos; + + /* Accumulate global total_period across all merged hists matching hybrid type */ + for_each_group_member(pos, evsel) { + if (pos->first_wildcard_match == evsel) + total_period += hists__total_period(evsel__hists(pos)); + } + + if (unlikely(total_period == 0)) + return 0; + + if (symbol_conf.cumulate_callchain) { + period = he->stat_acc->period; + list_for_each_entry(pair, &he->pairs.head, pairs.node) { + struct evsel *pair_evsel = hists_to_evsel(pair->hists); + + if (pair_evsel->first_wildcard_match == evsel) + period += pair->stat_acc->period; + } + } else { + /* Accumulate symbol specific period across pairs matching hybrid type */ + list_for_each_entry(pair, &he->pairs.head, pairs.node) { + struct evsel *pair_evsel = hists_to_evsel(pair->hists); + + if (pair_evsel->first_wildcard_match == evsel) + period += pair->stat.period; + } + } + + return period * 100.0 / total_period; +} + +void evlist__merge_hists_hybrid(struct evlist *evlist, bool refresh_hists) +{ + struct evsel *pos; + struct evsel *member; + bool hybrid_group; + + /* + * Merged hists display all the events of a group in a single set of + * entries, which the hierarchy display has no way to render. Keeping + * hists->merge_entries false here means the rest of the display code + * can assume merge_entries implies !symbol_conf.report_hierarchy. + */ + if (symbol_conf.report_hierarchy) + return; + + /* Set merge_entries flag strictly on leaders formulated by hybrid topology */ + evlist__for_each_entry(evlist, pos) { + if (evsel__is_dummy_event(pos)) + continue; + + if (pos->core.leader == &pos->core && pos->core.nr_members > 1) { + hybrid_group = false; + + if (pos->first_wildcard_match || pos->merged_hybrid_group) { + hybrid_group = true; + } else { + for_each_group_member(member, pos) { + if (member->first_wildcard_match || + member->merged_hybrid_group) { + hybrid_group = true; + break; + } + } + } + + if (hybrid_group) { + evsel__hists(pos)->merge_entries = true; + symbol_conf.event_group = true; + symbol_conf.hybrid_merge = true; + if (!pos->group_name) { + pos->group_name = strdup("Merged hybrid events"); + if (!pos->group_name) + pr_warning("Failed to allocate hybrid group name\n"); + } + } + } + } + + if (!refresh_hists) + return; + + evlist__for_each_entry(evlist, pos) { + /* Match histograms dynamically since parsing happened before group toggling */ + if (symbol_conf.event_group && !evsel__is_group_leader(pos)) { + struct hists *leader_hists = evsel__hists(evsel__leader(pos)); + struct hists *hists = evsel__hists(pos); + + hists__match(leader_hists, hists); + hists__link(leader_hists, hists); + } + } + + /* Now that links are formed, safely resort the active tree so the UI renders accurately */ + if (symbol_conf.event_group) { + evlist__for_each_entry(evlist, pos) { + if (evsel__is_dummy_event(pos) || !evsel__is_group_leader(pos)) + continue; + if (pos->core.nr_members > 1 && evsel__hists(pos)->merge_entries) + hists__output_resort(evsel__hists(pos), NULL); + } + } +} diff --git a/tools/perf/util/hist.h b/tools/perf/util/hist.h index d0ed43807cf6..0d08011aebce 100644 --- a/tools/perf/util/hist.h +++ b/tools/perf/util/hist.h @@ -339,7 +339,13 @@ static inline struct hist_entry *hist_entry__next_pair(struct hist_entry *he) static inline void hist_entry__add_pair(struct hist_entry *pair, struct hist_entry *he) { - list_add_tail(&pair->pairs.node, &he->pairs.head); + struct list_head *pos; + + list_for_each(pos, &he->pairs.head) { + if (pos == &pair->pairs.node) + return; /* Already paired */ + } + list_move_tail(&pair->pairs.node, &he->pairs.head); } struct hist_entry *hists__add_entry(struct hists *hists, @@ -434,11 +440,17 @@ void hists__match(struct hists *leader, struct hists *other); int hists__link(struct hists *leader, struct hists *other); int hists__unlink(struct hists *hists); +float hist_entry__get_percent_limit_merged(struct hist_entry *he); +void evlist__merge_hists_hybrid(struct evlist *evlist, bool refresh); + static inline float hist_entry__get_percent_limit(struct hist_entry *he) { u64 period = he->stat.period; u64 total_period = hists__total_period(he->hists); + if (he->hists->merge_entries) + return hist_entry__get_percent_limit_merged(he); + if (unlikely(total_period == 0)) return 0; From 6fa291f8056bc57684ae668439eb413d30fab01a Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:07:02 -0700 Subject: [PATCH 086/425] perf tools: Expose opt-in --hybrid-merge Add --hybrid-merge to perf report and perf top. It merges the events a wildcard expanded to across the core PMUs, and their histograms, so that a symbol which ran on more than one kind of core is reported once with the total rather than once per PMU. evlist__merge_hybrid() links the events and evlist__merge_hists_hybrid() links the histograms. There is nothing to merge on a machine with a single core PMU, or when the events didn't come from a wildcard, so warn in that case rather than quietly producing an unmerged report. Merging collapses the per-PMU entries into one set, which doesn't combine with the per-level breakdown of --hierarchy, so asking for both is an error. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Tested-by: Arnaldo Carvalho de Melo Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-report.txt | 5 +++++ tools/perf/Documentation/perf-top.txt | 5 +++++ tools/perf/builtin-report.c | 19 +++++++++++++++++++ tools/perf/builtin-top.c | 17 +++++++++++++++++ tools/perf/util/symbol.c | 1 + 5 files changed, 47 insertions(+) diff --git a/tools/perf/Documentation/perf-report.txt b/tools/perf/Documentation/perf-report.txt index 1a4706329c6c..3718ebd297ce 100644 --- a/tools/perf/Documentation/perf-report.txt +++ b/tools/perf/Documentation/perf-report.txt @@ -578,6 +578,11 @@ include::itrace.txt[] --raw-trace:: When displaying traceevent output, do not use print fmt or plugins. +--hybrid-merge:: + Merge matching events from all hybrid core PMUs into one + display. For example, if a wildcard expands to run on both p-cores and + e-cores, this aggregates them into a single view. + -H:: --hierarchy:: Enable hierarchical output. In the hierarchy mode, each sort key groups diff --git a/tools/perf/Documentation/perf-top.txt b/tools/perf/Documentation/perf-top.txt index 2da2a16bbf26..c5e96da6ed82 100644 --- a/tools/perf/Documentation/perf-top.txt +++ b/tools/perf/Documentation/perf-top.txt @@ -49,6 +49,11 @@ Default is to monitor all CPUS. encoding with the layout of the event control registers as described by entries in /sys/bus/event_source/devices/cpu/format/*. +--hybrid-merge:: + Merge matching events from all hybrid core PMUs into one + display. For example, if a wildcard expands to run on both p-cores and + e-cores, this aggregates them into a single view. + --filter=:: Event filter. This option should follow an event selector (-e). For syntax see linkperf:perf-record[1]. diff --git a/tools/perf/builtin-report.c b/tools/perf/builtin-report.c index 279e61c2366c..bda4836fc524 100644 --- a/tools/perf/builtin-report.c +++ b/tools/perf/builtin-report.c @@ -1114,6 +1114,17 @@ static int __cmd_report(struct report *rep) evlist__for_each_entry(session->evlist, pos) rep->nr_entries += evsel__hists(pos)->nr_entries; + if (symbol_conf.hybrid_merge) { + struct perf_env *env = perf_session__env(session); + + if (evlist__can_merge_hybrid(session->evlist, env)) { + evlist__merge_hybrid(session->evlist, env); + evlist__merge_hists_hybrid(session->evlist, false); + } else { + ui__warning("--hybrid-merge: no events to merge across core PMUs\n"); + } + } + if (use_browser == 0) { if (verbose > 3) perf_session__fprintf(session, stdout); @@ -1449,6 +1460,8 @@ int cmd_report(int argc, const char **argv) parse_branch_mode), OPT_BOOLEAN(0, "branch-history", &branch_call_mode, "add last branch records to call history"), + OPT_BOOLEAN(0, "hybrid-merge", &symbol_conf.hybrid_merge, + "merge the same event across hybrid core PMUs"), OPT_STRING(0, "objdump", &objdump_path, "path", "objdump binary to use for disassembly and annotations"), OPT_STRING(0, "addr2line", &addr2line_path, "path", @@ -1548,6 +1561,12 @@ int cmd_report(int argc, const char **argv) report.symbol_filter_str = argv[0]; } + if (symbol_conf.report_hierarchy && symbol_conf.hybrid_merge) { + pr_err("Error: --hierarchy and --hybrid-merge are mutually exclusive.\n"); + ret = -EINVAL; + goto exit; + } + if (disassembler_style) { annotate_opts.disassembler_style = strdup(disassembler_style); if (!annotate_opts.disassembler_style) diff --git a/tools/perf/builtin-top.c b/tools/perf/builtin-top.c index c2562d49be46..3bb3337f1244 100644 --- a/tools/perf/builtin-top.c +++ b/tools/perf/builtin-top.c @@ -1336,6 +1336,15 @@ static int __cmd_top(struct perf_top *top) if (!target__none(&opts->target)) evlist__enable(top->evlist); + if (symbol_conf.hybrid_merge) { + if (evlist__can_merge_hybrid(top->evlist, /*env=*/NULL)) { + evlist__merge_hybrid(top->evlist, /*env=*/NULL); + evlist__merge_hists_hybrid(top->evlist, false); + } else { + ui__warning("--hybrid-merge: no events to merge across core PMUs\n"); + } + } + ret = -1; if (pthread_create(&thread_process, NULL, process_thread, top)) { ui__error("Could not create process thread.\n"); @@ -1491,6 +1500,8 @@ int cmd_top(int argc, const char **argv) OPT_CALLBACK('e', "event", &parse_events_option_args, "event", "event selector. use 'perf list' to list available events", parse_events_option), + OPT_BOOLEAN(0, "hybrid-merge", &symbol_conf.hybrid_merge, + "merge the same event across hybrid core PMUs"), OPT_CALLBACK(0, "filter", &top.evlist, "filter", "event filter", parse_filter), OPT_U64('c', "count", &opts->user_interval, "event period to sample"), @@ -1679,6 +1690,12 @@ int cmd_top(int argc, const char **argv) if (argc) usage_with_options(top_usage, options); + if (symbol_conf.report_hierarchy && symbol_conf.hybrid_merge) { + pr_err("Error: --hierarchy and --hybrid-merge are mutually exclusive.\n"); + status = -EINVAL; + goto out_put_evlist; + } + if (disassembler_style) { annotate_opts.disassembler_style = strdup(disassembler_style); if (!annotate_opts.disassembler_style) { diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c index 3587ad243159..4c1f8231d47f 100644 --- a/tools/perf/util/symbol.c +++ b/tools/perf/util/symbol.c @@ -73,6 +73,7 @@ struct symbol_conf symbol_conf = { .symfs = "", .symfs_layout_flat = false, .event_group = true, + .hybrid_merge = false, .inline_name = true, .res_sample = 0, .addr2line_timeout_ms = 5 * 1000, From aa2bad59fd3e3093d43177e6ee7cbc879af743fa Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:07:03 -0700 Subject: [PATCH 087/425] perf Documentation: Add tip for hybrid event merging Add informative text outlining the IPC imbalances associated with merging cross-hybrid core events like cycles. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Tested-by: Arnaldo Carvalho de Melo Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/tips.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/perf/Documentation/tips.txt b/tools/perf/Documentation/tips.txt index ebf12a8c5db5..1c7b309fe6e8 100644 --- a/tools/perf/Documentation/tips.txt +++ b/tools/perf/Documentation/tips.txt @@ -66,3 +66,4 @@ For latency profiling, try: perf record/report --latency For parallelism histogram, try: perf report --hierarchy --sort latency,parallelism,comm,symbol To analyze particular parallelism levels, try: perf report --latency --parallelism=32-64 To see how parallelism changes over time, try: perf report -F time,latency,parallelism --time-quantum=1s +When merging events like cycles, different core frequencies and instructions per cycle mean the counts may not fairly reflect time spent in a function. From a25262fb1610b7d52bf3ed5a72ebbd33f4252de6 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:07:04 -0700 Subject: [PATCH 088/425] perf evlist: Toggle merged_hybrid_group properly in enable/disable Merging makes the merged event the leader of the events of the other core PMUs so their histograms can be linked, but the result isn't a real event group. Each event keeps its own file descriptor and has to be enabled and disabled in its own right. __evlist__enable(), __evlist__disable() and evlist__is_enabled() skip anything that isn't a group leader, and the first two then walk the group members of the events they do act on. For a merged set that is backwards: the members are skipped by the leader test, so their file descriptors are never touched, and the walk over the leader's members only updates the bookkeeping in evsel->disabled. Treat an evsel with merged_hybrid_group set as a leader so that it is enabled and disabled in its own right via its own file descriptors. 'perf top' enables and disables by event name. A merged member carries the name of its own PMU rather than the name that was asked for, so also match it against its leader's name, otherwise naming the event toggles only part of the merged set. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/evlist.c | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/tools/perf/util/evlist.c b/tools/perf/util/evlist.c index 930efbb66bc8..22eb01116992 100644 --- a/tools/perf/util/evlist.c +++ b/tools/perf/util/evlist.c @@ -753,7 +753,9 @@ static bool evlist__is_enabled(struct evlist *evlist) struct evsel *pos; evlist__for_each_entry(evlist, pos) { - if (!evsel__is_group_leader(pos) || !pos->core.fd) + if (!pos->core.fd) + continue; + if (!evsel__is_group_leader(pos) && !pos->merged_hybrid_group) continue; /* If at least one event is enabled, evlist is enabled. */ if (!pos->disabled) @@ -767,14 +769,19 @@ static void __evlist__disable(struct evlist *evlist, char *evsel_name, bool excl struct evsel *pos, *member; struct evlist_cpu_iterator evlist_cpu_itr; bool has_imm = false; + bool match; /* Disable 'immediate' events last */ for (int imm = 0; imm <= 1; imm++) { evlist__for_each_cpu(evlist_cpu_itr, evlist) { pos = evlist_cpu_itr.evsel; - if (evsel__strcmp(pos, evsel_name)) + match = !evsel__strcmp(pos, evsel_name); + if (!match && pos->merged_hybrid_group && evsel__leader(pos)) + match = !evsel__strcmp(evsel__leader(pos), evsel_name); + if (!match) continue; - if (pos->disabled || !evsel__is_group_leader(pos) || !pos->core.fd) + if (pos->disabled || (!evsel__is_group_leader(pos) && + !pos->merged_hybrid_group) || !pos->core.fd) continue; if (excl_dummy && evsel__is_dummy_event(pos)) continue; @@ -789,9 +796,12 @@ static void __evlist__disable(struct evlist *evlist, char *evsel_name, bool excl } evlist__for_each_entry(evlist, pos) { - if (evsel__strcmp(pos, evsel_name)) + match = !evsel__strcmp(pos, evsel_name); + if (!match && pos->merged_hybrid_group && evsel__leader(pos)) + match = !evsel__strcmp(evsel__leader(pos), evsel_name); + if (!match) continue; - if (!evsel__is_group_leader(pos) || !pos->core.fd) + if ((!evsel__is_group_leader(pos) && !pos->merged_hybrid_group) || !pos->core.fd) continue; if (excl_dummy && evsel__is_dummy_event(pos)) continue; @@ -827,21 +837,28 @@ static void __evlist__enable(struct evlist *evlist, char *evsel_name, bool excl_ { struct evsel *pos, *member; struct evlist_cpu_iterator evlist_cpu_itr; + bool match; evlist__for_each_cpu(evlist_cpu_itr, evlist) { pos = evlist_cpu_itr.evsel; - if (evsel__strcmp(pos, evsel_name)) + match = !evsel__strcmp(pos, evsel_name); + if (!match && pos->merged_hybrid_group && evsel__leader(pos)) + match = !evsel__strcmp(evsel__leader(pos), evsel_name); + if (!match) continue; - if (!evsel__is_group_leader(pos) || !pos->core.fd) + if ((!evsel__is_group_leader(pos) && !pos->merged_hybrid_group) || !pos->core.fd) continue; if (excl_dummy && evsel__is_dummy_event(pos)) continue; evsel__enable_cpu(pos, evlist_cpu_itr.cpu_map_idx); } evlist__for_each_entry(evlist, pos) { - if (evsel__strcmp(pos, evsel_name)) + match = !evsel__strcmp(pos, evsel_name); + if (!match && pos->merged_hybrid_group && evsel__leader(pos)) + match = !evsel__strcmp(evsel__leader(pos), evsel_name); + if (!match) continue; - if (!evsel__is_group_leader(pos) || !pos->core.fd) + if ((!evsel__is_group_leader(pos) && !pos->merged_hybrid_group) || !pos->core.fd) continue; if (excl_dummy && evsel__is_dummy_event(pos)) continue; From 59eca524949b5e4b9f3e0691e041996508d37589 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:07:05 -0700 Subject: [PATCH 089/425] perf tools: Add TUI hints for --hybrid-merge Add dynamic TUI hints prompting users to restart with --hybrid-merge when heterogeneous core PMU events are populated in the sample view. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Tested-by: Arnaldo Carvalho de Melo Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/tips.txt | 1 + tools/perf/ui/browsers/hists.c | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/perf/Documentation/tips.txt b/tools/perf/Documentation/tips.txt index 1c7b309fe6e8..eb6d56d4d8ba 100644 --- a/tools/perf/Documentation/tips.txt +++ b/tools/perf/Documentation/tips.txt @@ -67,3 +67,4 @@ For parallelism histogram, try: perf report --hierarchy --sort latency,paralleli To analyze particular parallelism levels, try: perf report --latency --parallelism=32-64 To see how parallelism changes over time, try: perf report -F time,latency,parallelism --time-quantum=1s When merging events like cycles, different core frequencies and instructions per cycle mean the counts may not fairly reflect time spent in a function. +Use --hybrid-merge to merge matching events from hybrid cores into one view diff --git a/tools/perf/ui/browsers/hists.c b/tools/perf/ui/browsers/hists.c index f62cb2d534ed..593e1fd5759d 100644 --- a/tools/perf/ui/browsers/hists.c +++ b/tools/perf/ui/browsers/hists.c @@ -3580,9 +3580,12 @@ static int perf_evsel_menu__run(struct evsel_menu *menu, const char *title = "Available samples"; int delay_secs = hbt ? hbt->refresh : 0; int key; + const char *help_text = "ESC: exit, ENTER|->: Browse histograms"; - if (ui_browser__show(&menu->b, title, - "ESC: exit, ENTER|->: Browse histograms") < 0) + if (!symbol_conf.hybrid_merge && evlist__can_merge_hybrid(evlist, menu->env)) + help_text = "ESC: exit, ENTER|->: Browse. Try --hybrid-merge to combine events."; + + if (ui_browser__show(&menu->b, title, help_text) < 0) return -1; while (1) { From 5c8edd351d1f48f1534260220d5fd4568f423c17 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:07:06 -0700 Subject: [PATCH 090/425] perf config: Add core.hybrid-merge to configure event merging Provide a core.hybrid-merge configuration option in .perfconfig to allow enabling hybrid event aggregation by default, avoiding the need to pass --hybrid-merge explicitly on every invocation. The config value is a default rather than an explicit request, so with --hierarchy it is ignored with a warning, while giving both --hierarchy and --hybrid-merge on the command line remains an error. For the same reason it is silently ignored when there are no events to merge, as would be the case on any machine with a single core PMU, whereas an explicit --hybrid-merge still warns. Record whether the option came from the command line in symbol_conf so both can tell the two apart. 'perf stat' has its own merging options for counting, the sampling core.hybrid-merge deliberately doesn't alter it and its --hybrid-merge must still be given explicitly. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-config.txt | 10 ++++++++ tools/perf/builtin-report.c | 24 ++++++++++++++----- tools/perf/builtin-top.c | 30 +++++++++++++++++------- tools/perf/util/config.c | 10 ++++++++ tools/perf/util/symbol_conf.h | 2 ++ 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/tools/perf/Documentation/perf-config.txt b/tools/perf/Documentation/perf-config.txt index 9b223f892829..6cff6f26af8d 100644 --- a/tools/perf/Documentation/perf-config.txt +++ b/tools/perf/Documentation/perf-config.txt @@ -217,6 +217,16 @@ core.*:: Sets a timeout (in milliseconds) for parsing 'addr2line' output. The default timeout is 5s. + hybrid-merge:: + Merge matching events from all hybrid core PMUs into one display + by default. For example, if a wildcard expands to run on both p-cores and + e-cores, this aggregates them into a single view. This applies to + 'perf report' and 'perf top', it is ignored with '--hierarchy' and + doesn't alter 'perf stat' where '--hybrid-merge' must be given + explicitly. As a default it is silently ignored when there is + nothing to merge, such as on a machine with a single core PMU, + whereas an explicit '--hybrid-merge' warns. + tui.*, gtk.*:: Subcommands that can be configured here are 'top', 'report' and 'annotate'. These values are booleans, for example: diff --git a/tools/perf/builtin-report.c b/tools/perf/builtin-report.c index bda4836fc524..57225bc87731 100644 --- a/tools/perf/builtin-report.c +++ b/tools/perf/builtin-report.c @@ -1120,7 +1120,13 @@ static int __cmd_report(struct report *rep) if (evlist__can_merge_hybrid(session->evlist, env)) { evlist__merge_hybrid(session->evlist, env); evlist__merge_hists_hybrid(session->evlist, false); - } else { + } else if (symbol_conf.hybrid_merge_set) { + /* + * Only an explicit --hybrid-merge warns. A + * core.hybrid-merge default is silent, as most + * machines aren't hybrid and there is nothing the + * user needs to do about it. + */ ui__warning("--hybrid-merge: no events to merge across core PMUs\n"); } } @@ -1460,8 +1466,9 @@ int cmd_report(int argc, const char **argv) parse_branch_mode), OPT_BOOLEAN(0, "branch-history", &branch_call_mode, "add last branch records to call history"), - OPT_BOOLEAN(0, "hybrid-merge", &symbol_conf.hybrid_merge, - "merge the same event across hybrid core PMUs"), + OPT_BOOLEAN_SET(0, "hybrid-merge", &symbol_conf.hybrid_merge, + &symbol_conf.hybrid_merge_set, + "merge the same event across hybrid core PMUs"), OPT_STRING(0, "objdump", &objdump_path, "path", "objdump binary to use for disassembly and annotations"), OPT_STRING(0, "addr2line", &addr2line_path, "path", @@ -1562,9 +1569,14 @@ int cmd_report(int argc, const char **argv) } if (symbol_conf.report_hierarchy && symbol_conf.hybrid_merge) { - pr_err("Error: --hierarchy and --hybrid-merge are mutually exclusive.\n"); - ret = -EINVAL; - goto exit; + if (symbol_conf.hybrid_merge_set) { + pr_err("Error: --hierarchy and --hybrid-merge are mutually exclusive.\n"); + ret = -EINVAL; + goto exit; + } + /* A config file default shouldn't fail an explicit option. */ + pr_warning("core.hybrid-merge ignored: --hierarchy cannot display merged hybrid events\n"); + symbol_conf.hybrid_merge = false; } if (disassembler_style) { diff --git a/tools/perf/builtin-top.c b/tools/perf/builtin-top.c index 3bb3337f1244..a3442d2d6edf 100644 --- a/tools/perf/builtin-top.c +++ b/tools/perf/builtin-top.c @@ -1314,9 +1314,11 @@ static int __cmd_top(struct perf_top *top) } /* - * Use global stat_config that is zero meaning aggr_mode is AGGR_NONE - * and hybrid_merge is false. + * Use global stat_config that is zero meaning aggr_mode is AGGR_NONE. + * Merging affects the event names as merged events share a name, all + * other stat_config behavior is unwanted here. */ + stat_config.hybrid_merge = symbol_conf.hybrid_merge; evlist__uniquify_evsel_names(top->evlist, &stat_config); ret = perf_top__start_counters(top); if (ret) @@ -1340,7 +1342,13 @@ static int __cmd_top(struct perf_top *top) if (evlist__can_merge_hybrid(top->evlist, /*env=*/NULL)) { evlist__merge_hybrid(top->evlist, /*env=*/NULL); evlist__merge_hists_hybrid(top->evlist, false); - } else { + } else if (symbol_conf.hybrid_merge_set) { + /* + * Only an explicit --hybrid-merge warns. A + * core.hybrid-merge default is silent, as most + * machines aren't hybrid and there is nothing the + * user needs to do about it. + */ ui__warning("--hybrid-merge: no events to merge across core PMUs\n"); } } @@ -1500,8 +1508,9 @@ int cmd_top(int argc, const char **argv) OPT_CALLBACK('e', "event", &parse_events_option_args, "event", "event selector. use 'perf list' to list available events", parse_events_option), - OPT_BOOLEAN(0, "hybrid-merge", &symbol_conf.hybrid_merge, - "merge the same event across hybrid core PMUs"), + OPT_BOOLEAN_SET(0, "hybrid-merge", &symbol_conf.hybrid_merge, + &symbol_conf.hybrid_merge_set, + "merge the same event across hybrid core PMUs"), OPT_CALLBACK(0, "filter", &top.evlist, "filter", "event filter", parse_filter), OPT_U64('c', "count", &opts->user_interval, "event period to sample"), @@ -1691,9 +1700,14 @@ int cmd_top(int argc, const char **argv) usage_with_options(top_usage, options); if (symbol_conf.report_hierarchy && symbol_conf.hybrid_merge) { - pr_err("Error: --hierarchy and --hybrid-merge are mutually exclusive.\n"); - status = -EINVAL; - goto out_put_evlist; + if (symbol_conf.hybrid_merge_set) { + pr_err("Error: --hierarchy and --hybrid-merge are mutually exclusive.\n"); + status = -EINVAL; + goto out_put_evlist; + } + /* A config file default shouldn't fail an explicit option. */ + pr_warning("core.hybrid-merge ignored: --hierarchy cannot display merged hybrid events\n"); + symbol_conf.hybrid_merge = false; } if (disassembler_style) { diff --git a/tools/perf/util/config.c b/tools/perf/util/config.c index b2972c35c1ec..8fe43b032e9a 100644 --- a/tools/perf/util/config.c +++ b/tools/perf/util/config.c @@ -470,6 +470,16 @@ static int perf_default_core_config(const char *var, const char *value) if (!strcmp(var, "core.addr2line-disable-warn")) symbol_conf.addr2line_disable_warn = perf_config_bool(var, value); + if (!strcmp(var, "core.hybrid-merge")) { + /* + * Note, this is for sampling tools like perf report and top. + * perf stat has its own merging options and the + * stat_config.hybrid_merge of "perf stat --hybrid-merge" is + * deliberately not set here. + */ + symbol_conf.hybrid_merge = perf_config_bool(var, value); + } + /* Add other config variables here. */ return 0; } diff --git a/tools/perf/util/symbol_conf.h b/tools/perf/util/symbol_conf.h index 50317bdaf2b6..37d35f42dcc1 100644 --- a/tools/perf/util/symbol_conf.h +++ b/tools/perf/util/symbol_conf.h @@ -39,6 +39,8 @@ enum symbol__weight_mode { struct symbol_conf { bool hybrid_merge; + /* Was hybrid_merge given on the command line rather than in a config file? */ + bool hybrid_merge_set; bool nanosecs; unsigned short priv_size; bool try_vmlinux_path, From 06aa9ab061421a2bdbf3af4b51a06b46ea84fe17 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:07:07 -0700 Subject: [PATCH 091/425] perf test: Expand tests for --hybrid-merge Add a "Hybrid event merging" test that checks evlist__can_merge_hybrid() and evlist__merge_hybrid() directly, covering the events a wildcard expanded to over two core PMUs, the same over three, and events that must not be merged. Add a report_hybrid_merge.sh shell test that records and then reports with --hybrid-merge, and extend top.sh to cover perf top with it. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/Build | 1 + tools/perf/tests/builtin-test.c | 1 + tools/perf/tests/hybrid-merge.c | 206 ++++++++++++++++++ tools/perf/tests/shell/report_hybrid_merge.sh | 121 ++++++++++ tools/perf/tests/shell/top.sh | 73 ++++++- tools/perf/tests/tests.h | 1 + 6 files changed, 392 insertions(+), 11 deletions(-) create mode 100644 tools/perf/tests/hybrid-merge.c create mode 100755 tools/perf/tests/shell/report_hybrid_merge.sh diff --git a/tools/perf/tests/Build b/tools/perf/tests/Build index 66944a4f4968..406e48eed1c8 100644 --- a/tools/perf/tests/Build +++ b/tools/perf/tests/Build @@ -65,6 +65,7 @@ perf-test-y += perf-time-to-tsc.o perf-test-y += dlfilter-test.o perf-test-y += sigtrap.o perf-test-y += event_groups.o +perf-test-y += hybrid-merge.o perf-test-y += symbols.o perf-test-y += util.o perf-test-y += hwmon_pmu.o diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-test.c index 4d0784b16723..6293b37266cf 100644 --- a/tools/perf/tests/builtin-test.c +++ b/tools/perf/tests/builtin-test.c @@ -149,6 +149,7 @@ static struct test_suite *generic_tests[] = { &suite__dlfilter, &suite__sigtrap, &suite__event_groups, + &suite__hybrid_merge, &suite__symbols, &suite__util, &suite__subcmd_help, diff --git a/tools/perf/tests/hybrid-merge.c b/tools/perf/tests/hybrid-merge.c new file mode 100644 index 000000000000..426b1d09618e --- /dev/null +++ b/tools/perf/tests/hybrid-merge.c @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include "debug.h" +#include "env.h" +#include "evlist.h" +#include "evsel.h" +#include "tests.h" + +/* A hybrid machine with 2 core PMUs, as read from a perf.data file. */ +static struct hybrid_node two_core_pmus[] = { + { .pmu_name = (char *)"cpu_atom", .cpus = (char *)"0-3", }, + { .pmu_name = (char *)"cpu_core", .cpus = (char *)"4-7", }, +}; + +/* A machine with more than 2 kinds of core, like some Arm big.LITTLE. */ +static struct hybrid_node three_core_pmus[] = { + { .pmu_name = (char *)"cpu_atom", .cpus = (char *)"0-3", }, + { .pmu_name = (char *)"cpu_core", .cpus = (char *)"4-7", }, + { .pmu_name = (char *)"cpu_lowpower", .cpus = (char *)"8", }, +}; + +static struct evsel *test_evsel__new(struct evlist *evlist, const char *name) +{ + struct perf_event_attr attr = { + .type = PERF_TYPE_RAW, + .size = sizeof(attr), + .config = 0x3c, + }; + struct evsel *evsel = evsel__new(&attr); + + if (!evsel) + return NULL; + + evsel->name = strdup(name); + if (!evsel->name) { + evsel__put(evsel); + return NULL; + } + evlist__add(evlist, evsel); + return evsel; +} + +/* + * The assertions are in helpers taking an already allocated evlist, so that the + * caller can release the evlist however an assertion fails. + */ +static int check_merge_events(struct evlist *evlist, struct perf_env *env) +{ + struct evsel *atom_cycles, *core_cycles, *atom_insns, *core_insns, *pos; + + /* As if "perf record -e cycles,instructions" ran on a hybrid machine. */ + atom_cycles = test_evsel__new(evlist, "cpu_atom/cycles/"); + core_cycles = test_evsel__new(evlist, "cpu_core/cycles/"); + atom_insns = test_evsel__new(evlist, "cpu_atom/instructions/"); + core_insns = test_evsel__new(evlist, "cpu_core/instructions/"); + TEST_ASSERT_VAL("failed to allocate evsels", + atom_cycles && core_cycles && atom_insns && core_insns); + + TEST_ASSERT_VAL("events should be mergeable", + evlist__can_merge_hybrid(evlist, env)); + + /* Testing for merging must not alter the evlist. */ + evlist__for_each_entry(evlist, pos) { + TEST_ASSERT_VAL("evlist modified by evlist__can_merge_hybrid", + !pos->first_wildcard_match); + } + + evlist__merge_hybrid(evlist, env); + + TEST_ASSERT_VAL("cycles not merged", + core_cycles->first_wildcard_match == atom_cycles); + /* All the events must be merged, not just the first pair found. */ + TEST_ASSERT_VAL("instructions not merged", + core_insns->first_wildcard_match == atom_insns); + TEST_ASSERT_VAL("wrong cycles leader", + evsel__leader(core_cycles) == atom_cycles); + TEST_ASSERT_VAL("wrong instructions leader", + evsel__leader(core_insns) == atom_insns); + TEST_ASSERT_VAL("wrong cycles group size", atom_cycles->core.nr_members == 2); + TEST_ASSERT_VAL("wrong instructions group size", atom_insns->core.nr_members == 2); + + return TEST_OK; +} + +static int test__hybrid_merge_events(struct test_suite *test __maybe_unused, + int subtest __maybe_unused) +{ + struct perf_env env = { + .nr_hybrid_nodes = ARRAY_SIZE(two_core_pmus), + .hybrid_nodes = two_core_pmus, + }; + struct evlist *evlist = evlist__new(); + int ret; + + TEST_ASSERT_VAL("failed to allocate evlist", evlist); + + ret = check_merge_events(evlist, &env); + evlist__put(evlist); + return ret; +} + +static int check_merge_3_core_pmus(struct evlist *evlist, struct perf_env *env) +{ + struct evsel *atom_cycles, *core_cycles, *lowpower_cycles; + + atom_cycles = test_evsel__new(evlist, "cpu_atom/cycles/"); + core_cycles = test_evsel__new(evlist, "cpu_core/cycles/"); + lowpower_cycles = test_evsel__new(evlist, "cpu_lowpower/cycles/"); + TEST_ASSERT_VAL("failed to allocate evsels", + atom_cycles && core_cycles && lowpower_cycles); + + evlist__merge_hybrid(evlist, env); + + TEST_ASSERT_VAL("second core PMU not merged", + core_cycles->first_wildcard_match == atom_cycles); + TEST_ASSERT_VAL("third core PMU not merged", + lowpower_cycles->first_wildcard_match == atom_cycles); + TEST_ASSERT_VAL("wrong group size", atom_cycles->core.nr_members == 3); + + return TEST_OK; +} + +static int test__hybrid_merge_3_core_pmus(struct test_suite *test __maybe_unused, + int subtest __maybe_unused) +{ + struct perf_env env = { + .nr_hybrid_nodes = ARRAY_SIZE(three_core_pmus), + .hybrid_nodes = three_core_pmus, + }; + struct evlist *evlist = evlist__new(); + int ret; + + TEST_ASSERT_VAL("failed to allocate evlist", evlist); + + ret = check_merge_3_core_pmus(evlist, &env); + evlist__put(evlist); + return ret; +} + +static int check_unmergeable_core_events(struct evlist *evlist, struct perf_env *env) +{ + /* A single event has nothing to merge with. */ + TEST_ASSERT_VAL("failed to allocate evsel", + test_evsel__new(evlist, "cpu_core/cycles/")); + TEST_ASSERT_VAL("a single event shouldn't merge", + !evlist__can_merge_hybrid(evlist, env)); + + /* Events of different names shouldn't merge. */ + TEST_ASSERT_VAL("failed to allocate evsel", + test_evsel__new(evlist, "cpu_atom/instructions/")); + TEST_ASSERT_VAL("events with different names shouldn't merge", + !evlist__can_merge_hybrid(evlist, env)); + + return TEST_OK; +} + +static int check_unmergeable_uncore_events(struct evlist *evlist, struct perf_env *env) +{ + /* Matching events on non-core PMUs shouldn't merge. */ + TEST_ASSERT_VAL("failed to allocate evsels", + test_evsel__new(evlist, "uncore_imc_0/clockticks/") && + test_evsel__new(evlist, "uncore_imc_1/clockticks/")); + TEST_ASSERT_VAL("uncore events shouldn't merge", + !evlist__can_merge_hybrid(evlist, env)); + + return TEST_OK; +} + +static int test__hybrid_merge_unmergeable(struct test_suite *test __maybe_unused, + int subtest __maybe_unused) +{ + struct perf_env env = { + .nr_hybrid_nodes = ARRAY_SIZE(two_core_pmus), + .hybrid_nodes = two_core_pmus, + }; + struct evlist *evlist = evlist__new(); + int ret; + + TEST_ASSERT_VAL("failed to allocate evlist", evlist); + + ret = check_unmergeable_core_events(evlist, &env); + evlist__put(evlist); + if (ret != TEST_OK) + return ret; + + evlist = evlist__new(); + TEST_ASSERT_VAL("failed to allocate evlist", evlist); + + ret = check_unmergeable_uncore_events(evlist, &env); + evlist__put(evlist); + return ret; +} + +static struct test_case tests__hybrid_merge[] = { + TEST_CASE("Merge events of 2 core PMUs", hybrid_merge_events), + TEST_CASE("Merge events of 3 core PMUs", hybrid_merge_3_core_pmus), + TEST_CASE("Events that shouldn't merge", hybrid_merge_unmergeable), + { .name = NULL, } +}; + +struct test_suite suite__hybrid_merge = { + .desc = "Hybrid event merging", + .test_cases = tests__hybrid_merge, +}; diff --git a/tools/perf/tests/shell/report_hybrid_merge.sh b/tools/perf/tests/shell/report_hybrid_merge.sh new file mode 100755 index 000000000000..aca5d16c6c9d --- /dev/null +++ b/tools/perf/tests/shell/report_hybrid_merge.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# perf report hybrid merge tests + +set -e + +err=0 +log_dir=$(mktemp -d /tmp/__perf_test.report_hybrid_merge.XXXXXX) +perf_data="${log_dir}/perf.data" +perf_out="${log_dir}/perf.out" +perf_config="${log_dir}/perfconfig" + +cleanup() { + rm -rf "${log_dir}" + trap - EXIT TERM INT +} + +trap_cleanup() { + echo "Unexpected signal in ${FUNCNAME[1]}" + cleanup + exit 1 +} +trap trap_cleanup EXIT TERM INT + +# Record 2 events so that all the events of a hybrid machine must be merged, +# not just the first pair found. +events="" +record_events() { + for try in "cycles,instructions" "cpu-clock,task-clock"; do + if perf record -o "${perf_data}" -e "${try}" -- \ + perf test -w thloop 1 >/dev/null 2>&1; then + events="${try//,/ }" + return 0 + fi + done + return 1 +} + +test_hybrid_merge_report() { + echo "Perf report hybrid merge test" + + # Test with multiple fields to ensure formatting alignment doesn't hide members + if ! perf report -i "${perf_data}" --hybrid-merge \ + -F comm,overhead --stdio > "${perf_out}" 2>&1; then + echo "Perf report hybrid merge test [Failed: run err]" + err=1 + return + fi + + # Check if the output actually contains the 'comm' and 'overhead' headers correctly + if ! grep -qi "Overhead" "${perf_out}"; then + echo "Perf report hybrid merge test [Failed: missing overhead header]" + err=1 + return + fi + + if ! grep -qi "Command" "${perf_out}"; then + echo "Perf report hybrid merge test [Failed: missing comm header]" + err=1 + return + fi + + # Every recorded event must be displayed, merged into a group on a + # hybrid machine and separately elsewhere. + if ! perf report -i "${perf_data}" --hybrid-merge --stdio \ + > "${perf_out}" 2>&1; then + echo "Perf report hybrid merge test [Failed: run err]" + err=1 + return + fi + for event in ${events}; do + if ! grep -q -- "${event}" "${perf_out}"; then + echo "Perf report hybrid merge test [Failed: missing event ${event}]" + cat "${perf_out}" + err=1 + return + fi + done + + echo "Perf report hybrid merge test [Success]" +} + +test_hybrid_merge_config() { + echo "Perf report hybrid merge config test" + + cat < "${perf_config}" +[core] + hybrid-merge = true +EOF + + # Merging from the config file is a default, it must give way to an + # explicitly requested --hierarchy rather than failing. + if ! PERF_CONFIG="${perf_config}" perf report -i "${perf_data}" \ + --hierarchy --stdio > "${perf_out}" 2>&1; then + echo "Perf report hybrid merge config test [Failed: --hierarchy]" + cat "${perf_out}" + err=1 + return + fi + + # Asking for both on the command line remains an error. + if PERF_CONFIG="${perf_config}" perf report -i "${perf_data}" \ + --hierarchy --hybrid-merge --stdio > "${perf_out}" 2>&1; then + echo "Perf report hybrid merge config test [Failed: no error for both]" + err=1 + return + fi + + echo "Perf report hybrid merge config test [Success]" +} + +if ! record_events; then + echo "Perf report hybrid merge test [Skipped: perf record failed]" + cleanup + exit 2 +fi + +test_hybrid_merge_report +test_hybrid_merge_config +cleanup +exit $err diff --git a/tools/perf/tests/shell/top.sh b/tools/perf/tests/shell/top.sh index ad7fccd09025..3d52677ccb5f 100755 --- a/tools/perf/tests/shell/top.sh +++ b/tools/perf/tests/shell/top.sh @@ -35,21 +35,22 @@ test_basic_perf_top() { # Use -d 1 to avoid flooding output # Use -e cpu-clock to ensure we get samples # Use sleep to keep stdin open but silent, preventing EOF loop or interactive spam - if ! sleep 10 | timeout 5s perf top --stdio -d 1 -e cpu-clock -p $PID > "${log_file}" 2>&1; then - retval=$? - if [ $retval -ne 124 ] && [ $retval -ne 0 ]; then - echo "Basic perf top test [Failed: perf top failed to start or run (ret=$retval)]" - head -n 50 "${log_file}" - kill $PID - wait $PID 2>/dev/null || true - err=1 - return - fi + retval=0 + sleep 10 | timeout 5s perf top --stdio -d 1 -e cpu-clock \ + -p $PID > "${log_file}" 2>&1 || retval=$? + if [ "${retval:-0}" -ne 124 ] && [ "${retval:-0}" -ne 0 ]; then + echo "Basic perf top test [Failed: perf top failed to start or run (ret=$retval)]" + head -n 50 "${log_file}" + kill $PID 2>/dev/null || true + wait $PID 2>/dev/null || true + err=1 + return fi - kill $PID + kill $PID 2>/dev/null || true wait $PID 2>/dev/null || true + # Check for some sample data (percentage) if ! grep -E -q "[0-9]+\.[0-9]+%" "${log_file}"; then echo "Basic perf top test [Failed: no sample percentage found]" @@ -69,6 +70,56 @@ test_basic_perf_top() { echo "Basic perf top test [Success]" } +test_hybrid_merge_perf_top() { + echo "Perf top hybrid merge test" + + perf test -w thloop 20 & + PID=$! + + # Allow it to start + sleep 0.1 + + # Run without explicitly requesting -e cycles so heavily virtualized + # environments can seamlessly fall back to cpu-clock while real + # hybrid hardware will naturally cover the merge logic. + retval=0 + sleep 10 | timeout 5s perf top \ + --stdio --hybrid-merge -d 1 -p $PID > "${log_file}" 2>&1 || retval=$? + if [ "${retval:-0}" -ne 124 ] && [ "${retval:-0}" -ne 0 ]; then + echo "Perf top hybrid merge test [Failed: run err=$retval]" + head -n 50 "${log_file}" + kill $PID 2>/dev/null || true + wait $PID 2>/dev/null || true + err=1 + return + fi + + kill $PID 2>/dev/null || true + wait $PID 2>/dev/null || true + + # Wait a tiny bit for the file system to catch up on the logs + sleep 0.1 + + # Check for some sample data (percentage) + if ! grep -E -q "[0-9]+\.[0-9]+%" "${log_file}"; then + echo "Perf top hybrid merge test [Failed: no sample percentage found]" + head -n 50 "${log_file}" + err=1 + return + fi + + # Check for the test loop symbol to ensure attribution worked + if ! grep -q "test_loop" "${log_file}"; then + echo "Perf top hybrid merge test [Failed: test_loop symbol not found]" + head -n 50 "${log_file}" + err=1 + return + fi + + echo "Perf top hybrid merge test [Success]" +} + test_basic_perf_top +test_hybrid_merge_perf_top cleanup exit $err diff --git a/tools/perf/tests/tests.h b/tools/perf/tests/tests.h index cee9e6b62dcc..9c96f33483d1 100644 --- a/tools/perf/tests/tests.h +++ b/tools/perf/tests/tests.h @@ -177,6 +177,7 @@ DECLARE_SUITE(perf_time_to_tsc); DECLARE_SUITE(dlfilter); DECLARE_SUITE(sigtrap); DECLARE_SUITE(event_groups); +DECLARE_SUITE(hybrid_merge); DECLARE_SUITE(symbols); DECLARE_SUITE(util); DECLARE_SUITE(uncore_event_sorting); From 63394d06643ae1a094bc1af832d9710d7318f1b3 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 22:07:08 -0700 Subject: [PATCH 092/425] perf test: Isolate test suite from user .perfconfig natively Running 'perf test' should not inherit the user's ~/.perfconfig environment as customizing core properties (like core.hybrid-merge) will trivially break stdout matching checks across the shell suite. Explicitly set PERF_CONFIG to /dev/null inside cmd_test to globally sandbox the environment for the entire test workflow. Test specific config, like annotate.objdump, is read before the sandboxing so that 'perf test' still honors it. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/builtin-test.c | 49 +++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-test.c index 6293b37266cf..2d53414d8271 100644 --- a/tools/perf/tests/builtin-test.c +++ b/tools/perf/tests/builtin-test.c @@ -14,6 +14,8 @@ #include #include +#include "util/config.h" + #include #include #include @@ -1655,11 +1657,30 @@ static int run_workload(const char *work, int argc, const char **argv) return -1; } +/* + * Owns the string test_objdump_path points at when it came from the config. It + * is reachable for the lifetime of the process so leak checking won't report + * it. + */ +static char *test_objdump_config_path; + static int perf_test__config(const char *var, const char *value, void *data __maybe_unused) { - if (!strcmp(var, "annotate.objdump")) - test_objdump_path = value; + if (!strcmp(var, "annotate.objdump")) { + /* + * The config, and so value, is freed by perf_config__exit() + * below, take a copy that lives as long as the tests. + */ + char *dup = strdup(value); + + if (!dup) + return -ENOMEM; + + free(test_objdump_config_path); + test_objdump_config_path = dup; + test_objdump_path = dup; + } return 0; } @@ -1755,13 +1776,31 @@ int cmd_test(int argc, const char **argv) }; const char * const test_subcommands[] = { "list", NULL }; struct intlist *skiplist = NULL; - int ret = hists__init(); struct test_suite **suites; + int ret; - if (ret < 0) + ret = hists__init(); + if (ret < 0) return ret; - perf_config(perf_test__config, NULL); + /* Read test related config, like annotate.objdump, before isolating. */ + ret = perf_config(perf_test__config, NULL); + if (ret < 0) + return ret; + + /* + * Isolate the test suite from the user's ~/.perfconfig to ensure + * behavior isn't impacted by custom variables (e.g. core.hybrid-merge). + * Setting PERF_CONFIG isolates the perf commands run by the shell + * tests, config_exclusive_filename does the same for this process. + * Config values already read by main's perf_config are reset here, so + * far only the hybrid-merge value is known to alter test output. + */ + if (setenv("PERF_CONFIG", "/dev/null", 1) < 0) + return -1; + config_exclusive_filename = "/dev/null"; + perf_config__exit(); + symbol_conf.hybrid_merge = false; /* Unbuffered output */ setvbuf(stdout, NULL, _IONBF, 0); From d673d25d3e27a97813b7a8af2870f8fd63f052e5 Mon Sep 17 00:00:00 2001 From: Namhyung Kim Date: Fri, 18 Sep 2026 23:37:42 -0700 Subject: [PATCH 093/425] perf dwarf-aux: Add die_has_flex_array() helper The die_has_flex_array() returns true when the given type is a compound type and contains an array at the end. To prevent an infinite recursion add a depth field to the internal function. Acked-by: Masami Hiramatsu (Google) Signed-off-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/dwarf-aux.c | 107 ++++++++++++++++++++++++++++++++++++ tools/perf/util/dwarf-aux.h | 3 + 2 files changed, 110 insertions(+) diff --git a/tools/perf/util/dwarf-aux.c b/tools/perf/util/dwarf-aux.c index d7160f87ac7d..1560e721479a 100644 --- a/tools/perf/util/dwarf-aux.c +++ b/tools/perf/util/dwarf-aux.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "debug.h" #include "dwarf-aux.h" #include "dwarf-regs.h" @@ -2180,3 +2181,109 @@ Dwarf_Die *die_deref_ptr_type(Dwarf_Die *ptr_die, int offset, return die_get_member_type(&type_die, offset, die_mem); } + +static bool is_flex_array_member(Dwarf_Die *mb_die) +{ + Dwarf_Die type_die; + Dwarf_Word size; + + /* get the type of the member */ + if (die_get_real_type(mb_die, &type_die) == NULL) + return false; + + if (dwarf_tag(&type_die) != DW_TAG_array_type) + return false; + + return dwarf_aggregate_size(&type_die, &size) < 0 || size == 0; +} + +#define MAX_FLEX_ARRAY_RECURSION 256 /* arbitrary */ + +static bool die_has_flex_array_recurse(Dwarf_Die *parent_die, int depth) +{ + Dwarf_Die die_mem, last_mb; + int tag = dwarf_tag(parent_die); + bool found = false; + Dwarf_Word loc, last_loc = 0; + + if (tag != DW_TAG_structure_type && tag != DW_TAG_union_type) + return false; + + /* prevent infinite recursion */ + if (depth > MAX_FLEX_ARRAY_RECURSION) + return false; + + if (dwarf_child(parent_die, &die_mem)) + return false; + + do { + if (dwarf_tag(&die_mem) != DW_TAG_member) + continue; + + if (tag == DW_TAG_union_type) { + if (is_flex_array_member(&die_mem)) + return true; + + if (die_get_real_type(&die_mem, &last_mb) && + die_has_flex_array_recurse(&last_mb, depth + 1)) + return true; + } + + if (tag == DW_TAG_structure_type) { + if (die_get_data_member_location(&die_mem, &loc) < 0) { + /* ignore bitfields */ + loc = 0; + } + + if (last_loc <= loc) { + memcpy(&last_mb, &die_mem, sizeof(last_mb)); + last_loc = loc; + } + } + + found = true; + } while (dwarf_siblingof(&die_mem, &die_mem) == 0); + + if (tag == DW_TAG_structure_type && found) { + if (is_flex_array_member(&last_mb)) + return true; + + if (die_get_real_type(&last_mb, &die_mem)) + return die_has_flex_array_recurse(&die_mem, depth + 1); + } + + return false; +} + +/** + * die_has_flex_array - Check if the given type has a flex-array at the end + * @type_die: a pointer to type DIE + * + * This function returns %true iff @type_die is a struct or union type and has + * an array at the end. Note that the flex-array has no element, it should have + * no size and the parent size doesn't include the flex-array. So it should + * locate at the offset of the parent size. + * + * For simplicity, it assumes the parent size of aligned with the flex-array. + */ +bool die_has_flex_array(Dwarf_Die *type_die) +{ + Dwarf_Die real_type; + + switch (dwarf_tag(type_die)) { + case DW_TAG_typedef: + case DW_TAG_const_type: + case DW_TAG_restrict_type: + case DW_TAG_volatile_type: + case DW_TAG_shared_type: + if (die_get_real_type(type_die, &real_type) == NULL) + return false; + + type_die = &real_type; + break; + default: + break; + } + + return die_has_flex_array_recurse(type_die, 0); +} diff --git a/tools/perf/util/dwarf-aux.h b/tools/perf/util/dwarf-aux.h index 161f0bf980b6..9b662db71022 100644 --- a/tools/perf/util/dwarf-aux.h +++ b/tools/perf/util/dwarf-aux.h @@ -189,4 +189,7 @@ void die_collect_global_vars(Dwarf_Die *cu_die, struct die_var_type **var_types) /* Get the frame base information from CFA */ int die_get_cfa(Dwarf *dwarf, u64 pc, int *preg, int *poffset); +/* Check whether given type has a flex array */ +bool die_has_flex_array(Dwarf_Die *parent_die); + #endif /* _DWARF_AUX_H */ From b827d234695527d7960dd9c825d2aaa43a3b232e Mon Sep 17 00:00:00 2001 From: Namhyung Kim Date: Fri, 18 Sep 2026 23:37:43 -0700 Subject: [PATCH 094/425] perf annotate-data: A small cleanup in __add_member_cb() Check return value of strbuf_init() and die_get_typename() to make sure if typename of a member gets never NULL. Also remove the check for typedef by just using die_get_real_type(). It can get rid of member_type variable as well. Signed-off-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/annotate-data.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tools/perf/util/annotate-data.c b/tools/perf/util/annotate-data.c index aff60a630fd0..845a5d8c2b84 100644 --- a/tools/perf/util/annotate-data.c +++ b/tools/perf/util/annotate-data.c @@ -226,7 +226,7 @@ static int __add_member_cb(Dwarf_Die *die, void *arg) { struct annotated_member *parent = arg; struct annotated_member *member; - Dwarf_Die member_type, die_mem; + Dwarf_Die die_mem; Dwarf_Word size, loc, bit_size = 0; Dwarf_Attribute attr; struct strbuf sb; @@ -239,14 +239,15 @@ static int __add_member_cb(Dwarf_Die *die, void *arg) if (member == NULL) return DIE_FIND_CB_END; - strbuf_init(&sb, 32); - die_get_typename(die, &sb); + if (strbuf_init(&sb, 32) < 0) { + free(member); + return DIE_FIND_CB_END; + } - __die_get_real_type(die, &member_type); - if (dwarf_tag(&member_type) == DW_TAG_typedef) - die_get_real_type(&member_type, &die_mem); - else - die_mem = member_type; + if (die_get_typename(die, &sb) < 0) + strbuf_add(&sb, "(unknown type)", 14); + + die_get_real_type(die, &die_mem); if (dwarf_aggregate_size(&die_mem, &size) < 0) size = 0; From b4ad992f8ff69695109fe9ac656f61a8b7b38742 Mon Sep 17 00:00:00 2001 From: Namhyung Kim Date: Fri, 18 Sep 2026 23:37:44 -0700 Subject: [PATCH 095/425] perf annotate-data: Allow out-of-size access for flex-array types Structs that have a flex array will have accesses beyond its original size as the array was declared as 0 sized. For now, it just allow any offset bigger than the size. It could be refined later. Signed-off-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/annotate-data.c | 63 ++++++++++++++++++--------------- tools/perf/util/annotate-data.h | 2 ++ 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/tools/perf/util/annotate-data.c b/tools/perf/util/annotate-data.c index 845a5d8c2b84..5dd6c6ec2d42 100644 --- a/tools/perf/util/annotate-data.c +++ b/tools/perf/util/annotate-data.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -249,8 +250,15 @@ static int __add_member_cb(Dwarf_Die *die, void *arg) die_get_real_type(die, &die_mem); - if (dwarf_aggregate_size(&die_mem, &size) < 0) - size = 0; + if (dwarf_aggregate_size(&die_mem, &size) < 0 || size == 0) { + if (dwarf_tag(&die_mem) == DW_TAG_array_type) { /* flex-array? */ + die_get_real_type(&die_mem, &die_mem); + if (dwarf_aggregate_size(&die_mem, &size) < 0) + size = 0; + } else { + size = 0; + } + } if (dwarf_attr_integrate(die, DW_AT_data_member_location, &attr)) { if (dwarf_formudata(&attr, &loc) != 0) { @@ -400,6 +408,7 @@ static struct annotated_data_type *dso__findnew_data_type(struct dso *dso, result->self.type_name = type_name; result->self.size = size; INIT_LIST_HEAD(&result->self.children); + result->flex_array = die_has_flex_array(type_die); if (symbol_conf.annotate_data_member) add_member_types(result, type_die); @@ -518,13 +527,30 @@ static bool is_better_type(Dwarf_Die *type_a, Dwarf_Die *type_b) return false; } +static enum type_match_result check_type_offset(Dwarf_Die *type_die, int offset) +{ + Dwarf_Word size; + + /* Get the size of the actual type */ + if (dwarf_aggregate_size(type_die, &size) < 0) + return PERF_TMR_NO_SIZE; + + /* Minimal sanity check */ + if (offset < 0) + return PERF_TMR_BAD_OFFSET; + + if ((unsigned)offset >= size && !die_has_flex_array(type_die)) + return PERF_TMR_BAD_OFFSET; + + return PERF_TMR_OK; +} + /* The type info will be saved in @type_die */ static enum type_match_result check_variable(struct data_loc_info *dloc, Dwarf_Die *var_die, Dwarf_Die *type_die, int reg, int offset, bool is_fbreg) { - Dwarf_Word size; bool needs_pointer = true; Dwarf_Die sized_type; @@ -555,15 +581,7 @@ static enum type_match_result check_variable(struct data_loc_info *dloc, else sized_type = *type_die; - /* Get the size of the actual type */ - if (dwarf_aggregate_size(&sized_type, &size) < 0) - return PERF_TMR_NO_SIZE; - - /* Minimal sanity check */ - if ((unsigned)offset >= size) - return PERF_TMR_BAD_OFFSET; - - return PERF_TMR_OK; + return check_type_offset(&sized_type, offset); } struct type_state_stack *find_stack_state(struct type_state *state, @@ -1113,7 +1131,6 @@ static enum type_match_result check_matching_type(struct type_state *state, struct disasm_line *dl, Dwarf_Die *type_die) { - Dwarf_Word size; u32 insn_offset = dl->al.offset; int reg = dloc->op->reg1; int offset = dloc->op->offset; @@ -1167,12 +1184,7 @@ static enum type_match_result check_matching_type(struct type_state *state, else sized_type = *type_die; - /* Get the size of the actual type */ - if (dwarf_aggregate_size(&sized_type, &size) < 0 || - (unsigned)dloc->type_offset >= size) - return PERF_TMR_BAD_OFFSET; - - return PERF_TMR_OK; + return check_type_offset(&sized_type, dloc->type_offset); } if (state->regs[reg].kind == TSR_KIND_POINTER) { @@ -1191,12 +1203,7 @@ static enum type_match_result check_matching_type(struct type_state *state, dloc->type_offset = dloc->op->offset + state->regs[reg].offset; - /* Get the size of the actual type */ - if (dwarf_aggregate_size(type_die, &size) < 0 || - (unsigned)dloc->type_offset >= size) - return PERF_TMR_BAD_OFFSET; - - return PERF_TMR_OK; + return check_type_offset(type_die, dloc->type_offset); } if (state->regs[reg].kind == TSR_KIND_PERCPU_POINTER) { @@ -1210,9 +1217,7 @@ static enum type_match_result check_matching_type(struct type_state *state, dloc->type_offset = dloc->op->offset; - /* Get the size of the actual type */ - if (dwarf_aggregate_size(type_die, &size) < 0 || - (unsigned)dloc->type_offset >= size) + if (check_type_offset(type_die, dloc->type_offset) != PERF_TMR_OK) return PERF_TMR_BAIL_OUT; return PERF_TMR_OK; @@ -1840,7 +1845,7 @@ int annotated_data_type__update_samples(struct annotated_data_type *adt, return -1; } - if (offset < 0 || offset >= adt->self.size) + if (offset < 0 || (offset >= adt->self.size && !adt->flex_array)) return -1; h = &adt->histograms[evsel->core.idx]; diff --git a/tools/perf/util/annotate-data.h b/tools/perf/util/annotate-data.h index ca2096a9ee62..957726334907 100644 --- a/tools/perf/util/annotate-data.h +++ b/tools/perf/util/annotate-data.h @@ -85,6 +85,7 @@ struct type_hist { * struct annotated_data_type - Data type to profile * @node: RB-tree node for dso->type_tree * @self: Actual type information + * @flex_array: Whether it has a flex array * @nr_histogram: Number of histogram entries * @histograms: An array of histograms * @@ -93,6 +94,7 @@ struct type_hist { struct annotated_data_type { struct rb_node node; struct annotated_member self; + bool flex_array; int nr_histograms; struct type_hist *histograms; }; From aaca646915a1bb531c745b5689d7a9e258f5f87b Mon Sep 17 00:00:00 2001 From: Namhyung Kim Date: Fri, 18 Sep 2026 23:37:45 -0700 Subject: [PATCH 096/425] perf annotate-data: Adjust type offset for flex-array The flex array members are located beyond the original type size. Also it needs to adjust the offset in an array to find a corresponding element using module operation. Note that we focus on access to type and field, so array index is not important. Make sure to find a field name for flex arrays. Signed-off-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/annotate-data.c | 114 +++++++++++++++++++++++++++++--- tools/perf/util/annotate-data.h | 4 ++ 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/tools/perf/util/annotate-data.c b/tools/perf/util/annotate-data.c index 5dd6c6ec2d42..e9b58a3d587b 100644 --- a/tools/perf/util/annotate-data.c +++ b/tools/perf/util/annotate-data.c @@ -226,7 +226,7 @@ static bool data_type_less(struct rb_node *node_a, const struct rb_node *node_b) static int __add_member_cb(Dwarf_Die *die, void *arg) { struct annotated_member *parent = arg; - struct annotated_member *member; + struct annotated_member *member, *prev; Dwarf_Die die_mem; Dwarf_Word size, loc, bit_size = 0; Dwarf_Attribute attr; @@ -253,6 +253,7 @@ static int __add_member_cb(Dwarf_Die *die, void *arg) if (dwarf_aggregate_size(&die_mem, &size) < 0 || size == 0) { if (dwarf_tag(&die_mem) == DW_TAG_array_type) { /* flex-array? */ die_get_real_type(&die_mem, &die_mem); + member->is_flex_array = true; if (dwarf_aggregate_size(&die_mem, &size) < 0) size = 0; } else { @@ -299,12 +300,19 @@ static int __add_member_cb(Dwarf_Die *die, void *arg) member->size = size; member->offset = loc + parent->offset; INIT_LIST_HEAD(&member->children); - list_add_tail(&member->node, &parent->children); + + list_for_each_entry_reverse(prev, &parent->children, node) { + if (prev->offset <= member->offset) + break; + } + list_add(&member->node, &prev->node); tag = dwarf_tag(&die_mem); switch (tag) { - case DW_TAG_structure_type: case DW_TAG_union_type: + member->is_union = true; + /* fall through */ + case DW_TAG_structure_type: die_find_child(&die_mem, __add_member_cb, member, &die_mem); break; default: @@ -333,20 +341,84 @@ static void delete_members(struct annotated_member *member) } } -static int fill_member_name(char *buf, size_t sz, struct annotated_member *m, - int offset, bool first) +static struct annotated_member *find_flex_array(struct annotated_member *m) { struct annotated_member *child; + if (list_empty(&m->children)) + return NULL; + + if (m->is_union) { + list_for_each_entry(child, &m->children, node) { + if (child->is_flex_array) + return child; + } + list_for_each_entry(child, &m->children, node) { + struct annotated_member *grand_child; + + grand_child = find_flex_array(child); + if (grand_child) + return grand_child; + } + return NULL; + } + + child = list_last_entry(&m->children, struct annotated_member, node); + if (child->is_flex_array) + return child; + + return find_flex_array(child); +} + +static struct annotated_member *get_flex_array_member(struct annotated_data_type *adt) +{ + return find_flex_array(&adt->self); +} + +static int fill_member_name(char *buf, size_t sz, struct annotated_member *m, + int offset, bool first, bool has_flex_array) +{ + struct annotated_member *child; + bool found = false; + int len; + if (list_empty(&m->children)) return 0; list_for_each_entry(child, &m->children, node) { - int len; - if (offset < child->offset || offset >= child->offset + child->size) continue; + found = true; + break; + } + + if (!found && has_flex_array) { + /* + * It may have an intermediate struct that has another struct that + * contains a flex array. In that case, the outer struct itself is + * has no array and the size is less than the offset so the above + * logic won't find the outer struct at the offset. + */ + child = find_flex_array(m); + if (child == NULL || offset < child->offset) + return 0; + + /* find the immediate child that includes a flex array */ + if (m->is_union) { + list_for_each_entry(child, &m->children, node) { + if (child->is_flex_array || find_flex_array(child)) { + found = true; + break; + } + } + } else { + child = list_last_entry(&m->children, struct annotated_member, node); + found = true; + } + } + + if (found) { /* It can have anonymous struct/union members */ if (child->var_name) { len = scnprintf(buf, sz, "%s%s", @@ -356,15 +428,18 @@ static int fill_member_name(char *buf, size_t sz, struct annotated_member *m, len = 0; } - return fill_member_name(buf + len, sz - len, child, offset, first) + len; + return fill_member_name(buf + len, sz - len, child, offset, first, + has_flex_array) + len; } + return 0; } int annotated_data_type__get_member_name(struct annotated_data_type *adt, char *buf, size_t sz, int member_offset) { - return fill_member_name(buf, sz, &adt->self, member_offset, /*first=*/true); + return fill_member_name(buf, sz, &adt->self, member_offset, /*first=*/true, + adt->flex_array); } static struct annotated_data_type *dso__findnew_data_type(struct dso *dso, @@ -1741,6 +1816,7 @@ struct annotated_data_type *find_data_type(struct data_loc_info *dloc) { struct dso *dso = map__dso(dloc->ms->map); Dwarf_Die type_die; + struct annotated_data_type *result; /* * The type offset is the same as instruction offset by default. @@ -1753,7 +1829,25 @@ struct annotated_data_type *find_data_type(struct data_loc_info *dloc) if (find_data_type_die(dloc, &type_die) < 0) return NULL; - return dso__findnew_data_type(dso, &type_die); + result = dso__findnew_data_type(dso, &type_die); + if (result == NULL) + return NULL; + + if (result->flex_array && dloc->type_offset > result->self.size) { + struct annotated_member *flex_array = get_flex_array_member(result); + + if (flex_array && flex_array->size > 0) { + int offset = dloc->type_offset; + + /* adjust offset in the flex array */ + offset -= flex_array->offset; + offset %= flex_array->size; + offset += flex_array->offset; + + dloc->type_offset = offset; + } + } + return result; } static size_t data_type_hash(long key, void *ctx __maybe_unused) diff --git a/tools/perf/util/annotate-data.h b/tools/perf/util/annotate-data.h index 957726334907..14b8113521a9 100644 --- a/tools/perf/util/annotate-data.h +++ b/tools/perf/util/annotate-data.h @@ -47,6 +47,8 @@ enum type_state_kind { * @var_name: Name of the member variable * @offset: Offset from the outer data type * @size: Size of the member field + * @is_union: Whether it's an union type + * @is_flex_array: Whether it's a flex array * * This represents a member type in a data type. */ @@ -57,6 +59,8 @@ struct annotated_member { char *var_name; int offset; int size; + bool is_union; + bool is_flex_array; }; /** From 268d5c3979251c31305dc8e8afeeaaa296f2e252 Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:15 +0000 Subject: [PATCH 097/425] perf capstone: Symbolize address operands to match objdump on arm64 Capstone currently outputs address-bearing instructions with a raw hex value prefixed by '#', without symbolic resolution. This differs from objdump output and leads to parse failures in jump__parse() and arm64_mov__parse() on arm64. Example of the mismatch: Current: b #0xffff8000800114c8 Objdump: b ffff8000800114c8 Current: adrp x18, #0xffff800081f5f000 Objdump: adrp x18, ffff800081f5f000 Fix this by extending symbol__disassemble_capstone() to: - Enable CS_OPT_DETAIL for arm64 to access instruction group info. - Detect instructions with address operands requiring symbolization (jump/call/branch/adr/adrp) via opcode or Capstone group ID. - Resolve target addresses to symbols via find_symbol_from_addr(). - Format output to match objdump's style (strip '#' prefix, add symbol and offset). Signed-off-by: Tengda Wu Reviewed-by: Namhyung Kim Reviewed-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/capstone.c | 186 ++++++++++++++++++++++++++++++------- tools/perf/util/disasm.c | 5 + tools/perf/util/disasm.h | 1 + 3 files changed, 157 insertions(+), 35 deletions(-) diff --git a/tools/perf/util/capstone.c b/tools/perf/util/capstone.c index 74213daf8786..95881f15324a 100644 --- a/tools/perf/util/capstone.c +++ b/tools/perf/util/capstone.c @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -31,6 +32,10 @@ #define CS_MODE_RISCVC 4 #endif +#if CS_VERSION_MAJOR < 4 +#define ARM64_GRP_BRANCH_RELATIVE 7 /* = CS_GRP_BRANCH_RELATIVE */ +#endif + #ifdef LIBCAPSTONE_DLOPEN static void *perf_cs_dll_handle(void) { @@ -225,6 +230,12 @@ static int capstone_init(uint16_t e_machine, csh *cs_handle, bool is64, bool is_ * on x86 by investigating instruction details. */ perf_cs_option(*cs_handle, CS_OPT_DETAIL, CS_OPT_ON); + } else if (arch == CS_ARCH_ARM64) { + /* + * Same as x86: arm64 needs instruction details to resolve + * symbolic addresses. + */ + perf_cs_option(*cs_handle, CS_OPT_DETAIL, CS_OPT_ON); } return 0; @@ -292,6 +303,40 @@ ssize_t capstone__fprintf_insn_asm(struct machine *machine, struct thread *threa return printed; } +static struct symbol *find_symbol_from_addr(struct map *map, u64 addr, + struct map **found_map, u64 *sym_offset) +{ + struct symbol *sym; + u64 al_addr; + u64 ip = map__objdump_2mem(map, addr); + + *found_map = NULL; + if (dso__kernel(map__dso(map))) { + /* + * The kernel maps can be split into sections, let's + * find the map first and then search the symbol. + */ + *found_map = maps__find(map__kmaps(map), ip); + if (*found_map == NULL) + return NULL; + map = *found_map; + } + + /* convert it to map-relative address for search */ + al_addr = map__map_ip(map, ip); + + sym = map__find_symbol(map, al_addr); + + if (sym == NULL) { + map__put(*found_map); + *found_map = NULL; + return NULL; + } + + *sym_offset = al_addr - sym->start; + return sym; +} + static void print_capstone_detail(struct cs_insn *insn, char *buf, size_t len, struct annotate_args *args, u64 addr) { @@ -299,17 +344,14 @@ static void print_capstone_detail(struct cs_insn *insn, char *buf, size_t len, struct map *map = args->ms->map; struct symbol *sym; - /* TODO: support more architectures */ - if (!arch__is_x86(args->arch)) - return; - if (insn->detail == NULL) return; for (i = 0; i < insn->detail->x86.op_count; i++) { struct cs_x86_op *op = &insn->detail->x86.operands[i]; u64 orig_addr; - struct map *found_map = NULL; + struct map *found_map; + u64 sym_offset; if (op->type != X86_OP_MEM) continue; @@ -320,40 +362,119 @@ static void print_capstone_detail(struct cs_insn *insn, char *buf, size_t len, /* get the target address */ orig_addr = addr + insn->size + op->mem.disp; - addr = map__objdump_2mem(map, orig_addr); - - if (dso__kernel(map__dso(map))) { - /* - * The kernel maps can be split into sections, let's - * find the map first and then search the symbol. - */ - found_map = maps__find(map__kmaps(map), addr); - if (found_map == NULL) - continue; - map = found_map; - } - - /* convert it to map-relative address for search */ - addr = map__map_ip(map, addr); - - sym = map__find_symbol(map, addr); - if (sym == NULL) { - map__put(found_map); + sym = find_symbol_from_addr(map, orig_addr, &found_map, &sym_offset); + if (sym == NULL) continue; - } - if (addr == sym->start) { + if (sym_offset == 0) { scnprintf(buf, len, "\t# %"PRIx64" <%s>", orig_addr, sym->name); } else { scnprintf(buf, len, "\t# %"PRIx64" <%s+%#"PRIx64">", - orig_addr, sym->name, addr - sym->start); + orig_addr, sym->name, sym_offset); } map__put(found_map); break; } } +static int print_default_format(struct cs_insn *insn, char *buf, size_t len) +{ + return scnprintf(buf, len, " %-7s %s", + insn->mnemonic, insn->op_str); +} + +static void format_capstone_insn_x86(struct cs_insn *insn, char *buf, + size_t len, struct annotate_args *args, + u64 addr) +{ + int printed; + + printed = print_default_format(insn, buf, len); + buf += printed; + len -= printed; + + print_capstone_detail(insn, buf, len, args, addr); +} + +static bool needs_symbolic_address(struct cs_insn *insn) +{ + int i; + + if (insn->id == ARM64_INS_ADR || insn->id == ARM64_INS_ADRP) + return true; + + if (insn->detail == NULL) + return false; + + for (i = 0; i < insn->detail->groups_count; i++) { + if (insn->detail->groups[i] == ARM64_GRP_JUMP || + insn->detail->groups[i] == ARM64_GRP_CALL || + insn->detail->groups[i] == ARM64_GRP_BRANCH_RELATIVE) + return true; + } + + return false; +} + +static void format_capstone_insn_arm64(struct cs_insn *insn, char *buf, + size_t len, struct annotate_args *args) +{ + struct map *map = args->ms->map; + char *last_imm, *endptr; + u64 addr; + + print_default_format(insn, buf, len); + /* + * Adjust instructions to keep the existing behavior with objdump. + * + * Example conversion: + * From: b #0xffff8000800114c8 + * To: b ffff8000800114c8 + */ + if (needs_symbolic_address(insn)) { + struct symbol *sym; + struct map *found_map; + u64 sym_offset; + + /* Extract last immediate value as address */ + last_imm = strrchr(buf, '#'); + if (!last_imm) + return; + + addr = strtoull(last_imm + 1, &endptr, 16); + if (endptr == last_imm + 1) + return; + + sym = find_symbol_from_addr(map, addr, &found_map, &sym_offset); + if (sym == NULL) + return; + + /* Symbolize the resolved address */ + len = len - (last_imm - buf); + if (sym_offset == 0) { + scnprintf(last_imm, len, "%"PRIx64" <%s>", + addr, sym->name); + } else { + scnprintf(last_imm, len, "%"PRIx64" <%s+%#"PRIx64">", + addr, sym->name, sym_offset); + } + map__put(found_map); + } +} + +static void format_capstone_insn(struct cs_insn *insn, char *buf, size_t len, + struct annotate_args *args, u64 addr) +{ + /* TODO: support more architectures */ + if (arch__is_x86(args->arch)) + format_capstone_insn_x86(insn, buf, len, args, addr); + else if (arch__is_arm64(args->arch)) + format_capstone_insn_arm64(insn, buf, len, args); + else + print_default_format(insn, buf, len); +} + struct find_file_offset_data { u64 ip; u64 offset; @@ -446,14 +567,9 @@ int symbol__disassemble_capstone(const char *filename, struct symbol *sym, free_count = count = perf_cs_disasm(handle, buf, buf_len, start, buf_len, &insn); for (i = 0, offset = 0; i < count; i++) { - int printed; - - printed = scnprintf(disasm_buf, sizeof(disasm_buf), - " %-7s %s", - insn[i].mnemonic, insn[i].op_str); - print_capstone_detail(&insn[i], disasm_buf + printed, - sizeof(disasm_buf) - printed, args, - start + offset); + format_capstone_insn(&insn[i], disasm_buf, + sizeof(disasm_buf), args, + start + offset); args->offset = offset; args->line = disasm_buf; diff --git a/tools/perf/util/disasm.c b/tools/perf/util/disasm.c index 49c206a88eec..b39636ec35c1 100644 --- a/tools/perf/util/disasm.c +++ b/tools/perf/util/disasm.c @@ -214,6 +214,11 @@ bool arch__is_powerpc(const struct arch *arch) return arch->id.e_machine == EM_PPC || arch->id.e_machine == EM_PPC64; } +bool arch__is_arm64(const struct arch *arch) +{ + return arch->id.e_machine == EM_AARCH64; +} + static void ins_ops__delete(struct ins_operands *ops) { if (ops == NULL) diff --git a/tools/perf/util/disasm.h b/tools/perf/util/disasm.h index adbdbf8f1f35..3a42166f7f49 100644 --- a/tools/perf/util/disasm.h +++ b/tools/perf/util/disasm.h @@ -111,6 +111,7 @@ struct annotate_args { const struct arch *arch__find(uint16_t e_machine, uint32_t e_flags, const char *cpuid); bool arch__is_x86(const struct arch *arch); bool arch__is_powerpc(const struct arch *arch); +bool arch__is_arm64(const struct arch *arch); extern const struct ins_ops call_ops; extern const struct ins_ops dec_ops; From 705390bdcf625d7cb573c6d001569b68be0d9755 Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:16 +0000 Subject: [PATCH 098/425] perf llvm: Fix arm64 adrp instruction disassembly mismatch with objdump The operands of 'adrp' instructions parsed by libllvm are currently represented as raw immediates rather than the "address " format used by objdump. This inconsistency causes arm64_mov__parse() to fail when parsing these instructions during post-processing. Example of the mismatch: Current: adrp x18, 8014 Objdump: adrp x18, ffff800081f5f000 Fix this by manually extracting the target address from the raw adrp instruction via symbol_lookup_callback(). The address is then converted to a specific symbol during symbol__disassemble_llvm() and formatted to match objdump's output, ensuring compatibility with existing parsers. Reviewed-by: Namhyung Kim Signed-off-by: Tengda Wu Reviewed-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/llvm.c | 52 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/tools/perf/util/llvm.c b/tools/perf/util/llvm.c index a0deb742a733..a78845689aa1 100644 --- a/tools/perf/util/llvm.c +++ b/tools/perf/util/llvm.c @@ -9,8 +9,10 @@ #include "symbol.h" #include #include +#include #include #include +#include #ifdef HAVE_LIBLLVM_SUPPORT #include "llvm-c-helpers.h" @@ -108,6 +110,16 @@ symbol_lookup_callback(void *disinfo, uint64_t value, storage->branch_addr = value; else if (*ref_type == LLVMDisassembler_ReferenceType_In_PCrel_Load) storage->pcrel_load_addr = value; + else if (*ref_type == LLVMDisassembler_ReferenceType_In_ARM64_ADRP) { + uint64_t adrp_imm; + + /* immhi (bits 23:5) and immlo (bits 30:29) */ + adrp_imm = ((value & 0x00ffffe0) >> 3) | ((value >> 29) & 0x3); + adrp_imm = sign_extend64(adrp_imm, 20); + + /* Calculate the target page address */ + storage->pcrel_load_addr = (address & ~0xfffULL) + (adrp_imm << 12); + } *ref_type = LLVMDisassembler_ReferenceType_InOut_None; return NULL; } @@ -228,12 +240,42 @@ int symbol__disassemble_llvm(const char *filename, struct symbol *sym, } } if (storage.pcrel_load_addr != 0) { - char *name = llvm_name_for_data(dso, filename, + char *name; + + if (arch__is_arm64(args->arch)) { + /* + * For arm64, replace the immediate operand with + * the resolved address to match objdump's output. + * + * Example conversion: + * From: adrp x18, 8014 + * To: adrp x18, ffff800081f5f000 + */ + char *s = strchr(disasm_buf, ','); + + if (s == NULL) + goto err; + + s++; + *s = '\0'; + disasm_len = strlen(disasm_buf); + disasm_len += scnprintf(disasm_buf + disasm_len, + sizeof(disasm_buf) - disasm_len, + " %"PRIx64, storage.pcrel_load_addr); - disasm_len += scnprintf(disasm_buf + disasm_len, - sizeof(disasm_buf) - disasm_len, - " # %#"PRIx64, - storage.pcrel_load_addr); + } else { + /* + * For other archs, append the resolved address + * as an inline comment. + */ + disasm_len += scnprintf(disasm_buf + disasm_len, + sizeof(disasm_buf) - disasm_len, + " # %#"PRIx64, + storage.pcrel_load_addr); + } + + /* Append the resolved symbol name if available */ + name = llvm_name_for_data(dso, filename, storage.pcrel_load_addr); if (name) { disasm_len += scnprintf(disasm_buf + disasm_len, sizeof(disasm_buf) - From 699c52c69f2a10a6b3b9465883d9e5a285cd7b6f Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:17 +0000 Subject: [PATCH 099/425] perf annotate-arm64: Generalize arm64_mov__parse to support more instructions As the default parser for arm64 instructions, arm64_mov__parse() currently only supports parsing instructions with the 'addr ' suffix. Other instructions are not supported, causing a lack of source and target information for most regular instructions. Generalize arm64_mov__parse to extend its parsing capabilities: 1. Parse 'ops->target.raw' and 'ops->source.raw' by default. 2. Utilize arm64__check_multi_regs() to identify if source and target contain multiple registers. 3. Maintain backward compatibility for instructions containing the 'addr ' suffix. Reviewed-by: Namhyung Kim Signed-off-by: Tengda Wu Reviewed-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- .../perf/util/annotate-arch/annotate-arm64.c | 162 ++++++++++++++---- 1 file changed, 132 insertions(+), 30 deletions(-) diff --git a/tools/perf/util/annotate-arch/annotate-arm64.c b/tools/perf/util/annotate-arch/annotate-arm64.c index 33080fdca125..151184210691 100644 --- a/tools/perf/util/annotate-arch/annotate-arm64.c +++ b/tools/perf/util/annotate-arch/annotate-arm64.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include "../annotate.h" @@ -14,59 +16,158 @@ struct arch_arm64 { regex_t jump_insn; }; -static int arm64_mov__parse(const struct arch *arch __maybe_unused, +static bool arm64__is_reg(const char *op) +{ + if (!op || !*op) + return false; + + /* + * General-purpose registers: x0-x30, w0-w30. + * Check for 'x' or 'w' prefix followed by a numeric index. + */ + if ((op[0] == 'x' || op[0] == 'w') && isdigit(op[1])) + return true; + + /* Special-purpose registers: sp, wzr, xzr. */ + if (!strncmp(op, "sp", 2) || !strncmp(op, "xzr", 3) || + !strncmp(op, "wzr", 3)) + return true; + + /* TODO: Support more registers. */ + return false; +} + +static bool arm64__check_multi_regs(const struct arch *arch, const char *op) +{ + const char *p = op; + int reg_count = 0; + + while (p && *p) { + p = skip_spaces(p); + if (*p == arch->objdump.memory_ref_char) + p++; + + if (arm64__is_reg(p)) + reg_count++; + + if (reg_count >= 2) + return true; + + /* Move to next operand after comma */ + p = strchr(p, ','); + if (p) + p++; + } + + return false; +} + +/* + * Duplicate @insn, stripping the comment and trailing whitespace. + * Returns a newly allocated string which the caller must free(), + * or NULL on allocation failure or if @insn is NULL. + */ +static char *rstrip_space_and_comment(const char *insn, char comment_char) +{ + const char *end, *comment; + size_t len; + char *result; + + if (insn == NULL) + return NULL; + + comment = strchr(insn, comment_char); + if (comment != NULL) + end = comment; + else + end = insn + strlen(insn); + + while (end > insn && isspace(end[-1])) + --end; + + len = end - insn; + result = malloc(len + 1); + if (result == NULL) + return NULL; + + memcpy(result, insn, len); + result[len] = '\0'; + + return result; +} + +static int arm64_mov__parse(const struct arch *arch, struct ins_operands *ops, struct map_symbol *ms __maybe_unused, struct disasm_line *dl __maybe_unused) { - char *s = strchr(ops->raw, ','), *target, *endptr; + char *s = strchr(ops->raw, ','), *source, *endptr; if (s == NULL) return -1; + /* Parse target */ *s = '\0'; - ops->source.raw = strdup(ops->raw); + ops->target.raw = strdup(ops->raw); *s = ','; - if (ops->source.raw == NULL) + if (ops->target.raw == NULL) return -1; - target = ++s; - ops->target.raw = strdup(target); - if (ops->target.raw == NULL) - goto out_free_source; + ops->target.multi_regs = arm64__check_multi_regs(arch, ops->target.raw); - ops->target.addr = strtoull(target, &endptr, 16); - if (endptr == target) - goto out_free_target; + /* Parse source, stripping comment if present */ + source = skip_spaces(++s); + ops->source.raw = rstrip_space_and_comment(source, arch->objdump.comment_char); + if (ops->source.raw == NULL) { + zfree(&ops->target.raw); + return -1; + } - s = strchr(endptr, '<'); - if (s == NULL) - goto out_free_target; - endptr = strchr(s + 1, '>'); - if (endptr == NULL) - goto out_free_target; + ops->source.multi_regs = arm64__check_multi_regs(arch, ops->source.raw); - *endptr = '\0'; - *s = ' '; - ops->target.name = strdup(s); - *s = '<'; - *endptr = '>'; - if (ops->target.name == NULL) - goto out_free_target; + /* + * Parse 'addr ' from source (if any). + * + * A raw hex string may be a scalar immediate, not an address. + * Only validate 'source.addr' if accompanied by a '' tag, + * otherwise reset it to 0 to avoid false positive address tracking. + */ + ops->source.addr = strtoull(ops->source.raw, &endptr, 16); + if (endptr != ops->source.raw) { + s = strchr(endptr, '<'); + if (s == NULL) { + ops->source.addr = 0; + return 0; + } + endptr = strrchr(s + 1, '>'); + if (endptr == NULL) { + ops->source.addr = 0; + return 0; + } + + *endptr = '\0'; + ops->source.name = strdup(s + 1); + *endptr = '>'; + if (ops->source.name == NULL) { + ops->source.addr = 0; + return 0; + } + } return 0; +} -out_free_target: - zfree(&ops->target.raw); -out_free_source: - zfree(&ops->source.raw); - return -1; +static int arm64_mov__scnprintf(const struct ins *ins, char *bf, size_t size, + struct ins_operands *ops, int max_ins_name) +{ + return scnprintf(bf, size, "%-*s %s, %s", max_ins_name, ins->name, + ops->target.raw, ops->source.name ?: ops->source.raw); } static const struct ins_ops arm64_mov_ops = { .parse = arm64_mov__parse, - .scnprintf = mov__scnprintf, + .scnprintf = arm64_mov__scnprintf, }; static const struct ins_ops *arm64__associate_instruction_ops(struct arch *arch, const char *name) @@ -103,6 +204,7 @@ const struct arch *arch__new_arm64(const struct e_machine_and_e_flags *id, arch->id = *id; arch->objdump.comment_char = '/'; arch->objdump.skip_functions_char = '+'; + arch->objdump.memory_ref_char = '['; arch->associate_instruction_ops = arm64__associate_instruction_ops; /* bl, blr */ From d9700ad94a9f38a9997386ae349c9de947c87c4f Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:18 +0000 Subject: [PATCH 100/425] perf annotate-arm64: Handle load and store instructions Add ldst_ops to handle load and store instructions in order to parse the data types and offsets associated with PMU events for memory access instructions. There are many variants of load and store instructions in arm64, making it difficult to match all of these instruction names completely. Therefore, only the instruction prefixes are matched. The prefix 'ld|st' covers most of the memory access instructions, 'cas|swp' matches atomic instructions, and 'prf' matches memory prefetch instructions. Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Signed-off-by: Li Huafei Signed-off-by: Tengda Wu Signed-off-by: Arnaldo Carvalho de Melo --- .../perf/util/annotate-arch/annotate-arm64.c | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/tools/perf/util/annotate-arch/annotate-arm64.c b/tools/perf/util/annotate-arch/annotate-arm64.c index 151184210691..a7595a954cfa 100644 --- a/tools/perf/util/annotate-arch/annotate-arm64.c +++ b/tools/perf/util/annotate-arch/annotate-arm64.c @@ -14,6 +14,7 @@ struct arch_arm64 { struct arch arch; regex_t call_insn; regex_t jump_insn; + regex_t ldst_insn; /* load and store instruction */ }; static bool arm64__is_reg(const char *op) @@ -170,6 +171,130 @@ static const struct ins_ops arm64_mov_ops = { .scnprintf = arm64_mov__scnprintf, }; +static bool arm64__insn_is_target_on_right(const char *ins_name) +{ + /* + * Store instructions write to the memory operand on the right, + * unlike standard syntax where the target is the left operand. + */ + return !strncmp(ins_name, "st", 2); +} + +/* + * This function is used to parse arm64 load/store instructions into + * instruction operands. + * + * Typical instructions and their parsing logic: + * + * 1. Immediate offset: + * ldr x2, [x0] -> target="x2", source="[x0]" + * ldr x2, [x0, #24] -> target="x2", source="[x0, #24]" + * ldp x19, x20, [sp, #16] -> target="x19, x20", source="[sp, #16]" + * + * 2. Pre-index addressing: + * stp x29, x30, [sp, #-64]! -> target="[sp, #-64]!", source="x29, x30" + * + * 3. Post-index addressing: + * str x1, [x0], #8 -> target="[x0], #8", source="x1" + * ldr w1, [x21], #4 -> target="w1", source="[x21], #4" + * ldp x29, x30, [sp], #32 -> target="x29, x30", source="[sp], #32" + * + * 4. Register offset / extension: + * ldr x0, [x1, w0, sxtw #3] -> target="x0", source="[x1, w0, sxtw #3]" + * ldr x0, [x1, x0, lsl #3] -> target="x0", source="[x1, x0, lsl #3]" + * + * 5. Atomic operations: + * cas w3, w1, [x0] -> target="w3, w1", source="[x0]" + * swp x3, x0, [x2] -> target="x3, x0", source="[x2]" + * + * 6. Prefetch memory: + * prfm pstl1strm, [x4] -> target="pstl1strm", source="[x4]" + * + * 7. PC-relative loads (No bracket found): + * ldr x0, ffff800080f40c68 <__kvm_nvhe_$d> -> Fallback to default parser + * + * Parsing strategy: + * Use the '[' bracket as the boundary to split the operands into left + * and right sides. For non-store instructions, the left side is the + * target and the right side is the source. For store instructions, the + * roles are reversed. + */ +static int arm64_ldst__parse(const struct arch *arch, struct ins_operands *ops, + struct map_symbol *ms, struct disasm_line *dl) +{ + char *raw, *s, *left, *right; + int ret = -1; + + raw = rstrip_space_and_comment(ops->raw, arch->objdump.comment_char); + if (!raw) + return -1; + + s = strchr(raw, arch->objdump.memory_ref_char); + if (!s) { + /* Fallback to default parser for PC-relative loads. */ + free(raw); + return arm64_mov__parse(arch, ops, ms, dl); + } + + right = strdup(s); + if (!right) + goto out_free_raw; + + while (s > raw && *s != ',') + --s; + + if (s == raw) + goto out_free_right; + + *s = '\0'; + left = strdup(raw); + *s = ','; + if (!left) + goto out_free_right; + + free(raw); + + if (arm64__insn_is_target_on_right(dl->ins.name)) { + ops->source.raw = left; + ops->source.mem_ref = false; + + ops->target.raw = right; + ops->target.mem_ref = true; + } else { + ops->source.raw = right; + ops->source.mem_ref = true; + + ops->target.raw = left; + ops->target.mem_ref = false; + } + + ops->source.multi_regs = arm64__check_multi_regs(arch, ops->source.raw); + ops->target.multi_regs = arm64__check_multi_regs(arch, ops->target.raw); + + return 0; + +out_free_right: + free(right); +out_free_raw: + free(raw); + return ret; +} + +static int arm64_ldst__scnprintf(const struct ins *ins, char *bf, size_t size, + struct ins_operands *ops, int max_ins_name) +{ + if (arm64__insn_is_target_on_right(ins->name)) + return scnprintf(bf, size, "%-*s %s", max_ins_name, ins->name, ops->raw); + + return scnprintf(bf, size, "%-*s %s, %s", max_ins_name, ins->name, + ops->target.raw, ops->source.name ?: ops->source.raw); +} + +static struct ins_ops arm64_ldst_ops = { + .parse = arm64_ldst__parse, + .scnprintf = arm64_ldst__scnprintf, +}; + static const struct ins_ops *arm64__associate_instruction_ops(struct arch *arch, const char *name) { struct arch_arm64 *arm = container_of(arch, struct arch_arm64, arch); @@ -180,6 +305,8 @@ static const struct ins_ops *arm64__associate_instruction_ops(struct arch *arch, ops = &jump_ops; else if (!regexec(&arm->call_insn, name, 2, match, 0)) ops = &call_ops; + else if (!regexec(&arm->ldst_insn, name, 2, match, 0)) + ops = &arm64_ldst_ops; else if (!strcmp(name, "ret")) ops = &ret_ops; else @@ -205,6 +332,7 @@ const struct arch *arch__new_arm64(const struct e_machine_and_e_flags *id, arch->objdump.comment_char = '/'; arch->objdump.skip_functions_char = '+'; arch->objdump.memory_ref_char = '['; + arch->objdump.imm_char = '#'; arch->associate_instruction_ops = arm64__associate_instruction_ops; /* bl, blr */ @@ -218,8 +346,20 @@ const struct arch *arch__new_arm64(const struct e_machine_and_e_flags *id, if (err) goto out_free_call; + /* + * The ARM64 architecture has many variants of load/store instructions. + * It is quite challenging to match all of them completely. Here, we + * only match the prefixes of these instructions. + */ + err = regcomp(&arm->ldst_insn, "^(ld|st|cas|prf|swp)", + REG_EXTENDED); + if (err) + goto out_free_jump; + return arch; +out_free_jump: + regfree(&arm->jump_insn); out_free_call: regfree(&arm->call_insn); out_free_arm: From 988b4e46980b3d80548dae4b748a8d144f01352c Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:19 +0000 Subject: [PATCH 101/425] perf annotate: Normalize arch__dwarf_regnum() error return values arch__dwarf_regnum() returns inconsistent error values: early path returns -1 while the main lookup returns -ENOENT/-EINVAL from get_dwarf_regnum(). Callers only check for -1, so genuine failures can slip through. Normalize all failures to -1 to match the callers' expectation. Suggested-by: Shuai Xue Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Signed-off-by: Tengda Wu Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/annotate.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/util/annotate.c b/tools/perf/util/annotate.c index fd789eb53e74..b0f8a8d37333 100644 --- a/tools/perf/util/annotate.c +++ b/tools/perf/util/annotate.c @@ -2600,7 +2600,7 @@ static int arch__dwarf_regnum(const struct arch *arch, const char *str) reg = get_dwarf_regnum(regname, arch->id.e_machine, arch->id.e_flags); free(regname); - return reg; + return reg < 0 ? -1 : reg; } /* From 487e428bb30eb1311887f05ece5b2c6beceb42ea Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:20 +0000 Subject: [PATCH 102/425] perf annotate: Introduce extract_op_location callback for arch-specific parsing Assembly syntax for operands varies significantly across different architectures, which prevents the operand location (op_loc) parsing logic in annotate_get_insn_location() from being directly reused. To simplify the core logic and improve maintainability, move the operand parsing inside the for_each_insn_op_loc loop into arch-specific extract_op_location callbacks. This refactoring is intended to be a cleanup with no functional changes. Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Signed-off-by: Li Huafei Signed-off-by: Tengda Wu Signed-off-by: Arnaldo Carvalho de Melo --- .../util/annotate-arch/annotate-powerpc.c | 9 ++ tools/perf/util/annotate-arch/annotate-x86.c | 79 ++++++++++++++++ tools/perf/util/annotate.c | 94 +------------------ tools/perf/util/annotate.h | 2 + tools/perf/util/disasm.h | 4 + 5 files changed, 99 insertions(+), 89 deletions(-) diff --git a/tools/perf/util/annotate-arch/annotate-powerpc.c b/tools/perf/util/annotate-arch/annotate-powerpc.c index 218207b52581..00b93296fc94 100644 --- a/tools/perf/util/annotate-arch/annotate-powerpc.c +++ b/tools/perf/util/annotate-arch/annotate-powerpc.c @@ -390,6 +390,14 @@ static void update_insn_state_powerpc(struct type_state *state, } #endif /* HAVE_LIBDW_SUPPORT */ +static void extract_op_location_powerpc(const struct arch *arch __maybe_unused, + struct disasm_line *dl, + const char *op_str __maybe_unused, int op_idx, + struct annotated_op_loc *op_loc) +{ + get_powerpc_regs(dl->raw.raw_insn, !op_idx, op_loc); +} + const struct arch *arch__new_powerpc(const struct e_machine_and_e_flags *id, const char *cpuid __maybe_unused) { @@ -406,5 +414,6 @@ const struct arch *arch__new_powerpc(const struct e_machine_and_e_flags *id, #ifdef HAVE_LIBDW_SUPPORT arch->update_insn_state = update_insn_state_powerpc; #endif + arch->extract_op_location = extract_op_location_powerpc; return arch; } diff --git a/tools/perf/util/annotate-arch/annotate-x86.c b/tools/perf/util/annotate-arch/annotate-x86.c index 7e6136536393..1acf31a2c759 100644 --- a/tools/perf/util/annotate-arch/annotate-x86.c +++ b/tools/perf/util/annotate-arch/annotate-x86.c @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0 #include +#include #include +#include #include #include #include @@ -809,6 +811,82 @@ static void update_insn_state_x86(struct type_state *state, } #endif +/* + * Get register number and access offset from the given instruction. + * It assumes AT&T x86 asm format like OFFSET(REG). + * Fills @reg and @offset when return 0. + */ +static int extract_reg_offset(const struct arch *arch, const char *str, + struct annotated_op_loc *op_loc) +{ + char *p; + + if (arch->objdump.register_char == 0) + return -1; + + /* + * It should start from offset, but it's possible to skip 0 + * in the asm. So 0(%rax) should be same as (%rax). + * + * However, it also start with a segment select register like + * %gs:0x18(%rbx). In that case it should skip the part. + */ + if (*str == arch->objdump.register_char) { + /* FIXME: Handle other segment registers */ + if (!strncmp(str, "%gs:", 4)) + op_loc->segment = INSN_SEG_X86_GS; + + while (*str && !isdigit(*str) && + *str != arch->objdump.memory_ref_char) + str++; + } + + op_loc->offset = strtol(str, &p, 0); + op_loc->reg1 = arch__dwarf_regnum(arch, p); + if (op_loc->reg1 == -1) + return -1; + + /* Get the second register */ + if (op_loc->multi_regs) + op_loc->reg2 = arch__dwarf_regnum(arch, p + 1); + + return 0; +} + +static void extract_op_location_x86(const struct arch *arch, + struct disasm_line *dl __maybe_unused, + const char *op_str, int op_idx __maybe_unused, + struct annotated_op_loc *op_loc) +{ + if (op_str == NULL) + return; + + if (strchr(op_str, arch->objdump.memory_ref_char)) { + op_loc->mem_ref = true; + extract_reg_offset(arch, op_str, op_loc); + } else { + const char *s = op_str; + char *p = NULL; + + /* FIXME: Handle other segment registers */ + if (!strncmp(op_str, "%gs:", 4)) { + op_loc->segment = INSN_SEG_X86_GS; + op_loc->offset = strtol(op_str + 4, &p, 0); + if (p && p != op_str + 4) + op_loc->imm = true; + return; + } + + if (*s == arch->objdump.register_char) { + op_loc->reg1 = arch__dwarf_regnum(arch, s); + } else if (*s == arch->objdump.imm_char) { + op_loc->offset = strtol(s + 1, &p, 0); + if (p && p != s + 1) + op_loc->imm = true; + } + } +} + const struct arch *arch__new_x86(const struct e_machine_and_e_flags *id, const char *cpuid) { struct arch *arch = zalloc(sizeof(*arch)); @@ -848,5 +926,6 @@ const struct arch *arch__new_x86(const struct e_machine_and_e_flags *id, const c #ifdef HAVE_LIBDW_SUPPORT arch->update_insn_state = update_insn_state_x86; #endif + arch->extract_op_location = extract_op_location_x86; return arch; } diff --git a/tools/perf/util/annotate.c b/tools/perf/util/annotate.c index b0f8a8d37333..d3482eba9283 100644 --- a/tools/perf/util/annotate.c +++ b/tools/perf/util/annotate.c @@ -2580,7 +2580,7 @@ int annotate_check_args(void) return 0; } -static int arch__dwarf_regnum(const struct arch *arch, const char *str) +int arch__dwarf_regnum(const struct arch *arch, const char *str) { const char *p; char *regname, *q; @@ -2603,51 +2603,6 @@ static int arch__dwarf_regnum(const struct arch *arch, const char *str) return reg < 0 ? -1 : reg; } -/* - * Get register number and access offset from the given instruction. - * It assumes AT&T x86 asm format like OFFSET(REG). Maybe it needs - * to revisit the format when it handles different architecture. - * Fills @reg and @offset when return 0. - */ -static int extract_reg_offset(const struct arch *arch, const char *str, - struct annotated_op_loc *op_loc) -{ - char *p; - - if (arch->objdump.register_char == 0) - return -1; - - /* - * It should start from offset, but it's possible to skip 0 - * in the asm. So 0(%rax) should be same as (%rax). - * - * However, it also start with a segment select register like - * %gs:0x18(%rbx). In that case it should skip the part. - */ - if (*str == arch->objdump.register_char) { - if (arch__is_x86(arch)) { - /* FIXME: Handle other segment registers */ - if (!strncmp(str, "%gs:", 4)) - op_loc->segment = INSN_SEG_X86_GS; - } - - while (*str && !isdigit(*str) && - *str != arch->objdump.memory_ref_char) - str++; - } - - op_loc->offset = strtol(str, &p, 0); - op_loc->reg1 = arch__dwarf_regnum(arch, p); - if (op_loc->reg1 == -1) - return -1; - - /* Get the second register */ - if (op_loc->multi_regs) - op_loc->reg2 = arch__dwarf_regnum(arch, p + 1); - - return 0; -} - /** * annotate_get_insn_location - Get location of instruction * @arch: the architecture info @@ -2703,50 +2658,11 @@ int annotate_get_insn_location(const struct arch *arch, struct disasm_line *dl, /* Invalidate the register by default */ op_loc->reg1 = -1; op_loc->reg2 = -1; + op_loc->mem_ref = mem_ref; + op_loc->multi_regs = multi_regs; - if (insn_str == NULL) { - if (!arch__is_powerpc(arch)) - continue; - } - - /* - * For powerpc, call get_powerpc_regs function which extracts the - * required fields for op_loc, ie reg1, reg2, offset from the - * raw instruction. - */ - if (arch__is_powerpc(arch)) { - op_loc->mem_ref = mem_ref; - op_loc->multi_regs = multi_regs; - get_powerpc_regs(dl->raw.raw_insn, !i, op_loc); - } else if (strchr(insn_str, arch->objdump.memory_ref_char)) { - op_loc->mem_ref = true; - op_loc->multi_regs = multi_regs; - extract_reg_offset(arch, insn_str, op_loc); - } else { - const char *s = insn_str; - char *p = NULL; - - if (arch__is_x86(arch)) { - /* FIXME: Handle other segment registers */ - if (!strncmp(insn_str, "%gs:", 4)) { - op_loc->segment = INSN_SEG_X86_GS; - op_loc->offset = strtol(insn_str + 4, - &p, 0); - if (p && p != insn_str + 4) - op_loc->imm = true; - continue; - } - } - - if (*s == arch->objdump.register_char) { - op_loc->reg1 = arch__dwarf_regnum(arch, s); - } - else if (*s == arch->objdump.imm_char) { - op_loc->offset = strtol(s + 1, &p, 0); - if (p && p != s + 1) - op_loc->imm = true; - } - } + if (arch->extract_op_location) + arch->extract_op_location(arch, dl, insn_str, i, op_loc); } return 0; diff --git a/tools/perf/util/annotate.h b/tools/perf/util/annotate.h index 4c49d41e73a4..6cd3ef537564 100644 --- a/tools/perf/util/annotate.h +++ b/tools/perf/util/annotate.h @@ -524,6 +524,8 @@ int annotate_parse_percent_type(const struct option *opt, const char *_str, int annotate_check_args(void); +int arch__dwarf_regnum(const struct arch *arch, const char *str); + /** * struct annotated_op_loc - Location info of instruction operand * @reg1: First register in the operand diff --git a/tools/perf/util/disasm.h b/tools/perf/util/disasm.h index 3a42166f7f49..7a1c889f2c50 100644 --- a/tools/perf/util/disasm.h +++ b/tools/perf/util/disasm.h @@ -16,6 +16,7 @@ struct symbol; struct data_loc_info; struct type_state; struct disasm_line; +struct annotated_op_loc; struct e_machine_and_e_flags { uint32_t e_flags; @@ -49,6 +50,9 @@ struct arch { struct data_loc_info *dloc, Dwarf_Die *cu_die, struct disasm_line *dl); #endif + void (*extract_op_location)(const struct arch *arch, struct disasm_line *dl, + const char *op_str, int op_idx, + struct annotated_op_loc *op_loc); }; struct ins { From 4c42173d98cbbb53430a8d28a7823afc9e7b8b71 Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:21 +0000 Subject: [PATCH 103/425] perf dwarf-regs: Adapt get_dwarf_regnum() for arm64 The current arm64 DWARF register lookup relies on 'aarch64_regstr_tbl', a static string table. While this works for kprobe-tracer where register names start with '%', it is insufficient for parsing register numbers directly from raw instructions (e.g., extracting '6' from 'x6' or 'w6') during annotation. Since get_dwarf_regnum() is currently used only by 'perf annotate' and does not affect kprobe-tracer, replace the limited table-based lookup with a programmatic implementation in __get_dwarf_regnum_arm64(). This allows resolving arm64 register names (x0-x30, w0-w30, sp, etc.) directly into their corresponding DWARF register numbers. Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Signed-off-by: Tengda Wu Signed-off-by: Arnaldo Carvalho de Melo --- .../util/dwarf-regs-arch/dwarf-regs-arm64.c | 25 +++++++++++++++++++ tools/perf/util/dwarf-regs.c | 2 +- tools/perf/util/include/dwarf-regs.h | 1 + 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tools/perf/util/dwarf-regs-arch/dwarf-regs-arm64.c b/tools/perf/util/dwarf-regs-arch/dwarf-regs-arm64.c index 593ca7d4fccc..425c8aa2e984 100644 --- a/tools/perf/util/dwarf-regs-arch/dwarf-regs-arm64.c +++ b/tools/perf/util/dwarf-regs-arch/dwarf-regs-arm64.c @@ -1,5 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 #include +#include +#include #include #include "../../../arch/arm64/include/uapi/asm/perf_regs.h" @@ -10,3 +12,26 @@ int __get_dwarf_regnum_for_perf_regnum_arm64(int perf_regnum) return perf_regnum; } + +int __get_dwarf_regnum_arm64(const char *name) +{ + int reg; + + if (!strcmp(name, "sp")) + return 31; + + if (*name != 'x' && *name != 'w') + return -ENOENT; + + name++; + if (!isdigit(name[0])) + return -ENOENT; + else if (isdigit(name[1]) && name[2] == '\0') + reg = (name[0] - '0') * 10 + name[1] - '0'; + else if (name[1] == '\0') + reg = name[0] - '0'; + else + return -ENOENT; + + return reg >= 0 && reg <= 30 ? reg : -ENOENT; +} diff --git a/tools/perf/util/dwarf-regs.c b/tools/perf/util/dwarf-regs.c index 67f38342247e..09aca6304002 100644 --- a/tools/perf/util/dwarf-regs.c +++ b/tools/perf/util/dwarf-regs.c @@ -114,7 +114,7 @@ int get_dwarf_regnum(const char *name, unsigned int machine, unsigned int flags) reg = _get_dwarf_regnum(arm_regstr_tbl, name); break; case EM_AARCH64: - reg = _get_dwarf_regnum(aarch64_regstr_tbl, name); + reg = __get_dwarf_regnum_arm64(name); break; case EM_CSKY: reg = __get_csky_regnum(name, flags); diff --git a/tools/perf/util/include/dwarf-regs.h b/tools/perf/util/include/dwarf-regs.h index 92cf0af93e9e..1da915c2683a 100644 --- a/tools/perf/util/include/dwarf-regs.h +++ b/tools/perf/util/include/dwarf-regs.h @@ -105,6 +105,7 @@ int __get_dwarf_regnum_x86_64(const char *name); int __get_dwarf_regnum_for_perf_regnum_i386(int perf_regnum); int __get_dwarf_regnum_for_perf_regnum_x86_64(int perf_regnum, int abi); +int __get_dwarf_regnum_arm64(const char *name); int __get_dwarf_regnum_for_perf_regnum_arm(int perf_regnum); int __get_dwarf_regnum_for_perf_regnum_arm64(int perf_regnum); From 0ba32f84fcd17a7a92ea1ff251fb2245ff29180a Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:22 +0000 Subject: [PATCH 104/425] perf annotate: Adapt arch__dwarf_regnum() for arm64 Currently, arch__dwarf_regnum() assumes that all architectures use a register prefix character (e.g., '%' for x86) defined by arch->objdump.register_char, and uses it to match register names in objdump output. However, this assumption does not hold for arm64, where assembly syntax uses bare register names like 'x0', 'w1' without any prefix. As a result, arm64 builds may fail to correctly recognize register names from objdump disassembly, leading to incomplete or incorrect annotation output. To address this: - Make the register prefix check optional, allowing architectures without a prefix character to be parsed correctly. - Extend the delimiter set in strpbrk() to include the closing square bracket ']'. In arm64 assembly, memory operands often use bracketed syntax such as '[x1, #16]' or '[x2]'. Adding ']' ensures clean extraction of register names like 'x2' without trailing characters. Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Signed-off-by: Tengda Wu Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/annotate.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/perf/util/annotate.c b/tools/perf/util/annotate.c index d3482eba9283..86a4beec6528 100644 --- a/tools/perf/util/annotate.c +++ b/tools/perf/util/annotate.c @@ -2582,19 +2582,21 @@ int annotate_check_args(void) int arch__dwarf_regnum(const struct arch *arch, const char *str) { - const char *p; + const char *p = str; char *regname, *q; int reg; - p = strchr(str, arch->objdump.register_char); - if (p == NULL) - return -1; + if (arch->objdump.register_char) { + p = strchr(str, arch->objdump.register_char); + if (p == NULL) + return -1; + } regname = strdup(p); if (regname == NULL) return -1; - q = strpbrk(regname, ",) "); + q = strpbrk(regname, ",)] "); if (q) *q = '\0'; From 20aca193502c08d11a4a8ef94c110cb20761b5fb Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:23 +0000 Subject: [PATCH 105/425] perf annotate-arm64: Implement extract_op_location() callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the extract_op_location() callback for the arm64 architecture to handle its specific assembly syntax and addressing modes. The extractor handles: 1. Immediate operands (e.g., '#0x10' or '#0xc600, lsl #16'). 2. Memory references with diverse addressing modes: - Signed offset: [base{, #imm|reg}] - Pre-index: [base, #imm]! - Post-index: [base], #imm|reg 3. Multi-register operands and primary/secondary register extraction. 4. Register extension specifiers (e.g., uxtw, sxtx) and shift amounts for shifted/extended operands. With this callback in place, 'perf annotate' can now resolve memory locations and register types on arm64, marking the initial enablement of data type profiling for this architecture. Commiter notes: Added a cast to overcome a compiler error about setting a non cost char * var to strchr(), the var is not modified. util/annotate-arch/annotate-arm64.c: In function ‘extract_op_location_arm64’: util/annotate-arch/annotate-arm64.c:385:19: error: assignment discards ‘const’ qualifier from pointer target type [-Werror=discarded-qualifiers] 385 | p = strchr(s, ']'); | ^ cc1: all warnings being treated as errors Reviewed-by: Ian Rogers Acked-by: Namhyung Kim Signed-off-by: Tengda Wu Signed-off-by: Arnaldo Carvalho de Melo --- .../perf/util/annotate-arch/annotate-arm64.c | 133 ++++++++++++++++++ tools/perf/util/annotate.h | 61 ++++++++ 2 files changed, 194 insertions(+) diff --git a/tools/perf/util/annotate-arch/annotate-arm64.c b/tools/perf/util/annotate-arch/annotate-arm64.c index a7595a954cfa..6549535961a9 100644 --- a/tools/perf/util/annotate-arch/annotate-arm64.c +++ b/tools/perf/util/annotate-arch/annotate-arm64.c @@ -316,6 +316,138 @@ static const struct ins_ops *arm64__associate_instruction_ops(struct arch *arch, return ops; } +static enum annotated_ext_type arm64__check_ext_type(const char *op) +{ + if (!strncmp(op, "uxtb", 4)) return PERF_EXT_UXTB; + if (!strncmp(op, "uxth", 4)) return PERF_EXT_UXTH; + if (!strncmp(op, "uxtw", 4)) return PERF_EXT_UXTW; + if (!strncmp(op, "uxtx", 4)) return PERF_EXT_UXTX; + if (!strncmp(op, "sxtb", 4)) return PERF_EXT_SXTB; + if (!strncmp(op, "sxth", 4)) return PERF_EXT_SXTH; + if (!strncmp(op, "sxtw", 4)) return PERF_EXT_SXTW; + if (!strncmp(op, "sxtx", 4)) return PERF_EXT_SXTX; + + return PERF_EXT_NONE; +} + +static enum annotated_shift_type arm64__check_shift_type(const char *op) +{ + if (!strncmp(op, "lsl", 3)) return PERF_SHIFT_LSL; + if (!strncmp(op, "lsr", 3)) return PERF_SHIFT_LSR; + if (!strncmp(op, "asr", 3)) return PERF_SHIFT_ASR; + if (!strncmp(op, "ror", 3)) return PERF_SHIFT_ROR; + + return PERF_SHIFT_NONE; +} + +static const char *next_operand_field(const char *s) +{ + s = strchr(s, ','); + return s ? skip_spaces(s + 1) : NULL; +} + +static void extract_op_location_arm64(const struct arch *arch, + struct disasm_line *dl __maybe_unused, + const char *op_str, int op_idx __maybe_unused, + struct annotated_op_loc *op_loc) +{ + const char *s = op_str; + char *p = NULL; + + if (op_str == NULL) + return; + + /* + * Handle immediate operand. + * e.g., "#0xc600" -> offset = 0xc600, imm = true + * + * After parsing, check for post-modifiers like "lsl #16", which + * typically occur in instructions such as movk, movz, movn, etc. + */ + if (*s == arch->objdump.imm_char) { + op_loc->offset = strtol(s + 1, &p, 0); + if (p && p != s + 1) { + op_loc->imm = true; + s = p; + } + s = next_operand_field(s); + goto check_modifiers; + } + + /* + * Handle memory references, identify arm64 specific addressing modes. + * Reference: Arm Architecture Reference Manual + * (DDI 0487), Chapter C1.3.3: Load/store addressing modes. + */ + if (*s == arch->objdump.memory_ref_char) { + op_loc->mem_ref = true; + + p = (char *)strchr(s, ']'); + if (p == NULL) + return; + + /* Pre-index: [base, #imm]! */ + if (p[1] == '!') + op_loc->addr_mode = PERF_AAM_PRE_INDEX; + /* Post-index: [base], #imm|reg */ + else if (p[1] == ',' && + (strchr(p + 1, arch->objdump.imm_char) || + arm64__is_reg(skip_spaces(p + 2)))) + op_loc->addr_mode = PERF_AAM_POST_INDEX; + /* Signed offset: [base{, #imm|reg}] */ + else + op_loc->addr_mode = PERF_AAM_SIGNED_OFFSET; + + s++; + } + + /* Extract the primary register */ + op_loc->reg1 = arch__dwarf_regnum(arch, s); + if (op_loc->reg1 == -1) + return; + + s = next_operand_field(s); + if (s == NULL) + return; + + /* Extract secondary register or immediate offset */ + if (op_loc->multi_regs) { + op_loc->reg2 = arch__dwarf_regnum(arch, s); + s = next_operand_field(s); + } else if (*s == arch->objdump.imm_char) { + op_loc->offset = strtol(s + 1, NULL, 0); + s = next_operand_field(s); + } + +check_modifiers: + /* + * Look for a following shift or extension modifier: + * "lsl #3" -> extend_type = PERF_EXT_NONE, + * shift_type = PERF_SHIFT_LSL, amount = 3 + * "uxtw #3" -> extend_type = PERF_EXT_UXTW, + * shift_type = PERF_SHIFT_LSL, amount = 3 + * "uxtw" -> extend_type = PERF_EXT_UXTW, + * shift_type = PERF_SHIFT_LSL, amount = 0 + */ + if (s == NULL) + return; + + op_loc->extend_type = arm64__check_ext_type(s); + op_loc->shift_type = arm64__check_shift_type(s); + /* ARM64 extended operands are implicitly shifted by LSL. */ + if (op_loc->extend_type != PERF_EXT_NONE) + op_loc->shift_type = PERF_SHIFT_LSL; + + /* Parse shift amount if present */ + op_loc->amount = 0; + if (op_loc->extend_type != PERF_EXT_NONE || + op_loc->shift_type != PERF_SHIFT_NONE) { + s = strchr(s, arch->objdump.imm_char); + if (s) + op_loc->amount = (u8)strtol(s + 1, NULL, 0); + } +} + const struct arch *arch__new_arm64(const struct e_machine_and_e_flags *id, const char *cpuid __maybe_unused) { @@ -334,6 +466,7 @@ const struct arch *arch__new_arm64(const struct e_machine_and_e_flags *id, arch->objdump.memory_ref_char = '['; arch->objdump.imm_char = '#'; arch->associate_instruction_ops = arm64__associate_instruction_ops; + arch->extract_op_location = extract_op_location_arm64; /* bl, blr */ err = regcomp(&arm->call_insn, "^blr?$", REG_EXTENDED); diff --git a/tools/perf/util/annotate.h b/tools/perf/util/annotate.h index 6cd3ef537564..c528fbef8b38 100644 --- a/tools/perf/util/annotate.h +++ b/tools/perf/util/annotate.h @@ -532,6 +532,10 @@ int arch__dwarf_regnum(const struct arch *arch, const char *str); * @reg2: Second register in the operand * @offset: Memory access offset in the operand * @segment: Segment selector register + * @addr_mode: Addressing mode, only valid if @mem_ref is true + * @extend_type: Operand extension type (enum annotated_ext_type) + * @shift_type: Operand shift type (enum annotated_shift_type) + * @amount: Number of bits to shift * @mem_ref: Whether the operand accesses memory * @multi_regs: Whether the second register is used * @imm: Whether the operand is an immediate value (in offset) @@ -541,6 +545,10 @@ struct annotated_op_loc { int reg2; int offset; u8 segment; + u8 addr_mode; + u8 extend_type; + u8 shift_type; + u8 amount; bool mem_ref; bool multi_regs; bool imm; @@ -564,6 +572,59 @@ enum annotated_x86_segment { INSN_SEG_X86_SS, }; +/* + * ARM64 addressing modes for memory operations. + * + * [Xn, #imm] -> SIGNED_OFFSET (base + offset, base unchanged) + * [Xn, #imm]! -> PRE_INDEX (base += offset, then access) + * [Xn], #imm -> POST_INDEX (access, then base += offset) + */ +enum annotated_addr_mode { + PERF_AAM_NONE = 0, + + PERF_AAM_SIGNED_OFFSET, + PERF_AAM_PRE_INDEX, + PERF_AAM_POST_INDEX, +}; + +/* + * ARM64 register extension types. + * UXT* = zero-extend, SXT* = sign-extend. + * B=8bit, H=16bit, W=32bit, X=64bit. + * + * Example: UXTW = zero-extend 32-bit Wn to 64-bit Xn + */ +enum annotated_ext_type { + PERF_EXT_NONE = 0, + + PERF_EXT_UXTB, + PERF_EXT_UXTH, + PERF_EXT_UXTW, + PERF_EXT_UXTX, + PERF_EXT_SXTB, + PERF_EXT_SXTH, + PERF_EXT_SXTW, + PERF_EXT_SXTX, +}; + +/* + * ARM64 operand shift types. + * LSL = logical shift left. + * LSR = logical shift right. + * ASR = arithmetic shift right. + * ROR = rotate right. + * + * Example: LSL #3 = shift the operand left by 3 bits. + */ +enum annotated_shift_type { + PERF_SHIFT_NONE = 0, + + PERF_SHIFT_LSL, + PERF_SHIFT_LSR, + PERF_SHIFT_ASR, + PERF_SHIFT_ROR, +}; + /** * struct annotated_insn_loc - Location info of instruction * @ops: Array of location info for source and target operands From 035efae7df32bc2800bbe525b28f5be5dbcaf701 Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:24 +0000 Subject: [PATCH 106/425] perf annotate: Default to --itrace=i1i for data type profiling ARM SPE-based sampling can synthesize multiple events on a single instruction, as shown below. This would affect data type profiling statistics: Available samples 0 arm_spe_0/ts_enable=1,pa_enable=1,load_filter=1,store_filter=1,min_latency=30/ 0 dummy:u 84K l1d-miss 95K l1d-access 77K llc-miss 58K llc-access 9K tlb-miss 108K tlb-access 0 branch 13K remote-access 108K memory 108K instructions While 'perf report' provides an interactive menu for users to select a specific event to prevent duplicate counting, 'perf annotate' lacks such a mechanism. Consequently, it counts all instructions across these overlapping events, which inflates the profile and distorts the data type statistics. Although using the '--itrace' option can work around this issue (e.g.: perf annotate --data-type --stdio --itrace=i1i), it is inconvenient for users to specify this explicitly every time. To address this, introduce itrace_synth_opts.default_single_event_per_ip. Set this field to true when data type profiling is enabled and the user has not explicitly specified an itrace option. In itrace_synth_opts__set_default(), use this flag to default to synthesizing at most one event per IP (equivalent to --itrace=i1i) to prevent duplicate sample counts. Reviewed-by: Ian Rogers Reviewed-by: Adrian Hunter Acked-by: Namhyung Kim Signed-off-by: Tengda Wu Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-annotate.c | 8 ++++++++ tools/perf/util/arm-spe.c | 5 ++++- tools/perf/util/auxtrace.c | 12 ++++++++++-- tools/perf/util/auxtrace.h | 7 ++++++- tools/perf/util/cs-etm.c | 2 +- tools/perf/util/intel-bts.c | 2 +- tools/perf/util/intel-pt.c | 3 ++- 7 files changed, 32 insertions(+), 7 deletions(-) diff --git a/tools/perf/builtin-annotate.c b/tools/perf/builtin-annotate.c index 850fc72fa75f..8863f3a570f6 100644 --- a/tools/perf/builtin-annotate.c +++ b/tools/perf/builtin-annotate.c @@ -885,6 +885,14 @@ int cmd_annotate(int argc, const char **argv) annotate.session = perf_session__new(&data, &annotate.tool); if (IS_ERR(annotate.session)) return PTR_ERR(annotate.session); + /* + * Hardware tracing (e.g., ARM SPE) may synthesize multiple events per + * instruction. When data type profiling is enabled, default to synthesizing + * at most one event (equivalent to --itrace=i1i) to prevent skewed + * statistics. + */ + if (annotate.data_type && !itrace_synth_opts.set) + itrace_synth_opts.default_single_event_per_ip = true; annotate.session->itrace_synth_opts = &itrace_synth_opts; diff --git a/tools/perf/util/arm-spe.c b/tools/perf/util/arm-spe.c index 401aab529309..ae7a5e9c3892 100644 --- a/tools/perf/util/arm-spe.c +++ b/tools/perf/util/arm-spe.c @@ -2029,7 +2029,10 @@ int arm_spe_process_auxtrace_info(union perf_event *event, if (session->itrace_synth_opts && session->itrace_synth_opts->set) { spe->synth_opts = *session->itrace_synth_opts; } else { - itrace_synth_opts__set_default(&spe->synth_opts, false); + struct itrace_synth_opts *opts = session->itrace_synth_opts; + bool single_event_per_ip = opts ? opts->default_single_event_per_ip : false; + + itrace_synth_opts__set_default(&spe->synth_opts, false, single_event_per_ip); /* Default nanoseconds period not supported */ spe->synth_opts.period_type = PERF_ITRACE_PERIOD_INSTRUCTIONS; spe->synth_opts.period = 1; diff --git a/tools/perf/util/auxtrace.c b/tools/perf/util/auxtrace.c index aa749e1c3036..ed2113190785 100644 --- a/tools/perf/util/auxtrace.c +++ b/tools/perf/util/auxtrace.c @@ -1478,8 +1478,15 @@ s64 perf_event__process_auxtrace(const struct perf_tool *tool __maybe_unused, #define PERF_ITRACE_MAX_LAST_BRANCH_SZ 1024 void itrace_synth_opts__set_default(struct itrace_synth_opts *synth_opts, - bool no_sample) + bool no_sample, bool single_event_per_ip) { + if (single_event_per_ip) { + synth_opts->instructions = true; + synth_opts->period_type = PERF_ITRACE_PERIOD_INSTRUCTIONS; + synth_opts->period = 1; + return; + } + synth_opts->branches = true; synth_opts->transactions = true; synth_opts->ptwrites = true; @@ -1582,7 +1589,8 @@ int itrace_do_parse_synth_opts(struct itrace_synth_opts *synth_opts, if (!str) { itrace_synth_opts__set_default(synth_opts, - synth_opts->default_no_sample); + synth_opts->default_no_sample, + synth_opts->default_single_event_per_ip); return 0; } diff --git a/tools/perf/util/auxtrace.h b/tools/perf/util/auxtrace.h index 6947f3f284c0..5012bead1b92 100644 --- a/tools/perf/util/auxtrace.h +++ b/tools/perf/util/auxtrace.h @@ -65,6 +65,10 @@ enum itrace_period_type { * struct itrace_synth_opts - AUX area tracing synthesis options. * @set: indicates whether or not options have been set * @default_no_sample: Default to no sampling. + * @default_single_event_per_ip: Default to synthesizing at most 1 event per IP + * (equivalent to --itrace=i1i). Used by commands + * like 'perf annotate --data-type' to avoid duplicate + * sample counts for a single instruction. * @inject: indicates the event (not just the sample) must be fully synthesized * because 'perf inject' will write it out * @instructions: whether to synthesize 'instructions' events @@ -117,6 +121,7 @@ enum itrace_period_type { struct itrace_synth_opts { bool set; bool default_no_sample; + bool default_single_event_per_ip; bool inject; bool instructions; bool cycles; @@ -622,7 +627,7 @@ int itrace_do_parse_synth_opts(struct itrace_synth_opts *synth_opts, int itrace_parse_synth_opts(const struct option *opt, const char *str, int unset); void itrace_synth_opts__set_default(struct itrace_synth_opts *synth_opts, - bool no_sample); + bool no_sample, bool single_event_per_ip); size_t perf_event__fprintf_auxtrace_error(union perf_event *event, FILE *fp); void perf_session__auxtrace_error_inc(struct perf_session *session, diff --git a/tools/perf/util/cs-etm.c b/tools/perf/util/cs-etm.c index 114b3cd2da49..2d1ab34f7b6b 100644 --- a/tools/perf/util/cs-etm.c +++ b/tools/perf/util/cs-etm.c @@ -3626,7 +3626,7 @@ int cs_etm__process_auxtrace_info_full(union perf_event *event, etm->synth_opts = *session->itrace_synth_opts; } else { itrace_synth_opts__set_default(&etm->synth_opts, - session->itrace_synth_opts->default_no_sample); + session->itrace_synth_opts->default_no_sample, false); etm->synth_opts.callchain = false; etm->synth_opts.thread_stack = session->itrace_synth_opts->thread_stack; } diff --git a/tools/perf/util/intel-bts.c b/tools/perf/util/intel-bts.c index 02df3e460489..4c9cca3acb6c 100644 --- a/tools/perf/util/intel-bts.c +++ b/tools/perf/util/intel-bts.c @@ -877,7 +877,7 @@ int intel_bts_process_auxtrace_info(union perf_event *event, bts->synth_opts = *session->itrace_synth_opts; } else { itrace_synth_opts__set_default(&bts->synth_opts, - session->itrace_synth_opts->default_no_sample); + session->itrace_synth_opts->default_no_sample, false); bts->synth_opts.thread_stack = session->itrace_synth_opts->thread_stack; } diff --git a/tools/perf/util/intel-pt.c b/tools/perf/util/intel-pt.c index 9d6628169fd9..8c21c9f52d57 100644 --- a/tools/perf/util/intel-pt.c +++ b/tools/perf/util/intel-pt.c @@ -4463,7 +4463,8 @@ int intel_pt_process_auxtrace_info(union perf_event *event, } else { struct itrace_synth_opts *opts = session->itrace_synth_opts; - itrace_synth_opts__set_default(&pt->synth_opts, opts->default_no_sample); + itrace_synth_opts__set_default(&pt->synth_opts, + opts->default_no_sample, false); if (!opts->default_no_sample && !opts->inject) { pt->synth_opts.branches = false; pt->synth_opts.callchain = true; From 1c12f13c079c35a1e5ef2dd9e6aeea1810c58e87 Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Wed, 16 Sep 2026 01:29:25 +0000 Subject: [PATCH 107/425] perf arm-spe: Set default synthesized event period to 1 When using --itrace=M for data type profiling on arm64, the 'Percent' values in perf annotate output are all zero: Annotate type: 'struct mmu_gather_batch' in [kernel.kallsyms] (15 samples): ============================================================================ Percent offset size field 0.00 0 0x10 struct mmu_gather_batch { 0.00 0 0x8 struct mmu_gather_batch* next; 0.00 0x8 0x4 unsigned int nr; 0.00 0xc 0x4 unsigned int max; 0.00 0x10 0 struct encoded_page*[] encoded_pages; }; However, adding the -n option (show-nr-samples) reveals non-zero sample counts: Annotate type: 'struct mmu_gather_batch' in [kernel.kallsyms] (15 samples): ============================================================================ Samples offset size field 15 0 0x10 struct mmu_gather_batch { 13 0 0x8 struct mmu_gather_batch* next; 2 0x8 0x4 unsigned int nr; 0 0xc 0x4 unsigned int max; 0 0x10 0 struct encoded_page*[] encoded_pages; }; The root cause is that when --itrace is used with non-i/y options (e.g., --itrace=M), the period remains zero after parsing. During subsequent propagation, the final percentage calculation results in zero: arm_spe_prep_sample() sample->period = spe->synth_opts.period; __hists__add_entry() he.stat.period = sample->period; annotated_data_type__update_samples() h->period += period; h->addr[offset].period += period; print_annotated_data_value() double percent = h->period ? (100.0 * period / h->period) : 0; Fix this by defaulting the period to 1 in the ARM SPE auxtrace info processing path if it is not explicitly specified. This ensures that downstream tool components can correctly accumulate periods and compute event percentages for all synthesized SPE events. With this fix applied, the result is as follows: Annotate type: 'struct mmu_gather_batch' in [kernel.kallsyms] (15 samples): ============================================================================ Percent offset size field 100.00 0 0x10 struct mmu_gather_batch { 86.67 0 0x8 struct mmu_gather_batch* next; 13.33 0x8 0x4 unsigned int nr; 0.00 0xc 0x4 unsigned int max; 0.00 0x10 0 struct encoded_page*[] encoded_pages; }; Reviewed-by: Ian Rogers Acked-by: Namhyung Kim Signed-off-by: Tengda Wu Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/arm-spe.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/perf/util/arm-spe.c b/tools/perf/util/arm-spe.c index ae7a5e9c3892..7805a1a98a89 100644 --- a/tools/perf/util/arm-spe.c +++ b/tools/perf/util/arm-spe.c @@ -2043,6 +2043,17 @@ int arm_spe_process_auxtrace_info(union perf_event *event, err = -EINVAL; goto err_free_queues; } + + /* + * When --itrace is used with non-i/y options (e.g., --itrace=M), + * the period remains 0 because the parser does not apply a default + * for those paths. However, synthesized SPE events such as memory + * accesses, TLB walks, and cache misses still require a valid + * sample->period to correctly accumulate periods and compute event + * percentages. Set it to 1 to ensure proper accounting. + */ + spe->synth_opts.period = spe->synth_opts.period ?: 1; + if (spe->synth_opts.period > 1) ui__warning("Arm SPE has a hardware-based sampling period.\n\n" "--itrace periods > 1i downsample by an interval of n SPE samples rather than n instructions.\n"); From d023b2110d0d7232ba952a486c497ba8817f6f66 Mon Sep 17 00:00:00 2001 From: Hui Su Date: Sat, 12 Sep 2026 12:11:10 +0900 Subject: [PATCH 108/425] perf evsel: Restrict incremental open error unwind evsel__open_cpu(), evsel__tool_pmu_open() and evsel__hwmon_pmu_open() support opening a subrange of a CPU map, bounded by [start_cpu_map_idx, end_cpu_map_idx). Their error paths, however, unwind using "while (--idx >= 0)" all the way back to CPU index 0 regardless of start_cpu_map_idx. If lower CPU indices were opened by an earlier invocation, a later incremental open failure therefore closes those existing file descriptors and overwrites their slots with -1. Before this fix, running the new incremental open test demonstrates this corruption: $ perf test -v "Tool PMU" 12: Tool PMU: 12.1: Tool PMU : Ok 12.2: Tool PMU leader : Ok 12.3: Incremental open error unwind boundary: FAILED: CPU 0 FD overwritten: FD(evsel, 0, 0)=-1, expected 4 FAILED! Stop the unwind at start_cpu_map_idx so that only file descriptors opened by the current invocation are rolled back. After this fix: $ perf test -v "Tool PMU" 12: Tool PMU: 12.1: Tool PMU : Ok 12.2: Tool PMU leader : Ok 12.3: Incremental open error unwind boundary : Ok Fixes: 4804e0111662d7d8 ("perf stat: Use affinity for opening events") Fixes: 240505b2d0adcdc8 ("perf tool_pmu: Factor tool events into their own PMU") Fixes: 53cc0b351ec99278 ("perf hwmon_pmu: Add a tool PMU exposing events from hwmon in sysfs") Reviewed-by: Ian Rogers Signed-off-by: Hui Su Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/tool_pmu.c | 79 +++++++++++++++++++++++++++++++++++++ tools/perf/util/evsel.c | 2 +- tools/perf/util/hwmon_pmu.c | 2 +- tools/perf/util/tool_pmu.c | 2 +- 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/tools/perf/tests/tool_pmu.c b/tools/perf/tests/tool_pmu.c index c6c5ebf0e935..667160f9e2cd 100644 --- a/tools/perf/tests/tool_pmu.c +++ b/tools/perf/tests/tool_pmu.c @@ -1,8 +1,16 @@ // SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +#include +#include +#include +#include +#include +#include "cpumap.h" #include "debug.h" #include "evlist.h" +#include "evsel.h" #include "parse-events.h" #include "tests.h" +#include "thread_map.h" #include "tool_pmu.h" static int do_test(enum tool_pmu_event ev, bool with_pmu) @@ -100,9 +108,80 @@ static int test__tool_pmu_with_pmu(struct test_suite *test __maybe_unused, return TEST_OK; } +static int test__tool_pmu_incremental_open_unwind(struct test_suite *test __maybe_unused, + int subtest __maybe_unused) +{ + struct evlist *evlist = evlist__new(); + struct parse_events_error err; + struct perf_cpu_map *cpus = NULL; + struct perf_thread_map *threads = NULL; + struct evsel *evsel; + int ret = TEST_FAIL, fd0 = -1; + + if (!evlist) + return TEST_FAIL; + + parse_events_error__init(&err); + if (parse_events(evlist, "tool/user_time/", &err)) { + parse_events_error__exit(&err); + evlist__put(evlist); + return TEST_FAIL; + } + parse_events_error__exit(&err); + + evsel = evlist__first(evlist); + cpus = perf_cpu_map__new("0,1"); + threads = thread_map__new_by_tid(getpid()); + if (!cpus || !threads) + goto out; + + /* Step 1: Open CPU index 0 successfully */ + if (evsel__open_per_cpu_and_thread(evsel, cpus, 0, threads) < 0) { + pr_debug("Failed to open CPU index 0\n"); + goto out; + } + + fd0 = (*(int *)xyarray__entry(evsel->core.fd, 0, 0)); + if (fd0 < 0 || fcntl(fd0, F_GETFD) < 0) { + pr_debug("CPU index 0 FD is invalid (%d)\n", fd0); + goto out; + } + + /* Step 2: Intentionally trigger failure on CPU index 1 */ + evsel->core.attr.sample_period = 1; /* Not supported for tool PMU -> -EINVAL */ + if (evsel__open_per_cpu_and_thread(evsel, cpus, 1, threads) >= 0) { + pr_debug("Unexpected success opening CPU index 1 with sample_period=1\n"); + goto out; + } + + /* + * Step 3: Check that CPU index 0's FD was NOT destroyed by CPU index 1's unwind. + */ + if ((*(int *)xyarray__entry(evsel->core.fd, 0, 0)) != fd0) { + pr_debug("FAILED: CPU 0 FD overwritten: FD(evsel, 0, 0)=%d, expected %d\n", + (*(int *)xyarray__entry(evsel->core.fd, 0, 0)), fd0); + goto out; + } + + if (fcntl(fd0, F_GETFD) < 0) { + pr_debug("FAILED: CPU 0 FD %d was closed by error unwind! errno=%d (%s)\n", + fd0, errno, strerror(errno)); + goto out; + } + + ret = TEST_OK; +out: + evsel__close(evsel); + perf_cpu_map__put(cpus); + perf_thread_map__put(threads); + evlist__put(evlist); + return ret; +} + static struct test_case tests__tool_pmu[] = { TEST_CASE("Parsing without PMU name", tool_pmu_without_pmu), TEST_CASE("Parsing with PMU name", tool_pmu_with_pmu), + TEST_CASE("Incremental open error unwind boundary", tool_pmu_incremental_open_unwind), { .name = NULL, } }; diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index c663aafa88b2..0d0b2804d238 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -3160,7 +3160,7 @@ static int evsel__open_cpu(struct evsel *evsel, struct perf_cpu_map *cpus, FD(evsel, idx, thread) = -1; } thread = nthreads; - } while (--idx >= 0); + } while (--idx >= start_cpu_map_idx); errno = old_errno; out: if (err) diff --git a/tools/perf/util/hwmon_pmu.c b/tools/perf/util/hwmon_pmu.c index ed544dca70c3..96aeb379e169 100644 --- a/tools/perf/util/hwmon_pmu.c +++ b/tools/perf/util/hwmon_pmu.c @@ -803,7 +803,7 @@ int evsel__hwmon_pmu_open(struct evsel *evsel, FD(evsel, idx, thread) = -1; } thread = nthreads; - } while (--idx >= 0); + } while (--idx >= start_cpu_map_idx); close(dir); return err; } diff --git a/tools/perf/util/tool_pmu.c b/tools/perf/util/tool_pmu.c index 5c30854b4644..d2f7591747d9 100644 --- a/tools/perf/util/tool_pmu.c +++ b/tools/perf/util/tool_pmu.c @@ -349,7 +349,7 @@ int evsel__tool_pmu_open(struct evsel *evsel, FD(evsel, idx, thread) = -1; } thread = nthreads; - } while (--idx >= 0); + } while (--idx >= start_cpu_map_idx); errno = old_errno; return err; } From cf3d239189da0bd5122ae38c62f5fefdb744eb86 Mon Sep 17 00:00:00 2001 From: Hui Su Date: Sat, 12 Sep 2026 14:56:19 +0900 Subject: [PATCH 109/425] perf synthetic-events: Fix schedstat event lifetime handling perf_event__synthesize_schedstat() has two event lifetime issues. After a successful iteration, event is freed but retains its value. If the next iteration starts with an unrecognized schedstat record type, neither synthesizer assigns a new value. The stale pointer then passes the NULL check, may be passed to process(), and is freed again. In addition, when user_requested_cpus filters out a synthesized event, the continue path skips free(event), leaking the event. Make event local to each loop iteration so it always starts as NULL. Also avoid the filter continue and unconditionally free each synthesized event at the end of the iteration. Fixes: c3030995f23b3d35 ("perf sched stats: Add record and rawdump support") Reviewed-by: Ian Rogers Signed-off-by: Hui Su Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/synthetic-events.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/perf/util/synthetic-events.c b/tools/perf/util/synthetic-events.c index 6c5e2327fdfa..44eff2391539 100644 --- a/tools/perf/util/synthetic-events.c +++ b/tools/perf/util/synthetic-events.c @@ -2855,7 +2855,6 @@ int perf_event__synthesize_schedstat(const struct perf_tool *tool, struct perf_cpu_map *user_requested_cpus) { char *line = NULL, path[PATH_MAX]; - union perf_event *event = NULL; size_t line_len = 0; char bf[BUFSIZ]; __u64 timestamp; @@ -2896,6 +2895,7 @@ int perf_event__synthesize_schedstat(const struct perf_tool *tool, * for filtered out cpus. */ for (ch = io__get_char(&io); !io.eof; ch = io__get_char(&io)) { + union perf_event *event = NULL; struct perf_cpu this_cpu; if (ch == 'c') { @@ -2910,12 +2910,12 @@ int perf_event__synthesize_schedstat(const struct perf_tool *tool, this_cpu.cpu = cpu; - if (user_requested_cpus && !perf_cpu_map__has(user_requested_cpus, this_cpu)) - continue; - - if (process(tool, event, NULL, NULL) < 0) { - free(event); - goto out_free_line; + if (!user_requested_cpus || + perf_cpu_map__has(user_requested_cpus, this_cpu)) { + if (process(tool, event, NULL, NULL) < 0) { + free(event); + goto out_free_line; + } } free(event); From 1622c9ae39e2882fcbbae7c9d27dda13f1880b93 Mon Sep 17 00:00:00 2001 From: Chen Pei Date: Mon, 14 Sep 2026 20:19:03 +0800 Subject: [PATCH 110/425] perf test: Fix a leaked map reference in the arch dwarf unwind tests maps__find() has returned a reference to the map it finds since commit 42fd623b58db ("perf maps: Get map before returning in maps__find"), which updated arch/x86/tests/dwarf-unwind.c but missed the arm, arm64 and powerpc copies. Release the map after reading its end, as x86 does. All other callers of maps__find() already release what they get. Fixes: 42fd623b58dbcc48 ("perf maps: Get map before returning in maps__find") Reviewed-by: Ian Rogers Signed-off-by: Chen Pei Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/arch/arm/tests/dwarf-unwind.c | 1 + tools/perf/arch/arm64/tests/dwarf-unwind.c | 1 + tools/perf/arch/powerpc/tests/dwarf-unwind.c | 1 + 3 files changed, 3 insertions(+) diff --git a/tools/perf/arch/arm/tests/dwarf-unwind.c b/tools/perf/arch/arm/tests/dwarf-unwind.c index f421910e0709..d4707e9ffd2a 100644 --- a/tools/perf/arch/arm/tests/dwarf-unwind.c +++ b/tools/perf/arch/arm/tests/dwarf-unwind.c @@ -34,6 +34,7 @@ static int sample_ustack(struct perf_sample *sample, } stack_size = map__end(map) - sp; + map__put(map); stack_size = stack_size > STACK_SIZE ? STACK_SIZE : stack_size; memcpy(buf, (void *) sp, stack_size); diff --git a/tools/perf/arch/arm64/tests/dwarf-unwind.c b/tools/perf/arch/arm64/tests/dwarf-unwind.c index 440d00f0de14..fbc161a49369 100644 --- a/tools/perf/arch/arm64/tests/dwarf-unwind.c +++ b/tools/perf/arch/arm64/tests/dwarf-unwind.c @@ -34,6 +34,7 @@ static int sample_ustack(struct perf_sample *sample, } stack_size = map__end(map) - sp; + map__put(map); stack_size = stack_size > STACK_SIZE ? STACK_SIZE : stack_size; memcpy(buf, (void *) sp, stack_size); diff --git a/tools/perf/arch/powerpc/tests/dwarf-unwind.c b/tools/perf/arch/powerpc/tests/dwarf-unwind.c index 66af884baa66..b4bade5d59e2 100644 --- a/tools/perf/arch/powerpc/tests/dwarf-unwind.c +++ b/tools/perf/arch/powerpc/tests/dwarf-unwind.c @@ -34,6 +34,7 @@ static int sample_ustack(struct perf_sample *sample, } stack_size = map__end(map) - sp; + map__put(map); stack_size = stack_size > STACK_SIZE ? STACK_SIZE : stack_size; memcpy(buf, (void *) sp, stack_size); From 0b26b346a8a2bdcb3a7b6ab6f4e6450ee300eec3 Mon Sep 17 00:00:00 2001 From: Chen Pei Date: Mon, 14 Sep 2026 20:41:45 +0800 Subject: [PATCH 111/425] perf riscv: Add perf_regs_load and enable the dwarf unwind test riscv has no arch level perf test: tests/builtin-test.c only picks up arch_tests[] for x86, arm64 and powerpc64, tests/Build only builds the generic dwarf unwind test driver for x86, arm, arm64 and powerpc, and arch/riscv/tests/ did not exist. "Test dwarf unwind" was therefore never registered on riscv, even though DWARF is the only reliable way to walk a riscv user stack -- the arch callchain code walks frame pointers only -- and both unwinders already support riscv, elfutils libdw generically and libunwind through util/libunwind-arch/libunwind-riscv.c. Add perf_regs_load() and the arch unwind sample helper modelled on the arm64 ones, and wire riscv into the two gates. The x1..x31 slots of enum perf_event_riscv_regs match the hardware registers one to one, and PERF_REG_RISCV_PC is filled with ra so that unwinding starts at the caller. Reviewed-by: Ian Rogers Signed-off-by: Chen Pei Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/arch/riscv/Build | 1 + tools/perf/arch/riscv/include/arch-tests.h | 9 +++ tools/perf/arch/riscv/include/perf_regs.h | 2 + tools/perf/arch/riscv/tests/Build | 4 ++ tools/perf/arch/riscv/tests/arch-tests.c | 10 ++++ tools/perf/arch/riscv/tests/dwarf-unwind.c | 64 ++++++++++++++++++++++ tools/perf/arch/riscv/tests/regs_load.S | 58 ++++++++++++++++++++ tools/perf/tests/Build | 2 +- tools/perf/tests/builtin-test.c | 3 +- 9 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tools/perf/arch/riscv/include/arch-tests.h create mode 100644 tools/perf/arch/riscv/tests/Build create mode 100644 tools/perf/arch/riscv/tests/arch-tests.c create mode 100644 tools/perf/arch/riscv/tests/dwarf-unwind.c create mode 100644 tools/perf/arch/riscv/tests/regs_load.S diff --git a/tools/perf/arch/riscv/Build b/tools/perf/arch/riscv/Build index e63eabc2c8f4..12ebc65ea7a3 100644 --- a/tools/perf/arch/riscv/Build +++ b/tools/perf/arch/riscv/Build @@ -1 +1,2 @@ perf-util-y += util/ +perf-test-y += tests/ diff --git a/tools/perf/arch/riscv/include/arch-tests.h b/tools/perf/arch/riscv/include/arch-tests.h new file mode 100644 index 000000000000..2b3834a5ddbb --- /dev/null +++ b/tools/perf/arch/riscv/include/arch-tests.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef ARCH_TESTS_H +#define ARCH_TESTS_H + +struct test_suite; + +extern struct test_suite *arch_tests[]; + +#endif diff --git a/tools/perf/arch/riscv/include/perf_regs.h b/tools/perf/arch/riscv/include/perf_regs.h index af7a1b47bf66..30cfd3f63eb5 100644 --- a/tools/perf/arch/riscv/include/perf_regs.h +++ b/tools/perf/arch/riscv/include/perf_regs.h @@ -8,6 +8,8 @@ #include #include "../../../../arch/riscv/include/uapi/asm/perf_regs.h" +void perf_regs_load(u64 *regs); + #define PERF_REGS_MASK ((1ULL << PERF_REG_RISCV_MAX) - 1) #define PERF_REGS_MAX PERF_REG_RISCV_MAX diff --git a/tools/perf/arch/riscv/tests/Build b/tools/perf/arch/riscv/tests/Build new file mode 100644 index 000000000000..bd15c6bbc1b2 --- /dev/null +++ b/tools/perf/arch/riscv/tests/Build @@ -0,0 +1,4 @@ +perf-test-y += regs_load.o +perf-test-$(CONFIG_DWARF_UNWIND) += dwarf-unwind.o + +perf-test-y += arch-tests.o diff --git a/tools/perf/arch/riscv/tests/arch-tests.c b/tools/perf/arch/riscv/tests/arch-tests.c new file mode 100644 index 000000000000..756706a09609 --- /dev/null +++ b/tools/perf/arch/riscv/tests/arch-tests.c @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "tests/tests.h" +#include "arch-tests.h" + +struct test_suite *arch_tests[] = { +#ifdef HAVE_DWARF_UNWIND_SUPPORT + &suite__dwarf_unwind, +#endif + NULL, +}; diff --git a/tools/perf/arch/riscv/tests/dwarf-unwind.c b/tools/perf/arch/riscv/tests/dwarf-unwind.c new file mode 100644 index 000000000000..e981b21f918d --- /dev/null +++ b/tools/perf/arch/riscv/tests/dwarf-unwind.c @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include "perf_regs.h" +#include "thread.h" +#include "map.h" +#include "maps.h" +#include "event.h" +#include "debug.h" +#include "tests/tests.h" + +#define STACK_SIZE 8192 + +static int sample_ustack(struct perf_sample *sample, + struct thread *thread, u64 *regs) +{ + struct stack_dump *stack = &sample->user_stack; + struct map *map; + unsigned long sp; + u64 stack_size, *buf; + + buf = malloc(STACK_SIZE); + if (!buf) { + pr_debug("failed to allocate sample uregs data\n"); + return -1; + } + + sp = (unsigned long) regs[PERF_REG_RISCV_SP]; + + map = maps__find(thread__maps(thread), (u64)sp); + if (!map) { + pr_debug("failed to get stack map\n"); + free(buf); + return -1; + } + + stack_size = map__end(map) - sp; + map__put(map); + stack_size = stack_size > STACK_SIZE ? STACK_SIZE : stack_size; + + memcpy(buf, (void *) sp, stack_size); + stack->data = (char *) buf; + stack->size = stack_size; + return 0; +} + +int test__arch_unwind_sample(struct perf_sample *sample, + struct thread *thread) +{ + struct regs_dump *regs = perf_sample__user_regs(sample); + u64 *buf; + + buf = calloc(PERF_REGS_MAX, sizeof(u64)); + if (!buf) { + pr_debug("failed to allocate sample uregs data\n"); + return -1; + } + + perf_regs_load(buf); + regs->abi = PERF_SAMPLE_REGS_ABI; + regs->regs = buf; + regs->mask = PERF_REGS_MASK; + + return sample_ustack(sample, thread, buf); +} diff --git a/tools/perf/arch/riscv/tests/regs_load.S b/tools/perf/arch/riscv/tests/regs_load.S new file mode 100644 index 000000000000..703bbf3374a6 --- /dev/null +++ b/tools/perf/arch/riscv/tests/regs_load.S @@ -0,0 +1,58 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#include + +/* + * Fill the u64 array 'regs' at the slots of enum perf_event_riscv_regs: + * x1..x31 map one-to-one onto PERF_REG_RISCV_RA..PERF_REG_RISCV_T6, and the + * stride stays 8 bytes on RV32 because the slots are u64 and the caller + * zeroes them. + * + * PERF_REG_RISCV_PC is filled with ra, the return address of the call to + * this function, so that unwinding starts at the caller rather than here. + */ + +#if __riscv_xlen == 64 +#define REG_S sd +#else +#define REG_S sw +#endif + +#define STR_REG(r) REG_S x##r, 8 * r(a0) + + .text + .type perf_regs_load,%function +SYM_FUNC_START(perf_regs_load) + STR_REG(1) + STR_REG(2) + STR_REG(3) + STR_REG(4) + STR_REG(5) + STR_REG(6) + STR_REG(7) + STR_REG(8) + STR_REG(9) + STR_REG(10) + STR_REG(11) + STR_REG(12) + STR_REG(13) + STR_REG(14) + STR_REG(15) + STR_REG(16) + STR_REG(17) + STR_REG(18) + STR_REG(19) + STR_REG(20) + STR_REG(21) + STR_REG(22) + STR_REG(23) + STR_REG(24) + STR_REG(25) + STR_REG(26) + STR_REG(27) + STR_REG(28) + STR_REG(29) + STR_REG(30) + STR_REG(31) + REG_S ra, 0(a0) + ret +SYM_FUNC_END(perf_regs_load) diff --git a/tools/perf/tests/Build b/tools/perf/tests/Build index 406e48eed1c8..81c311b131b7 100644 --- a/tools/perf/tests/Build +++ b/tools/perf/tests/Build @@ -73,7 +73,7 @@ perf-test-y += tool_pmu.o perf-test-y += subcmd-help.o perf-test-y += kallsyms-split.o -ifeq ($(SRCARCH),$(filter $(SRCARCH),x86 arm arm64 powerpc)) +ifeq ($(SRCARCH),$(filter $(SRCARCH),x86 arm arm64 powerpc riscv)) perf-test-$(CONFIG_DWARF_UNWIND) += dwarf-unwind.o endif diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-test.c index 2d53414d8271..d2f594921e25 100644 --- a/tools/perf/tests/builtin-test.c +++ b/tools/perf/tests/builtin-test.c @@ -69,7 +69,8 @@ static const char *workload_control; * dependent on the initialization, as such GCC with LTO complains of * conflicting definitions with a weak symbol. */ -#if defined(__i386__) || defined(__x86_64__) || defined(__aarch64__) || defined(__powerpc64__) +#if defined(__i386__) || defined(__x86_64__) || defined(__aarch64__) || \ + defined(__powerpc64__) || defined(__riscv) extern struct test_suite *arch_tests[]; #else static struct test_suite *arch_tests[] = { From c4c43343501bd9fc24ec501ecaf43b7ef2da44d2 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 15 Sep 2026 23:12:10 -0700 Subject: [PATCH 112/425] perf libbfd: Fix the clang -Wthread-safety build failure perf_bfd_lock() and perf_bfd_unlock() are the callbacks given to bfd_thread_init(), so by design they acquire and release the lock on libbfd's behalf and the lock state differs between entry and exit. clang's thread safety analysis, which perf builds with as an error, flags both: util/libbfd.c:56:1: error: mutex 'bfd_mutex' is still held at the end of function [-Werror,-Wthread-safety-analysis] util/libbfd.c:60:2: error: releasing mutex 'bfd_mutex' that was not held [-Werror,-Wthread-safety-analysis] Mark both functions NO_THREAD_SAFETY_ANALYSIS so that libbfd.c compiles with "make CC=clang BUILD_NONDISTRO=1". Fixes: b72b8132d8fd2d6b ("perf libbfd: Ensure libbfd is initialized prior to use") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/libbfd.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/perf/util/libbfd.c b/tools/perf/util/libbfd.c index 936555217e81..d1d1c9b2ef22 100644 --- a/tools/perf/util/libbfd.c +++ b/tools/perf/util/libbfd.c @@ -39,13 +39,13 @@ struct a2l_data { asymbol **syms; }; -static bool perf_bfd_lock(void *bfd_mutex) +static bool perf_bfd_lock(void *bfd_mutex) NO_THREAD_SAFETY_ANALYSIS { mutex_lock(bfd_mutex); return true; } -static bool perf_bfd_unlock(void *bfd_mutex) +static bool perf_bfd_unlock(void *bfd_mutex) NO_THREAD_SAFETY_ANALYSIS { mutex_unlock(bfd_mutex); return true; From ecbc5aac7d9218320f2c734d75798f34c15a9d85 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 15 Sep 2026 23:12:11 -0700 Subject: [PATCH 113/425] perf symbol: Fix the build when demangling with libbfd cxx_demangle_sym() calls bfd_demangle() with the DMGL_PARAMS and DMGL_ANSI flags, but nothing declares them. The "#define PACKAGE 'perf'", "#include " and the DMGL_ definitions were left behind in symbol-elf.c when the demangling code moved to symbol.c, and libiberty's demangle.h that defines the flags isn't installed by every binutils package. The cplus_demangle() variant is missing a declaration for the same reason. The code is only built when the C++ ABI's __cxa_demangle() is unavailable, so most builds never compile it: $ make BUILD_NONDISTRO=1 NO_DEMANGLE=1 util/symbol.c:2714: error: 'DMGL_PARAMS' undeclared util/symbol.c:2714: error: 'DMGL_ANSI' undeclared util/symbol.c:2716: error: implicit declaration of 'bfd_demangle' Move the libbfd demangling into libbfd.c, which already includes bfd.h, and declare in symbol.c the libiberty interfaces the cplus_demangle() variant needs. Fixes: 4d9b5146f0d91472 ("perf symbol: Move demangling code out of symbol-elf.c") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/libbfd.c | 17 +++++++++++++++++ tools/perf/util/libbfd.h | 9 +++++++++ tools/perf/util/symbol.c | 18 +++++++++++++++--- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/tools/perf/util/libbfd.c b/tools/perf/util/libbfd.c index d1d1c9b2ef22..59f4ab252bb1 100644 --- a/tools/perf/util/libbfd.c +++ b/tools/perf/util/libbfd.c @@ -72,6 +72,23 @@ static void ensure_bfd_init(void) pthread_once(&bfd_init_once, perf_bfd_init); } +/* + * Flags from libiberty's demangle.h. bfd.h declares bfd_demangle but not the + * flags to pass to it, and demangle.h isn't installed by every binutils + * package. + */ +#ifndef DMGL_PARAMS +#define DMGL_PARAMS (1 << 0) /* Include function arguments. */ +#define DMGL_ANSI (1 << 1) /* Include const, volatile, etc. */ +#endif + +char *libbfd__demangle_sym(const char *str, bool params, bool modifiers) +{ + int flags = (params ? DMGL_PARAMS : 0) | (modifiers ? DMGL_ANSI : 0); + + return bfd_demangle(/*abfd=*/NULL, str, flags); +} + static int bfd_error(const char *string) { const char *errmsg; diff --git a/tools/perf/util/libbfd.h b/tools/perf/util/libbfd.h index 953886f3d62f..14171db8b112 100644 --- a/tools/perf/util/libbfd.h +++ b/tools/perf/util/libbfd.h @@ -31,6 +31,8 @@ int libbfd_filename__read_debuglink(const char *filename, char *debuglink, size_ int symbol__disassemble_bpf_libbfd(struct symbol *sym, struct annotate_args *args); +char *libbfd__demangle_sym(const char *str, bool params, bool modifiers); + #else // !defined(HAVE_LIBBFD_SUPPORT) #include "annotate.h" @@ -77,6 +79,13 @@ static inline int symbol__disassemble_bpf_libbfd(struct symbol *sym __always_unu return SYMBOL_ANNOTATE_ERRNO__NO_LIBOPCODES_FOR_BPF; } +static inline char *libbfd__demangle_sym(const char *str __always_unused, + bool params __always_unused, + bool modifiers __always_unused) +{ + return NULL; +} + #endif // defined(HAVE_LIBBFD_SUPPORT) #endif /* __PERF_LIBBFD_H */ diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c index 4c1f8231d47f..163652f071c6 100644 --- a/tools/perf/util/symbol.c +++ b/tools/perf/util/symbol.c @@ -27,6 +27,7 @@ #include "dso.h" #include "util.h" // lsdir() #include "event.h" +#include "libbfd.h" #include "machine.h" #include "map.h" #include "symbol.h" @@ -2708,13 +2709,24 @@ static bool want_demangle(bool is_kernel_sym) * version. */ #ifndef HAVE_CXA_DEMANGLE_SUPPORT +#ifdef HAVE_CPLUS_DEMANGLE_SUPPORT +/* + * Declarations from libiberty's demangle.h, the header isn't installed by + * every binutils package. + */ +#ifndef DMGL_PARAMS +#define DMGL_PARAMS (1 << 0) /* Include function arguments. */ +#define DMGL_ANSI (1 << 1) /* Include const, volatile, etc. */ +#endif + +char *cplus_demangle(const char *mangled, int options); +#endif + char *cxx_demangle_sym(const char *str __maybe_unused, bool params __maybe_unused, bool modifiers __maybe_unused) { #ifdef HAVE_LIBBFD_SUPPORT - int flags = (params ? DMGL_PARAMS : 0) | (modifiers ? DMGL_ANSI : 0); - - return bfd_demangle(NULL, str, flags); + return libbfd__demangle_sym(str, params, modifiers); #elif defined(HAVE_CPLUS_DEMANGLE_SUPPORT) int flags = (params ? DMGL_PARAMS : 0) | (modifiers ? DMGL_ANSI : 0); From f96322601645064ea7773576c61e4297d795f05f Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 15 Sep 2026 23:12:12 -0700 Subject: [PATCH 114/425] perf libbfd: Include the headers that are used libbfd.c uses errno, PATH_MAX, strdup(), strlen(), strcmp(), memcpy() and pthread_once() but relies on other headers to drag in , , and . The BPF disassembly code needs for PRIx64 and for uintptr_t for the same reason. Include them directly and group the includes so that the system ones come before perf's own. No functional change. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/libbfd.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tools/perf/util/libbfd.c b/tools/perf/util/libbfd.c index 59f4ab252bb1..a907d7c1ac94 100644 --- a/tools/perf/util/libbfd.c +++ b/tools/perf/util/libbfd.c @@ -1,5 +1,19 @@ // SPDX-License-Identifier: GPL-2.0 #include "libbfd.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + #include "annotate.h" #include "bpf-event.h" #include "bpf-utils.h" @@ -11,15 +25,13 @@ #include "symbol.h" #include "symbol_conf.h" #include "util.h" -#include + #ifdef HAVE_LIBBPF_SUPPORT #include #include #include #endif -#include -#include -#include + #define PACKAGE "perf" #include From cd1f5f952ab1f88488a8eb60b3673470dbeeb3c3 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 15 Sep 2026 23:12:13 -0700 Subject: [PATCH 115/425] perf pmu: Include the headers that are used pmu.c uses errno, free() and the str*() functions but declares none of them directly. arrives by way of , and and come from whatever the other headers happen to drag in, which is fragile and breaks on C libraries that don't nest their headers the same way. Include the three directly. is deliberately not added. The isdigit(), tolower() and toupper() used here are the macros from , which is included on purpose and documents that it doesn't handle EOF the way the standard C library does, so pulling in would change which implementation is used. size_t is left alone too, as is already included directly and declares it. No functional change. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/pmu.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/perf/util/pmu.c b/tools/perf/util/pmu.c index 825c0801c86a..01753d5c69d1 100644 --- a/tools/perf/util/pmu.c +++ b/tools/perf/util/pmu.c @@ -8,8 +8,11 @@ #include #include #include -#include +#include #include +#include +#include +#include #include #include #include From c8592f8da838a07ddd0011eeb444771b64193a0f Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 15 Sep 2026 23:12:14 -0700 Subject: [PATCH 116/425] perf build: Only pass clang flags to CXX when CXX is clang Building with "make CC=clang" appends clang's --target= and -fintegrated-as to CXX, but CXX is only set to clang++ by "make LLVM=1". With just CC=clang, CXX is still g++ and every C++ feature test dies: g++: error: unrecognized command-line option '--target=...' g++: error: unrecognized command-line option '-fintegrated-as' The failures are silent, as feature tests are allowed to fail, so the build quietly loses libllvm and C++ demangling support: $ make CC=clang && grep LIBLLVM .config-detected (nothing, whereas a gcc build reports CONFIG_LIBLLVM=y) The block is entered when CC_NO_CLANG says CC is clang, which says nothing about CXX. Probe CXX the same way tools/scripts/Makefile.include probes CC, and only add the flags when CXX really is clang++. Mixing a clang-built C with a g++-built C++ is fine here, the C++ code is linked with -lstdc++ already. Fixes: 4772e66cb45e0dd2 ("perf build: Support build with clang") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Makefile.config | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config index 4ee7393a39f9..31e4cbe192b9 100644 --- a/tools/perf/Makefile.config +++ b/tools/perf/Makefile.config @@ -54,7 +54,14 @@ ifeq ($(CC_NO_CLANG), 0) endif # CROSS_COMPILE CC := $(CLANG) $(CLANG_FLAGS) -fintegrated-as - CXX := $(CXX) $(CLANG_FLAGS) -fintegrated-as + + # CC being clang says nothing about CXX: "make LLVM=1" overrides both, but + # "make CC=clang" leaves CXX as g++. Probe CXX the way Makefile.include + # probes CC rather than assuming the two match. + CXX_NO_CLANG := $(shell if command -v $(firstword $(CXX)) >/dev/null 2>&1; then $(CXX) -dM -E -x c++ /dev/null; fi | grep -Fq "__clang__"; echo $$?) + ifeq ($(CXX_NO_CLANG), 0) + CXX := $(CXX) $(CLANG_FLAGS) -fintegrated-as + endif # Enabled Wthread-safety analysis for clang builds. CFLAGS += -Wthread-safety From bd874cd8356166d8fb262d32f368c0e460910d5d Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 15 Sep 2026 23:12:15 -0700 Subject: [PATCH 117/425] perf build: Use the given CC rather than clang in clang builds The clang block is entered when CC_NO_CLANG has determined that CC is clang, but it then ignores CC and hard codes $(CLANG), which defaults to plain "clang". Building with a versioned compiler silently switches compiler: $ make CC=clang-19 # actually builds with "clang" Worse, the target triple is probed with $(CLANG) too, so on a machine that only has versioned binaries installed the probe produces nothing and the build stops: $ make CC=clang-19 *** Specify CROSS_COMPILE or add CLANG_TARGET_FLAGS for x86_64 Use $(CC) in both places. Within the block CC is known to be clang, and for "make LLVM=1" CC is already $(CLANG), so nothing changes there. $(CLANG) is left as it is for BPF skeletons, as bpf_skel.mak builds those with "$(CLANG) --target=bpf" whatever CC happens to be. Fixes: 4772e66cb45e0dd2 ("perf build: Support build with clang") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Makefile.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config index 31e4cbe192b9..dca62e155fb5 100644 --- a/tools/perf/Makefile.config +++ b/tools/perf/Makefile.config @@ -38,7 +38,7 @@ ifeq ($(CC_NO_CLANG), 0) # Default to host architecture if ARCH is not explicitly given. ifeq ($(ARCH), $(HOSTARCH)) - CLANG_TARGET_FLAGS := $(shell $(CLANG) -print-target-triple) + CLANG_TARGET_FLAGS := $(shell $(CC) -print-target-triple) else CLANG_TARGET_FLAGS := $(CLANG_TARGET_FLAGS_$(ARCH)) endif @@ -53,7 +53,7 @@ ifeq ($(CC_NO_CLANG), 0) CLANG_FLAGS += --target=$(notdir $(CROSS_COMPILE:%-=%)) endif # CROSS_COMPILE - CC := $(CLANG) $(CLANG_FLAGS) -fintegrated-as + CC := $(CC) $(CLANG_FLAGS) -fintegrated-as # CC being clang says nothing about CXX: "make LLVM=1" overrides both, but # "make CC=clang" leaves CXX as g++. Probe CXX the way Makefile.include From 4f4387a07f601988e44fc6a4f4663aa1ff264f94 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 15 Sep 2026 23:12:16 -0700 Subject: [PATCH 118/425] perf build: Check the version of the compiler that is used The flex and bison flags are adjusted for clang older than 13.0.0, which doesn't grok -Wno-unused-but-set-variable. The test is reached because CC_NO_CLANG says CC is clang, but the version comes from $(CLANG), which defaults to plain "clang" and needn't be the compiler in use: $ make CC=clang-19 CLANG=clang-12 version from $(CLANG) -> 12.0.0, flag stripped version from $(CC) -> 19.1.7, flag kept Dropping the flag lets the warnings it suppresses in the generated flex and bison code through, and those are errors under -Werror. Ask $(CC) instead, as that is what compiles the code. Fixes: 878460e8d0ff84a0 ("perf build: Remove -Wno-unused-but-set-variable from the flex flags when building with clang < 13.0.0") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/Build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/util/Build b/tools/perf/util/Build index d08f2af7d970..a4618e4e4848 100644 --- a/tools/perf/util/Build +++ b/tools/perf/util/Build @@ -339,7 +339,7 @@ bison_flags := -DYYENABLE_NLS=0 -Wno-unused-but-set-variable # Old clangs don't grok -Wno-unused-but-set-variable, remove it ifeq ($(CC_NO_CLANG), 0) - CLANG_VERSION := $(shell $(CLANG) --version | head -1 | sed 's/.*clang version \([[:digit:]]\+.[[:digit:]]\+.[[:digit:]]\+\).*/\1/g') + CLANG_VERSION := $(shell $(CC) --version | head -1 | sed 's/.*clang version \([[:digit:]]\+.[[:digit:]]\+.[[:digit:]]\+\).*/\1/g') ifeq ($(call version-lt3,$(CLANG_VERSION),13.0.0),1) bison_flags := $(subst -Wno-unused-but-set-variable,,$(bison_flags)) flex_flags := $(subst -Wno-unused-but-set-variable,,$(flex_flags)) From a5cac2df8b4af0410dd6339e6491cb1e4fbc6986 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 15 Sep 2026 23:12:17 -0700 Subject: [PATCH 119/425] perf build: Remove leftovers of removed build options NO_LIBDW_DWARF_UNWIND, NO_SYSCALL_TABLE and the embedded clang/LLVM support were all removed, but each removal left the matching entry in the build test suite behind: run += make_no_libdw_dwarf_unwind run += make_no_syscall_tbl run += make_with_clangllvm None of these have a make_* variable defining what to build, so $($@) expands to nothing and each one silently repeats the plain "make_pure" build. The suite therefore spends six builds, as every test is also run with O=, appearing to cover options that no longer exist. Remove them, along with the NO_LIBDW_DWARF_UNWIND comment that is still in Makefile.perf. Fixes: 2e9191573a69ff96 ("perf build: Remove NO_LIBDW_DWARF_UNWIND option") Fixes: 3cc550f5bbcf54ab ("perf tools: Remove dependency on libaudit") Fixes: 56b11a2126bf2f42 ("perf bpf: Remove support for embedding clang for compiling BPF events (-e foo.c)") Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Makefile.perf | 3 --- tools/perf/tests/make | 3 --- 2 files changed, 6 deletions(-) diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf index 2438b40eaaec..53f538cc8e1a 100644 --- a/tools/perf/Makefile.perf +++ b/tools/perf/Makefile.perf @@ -64,9 +64,6 @@ include ../scripts/utilities.mak # # Define NO_LIBBIONIC if you do not want bionic support # -# Define NO_LIBDW_DWARF_UNWIND if you do not want libdw support -# for dwarf backtrace post unwind. -# # Define NO_LIBTRACEEVENT=1 if you don't want libtraceevent to be linked, # this will remove multiple features and tools, such as 'perf trace', # that need it to read tracefs event format files, etc. diff --git a/tools/perf/tests/make b/tools/perf/tests/make index f879f8109072..608470ff9c10 100644 --- a/tools/perf/tests/make +++ b/tools/perf/tests/make @@ -157,7 +157,6 @@ run += make_no_demangle run += make_no_libelf run += make_no_libdw run += make_libunwind -run += make_no_libdw_dwarf_unwind run += make_no_backtrace run += make_no_libcapstone run += make_libcapstone_dlopen @@ -167,10 +166,8 @@ run += make_no_libbpf run += make_no_libbpf_DEBUG run += make_no_libllvm run += make_no_sdt -run += make_no_syscall_tbl run += make_no_babeltrace2 run += make_with_coresight -run += make_with_clangllvm run += make_no_libpfm4 run += make_refcnt_check run += make_help From 551fbfaa062d0c12b6cec9000e5a738020764ce6 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Tue, 15 Sep 2026 23:12:18 -0700 Subject: [PATCH 120/425] perf test: Add build tests for clang and libbfd demangling Three build configurations broke recently without the build test suite noticing, as none of them were covered: - "CC=clang" on its own. CXX is only set to clang++ by "make LLVM=1", so CXX stays as g++ and used to be passed clang's flags. - "CC=clang BUILD_NONDISTRO=1", where libbfd.c failed clang's -Wthread-safety analysis. - "BUILD_NONDISTRO=1 NO_DEMANGLE=1", the only combination that compiles the libbfd demangling in symbol.c, which referred to identifiers nothing declared. Add the three. The clang ones need clang installed, and the libbfd ones reuse the existing binutils check. Testing that perf was built isn't enough for the first one. Feature tests are allowed to fail, so a broken C++ compiler command line doesn't fail the build, it silently turns off every feature needing a C++ compiler and the build still produces a working perf. Check that libLLVM is still built in, which is the C++ dependent feature perf can report on, so the test fails instead of quietly producing a perf with less in it than the default build has. Only make that check when libLLVM can be built in the first place. The llvm-perf feature test compiles and links against the llvm-devel/llvm-dev headers and libraries and needs version 13 or newer, so the presence of llvm-config on its own says nothing. Probe the same way the feature test does and fall back to only testing that perf was built when the probe fails, rather than turning a machine without the LLVM development files into a spurious failure. For the same reason the clang tests must not reuse a feature dump made with the default compiler, so exclude them from REUSE_FEATURES_DUMP. Signed-off-by: Ian Rogers Assisted-by: Antigravity:gemini-3.1-pro Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/make | 47 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/tools/perf/tests/make b/tools/perf/tests/make index 608470ff9c10..8e55ff1aecbd 100644 --- a/tools/perf/tests/make +++ b/tools/perf/tests/make @@ -71,6 +71,9 @@ make_clean_all := clean all make_python_perf_so := $(python_perf_so) make_debug := DEBUG=1 make_nondistro := BUILD_NONDISTRO=1 +make_nondistro_no_demangle := BUILD_NONDISTRO=1 NO_DEMANGLE=1 +make_clang := CC=clang +make_clang_nondistro := CC=clang BUILD_NONDISTRO=1 make_extra_tests := EXTRA_TESTS=1 make_no_jevents := NO_JEVENTS=1 make_jevents_all := JEVENTS_ARCH=all @@ -128,6 +131,18 @@ make_minimal += NO_CAPSTONE=1 # binutils 2_42 and newer have bfd_thread_init() new_libbfd := $(shell echo '#include ' | $(CC) -E -x c - | grep bfd_thread_init) +# Whether libLLVM can be built, which needs the llvm-devel/llvm-dev headers and +# libraries of version 13 or newer, not just llvm-config. This mirrors the +# llvm-perf feature test in tools/build/feature/test-llvm-perf.cpp so that +# libLLVM is only asserted below when the default compiler can really build it. +ifneq ($(call has,$(LLVM_CONFIG)),) +have_libllvm := $(shell printf '#include \n#include \n#if LLVM_VERSION_MAJOR < 13\n#error "llvm-devel/llvm-dev version 13 or greater is required"\n#endif\nint main(){llvm::errs()<<"";llvm::llvm_shutdown();return 0;}\n' | \ + $(CXX) -x c++ -std=gnu++17 -I$(shell $(LLVM_CONFIG) --includedir 2>/dev/null) - -o /dev/null \ + -L$(shell $(LLVM_CONFIG) --libdir 2>/dev/null) \ + $(shell $(LLVM_CONFIG) --libs Core BPF 2>/dev/null) \ + $(shell $(LLVM_CONFIG) --system-libs 2>/dev/null) >/dev/null 2>&1 && echo y) +endif + # $(run) contains all available tests run := make_pure # Targets 'clean all' can be run together only through top level @@ -143,6 +158,17 @@ run += make_python_perf_so run += make_debug ifneq ($(new_libbfd),) run += make_nondistro +# Demangling with libbfd is only built when the C++ ABI's __cxa_demangle +# isn't available, so it needs a build of its own to be compiled at all. +run += make_nondistro_no_demangle +endif +# CXX is only set to clang++ by LLVM=1, so a CC=clang build has to cope with +# a C compiler and a C++ compiler that don't match. +ifneq ($(call has,clang),) +run += make_clang +ifneq ($(new_libbfd),) +run += make_clang_nondistro +endif endif run += make_extra_tests run += make_no_jevents @@ -278,6 +304,17 @@ test_make_install_pdf_O := $(test_ok) test_make_libbpf_dynamic := ldd $(PERF_O)/perf | grep -q libbpf test_make_libbpf_dynamic_O := ldd $$TMP_O/perf | grep -q libbpf +# Feature tests are allowed to fail, so a broken C++ compiler command line +# doesn't fail the build, it just quietly turns off everything that needs a +# C++ compiler. Check a feature that does, otherwise these tests would pass +# while producing a perf with less in it than the default build has. +ifneq ($(have_libllvm),) +test_make_clang := test -x $(PERF_O)/perf && $(PERF_O)/perf check -q feature libLLVM +test_make_clang_O := test -x $$TMP_O/perf && $$TMP_O/perf check -q feature libLLVM +test_make_clang_nondistro := $(test_make_clang) +test_make_clang_nondistro_O := $(test_make_clang_O) +endif + test_make_python_perf_so_O := test -f $$TMP_O/python/perf.so test_make_perf_o_O := test -f $$TMP_O/perf.o test_make_util_map_o_O := test -f $$TMP_O/util/map.o @@ -407,8 +444,14 @@ $(FEATURES_DUMP_FILE_STATIC): echo "- $@: $$cmd" && echo $$cmd && \ ( eval $$cmd ) > /dev/null 2>&1 +# The clang tests exist to run feature detection with a compiler other than +# the default one, so they have to do their own and are left out of both the +# dependency and the 'FEATURES_DUMP=' append below. +no_features_dump := make_clang make_clang_nondistro +no_features_dump += $(addsuffix _O,$(no_features_dump)) + # Add feature dump dependency for run/run_O targets -$(foreach t,$(run) $(run_O),$(eval \ +$(foreach t,$(filter-out $(no_features_dump),$(run) $(run_O)),$(eval \ $(t): $(if $(findstring make_static,$(t)),\ $(FEATURES_DUMP_FILE_STATIC),\ $(FEATURES_DUMP_FILE)))) @@ -416,7 +459,7 @@ $(foreach t,$(run) $(run_O),$(eval \ # Append 'FEATURES_DUMP=' option to all test cases. For example: # make_no_libbpf: NO_LIBBPF=1 --> NO_LIBBPF=1 FEATURES_DUMP=/a/b/BUILD_TEST_FEATURE_DUMP # make_static: LDFLAGS=-static --> LDFLAGS=-static FEATURES_DUMP=/a/b/BUILD_TEST_FEATURE_DUMP_STATIC -$(foreach t,$(run),$(if $(findstring make_static,$(t)),\ +$(foreach t,$(filter-out $(no_features_dump),$(run)),$(if $(findstring make_static,$(t)),\ $(eval $(t) := $($(t)) FEATURES_DUMP=$(FEATURES_DUMP_FILE_STATIC)),\ $(eval $(t) := $($(t)) FEATURES_DUMP=$(FEATURES_DUMP_FILE)))) endif From 67b2c43c0c17b58296c5a832ccd2a3556961f53a Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Tue, 15 Sep 2026 17:11:11 +0800 Subject: [PATCH 121/425] perf aslr: Add missing SIMD handling in aslr_tool__process_sample() Currently the SIMD register sampling is supported, but the corresponding handling is missed in aslr_tool__process_sample(). This can break parsing of the sample record. Add the missing SIMD register handling in aslr_tool__process_sample(). Reviewed-by: Ian Rogers Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/aslr.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tools/perf/util/aslr.c b/tools/perf/util/aslr.c index 027695d96779..a097084201a0 100644 --- a/tools/perf/util/aslr.c +++ b/tools/perf/util/aslr.c @@ -19,6 +19,7 @@ #include /* page_size */ #include #include +#include #include #include #include @@ -992,6 +993,34 @@ static int aslr_tool__process_sample(const struct perf_tool *tool, COPY_U64(); } } + +#define CHECK_SIMD() \ + do { \ + if (abi & PERF_SAMPLE_REGS_ABI_SIMD) { \ + u64 nr_vector, vec_qwords; \ + u64 nr_pred, pred_qwords; \ + u64 header_len, vec_len, pred_len, simd_len; \ + if (CHECK_BOUNDS(nr + 4, nr + 4)) { \ + ret = -EFAULT; \ + goto out_put; \ + } \ + header_len = 4; \ + nr_vector = in_array[i + nr]; \ + vec_qwords = in_array[i + nr + 1]; \ + nr_pred = in_array[i + nr + 2]; \ + pred_qwords = in_array[i + nr + 3]; \ + if (check_mul_overflow(nr_vector, vec_qwords, &vec_len) || \ + check_mul_overflow(nr_pred, pred_qwords, &pred_len) || \ + check_add_overflow(vec_len, pred_len, &simd_len) || \ + check_add_overflow(header_len, simd_len, &simd_len) || \ + check_add_overflow(nr, simd_len, &nr) || \ + nr > max_i - i) { \ + ret = -EFAULT; \ + goto out_put; \ + } \ + } \ + } while (0) + if (orig_sample_type & PERF_SAMPLE_REGS_USER) { u64 abi; @@ -1007,6 +1036,7 @@ static int aslr_tool__process_sample(const struct perf_tool *tool, ret = -EFAULT; goto out_put; } + CHECK_SIMD(); i += nr; } } @@ -1061,6 +1091,7 @@ static int aslr_tool__process_sample(const struct perf_tool *tool, ret = -EFAULT; goto out_put; } + CHECK_SIMD(); i += nr; } } From 4cc2f9c885ab629128e4071c32d7b62d0d6b6001 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Tue, 15 Sep 2026 17:11:12 +0800 Subject: [PATCH 122/425] perf util: Explicitly include required headers Sashiko complains "the perf tool can be compiled with musl libc, which enforces strict POSIX namespace separation and does not implicitly include headers. Relying on implicit header inclusion can cause build failures under musl." Include the required headers explicitly to avoid such failures. Reviewed-by: Ian Rogers Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/parse-regs-options.c | 1 + tools/perf/util/perf-regs-arch/perf_regs_x86.c | 2 ++ tools/perf/util/session.c | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/tools/perf/util/parse-regs-options.c b/tools/perf/util/parse-regs-options.c index 709fcfb077a2..e70393388fae 100644 --- a/tools/perf/util/parse-regs-options.c +++ b/tools/perf/util/parse-regs-options.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "util/debug.h" #include diff --git a/tools/perf/util/perf-regs-arch/perf_regs_x86.c b/tools/perf/util/perf-regs-arch/perf_regs_x86.c index fb6cf34f42f2..ea1d402b0893 100644 --- a/tools/perf/util/perf-regs-arch/perf_regs_x86.c +++ b/tools/perf/util/perf-regs-arch/perf_regs_x86.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include diff --git a/tools/perf/util/session.c b/tools/perf/util/session.c index a5b596cd14be..04fba6fb02e8 100644 --- a/tools/perf/util/session.c +++ b/tools/perf/util/session.c @@ -1,4 +1,9 @@ // SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include +#include #include #include #include From 9db4e9d6cc7c7d318fb6d6bb73abe3fed27385a8 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Wed, 16 Sep 2026 08:47:34 -0300 Subject: [PATCH 123/425] perf annotate-data: Show the sample count in the data-type browser The data-type browser has a samples view, selected with -n (or with annotate.show_nr_samples), in which browser__write_overhead() prints a local nr_samples variable that is initialized to zero and never updated, so every member is listed as having no samples while the period and percent columns for the same entry are filled in. Print the histogram entry's own count instead. This predates the load/store counter split, so fix it ahead of that patch: the split then only has to adapt a line that is already correct, and this fix can be picked on its own. Fixes: d001c7a7f4736743 ("perf annotate-data: Add hist_entry__annotate_data_tui()") Acked-by: Namhyung Kim Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/ui/browsers/annotate-data.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/ui/browsers/annotate-data.c b/tools/perf/ui/browsers/annotate-data.c index c6e07a9b6408..a15608f6ee53 100644 --- a/tools/perf/ui/browsers/annotate-data.c +++ b/tools/perf/ui/browsers/annotate-data.c @@ -374,7 +374,7 @@ static void browser__write_overhead(struct ui_browser *uib, u64 period = hist->period; double percent = total->period ? (100.0 * period / total->period) : 0; bool current = ui_browser__is_current_entry(uib, row); - int nr_samples = 0; + int nr_samples = hist->nr_samples; ui_browser__set_percent_color(uib, percent, current); From 1b9822a29782b9a49937b0ebcc0f2981c4508ee9 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 18 Sep 2026 14:19:15 -0700 Subject: [PATCH 124/425] perf trace: Include the headers declaring pid_t, strcmp and assert trace_augment.h uses pid_t in the augmented_syscalls__set_filter_pids() prototype and in the !HAVE_BPF_SKEL stub. bpf_trace_augment.c calls strcmp() in augmented_syscalls__find_by_title() and assert() in augmented_syscalls__create_bpf_output(), but neither pulls in the header that declares what it uses. Both happen to build today only because something else in the include chain drags , and in first, which is not guaranteed and does not hold on libcs such as musl that keep the POSIX namespaces strictly separated. Include , and explicitly. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/bpf_trace_augment.c | 2 ++ tools/perf/util/trace_augment.h | 1 + 2 files changed, 3 insertions(+) diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index a9cf2a77ded1..ebb26225fb04 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,5 +1,7 @@ +#include #include #include +#include #include "bpf_skel/augmented_raw_syscalls.skel.h" #include "debug.h" diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 4f729bc67753..a1cd9a5e0213 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -2,6 +2,7 @@ #define TRACE_AUGMENT_H #include +#include struct bpf_program; struct evlist; From 1865b4164b515fdbf2cfb7916b942bf335b35b1e Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 18 Sep 2026 14:19:16 -0700 Subject: [PATCH 125/425] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Every evsel->priv in builtin-trace.c is a struct evsel_trace, allocated by evsel_trace__new(). It holds a syscall_arg_fmt array in its fmt member, which evsel__syscall_arg_fmt() allocates on demand for the syscalls:sys_{enter,exit}_SYSCALL tracepoints and for every other tracepoint that gets its arguments pretty printed. evsel__put_and_free_priv() only did zfree(&evsel->priv), releasing the evsel_trace itself and leaking that array. Use evsel_trace__delete(), which frees fmt first, exactly as the out_delete path of evsel__syscall_arg_fmt() already does. The current callers are all error paths that run before fmt can have been allocated, so nothing leaks in practice today, but the helper is the obvious thing to reach for whenever an evsel is discarded and it should be safe for that. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-trace.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 20fffc24507b..f67557e7a254 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -464,7 +464,13 @@ static int evsel__init_tp_ptr_field(struct evsel *evsel, struct tp_field *field, static void evsel__put_and_free_priv(struct evsel *evsel) { - zfree(&evsel->priv); + /* + * evsel->priv is always a struct evsel_trace here, so it has to go + * through evsel_trace__delete(): zfree() on its own would release the + * struct while leaking the syscall_arg_fmt array hanging off it. + */ + evsel_trace__delete(evsel->priv); + evsel->priv = NULL; evsel__put(evsel); } From edd8a9fe2eca009599e013a29c421c7a6b5ad1b9 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 18 Sep 2026 14:19:17 -0700 Subject: [PATCH 126/425] perf evsel: Report an allocation failure as ENOMEM when setting filters evsel__set_filter() and evsel__append_filter() return -1 when the strdup() or asprintf() that builds the new filter string fails, and evlist__set_tp_filter() and evlist__append_tp_filter() do the same when handed the NULL that asprintf__tp_filter_pids() returns for the same reason. In every case the only thing that can have gone wrong is an allocation. Callers all test the result with "< 0" or for being non-zero, so -1 has been as good as any other error so far, but it is not an errno and so cannot be printed as one. A caller that does, such as str_error_r(-err, errbuf, sizeof(errbuf)) turns it into EPERM and reports a failure to allocate as "Operation not permitted", which is no help to anyone trying to work out what happened. Return -ENOMEM instead, which is what these failures are. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/evlist.c | 12 ++++++++++-- tools/perf/util/evsel.c | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tools/perf/util/evlist.c b/tools/perf/util/evlist.c index 22eb01116992..9392d912d254 100644 --- a/tools/perf/util/evlist.c +++ b/tools/perf/util/evlist.c @@ -1432,8 +1432,12 @@ int evlist__set_tp_filter(struct evlist *evlist, const char *filter) struct evsel *evsel; int err = 0; + /* + * The only caller that passes NULL is evlist__set_tp_filter_pids(), + * where it means asprintf__tp_filter_pids() failed to allocate. + */ if (filter == NULL) - return -1; + return -ENOMEM; evlist__for_each_entry(evlist, evsel) { if (evsel->core.attr.type != PERF_TYPE_TRACEPOINT) @@ -1452,8 +1456,12 @@ int evlist__append_tp_filter(struct evlist *evlist, const char *filter) struct evsel *evsel; int err = 0; + /* + * As above, a NULL filter is asprintf__tp_filter_pids() having failed + * to allocate in evlist__append_tp_filter_pids(). + */ if (filter == NULL) - return -1; + return -ENOMEM; evlist__for_each_entry(evlist, evsel) { if (evsel->core.attr.type != PERF_TYPE_TRACEPOINT) diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index 0d0b2804d238..dd468716f424 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -1872,7 +1872,7 @@ int evsel__set_filter(struct evsel *evsel, const char *filter) return 0; } - return -1; + return -ENOMEM; } static int evsel__append_filter(struct evsel *evsel, const char *fmt, const char *filter) @@ -1888,7 +1888,7 @@ static int evsel__append_filter(struct evsel *evsel, const char *fmt, const char return 0; } - return -1; + return -ENOMEM; } int evsel__append_tp_filter(struct evsel *evsel, const char *filter) From d8fd56321d7bb5df31cdf33e9acbd62f4606b483 Mon Sep 17 00:00:00 2001 From: Stian Halseth Date: Wed, 23 Sep 2026 21:34:32 +0200 Subject: [PATCH 127/425] perf unwind-libdw: Fix reading the stack of a 32-bit task memory_read() hands libdw 8 bytes whatever the sampled task's ABI. For a 32-bit task libdw keeps the low 32 bits (__libdwfl_frame_reg_set()), so on a big-endian machine each word read from the stack or a DSO is the one at addr + 4, and unwinding stops at the first frame restored from the stack. On sparc the 8-byte load from a stack address that is only 4-byte aligned even traps, and perf dies with SIGBUS. Read 4 bytes for a PERF_SAMPLE_REGS_ABI_32 sample. A recording of the other byte order has each sample swapped in 8-byte units when it is read (perf_event__all64_swap()), which leaves the 4-byte words of a 32-bit stack dump reversed and paired up wrongly. Undo the swap of the unit holding the word and swap the word itself, as __evsel__parse_sample() does for PERF_SAMPLE_CPU. Suggested-by: Ian Rogers Reviewed-by: Ian Rogers Signed-off-by: Stian Halseth Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/unwind-libdw.c | 46 ++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/tools/perf/util/unwind-libdw.c b/tools/perf/util/unwind-libdw.c index 4ccfcc7c2dfc..d49901b1571e 100644 --- a/tools/perf/util/unwind-libdw.c +++ b/tools/perf/util/unwind-libdw.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "debug.h" #include "dso.h" @@ -17,6 +18,8 @@ #include #include #include "event.h" +#include "evsel.h" +#include "memswap.h" #include "perf_regs.h" #include "callchain.h" #include "util/env.h" @@ -203,9 +206,10 @@ static bool get_thread(Dwfl *dwfl __maybe_unused, pid_t tid, void *arg, } static int access_dso_mem(struct unwind_info *ui, Dwarf_Addr addr, - Dwarf_Word *data) + Dwarf_Word *data, size_t len) { struct addr_location al; + union u64_swap u; ssize_t size; struct dso *dso; @@ -218,15 +222,38 @@ static int access_dso_mem(struct unwind_info *ui, Dwarf_Addr addr, if (!dso) goto out_fail; - size = dso__data_read_addr(dso, al.map, ui->machine, addr, (u8 *) data, sizeof(*data)); + size = dso__data_read_addr(dso, al.map, ui->machine, addr, (u8 *)&u, len); addr_location__exit(&al); - return !(size == sizeof(*data)); + if (size != (ssize_t)len) + return 1; + *data = len == sizeof(u32) ? u.val32[0] : u.val64; + return 0; out_fail: addr_location__exit(&al); return -1; } +/* + * libdw expects a 32-bit task's words zero-extended. A recording of the + * other byte order was swapped in 8-byte units when it was read (see + * perf_event__all64_swap()), so a 4-byte word is picked out of its unit with + * that swap undone, as for PERF_SAMPLE_CPU in __evsel__parse_sample(). + */ +static Dwarf_Word stack_word(struct stack_dump *stack, int offset, size_t len, + bool swapped) +{ + union u64_swap u; + + if (len == sizeof(u64)) + return *(Dwarf_Word *)&stack->data[offset]; + if (!swapped) + return *(u32 *)&stack->data[offset]; + + u.val64 = bswap_64(*(u64 *)&stack->data[offset & ~7]); + return bswap_32(u.val32[(offset & 4) / 4]); +} + static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word *result, void *arg) { @@ -234,11 +261,16 @@ static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word * struct unwind_info *ui = dwfl_ui_ti->ui; struct stack_dump *stack = &ui->sample->user_stack; u64 start, end; + bool swapped; + size_t len; int offset; int ret; if (!ui->sample->user_regs) return false; + len = ui->sample->user_regs->abi == PERF_SAMPLE_REGS_ABI_32 ? + sizeof(u32) : sizeof(u64); + swapped = ui->sample->evsel && ui->sample->evsel->needs_swap; ret = perf_reg_value(&start, ui->sample->user_regs, perf_arch_reg_sp(ui->e_machine)); @@ -248,11 +280,11 @@ static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word * end = start + stack->size; /* Check overflow. */ - if (addr + sizeof(Dwarf_Word) < addr) + if (addr + len < addr) return false; - if (addr < start || addr + sizeof(Dwarf_Word) > end) { - ret = access_dso_mem(ui, addr, result); + if (addr < start || addr + len > end) { + ret = access_dso_mem(ui, addr, result, len); if (ret) { pr_debug("unwind: access_mem 0x%" PRIx64 " not inside range" " 0x%" PRIx64 "-0x%" PRIx64 "\n", @@ -263,7 +295,7 @@ static bool memory_read(Dwfl *dwfl __maybe_unused, Dwarf_Addr addr, Dwarf_Word * } offset = addr - start; - *result = *(Dwarf_Word *)&stack->data[offset]; + *result = stack_word(stack, offset, len, swapped); pr_debug("unwind: access_mem addr 0x%" PRIx64 ", val %lx, offset %d\n", addr, (unsigned long)*result, offset); return true; From ed1293302f811fa709897d4a90ad78592938e4aa Mon Sep 17 00:00:00 2001 From: Tiezhu Yang Date: Thu, 24 Sep 2026 10:17:56 +0800 Subject: [PATCH 128/425] perf loongarch: Fix discarded const qualifier in _get_field() When building perf on LoongArch, the following compilation errors occur: CC arch/loongarch/util/header.o arch/loongarch/util/header.c: In function '_get_field': arch/loongarch/util/header.c:31:15: error: assignment discards 'const' qualifier from pointer target type [-Werror=discarded-qualifiers] 31 | line2 = strrchr(line, ' '); | ^ arch/loongarch/util/header.c:36:12: error: assignment discards 'const' qualifier from pointer target type [-Werror=discarded-qualifiers] 36 | nl = strrchr(line, '\n'); | ^ cc1: all warnings being treated as errors Declare 'line2' and 'nl' as const char * to ensure type safety. In modern glibc versions, strrchr() returns a 'const char *' if the input string is const. This is similar to commit 7378b6656aa4 ("perf riscv: Fix discarded const qualifier in _get_field()"). Fixes: 492ac37fa38faf52 ("perf kvm: Add kvm-stat for loongarch64") Signed-off-by: Tiezhu Yang Acked-by: Huacai Chen Acked-by: Namhyung Kim Cc: Bibo Mao Cc: Huacai Chen Cc: stable@vger.kernel.org Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/arch/loongarch/util/header.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/arch/loongarch/util/header.c b/tools/perf/arch/loongarch/util/header.c index 0c6d823334a2..46f53a74f71e 100644 --- a/tools/perf/arch/loongarch/util/header.c +++ b/tools/perf/arch/loongarch/util/header.c @@ -26,7 +26,7 @@ static char *_get_field(const char *line) { - char *line2, *nl; + const char *line2, *nl; line2 = strrchr(line, ' '); if (!line2) From 06d5ce78e4fea2cf0738da903aa248a81101c155 Mon Sep 17 00:00:00 2001 From: Thomas Falcon Date: Wed, 23 Sep 2026 12:08:58 -0500 Subject: [PATCH 129/425] perf mem: Fix size tracking for mem_lvl's in perf_script__meminfo_scnprintf() When printing memory info in perf script, the entire size of the buffer is passed to perf_mem__lvl_scnprintf() instead of the remaining size. Pass the remaining buffer size instead. This issue was detected by sashiko during an internal code review. Fixes: fdefc3750e847a91 ("perf mem: Print memory operation type") Reviewed-by: Dapeng Mi Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Cc: Leo Yan Assisted-by: Sashiko:gemini-3.1-pro-preview Signed-off-by: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/mem-events.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/util/mem-events.c b/tools/perf/util/mem-events.c index 0b49fce251fc..4e490f9cd348 100644 --- a/tools/perf/util/mem-events.c +++ b/tools/perf/util/mem-events.c @@ -610,7 +610,7 @@ int perf_script__meminfo_scnprintf(char *out, size_t sz, const struct mem_info * i += scnprintf(out, sz, "|OP "); i += perf_mem__op_scnprintf(out + i, sz - i, mem_info); i += scnprintf(out + i, sz - i, "|LVL "); - i += perf_mem__lvl_scnprintf(out + i, sz, mem_info); + i += perf_mem__lvl_scnprintf(out + i, sz - i, mem_info); i += scnprintf(out + i, sz - i, "|SNP "); i += perf_mem__snp_scnprintf(out + i, sz - i, mem_info); i += scnprintf(out + i, sz - i, "|TLB "); From f6659329a4cd3a0d335e3830426f10dd440afd2b Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Wed, 23 Sep 2026 12:08:59 -0500 Subject: [PATCH 130/425] perf mem: Add support for printing PERF_MEM_LVLNUM_L0 Add support for printing PERF_MEM_LVLNUM_L0 in perf mem report. The L0 cache is newly added a small piece of cache which is the closest, lowest-latency memory cache tied directly to the execution pipeline. The table "Table 9-4. Data Source Field Encodings for Panther Cove and Coyote Cove Microarchitectures" in the ISE doc chapter "9.2.1 Panther Cove and Coyote Cove Microarchitectures Memory Auxiliary Field Layout"[1] indicates the code "01H" means the "L0 Hit - Minimal latency core cache hit. This request was satisfied by the L0 data cache." [1]: https://www.intel.com/content/www/us/en/content-details/922690/intel-architecture-instruction-set-extensions-programming-reference.html Assisted-by: Sashiko:gemini-3.1-pro-preview Reviewed-by: Dapeng Mi Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Signed-off-by: Dapeng Mi Co-developed-by: Thomas Falcon Signed-off-by: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-record.txt | 2 +- tools/perf/util/bpf-filter.l | 1 + tools/perf/util/mem-events.c | 5 +++++ tools/perf/util/mem-events.h | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/perf/Documentation/perf-record.txt b/tools/perf/Documentation/perf-record.txt index 388455f9b07b..bd2c56d8cdc0 100644 --- a/tools/perf/Documentation/perf-record.txt +++ b/tools/perf/Documentation/perf-record.txt @@ -212,7 +212,7 @@ OPTIONS The can be one of: (for any term) na, load, store, pfetch, exec (for mem_op) - l1, l2, l3, l4, cxl, io, any_cache, lfb, ram, pmem (for mem_lvl) + l0, l1, l2, l3, l4, cxl, io, any_cache, lfb, ram, pmem (for mem_lvl) na, none, hit, miss, hitm, fwd, peer (for mem_snoop) remote (for mem_remote) na, locked (for mem_locked) diff --git a/tools/perf/util/bpf-filter.l b/tools/perf/util/bpf-filter.l index 6aa65ade3385..1be9df6550fc 100644 --- a/tools/perf/util/bpf-filter.l +++ b/tools/perf/util/bpf-filter.l @@ -131,6 +131,7 @@ store { return constant(PERF_MEM_OP_STORE); } pfetch { return constant(PERF_MEM_OP_PFETCH); } exec { return constant(PERF_MEM_OP_EXEC); } +l0 { return constant(PERF_MEM_LVLNUM_L0); } l1 { return constant(PERF_MEM_LVLNUM_L1); } l2 { return constant(PERF_MEM_LVLNUM_L2); } l3 { return constant(PERF_MEM_LVLNUM_L3); } diff --git a/tools/perf/util/mem-events.c b/tools/perf/util/mem-events.c index 4e490f9cd348..4fd48fd20055 100644 --- a/tools/perf/util/mem-events.c +++ b/tools/perf/util/mem-events.c @@ -391,6 +391,7 @@ static const char * const mem_lvlnum[] = { [PERF_MEM_LVLNUM_L4] = "L4", [PERF_MEM_LVLNUM_L2_MHB] = "L2 MHB", [PERF_MEM_LVLNUM_MSC] = "Memory-side Cache", + [PERF_MEM_LVLNUM_L0] = "L0", [PERF_MEM_LVLNUM_UNC] = "Uncached", [PERF_MEM_LVLNUM_CXL] = "CXL", [PERF_MEM_LVLNUM_IO] = "I/O", @@ -831,6 +832,8 @@ int mem_stat_index(const enum mem_stat_type mst, const u64 val) } case PERF_MEM_STAT_CACHE: switch (src.mem_lvl_num) { + case PERF_MEM_LVLNUM_L0: + return MEM_STAT_CACHE_L0; case PERF_MEM_LVLNUM_L1: return MEM_STAT_CACHE_L1; case PERF_MEM_LVLNUM_L2: @@ -915,6 +918,8 @@ const char *mem_stat_name(const enum mem_stat_type mst, const int idx) } case PERF_MEM_STAT_CACHE: switch (idx) { + case MEM_STAT_CACHE_L0: + return "L0"; case MEM_STAT_CACHE_L1: return "L1"; case MEM_STAT_CACHE_L2: diff --git a/tools/perf/util/mem-events.h b/tools/perf/util/mem-events.h index 5b98076904b0..daa22748f9fe 100644 --- a/tools/perf/util/mem-events.h +++ b/tools/perf/util/mem-events.h @@ -109,6 +109,7 @@ enum mem_stat_op { }; enum mem_stat_cache { + MEM_STAT_CACHE_L0, MEM_STAT_CACHE_L1, MEM_STAT_CACHE_L2, MEM_STAT_CACHE_L3, From 6cdecad561fffb7c44241ad35a8bdb6a0ed30b5b Mon Sep 17 00:00:00 2001 From: Thomas Falcon Date: Wed, 23 Sep 2026 12:09:00 -0500 Subject: [PATCH 131/425] perf header: Support memory ranges Memory ranges were created to track different types of memory, such as persistent, high bandwidth, or CXL-attached, that may be present on a system for performance monitoring and resource control purposes. Memory range data are parsed from the ACPI MRRM table and exposed to userspace tools via sysfs [1]. Memory range data is read from: /sys/firmware/acpi/memory_ranges/rangeX With the following attributes: u64 base; u64 length; int node; u8 local_region_id; u8 remote_region_id; Read memory range data from sysfs if present and save it in the header of the perf data file under a new feature bit, HEADER_MEMORY_RANGES (35). Memory range data can be viewed with the --header or --header-only options of perf-report and perf-script. Example output: # memory ranges (nr 5): # range0: [0x0000000000000000-0x00000000bfffffff], node = 0, local_region_id = 0, remote_region_id = 255 # range1: [0x0000000100000000-0x000000203fffffff], node = 0, local_region_id = 0, remote_region_id = 255 # range2: [0x0000008000000000-0x0000027fffffffff], node = -2, local_region_id = 1, remote_region_id = 255 # range3: [0x0000028000000000-0x0000047fffffffff], node = -2, local_region_id = 1, remote_region_id = 255 # range4: [0x0000048000000000-0x000004ffffffffff], node = -2, local_region_id = 1, remote_region_id = 255 [1]: https://lore.kernel.org/lkml/20250505173819.419271-1-tony.luck@intel.com/ Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Assisted-by: GitHub-Copilot:claude-opus-4-8 Assisted-by: Sashiko:gemini-3.1-pro-preview Signed-off-by: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- .../Documentation/perf.data-file-format.txt | 13 ++ tools/perf/builtin-inject.c | 1 + tools/perf/util/env.c | 1 + tools/perf/util/env.h | 10 + tools/perf/util/header.c | 205 ++++++++++++++++++ tools/perf/util/header.h | 1 + 6 files changed, 231 insertions(+) diff --git a/tools/perf/Documentation/perf.data-file-format.txt b/tools/perf/Documentation/perf.data-file-format.txt index b90cba9168f8..1f8e7d8e9ae2 100644 --- a/tools/perf/Documentation/perf.data-file-format.txt +++ b/tools/perf/Documentation/perf.data-file-format.txt @@ -478,6 +478,19 @@ The size of the cacheline in bytes. Format: unsigned int cln_size; + HEADER_MEMORY_RANGES = 35, + +List of memory ranges. The format of the data is as below. + +u32 nr_memory_ranges; +struct memory_range { + u64 base; + u64 length; + int node; + u8 local_region_id; + u8 remote_region_id; +}; [nr_memory_ranges] + other bits are reserved and should be ignored for now HEADER_FEAT_BITS = 256, diff --git a/tools/perf/builtin-inject.c b/tools/perf/builtin-inject.c index 70bbfad5653e..169617a884d3 100644 --- a/tools/perf/builtin-inject.c +++ b/tools/perf/builtin-inject.c @@ -2306,6 +2306,7 @@ static bool keep_feat(struct perf_inject *inject, int feat) case HEADER_PMU_CAPS: case HEADER_CPU_DOMAIN_INFO: case HEADER_CLN_SIZE: + case HEADER_MEMORY_RANGES: return true; /* Information that can be updated */ case HEADER_BUILD_ID: diff --git a/tools/perf/util/env.c b/tools/perf/util/env.c index c0e2b9d5f0b2..50993b0dedc5 100644 --- a/tools/perf/util/env.c +++ b/tools/perf/util/env.c @@ -297,6 +297,7 @@ void perf_env__exit(struct perf_env *env) zfree(&env->pmu_caps[i].pmu_name); } zfree(&env->pmu_caps); + zfree(&env->memory_ranges); free_cpu_domain_info(env->cpu_domain, env->schedstat_version, env->nr_cpus_avail); } diff --git a/tools/perf/util/env.h b/tools/perf/util/env.h index 7acca39b42ff..23d35425efe3 100644 --- a/tools/perf/util/env.h +++ b/tools/perf/util/env.h @@ -68,6 +68,14 @@ struct cpu_domain_map { struct domain_info **domains; }; +struct memory_range { + u64 base; + u64 length; + int node; + u8 local_region_id; + u8 remote_region_id; +}; + struct perf_env { char *hostname; char *os_release; @@ -100,6 +108,7 @@ struct perf_env { int nr_cpu_pmu_caps; int nr_hybrid_nodes; int nr_pmus_with_caps; + int nr_memory_ranges; char *cmdline; const char **cmdline_argv; char *sibling_cores; @@ -122,6 +131,7 @@ struct perf_env { unsigned long long memory_bsize; struct hybrid_node *hybrid_nodes; struct pmu_caps *pmu_caps; + struct memory_range *memory_ranges; #ifdef HAVE_LIBBPF_SUPPORT /* * bpf_info_lock protects bpf rbtrees. This is needed because the diff --git a/tools/perf/util/header.c b/tools/perf/util/header.c index ec2935c0ed04..9eec6deee190 100644 --- a/tools/perf/util/header.c +++ b/tools/perf/util/header.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -95,6 +96,7 @@ #define MAX_PMU_CAPS 512 #define MAX_PMU_MAPPINGS 4096 #define MAX_SCHED_DOMAINS 64 +#define MAX_MEMORY_RANGES 256 /* * magic2 = "PERFILE2" @@ -1914,6 +1916,132 @@ static int write_cpu_domain_info(struct feat_fd *ff, return ret; } +static int memory_range__read(struct memory_range *range, const char *path) +{ + char buf[32]; + ssize_t n; + int dfd, fd, tmp, ret = -1; + + dfd = open(path, O_RDONLY | O_DIRECTORY); + if (dfd < 0) + return -1; + +#define _R(name, dst, conv) \ + fd = openat(dfd, name, O_RDONLY); \ + if (fd < 0) \ + goto out; \ + n = read(fd, buf, sizeof(buf) - 1); \ + close(fd); \ + if (n <= 0) \ + goto out; \ + buf[n] = '\0'; \ + dst = conv(buf, NULL, 0); + + _R("base", range->base, strtoull); + _R("length", range->length, strtoull); + _R("node", range->node, strtol); + _R("local_region_id", tmp, strtol); + if (tmp < 0 || tmp > UINT8_MAX) + goto out; + range->local_region_id = tmp; + _R("remote_region_id", tmp, strtol); + if (tmp < 0 || tmp > UINT8_MAX) + goto out; + range->remote_region_id = tmp; +#undef _R + + ret = 0; +out: + close(dfd); + return ret; +} + +static int memory_range__parse(struct memory_range **ranges) +{ + const char *sysfs = sysfs__mountpoint(); + int i, err, nr_memory_ranges = 0; + char path[PATH_MAX]; + struct stat st; + + if (!sysfs) + return 0; + + scnprintf(path, PATH_MAX, "%s/firmware/acpi/memory_ranges", sysfs); + if (stat(path, &st)) + return 0; + + while (1) { + scnprintf(path, PATH_MAX, + "%s/firmware/acpi/memory_ranges/range%d", + sysfs, nr_memory_ranges); + if (stat(path, &st)) + break; + + nr_memory_ranges++; + } + + if (nr_memory_ranges == 0) + return 0; + + *ranges = calloc(nr_memory_ranges, sizeof(struct memory_range)); + if (!(*ranges)) + return -ENOMEM; + + for (i = 0; i < nr_memory_ranges; i++) { + struct memory_range range; + + scnprintf(path, PATH_MAX, + "%s/firmware/acpi/memory_ranges/range%d", sysfs, i); + err = memory_range__read(&range, path); + if (err < 0) + goto out_error; + + (*ranges)[i] = range; + } + + return nr_memory_ranges; + +out_error: + zfree(ranges); + return -1; +} + +static int write_memory_ranges(struct feat_fd *ff, + struct evlist *evlist __maybe_unused) +{ + struct memory_range *ranges = NULL; + int nr_memory_ranges = 0, ret; + + nr_memory_ranges = memory_range__parse(&ranges); + if (nr_memory_ranges < 0) + return nr_memory_ranges; + + ret = do_write(ff, &nr_memory_ranges, sizeof(nr_memory_ranges)); + if (ret < 0) + goto out; + + for (int i = 0; i < nr_memory_ranges; i++) { + ret = do_write(ff, &ranges[i].base, sizeof(u64)); + if (ret < 0) + goto out; + ret = do_write(ff, &ranges[i].length, sizeof(u64)); + if (ret < 0) + goto out; + ret = do_write(ff, &ranges[i].node, sizeof(u32)); + if (ret < 0) + goto out; + ret = do_write(ff, &ranges[i].local_region_id, sizeof(u8)); + if (ret < 0) + goto out; + ret = do_write(ff, &ranges[i].remote_region_id, sizeof(u8)); + if (ret < 0) + goto out; + } +out: + zfree(&ranges); + return ret; +} + static void print_hostname(struct feat_fd *ff, FILE *fp) { fprintf(fp, "# hostname : %s\n", ff->ph->env.hostname); @@ -2652,6 +2780,23 @@ static void print_cpu_domain_info(struct feat_fd *ff, FILE *fp) } } +static void print_memory_ranges(struct feat_fd *ff, FILE *fp) +{ + struct memory_range *ranges = ff->ph->env.memory_ranges; + int nr_memory_ranges = ff->ph->env.nr_memory_ranges; + int i; + + fprintf(fp, "# memory ranges (nr %d):\n", nr_memory_ranges); + + for (i = 0; i < nr_memory_ranges; i++) { + fprintf(fp, "# range%u: [0x%016" PRIx64 "-0x%016" PRIx64, + i, ranges[i].base, ranges[i].base + ranges[i].length - 1); + fprintf(fp, "], node = %d, local_region_id = %d, remote_region_id = %d\n", + ranges[i].node, ranges[i].local_region_id, + ranges[i].remote_region_id); + } +} + static int __event_process_build_id(struct perf_record_header_build_id *bev, char *filename, struct perf_session *session) @@ -4227,6 +4372,65 @@ static int process_cpu_domain_info(struct feat_fd *ff, void *data __maybe_unused return ret; } +static int process_memory_ranges(struct feat_fd *ff, void *data __maybe_unused) +{ + struct perf_env *env = &ff->ph->env; + struct memory_range *ranges, *r; + u32 nr_memory_ranges, i; + + if (do_read_u32(ff, &nr_memory_ranges)) + return -1; + + if (!nr_memory_ranges) { + pr_debug("memory ranges not available\n"); + return 0; + } + + /* + * According to version 1.1 of the ACPI MRRM table, the maximum + * number of memory regions can be at most 255. Do a sanity check + * here to guard against a malformed perf.data file. + */ + if (nr_memory_ranges > MAX_MEMORY_RANGES) { + pr_err("Invalid memory_ranges: nr_memory_ranges (%u) > %u\n", + nr_memory_ranges, MAX_MEMORY_RANGES); + return -1; + } + + if (ff->size < sizeof(u32) + nr_memory_ranges * (2 * sizeof(u64) + sizeof(u32) + 2 * sizeof(u8))) { + pr_err("Invalid HEADER_MEMORY_RANGES: section too small (%zu) for %u range entries\n", + ff->size, nr_memory_ranges); + return -1; + } + + ranges = calloc(nr_memory_ranges, sizeof(*ranges)); + if (!ranges) + return -1; + + for (i = 0; i < nr_memory_ranges; i++) { + r = &ranges[i]; + + if (do_read_u64(ff, &r->base)) + goto error; + if (do_read_u64(ff, &r->length)) + goto error; + if (do_read_u32(ff, (u32 *) &r->node)) + goto error; + if (__do_read(ff, &r->local_region_id, sizeof(u8))) + goto error; + if (__do_read(ff, &r->remote_region_id, sizeof(u8))) + goto error; + } + + env->memory_ranges = ranges; + env->nr_memory_ranges = nr_memory_ranges; + + return 0; +error: + zfree(&ranges); + return -1; +} + #define FEAT_OPR(n, func, __full_only) \ [HEADER_##n] = { \ .name = __stringify(n), \ @@ -4291,6 +4495,7 @@ const struct perf_header_feature_ops feat_ops[HEADER_LAST_FEATURE] = { FEAT_OPR(CPU_DOMAIN_INFO, cpu_domain_info, true), FEAT_OPR(E_MACHINE, e_machine, false), FEAT_OPR(CLN_SIZE, cln_size, false), + FEAT_OPR(MEMORY_RANGES, memory_ranges, false), }; struct header_print_data { diff --git a/tools/perf/util/header.h b/tools/perf/util/header.h index 5e03f884b7cc..765037762758 100644 --- a/tools/perf/util/header.h +++ b/tools/perf/util/header.h @@ -56,6 +56,7 @@ enum { HEADER_CPU_DOMAIN_INFO, HEADER_E_MACHINE, HEADER_CLN_SIZE, + HEADER_MEMORY_RANGES, HEADER_LAST_FEATURE, HEADER_FEAT_BITS = 256, }; From 41544ef9c945078b0f38320189d2f8cdbe5e8e6a Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Wed, 23 Sep 2026 12:09:01 -0500 Subject: [PATCH 132/425] perf tools: Show memory region in perf-c2c subcommand Add memory region field to the cacheline list view to help users identify the memory region to which the cacheline belongs. The memory region field was included with the introduction of support for the Off-module Response facility (OMR) [1] in Intel's Diamond Rapids and Nova Lake architectures. An example of the new perf c2c output including the memory region field is shown below: 0 0xffffffff84eb0040 N/A 0 1205 23.25% 544 544 0 1677 1677 1 0xffffffff84f03a80 N/A 0 1 18.12% 424 424 0 585 585 [...] 19 0xffffffff845bc840 Mem-0 0 13 0.30% 7 7 0 31 31 20 0xff3bbba284520a00 N/A 0 10 0.26% 6 6 0 11 11 21 0xff3bbbe0bc833500 N/A 0 24 0.26% 6 6 0 30 30 [1]: https://lore.kernel.org/all/20260114011750.350569-1-dapeng1.mi@linux.intel.com/ Reviewed-by: Dapeng Mi Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Assisted-by: GitHub-Copilot:claude-opus-4-8 Assisted-by: Sashiko:gemini-3.1-pro-preview Co-developed-by: Thomas Falcon Signed-off-by: Dapeng Mi Signed-off-by: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-c2c.c | 114 +++++++++++++++++++++++++++++++++++---- tools/perf/util/c2c.h | 1 + 2 files changed, 104 insertions(+), 11 deletions(-) diff --git a/tools/perf/builtin-c2c.c b/tools/perf/builtin-c2c.c index 6b64c0d6b24f..624f9d915423 100644 --- a/tools/perf/builtin-c2c.c +++ b/tools/perf/builtin-c2c.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -72,6 +73,7 @@ struct perf_c2c { bool show_src; bool show_all; + bool show_mem_region; bool use_stdio; bool stats_only; bool symbol_full; @@ -248,6 +250,18 @@ static void c2c_he__set_node(struct c2c_hist_entry *c2c_he, } } +static void c2c_he__set_mem_region(struct c2c_hist_entry *c2c_he, + unsigned int mem_region) +{ + if (WARN_ONCE(mem_region > PERF_MEM_REGION_MEM7, + "WARNING: invalid memory region ID\n")) + return; + + /* Update mem_region only if it really accesses memory */ + if (mem_region >= PERF_MEM_REGION_MMIO) + c2c_he->mem_region = mem_region; +} + static void compute_stats(struct c2c_hist_entry *c2c_he, struct c2c_stats *stats, u64 weight) @@ -306,6 +320,7 @@ static int process_sample_event(const struct perf_tool *tool __maybe_unused, struct addr_location al; struct mem_info *mi = NULL; struct callchain_cursor *cursor; + unsigned int mem_region; int ret; addr_location__init(&al); @@ -333,6 +348,7 @@ static int process_sample_event(const struct perf_tool *tool __maybe_unused, } c2c_decode_stats(&stats, mi); + mem_region = mem_info__data_src(mi)->mem_region; he = hists__add_entry_ops(&c2c_hists->hists, &c2c_entry_ops, &al, NULL, NULL, mi, NULL, @@ -349,6 +365,7 @@ static int process_sample_event(const struct perf_tool *tool __maybe_unused, c2c_he__set_cpu(c2c_he, sample); c2c_he__set_node(c2c_he, sample); c2c_he__set_evsel(c2c_he, evsel); + c2c_he__set_mem_region(c2c_he, mem_region); hists__inc_nr_samples(&c2c_hists->hists, he->filtered); @@ -402,6 +419,7 @@ static int process_sample_event(const struct perf_tool *tool __maybe_unused, c2c_he__set_cpu(c2c_he, sample); c2c_he__set_node(c2c_he, sample); c2c_he__set_evsel(c2c_he, evsel); + c2c_he__set_mem_region(c2c_he, mem_region); hists__inc_nr_samples(&c2c_hists->hists, he->filtered); ret = hist_entry__append_callchain(he, sample); @@ -540,6 +558,45 @@ dcacheline_node_count(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp, return scnprintf(hpp->buf, hpp->size, "%*lu", width, c2c_he->paddr_cnt); } +static int +dcacheline_node_mem_region(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp, + struct hist_entry *he) +{ + int width = c2c_width(fmt, hpp, he->hists); + struct c2c_hist_entry *c2c_he; + unsigned int mem_region; + char buf[20]; + + c2c_he = container_of(he, struct c2c_hist_entry, he); + mem_region = c2c_he->mem_region; + + switch (mem_region) { + case PERF_MEM_REGION_NA: + case PERF_MEM_REGION_RSVD: + scnprintf(buf, sizeof(buf), "N/A"); + break; + case PERF_MEM_REGION_MMIO: + scnprintf(buf, sizeof(buf), "MMIO"); + break; + case PERF_MEM_REGION_MEM0: + case PERF_MEM_REGION_MEM1: + case PERF_MEM_REGION_MEM2: + case PERF_MEM_REGION_MEM3: + case PERF_MEM_REGION_MEM4: + case PERF_MEM_REGION_MEM5: + case PERF_MEM_REGION_MEM6: + case PERF_MEM_REGION_MEM7: + scnprintf(buf, sizeof(buf), "Mem-%d", + mem_region - PERF_MEM_REGION_MEM0); + break; + default: + scnprintf(buf, sizeof(buf), "N/A"); + break; + } + + return scnprintf(hpp->buf, hpp->size, "%*s", width, buf); +} + static int offset_entry(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp, struct hist_entry *he) { @@ -1360,6 +1417,14 @@ static struct c2c_dimension dim_dcacheline_node = { .width = 4, }; +static struct c2c_dimension dim_dcacheline_mem_region = { + .header = HEADER_LOW("Region"), + .name = "dcacheline_mem_region", + .cmp = empty_cmp, + .entry = dcacheline_node_mem_region, + .width = 6, +}; + static struct c2c_dimension dim_dcacheline_count = { .header = HEADER_LOW("PA cnt"), .name = "dcacheline_count", @@ -1791,6 +1856,7 @@ static struct c2c_dimension dim_dcacheline_num_empty = { static struct c2c_dimension *dimensions[] = { &dim_dcacheline, + &dim_dcacheline_mem_region, &dim_dcacheline_node, &dim_dcacheline_count, &dim_offset, @@ -2854,8 +2920,11 @@ static int ui_quirks(void) /* Fix the zero line for dcacheline column. */ buf = fill_line(chk_double_cl ? "Double-Cacheline" : "Cacheline", dim_dcacheline.width + + (c2c.show_mem_region ? + dim_dcacheline_mem_region.width : 0) + dim_dcacheline_node.width + - dim_dcacheline_count.width + 4); + dim_dcacheline_count.width + + (c2c.show_mem_region ? 6 : 4)); if (!buf) return -ENOMEM; @@ -3107,7 +3176,8 @@ static int perf_c2c__report(int argc, const char **argv) OPT_END() }; int err = 0; - const char *output_str, *sort_str = NULL; + const char *sort_str = NULL; + char *output_str = NULL; struct perf_env *env; annotation_options__init(); @@ -3272,9 +3342,16 @@ static int perf_c2c__report(int argc, const char **argv) goto out_mem2node; } - if (c2c.display != DISPLAY_SNP_PEER) - output_str = "cl_idx," + c2c.show_mem_region = perf_header__has_feat(&session->header, + HEADER_MEMORY_RANGES); + if (c2c.show_mem_region) + dim_dcacheline.header.line[0].span = 3; + + if (c2c.display != DISPLAY_SNP_PEER) { + if (asprintf(&output_str, + "cl_idx," "dcacheline," + "%s" "dcacheline_node," "dcacheline_count," "percent_costly_snoop," @@ -3286,10 +3363,17 @@ static int perf_c2c__report(int argc, const char **argv) "ld_fbhit,ld_l1hit,ld_l2hit," "ld_lclhit,lcl_hitm," "ld_rmthit,rmt_hitm," - "dram_lcl,dram_rmt"; - else - output_str = "cl_idx," + "dram_lcl,dram_rmt", + c2c.show_mem_region ? + "dcacheline_mem_region," : "") < 0) { + err = -ENOMEM; + goto out_mem2node; + } + } else { + if (asprintf(&output_str, + "cl_idx," "dcacheline," + "%s" "dcacheline_node," "dcacheline_count," "percent_costly_snoop," @@ -3301,7 +3385,13 @@ static int perf_c2c__report(int argc, const char **argv) "ld_fbhit,ld_l1hit,ld_l2hit," "ld_lclhit,lcl_hitm," "ld_rmthit,rmt_hitm," - "dram_lcl,dram_rmt"; + "dram_lcl,dram_rmt", + c2c.show_mem_region ? + "dcacheline_mem_region," : "") < 0) { + err = -ENOMEM; + goto out_mem2node; + } + } if (c2c.display == DISPLAY_TOT_HITM) sort_str = "tot_hitm"; @@ -3315,7 +3405,7 @@ static int perf_c2c__report(int argc, const char **argv) err = c2c_hists__reinit(&c2c.hists, output_str, sort_str, perf_session__env(session)); if (err) { pr_err("Failed to reinitialize hists\n"); - goto out_mem2node; + goto out_str; } ui_progress__init(&prog, c2c.hists.hists.nr_entries, "Sorting..."); @@ -3324,17 +3414,19 @@ static int perf_c2c__report(int argc, const char **argv) hists__output_resort_cb(&c2c.hists.hists, &prog, resort_shared_cl_cb); err = hists__iterate_cb(&c2c.hists.hists, resort_cl_cb, perf_session__env(session)); if (err) - goto out_mem2node; + goto out_str; ui_progress__finish(); if (ui_quirks()) { pr_err("failed to setup UI\n"); - goto out_mem2node; + goto out_str; } perf_c2c_display(session); +out_str: + free(output_str); out_mem2node: mem2node__exit(&c2c.mem2node); out_session: diff --git a/tools/perf/util/c2c.h b/tools/perf/util/c2c.h index 53f024e25d99..f04e78e1a2e3 100644 --- a/tools/perf/util/c2c.h +++ b/tools/perf/util/c2c.h @@ -33,6 +33,7 @@ struct c2c_hist_entry { unsigned long *nodeset; struct c2c_stats *node_stats; unsigned int cacheline_idx; + unsigned int mem_region; struct compute_stats cstats; From 8eca6fa643b8b0d2474ca0a7e722cfa0ba9a6156 Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Wed, 23 Sep 2026 12:09:02 -0500 Subject: [PATCH 133/425] perf tools: Show memory region in perf-script subcommand Show the memory region in perf-script subcommand. Memory region is found in the mem_region field of the memory information data source. This field was included with the introduction of support for the Off-module Response facility (OMR) [1] in Intel's Diamond Rapids and Nova Lake Architectures. An example of perf-script output with the new memory region field is shown below: thread 5/0 ... |OP LOAD|LVL L3 hit|SNP HitM|TLB L1 or L2 hit|LCK No|BLK N/A|Region Local-shared-cache ffffffff9f002aba thread 2/0 ... |OP LOAD|LVL L3 hit|SNP None|TLB L1 or L2 hit|LCK No|BLK N/A|Region Local-shared-cache ffffffff9dc54c95 thread 8/0 ... |OP LOAD|LVL L3 hit|SNP HitM|TLB L1 or L2 hit|LCK No|BLK N/A|Region Other-non-shared-cache ffffffff9dc54db3 thread 0/0 ... |OP LOAD|LVL LFB/MAB hit|SNP None|TLB L1 or L2 hit|LCK No|BLK Addr|Region N/A ffffffff9dc54db3 thread 2/0 ... |OP LOAD|LVL L0 hit|SNP None|TLB L1 or L2 hit|LCK Yes|BLK N/A|Region N/A ffffffff9f002a8c thread 0/0 ... |OP LOAD|LVL L3 hit|SNP HitM or Fwd|TLB L1 or L2 hit|LCK No|BLK N/A|Region Local-non-shared-cache ffffffff9f002aba thread 0/0 ... |OP LOAD|LVL L3 hit|SNP None|TLB L1 or L2 hit|LCK No|BLK N/A|Region Local-shared-cache ffffffff9dc54ca7 thread 30/0 ... |OP LOAD|LVL LFB/MAB hit|SNP None|TLB L1 or L2 hit|LCK Yes|BLK N/A|Region N/A ffffffff9dcc1ff2 thread 30/0 ... |OP LOAD|LVL RAM hit|SNP None|TLB L1 or L2 hit|LCK No|BLK N/A|Region Mem-0 ffffffff9dc54c95 thread 19/0 ... |OP LOAD|LVL LFB/MAB hit|SNP None|TLB L1 or L2 hit|LCK No|BLK N/A|Region N/A ffffffff9f002cbb thread 19/0 ... |OP LOAD|LVL L3 hit|SNP HitM or Fwd|TLB L1 or L2 hit|LCK No|BLK N/A|Region Local-non-shared-cache ffffffff9f002aba [1]: https://lore.kernel.org/all/20260114011750.350569-1-dapeng1.mi@linux.intel.com/ Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Assisted-by: GitHub-Copilot:claude-opus-4-8 Assisted-by: Sashiko:gemini-3.1-pro-preview Co-developed-by: Thomas Falcon Signed-off-by: Dapeng Mi Signed-off-by: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-script.c | 13 +-- tools/perf/util/mem-events.c | 87 ++++++++++++++++++- tools/perf/util/mem-events.h | 4 +- .../scripting-engines/trace-event-python.c | 6 +- 4 files changed, 100 insertions(+), 10 deletions(-) diff --git a/tools/perf/builtin-script.c b/tools/perf/builtin-script.c index 0174489d1c0f..565ee8d55f80 100644 --- a/tools/perf/builtin-script.c +++ b/tools/perf/builtin-script.c @@ -2153,11 +2153,12 @@ static int evlist__max_name_len(struct evlist *evlist) return max; } -static int data_src__fprintf(u64 data_src, FILE *fp) +static int data_src__fprintf(struct perf_session *session, + u64 data_src, FILE *fp) { struct mem_info *mi = mem_info__new(); - char decode[100]; - char out[100]; + char decode[200]; + char out[200]; static int maxlen; int len; @@ -2165,10 +2166,10 @@ static int data_src__fprintf(u64 data_src, FILE *fp) return -ENOMEM; mem_info__data_src(mi)->val = data_src; - perf_script__meminfo_scnprintf(decode, 100, mi); + perf_script__meminfo_scnprintf(decode, 200, mi, session); mem_info__put(mi); - len = scnprintf(out, 100, "%16" PRIx64 " %s", data_src, decode); + len = scnprintf(out, 200, "%16" PRIx64 " %s", data_src, decode); if (maxlen < len) maxlen = len; @@ -2562,7 +2563,7 @@ static void process_event(struct perf_script *script, perf_sample__fprintf_addr(sample, thread, evsel, fp); if (PRINT_FIELD(DATA_SRC)) - data_src__fprintf(sample->data_src, fp); + data_src__fprintf(evsel__session(evsel), sample->data_src, fp); if (PRINT_FIELD(WEIGHT)) fprintf(fp, "%16" PRIu64, sample->weight); diff --git a/tools/perf/util/mem-events.c b/tools/perf/util/mem-events.c index 4fd48fd20055..0cdcb47647bd 100644 --- a/tools/perf/util/mem-events.c +++ b/tools/perf/util/mem-events.c @@ -17,6 +17,7 @@ #include "symbol.h" #include "pmu.h" #include "pmus.h" +#include "session.h" unsigned int perf_mem_events__loads_ldlat = 30; @@ -604,8 +605,77 @@ int perf_mem__blk_scnprintf(char *out, size_t sz, const struct mem_info *mem_inf return l; } -int perf_script__meminfo_scnprintf(char *out, size_t sz, const struct mem_info *mem_info) +static int perf_mem__region_scnprintf(char *out, size_t sz, const struct mem_info *mem_info) { + size_t l = 0; + u64 mem = PERF_MEM_REGION_NA; + + sz -= 1; /* -1 for null termination */ + out[0] = '\0'; + + if (mem_info) + mem = mem_info__const_data_src(mem_info)->mem_region; + + switch (mem) { + case PERF_MEM_REGION_NA: + case PERF_MEM_REGION_RSVD: + l += scnprintf(out + l, sz - l, "N/A"); + break; + case PERF_MEM_REGION_L_SHARE: + l += scnprintf(out + l, sz - l, "Local-shared-cache"); + break; + case PERF_MEM_REGION_L_NON_SHARE: + l += scnprintf(out + l, sz - l, "Local-non-shared-cache"); + break; + case PERF_MEM_REGION_O_IO: + l += scnprintf(out + l, sz - l, "Other-IO"); + break; + case PERF_MEM_REGION_O_SHARE: + l += scnprintf(out + l, sz - l, "Other-shared-cache"); + break; + case PERF_MEM_REGION_O_NON_SHARE: + l += scnprintf(out + l, sz - l, "Other-non-shared-cache"); + break; + case PERF_MEM_REGION_MMIO: + l += scnprintf(out + l, sz - l, "MMIO"); + break; + case PERF_MEM_REGION_MEM0: + l += scnprintf(out + l, sz - l, "Mem-0"); + break; + case PERF_MEM_REGION_MEM1: + l += scnprintf(out + l, sz - l, "Mem-1"); + break; + case PERF_MEM_REGION_MEM2: + l += scnprintf(out + l, sz - l, "Mem-2"); + break; + case PERF_MEM_REGION_MEM3: + l += scnprintf(out + l, sz - l, "Mem-3"); + break; + case PERF_MEM_REGION_MEM4: + l += scnprintf(out + l, sz - l, "Mem-4"); + break; + case PERF_MEM_REGION_MEM5: + l += scnprintf(out + l, sz - l, "Mem-5"); + break; + case PERF_MEM_REGION_MEM6: + l += scnprintf(out + l, sz - l, "Mem-6"); + break; + case PERF_MEM_REGION_MEM7: + l += scnprintf(out + l, sz - l, "Mem-7"); + break; + default: + l += scnprintf(out + l, sz - l, "N/A"); + break; + } + + return l; +} + +int perf_script__meminfo_scnprintf(char *out, size_t sz, + const struct mem_info *mem_info, + struct perf_session *session) +{ + struct perf_env *env; int i = 0; i += scnprintf(out, sz, "|OP "); @@ -620,6 +690,21 @@ int perf_script__meminfo_scnprintf(char *out, size_t sz, const struct mem_info * i += perf_mem__lck_scnprintf(out + i, sz - i, mem_info); i += scnprintf(out + i, sz - i, "|BLK "); i += perf_mem__blk_scnprintf(out + i, sz - i, mem_info); + if (session) { + /* + * In case the feature bits are not available, as in + * pipe mode, fallback to checking for the existence of + * memory ranges + */ + env = perf_session__env(session); + if ((env && session->data->is_pipe && env->nr_memory_ranges) || + perf_header__has_feat(&session->header, + HEADER_MEMORY_RANGES)) { + i += scnprintf(out + i, sz - i, "|Region "); + i += perf_mem__region_scnprintf(out + i, sz - i, + mem_info); + } + } return i; } diff --git a/tools/perf/util/mem-events.h b/tools/perf/util/mem-events.h index daa22748f9fe..38b14c03392c 100644 --- a/tools/perf/util/mem-events.h +++ b/tools/perf/util/mem-events.h @@ -24,6 +24,7 @@ enum { struct evsel; struct mem_info; struct perf_pmu; +struct perf_session; extern unsigned int perf_mem_events__loads_ldlat; extern struct perf_mem_event perf_mem_events[PERF_MEM_EVENTS__MAX]; @@ -47,7 +48,8 @@ int perf_mem__snp_scnprintf(char *out, size_t sz, const struct mem_info *mem_inf int perf_mem__lck_scnprintf(char *out, size_t sz, const struct mem_info *mem_info); int perf_mem__blk_scnprintf(char *out, size_t sz, const struct mem_info *mem_info); -int perf_script__meminfo_scnprintf(char *bf, size_t size, const struct mem_info *mem_info); +int perf_script__meminfo_scnprintf(char *bf, size_t size, const struct mem_info *mem_info, + struct perf_session *session); struct c2c_stats { u32 nr_entries; diff --git a/tools/perf/util/scripting-engines/trace-event-python.c b/tools/perf/util/scripting-engines/trace-event-python.c index 239104b0337d..038c4268a429 100644 --- a/tools/perf/util/scripting-engines/trace-event-python.c +++ b/tools/perf/util/scripting-engines/trace-event-python.c @@ -58,6 +58,7 @@ #include "stat.h" #include "mem-events.h" #include "util/perf_regs.h" +#include "util/session.h" #define _PyUnicode_FromString(arg) \ PyUnicode_FromString(arg) @@ -694,8 +695,9 @@ static void set_sample_read_in_dict(PyObject *dict_sample, struct perf_sample *s static void set_sample_datasrc_in_dict(PyObject *dict, struct perf_sample *sample) { + struct perf_session *session = evsel__session(sample->evsel); struct mem_info *mi = mem_info__new(); - char decode[100]; + char decode[200]; if (!mi) Py_FatalError("couldn't create mem-info"); @@ -704,7 +706,7 @@ static void set_sample_datasrc_in_dict(PyObject *dict, PyLong_FromUnsignedLongLong(sample->data_src)); mem_info__data_src(mi)->val = sample->data_src; - perf_script__meminfo_scnprintf(decode, 100, mi); + perf_script__meminfo_scnprintf(decode, 200, mi, session); mem_info__put(mi); pydict_set_item_string_decref(dict, "datasrc_decode", From dd0b7ec38dedc2c433ab6d265c72fb7652400888 Mon Sep 17 00:00:00 2001 From: Thomas Falcon Date: Wed, 23 Sep 2026 12:09:03 -0500 Subject: [PATCH 134/425] perf c2c: Print memory region data with stdio output Print memory range data in perf-c2c subcommand. Only available in stdio mode currently. If memory ranges are not supported or present, print nothing. Example output of perf c2c report --stdio: ================================================= Memory Ranges ================================================= Range 0: [0x0000000000000000-0x00000000bfffffff] Node 0, local region id 0, remote region id 255 Range 1: [0x0000000100000000-0x000000203fffffff] Node 0, local region id 0, remote region id 255 Range 2: [0x0000008000000000-0x0000027fffffffff] Node -2, local region id 1, remote region id 255 Range 3: [0x0000028000000000-0x0000047fffffffff] Node -2, local region id 1, remote region id 255 Range 4: [0x0000048000000000-0x000004ffffffffff] Node -2, local region id 1, remote region id 255 Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Assisted-by: Sashiko:gemini-3.1-pro-preview Signed-off-by: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-c2c.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tools/perf/builtin-c2c.c b/tools/perf/builtin-c2c.c index 624f9d915423..ee294f12a542 100644 --- a/tools/perf/builtin-c2c.c +++ b/tools/perf/builtin-c2c.c @@ -2597,6 +2597,33 @@ static void print_c2c_info(FILE *out, struct perf_session *session) fprintf(out, " Cacheline data grouping : %s\n", c2c.cl_sort); } +static void print_memory_ranges_info(FILE *out, struct perf_session *session) +{ + struct perf_env *env = perf_session__env(session); + int nr_ranges = 0; + + if (!perf_header__has_feat(&session->header, HEADER_MEMORY_RANGES)) + return; + nr_ranges = env->nr_memory_ranges; + if (nr_ranges == 0) { + pr_debug("No memory ranges found, skipping\n"); + return; + } + + fprintf(out, "\n"); + fprintf(out, "=================================================\n"); + fprintf(out, " Memory Ranges \n"); + fprintf(out, "=================================================\n"); + + for (int i = 0; i < nr_ranges; i++) { + struct memory_range *r = &env->memory_ranges[i]; + + fprintf(out, "Range %d: [0x%016" PRIx64 "-0x%016" PRIx64 "] Node %d, local region id %u, remote region id %u\n", + i, r->base, r->base + r->length - 1, r->node, + r->local_region_id, r->remote_region_id); + } +} + static void perf_c2c__hists_fprintf(FILE *out, struct perf_session *session) { setup_pager(); @@ -2610,6 +2637,8 @@ static void perf_c2c__hists_fprintf(FILE *out, struct perf_session *session) if (c2c.stats_only) return; + print_memory_ranges_info(out, session); + fprintf(out, "\n"); fprintf(out, "=================================================\n"); fprintf(out, " Shared Data Cache Line Table \n"); From d6f751c7adadf305d90001e86943af65ac75ef8f Mon Sep 17 00:00:00 2001 From: Christian Melendez Date: Wed, 23 Sep 2026 15:51:40 +0200 Subject: [PATCH 135/425] perf stat: Document task-clock/cpu-clock in TIMINGS task-clock/cpu-clock appear in perf stat's own example output but were never explained in perf-stat.txt, unlike time elapsed/user/sys. This came up unanswered on the list in 2013 and 2015 without docs ever being added. Reviewed-by: Ian Rogers Signed-off-by: Christian Melendez Link: https://lore.kernel.org/all/arWi_dY4y15J2rqh@google.com ] [ Removed the :u modifier from the task-clock example, per Namhyung request ] Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-stat.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/perf/Documentation/perf-stat.txt b/tools/perf/Documentation/perf-stat.txt index f334aabdc809..8ea17da89976 100644 --- a/tools/perf/Documentation/perf-stat.txt +++ b/tools/perf/Documentation/perf-stat.txt @@ -621,6 +621,16 @@ user/system lands: Those times are the very same as displayed by the 'time' tool. +We also display a clock-based timing metric that depends on the +target being monitored: + + 83723.452481 task-clock (msec) # 1.004 CPUs utilized + +For a single task, task-clock reports a clock count specific to that +task. When the target is a CPU or the whole system (-a or -C), +cpu-clock is reported instead. cpu-clock is a high-resolution per-CPU +timer. See perf_event_open(2) for more details. + CSV FORMAT ---------- @@ -671,4 +681,4 @@ that can be used for parsing. SEE ALSO -------- -linkperf:perf-top[1], linkperf:perf-list[1] +linkperf:perf-top[1], linkperf:perf-list[1], perf_event_open(2) From 1eb782d60d9926f0e3c3131c568c3ea0fa69d0fb Mon Sep 17 00:00:00 2001 From: Jens Remus Date: Tue, 22 Sep 2026 16:43:00 +0200 Subject: [PATCH 136/425] perf evsel: Improve callchain warning for s390 On s390 the kernel uses s390 back chain instead of frame pointers for stack tracing of user space since v6.7 commit aa44433ac4ee ("s390: add USER_STACKTRACE support"). This is because frame pointers on s390 cannot be used for stack tracing. [1] This requires user space to maintain a s390 back chain. For instance user space to be built with compiler option '-mbackchain' (instead of '-fno-omit-frame-pointer' used on other architectures, which should better not be used on s390 [1]). Only few distributions and users build user space with '-mbackchain'. Therefore '--call-graph fp' may in practice not produce the expected results. Commit ca76fb67ebdd ("perf evlist: Improve default event for s390") changed the default '-g' option to 'dwarf' on s390 and added a warning for s390 that wrongly claimed that "Framepointer unwinding lacks kernel support". This warning resulted from a misinterpretation of comments in s390 cpumsf_pmu_event_init(). The restriction in cpumsf_pmu_event_init() applies to callchain sampling with the s390 CPU Measurement Sampling Facility (CPUMSF) hardware PMU. CPUMSF events, such as 'cycles', do not support callchain sampling. This is independent of whether perf uses the 'fp' or 'dwarf' call-graph mode. The CPUMSF PMU provides samples collected asynchronously, making it impossible to associate a callchain to the historic IPs. Therefore callchains can only be used with software events on s390. Remove the incorrect warning. The kernel supports the 'fp' call-graph mode on s390 by walking the back chain. Do not replace it with a hint to use 'dwarf' when the resulting callchain is incomplete. Such a suggestion could imply that 'fp' is inherently inferior. The same general limitation exists on other architectures when user space is not built with frame pointers (i.e. '-fno-omit-frame-pointers'). Instead, warn on s390 when callchain sampling is requested with an event provided by the CPUMCF or CPUMSF hardware PMUs, because that combination is not supported. Update the perf record documentation to state that 'dwarf' is the default call-graph mode on s390 and that 'fp' uses back chain instead of frame pointers on s390. Note that '--call-graph fp' may also be useful for other applications, such as OpenJDK maintaining a s390 back chain (does not require JVM option '-XX:+PreserveFramePointer' on s390): $ perf record --call-graph fp ... -- \ java -XX:+UnlockDiagnosticVMOptions -XX:+DumpPerfMapAtExit ... [1]: s390: Stack tracing using Frame Pointer, Back Chain, and SFrame, https://conf.gnu-tools-cauldron.org/opo25/talk/Y3CVHY/ Fixes: ca76fb67ebdd5e1a ("perf evlist: Improve default event for s390") Reviewed-by: Thomas Richter Signed-off-by: Jens Remus Acked-by: Ian Rogers Cc: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-record.txt | 7 ++++++- tools/perf/util/evsel.c | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tools/perf/Documentation/perf-record.txt b/tools/perf/Documentation/perf-record.txt index bd2c56d8cdc0..2351f7b4f5fc 100644 --- a/tools/perf/Documentation/perf-record.txt +++ b/tools/perf/Documentation/perf-record.txt @@ -293,7 +293,7 @@ OPTIONS --call-graph:: Setup and enable call-graph (stack chain/backtrace) recording, - implies -g. Default is "fp" (for user space). + implies -g. Default is "fp" for user space (s390 uses "dwarf"). The unwinding method used for kernel space is dependent on the unwinder used by the active kernel configuration, i.e @@ -329,6 +329,11 @@ OPTIONS enable deferred user callchain which will collect user-space callchains when the thread returns to the user space. + On s390, the "fp" method uses back chain instead of frame pointers, + which requires user space to maintain a back chain (e.g. compiler + option -mbackchain). Without a maintained back chain this produces + bogus call graphs. In that case use "dwarf" instead. + -q:: --quiet:: Don't print any warnings or messages, useful for scripting. diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index dd468716f424..a2e9a31c1472 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -1096,9 +1096,9 @@ static void __evsel__config_callchain(struct evsel *evsel, const struct record_o bool function = evsel__is_function_event(evsel); struct perf_event_attr *attr = &evsel->core.attr; - if (EM_HOST == EM_S390 && param->record_mode == CALLCHAIN_FP) { - pr_warning_once( - "Framepointer unwinding lacks kernel support. Use '--call-graph dwarf'\n"); + if (EM_HOST == EM_S390 && evsel->pmu && !perf_pmu__is_software(evsel->pmu)) { + pr_warning_once("Cannot use hardware PMU 'cpum_cf' / 'cpum_sf' event with callchain. " + "Use a software 'cpu-clock' / 'task-clock' event.\n"); } evsel__set_sample_bit(evsel, CALLCHAIN); From 3b364119a9afef9d22b9aee64c96b54553a7117d Mon Sep 17 00:00:00 2001 From: James Clark Date: Tue, 22 Sep 2026 15:47:01 +0100 Subject: [PATCH 137/425] perf buildid-list: Fix empty output for AUX data perf record enables buildid_all for AUX trace because buildid-list does not decode AUX data to identify hit DSOs. Historically those build IDs were available in HEADER_BUILD_ID, and buildid-list disabled hit filtering when HEADER_AUXTRACE was present. Build IDs are now carried by MMAP2 records by default and the build-ID header is omitted. perf_session__list_build_ids() forces with_hits when the header is absent so that it processes the event stream. That also filters the result to DSOs referenced by ordinary samples. AUX data has no such samples unless the trace is decoded, so every DSO is dropped and perf archive reports that no build IDs were found. Process the event stream whenever the build-ID header is absent, allowing MMAP2 build IDs to be read, but do not enable hit filtering for AUX data, this ensures that all build-IDs are included for AUX data. Fixes: 6bd89ae7d14788ba ("perf record: Make sure to update build-ID cache") Reviewed-by: Ian Rogers Assisted-by: Codex:GPT-5.6-Sol Signed-off-by: James Clark Cc: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-buildid-list.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tools/perf/builtin-buildid-list.c b/tools/perf/builtin-buildid-list.c index e0881b0ac38f..5555f5dcefe4 100644 --- a/tools/perf/builtin-buildid-list.c +++ b/tools/perf/builtin-buildid-list.c @@ -87,6 +87,7 @@ static bool dso__skip_buildid(struct dso *dso, int with_hits) static int perf_session__list_build_ids(bool force, bool with_hits) { struct perf_session *session; + bool has_auxtrace, has_build_id; struct perf_data data = { .path = input_name, .mode = PERF_DATA_MODE_READ, @@ -118,11 +119,19 @@ static int perf_session__list_build_ids(bool force, bool with_hits) * We take all buildids when the file contains AUX area tracing data * because we do not decode the trace because it would take too long. */ - if (!perf_data__is_pipe(&data) && - perf_header__has_feat(&session->header, HEADER_AUXTRACE)) + has_auxtrace = !perf_data__is_pipe(&data) && + perf_header__has_feat(&session->header, HEADER_AUXTRACE); + has_build_id = perf_header__has_feat(&session->header, HEADER_BUILD_ID); + + if (has_auxtrace) with_hits = false; - if (!perf_header__has_feat(&session->header, HEADER_BUILD_ID)) + /* + * Without a build-ID header, the event stream is processed below to find + * build IDs in MMAP2 records. For AUX data keep all DSOs because the trace + * is not decoded and consequently none of them can be marked as hit. + */ + if (!has_build_id && !has_auxtrace) with_hits = true; if (zstd_init(&(session->zstd_data), 0) < 0) @@ -132,7 +141,7 @@ static int perf_session__list_build_ids(bool force, bool with_hits) * in pipe-mode, the only way to get the buildids is to parse * the record stream. Buildids are stored as RECORD_HEADER_BUILD_ID */ - if (with_hits || perf_data__is_pipe(&data)) + if (with_hits || perf_data__is_pipe(&data) || !has_build_id) perf_session__process_events(session); perf_session__fprintf_dsos_buildid(session, stdout, dso__skip_buildid, with_hits); From a18bf13d9bf3e5481ed9da9ee4b547b0d30ad0de Mon Sep 17 00:00:00 2001 From: Dapeng Mi Date: Wed, 23 Sep 2026 09:55:17 +0800 Subject: [PATCH 138/425] perf jevents: Rename OMR offcore_rsp attribute to offmodule_rsp The kernel renamed the OMR data attribute from offcore_rsp to offmodule_rsp to avoid confusion and keep the naming consistent. So update the perf jevents msrmap accordingly. Reviewed-by: Ian Rogers Signed-off-by: Dapeng Mi Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/pmu-events/jevents.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py index 860027bb71b2..93cf04fa8ca4 100755 --- a/tools/perf/pmu-events/jevents.py +++ b/tools/perf/pmu-events/jevents.py @@ -249,10 +249,10 @@ class JsonEvent: 0x3F6: 'ldlat=', 0x1A6: 'offcore_rsp=', 0x1A7: 'offcore_rsp=', - 0x3E0: 'offcore_rsp=', - 0x3E1: 'offcore_rsp=', - 0x3E2: 'offcore_rsp=', - 0x3E3: 'offcore_rsp=', + 0x3E0: 'offmodule_rsp=', + 0x3E1: 'offmodule_rsp=', + 0x3E2: 'offmodule_rsp=', + 0x3E3: 'offmodule_rsp=', 0x3F7: 'frontend=', } return msrmap[int(num.split(',', 1)[0], 0)] From a9aa5c5e40d9f81d63100cb7820b696ac9a93542 Mon Sep 17 00:00:00 2001 From: James Clark Date: Mon, 21 Sep 2026 16:47:28 +0100 Subject: [PATCH 139/425] perf test: Remove redundant shellcheck source paths The minimum required shellcheck version treats paths with leading variables as starting from the script's current directory, so shellcheck is able to source all of these files without needing the extra directives: v0.7.2 - 2021-04-19 Changed * SC1090: A leading $x/ or $(x)/ is now treated as ./ when locating files Remove them to avoid future copy pasting and confusion that they are required. Because the paths weren't actually followed, they didn't have to be accurate and accepted any string to silence the warning. Signed-off-by: James Clark Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/annotate.sh | 1 - tools/perf/tests/shell/base_probe/test_adding_kernel.sh | 1 - tools/perf/tests/shell/diff.sh | 1 - tools/perf/tests/shell/inject_aslr.sh | 1 - tools/perf/tests/shell/jitdump-python.sh | 1 - tools/perf/tests/shell/list.sh | 1 - tools/perf/tests/shell/pipe_test.sh | 1 - tools/perf/tests/shell/probe_vfs_getname.sh | 2 -- tools/perf/tests/shell/python-use.sh | 1 - tools/perf/tests/shell/record+probe_libc_inet_pton.sh | 2 -- tools/perf/tests/shell/record+script_probe_vfs_getname.sh | 2 -- tools/perf/tests/shell/record.sh | 2 -- tools/perf/tests/shell/record_weak_term.sh | 1 - tools/perf/tests/shell/script_dlfilter.sh | 1 - tools/perf/tests/shell/stat+csv_output.sh | 1 - tools/perf/tests/shell/stat+json_output.sh | 1 - tools/perf/tests/shell/stat+std_output.sh | 1 - tools/perf/tests/shell/stat_metrics_values.sh | 1 - tools/perf/tests/shell/test_arm_callgraph_fp.sh | 1 - tools/perf/tests/shell/test_brstack.sh | 1 - tools/perf/tests/shell/test_data_symbol.sh | 1 - tools/perf/tests/shell/test_intel_pt.sh | 1 - tools/perf/tests/shell/test_perf_data_converter_json.sh | 1 - tools/perf/tests/shell/test_test_junit_output.sh | 1 - tools/perf/tests/shell/test_uprobe_from_different_cu.sh | 1 - tools/perf/tests/shell/trace+probe_vfs_getname.sh | 1 - tools/perf/tests/shell/trace_btf_enum.sh | 1 - tools/perf/tests/shell/trace_btf_general.sh | 1 - tools/perf/tests/shell/trace_exit_race.sh | 1 - tools/perf/tests/shell/trace_record_replay.sh | 2 -- tools/perf/tests/shell/trace_summary.sh | 1 - 31 files changed, 36 deletions(-) diff --git a/tools/perf/tests/shell/annotate.sh b/tools/perf/tests/shell/annotate.sh index 689de58e9238..26411edf5cb4 100755 --- a/tools/perf/tests/shell/annotate.sh +++ b/tools/perf/tests/shell/annotate.sh @@ -6,7 +6,6 @@ set -e shelldir=$(dirname "$0") -# shellcheck source=lib/perf_has_symbol.sh . "${shelldir}"/lib/perf_has_symbol.sh testsym="noploop" diff --git a/tools/perf/tests/shell/base_probe/test_adding_kernel.sh b/tools/perf/tests/shell/base_probe/test_adding_kernel.sh index 555a825d55f2..eadac837f108 100755 --- a/tools/perf/tests/shell/base_probe/test_adding_kernel.sh +++ b/tools/perf/tests/shell/base_probe/test_adding_kernel.sh @@ -19,7 +19,6 @@ TEST_RESULT=0 # include working environment . "$DIR_PATH/../common/init.sh" -# shellcheck source=lib/probe_vfs_getname.sh . "$DIR_PATH/../lib/probe_vfs_getname.sh" TEST_PROBE=${TEST_PROBE:-"inode_permission"} diff --git a/tools/perf/tests/shell/diff.sh b/tools/perf/tests/shell/diff.sh index fe05fdebcab5..88f37b8ef893 100755 --- a/tools/perf/tests/shell/diff.sh +++ b/tools/perf/tests/shell/diff.sh @@ -11,7 +11,6 @@ perfdata3=$(mktemp /tmp/__perf_test.perf.data.XXXXX) testprog="perf test -w thloop" shelldir=$(dirname "$0") -# shellcheck source=lib/perf_has_symbol.sh . "${shelldir}"/lib/perf_has_symbol.sh testsym="test_loop" diff --git a/tools/perf/tests/shell/inject_aslr.sh b/tools/perf/tests/shell/inject_aslr.sh index d83c2591db8f..6ee4c8b5c850 100755 --- a/tools/perf/tests/shell/inject_aslr.sh +++ b/tools/perf/tests/shell/inject_aslr.sh @@ -6,7 +6,6 @@ set -e set -o pipefail shelldir=$(dirname "$0") -# shellcheck source=lib/perf_has_symbol.sh . "${shelldir}"/lib/perf_has_symbol.sh sym="noploop" diff --git a/tools/perf/tests/shell/jitdump-python.sh b/tools/perf/tests/shell/jitdump-python.sh index 05aaa3bd900b..ad345a35c16d 100755 --- a/tools/perf/tests/shell/jitdump-python.sh +++ b/tools/perf/tests/shell/jitdump-python.sh @@ -3,7 +3,6 @@ # SPDX-License-Identifier: GPL-2.0 SHELLDIR=$(dirname $0) -# shellcheck source=lib/setup_python.sh . "${SHELLDIR}"/lib/setup_python.sh OUTPUT=$(${PYTHON} -Xperf_jit -c 'import os, sys; print(os.getpid(), sys.is_stack_trampoline_active())' 2> /dev/null) diff --git a/tools/perf/tests/shell/list.sh b/tools/perf/tests/shell/list.sh index 0c04b3159cef..14a03e0290ed 100755 --- a/tools/perf/tests/shell/list.sh +++ b/tools/perf/tests/shell/list.sh @@ -5,7 +5,6 @@ set -e shelldir=$(dirname "$0") -# shellcheck source=lib/setup_python.sh . "${shelldir}"/lib/setup_python.sh list_output=$(mktemp /tmp/__perf_test.list_output.json.XXXXX) diff --git a/tools/perf/tests/shell/pipe_test.sh b/tools/perf/tests/shell/pipe_test.sh index ce68d850c983..f4eb129afda2 100755 --- a/tools/perf/tests/shell/pipe_test.sh +++ b/tools/perf/tests/shell/pipe_test.sh @@ -3,7 +3,6 @@ # SPDX-License-Identifier: GPL-2.0 shelldir=$(dirname "$0") -# shellcheck source=lib/perf_has_symbol.sh . "${shelldir}"/lib/perf_has_symbol.sh sym="noploop" diff --git a/tools/perf/tests/shell/probe_vfs_getname.sh b/tools/perf/tests/shell/probe_vfs_getname.sh index 5fe5682c28ce..36dc319eea0e 100755 --- a/tools/perf/tests/shell/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/probe_vfs_getname.sh @@ -4,13 +4,11 @@ # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo , 2017 -# shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh skip_if_no_perf_probe || exit 2 [ "$(id -u)" = 0 ] || exit 2 -# shellcheck source=lib/probe_vfs_getname.sh . "$(dirname $0)"/lib/probe_vfs_getname.sh add_probe_vfs_getname diff --git a/tools/perf/tests/shell/python-use.sh b/tools/perf/tests/shell/python-use.sh index fd2ee5390060..2d6017d5dd9d 100755 --- a/tools/perf/tests/shell/python-use.sh +++ b/tools/perf/tests/shell/python-use.sh @@ -5,7 +5,6 @@ set -e shelldir=$(dirname "$0") -# shellcheck source=lib/setup_python.sh . "${shelldir}"/lib/setup_python.sh MODULE_DIR=$(dirname "$(which perf)")/python diff --git a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh index eca629ee83f0..4c9a308a450d 100755 --- a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh +++ b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh @@ -10,9 +10,7 @@ # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo , 2017 -# shellcheck source=lib/probe.sh . "$(dirname "$0")/lib/probe.sh" -# shellcheck source=lib/probe_vfs_getname.sh . "$(dirname "$0")/lib/probe_vfs_getname.sh" libc=$(grep -w libc /proc/self/maps | head -1 | sed -r 's/.*[[:space:]](\/.*)/\1/g') diff --git a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh index 002f7037f182..ce20663601dc 100755 --- a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh +++ b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh @@ -9,13 +9,11 @@ # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo , 2017 -# shellcheck source=lib/probe.sh . "$(dirname "$0")/lib/probe.sh" skip_if_no_perf_probe || exit 2 [ "$(id -u)" = 0 ] || exit 2 -# shellcheck source=lib/probe_vfs_getname.sh . "$(dirname "$0")/lib/probe_vfs_getname.sh" record_open_file() { diff --git a/tools/perf/tests/shell/record.sh b/tools/perf/tests/shell/record.sh index 4a4931cb5ba9..a13e0fd9c0f7 100755 --- a/tools/perf/tests/shell/record.sh +++ b/tools/perf/tests/shell/record.sh @@ -8,10 +8,8 @@ shelldir=$(dirname "$0") . "${shelldir}"/lib/perf_record.sh -# shellcheck source=lib/waiting.sh . "${shelldir}"/lib/waiting.sh -# shellcheck source=lib/perf_has_symbol.sh . "${shelldir}"/lib/perf_has_symbol.sh testsym="test_loop" diff --git a/tools/perf/tests/shell/record_weak_term.sh b/tools/perf/tests/shell/record_weak_term.sh index 811b00ffb47a..da418669ab52 100755 --- a/tools/perf/tests/shell/record_weak_term.sh +++ b/tools/perf/tests/shell/record_weak_term.sh @@ -5,7 +5,6 @@ set -e shelldir=$(dirname "$0") -# shellcheck source=lib/setup_python.sh . "${shelldir}"/lib/setup_python.sh # Find the first event with a specified period, such as diff --git a/tools/perf/tests/shell/script_dlfilter.sh b/tools/perf/tests/shell/script_dlfilter.sh index aaed92bb7828..88222280f609 100755 --- a/tools/perf/tests/shell/script_dlfilter.sh +++ b/tools/perf/tests/shell/script_dlfilter.sh @@ -5,7 +5,6 @@ set -e shelldir=$(dirname "$0") -# shellcheck source=lib/setup_python.sh . "${shelldir}"/lib/setup_python.sh # skip if there's no compiler diff --git a/tools/perf/tests/shell/stat+csv_output.sh b/tools/perf/tests/shell/stat+csv_output.sh index cd6fff597091..a5b34c42e8d7 100755 --- a/tools/perf/tests/shell/stat+csv_output.sh +++ b/tools/perf/tests/shell/stat+csv_output.sh @@ -6,7 +6,6 @@ set -e -# shellcheck source=lib/stat_output.sh . "$(dirname $0)"/lib/stat_output.sh csv_sep=@ diff --git a/tools/perf/tests/shell/stat+json_output.sh b/tools/perf/tests/shell/stat+json_output.sh index 85d1ad7186c6..4969ad70b67b 100755 --- a/tools/perf/tests/shell/stat+json_output.sh +++ b/tools/perf/tests/shell/stat+json_output.sh @@ -9,7 +9,6 @@ set -e skip_test=0 shelldir=$(dirname "$0") -# shellcheck source=lib/setup_python.sh . "${shelldir}"/lib/setup_python.sh pythonchecker=$(dirname $0)/lib/perf_json_output_lint.py diff --git a/tools/perf/tests/shell/stat+std_output.sh b/tools/perf/tests/shell/stat+std_output.sh index 9c4b92ecf448..64c2b81f02de 100755 --- a/tools/perf/tests/shell/stat+std_output.sh +++ b/tools/perf/tests/shell/stat+std_output.sh @@ -6,7 +6,6 @@ set -e -# shellcheck source=lib/stat_output.sh . "$(dirname $0)"/lib/stat_output.sh stat_output=$(mktemp /tmp/__perf_test.stat_output.std.XXXXX) diff --git a/tools/perf/tests/shell/stat_metrics_values.sh b/tools/perf/tests/shell/stat_metrics_values.sh index 86c5c7f70933..0982f6b9737a 100755 --- a/tools/perf/tests/shell/stat_metrics_values.sh +++ b/tools/perf/tests/shell/stat_metrics_values.sh @@ -3,7 +3,6 @@ # SPDX-License-Identifier: GPL-2.0 shelldir=$(dirname "$0") -# shellcheck source=lib/setup_python.sh . "${shelldir}"/lib/setup_python.sh grep -q GenuineIntel /proc/cpuinfo || { echo Skipping non-Intel; exit 2; } diff --git a/tools/perf/tests/shell/test_arm_callgraph_fp.sh b/tools/perf/tests/shell/test_arm_callgraph_fp.sh index 9172dd68a81d..8adfe81fa8dc 100755 --- a/tools/perf/tests/shell/test_arm_callgraph_fp.sh +++ b/tools/perf/tests/shell/test_arm_callgraph_fp.sh @@ -3,7 +3,6 @@ # SPDX-License-Identifier: GPL-2.0 shelldir=$(dirname "$0") -# shellcheck source=lib/perf_has_symbol.sh . "${shelldir}"/lib/perf_has_symbol.sh if [ "$(uname -m)" != "aarch64" ]; then diff --git a/tools/perf/tests/shell/test_brstack.sh b/tools/perf/tests/shell/test_brstack.sh index 71550e0b37ba..fd05ebb5ae74 100755 --- a/tools/perf/tests/shell/test_brstack.sh +++ b/tools/perf/tests/shell/test_brstack.sh @@ -5,7 +5,6 @@ # German Gomez , 2022 shelldir=$(dirname "$0") -# shellcheck source=lib/perf_has_symbol.sh . "${shelldir}"/lib/perf_has_symbol.sh # skip the test if the hardware doesn't support branch stack sampling diff --git a/tools/perf/tests/shell/test_data_symbol.sh b/tools/perf/tests/shell/test_data_symbol.sh index d61b5659a46d..edc528c11dda 100755 --- a/tools/perf/tests/shell/test_data_symbol.sh +++ b/tools/perf/tests/shell/test_data_symbol.sh @@ -6,7 +6,6 @@ shelldir=$(dirname "$0") -# shellcheck source=lib/perf_has_symbol.sh . "${shelldir}"/lib/perf_has_symbol.sh skip_if_no_mem_event() { diff --git a/tools/perf/tests/shell/test_intel_pt.sh b/tools/perf/tests/shell/test_intel_pt.sh index 26243ff760ec..a2e87ef09593 100755 --- a/tools/perf/tests/shell/test_intel_pt.sh +++ b/tools/perf/tests/shell/test_intel_pt.sh @@ -8,7 +8,6 @@ set -e perf list pmu | grep -q 'intel_pt//' || exit 2 shelldir=$(dirname "$0") -# shellcheck source=lib/waiting.sh . "${shelldir}"/lib/waiting.sh skip_cnt=0 diff --git a/tools/perf/tests/shell/test_perf_data_converter_json.sh b/tools/perf/tests/shell/test_perf_data_converter_json.sh index 35d81e39a26c..c2c4ade7609a 100755 --- a/tools/perf/tests/shell/test_perf_data_converter_json.sh +++ b/tools/perf/tests/shell/test_perf_data_converter_json.sh @@ -7,7 +7,6 @@ set -e err=0 shelldir=$(dirname "$0") -# shellcheck source=lib/setup_python.sh . "${shelldir}"/lib/setup_python.sh perfdata=$(mktemp /tmp/__perf_test.perf.data.XXXXX) diff --git a/tools/perf/tests/shell/test_test_junit_output.sh b/tools/perf/tests/shell/test_test_junit_output.sh index 5104ac1e1e6d..38a29e646906 100755 --- a/tools/perf/tests/shell/test_test_junit_output.sh +++ b/tools/perf/tests/shell/test_test_junit_output.sh @@ -7,7 +7,6 @@ set -e err=0 shelldir=$(dirname "$0") -# shellcheck source=lib/setup_python.sh . "${shelldir}"/lib/setup_python.sh result=$(mktemp /tmp/__perf_test.output.xml.XXXXX) diff --git a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh index 7adf9755d6de..83505093257d 100755 --- a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh +++ b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh @@ -4,7 +4,6 @@ set -e -# shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh skip_if_no_perf_probe || exit 2 diff --git a/tools/perf/tests/shell/trace+probe_vfs_getname.sh b/tools/perf/tests/shell/trace+probe_vfs_getname.sh index 7a0b1145d0cd..28d3aa8a9e33 100755 --- a/tools/perf/tests/shell/trace+probe_vfs_getname.sh +++ b/tools/perf/tests/shell/trace+probe_vfs_getname.sh @@ -10,7 +10,6 @@ # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo , 2017 -# shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh skip_if_no_perf_probe || exit 2 diff --git a/tools/perf/tests/shell/trace_btf_enum.sh b/tools/perf/tests/shell/trace_btf_enum.sh index 03e9f680a4a6..b1a659e0245f 100755 --- a/tools/perf/tests/shell/trace_btf_enum.sh +++ b/tools/perf/tests/shell/trace_btf_enum.sh @@ -9,7 +9,6 @@ non_syscall="timer:hrtimer_start" TESTPROG="perf test -w landlock" -# shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh skip_if_no_perf_trace || exit 2 [ "$(id -u)" = 0 ] || exit 2 diff --git a/tools/perf/tests/shell/trace_btf_general.sh b/tools/perf/tests/shell/trace_btf_general.sh index 7a94a5743924..06f4a704daf8 100755 --- a/tools/perf/tests/shell/trace_btf_general.sh +++ b/tools/perf/tests/shell/trace_btf_general.sh @@ -4,7 +4,6 @@ err=0 -# shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh file1=$(mktemp /tmp/file1_XXXX) diff --git a/tools/perf/tests/shell/trace_exit_race.sh b/tools/perf/tests/shell/trace_exit_race.sh index db300cde94fb..024ff8f1d388 100755 --- a/tools/perf/tests/shell/trace_exit_race.sh +++ b/tools/perf/tests/shell/trace_exit_race.sh @@ -6,7 +6,6 @@ # lost. Specifically, trace the exiting syscall of "true" 10 times and ensure # the output contains 10 correct lines. -# shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh skip_if_no_perf_trace || exit 2 diff --git a/tools/perf/tests/shell/trace_record_replay.sh b/tools/perf/tests/shell/trace_record_replay.sh index 38fcafcdfb91..76a7bba6ef23 100755 --- a/tools/perf/tests/shell/trace_record_replay.sh +++ b/tools/perf/tests/shell/trace_record_replay.sh @@ -4,9 +4,7 @@ # Check that perf trace works with record and replay -# shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh -# shellcheck source=lib/perf_record.sh . "$(dirname $0)"/lib/perf_record.sh skip_if_no_perf_trace || exit 2 diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index b80dea77cec6..d5da1d7d8d88 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -4,7 +4,6 @@ # Check that perf trace works with various summary mode -# shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh skip_if_no_perf_trace || exit 2 From 1bb97afa30aa6668ac76a834af7d81b80ae9115f Mon Sep 17 00:00:00 2001 From: James Clark Date: Mon, 21 Sep 2026 16:47:29 +0100 Subject: [PATCH 140/425] perf build: Follow sourced files when running shellcheck Shellcheck follows sourced scripts when -a is provided, but only when that file is also provided on the command line along with the one being checked. We only ever provide one file at a time to shellcheck, so -a does nothing. Improve coverage by following sources using the script's directory as the base path. Signed-off-by: James Clark Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Makefile.perf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf index 53f538cc8e1a..e91d6138330e 100644 --- a/tools/perf/Makefile.perf +++ b/tools/perf/Makefile.perf @@ -255,7 +255,7 @@ ifneq ($(SHELLCHECK),) sed -e 's/.\+ \([0-9]\+\).\([0-9]\+\).\([0-9]\+\)/\1\2\3/g') \< 072), 1) SHELLCHECK := else - SHELLCHECK := $(SHELLCHECK) -s bash -a -S warning + SHELLCHECK := $(SHELLCHECK) -s bash -a -S warning -x -P SCRIPTDIR endif endif From fcf1fccdc9d5d7fe463c6c1869c9af6edbdd59eb Mon Sep 17 00:00:00 2001 From: Jianping Liu Date: Thu, 23 Apr 2026 18:35:30 +0800 Subject: [PATCH 141/425] perf annotate: Fix NULL pointer dereference in loongarch_call__parse loongarch_call__parse() dereferences the return value of strchr() without NULL check: name = strchr(endptr, '<'); name++; When objdump output for a 'bl' (branch-and-link) instruction does not contain a symbol name delimited by '<' and '>' (e.g., when the call target has no associated symbol), strchr() returns NULL, and the subsequent name++ produces a wild pointer (0x1). This leads to a segmentation fault when strchr() is later called with this invalid pointer: #0 __strchr_lasx () from /lib64/libc.so.6 #1 loongarch_call__parse () at arch/loongarch/annotate/instructions.c:29 #2 disasm_line__init_ins () at util/annotate.c:1205 ... The objdump line triggering the crash looks like: bl 98824 # 0x9000000000641ea0 Note the absence of "" after the address. Rather than aborting parse with -1 (which would clear dl->ins.ops and lose the is_call attribute, preventing the TUI from drawing branch arrows and navigating the call), follow the same pattern as the generic call__parse() in util/disasm.c: when the symbol name is absent, jump to the address-based symbol lookup so that ops->target.sym can still be resolved via maps__find_ams() using the already-parsed target address. This preserves is_call semantics and keeps TUI navigation working. Reviewed-by: Ming Wang Tested-by: WANG Rui Signed-off-by: Jianping Liu Acked-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/annotate-arch/annotate-loongarch.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/perf/util/annotate-arch/annotate-loongarch.c b/tools/perf/util/annotate-arch/annotate-loongarch.c index c2addca77320..a949b4392dd4 100644 --- a/tools/perf/util/annotate-arch/annotate-loongarch.c +++ b/tools/perf/util/annotate-arch/annotate-loongarch.c @@ -29,6 +29,9 @@ static int loongarch_call__parse(const struct arch *arch, struct ins_operands *o ops->target.addr = strtoull(c, &endptr, 16); name = strchr(endptr, '<'); + if (name == NULL) + goto find_target; + name++; if (arch->objdump.skip_functions_char && @@ -46,6 +49,7 @@ static int loongarch_call__parse(const struct arch *arch, struct ins_operands *o if (ops->target.name == NULL) return -1; +find_target: target = (struct addr_map_symbol) { .ms = { .map = map__get(map), }, .addr = map__objdump_2mem(map, ops->target.addr), From fe529886843f451ad6da7e65d53325432e0f80fb Mon Sep 17 00:00:00 2001 From: Tengda Wu Date: Thu, 17 Sep 2026 12:58:02 +0000 Subject: [PATCH 142/425] perf llvm: Fix memory leak of args->fileloc in symbol__disassemble_llvm() In symbol__disassemble_llvm(), after calling llvm_addr2line(), if the subsequent disasm_line__new() fails, the code directly jumps to 'err' without freeing args->fileloc, leading to a memory leak. Fix this by explicitly calling free(args->fileloc) before jumping to the error handling path when disasm_line__new() fails. Fixes: 04885681788855bb ("perf annotate: LLVM-based disassembler") Reviewed-by: Ian Rogers Signed-off-by: Tengda Wu Cc: Steinar H. Gunderson Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/llvm.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/perf/util/llvm.c b/tools/perf/util/llvm.c index a78845689aa1..722e71aab100 100644 --- a/tools/perf/util/llvm.c +++ b/tools/perf/util/llvm.c @@ -296,8 +296,10 @@ int symbol__disassemble_llvm(const char *filename, struct symbol *sym, (unsigned int *)&args->line_nr, false, NULL); dl = disasm_line__new(args); - if (dl == NULL) + if (dl == NULL) { + free(args->fileloc); goto err; + } annotation_line__add(&dl->al, ¬es->src->source); From 1a097d93caf62c4e6c834bff81609512070e5008 Mon Sep 17 00:00:00 2001 From: Sandipan Das Date: Thu, 17 Sep 2026 15:29:21 +0530 Subject: [PATCH 143/425] perf jevents: Limit the number of JSON reading workers The ProcessPoolExecutor is created without max_workers, so concurrent.futures defaults to one worker per CPU and forks all of them when the first task is submitted. Each worker costs the parent a few file descriptors. For systems with high core-count processors like the AMD EPYC 9996, the pool can easily exhaust RLIMIT_NOFILE as seen below. File ".../multiprocessing/popen_fork.py", line 65, in _launch child_r, parent_w = os.pipe() OSError: [Errno 24] Too many open files The error is reported, but jevents.py does not exit. The workers that did start stay blocked waiting for work and are never joined, so the build eventually hangs. Other targets keep building in the meantime, leaving the traceback several lines above the point where the build stalls. Limit the pool instead of sizing it against the descriptor limit, which would mean relying on how many descriptors concurrent.futures needs for each worker. At least for recent AMD EPYC processors, reading the JSON files does not need many workers to be fast and things typically stop scaling beyond 32 workers. Hence, a small upper bound keeps the speedup and stays well inside any usable limit. Fixes: eaab2eb09dc2f86f ("perf pmu-events: Parallelize JSON and metric pre-computation in jevents.py") Reviewed-by: Ian Rogers Signed-off-by: Sandipan Das Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/pmu-events/jevents.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py index 93cf04fa8ca4..074daae5308b 100755 --- a/tools/perf/pmu-events/jevents.py +++ b/tools/perf/pmu-events/jevents.py @@ -1450,6 +1450,13 @@ const char *describe_metricgroup(const char *group) } """) +# Upper bound on the number of workers reading JSON files in parallel. +_max_parallel_workers = 32 + +def _parallel_worker_count(num_tasks: int) -> int: + return max(1, min(getattr(os, 'process_cpu_count', os.cpu_count)() or 1, + _max_parallel_workers, num_tasks)) + def _parallel_read_json_events(task: Tuple[str, str]) -> Tuple[str, str, Sequence[JsonEvent]]: path, topic = task return path, topic, _read_json_events_impl(path, topic) @@ -1549,7 +1556,9 @@ struct pmu_table_entry { preprocess_arch_std_files(arch_path) ftw(arch_path, [], collect_json) - with concurrent.futures.ProcessPoolExecutor(initializer=_init_worker, initargs=(_arch_std_events,)) as executor: + with concurrent.futures.ProcessPoolExecutor(max_workers=_parallel_worker_count(len(tasks)), + initializer=_init_worker, + initargs=(_arch_std_events,)) as executor: for path, topic, events in executor.map(_parallel_read_json_events, tasks): _json_cache[(path, topic)] = events From da5f6a6a3221d3841dd0b12f9de8e5d0a2453f21 Mon Sep 17 00:00:00 2001 From: Namhyung Kim Date: Wed, 23 Sep 2026 14:52:53 -0700 Subject: [PATCH 144/425] perf test: Fix errors when deleting data in 'perf record' tests 'perf record' with multithreads would create a data directory instead of a file. But the cleanup and retry logic expect a file and try to delete them with 'rm -f' which would fail. Add '-r' flag to remove directories. rm: cannot remove '/tmp/__perf_test.perf.data.ZVHTw.old': Is a directory Signed-off-by: Namhyung Kim Tested-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/lib/perf_record.sh | 2 +- tools/perf/tests/shell/record.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/perf/tests/shell/lib/perf_record.sh b/tools/perf/tests/shell/lib/perf_record.sh index 2b9e11b66dc7..a95fafb8b6fa 100644 --- a/tools/perf/tests/shell/lib/perf_record.sh +++ b/tools/perf/tests/shell/lib/perf_record.sh @@ -30,7 +30,7 @@ perf_record_with_retry() { fi for duration in 0.01 0.1 0.3 1.0 2.0; do - rm -f "${perfdata}".old + rm -rf "${perfdata}".old ${cmd_prefix} "$@" -o "${perfdata}" ${testprog_base} ${duration} > "$logfile" 2>&1 local record_exit=$? diff --git a/tools/perf/tests/shell/record.sh b/tools/perf/tests/shell/record.sh index a13e0fd9c0f7..c7a7ac128d6f 100755 --- a/tools/perf/tests/shell/record.sh +++ b/tools/perf/tests/shell/record.sh @@ -37,8 +37,8 @@ default_fd_limit=$(ulimit -Sn) min_fd_limit=$(($(getconf _NPROCESSORS_ONLN) * 16)) cleanup() { - rm -f "${perfdata}" - rm -f "${perfdata}".old + rm -rf "${perfdata}" + rm -rf "${perfdata}".old rm -f "${script_output}" perf_record_cleanup From 79b617136e9d2da52f61cb67f2b30d769312c379 Mon Sep 17 00:00:00 2001 From: Namhyung Kim Date: Wed, 23 Sep 2026 14:52:54 -0700 Subject: [PATCH 145/425] perf test: Fix record tests on Intel Broadwell I noticed it failed for leader sampling tests on Broadwell. It's because the period of groups members are a little bit different. Maybe there's an hardware issue but anyway it's close enough to treat them as same. I printed the value like below. $ perf test record ... prev: 462507, curr: 462506 prev: 494783, curr: 494782 prev: 454350, curr: 454349 prev: 502782, curr: 502781 prev: 566693, curr: 566692 prev: 606558, curr: 606557 prev: 559770, curr: 559769 prev: 595737, curr: 595736 prev: 643386, curr: 643385 prev: 663672, curr: 663671 prev: 620622, curr: 620621 prev: 650419, curr: 650418 Leader sampling [Failed inconsistent cycles count] Let's update the logic to compare values to allow 1% of errors. Also I think it should compare only if sees the non-leader samples so split the check of (($index % 2)). Reviewed-by: Dapeng Mi Signed-off-by: Namhyung Kim Tested-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/record.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/record.sh b/tools/perf/tests/shell/record.sh index c7a7ac128d6f..a1314e40d7b2 100755 --- a/tools/perf/tests/shell/record.sh +++ b/tools/perf/tests/shell/record.sh @@ -548,11 +548,14 @@ test_leader_sampling() { while IFS= read -r line do cycles=$(echo $line | awk '{for(i=1;i<=NF;i++) if($i=="cycles:") print $(i-1)}') - if [ $(($index%2)) -ne 0 ] && [ ${cycles}x != ${prev_cycles}x ] + if [ $(($index%2)) -ne 0 ] then - invalid_counts=$(($invalid_counts+1)) - else - valid_counts=$(($valid_counts+1)) + if (( $(bc <<< "scale=4; r = ${cycles} / ${prev_cycles}; r >= 0.99 && r <= 1.01") )) + then + valid_counts=$(($valid_counts+1)) + else + invalid_counts=$(($invalid_counts+1)) + fi fi index=$(($index+1)) prev_cycles=$cycles From 9d86dd191dbd2636619543d2e84c85d7e465b8c9 Mon Sep 17 00:00:00 2001 From: Namhyung Kim Date: Wed, 23 Sep 2026 14:52:55 -0700 Subject: [PATCH 146/425] perf test: Fix record tests on hybrid machines Currently the leader sampling test looks for "cycles:" string to match the event and retrieve the value before the event name. But it won't work on hybrid systems as the event name is different like below. $ perf script | grep -m 10 brstack perf 3322152 1873980.978431: 625379 cpu_atom/cycles/: 558200cc40cf brstack_bench+0x31 (/home/namhyung/project/linux/tools/perf/perf) perf 3322152 1873980.978431: 625379 cpu_atom/cycles/: 558200cc40cf brstack_bench+0x31 (/home/namhyung/project/linux/tools/perf/perf) perf 3322152 1873980.978598: 612177 cpu_atom/cycles/: 558200cc4082 brstack_foo+0x17 (/home/namhyung/project/linux/tools/perf/perf) perf 3322152 1873980.978598: 612177 cpu_atom/cycles/: 558200cc4082 brstack_foo+0x17 (/home/namhyung/project/linux/tools/perf/perf) perf 3322152 1873980.978762: 591859 cpu_atom/cycles/: 558200cc403d brstack_bar+0x0 (/home/namhyung/project/linux/tools/perf/perf) perf 3322152 1873980.978762: 591859 cpu_atom/cycles/: 558200cc403d brstack_bar+0x0 (/home/namhyung/project/linux/tools/perf/perf) perf 3322152 1873980.978948: 628505 cpu_atom/cycles/: 558200cc403e brstack_bar+0x1 (/home/namhyung/project/linux/tools/perf/perf) perf 3322152 1873980.978948: 628505 cpu_atom/cycles/: 558200cc403e brstack_bar+0x1 (/home/namhyung/project/linux/tools/perf/perf) perf 3322152 1873980.979146: 669816 cpu_atom/cycles/: 558200cc40e5 brstack_bench+0x47 (/home/namhyung/project/linux/tools/perf/perf) perf 3322152 1873980.979146: 669816 cpu_atom/cycles/: 558200cc40e5 brstack_bench+0x47 (/home/namhyung/project/linux/tools/perf/perf) But we can control the output format of perf script using -F option. So we knows where the period is in the output. To filter by symbol name, "ip" and "sym" fields are required as well. $ perf script -F period,ip,sym | grep -m 10 brstack 625379 558200cc40cf brstack_bench 625379 558200cc40cf brstack_bench 612177 558200cc4082 brstack_foo 612177 558200cc4082 brstack_foo 591859 558200cc403d brstack_bar 591859 558200cc403d brstack_bar 628505 558200cc403e brstack_bar 628505 558200cc403e brstack_bar 669816 558200cc40e5 brstack_bench 669816 558200cc40e5 brstack_bench Signed-off-by: Namhyung Kim Tested-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/record.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/perf/tests/shell/record.sh b/tools/perf/tests/shell/record.sh index a1314e40d7b2..01dc3e22fa73 100755 --- a/tools/perf/tests/shell/record.sh +++ b/tools/perf/tests/shell/record.sh @@ -535,7 +535,7 @@ test_leader_sampling() { err=1 return fi - perf script -i "${perfdata}" | grep brstack > $script_output + perf script -i "${perfdata}" -F period,ip,sym | grep brstack > $script_output # Check if the two instruction counts are equal in each record. # However, the throttling code doesn't consider event grouping. During throttling, only the # leader is stopped, causing the slave's counts significantly higher. To temporarily solve this, @@ -547,7 +547,7 @@ test_leader_sampling() { tolerance_rate=0.8 while IFS= read -r line do - cycles=$(echo $line | awk '{for(i=1;i<=NF;i++) if($i=="cycles:") print $(i-1)}') + cycles=$(echo $line | awk '{ print $1 }') if [ $(($index%2)) -ne 0 ] then if (( $(bc <<< "scale=4; r = ${cycles} / ${prev_cycles}; r >= 0.99 && r <= 1.01") )) From 97e94e5c5af1aef53f2fefd00641a5407cf282a9 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 23 Sep 2026 15:41:06 -0700 Subject: [PATCH 147/425] perf test: Fix PMU metric parsing tests for unknown literals On ARM the #slots literal may or may not be present, it won't be present on x86. As it is missing it causes the ampereone and ampereonex PMU metric tests to fail when run say on x86. Extend the fake_pmu logic so that in these test cases we fake the unknown literals like #slots as 1.0. When building with JEVENTS_ARCH=all the ampere metric tests no longer fail. Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/expr.c | 5 ++++- tools/perf/util/metricgroup.c | 37 ++++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/tools/perf/util/expr.c b/tools/perf/util/expr.c index 8aef3c7418f7..f1164c4666b1 100644 --- a/tools/perf/util/expr.c +++ b/tools/perf/util/expr.c @@ -431,7 +431,10 @@ double expr__get_literal(const char *literal, const struct expr_scanner_ctx *ctx } pr_debug2("literal: %s = %f\n", literal, result); - return result; + if (!ctx->is_test) + return result; + else + return isnan(result) ? 1.0 : result; } /* Does the event 'id' parse? Determine via ctx->ids if possible. */ diff --git a/tools/perf/util/metricgroup.c b/tools/perf/util/metricgroup.c index 5a60cb95e31c..779fe56fe8d0 100644 --- a/tools/perf/util/metricgroup.c +++ b/tools/perf/util/metricgroup.c @@ -224,7 +224,8 @@ static struct metric *metric__new(const struct pmu_metric *pm, bool metric_no_threshold, int runtime, const char *user_requested_cpu_list, - bool system_wide) + bool system_wide, + bool fake_pmu) { struct metric *m; @@ -256,6 +257,7 @@ static struct metric *metric__new(const struct pmu_metric *pm, } m->pctx->sctx.runtime = runtime; m->pctx->sctx.system_wide = system_wide; + m->pctx->sctx.is_test = fake_pmu; m->group_events = !metric_no_group && metric__group_events(pm, metric_no_threshold); m->default_show_events = pm->default_show_events; m->metric_refs = NULL; @@ -699,6 +701,7 @@ static int add_metric(struct list_head *metric_list, bool metric_no_threshold, const char *user_requested_cpu_list, bool system_wide, + bool fake_pmu, struct metric *root_metric, const struct visited_metric *visited, const struct pmu_metrics_table *table); @@ -724,6 +727,7 @@ static int metricgroup__find_metric_callback(const struct pmu_metric *pm, * user may override. * @user_requested_cpu_list: Command line specified CPUs to record on. * @system_wide: Are events for all processes recorded. + * @fake_pmu: Are PMUs and events being faked for testing? * @root_metric: Metrics may reference other metrics to form a tree. In this * case the root_metric holds all the IDs and a list of referenced * metrics. When adding a root this argument is NULL. @@ -739,6 +743,7 @@ static int resolve_metric(struct list_head *metric_list, bool metric_no_threshold, const char *user_requested_cpu_list, bool system_wide, + bool fake_pmu, struct metric *root_metric, const struct visited_metric *visited, const struct pmu_metrics_table *table) @@ -788,7 +793,7 @@ static int resolve_metric(struct list_head *metric_list, for (i = 0; i < pending_cnt; i++) { ret = add_metric(metric_list, &pending[i].pm, modifier, metric_no_group, metric_no_threshold, user_requested_cpu_list, system_wide, - root_metric, visited, table); + fake_pmu, root_metric, visited, table); if (ret) break; } @@ -809,6 +814,7 @@ static int resolve_metric(struct list_head *metric_list, * @runtime: A special argument for the parser only known at runtime. * @user_requested_cpu_list: Command line specified CPUs to record on. * @system_wide: Are events for all processes recorded. + * @fake_pmu: Are PMUs and events being faked for testing? * @root_metric: Metrics may reference other metrics to form a tree. In this * case the root_metric holds all the IDs and a list of referenced * metrics. When adding a root this argument is NULL. @@ -825,6 +831,7 @@ static int __add_metric(struct list_head *metric_list, int runtime, const char *user_requested_cpu_list, bool system_wide, + bool fake_pmu, struct metric *root_metric, const struct visited_metric *visited, const struct pmu_metrics_table *table) @@ -851,7 +858,7 @@ static int __add_metric(struct list_head *metric_list, * metrics that are added recursively. */ root_metric = metric__new(pm, modifier, metric_no_group, metric_no_threshold, - runtime, user_requested_cpu_list, system_wide); + runtime, user_requested_cpu_list, system_wide, fake_pmu); if (!root_metric) return -ENOMEM; @@ -924,7 +931,7 @@ static int __add_metric(struct list_head *metric_list, ret = resolve_metric(metric_list, pmu, modifier, metric_no_group, metric_no_threshold, user_requested_cpu_list, - system_wide, root_metric, &visited_node, + system_wide, fake_pmu, root_metric, &visited_node, table); } if (ret) { @@ -944,6 +951,7 @@ static int add_metric(struct list_head *metric_list, bool metric_no_threshold, const char *user_requested_cpu_list, bool system_wide, + bool fake_pmu, struct metric *root_metric, const struct visited_metric *visited, const struct pmu_metrics_table *table) @@ -955,7 +963,7 @@ static int add_metric(struct list_head *metric_list, if (!strstr(pm->metric_expr, "?")) { ret = __add_metric(metric_list, pm, modifier, metric_no_group, metric_no_threshold, 0, user_requested_cpu_list, - system_wide, root_metric, visited, table); + system_wide, fake_pmu, root_metric, visited, table); } else { int j, count; @@ -969,7 +977,7 @@ static int add_metric(struct list_head *metric_list, for (j = 0; j < count && !ret; j++) ret = __add_metric(metric_list, pm, modifier, metric_no_group, metric_no_threshold, j, user_requested_cpu_list, - system_wide, root_metric, visited, table); + system_wide, fake_pmu, root_metric, visited, table); } return ret; @@ -1030,6 +1038,7 @@ struct metricgroup__add_metric_data { bool metric_no_group; bool metric_no_threshold; bool system_wide; + bool fake_pmu; bool has_match; }; @@ -1047,7 +1056,7 @@ static int metricgroup__add_metric_callback(const struct pmu_metric *pm, data->has_match = true; ret = add_metric(data->list, pm, data->modifier, metric_no_group, data->metric_no_threshold, data->user_requested_cpu_list, - data->system_wide, /*root_metric=*/NULL, + data->system_wide, data->fake_pmu, /*root_metric=*/NULL, /*visited_metrics=*/NULL, table); } return ret; @@ -1065,6 +1074,7 @@ static int metricgroup__add_metric_callback(const struct pmu_metric *pm, * user may override. * @user_requested_cpu_list: Command line specified CPUs to record on. * @system_wide: Are events for all processes recorded. + * @fake_pmu: Are PMUs and events being faked for testing? * @metric_list: The list that the metric or metric group are added to. * @table: The table that is searched for metrics, most commonly the table for the * architecture perf is running upon. @@ -1072,7 +1082,7 @@ static int metricgroup__add_metric_callback(const struct pmu_metric *pm, static int metricgroup__add_metric(const char *pmu, const char *metric_name, const char *modifier, bool metric_no_group, bool metric_no_threshold, const char *user_requested_cpu_list, - bool system_wide, + bool system_wide, bool fake_pmu, struct list_head *metric_list, const struct pmu_metrics_table *table) { @@ -1087,6 +1097,7 @@ static int metricgroup__add_metric(const char *pmu, const char *metric_name, con .metric_no_threshold = metric_no_threshold, .user_requested_cpu_list = user_requested_cpu_list, .system_wide = system_wide, + .fake_pmu = fake_pmu, .has_match = false, }; @@ -1118,6 +1129,7 @@ static int metricgroup__add_metric(const char *pmu, const char *metric_name, con * user may override. * @user_requested_cpu_list: Command line specified CPUs to record on. * @system_wide: Are events for all processes recorded. + * @fake_pmu: Are PMUs and events being faked for testing? * @metric_list: The list that metrics are added to. * @table: The table that is searched for metrics, most commonly the table for the * architecture perf is running upon. @@ -1126,7 +1138,8 @@ static int metricgroup__add_metric_list(const char *pmu, const char *list, bool metric_no_group, bool metric_no_threshold, const char *user_requested_cpu_list, - bool system_wide, struct list_head *metric_list, + bool system_wide, bool fake_pmu, + struct list_head *metric_list, const struct pmu_metrics_table *table) { char *list_itr, *list_copy, *metric_name, *modifier; @@ -1145,7 +1158,8 @@ static int metricgroup__add_metric_list(const char *pmu, const char *list, ret = metricgroup__add_metric(pmu, metric_name, modifier, metric_no_group, metric_no_threshold, user_requested_cpu_list, - system_wide, metric_list, table); + system_wide, fake_pmu, + metric_list, table); if (ret == -EINVAL) pr_err("Fail to parse metric or group `%s'\n", metric_name); else if (ret == -ENOENT) @@ -1403,7 +1417,8 @@ static int parse_groups(struct evlist *perf_evlist, ret = metricgroup__add_metric_list(pmu, str, metric_no_group, metric_no_threshold, user_requested_cpu_list, - system_wide, &metric_list, table); + system_wide, fake_pmu, + &metric_list, table); if (ret) goto out; From 60d454456f1d3cb1e09f1e07763d96f80dc4c567 Mon Sep 17 00:00:00 2001 From: Ethan Nelson-Moore Date: Thu, 17 Sep 2026 20:23:53 -0700 Subject: [PATCH 148/425] perf tools: Remove unused empty help-unknown-cmd.h header tools/perf/util/help-unknown-cmd.h has been empty and unused ever since it was added to the kernel in commit 5feaac248a46dd5f ("perf tools: Move help_unknown_cmd() to its own file") to its own file"). Remove it. Signed-off-by: Ethan Nelson-Moore Reviewed-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/help-unknown-cmd.h | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tools/perf/util/help-unknown-cmd.h diff --git a/tools/perf/util/help-unknown-cmd.h b/tools/perf/util/help-unknown-cmd.h deleted file mode 100644 index e69de29bb2d1..000000000000 From 7e27239d2230394a797e2791bccf7ad24fc45c43 Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Tue, 22 Sep 2026 21:19:52 +0800 Subject: [PATCH 149/425] perf symbols: Apply the symfs flat layout to the composed filename --symfs=,flat documents itself as matching only the base name, so a debuginfo lookup for /usr/lib/x86_64-linux-gnu/libc.so.6 should land on /libc.so.6.debug. It lands on /debug/usr/lib/x86_64-linux-gnu/libc.so.6.debug instead, because the layout is applied to a fixed prefix and the file name arrives afterwards: len = __symbol__join_symfs(filename, size, "/usr/lib/debug"); snprintf(filename + len, size - len, "%s.debug", dso__long_name(dso)); perf_basename() only ever sees "/usr/lib/debug", so neither layout comes out of it. Compose the path as it would appear on the profiled system and join it once, which puts the layout on the whole name. That covers FEDORA_DEBUGINFO, UBUNTU_DEBUGINFO, MIXEDUP_UBUNTU_DEBUGINFO, OPENEMBEDDED_DEBUGINFO and BUILDID_DEBUGINFO, plus the same composition in build_id_cache__find_debug(). BUILDID_DEBUGINFO needs one more step: the build id cache splits the first two characters of the build id off into a directory name, so the basename of the hierarchy path is only part of the build id. Name the flat file after the whole build id. dso__disassemble_filename() passes a path from perf's own build id cache rather than one on the profiled system, and every entry there is named "elf" or "debug". Use path__join() so the layout does not apply. With --symfs /s,flat the lookups are now /s/libc.so.6, /s/libc.so.6.debug, /s/sleep and /s/.debug. hierarchy is unchanged. Reviewed-by: Namhyung Kim Signed-off-by: Zhan Xusheng Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/build-id.c | 12 +++++++---- tools/perf/util/disasm.c | 8 ++++++- tools/perf/util/dso.c | 43 +++++++++++++++++++++++++------------- 3 files changed, 44 insertions(+), 19 deletions(-) diff --git a/tools/perf/util/build-id.c b/tools/perf/util/build-id.c index eb95ab90f974..02233ef0eaff 100644 --- a/tools/perf/util/build-id.c +++ b/tools/perf/util/build-id.c @@ -586,9 +586,9 @@ static char *build_id_cache__find_debug(const char *sbuild_id, const char *dirname = "/usr/lib/debug/.build-id/"; char *realname = NULL; char dirbuf[PATH_MAX]; + char pathbuf[PATH_MAX]; char *debugfile; struct nscookie nsc; - size_t len = 0; debugfile = calloc(1, PATH_MAX); if (!debugfile) @@ -599,9 +599,13 @@ static char *build_id_cache__find_debug(const char *sbuild_id, dirname = dirbuf; } - len = __symbol__join_symfs(debugfile, PATH_MAX, dirname); - snprintf(debugfile + len, PATH_MAX - len, "%.2s/%s.debug", sbuild_id, - sbuild_id + 2); + /* See the build id note in dso__read_binary_type_filename(). */ + if (symbol_conf.symfs_layout_flat) + scnprintf(pathbuf, PATH_MAX, "/%s.debug", sbuild_id); + else + scnprintf(pathbuf, PATH_MAX, "%s%.2s/%s.debug", dirname, + sbuild_id, sbuild_id + 2); + __symbol__join_symfs(debugfile, PATH_MAX, pathbuf); nsinfo__mountns_enter(nsi, &nsc); realname = realpath(debugfile, NULL); diff --git a/tools/perf/util/disasm.c b/tools/perf/util/disasm.c index b39636ec35c1..5478c134e7e3 100644 --- a/tools/perf/util/disasm.c +++ b/tools/perf/util/disasm.c @@ -31,6 +31,7 @@ #include "map.h" #include "maps.h" #include "namespaces.h" +#include "path.h" #include "srcline.h" #include "symbol.h" #include "thread.h" @@ -1189,7 +1190,12 @@ static int dso__disassemble_filename(struct dso *dso, char *filename, size_t fil build_id_filename = dso__build_id_filename(dso, NULL, 0, false); if (build_id_filename) { - __symbol__join_symfs(filename, filename_size, build_id_filename); + /* + * This is a path in perf's own build id cache, not a path on + * the profiled system, so the symfs layout does not apply. + */ + path__join(filename, filename_size, symbol_conf.symfs, + build_id_filename); free(build_id_filename); } else { if (dso__has_build_id(dso)) diff --git a/tools/perf/util/dso.c b/tools/perf/util/dso.c index 9e90de92fcfa..11db461f2032 100644 --- a/tools/perf/util/dso.c +++ b/tools/perf/util/dso.c @@ -121,6 +121,7 @@ int dso__read_binary_type_filename(const struct dso *dso, const char *root_dir, char *filename, size_t size) { char build_id_hex[SBUILD_ID_SIZE]; + char relative[PATH_MAX]; int ret = 0; size_t len; @@ -173,13 +174,15 @@ int dso__read_binary_type_filename(const struct dso *dso, break; case DSO_BINARY_TYPE__FEDORA_DEBUGINFO: - len = __symbol__join_symfs(filename, size, "/usr/lib/debug"); - snprintf(filename + len, size - len, "%s.debug", dso__long_name(dso)); + snprintf(relative, sizeof(relative), "/usr/lib/debug%s.debug", + dso__long_name(dso)); + __symbol__join_symfs(filename, size, relative); break; case DSO_BINARY_TYPE__UBUNTU_DEBUGINFO: - len = __symbol__join_symfs(filename, size, "/usr/lib/debug"); - snprintf(filename + len, size - len, "%s", dso__long_name(dso)); + snprintf(relative, sizeof(relative), "/usr/lib/debug%s", + dso__long_name(dso)); + __symbol__join_symfs(filename, size, relative); break; case DSO_BINARY_TYPE__MIXEDUP_UBUNTU_DEBUGINFO: @@ -193,8 +196,9 @@ int dso__read_binary_type_filename(const struct dso *dso, ret = -1; break; } - len = __symbol__join_symfs(filename, size, "/usr/lib/debug"); - snprintf(filename + len, size - len, "%s", dso__long_name(dso) + 4); + snprintf(relative, sizeof(relative), "/usr/lib/debug%s", + dso__long_name(dso) + 4); + __symbol__join_symfs(filename, size, relative); break; case DSO_BINARY_TYPE__OPENEMBEDDED_DEBUGINFO: @@ -206,15 +210,15 @@ int dso__read_binary_type_filename(const struct dso *dso, while (last_slash != dso__long_name(dso) && *last_slash != '/') last_slash--; - len = __symbol__join_symfs(filename, size, ""); dir_size = last_slash - dso__long_name(dso) + 2; - if (dir_size > (size - len)) { + if (dir_size > sizeof(relative)) { ret = -1; break; } - len += scnprintf(filename + len, dir_size, "%s", dso__long_name(dso)); - len += scnprintf(filename + len , size - len, ".debug%s", - last_slash); + len = scnprintf(relative, dir_size, "%s", dso__long_name(dso)); + scnprintf(relative + len, sizeof(relative) - len, ".debug%s", + last_slash); + __symbol__join_symfs(filename, size, relative); break; } @@ -225,9 +229,20 @@ int dso__read_binary_type_filename(const struct dso *dso, } build_id__snprintf(dso__bid(dso), build_id_hex, sizeof(build_id_hex)); - len = __symbol__join_symfs(filename, size, "/usr/lib/debug/.build-id/"); - snprintf(filename + len, size - len, "%.2s/%s.debug", - build_id_hex, build_id_hex + 2); + /* + * The build id cache layout splits the first two characters off + * into a directory name, so the basename of the hierarchy path + * is only part of the build id. Name the flat file after the + * whole build id instead. + */ + if (symbol_conf.symfs_layout_flat) + snprintf(relative, sizeof(relative), "/%s.debug", + build_id_hex); + else + snprintf(relative, sizeof(relative), + "/usr/lib/debug/.build-id/%.2s/%s.debug", + build_id_hex, build_id_hex + 2); + __symbol__join_symfs(filename, size, relative); break; case DSO_BINARY_TYPE__VMLINUX: From d227b45fc162ce9134bb96f33f71e6952ec36ea5 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 17 Sep 2026 23:26:20 -0700 Subject: [PATCH 150/425] perf machine: Add session back pointer to fix out of bounds read thread__e_machine_endian() decides whether a thread is live, and so whether to read /proc/pid/exe rather than fall back to the perf.data environment, by recovering the session from the machine: session = container_of(machine->machines, struct perf_session, machines); is_live = !session->data; That only holds when the struct machines is the one embedded in a struct perf_session. It is not the only one. aslr_tool keeps its own in struct aslr_tool, and hists_cumulate, hists_filter, hists_link, hists_output and thread-maps-share each put one on the stack. For those the container_of subtracts offsetof(struct perf_session, machines) from an address that was never inside a session, and the following load of session->data reads outside the object, which is what ASan reports. Replace the derivation with an explicit link. machines__init() takes the owning session, or NULL when the machines is standalone, and thread__e_machine_endian() reads it back through machines__session(). A NULL session is treated as live. The tests use synthetic pids that have no /proc entry, so the read fails there and the e_machine falls back to EM_HOST as before, rather than being decided by whatever the out of bounds read happened to return. perf inject registers its session with the aslr tool once the session exists, before any event is processed, so that tool keeps reporting file based threads as not live. Fixes: 70351029b556 ("perf thread: Add support for reading the e_machine type for a thread") Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-inject.c | 3 +++ tools/perf/tests/hists_cumulate.c | 2 +- tools/perf/tests/hists_filter.c | 2 +- tools/perf/tests/hists_link.c | 2 +- tools/perf/tests/hists_output.c | 2 +- tools/perf/tests/thread-maps-share.c | 3 ++- tools/perf/util/aslr.c | 10 +++++++++- tools/perf/util/aslr.h | 2 ++ tools/perf/util/machine.c | 3 ++- tools/perf/util/machine.h | 10 +++++++++- tools/perf/util/session.c | 2 +- tools/perf/util/thread.c | 8 +++----- 12 files changed, 35 insertions(+), 14 deletions(-) diff --git a/tools/perf/builtin-inject.c b/tools/perf/builtin-inject.c index 169617a884d3..ce7562fc788c 100644 --- a/tools/perf/builtin-inject.c +++ b/tools/perf/builtin-inject.c @@ -2881,6 +2881,9 @@ int cmd_inject(int argc, const char **argv) goto out_close_output; } + if (inject.aslr) + aslr_tool__register_session(tool, inject.session); + if (zstd_init(&(inject.session->zstd_data), 0) < 0) pr_warning("Decompression initialization failed.\n"); diff --git a/tools/perf/tests/hists_cumulate.c b/tools/perf/tests/hists_cumulate.c index 9356451a172e..c4a11a3255bc 100644 --- a/tools/perf/tests/hists_cumulate.c +++ b/tools/perf/tests/hists_cumulate.c @@ -723,7 +723,7 @@ static int test__hists_cumulate(struct test_suite *test __maybe_unused, int subt goto out; err = TEST_FAIL; - if (machines__init(&machines)) + if (machines__init(&machines, /*session=*/NULL)) goto out; /* setup threads/dso/map/symbols also */ diff --git a/tools/perf/tests/hists_filter.c b/tools/perf/tests/hists_filter.c index f9eaa511487b..6c7e4cdc71a7 100644 --- a/tools/perf/tests/hists_filter.c +++ b/tools/perf/tests/hists_filter.c @@ -131,7 +131,7 @@ static int test__hists_filter(struct test_suite *test __maybe_unused, int subtes goto out; err = TEST_FAIL; - if (machines__init(&machines)) + if (machines__init(&machines, /*session=*/NULL)) goto out; /* setup threads/dso/map/symbols also */ diff --git a/tools/perf/tests/hists_link.c b/tools/perf/tests/hists_link.c index d88591bcbe50..cd32c8277627 100644 --- a/tools/perf/tests/hists_link.c +++ b/tools/perf/tests/hists_link.c @@ -303,7 +303,7 @@ static int test__hists_link(struct test_suite *test __maybe_unused, int subtest goto out; err = TEST_FAIL; - if (machines__init(&machines)) + if (machines__init(&machines, /*session=*/NULL)) goto out; /* setup threads/dso/map/symbols also */ diff --git a/tools/perf/tests/hists_output.c b/tools/perf/tests/hists_output.c index f58c8d18fe33..35d230a83e43 100644 --- a/tools/perf/tests/hists_output.c +++ b/tools/perf/tests/hists_output.c @@ -610,7 +610,7 @@ static int test__hists_output(struct test_suite *test __maybe_unused, int subtes goto out; err = TEST_FAIL; - if (machines__init(&machines)) + if (machines__init(&machines, /*session=*/NULL)) goto out; /* setup threads/dso/map/symbols also */ diff --git a/tools/perf/tests/thread-maps-share.c b/tools/perf/tests/thread-maps-share.c index 0431bff31b3a..4a0bbf715acb 100644 --- a/tools/perf/tests/thread-maps-share.c +++ b/tools/perf/tests/thread-maps-share.c @@ -27,7 +27,8 @@ static int test__thread_maps_share(struct test_suite *test __maybe_unused, int s * other group (pid: 4, tids: 4, 5) */ - TEST_ASSERT_VAL("failed to init machines", machines__init(&machines) == 0); + TEST_ASSERT_VAL("failed to init machines", + machines__init(&machines, /*session=*/NULL) == 0); machine = &machines.host; /* create process with 4 threads */ diff --git a/tools/perf/util/aslr.c b/tools/perf/util/aslr.c index a097084201a0..e365f807b021 100644 --- a/tools/perf/util/aslr.c +++ b/tools/perf/util/aslr.c @@ -1274,7 +1274,7 @@ static int aslr_tool__init(struct aslr_tool *aslr, struct perf_tool *delegate) delegate_tool__init(&aslr->tool, delegate); aslr->tool.tool.ordered_events = true; - if (machines__init(&aslr->machines)) + if (machines__init(&aslr->machines, /*session=*/NULL)) return -ENOMEM; hashmap__init(&aslr->remap_addresses, @@ -1374,6 +1374,14 @@ void aslr_tool__delete(struct perf_tool *tool) free(aslr); } +void aslr_tool__register_session(struct perf_tool *tool, struct perf_session *session) +{ + struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool); + struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool); + + aslr->machines.session = session; +} + int aslr_tool__cache_orig_attrs(struct perf_tool *tool, struct evsel *evsel) { struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool); diff --git a/tools/perf/util/aslr.h b/tools/perf/util/aslr.h index 522e31c8e2c0..9cc14f0263db 100644 --- a/tools/perf/util/aslr.h +++ b/tools/perf/util/aslr.h @@ -28,6 +28,7 @@ PERF_SAMPLE_CODE_PAGE_SIZE | \ PERF_SAMPLE_AUX) +struct perf_session; struct perf_tool; struct evsel; struct evlist; @@ -36,6 +37,7 @@ union perf_event; struct perf_tool *aslr_tool__new(struct perf_tool *delegate); void aslr_tool__delete(struct perf_tool *tool); +void aslr_tool__register_session(struct perf_tool *tool, struct perf_session *session); void aslr_tool__strip_attr_event(union perf_event *event, struct evlist *evlist); int aslr_tool__cache_orig_attrs(struct perf_tool *tool, struct evsel *evsel); void aslr_tool__strip_evlist(const struct perf_tool *tool, struct evlist *evlist); diff --git a/tools/perf/util/machine.c b/tools/perf/util/machine.c index a1288fbed833..1d9d3bf57720 100644 --- a/tools/perf/util/machine.c +++ b/tools/perf/util/machine.c @@ -233,10 +233,11 @@ void machine__delete(struct machine *machine) } } -int machines__init(struct machines *machines) +int machines__init(struct machines *machines, struct perf_session *session) { int err = machine__init(&machines->host, "", HOST_KERNEL_ID); + machines->session = session; machines->host.machines = machines; machines->guests = RB_ROOT_CACHED; return err; diff --git a/tools/perf/util/machine.h b/tools/perf/util/machine.h index 26f9827062f5..973a59f7c26a 100644 --- a/tools/perf/util/machine.h +++ b/tools/perf/util/machine.h @@ -15,6 +15,7 @@ struct dso; struct dso_id; struct evsel; struct perf_sample; +struct perf_session; struct symbol; struct target; struct thread; @@ -148,13 +149,20 @@ int machine__process_event(struct machine *machine, union perf_event *event, typedef void (*machine__process_t)(struct machine *machine, void *data); struct machines { + /** @session: back link to owning session if there is one. */ + struct perf_session *session; struct machine host; struct rb_root_cached guests; }; -int machines__init(struct machines *machines); +int machines__init(struct machines *machines, struct perf_session *session); void machines__exit(struct machines *machines); +static inline struct perf_session *machines__session(struct machines *machines) +{ + return machines->session; +} + void machines__process_guests(struct machines *machines, machine__process_t process, void *data); diff --git a/tools/perf/util/session.c b/tools/perf/util/session.c index 04fba6fb02e8..7fea9e72726c 100644 --- a/tools/perf/util/session.c +++ b/tools/perf/util/session.c @@ -166,7 +166,7 @@ struct perf_session *__perf_session__new(struct perf_data *data, session->active_decomp = &session->decomp_data; INIT_LIST_HEAD(&session->auxtrace_index); perf_env__init(&session->header.env); - if (machines__init(&session->machines)) + if (machines__init(&session->machines, session)) goto out_delete; ordered_events__init(&session->ordered_events, diff --git a/tools/perf/util/thread.c b/tools/perf/util/thread.c index f0d3773d87db..8b8ca94e89ea 100644 --- a/tools/perf/util/thread.c +++ b/tools/perf/util/thread.c @@ -531,12 +531,10 @@ uint16_t thread__e_machine_endian(struct thread *thread, struct machine *machine bool is_live = machine->machines == NULL; if (!is_live) { - /* Check if the session has a data file. */ - struct perf_session *session = container_of(machine->machines, - struct perf_session, - machines); + /* Check if the session has a data file (assume no session is a test). */ + struct perf_session *session = machines__session(machine->machines); - is_live = !session->data; + is_live = !session || !session->data; } /* Read from /proc/pid/exe if live. */ if (is_live) { From e223a41c429bad47ed44907a14e8de8901eb93ce Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 17 Sep 2026 23:32:45 -0700 Subject: [PATCH 151/425] perf trace-event: Report tracepoint format errors with NULL and errno trace_event__tp_format() encoded failure with ERR_PTR() while trace_event__tp_format_id() returned a plain NULL when tep_find_event() found nothing, and tp_format() itself returned NULL when tep_parse_format() failed, as its return value was discarded. Callers test with IS_ERR(), which NULL does not satisfy, so those two failures were taken for success. In syscall__read_info() that leads straight to: if (IS_ERR(sc->tp_format)) { ... return err; } if (syscall__alloc_arg_fmts(sc, sc->tp_format->format.nr_fields - 1)) which dereferences NULL when a format file fails to parse. Mixing encoded error pointers with pointers that are compared against NULL is what allows that to happen, so drop ERR_PTR() here and report failures the way the rest of these paths already expect, by returning NULL with errno set. evsel__tp_format() no longer has to translate the error back into errno before printing it with %m, and the remaining callers become NULL tests. syscall__scnprintf_args() gains back its fallback of printing raw arguments: it asked for IS_ERR(sc->tp_format), but syscall__read_info() had already replaced the error pointer with NULL, so the branch could never be taken. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-kmem.c | 3 +- tools/perf/builtin-sched.c | 5 ++-- tools/perf/builtin-trace.c | 10 +++---- tools/perf/util/evsel.c | 5 +--- tools/perf/util/trace-event.c | 52 ++++++++++++++++++++++++++--------- 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/tools/perf/builtin-kmem.c b/tools/perf/builtin-kmem.c index e1b2f5bc1ba8..8693c6b135ca 100644 --- a/tools/perf/builtin-kmem.c +++ b/tools/perf/builtin-kmem.c @@ -1870,8 +1870,7 @@ static bool slab_legacy_tp_is_exposed(void) * means the tool is running on an old kernel, we need to * rollback to support these legacy tracepoints. */ - return IS_ERR(trace_event__tp_format("kmem", "kmalloc_node")) ? - false : true; + return trace_event__tp_format("kmem", "kmalloc_node"); } static int __cmd_record(int argc, const char **argv) diff --git a/tools/perf/builtin-sched.c b/tools/perf/builtin-sched.c index dd39a4fb6c7a..5b4092ae33ee 100644 --- a/tools/perf/builtin-sched.c +++ b/tools/perf/builtin-sched.c @@ -5182,8 +5182,7 @@ static bool schedstat_events_exposed(void) * Select "sched:sched_stat_wait" event to check * whether schedstat tracepoints are exposed. */ - return IS_ERR(trace_event__tp_format("sched", "sched_stat_wait")) ? - false : true; + return trace_event__tp_format("sched", "sched_stat_wait"); } static int __cmd_record(int argc, const char **argv) @@ -5240,7 +5239,7 @@ static int __cmd_record(int argc, const char **argv) rec_argv[i++] = strdup("-e"); waking_event = trace_event__tp_format("sched", "sched_waking"); - if (!IS_ERR(waking_event)) + if (waking_event) rec_argv[i++] = strdup("sched:sched_waking"); else rec_argv[i++] = strdup("sched:sched_wakeup"); diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f67557e7a254..7fe39455085f 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2391,7 +2391,7 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) snprintf(tp_name, sizeof(tp_name), "sys_enter_%s", sc->name); sc->tp_format = trace_event__tp_format("syscalls", tp_name); - if (IS_ERR(sc->tp_format) && sc->fmt && sc->fmt->alias) { + if (!sc->tp_format && sc->fmt && sc->fmt->alias) { snprintf(tp_name, sizeof(tp_name), "sys_enter_%s", sc->fmt->alias); sc->tp_format = trace_event__tp_format("syscalls", tp_name); } @@ -2400,11 +2400,9 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) * Fails to read trace point format via sysfs node, so the trace point * doesn't exist. Set the 'nonexistent' flag as true. */ - if (IS_ERR(sc->tp_format)) { + if (!sc->tp_format) { sc->nonexistent = true; - err = PTR_ERR(sc->tp_format); - sc->tp_format = NULL; - return err; + return -errno; } /* @@ -2687,7 +2685,7 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, printed += syscall_arg_fmt__scnprintf_val(&sc->arg_fmt[arg.idx], bf + printed, size - printed, &arg, val); } - } else if (IS_ERR(sc->tp_format)) { + } else if (!sc->tp_format) { /* * If we managed to read the tracepoint /format file, then we * may end up not having any args, like with gettid(), so only diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index a2e9a31c1472..1c9e5a30a0cd 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -720,10 +720,7 @@ struct tep_event *evsel__tp_format(struct evsel *evsel) else tp_format = trace_event__tp_format(evsel->tp_sys, evsel->tp_name); - if (IS_ERR(tp_format)) { - int err = -PTR_ERR(tp_format); - - errno = err; + if (!tp_format) { pr_err("Error getting tracepoint format '%s': %m\n", evsel__name(evsel)); return NULL; diff --git a/tools/perf/util/trace-event.c b/tools/perf/util/trace-event.c index 000c1e1d68c1..10a7652f0305 100644 --- a/tools/perf/util/trace-event.c +++ b/tools/perf/util/trace-event.c @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -77,7 +76,7 @@ void trace_event__cleanup(struct trace_event *t) } /* - * Returns pointer with encoded error via interface. + * Returns NULL and sets errno on failure. */ static struct tep_event* tp_format(const char *sys, const char *name) @@ -90,38 +89,65 @@ tp_format(const char *sys, const char *name) char *data; int err; - if (!tp_dir) - return ERR_PTR(-errno); + if (!tp_dir) { + errno = ENOMEM; + return NULL; + } scnprintf(path, PATH_MAX, "%s/%s/format", tp_dir, name); put_events_file(tp_dir); err = filename__read_str(path, &data, &size); - if (err) - return ERR_PTR(err); + if (err) { + errno = -err; + return NULL; + } - tep_parse_format(pevent, &event, data, size, sys); + err = tep_parse_format(pevent, &event, data, size, sys); free(data); + + /* + * A parse failure leaves no event behind, report it rather than + * letting a NULL be mistaken for a successfully parsed format. + */ + if (err != TEP_ERRNO__SUCCESS || !event) { + errno = EINVAL; + return NULL; + } + return event; } /* - * Returns pointer with encoded error via interface. + * Returns NULL and sets errno on failure. */ struct tep_event* trace_event__tp_format(const char *sys, const char *name) { - if (!tevent_initialized && trace_event__init2()) - return ERR_PTR(-ENOMEM); + if (!tevent_initialized && trace_event__init2()) { + errno = ENOMEM; + return NULL; + } return tp_format(sys, name); } +/* + * Returns NULL and sets errno on failure. + */ struct tep_event *trace_event__tp_format_id(int id) { - if (!tevent_initialized && trace_event__init2()) - return ERR_PTR(-ENOMEM); + struct tep_event *event; - return tep_find_event(tevent.pevent, id); + if (!tevent_initialized && trace_event__init2()) { + errno = ENOMEM; + return NULL; + } + + event = tep_find_event(tevent.pevent, id); + if (!event) + errno = ENOENT; + + return event; } From d9eba1d5b56e8a62158aa797d45e4e988aa60844 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 17 Sep 2026 23:32:46 -0700 Subject: [PATCH 152/425] perf trace-event: Reuse an already parsed tracepoint format tp_format() read and parsed the format file on every call. Each parse registers another tep_event with the global tep handle, and libtraceevent has no way to free an individual event, only the whole handle, so a repeated lookup of the same tracepoint both redoes the work of reading and parsing the file and leaves a duplicate behind for the rest of the session. Ask the handle for the event first with tep_find_event_by_name() and only fall back to reading the format file when it has not been parsed yet. The handle holds formats for the running kernel alone, so a name that is found is the same format that would have been read. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/trace-event.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tools/perf/util/trace-event.c b/tools/perf/util/trace-event.c index 10a7652f0305..826f75464171 100644 --- a/tools/perf/util/trace-event.c +++ b/tools/perf/util/trace-event.c @@ -81,14 +81,25 @@ void trace_event__cleanup(struct trace_event *t) static struct tep_event* tp_format(const char *sys, const char *name) { - char *tp_dir = get_events_file(sys); struct tep_handle *pevent = tevent.pevent; - struct tep_event *event = NULL; + struct tep_event *event; + char *tp_dir; char path[PATH_MAX]; size_t size; char *data; int err; + /* + * Each parse adds an event to the tep handle that can only be freed + * by freeing the whole handle, so re-reading a format file both + * repeats the work and grows the handle with a duplicate. Reuse the + * event if it was already parsed. + */ + event = tep_find_event_by_name(pevent, sys, name); + if (event) + return event; + + tp_dir = get_events_file(sys); if (!tp_dir) { errno = ENOMEM; return NULL; @@ -103,6 +114,7 @@ tp_format(const char *sys, const char *name) return NULL; } + event = NULL; err = tep_parse_format(pevent, &event, data, size, sys); free(data); From 5a3d7afdfb6c430e1377d779df4e16ec56e22e32 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 17 Sep 2026 23:32:47 -0700 Subject: [PATCH 153/425] perf trace-event: Free the global trace_event when a command ends The tep handle behind trace_event__tp_format() is a process wide cache of the running kernel's tracepoint formats and nothing ever released it, as the comment above it had noted since the code was added. Add trace_event__exit() and call it from run_builtin() next to the existing perf_config__exit(). By then the builtin has returned, so the tep_event pointers it handed out, such as the ones cached in evsel->tp_format, are no longer reachable. Note that this does not silence every libtraceevent report under leak sanitizer. Parsing a print fmt drops a token inside the library itself, and as those allocations are not linked into the handle, freeing the handle cannot reclaim them. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/perf.c | 6 ++++++ tools/perf/util/trace-event.c | 22 +++++++++++++++++----- tools/perf/util/trace-event.h | 1 + 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/tools/perf/perf.c b/tools/perf/perf.c index 6c5baa285b13..d60bc929c257 100644 --- a/tools/perf/perf.c +++ b/tools/perf/perf.c @@ -32,6 +32,9 @@ #include "util/build-id.h" #include "util/config.h" #include "util/debug.h" +#ifdef HAVE_LIBTRACEEVENT +#include "util/trace-event.h" +#endif const char perf_usage_string[] = "perf [--version] [--help] [OPTIONS] COMMAND [ARGS]"; @@ -379,6 +382,9 @@ static int run_builtin(const struct cmd_struct *p, int argc, const char **argv) status = p->fn(argc, argv); perf_config__exit(); +#ifdef HAVE_LIBTRACEEVENT + trace_event__exit(); +#endif exit_browser(status); if (status) diff --git a/tools/perf/util/trace-event.c b/tools/perf/util/trace-event.c index 826f75464171..819542a73f55 100644 --- a/tools/perf/util/trace-event.c +++ b/tools/perf/util/trace-event.c @@ -14,11 +14,9 @@ #include "machine.h" /* - * global trace_event object used by trace_event__tp_format - * - * TODO There's no cleanup call for this. Add some sort of - * __exit function support and call trace_event__cleanup - * there. + * Global trace_event object used by trace_event__tp_format. It caches the + * tracepoint formats of the running kernel for the lifetime of the command + * and is released by trace_event__exit. */ static struct trace_event tevent; static bool tevent_initialized; @@ -75,6 +73,20 @@ void trace_event__cleanup(struct trace_event *t) t->plugin_list = NULL; } +/* + * Release the global trace_event. Called once the command is done, when the + * tep_event pointers handed out by trace_event__tp_format are no longer in + * use. + */ +void trace_event__exit(void) +{ + if (!tevent_initialized) + return; + + trace_event__cleanup(&tevent); + tevent_initialized = false; +} + /* * Returns NULL and sets errno on failure. */ diff --git a/tools/perf/util/trace-event.h b/tools/perf/util/trace-event.h index 720121c74f1d..1c342fce36bb 100644 --- a/tools/perf/util/trace-event.h +++ b/tools/perf/util/trace-event.h @@ -32,6 +32,7 @@ bool have_tracepoints(struct list_head *evlist); int trace_event__init(struct trace_event *t); void trace_event__cleanup(struct trace_event *t); +void trace_event__exit(void); int trace_event__register_resolver(struct machine *machine, tep_func_resolver_t *func); struct tep_event* From 0cf5416f48be1198da1561897d77bbc203693164 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 17 Sep 2026 23:32:48 -0700 Subject: [PATCH 154/425] perf trace: Free the host machine allocation trace__symbols_init() creates the host machine with machine__new_host(), which allocates a struct machine, but trace__symbols__exit() released it with machine__exit(). That tears down the contents of the machine and leaves the allocation itself behind, so use machine__delete(), which does both. The replay path points trace->host at the machine embedded in the session and never reaches here, so nothing else is affected. The leak was hidden from leak sanitizer because trace__symbols_init() passes the machine to trace_event__register_resolver(), which stores it as the private pointer of the global tep handle. The handle kept the machine reachable for as long as the process lived, so it was only once the handle started being freed that this was reported: Direct leak of 1256 byte(s) in 1 object(s) allocated from: #1 __machine__new_host util/machine.c:135 #2 machine__new_host util/machine.c:155 #3 trace__symbols_init builtin-trace.c:2105 #4 trace__run builtin-trace.c:4759 #5 cmd_trace builtin-trace.c:6091 Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-trace.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 7fe39455085f..9b450c96469d 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2138,7 +2138,7 @@ static int trace__symbols_init(struct trace *trace, int argc, const char **argv, static void trace__symbols__exit(struct trace *trace) { - machine__exit(trace->host); + machine__delete(trace->host); trace->host = NULL; perf_env__exit(&trace->host_env); From 5d0e31779bca09f5baed745e175347fbad7d040b Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Thu, 17 Sep 2026 23:32:49 -0700 Subject: [PATCH 155/425] perf thread: Free the comm read from procfs procfs__read_str() allocates the buffer it hands back and thread__set_comm() only stores an interned copy of the string, so the caller owns the buffer. thread__set_comm_from_proc() freed it when the read came back empty but not once it had been used, leaking it on every successful call. Free it on both paths. The empty read still returns -1, err being untouched in that case. Found with leak sanitizer while running 'perf trace': Direct leak of 7 byte(s) in 1 object(s) allocated from: #1 io__getdelim fs/../io.h:179 #2 filename__read_str fs/fs.c:365 #3 procfs__read_str fs/fs.c:402 #4 thread__set_comm_from_proc util/thread.c:297 #5 syscall_arg__scnprintf_pid trace/beauty/pid.c:15 Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/thread.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/perf/util/thread.c b/tools/perf/util/thread.c index 8b8ca94e89ea..91118c301913 100644 --- a/tools/perf/util/thread.c +++ b/tools/perf/util/thread.c @@ -296,12 +296,12 @@ int thread__set_comm_from_proc(struct thread *thread) thread__pid(thread), thread__tid(thread)) >= (int)sizeof(path)) && procfs__read_str(path, &comm, &sz) == 0) { /* sz==0: read got nothing, e.g. race during exit teardown */ - if (sz == 0) { - free(comm); - return -1; + if (sz > 0) { + comm[sz - 1] = '\0'; + err = thread__set_comm(thread, comm, 0); } - comm[sz - 1] = '\0'; - err = thread__set_comm(thread, comm, 0); + /* thread__set_comm() copies the string, so release the buffer. */ + free(comm); } return err; From 8c74fcecb46c964af42385e492b43382ef0517d9 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 21:04:54 -0700 Subject: [PATCH 156/425] tools include: Add missing stdarg.h include Header file makes use of varargs but doesn't directly include stdarg.h. Closes: https://lore.kernel.org/linux-perf-users/20260916062233.192271F000FF@smtp.kernel.org/ Reported-by: sashiko-bot@kernel.org Signed-off-by: Ian Rogers Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/include/tools/dis-asm-compat.h | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/include/tools/dis-asm-compat.h b/tools/include/tools/dis-asm-compat.h index 70f331e23ed3..1e0586d8451b 100644 --- a/tools/include/tools/dis-asm-compat.h +++ b/tools/include/tools/dis-asm-compat.h @@ -2,6 +2,7 @@ #ifndef _TOOLS_DIS_ASM_COMPAT_H #define _TOOLS_DIS_ASM_COMPAT_H +#include #include #include From c9dd729e80aa3e9998da377648408e9a556346ee Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Mon, 7 Sep 2026 12:28:43 +0530 Subject: [PATCH 157/425] perf synthetic-events: Fix repeated word in log message Drop the word written twice, reported by checkpatch.pl as a possible repeated word. Only the message text changes, no code changes. Assisted-by: Cursor:claude-opus-5 Signed-off-by: Hemanth Selam Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/synthetic-events.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/util/synthetic-events.c b/tools/perf/util/synthetic-events.c index 44eff2391539..6ba50bdbd16e 100644 --- a/tools/perf/util/synthetic-events.c +++ b/tools/perf/util/synthetic-events.c @@ -2304,7 +2304,7 @@ int perf_event__synthesize_extra_attr(const struct perf_tool *tool, struct evlis if (has_scale(evsel)) { err = perf_event__synthesize_event_update_scale(tool, evsel, process); if (err < 0) { - pr_err("Couldn't synthesize evsel evsel.\n"); + pr_err("Couldn't synthesize evsel.\n"); return err; } } From d55214860794ba5509d8ad861a5b2d97a7af3521 Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Mon, 7 Sep 2026 12:24:21 +0530 Subject: [PATCH 158/425] perf tools: Fix typos in comments Fix typos in comments, reported by scripts/checkpatch.pl using the misspelling list in scripts/spelling.txt. Only touches comments, no code changes. Assisted-by: Cursor:claude-opus-5 Signed-off-by: Hemanth Selam Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-trace.c | 2 +- tools/perf/trace/beauty/include/uapi/linux/fs.h | 2 +- tools/perf/trace/beauty/syscalltbl.sh | 2 +- tools/perf/ui/hist.c | 2 +- tools/perf/util/annotate.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 9b450c96469d..d327603ae454 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -104,7 +104,7 @@ /* * strtoul: Go from a string to a value, i.e. for msr: MSR_FS_BASE to 0xc0000100 * - * We have to explicitely mark the direction of the flow of data, if from the + * We have to explicitly mark the direction of the flow of data, if from the * kernel to user space or the other way around, since the BPF collector we * have so far copies only from user to kernel space, mark the arguments that * go that direction, so that we don´t end up collecting the previous contents diff --git a/tools/perf/trace/beauty/include/uapi/linux/fs.h b/tools/perf/trace/beauty/include/uapi/linux/fs.h index bd87262f2e34..ee2203d60643 100644 --- a/tools/perf/trace/beauty/include/uapi/linux/fs.h +++ b/tools/perf/trace/beauty/include/uapi/linux/fs.h @@ -88,7 +88,7 @@ struct fstrim_range { * We include a length field because some filesystems (vfat) have an identifier * that we do want to expose as a UUID, but doesn't have the standard length. * - * We use a fixed size buffer beacuse this interface will, by fiat, never + * We use a fixed size buffer because this interface will, by fiat, never * support "UUIDs" longer than 16 bytes; we don't want to force all downstream * users to have to deal with that. */ diff --git a/tools/perf/trace/beauty/syscalltbl.sh b/tools/perf/trace/beauty/syscalltbl.sh index 1199618dc178..e70a8c79e019 100755 --- a/tools/perf/trace/beauty/syscalltbl.sh +++ b/tools/perf/trace/beauty/syscalltbl.sh @@ -55,7 +55,7 @@ build_tables() { echo "static const uint16_t syscall_sorted_names_${e_machine}[] = {" >> "$outfile" - # When sorting by name, add a suffix of 0s upto 20 characters so that + # When sorting by name, add a suffix of 0s up to 20 characters so that # system calls that differ with a numerical suffix don't sort before # those without. This default behavior of sort differs from that of # strcmp used at runtime. Use sed to strip the trailing 0s suffix diff --git a/tools/perf/ui/hist.c b/tools/perf/ui/hist.c index 09da45c80889..a61c4b1816e1 100644 --- a/tools/perf/ui/hist.c +++ b/tools/perf/ui/hist.c @@ -552,7 +552,7 @@ static int hpp__header_mem_stat_fn(struct perf_hpp_fmt *fmt, struct perf_hpp *hp int left, right; len = 0; - /* update fmt->len for acutally used columns only */ + /* update fmt->len for actually used columns only */ for (int i = 0; i < MEM_STAT_LEN; i++) { if (hists->mem_stat_total[mem_stat_idx].entries[i]) len += MEM_STAT_PRINT_LEN; diff --git a/tools/perf/util/annotate.h b/tools/perf/util/annotate.h index c528fbef8b38..1b47c1156b9a 100644 --- a/tools/perf/util/annotate.h +++ b/tools/perf/util/annotate.h @@ -290,7 +290,7 @@ struct cyc_hist { /** * struct annotated_source - symbols with hits have this attached as in annotation * - * @source: List head for annotated_line (embeded in disasm_line). + * @source: List head for annotated_line (embedded in disasm_line). * @histograms: Array of symbol histograms per event to maintain the total number * of samples and period. * @nr_histograms: This may not be the same as evsel->evlist->core.nr_entries if From 4c32b6d6bd657f16e7183cc90db0f85bc35d9e87 Mon Sep 17 00:00:00 2001 From: nanshuaibo Date: Thu, 3 Sep 2026 19:28:34 +0800 Subject: [PATCH 159/425] perf sched: Make output_name variable static The output_name variable is only used within builtin-sched.c for the stats subcommand, so declare it static. This fixes a sparse warning: builtin-sched.c:4140:12: warning: symbol 'output_name' was not declared. Should it be static? Reviewed-by: Ian Rogers Signed-off-by: nanshuaibo Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/builtin-sched.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/builtin-sched.c b/tools/perf/builtin-sched.c index 5b4092ae33ee..6e8bf619c86c 100644 --- a/tools/perf/builtin-sched.c +++ b/tools/perf/builtin-sched.c @@ -4137,7 +4137,7 @@ static int disable_sched_schedstat(void) } /* perf.data or any other output file name used by stats subcommand (only). */ -const char *output_name; +static const char *output_name; static int perf_sched__schedstat_record(struct perf_sched *sched, int argc, const char **argv) From b36a6f180baad35a962677ae181681ca60dda35b Mon Sep 17 00:00:00 2001 From: Jiebin Sun Date: Fri, 21 Aug 2026 13:56:08 +0800 Subject: [PATCH 160/425] perf c2c: Fix documented default coalesce fields The default coalesce fields dropped pid in favor of iaddr, but the man page still documents the old pid,iaddr default. Update it to match the command. Fixes: 423701a0c8d754d9 ("perf c2c: Change the default coalesce setup") Reviewed-by: Tianyou Li Reviewed-by: Wangyang Guo Signed-off-by: Jiebin Sun Acked-by: Namhyung Kim Cc: Dapeng Mi Cc: Ian Rogers Cc: James Clark Cc: Jiri Olsa Cc: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-c2c.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/Documentation/perf-c2c.txt b/tools/perf/Documentation/perf-c2c.txt index 8775889bc0a3..9e58a51c55de 100644 --- a/tools/perf/Documentation/perf-c2c.txt +++ b/tools/perf/Documentation/perf-c2c.txt @@ -341,7 +341,7 @@ output fields set for cacheline offsets output: Code address, Code symbol, Shared Object, Source line dso - coalesced by shared object -By default the coalescing is setup with 'pid,iaddr'. +By default, coalescing uses `iaddr`. STDIO OUTPUT ------------ From f4a58c7164957e73df36ed24c42b0b9b713a43b3 Mon Sep 17 00:00:00 2001 From: Jiebin Sun Date: Fri, 21 Aug 2026 13:56:09 +0800 Subject: [PATCH 161/425] perf tests c2c: Report skip when the workload fails The workload path leaves err at 0 and returns, so the test exits 0 and is reported as a pass even though nothing was recorded and neither report invocation ran. The check_c2c_support() path above it already sets err=2. Set err=2 there as well so a failing workload is reported as a skip. Fixes: ac881007c4bf7be4 ("perf tests c2c: Add a basic c2c") Reviewed-by: Tianyou Li Reviewed-by: Wangyang Guo Signed-off-by: Jiebin Sun Acked-by: Namhyung Kim Cc: Dapeng Mi Cc: Ian Rogers Cc: James Clark Cc: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/c2c.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/perf/tests/shell/c2c.sh b/tools/perf/tests/shell/c2c.sh index 2471d44595c3..f5f223cbf9cc 100755 --- a/tools/perf/tests/shell/c2c.sh +++ b/tools/perf/tests/shell/c2c.sh @@ -39,6 +39,7 @@ test_c2c_record_report() { # Run a workload that does some memory operations. if ! perf c2c record -o "${perfdata}" -- perf test -w datasym 1 > /dev/null 2>&1 ; then echo "c2c record and report test [Skipped: perf c2c record failed during workload]" + err=2 return fi From 89cfd46b7e6cab1d9fee534d4aca946c12065895 Mon Sep 17 00:00:00 2001 From: Jiebin Sun Date: Fri, 21 Aug 2026 13:56:10 +0800 Subject: [PATCH 162/425] perf c2c: Add stdio support for the function view The function view is currently TUI-only, so it cannot be used by builds without SLANG support, when output is piped, or from a script. Add a --function option that prints the fully expanded three-level hierarchy to stdout. Keep the stdio renderer in builtin-c2c.c and reuse the common function-view model introduced by the merged series. Export only the coalescing-field capability check from the model, preserving the util/UI boundary and leaving the TUI object in libperf-ui.a. Stop padding the final identity column in symbol_view_entry(). The generic formatter pads non-final columns but deliberately leaves the final column unpadded, avoiding trailing whitespace in function-view table rows. The TUI remains unchanged because its browser clears the rest of each rendered row. --function implies --stdio and is rejected together with --stats. Validate the iaddr requirement before processing events. Return function-view build failures from the report command, and preserve TUI browser errors when converting the display helpers to return a status. Committer notes: root@x2:~# uname -r 7.2.5-200.fc44.x86_64 root@x2:~# rpm -q kernel-debuginfo kernel-debuginfo-7.2.5-200.fc44.x86_64 root@x2:~# perf c2c record perf bench futex hash -t 4 -r 1 -s > /dev/null 2>&1 root@x2:~# perf c2c report --stdio --function | grep "Functions Table" -A25 Shared Data Functions Table ================================================= # # Cycles Store # % count Function / Contending function / Cacheline # ......... ....... .............................. # - 47.89% 703 - [k] _raw_spin_lock 322 - [k] _raw_spin_lock 24 0xffff8b6dcbadf040 23 0xffff8b6dcbadf2c0 23 0xffff8b6dcbadf080 23 0xffff8b6dcbadf380 22 0xffff8b6dcbadf280 22 0xffff8b6dcbadf140 22 0xffff8b6dcbadf340 21 0xffff8b6dcbadf300 21 0xffff8b6dcbadf400 20 0xffff8b6dcbadf1c0 19 0xffff8b6dcbadf3c0 19 0xffff8b6dcbadf180 19 0xffff8b6dcbadf0c0 17 0xffff8b6dcbadf100 16 0xffff8b6dcbadf240 11 0xffff8b6dcbadf200 205 - [k] futex_q_unlock root@x2:~# Reviewed-by: Tianyou Li Reviewed-by: Wangyang Guo Signed-off-by: Jiebin Sun Acked-by: Namhyung Kim Tested-by: Arnaldo Carvalho de Melo Cc: Dapeng Mi Cc: Ian Rogers Cc: James Clark Cc: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-c2c.txt | 12 +++- tools/perf/builtin-c2c.c | 85 ++++++++++++++++++++++++--- tools/perf/util/c2c-function.c | 9 +-- tools/perf/util/c2c.h | 1 + 4 files changed, 90 insertions(+), 17 deletions(-) diff --git a/tools/perf/Documentation/perf-c2c.txt b/tools/perf/Documentation/perf-c2c.txt index 9e58a51c55de..7a0cf31be7ed 100644 --- a/tools/perf/Documentation/perf-c2c.txt +++ b/tools/perf/Documentation/perf-c2c.txt @@ -107,6 +107,10 @@ REPORT OPTIONS --stats:: Display only statistic tables and force stdio mode. +--function:: + Display the function view and force stdio mode. This requires `iaddr` + in the cacheline coalescing fields and cannot be used with `--stats`. + --full-symbols:: Display full length of symbols. @@ -360,6 +364,10 @@ Following tables are displayed: Shared Cache Line Distribution Pareto - list of all accessed offsets for each cacheline +With `--function`, the cacheline and Pareto tables are replaced by a fully +expanded Shared Data Functions Table. Its three levels are the read-side +function, contending writer, and shared cacheline, as detailed below. + TUI OUTPUT ---------- The TUI output provides interactive interface to navigate @@ -374,8 +382,8 @@ Verbose mode also includes code addresses in function rows, and code addresses remain available in the per-cacheline detail view ('d'). The function view requires `iaddr` in the cacheline coalescing fields. If -`--coalesce` omits it, TAB reports that the view is unavailable rather than -attributing already-coalesced samples to an arbitrary function. +`--coalesce` omits it, TAB or `--function` reports that the view is unavailable +rather than attributing already-coalesced samples to an arbitrary function. Level 1: the read-side function, sorted by Cycles % (estimated load cycles: HITM, peer-snoop and other-load cycles) diff --git a/tools/perf/builtin-c2c.c b/tools/perf/builtin-c2c.c index ee294f12a542..4222ac784fab 100644 --- a/tools/perf/builtin-c2c.c +++ b/tools/perf/builtin-c2c.c @@ -76,6 +76,7 @@ struct perf_c2c { bool show_mem_region; bool use_stdio; bool stats_only; + bool function_view; bool symbol_full; bool stitch_lbr; @@ -2624,7 +2625,48 @@ static void print_memory_ranges_info(FILE *out, struct perf_session *session) } } -static void perf_c2c__hists_fprintf(FILE *out, struct perf_session *session) +static void c2c_function__unfold_all(struct rb_root_cached *root) +{ + struct rb_node *nd; + + for (nd = rb_first_cached(root); nd; nd = rb_next(nd)) { + struct hist_entry *he = rb_entry(nd, struct hist_entry, rb_node); + + if (!he->has_children) + continue; + he->unfolded = true; + c2c_function__unfold_all(&he->hroot_out); + } +} + +static int perf_c2c__function_fprintf(FILE *out) +{ + bool saved_use_callchain = symbol_conf.use_callchain; + struct hists *hists; + int ret; + + /* Function-view entries aggregate samples and never display callchains. */ + symbol_conf.use_callchain = false; + ret = c2c_function__build(&c2c.hists, c2c.cl_sort, c2c.symbol_full, + &hists); + if (ret) { + if (ret == -EOPNOTSUPP) + pr_err("The function view requires iaddr in --coalesce.\n"); + else + pr_err("Failed to build function view hierarchy (ret=%d)\n", ret); + goto out; + } + + /* Match fold signs to hists__fprintf()'s forced child traversal. */ + c2c_function__unfold_all(&hists->entries); + hists__fprintf(hists, true, 0, 0, 0, out, true); + c2c_function__reset(); +out: + symbol_conf.use_callchain = saved_use_callchain; + return ret; +} + +static int perf_c2c__hists_fprintf(FILE *out, struct perf_session *session) { setup_pager(); @@ -2635,7 +2677,17 @@ static void perf_c2c__hists_fprintf(FILE *out, struct perf_session *session) print_c2c_info(out, session); if (c2c.stats_only) - return; + return 0; + + if (c2c.function_view) { + fprintf(out, "\n"); + fprintf(out, "=================================================\n"); + fprintf(out, " Shared Data Functions Table\n"); + fprintf(out, "=================================================\n"); + fprintf(out, "#\n"); + + return perf_c2c__function_fprintf(out); + } print_memory_ranges_info(out, session); @@ -2654,6 +2706,7 @@ static void perf_c2c__hists_fprintf(FILE *out, struct perf_session *session) fprintf(out, "#\n"); print_pareto(out, perf_session__env(session)); + return 0; } #ifdef HAVE_SLANG_SUPPORT @@ -2890,18 +2943,18 @@ static int perf_c2c__hists_browse(struct hists *hists) return 0; } -static void perf_c2c_display(struct perf_session *session) +static int perf_c2c_display(struct perf_session *session) { if (use_browser == 0) - perf_c2c__hists_fprintf(stdout, session); - else - perf_c2c__hists_browse(&c2c.hists.hists); + return perf_c2c__hists_fprintf(stdout, session); + + return perf_c2c__hists_browse(&c2c.hists.hists); } #else -static void perf_c2c_display(struct perf_session *session) +static int perf_c2c_display(struct perf_session *session) { use_browser = 0; - perf_c2c__hists_fprintf(stdout, session); + return perf_c2c__hists_fprintf(stdout, session); } #endif /* HAVE_SLANG_SUPPORT */ @@ -3180,6 +3233,8 @@ static int perf_c2c__report(int argc, const char **argv) OPT_BOOLEAN(0, "stdio", &c2c.use_stdio, "Use the stdio interface"), OPT_BOOLEAN(0, "stats", &c2c.stats_only, "Display only statistic tables (implies --stdio)"), + OPT_BOOLEAN(0, "function", &c2c.function_view, + "Display the function view (implies --stdio)"), OPT_BOOLEAN(0, "full-symbols", &c2c.symbol_full, "Display full length of symbols"), OPT_BOOLEAN(0, "no-source", &no_source, @@ -3219,6 +3274,11 @@ static int perf_c2c__report(int argc, const char **argv) PARSE_OPT_STOP_AT_NON_OPTION); if (argc) usage_with_options(report_c2c_usage, options); + if (c2c.stats_only && c2c.function_view) { + pr_err("--stats and --function cannot be used together.\n"); + err = -EINVAL; + goto out; + } #ifndef HAVE_SLANG_SUPPORT c2c.use_stdio = true; @@ -3226,6 +3286,8 @@ static int perf_c2c__report(int argc, const char **argv) if (c2c.stats_only) c2c.use_stdio = true; + if (c2c.function_view) + c2c.use_stdio = true; /** * Annotation related options disassembler_style, objdump_path are set @@ -3299,6 +3361,11 @@ static int perf_c2c__report(int argc, const char **argv) pr_debug("Failed to initialize hists\n"); goto out_session; } + if (c2c.function_view && !c2c_function__has_iaddr(c2c.cl_sort)) { + pr_err("The function view requires iaddr in --coalesce.\n"); + err = -EINVAL; + goto out_session; + } err = c2c_hists__init(&c2c.hists, "dcacheline", 2, perf_session__env(session)); if (err) { @@ -3452,7 +3519,7 @@ static int perf_c2c__report(int argc, const char **argv) goto out_str; } - perf_c2c_display(session); + err = perf_c2c_display(session); out_str: free(output_str); diff --git a/tools/perf/util/c2c-function.c b/tools/perf/util/c2c-function.c index 5b6a06a5a067..d410196d0429 100644 --- a/tools/perf/util/c2c-function.c +++ b/tools/perf/util/c2c-function.c @@ -305,8 +305,8 @@ symbol_view_entry(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp, char symbuf[32]; scnprintf(symbuf, sizeof(symbuf), "0x%" PRIx64, addr); - ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%-*.*s", - text_width, text_width, symbuf); + ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%.*s", + text_width, symbuf); } else { /* Level 1 and level 2 are both functions. */ size_t cell_size; @@ -331,9 +331,6 @@ symbol_view_entry(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp, len = min_t(size_t, len, cell_size - 1); ret += len; - if (len < text_width) - ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%*s", - text_width - len, ""); } return ret; @@ -1519,7 +1516,7 @@ void c2c_function__reset(void) symbol_conf.use_callchain = saved_use_callchain; } -static bool c2c_function__has_iaddr(const char *cl_sort) +bool c2c_function__has_iaddr(const char *cl_sort) { const char *field = cl_sort; diff --git a/tools/perf/util/c2c.h b/tools/perf/util/c2c.h index f04e78e1a2e3..65117108ddb1 100644 --- a/tools/perf/util/c2c.h +++ b/tools/perf/util/c2c.h @@ -108,6 +108,7 @@ bool c2c_fmt_equal(struct perf_hpp_fmt *a, struct perf_hpp_fmt *b); int c2c_function__build(struct c2c_hists *cl_hists, const char *cl_sort, bool symbol_full, struct hists **hists); void c2c_function__reset(void); +bool c2c_function__has_iaddr(const char *cl_sort); /* Valid only between a successful build and c2c_function__reset(). */ struct hist_entry *c2c_function__find_cacheline(struct hist_entry *he); From 0d91c10f8897c222f25d2d61d04a82dbb8d35892 Mon Sep 17 00:00:00 2001 From: Jiebin Sun Date: Fri, 21 Aug 2026 13:56:11 +0800 Subject: [PATCH 163/425] perf tests c2c: Add function view stdio coverage Exercise the function view without driving a terminal now that it has a stdio path. Keep the existing datasym record/report coverage and make a separate recording of the contended locks used by the futex hash benchmark. Always check the function table headers, table replacement, missing-iaddr diagnostic, and conflicting options. A machine can support c2c recording without capturing a contended sample, so report a skip when the hierarchy is empty rather than treating hardware sampling variance as a failure. When samples are available, check generic row shapes for all three hierarchy levels, their expanded fold signs, cacheline addresses, and the absence of trailing whitespace in the table body. Reviewed-by: Tianyou Li Reviewed-by: Wangyang Guo Signed-off-by: Jiebin Sun Acked-by: Namhyung Kim Tested-by: Arnaldo Carvalho de Melo Cc: Dapeng Mi Cc: Ian Rogers Cc: James Clark Cc: Thomas Falcon Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/c2c.sh | 113 ++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/tools/perf/tests/shell/c2c.sh b/tools/perf/tests/shell/c2c.sh index f5f223cbf9cc..cf76359d19cc 100755 --- a/tools/perf/tests/shell/c2c.sh +++ b/tools/perf/tests/shell/c2c.sh @@ -6,10 +6,15 @@ set -e err=0 perfdata=$(mktemp /tmp/__perf_c2c_test.perf.data.XXXXX) +funcdata=$(mktemp /tmp/__perf_c2c_function_test.perf.data.XXXXX) +perfout=$(mktemp /tmp/__perf_c2c_test.output.XXXXX) cleanup() { rm -f "${perfdata}" rm -f "${perfdata}".old + rm -f "${funcdata}" + rm -f "${funcdata}".old + rm -f "${perfout}" trap - EXIT TERM INT } @@ -58,6 +63,114 @@ test_c2c_record_report() { echo "c2c record and report test [Success]" } +test_c2c_function_report() { + echo "c2c function stdio report test" + + if perf c2c report -i "${perfdata}" --function -c pid > "${perfout}" 2>&1 ; then + echo "c2c function stdio report test [Failed: report accepted missing iaddr]" + err=1 + return + fi + if ! grep -Fq "The function view requires iaddr in --coalesce." "${perfout}" ; then + echo "c2c function stdio report test [Failed: missing iaddr diagnostic]" + cat "${perfout}" + err=1 + return + fi + if grep -Fq "Shared Data Functions Table" "${perfout}" ; then + echo "c2c function stdio report test [Failed: partial report on missing iaddr]" + cat "${perfout}" + err=1 + return + fi + + if perf c2c report -i "${perfdata}" --function --stats > "${perfout}" 2>&1 ; then + echo "c2c function stdio report test [Failed: accepted conflicting options]" + err=1 + return + fi + if ! grep -Fq -- "--stats and --function cannot be used together." "${perfout}" ; then + echo "c2c function stdio report test [Failed: missing conflict diagnostic]" + cat "${perfout}" + err=1 + return + fi + + # Exercise contended futex hash-bucket locks so the function view can get + # reader, writer, and cacheline rows without changing the original test. + if ! perf c2c record -o "${funcdata}" -- \ + perf bench futex hash -t 4 -r 1 -s > /dev/null 2>&1 ; then + echo "c2c function stdio report test [Skipped: recording failed]" + err=2 + return + fi + + if ! perf c2c report -i "${funcdata}" --function > "${perfout}" 2>&1 ; then + echo "c2c function stdio report test [Failed: report failed]" + cat "${perfout}" + err=1 + return + fi + + for expected in "Shared Data Functions Table" \ + "# Cycles Store" \ + "# % count Function / Contending function / Cacheline" \ + "# ......... ......." ; do + if ! grep -Fq "${expected}" "${perfout}" ; then + echo "c2c function stdio report test [Failed: missing '${expected}']" + cat "${perfout}" + err=1 + return + fi + done + for unexpected in "Shared Data Cache Line Table" \ + "Shared Cache Line Distribution Pareto" ; do + if grep -Fq "${unexpected}" "${perfout}" ; then + echo "c2c function stdio report test [Failed: found '${unexpected}']" + cat "${perfout}" + err=1 + return + fi + done + + if ! awk '/^# \.+/ { body = 1; next } + body && !/^#/ && NF { found = 1 } + END { exit !found }' "${perfout}" ; then + echo "c2c function stdio report test [Skipped: no contended samples]" + err=2 + return + fi + + # The spaces intentionally verify per-level indentation and expanded + # fold-sign placement without depending on symbol names. + for expected in \ + '^ - +[0-9]+\.[0-9]+% +[0-9]+ - ' \ + '^ +[0-9]+ - ' \ + '^ +[0-9]+ 0x[[:xdigit:]]+$' ; do + if ! grep -Eq "${expected}" "${perfout}" ; then + echo "c2c function stdio report test [Failed: missing hierarchy row]" + echo " ${expected}" + cat "${perfout}" + err=1 + return + fi + done + + if awk '/^# \.+/ { body = 1; next } + body && /[[:blank:]]$/ { found = 1 } + END { exit !found }' "${perfout}" ; then + echo "c2c function stdio report test [Failed: trailing whitespace in table body]" + cat "${perfout}" + err=1 + return + fi + + echo "c2c function stdio report test [Success]" +} + test_c2c_record_report +if [ "${err}" -eq 0 ]; then + test_c2c_function_report +fi cleanup exit $err From aa9265d5073cb01b89f31ae030442873d4a259d3 Mon Sep 17 00:00:00 2001 From: Nia Su Date: Wed, 9 Sep 2026 19:31:52 -0700 Subject: [PATCH 164/425] perf evlist: Don't restrict uncore events to PMU CPUs The PMU CPU map has different semantics for core and uncore PMUs. For core PMUs, pmu_cpus contains the CPUs on which the event can be opened. Intersect the user-requested CPU map with the PMU CPU map to reject CPUs unsupported by the PMU. For uncore PMUs, pmu_cpus is read from the PMU's cpumask sysfs attribute, which lists the default CPU to open the event on per socket/die, not the set of CPUs it can only be opened on. The user-requested CPU may override these defaults, and the driver can redirect the event to the appropriate control CPU. Currently, __perf_evlist__propagate_maps() intersects the user-requested CPU map with pmu_cpus for all PMUs. If the user-requested CPU is not in pmu_cpus, the intersection becomes empty and the evsel is removed before perf_event_open() is called. Add evsel->is_pmu_uncore, set once from pmu->is_uncore at parse time, to skip the intersection for uncore PMUs. Core PMUs, and other non-core PMUs such as software events, breakpoints, or tracepoints, continue to be restricted to their PMU CPU map. Fixes: 811082e4b668db96 ("perf parse-events: Support user CPUs mixed with threads/processes") Reviewed-by: Ian Rogers Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Nia Su Signed-off-by: Arnaldo Carvalho de Melo --- tools/lib/perf/evlist.c | 5 +++-- tools/lib/perf/include/internal/evsel.h | 2 ++ tools/perf/util/evsel.c | 1 + tools/perf/util/parse-events.c | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/lib/perf/evlist.c b/tools/lib/perf/evlist.c index 1f210dadd666..c1c5b0684eed 100644 --- a/tools/lib/perf/evlist.c +++ b/tools/lib/perf/evlist.c @@ -79,8 +79,9 @@ static void __perf_evlist__propagate_maps(struct perf_evlist *evlist, } } - /* Ensure cpus only references valid PMU CPUs. */ - if (!perf_cpu_map__has_any_cpu(evsel->cpus) && + /* Ensure cpus only references valid PMU CPUs, except for uncore PMUs. */ + if (!evsel->is_pmu_uncore && + !perf_cpu_map__has_any_cpu(evsel->cpus) && !perf_cpu_map__is_subset(evsel->pmu_cpus, evsel->cpus)) { struct perf_cpu_map *tmp = perf_cpu_map__intersect(evsel->pmu_cpus, evsel->cpus); diff --git a/tools/lib/perf/include/internal/evsel.h b/tools/lib/perf/include/internal/evsel.h index b988034f1371..35447bed0f95 100644 --- a/tools/lib/perf/include/internal/evsel.h +++ b/tools/lib/perf/include/internal/evsel.h @@ -128,6 +128,8 @@ struct perf_evsel { bool requires_cpu; /** Is the PMU for the event a core one? Effects the handling of own_cpus. */ bool is_pmu_core; + /** Is the PMU for the event an uncore one? */ + bool is_pmu_uncore; /** Does the evsel on read on the first CPU index such as tool time events? */ bool reads_only_on_cpu_idx0; int idx; diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index 1c9e5a30a0cd..3367242c5764 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -555,6 +555,7 @@ struct evsel *evsel__clone(struct evsel *orig) evsel->core.system_wide = orig->core.system_wide; evsel->core.requires_cpu = orig->core.requires_cpu; evsel->core.is_pmu_core = orig->core.is_pmu_core; + evsel->core.is_pmu_uncore = orig->core.is_pmu_uncore; if (orig->name) { evsel->name = strdup(orig->name); diff --git a/tools/perf/util/parse-events.c b/tools/perf/util/parse-events.c index e40c16cbef01..2f3317ce17f0 100644 --- a/tools/perf/util/parse-events.c +++ b/tools/perf/util/parse-events.c @@ -269,6 +269,7 @@ __add_event(struct list_head *list, int *idx, evsel->core.pmu_cpus = pmu_cpus; evsel->core.requires_cpu = pmu ? pmu->is_uncore : false; evsel->core.is_pmu_core = is_pmu_core; + evsel->core.is_pmu_uncore = pmu ? pmu->is_uncore : false; evsel->core.reads_only_on_cpu_idx0 = perf_pmu__reads_only_on_cpu_idx0(attr); evsel->pmu = pmu; evsel->alternate_hw_config = alternate_hw_config; From 2d3135cdedfe26e0867a3f45a273dcdbf3bdbe86 Mon Sep 17 00:00:00 2001 From: longlong yan Date: Wed, 2 Sep 2026 15:09:13 +0800 Subject: [PATCH 165/425] perf bench numa: Add NULL check after calloc() Two calloc() calls in the numa benchmark lack NULL return checks, leading to potential NULL pointer dereferences on allocation failure: 1. worker_process(): the allocated `pthreads` array is used in the following for-loop via pthreads[t] without checking for NULL. 2. __bench_numa(): the allocated `pids` array is used in the following for-loop via pids[i] without checking for NULL. Add BUG_ON() checks after each calloc(), consistent with the existing NULL check style used elsewhere in the same file (e.g., node_present and nodes allocations). Signed-off-by: longlong yan Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/bench/numa.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/perf/bench/numa.c b/tools/perf/bench/numa.c index 42d7afc03f9b..371a74e3da1b 100644 --- a/tools/perf/bench/numa.c +++ b/tools/perf/bench/numa.c @@ -1419,6 +1419,7 @@ static void worker_process(int process_nr) bind_to_cpumask(td->bind_cpumask); pthreads = calloc(g->p.nr_threads, sizeof(pthread_t)); + BUG_ON(!pthreads); process_data = setup_private_data(g->p.bytes_process); if (g->p.show_details >= 3) { @@ -1625,6 +1626,7 @@ static int __bench_numa(const char *name) return -1; pids = calloc(g->p.nr_proc, sizeof(*pids)); + BUG_ON(!pids); pid = -1; if (g->p.serialize_startup) { From 66fcf1357a26d9cf8c7e34e2a70d12ebd7bd538f Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Fri, 25 Sep 2026 12:21:37 -0300 Subject: [PATCH 166/425] perf test workload: Use unsigned int in code_with_type instead of uint The declaration of the Rust helper test_rs() uses "uint", a BSD-ism that lives in , so on glibc it is only visible by accident: glibc's includes , where uint is exposed under __USE_MISC, which is on by default. musl does none of that: its doesn't include and it typedefs uint there only when _GNU_SOURCE/_BSD_SOURCE is defined, while nothing else in this translation unit includes . Building it on Alpine therefore fails with: tests/workloads/code_with_type.c:9:21: error: unknown type name 'uint'; did you mean 'int'? tests/workloads/code_with_type.c:39:17: error: implicit declaration of function 'test_rs' [-Wimplicit-function-declaration] The second error is a consequence of the first one: with the declaration at line 9 rejected, the call at line 39 has no prototype, which is an error since gcc 14. The breakage is only visible when rustc is present, because tests/workloads/Build adds code_with_type.o when CONFIG_RUST_SUPPORT is detected, and tools/build/feature only checks for "rustc --version". That is why alpine:3.23 and alpine:3.24 fail while alpine:3.22 and alpine:edge, lacking rustc, build fine and hide it. Use unsigned int, which needs no header at all. Fixes: 2e05bb52a12d3 ("perf test workload: Add code_with_type test workload") Reviewed-by: Ian Rogers Acked-by: Namhyung Kim Cc: Dmitrii Dolgov <9erthalion6@gmail.com> Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/workloads/code_with_type.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/tests/workloads/code_with_type.c b/tools/perf/tests/workloads/code_with_type.c index 65d7be7dac24..d2d437b86e0c 100644 --- a/tools/perf/tests/workloads/code_with_type.c +++ b/tools/perf/tests/workloads/code_with_type.c @@ -6,7 +6,7 @@ #include #include "../tests.h" -extern void test_rs(uint count); +extern void test_rs(unsigned int count); static volatile sig_atomic_t done; From 906af9e1c974a43eda25526c1046f0f4814961f1 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Wed, 16 Sep 2026 23:13:12 -0700 Subject: [PATCH 167/425] perf test stat metrics cgroup: Strip possible slash from cgroup name The grep to match cgroup name in metric output assumes no leading / on the cgroup name, however, cgroups in /proc/self/cgroup may have a leading /. Strip the / if it exists to avoid failures to match the cgroup name in the perf stat output. Signed-off-by: Ian Rogers Acked-by: Namhyung Kim Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/stat_metrics_cgrp.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/perf/tests/shell/stat_metrics_cgrp.sh b/tools/perf/tests/shell/stat_metrics_cgrp.sh index d4226ee0ae98..0728f48f21fa 100755 --- a/tools/perf/tests/shell/stat_metrics_cgrp.sh +++ b/tools/perf/tests/shell/stat_metrics_cgrp.sh @@ -86,6 +86,13 @@ check_metric_reported() for cgrp in "${cgrps[@]}"; do # Find metric lines for this cgroup + case "$cgrp" in + /) # The root cgroup, leave unchanged. + ;; + /*) # Strip any starting slashes. + cgrp="${cgrp#/}" + ;; + esac # We use exact cgroup match with surrounding commas local cgrp_lines cgrp_lines=$(echo "${output}" | grep -F ",${cgrp}," | grep "insn_per_cycle" || true) From a6ac1e9ca936b20fc450dda79ab834e93678f8d8 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Thu, 24 Sep 2026 16:27:56 -0300 Subject: [PATCH 168/425] perf dwarf-aux: Bound the type chases for broken debug info A DIE parsed at an offset that is not the start of a DIE can have a DW_AT_type that refers back to itself, making the typedef/qualifier chases in die_get_real_type(), die_get_pointer_type() and die_get_typename_from_type() spin forever; 'perf report -s type' did that on the dwz compressed debug info of zlib-ng (libz.so.1). No sane typedef/qualifier chain is 32 DIEs long, so give up on the type with a pr_debug instead of hanging. Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Cc: Masami Hiramatsu Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/dwarf-aux.c | 84 ++++++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/tools/perf/util/dwarf-aux.c b/tools/perf/util/dwarf-aux.c index 1560e721479a..2c135ebb1551 100644 --- a/tools/perf/util/dwarf-aux.c +++ b/tools/perf/util/dwarf-aux.c @@ -267,16 +267,27 @@ Dwarf_Die *die_get_type(Dwarf_Die *vr_die, Dwarf_Die *die_mem) return NULL; } +/* + * A DIE parsed at an offset that is not the start of a DIE can refer + * back to itself via DW_AT_type: bound these chases. + */ +#define MAX_TYPE_CHASE 32 + /* Get a type die, but skip qualifiers */ Dwarf_Die *__die_get_real_type(Dwarf_Die *vr_die, Dwarf_Die *die_mem) { - int tag; + int tag, chase = 0; do { vr_die = die_get_type(vr_die, die_mem); if (!vr_die) - break; + return NULL; tag = dwarf_tag(vr_die); + if (++chase > MAX_TYPE_CHASE) { + pr_debug("DWARF: qualifier chase limit reached at DIE 0x%lx\n", + (unsigned long)dwarf_dieoffset(vr_die)); + return NULL; + } } while (tag == DW_TAG_const_type || tag == DW_TAG_restrict_type || tag == DW_TAG_volatile_type || @@ -297,8 +308,15 @@ Dwarf_Die *__die_get_real_type(Dwarf_Die *vr_die, Dwarf_Die *die_mem) */ Dwarf_Die *die_get_real_type(Dwarf_Die *vr_die, Dwarf_Die *die_mem) { + int chase = 0; + do { vr_die = __die_get_real_type(vr_die, die_mem); + if (++chase > MAX_TYPE_CHASE) { + pr_debug("DWARF: typedef chase limit reached at DIE 0x%lx\n", + vr_die ? (unsigned long)dwarf_dieoffset(vr_die) : 0); + return NULL; + } } while (vr_die && dwarf_tag(vr_die) == DW_TAG_typedef); return vr_die; @@ -315,7 +333,7 @@ Dwarf_Die *die_get_real_type(Dwarf_Die *vr_die, Dwarf_Die *die_mem) */ Dwarf_Die *die_get_pointer_type(Dwarf_Die *type_die, Dwarf_Die *die_mem) { - int tag; + int tag, chase = 0; do { tag = dwarf_tag(type_die); @@ -325,6 +343,11 @@ Dwarf_Die *die_get_pointer_type(Dwarf_Die *type_die, Dwarf_Die *die_mem) tag != DW_TAG_restrict_type && tag != DW_TAG_volatile_type && tag != DW_TAG_shared_type) return NULL; + if (++chase > MAX_TYPE_CHASE) { + pr_debug("DWARF: pointer type chase limit reached at DIE 0x%lx\n", + (unsigned long)dwarf_dieoffset(type_die)); + return NULL; + } type_die = die_get_type(type_die, die_mem); } while (type_die); @@ -1119,17 +1142,25 @@ Dwarf_Die *die_find_member(Dwarf_Die *st_die, const char *name, die_mem); } -/** - * die_get_typename_from_type - Get the name of given type DIE - * @type_die: a type DIE - * @buf: a strbuf for result type name - * - * Get the name of @type_die and stores it to @buf. Return 0 if succeeded. - * and Return -ENOENT if failed to find type name. - * Note that the result will stores typedef name if possible, and stores - * "*(function_type)" if the type is a function pointer. +/* + * The name follows DW_AT_type, so a self-referring DIE makes this + * recurse forever: bound it like the chases above. */ -int die_get_typename_from_type(Dwarf_Die *type_die, struct strbuf *buf) +static int __die_get_typename_from_type(Dwarf_Die *type_die, struct strbuf *buf, + int depth); + +static int __die_get_typename(Dwarf_Die *vr_die, struct strbuf *buf, int depth) +{ + Dwarf_Die type; + + if (__die_get_real_type(vr_die, &type) == NULL) + return -ENOENT; + + return __die_get_typename_from_type(&type, buf, depth); +} + +static int __die_get_typename_from_type(Dwarf_Die *type_die, struct strbuf *buf, + int depth) { int tag, ret; const char *tmp = ""; @@ -1156,7 +1187,12 @@ int die_get_typename_from_type(Dwarf_Die *type_die, struct strbuf *buf) /* Write a base name */ return strbuf_addf(buf, "%s%s", tmp, name ?: ""); } - ret = die_get_typename(type_die, buf); + if (depth >= MAX_TYPE_CHASE) { + pr_debug("DWARF: type name recursion limit reached at DIE 0x%lx\n", + (unsigned long)dwarf_dieoffset(type_die)); + return -ENOENT; + } + ret = __die_get_typename(type_die, buf, depth + 1); if (ret < 0) { /* void pointer has no type attribute */ if (tag == DW_TAG_pointer_type && ret == -ENOENT) @@ -1167,6 +1203,19 @@ int die_get_typename_from_type(Dwarf_Die *type_die, struct strbuf *buf) return strbuf_addstr(buf, tmp); } +/** + * die_get_typename_from_type - Get the name of a type DIE + * @type_die: a type DIE + * @buf: a strbuf for the result type name + * + * Stores the typedef name if possible, "*(function_type)" for function + * pointers. Returns 0 or -ENOENT. + */ +int die_get_typename_from_type(Dwarf_Die *type_die, struct strbuf *buf) +{ + return __die_get_typename_from_type(type_die, buf, 0); +} + /** * die_get_typename - Get the name of given variable DIE * @vr_die: a variable DIE @@ -1179,12 +1228,7 @@ int die_get_typename_from_type(Dwarf_Die *type_die, struct strbuf *buf) */ int die_get_typename(Dwarf_Die *vr_die, struct strbuf *buf) { - Dwarf_Die type; - - if (__die_get_real_type(vr_die, &type) == NULL) - return -ENOENT; - - return die_get_typename_from_type(&type, buf); + return __die_get_typename(vr_die, buf, 0); } /** From 3dfdd195c7e45f5a6b85dd2b6e3722610dbd5bf5 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Thu, 24 Sep 2026 16:27:56 -0300 Subject: [PATCH 169/425] perf dwarf-aux: Add die_same_file() and die_get_type_die() dwarf_dieoffset() is relative to the file the DIE is in, so an offset saved by die_collect_vars()/die_collect_global_vars() is only meaningful in that file: add die_same_file(), comparing the Dwarf each DIE's CU belongs to, and die_get_type_die(), resolving the offset in it with the saved tag as a sanity check. Resolving the offset in the main file instead parses whatever is at it when it came from the dwz common file. dwarf_cu_getdwarf() is new in elfutils 0.160, so the libdw feature test probes for it and Makefile.config says 0.160. Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Cc: Masami Hiramatsu Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/build/feature/test-libdw.c | 10 +++++- tools/perf/Makefile.config | 2 +- tools/perf/util/dwarf-aux.c | 56 ++++++++++++++++++++++++++++++-- tools/perf/util/dwarf-aux.h | 13 ++++++++ 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/tools/build/feature/test-libdw.c b/tools/build/feature/test-libdw.c index aabd63ca76b4..46f75cde5947 100644 --- a/tools/build/feature/test-libdw.c +++ b/tools/build/feature/test-libdw.c @@ -49,8 +49,16 @@ int test_elfutils(void) return 0; } +/* Needs elfutils 0.160: taking its address fails the probe, not the link. */ +int test_libdw_cu_getdwarf(void) +{ + void *sym = (void *)dwarf_cu_getdwarf; + + return sym == NULL; +} + int main(void) { return test_libdw() + test_libdw_unwind() + test_libdw_getlocations() + - test_libdw_getcfi() + test_elfutils(); + test_libdw_getcfi() + test_libdw_cu_getdwarf() + test_elfutils(); } diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config index dca62e155fb5..b288ae0191ac 100644 --- a/tools/perf/Makefile.config +++ b/tools/perf/Makefile.config @@ -477,7 +477,7 @@ else else ifneq ($(feature-libdw), 1) ifndef NO_LIBDW - $(warning No libdw.h found or old libdw.h found or elfutils is older than 0.157, disables dwarf support. Please install new elfutils-devel/libdw-dev) + $(warning No libdw.h found or old libdw.h found or elfutils is older than 0.160, disables dwarf support. Please install new elfutils-devel/libdw-dev) NO_LIBDW := 1 endif endif # Dwarf support diff --git a/tools/perf/util/dwarf-aux.c b/tools/perf/util/dwarf-aux.c index 2c135ebb1551..54f8b5ec74a2 100644 --- a/tools/perf/util/dwarf-aux.c +++ b/tools/perf/util/dwarf-aux.c @@ -1677,6 +1677,15 @@ Dwarf_Die *die_find_variable_by_addr(Dwarf_Die *sc_die, Dwarf_Addr addr, return result; } +/* + * Whether two DIEs live in the same debug file: dwarf_dieoffset() is + * relative to the file the DIE is in. + */ +bool die_same_file(Dwarf_Die *die_a, Dwarf_Die *die_b) +{ + return dwarf_cu_getdwarf(die_a->cu) == dwarf_cu_getdwarf(die_b->cu); +} + static int __die_collect_vars_cb(Dwarf_Die *die_mem, void *arg) { struct die_var_type **var_types = arg; @@ -1721,6 +1730,8 @@ static int __die_collect_vars_cb(Dwarf_Die *die_mem, void *arg) vt->is_reg_var_addr = true; vt->die_off = dwarf_dieoffset(&type_die); + vt->die_tag = dwarf_tag(&type_die); + vt->from_alt = !die_same_file(die_mem, &type_die); vt->addr = start; vt->end = end; vt->has_range = (end != 0 || start != 0); @@ -1740,7 +1751,8 @@ static int __die_collect_vars_cb(Dwarf_Die *die_mem, void *arg) * * Save all variables and parameters in the @sc_die and save them to @var_types. * The @var_types is a singly-linked list containing type and location info. - * Actual type can be retrieved using dwarf_offdie() with 'die_off' later. + * Actual type can be retrieved using die_get_type_die() with 'die_off', + * 'die_tag' and 'from_alt' later. * * Callers should free @var_types. */ @@ -1786,6 +1798,8 @@ static int __die_collect_global_vars_cb(Dwarf_Die *die_mem, void *arg) return DIE_FIND_CB_END; vt->die_off = dwarf_dieoffset(&type_die); + vt->die_tag = dwarf_tag(&type_die); + vt->from_alt = !die_same_file(die_mem, &type_die); vt->addr = ops->number; vt->end = 0; vt->has_range = false; @@ -1797,6 +1811,43 @@ static int __die_collect_global_vars_cb(Dwarf_Die *die_mem, void *arg) return DIE_FIND_CB_SIBLING; } +/** + * die_get_type_die - Get a type DIE saved by die_collect_vars() + * @dbg: the main debug info + * @die_off: offset of the type DIE, from dwarf_dieoffset() + * @die_tag: tag that DIE had when the offset was saved + * @from_alt: whether the type DIE is in the dwz alt file + * @die_mem: where to store the resulting DIE + * + * Resolve @die_off in the file it was recorded as belonging to, with no + * fallback; @die_tag is a sanity check. + */ +Dwarf_Die *die_get_type_die(Dwarf *dbg, u64 die_off, int die_tag, bool from_alt, + Dwarf_Die *die_mem) +{ + Dwarf *target = dbg; + Dwarf_Die die; + + if (from_alt) { + /* No fallback: an alt offset here parses whatever is at it. */ + target = dwarf_getalt(dbg); + if (target == NULL) { + pr_debug("DWARF: no alt (dwz) debug file to resolve the type DIE at offset 0x%lx in\n", + (unsigned long)die_off); + return NULL; + } + } + + if (dwarf_offdie(target, die_off, &die) && dwarf_tag(&die) == die_tag) { + *die_mem = die; + return die_mem; + } + + pr_debug("DWARF: no DIE with tag %d at offset 0x%lx in the %s debug file\n", + die_tag, (unsigned long)die_off, from_alt ? "alt" : "main"); + return NULL; +} + /** * die_collect_global_vars - Save all global variables * @cu_die: a CU DIE @@ -1804,7 +1855,8 @@ static int __die_collect_global_vars_cb(Dwarf_Die *die_mem, void *arg) * * Save all global variables in the @cu_die and save them to @var_types. * The @var_types is a singly-linked list containing type and location info. - * Actual type can be retrieved using dwarf_offdie() with 'die_off' later. + * Actual type can be retrieved using die_get_type_die() with 'die_off', + * 'die_tag' and 'from_alt' later. * * Callers should free @var_types. */ diff --git a/tools/perf/util/dwarf-aux.h b/tools/perf/util/dwarf-aux.h index 9b662db71022..6f9145510adc 100644 --- a/tools/perf/util/dwarf-aux.h +++ b/tools/perf/util/dwarf-aux.h @@ -152,6 +152,8 @@ int die_get_scopes(Dwarf_Die *cu_die, Dwarf_Addr pc, Dwarf_Die **scopes); struct die_var_type { struct die_var_type *next; u64 die_off; + int die_tag; + bool from_alt; /* die_off is relative to the alt (dwz) file */ u64 addr; u64 end; /* end address of location range */ int reg; @@ -183,6 +185,17 @@ Dwarf_Die *die_find_variable_by_addr(Dwarf_Die *sc_die, Dwarf_Addr addr, /* Save all variables and parameters in this scope */ void die_collect_vars(Dwarf_Die *sc_die, struct die_var_type **var_types); +/* + * Get the type DIE saved by die_collect_vars()/die_collect_global_vars(): + * @from_alt says which file the offset must be resolved in, @die_tag is + * a sanity check. + */ +Dwarf_Die *die_get_type_die(Dwarf *dbg, u64 die_off, int die_tag, bool from_alt, + Dwarf_Die *die_mem); + +/* Whether two DIEs live in the same debug file */ +bool die_same_file(Dwarf_Die *die_a, Dwarf_Die *die_b); + /* Save all global variables in this CU */ void die_collect_global_vars(Dwarf_Die *cu_die, struct die_var_type **var_types); From 14ca0a28de37849afdda6ffd4c9047f58d8906c2 Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Thu, 24 Sep 2026 16:27:56 -0300 Subject: [PATCH 170/425] perf annotate-data: Resolve type DIEs in the debug file they came from 'perf report -s type' hangs burning CPU on the dwz compressed debug info of libz.so.1 (zlib-ng): die_collect_vars() saves the dwarf_dieoffset() of the type DIE, which is relative to the file it lives in - the dwz alt file for the types shared by more than one CU - and update_var_state() resolves that offset in the main debug file, parsing whatever is at it, here a typedef whose DW_AT_type refers to itself. Record whether the type DIE came from the alt file and resolve the offset with die_get_type_die() in that file, with no fallback. Fixes: 06b2ce75386df04b ("perf annotate-data: Maintain variable type info") Fixes: 55ee3d005d62279d ("perf annotate-data: Add a cache for global variable types") Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/annotate-data.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tools/perf/util/annotate-data.c b/tools/perf/util/annotate-data.c index e9b58a3d587b..bc9698abc6a9 100644 --- a/tools/perf/util/annotate-data.c +++ b/tools/perf/util/annotate-data.c @@ -739,6 +739,8 @@ struct global_var_entry { u64 start; u64 end; u64 die_offset; + int die_tag; + bool from_alt; /* die_offset is relative to the alt (dwz) file */ }; static int global_var_cmp(const void *_key, const struct rb_node *node) @@ -776,7 +778,7 @@ static struct global_var_entry *global_var__find(struct data_loc_info *dloc, u64 } static bool global_var__add(struct data_loc_info *dloc, u64 addr, - const char *name, Dwarf_Die *type_die) + const char *name, Dwarf_Die *type_die, bool from_alt) { struct dso *dso = map__dso(dloc->ms->map); struct global_var_entry *gvar; @@ -798,6 +800,8 @@ static bool global_var__add(struct data_loc_info *dloc, u64 addr, gvar->start = addr; gvar->end = addr + size; gvar->die_offset = dwarf_dieoffset(type_die); + gvar->die_tag = dwarf_tag(type_die); + gvar->from_alt = from_alt; rb_add(&gvar->node, dso__global_vars(dso), global_var_less); return true; @@ -872,12 +876,14 @@ static void global_var__collect(struct data_loc_info *dloc) if (pos->reg != -1) continue; - if (!dwarf_offdie(dwarf, pos->die_off, &type_die)) + if (!die_get_type_die(dwarf, pos->die_off, pos->die_tag, + pos->from_alt, &type_die)) continue; get_global_var_info(dloc, pos->addr, &var_name, &var_offset); - global_var__add(dloc, pos->addr, var_name, &type_die); + global_var__add(dloc, pos->addr, var_name, &type_die, + pos->from_alt); } delete_var_types(var_types); @@ -902,7 +908,8 @@ bool get_global_var_type(Dwarf_Die *cu_die, struct data_loc_info *dloc, gvar = global_var__find(dloc, var_addr); if (gvar) { - if (!dwarf_offdie(dloc->di->dbg, gvar->die_offset, type_die)) + if (!die_get_type_die(dloc->di->dbg, gvar->die_offset, + gvar->die_tag, gvar->from_alt, type_die)) return false; *var_offset = var_addr - gvar->start; @@ -932,7 +939,8 @@ bool get_global_var_type(Dwarf_Die *cu_die, struct data_loc_info *dloc, ok: /* The address should point to the start of the variable */ - global_var__add(dloc, var_addr - *var_offset, var_name, type_die); + global_var__add(dloc, var_addr - *var_offset, var_name, type_die, + !die_same_file(cu_die, type_die)); return true; } @@ -987,7 +995,8 @@ static void update_var_state(struct type_state *state, struct data_loc_info *dlo continue; } /* Get the type DIE using the offset */ - if (!dwarf_offdie(dloc->di->dbg, var->die_off, &mem_die)) + if (!die_get_type_die(dloc->di->dbg, var->die_off, + var->die_tag, var->from_alt, &mem_die)) continue; if (var->reg == DWARF_REG_FB || var->reg == fbreg || var->reg == state->stack_reg) { From 02ee4666a0cdeaa1cda4c46293aa367a344971ca Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Thu, 24 Sep 2026 16:27:56 -0300 Subject: [PATCH 171/425] perf annotate-data: Bound the member nesting recursion Members are added recursively, and a broken DIE can make a member's type point back at one of its own ancestors, recursing until the stack is gone; nothing usable comes out of nesting members 32 deep anyway, so stop there, marking the member as truncated and giving up on member types that don't resolve. The check is where the children of an aggregate would be expanded, so only struct/union members are marked as truncated. Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/annotate-data.c | 20 ++++++++++++++++++-- tools/perf/util/annotate-data.h | 3 +++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/tools/perf/util/annotate-data.c b/tools/perf/util/annotate-data.c index bc9698abc6a9..19a6ecd67f28 100644 --- a/tools/perf/util/annotate-data.c +++ b/tools/perf/util/annotate-data.c @@ -222,6 +222,12 @@ static bool data_type_less(struct rb_node *node_a, const struct rb_node *node_b) return strcmp(a->self.type_name, b->self.type_name) < 0; } +/* + * A broken type can point back at one of its own ancestors: bound the + * nesting so it doesn't recurse until the stack is gone. + */ +#define MAX_MEMBER_DEPTH 32 + /* Recursively add new members for struct/union */ static int __add_member_cb(Dwarf_Die *die, void *arg) { @@ -236,6 +242,9 @@ static int __add_member_cb(Dwarf_Die *die, void *arg) if (dwarf_tag(die) != DW_TAG_member) return DIE_FIND_CB_SIBLING; + if (die_get_real_type(die, &die_mem) == NULL) + return DIE_FIND_CB_SIBLING; + member = zalloc(sizeof(*member)); if (member == NULL) return DIE_FIND_CB_END; @@ -248,8 +257,6 @@ static int __add_member_cb(Dwarf_Die *die, void *arg) if (die_get_typename(die, &sb) < 0) strbuf_add(&sb, "(unknown type)", 14); - die_get_real_type(die, &die_mem); - if (dwarf_aggregate_size(&die_mem, &size) < 0 || size == 0) { if (dwarf_tag(&die_mem) == DW_TAG_array_type) { /* flex-array? */ die_get_real_type(&die_mem, &die_mem); @@ -299,6 +306,7 @@ static int __add_member_cb(Dwarf_Die *die, void *arg) } member->size = size; member->offset = loc + parent->offset; + member->depth = parent->depth + 1; INIT_LIST_HEAD(&member->children); list_for_each_entry_reverse(prev, &parent->children, node) { @@ -313,6 +321,14 @@ static int __add_member_cb(Dwarf_Die *die, void *arg) member->is_union = true; /* fall through */ case DW_TAG_structure_type: + /* Only aggregates have children to expand, so only they get truncated. */ + if (member->depth >= MAX_MEMBER_DEPTH) { + /* Consumed by the JSON exporter added in a later series. */ + member->truncated = true; + pr_debug_dtp("member nesting limit reached at %s\n", + member->type_name ?: "(unknown type)"); + break; + } die_find_child(&die_mem, __add_member_cb, member, &die_mem); break; default: diff --git a/tools/perf/util/annotate-data.h b/tools/perf/util/annotate-data.h index 14b8113521a9..bbf4fd35c1d7 100644 --- a/tools/perf/util/annotate-data.h +++ b/tools/perf/util/annotate-data.h @@ -61,6 +61,9 @@ struct annotated_member { int size; bool is_union; bool is_flex_array; + /* Children not expanded because the nesting limit was reached */ + bool truncated; + u8 depth; }; /** From 010147f87a0c0e940fe8bf14abbd20c0224d58de Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Thu, 24 Sep 2026 16:27:57 -0300 Subject: [PATCH 172/425] perf mem record: Request PERF_SAMPLE_CPU by default The data type profiling per-sample stream keys cross-CPU contention on sample->cpu, which without PERF_SAMPLE_CPU is the "no CPU info" sentinel, making same-instance accesses from different cores indistinguishable from same-CPU traffic. 'perf mem record' already passes -d and -W to the record parser, add --sample-cpu and document it in perf-mem(1). Also grow rec_argv: the new entry overflows it on PMUs with separate load and store events, as the space for the fixed arguments was not reserved. Reviewed-by: Ian Rogers Reviewed-by: Namhyung Kim Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-mem.txt | 4 ++++ tools/perf/builtin-mem.c | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/perf/Documentation/perf-mem.txt b/tools/perf/Documentation/perf-mem.txt index 4d164836d094..fe51c5e3333d 100644 --- a/tools/perf/Documentation/perf-mem.txt +++ b/tools/perf/Documentation/perf-mem.txt @@ -14,6 +14,10 @@ DESCRIPTION ----------- "perf mem record" runs a command and gathers memory operation data from it, into perf.data. Perf record options are accepted and are passed through. +It also requests the address (-d), the weight (-W, where supported) and the +CPU id (--sample-cpu) of every sampled access by default; the CPU id is what +lets per-sample analysis tell reads and writes to the same data from +different cores apart from same-CPU traffic. "perf mem report" displays the result. It invokes perf report with the right set of options to display a memory access profile. By default, loads diff --git a/tools/perf/builtin-mem.c b/tools/perf/builtin-mem.c index 6101a26b3a78..ade53688ebcd 100644 --- a/tools/perf/builtin-mem.c +++ b/tools/perf/builtin-mem.c @@ -99,8 +99,8 @@ static int __cmd_record(int argc, const char **argv, struct perf_mem *mem, argc = parse_options(argc, argv, options, record_usage, PARSE_OPT_KEEP_UNKNOWN); - /* Max number of arguments multiplied by number of PMUs that can support them. */ - rec_argc = argc + 9 * (perf_pmu__mem_events_num_mem_pmus(pmu) + 1); + /* Max number of arguments per PMU plus the fixed ones added below. */ + rec_argc = argc + 8 + 9 * (perf_pmu__mem_events_num_mem_pmus(pmu) + 1); if (mem->cpu_list) rec_argc += 2; @@ -135,6 +135,9 @@ static int __cmd_record(int argc, const char **argv, struct perf_mem *mem, rec_argv[i++] = "-d"; + /* Cross-CPU contention keys on sample->cpu, -1 without PERF_SAMPLE_CPU. */ + rec_argv[i++] = "--sample-cpu"; + if (mem->phys_addr) rec_argv[i++] = "--phys-data"; From 528b1475f9cffbaffb37f490e86780662471ef3f Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Thu, 24 Sep 2026 16:27:57 -0300 Subject: [PATCH 173/425] perf mem record: Use the IBS swfilt filter when available IBS events with exclude_{user,kernel} bits, as used for per-thread recording when kernel samples are not allowed, are rejected on hardware without the privilege filter, so per-thread 'perf mem record' fails on AMD: $ perf mem record -o /dev/null -- true Failure to open event 'ibs_op/ldlat=0/u' on PMU 'ibs_op' which will be removed. Kernel v6.14 added swfilt, a software privilege filter exposed as the 'swfilt' format term, making those events usable per-thread. Give the ibs_op memory events extra tables with the term, selected in perf_pmu__arch_init() when the PMU exposes it, keeping the names that need system wide mode otherwise; the knowledge that IBS needs this stays in the arch code. Suggested-by: Namhyung Kim Suggested-by: Ravi Bangoria Reviewed-by: Namhyung Kim Reviewed-by: Ravi Bangoria Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/arch/x86/util/mem-events.c | 17 +++++++++++++++++ tools/perf/arch/x86/util/mem-events.h | 2 ++ tools/perf/arch/x86/util/pmu.c | 10 ++++++++-- tools/perf/tests/shell/test_data_symbol.sh | 6 ++++-- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/tools/perf/arch/x86/util/mem-events.c b/tools/perf/arch/x86/util/mem-events.c index b38f519020ff..8b46c8a1ce45 100644 --- a/tools/perf/arch/x86/util/mem-events.c +++ b/tools/perf/arch/x86/util/mem-events.c @@ -21,14 +21,31 @@ struct perf_mem_event perf_mem_events_intel_aux[PERF_MEM_EVENTS__MAX] = { E(NULL, NULL, NULL, false, 0), }; +/* + * IBS events with exclude_{user,kernel} bits are rejected on hardware + * without the privilege filter unless swfilt is used: the extra tables + * carry the term, selected when the PMU exposes it. + */ struct perf_mem_event perf_mem_events_amd[PERF_MEM_EVENTS__MAX] = { E(NULL, NULL, NULL, false, 0), E(NULL, NULL, NULL, false, 0), E("mem-ldst", "%s//", NULL, false, 0), }; +struct perf_mem_event perf_mem_events_amd_swfilt[PERF_MEM_EVENTS__MAX] = { + E(NULL, NULL, NULL, false, 0), + E(NULL, NULL, NULL, false, 0), + E("mem-ldst", "%s/swfilt=1/", NULL, false, 0), +}; + struct perf_mem_event perf_mem_events_amd_ldlat[PERF_MEM_EVENTS__MAX] = { E(NULL, NULL, NULL, false, 0), E(NULL, NULL, NULL, false, 0), E("mem-ldst", "%s/ldlat=%u/", NULL, true, 0), }; + +struct perf_mem_event perf_mem_events_amd_ldlat_swfilt[PERF_MEM_EVENTS__MAX] = { + E(NULL, NULL, NULL, false, 0), + E(NULL, NULL, NULL, false, 0), + E("mem-ldst", "%s/ldlat=%u,swfilt=1/", NULL, true, 0), +}; diff --git a/tools/perf/arch/x86/util/mem-events.h b/tools/perf/arch/x86/util/mem-events.h index 11e09a256f5b..f707de380370 100644 --- a/tools/perf/arch/x86/util/mem-events.h +++ b/tools/perf/arch/x86/util/mem-events.h @@ -6,6 +6,8 @@ extern struct perf_mem_event perf_mem_events_intel[PERF_MEM_EVENTS__MAX]; extern struct perf_mem_event perf_mem_events_intel_aux[PERF_MEM_EVENTS__MAX]; extern struct perf_mem_event perf_mem_events_amd[PERF_MEM_EVENTS__MAX]; +extern struct perf_mem_event perf_mem_events_amd_swfilt[PERF_MEM_EVENTS__MAX]; extern struct perf_mem_event perf_mem_events_amd_ldlat[PERF_MEM_EVENTS__MAX]; +extern struct perf_mem_event perf_mem_events_amd_ldlat_swfilt[PERF_MEM_EVENTS__MAX]; #endif /* _X86_MEM_EVENTS_H */ diff --git a/tools/perf/arch/x86/util/pmu.c b/tools/perf/arch/x86/util/pmu.c index 2c24ef3140da..fd4491fda117 100644 --- a/tools/perf/arch/x86/util/pmu.c +++ b/tools/perf/arch/x86/util/pmu.c @@ -333,6 +333,7 @@ static void uncore_cha_imc_adjust_cpumask_for_snc(struct perf_pmu *pmu, bool cha void perf_pmu__arch_init(struct perf_pmu *pmu) { struct perf_pmu_caps *ldlat_cap; + bool swfilt_format; if (!strcmp(pmu->name, INTEL_PT_PMU_NAME)) { pmu->auxtrace = true; @@ -348,7 +349,10 @@ void perf_pmu__arch_init(struct perf_pmu *pmu) if (strcmp(pmu->name, "ibs_op")) return; - pmu->mem_events = perf_mem_events_amd; + swfilt_format = perf_pmu__has_format(pmu, "swfilt"); + pmu->mem_events = swfilt_format ? + perf_mem_events_amd_swfilt : + perf_mem_events_amd; if (!perf_pmu__caps_parse(pmu)) return; @@ -358,7 +362,9 @@ void perf_pmu__arch_init(struct perf_pmu *pmu) return; perf_mem_events__loads_ldlat = 0; - pmu->mem_events = perf_mem_events_amd_ldlat; + pmu->mem_events = swfilt_format ? + perf_mem_events_amd_ldlat_swfilt : + perf_mem_events_amd_ldlat; } else { if (pmu->is_core) { if (perf_pmu__have_event(pmu, "mem-loads-aux")) diff --git a/tools/perf/tests/shell/test_data_symbol.sh b/tools/perf/tests/shell/test_data_symbol.sh index edc528c11dda..8d47f14a8802 100755 --- a/tools/perf/tests/shell/test_data_symbol.sh +++ b/tools/perf/tests/shell/test_data_symbol.sh @@ -64,15 +64,17 @@ if (($is_amd >= 1)); then # --ldlat on AMD: # o Zen4 and earlier uarch does not support ldlat # o Even on supported platforms, it's disabled (--ldlat=0) by default. + # o Kernels with the swfilt term add it even when ldlat is not + # supported, so only check ldlat when the term is present. ldlat=${BASH_REMATCH[1]} - if [[ -n $ldlat ]]; then + if [[ $ldlat == *ldlat=* ]]; then if ! [[ "$ldlat" =~ ldlat=0 ]]; then echo "ERROR: ldlat not initialized to 0?" exit 1 fi mem_events="$(perf mem record -v --ldlat=150 -e list 2>&1)" - if ! [[ "$mem_events" =~ ^mem-ldst.*ibs_op/ldlat=150/.*available ]]; then + if ! [[ "$mem_events" =~ ^mem-ldst.*ibs_op/ldlat=150[,/].*available ]]; then echo "ERROR: --ldlat not honored?" exit 1 fi From b1d0b4093fe6055c9e534387ccdc28a5278e99ff Mon Sep 17 00:00:00 2001 From: Arnaldo Carvalho de Melo Date: Fri, 25 Sep 2026 19:05:18 -0300 Subject: [PATCH 174/425] perf tools: Bump the minimum Python version to 3.9 perf's build only refused interpreters older than 3.6, the check added when jevents.py started using f-strings, and jevents.py carried a removesuffix() shim to stay usable on them. The kernel itself requires Python 3.9.x since 5e25b972a22be224 ("docs: changes: update Python minimal version"), so tools in the tree are expected to work with it, and staying compatible with older interpreters stopped paying off: - the PEP 585 generics, list[...], used by the type annotations in jevents.py and util/setup.py are only subscriptable from Python 3.9 and, being evaluated at import or definition time, they break the build on RHEL 8's python3.6.8 with: TypeError: 'type' object is not subscriptable - ProcessPoolExecutor(initializer=, initargs=) requires Python 3.7, so generating pmu-events.c fails there as well. Rather than spreading version guards and typing.List() workarounds for interpreters that cannot be supported anyway, make 3.9 the floor: ask for it once in Makefile.config and use the result in both places that run python while building, jevents generation and the python binding, as the binding is still built when NO_JEVENTS=1 skips the generation check, and drop the removesuffix() shim, since str.removesuffix() was added in 3.9. RHEL 8 is in feature freeze, so those wanting a new perf and its python features have to vendor a newer interpreter, just as Documentation/process/changes.rst already asks for the kernel itself. Tested with Python 3.9.22: perf builds, generating pmu-events.c and the python binding, with byte for byte identical output to the build made with the shim, while Python 3.8.20 and 3.6.15 are rejected with the "older than 3.9" error, by the jevents check in a regular build and by the binding check when NO_JEVENTS=1 makes them sail past it, where they used to die on the TypeError from util/setup.py. With NO_JEVENTS=1 and NO_LIBPYTHON=1 no interpreter runs while building, so neither check applies. vermin reports no perf build file as requiring more than 3.9, the only offender being the runtime script scripts/python/mem-phys-addr.py, which the build doesn't run. Suggested-by: Ian Rogers Reviewed-by: Ian Rogers Reviewed-by: Mauro Carvalho Chehab Cc: Mauro Carvalho Chehab Assisted-by: LLM Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Makefile.config | 24 ++++++++++++++++++++---- tools/perf/pmu-events/jevents.py | 16 +++------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config index b288ae0191ac..c4ae1cf04e00 100644 --- a/tools/perf/Makefile.config +++ b/tools/perf/Makefile.config @@ -834,6 +834,16 @@ define disable-python_code NO_LIBPYTHON := 1 endef +# Minimum Python version for the interpreters perf runs while building, +# the same one the kernel requires, see +# Documentation/process/changes.rst: jevents generation (pmu-events/ +# jevents.py) and the python binding (util/setup.py) both use PEP 585 +# generics, list[...] and friends, that are subscriptable only from +# Python 3.9. +ifdef PYTHON + PYTHON_39_GOOD := $(shell $(PYTHON) -c 'import sys;print("1" if sys.version_info >= (3, 9) else "0")' 2> /dev/null) +endif + PYTHON_EXTENSION_SUFFIX := '.so' ifdef NO_LIBPYTHON $(call disable-python,Python support disabled by user) @@ -851,6 +861,14 @@ else ifneq ($(feature-libpython), 1) $(call disable-python,No 'Python.h' was found: disables Python support - please install python-devel/python-dev) else + # The binding is built even when jevents generation is turned off + # with NO_JEVENTS=1, which skips the check in that block below, so + # check the version here as well: otherwise util/setup.py dies on + # older interpreters with "TypeError: 'type' object is not + # subscriptable" instead of a Makefile error. + ifneq ($(PYTHON_39_GOOD), 1) + $(error ERROR: Python interpreter needed for the python binding too old (older than 3.9). Install a newer python or build with NO_LIBPYTHON=1.) + endif LDFLAGS += $(PYTHON_EMBED_LDFLAGS) EXTLIBS += $(PYTHON_EMBED_LIBADD) PYTHON_SETUPTOOLS_INSTALLED := $(shell $(PYTHON) -c 'import setuptools;' 2> /dev/null && echo "yes" || echo "no") @@ -885,10 +903,8 @@ ifneq ($(NO_JEVENTS),1) ifndef PYTHON $(error ERROR: No python interpreter needed for jevents generation. Install python or build with NO_JEVENTS=1.) else - # jevents.py uses f-strings present in Python 3.6 released in Dec. 2016. - JEVENTS_PYTHON_GOOD := $(shell $(PYTHON) -c 'import sys;print("1" if(sys.version_info.major >= 3 and sys.version_info.minor >= 6) else "0")' 2> /dev/null) - ifneq ($(JEVENTS_PYTHON_GOOD), 1) - $(error ERROR: Python interpreter needed for jevents generation too old (older than 3.6). Install a newer python or build with NO_JEVENTS=1.) + ifneq ($(PYTHON_39_GOOD), 1) + $(error ERROR: Python interpreter needed for jevents generation too old (older than 3.9). Install a newer python or build with NO_JEVENTS=1.) endif endif endif diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py index 074daae5308b..296bf68d1081 100755 --- a/tools/perf/pmu-events/jevents.py +++ b/tools/perf/pmu-events/jevents.py @@ -65,15 +65,6 @@ _json_metric_attributes = [ _json_enum_attributes = ['aggr_mode', 'deprecated', 'event_grouping', 'perpkg', 'default_show_events'] -def removesuffix(s: str, suffix: str) -> str: - """Remove the suffix from a string - - The removesuffix function is added to str in Python 3.9. We aim for 3.6 - compatibility and so provide our own function here. - """ - return s[0:-len(suffix)] if s.endswith(suffix) else s - - def file_name_to_table_name(prefix: str, parents: Sequence[str], dirname: str) -> str: """Generate a C table name from directory names.""" @@ -214,9 +205,8 @@ class JsonEvent: """Fix formatting issue for the desc string.""" if s is None: return None - return removesuffix(removesuffix(removesuffix(s, '. '), - '. '), '.').replace('\n', '\\n').replace( - '\"', '\\"').replace('\r', '\\r') + return s.removesuffix('. ').removesuffix('. ').removesuffix('.').replace( + '\n', '\\n').replace('\"', '\\"').replace('\r', '\\r') def convert_aggr_mode(aggr_mode: Optional[str]) -> Optional[str]: """Returns the aggr_mode_class enum value associated with the JSON string.""" @@ -641,7 +631,7 @@ static const struct pmu_table_entry {_pending_metrics_tblname}[] = {{ def get_topic(topic: str) -> str: if topic.endswith('metrics.json'): return 'metrics' - return removesuffix(topic, '.json').replace('-', ' ') + return topic.removesuffix('.json').replace('-', ' ') def preprocess_one_file(parents: Sequence[str], item: os.DirEntry) -> None: if item.is_dir(): From bac027175ada974b3fde9eb0690ca5d698e0a6d1 Mon Sep 17 00:00:00 2001 From: Hui Su Date: Sat, 12 Sep 2026 12:11:12 +0900 Subject: [PATCH 175/425] perf synthetic-events: Check schedstat domain allocation failure __synthesize_schedstat_domain() uses event after zalloc() without checking for allocation failure, unlike __synthesize_schedstat_cpu(). If zalloc() fails and returns NULL, dereferencing ds->header.type immediately crashes with a segmentation fault: Program received signal SIGSEGV, Segmentation fault. 0x00005555558aed8a in __synthesize_schedstat_domain () #0 0x00005555558aed8a in __synthesize_schedstat_domain () #1 0x00005555558b889a in perf_event__synthesize_schedstat () #2 0x000055555563c397 in cmd_sched () #3 0x00005555556583a1 in handle_internal_command () #4 0x00005555555bf739 in main () perf_event__synthesize_schedstat() already treats a NULL event as an error and cleans up, so return NULL when the allocation fails. After this fix, an allocation failure returns NULL safely, allowing the caller to exit gracefully without crashing: [ perf sched stats: Failed !! ] Fixes: c3030995f23b3d35 ("perf sched stats: Add record and rawdump support") Signed-off-by: Hui Su Cc: Swapnil Sapkal Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/util/synthetic-events.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/perf/util/synthetic-events.c b/tools/perf/util/synthetic-events.c index 6ba50bdbd16e..b9dbd105cdc3 100644 --- a/tools/perf/util/synthetic-events.c +++ b/tools/perf/util/synthetic-events.c @@ -2814,6 +2814,8 @@ static union perf_event *__synthesize_schedstat_domain(struct io *io, __u16 vers size = sizeof(*ds); size = PERF_ALIGN(size, sizeof(u64)); event = zalloc(size); + if (!event) + return NULL; ds = &event->schedstat_domain; ds->header.type = PERF_RECORD_SCHEDSTAT_DOMAIN; From c4925cc8b8b1647b3cf9a9d8e84ac4c9f1e962c5 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:28 -0700 Subject: [PATCH 176/425] perf python: Update syscall helpers and expose arch_strerrno Update the PyArg_ParseTupleAndKeywords format string for pyrf__syscall_name() and pyrf__syscall_id() from "i|$i" to "i|i" (and "s|$i" to "s|i"), allowing elf_machine to be passed as an optional positional argument as well as a keyword argument. Update syscall_id() and syscall_name() in perf.pyi to match, and set the return type of syscall_name() to Optional[str] as unknown syscall numbers return None. Also expose perf_env__arch_strerrno() as perf.arch_strerrno(err, elf_machine=None) so standalone Python scripts can resolve architecture-specific errno names directly from the auto-generated trace beauty tables instead of maintaining duplicate errno dictionaries in Python. Tighten the macro pattern in arch_errno_names.sh to E[A-Z0-9]+ so compiler -D flags containing underscores (such as LLVM's -DEXPERIMENTAL_KEY_INSTRUCTIONS=1) do not shadow EPERM (1), and add EM_SPARC32PLUS alongside EM_SPARC and EM_SPARCV9. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/perf.pyi | 16 +++++- tools/perf/trace/beauty/arch_errno_names.sh | 4 +- tools/perf/util/python.c | 61 ++++++++++++++++++++- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/tools/perf/python/perf.pyi b/tools/perf/python/perf.pyi index 58b0b3ed819d..c390e17708b1 100644 --- a/tools/perf/python/perf.pyi +++ b/tools/perf/python/perf.pyi @@ -20,7 +20,7 @@ def metrics() -> List[Dict[str, Union[str, List[str]]]]: """ ... -def syscall_name(id: int, *, elf_machine: Optional[int] = None) -> str: +def syscall_name(id: int, elf_machine: Optional[int] = None) -> Optional[str]: """Convert a syscall number to its name. Args: @@ -32,7 +32,7 @@ def syscall_name(id: int, *, elf_machine: Optional[int] = None) -> str: """ ... -def syscall_id(name: str, *, elf_machine: Optional[int] = None) -> int: +def syscall_id(name: str, elf_machine: Optional[int] = None) -> int: """Convert a syscall name to its number. Args: @@ -44,6 +44,18 @@ def syscall_id(name: str, *, elf_machine: Optional[int] = None) -> int: """ ... +def arch_strerrno(err: int, elf_machine: Optional[int] = None) -> Optional[str]: + """Convert an errno number to its symbolic name. + + Args: + err: The errno number (positive or negative). + elf_machine: Optional ELF machine type. + + Returns: + The symbolic name of the errno, or None if unknown. + """ + ... + def parse_events( event_string: str, cpus: Optional[cpu_map] = None, diff --git a/tools/perf/trace/beauty/arch_errno_names.sh b/tools/perf/trace/beauty/arch_errno_names.sh index 8751bfa4a2b2..e75cd5bc4328 100755 --- a/tools/perf/trace/beauty/arch_errno_names.sh +++ b/tools/perf/trace/beauty/arch_errno_names.sh @@ -46,7 +46,7 @@ process_arch() asm_errno=$(asm_errno_file "$arch") $gcc $CFLAGS $include_path -E -dM -x c $asm_errno \ - |grep -hE '^#define[[:blank:]]+(E[^[:blank:]]+)[[:blank:]]+([[:digit:]]+).*' \ + |grep -hE '^#define[[:blank:]]+(E[A-Z0-9]+)[[:blank:]]+([[:digit:]]+).*' \ |awk '{ print $2","$3; }' \ |sort -t, -k2 -nu \ |IFS=, create_errno_lookup_func "$arch" @@ -69,7 +69,7 @@ arch_to_e_machine() riscv) printf '\tcase EM_RISCV:\n' ;; s390) printf '\tcase EM_S390:\n' ;; sh) printf '\tcase EM_SH:\n' ;; - sparc) printf '\tcase EM_SPARC:\n\tcase EM_SPARCV9:\n' ;; + sparc) printf '\tcase EM_SPARC:\n\tcase EM_SPARC32PLUS:\n\tcase EM_SPARCV9:\n' ;; x86) printf '\tcase EM_386:\n\tcase EM_X86_64:\n' ;; xtensa) printf '\tcase EM_XTENSA:\n' ;; esac diff --git a/tools/perf/util/python.c b/tools/perf/util/python.c index d8a842621cda..543312c99c20 100644 --- a/tools/perf/util/python.c +++ b/tools/perf/util/python.c @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include @@ -25,6 +27,7 @@ #include "dso.h" #include "dwarf-regs.h" #include "event.h" +#include "env.h" #include "branch.h" #include "evlist.h" #include "evsel.h" @@ -4104,14 +4107,35 @@ static int pyrf_session__setup_types(void) return PyType_Ready(&pyrf_session__type); } +static int pyrf__parse_elf_machine(PyObject *obj, int *elf_machine) +{ + long val; + + if (!obj || obj == Py_None) { + *elf_machine = EM_HOST; + return 0; + } + val = PyLong_AsLong(obj); + if (val == -1 && PyErr_Occurred()) + return -1; + if (val <= 0 || val > UINT16_MAX) + *elf_machine = EM_HOST; + else + *elf_machine = (int)val; + return 0; +} + static PyObject *pyrf__syscall_name(PyObject *self, PyObject *args, PyObject *kwargs) { const char *name; int id; int elf_machine = EM_HOST; + PyObject *elf_machine_obj = NULL; static char *kwlist[] = { "id", "elf_machine", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|$i", kwlist, &id, &elf_machine)) + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|O", kwlist, &id, &elf_machine_obj)) + return NULL; + if (pyrf__parse_elf_machine(elf_machine_obj, &elf_machine) < 0) return NULL; name = syscalltbl__name(elf_machine, id); @@ -4125,9 +4149,12 @@ static PyObject *pyrf__syscall_id(PyObject *self, PyObject *args, PyObject *kwar const char *name; int id; int elf_machine = EM_HOST; + PyObject *elf_machine_obj = NULL; static char *kwlist[] = { "name", "elf_machine", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|$i", kwlist, &name, &elf_machine)) + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|O", kwlist, &name, &elf_machine_obj)) + return NULL; + if (pyrf__parse_elf_machine(elf_machine_obj, &elf_machine) < 0) return NULL; id = syscalltbl__id(elf_machine, name); @@ -4138,6 +4165,30 @@ static PyObject *pyrf__syscall_id(PyObject *self, PyObject *args, PyObject *kwar return PyLong_FromLong(id); } +static PyObject *pyrf__arch_strerrno(PyObject *self, PyObject *args, PyObject *kwargs) +{ + const char *name; + int err; + int elf_machine = EM_HOST; + PyObject *elf_machine_obj = NULL; + static char *kwlist[] = { "err", "elf_machine", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|O", kwlist, &err, &elf_machine_obj)) + return NULL; + if (pyrf__parse_elf_machine(elf_machine_obj, &elf_machine) < 0) + return NULL; + + if (err == INT_MIN) + Py_RETURN_NONE; + if (err < 0) + err = -err; + + name = perf_env__arch_strerrno((uint16_t)elf_machine, err); + if (!name || !strcmp(name, "(unknown)")) + Py_RETURN_NONE; + return PyUnicode_FromString(name); +} + static PyObject *pyrf__config_get(PyObject *self, PyObject *args) { const char *config_name, *val; @@ -4202,6 +4253,12 @@ static PyMethodDef perf__methods[] = { .ml_flags = METH_VARARGS | METH_KEYWORDS, .ml_doc = PyDoc_STR("Turns a syscall name to a number.") }, + { + .ml_name = "arch_strerrno", + .ml_meth = (PyCFunction) pyrf__arch_strerrno, + .ml_flags = METH_VARARGS | METH_KEYWORDS, + .ml_doc = PyDoc_STR("Turns an errno number to a string.") + }, { .ml_name = NULL, } }; From 514ecdfd0073686e2a61681fe6f9e08870d9a89a Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:29 -0700 Subject: [PATCH 177/425] perf python: Update callchain stubs and session thread lookup Update pyrf_session__find_thread() in util/python.c to accept an optional tid parameter (defaulting to pid) and return None instead of raising TypeError when a thread is not found in the session machines. Update perf.pyi to match: - Set session.find_thread() signature to (pid: int, tid: int = -1) -> Optional[thread]. - Update callchain_node.symbol and callchain_node.dso to str (the C getters return "[unknown]" fallback strings rather than None) and add __iter__() to callchain. Also update treport.py to handle Optional[thread] returned by session.find_thread(). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/perf.pyi | 7 ++++--- tools/perf/python/treport.py | 2 +- tools/perf/util/python.c | 19 +++++++++++-------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/tools/perf/python/perf.pyi b/tools/perf/python/perf.pyi index c390e17708b1..92aad84ca542 100644 --- a/tools/perf/python/perf.pyi +++ b/tools/perf/python/perf.pyi @@ -332,13 +332,14 @@ class branch_stack: class callchain_node: """Represents a frame in the callchain.""" ip: int - symbol: Optional[str] - dso: Optional[str] + symbol: str + dso: str class callchain: """Sequence of callchain frames.""" def __len__(self) -> int: ... def __getitem__(self, index: int) -> callchain_node: ... + def __iter__(self) -> Iterator[callchain_node]: ... class stat_event(_sample_members): """Represents a stat event from perf.""" @@ -453,7 +454,7 @@ class session: def process_events(self) -> None: """Process all events in the session.""" ... - def find_thread(self, pid: int) -> thread: + def find_thread(self, pid: int, tid: int = -1, /) -> Optional[thread]: """Returns the thread associated with a pid.""" ... diff --git a/tools/perf/python/treport.py b/tools/perf/python/treport.py index 43542599a884..786b852f471c 100755 --- a/tools/perf/python/treport.py +++ b/tools/perf/python/treport.py @@ -92,7 +92,7 @@ class ProfileNode: try: assert session thread = session.find_thread(sample.sample_tid) - comm = thread.comm() + comm = (thread.comm() if thread else None) or f"unknown ({pid})" except Exception: comm = f"unknown ({pid})" diff --git a/tools/perf/util/python.c b/tools/perf/util/python.c index 543312c99c20..40b327801b7d 100644 --- a/tools/perf/util/python.c +++ b/tools/perf/util/python.c @@ -3915,26 +3915,29 @@ static PyObject *pyrf_session__find_thread(struct pyrf_session *psession, PyObje struct machine *machine; struct thread *thread = NULL; PyObject *result; - int pid; + int pid, tid = -1; CHECK_INITIALIZED(psession->session, "session"); - if (!PyArg_ParseTuple(args, "i", &pid)) + if (!PyArg_ParseTuple(args, "i|i", &pid, &tid)) return NULL; + if (tid == -1) + tid = pid; + + /* Look up the thread in the host machine first, then fall back to guest machines. */ machine = &psession->session->machines.host; - thread = machine__find_thread(machine, pid, pid); + thread = machine__find_thread(machine, pid, tid); if (!thread) { machine = perf_session__find_machine(psession->session, pid); if (machine) - thread = machine__find_thread(machine, pid, pid); + thread = machine__find_thread(machine, pid, tid); } - if (!thread) { - PyErr_Format(PyExc_TypeError, "Failed to find thread %d", pid); - return NULL; - } + /* Return None rather than raising TypeError when a PID/TID is not known. */ + if (!thread) + Py_RETURN_NONE; result = pyrf_thread__from_thread(thread); thread__put(thread); return result; From 2dfe9eaec5c28f8f885c17d5b643299f685451b7 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:30 -0700 Subject: [PATCH 178/425] perf python: Clean up pylint warnings in ilist.py Replace bare 'except:' clauses (pylint W0702: bare-except) in ilist.py with specific exception tuples (OSError, ValueError, RuntimeError, TypeError, KeyError) and use a safe '.get("name", "")' sort key in IListApp.compose() so that ilist.py passes pylint without warning suppression comments and handles unnamed PMU events gracefully. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/ilist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/perf/python/ilist.py b/tools/perf/python/ilist.py index ebff0a843b7a..86bd65452e6e 100755 --- a/tools/perf/python/ilist.py +++ b/tools/perf/python/ilist.py @@ -82,7 +82,7 @@ class Metric(TreeValue): try: val = evlist.compute_metric(self.metric_name, cpu, thread) return 0 if math.isnan(val) else val - except: + except (OSError, ValueError, RuntimeError, TypeError): # Be tolerant of failures to compute metrics on particular CPUs/threads. return 0 @@ -419,7 +419,7 @@ class IListApp(App): if self.evlist: self.evlist.open() self.evlist.enable() - except: + except (OSError, ValueError, RuntimeError): self.evlist = None if not self.evlist: @@ -450,7 +450,7 @@ class IListApp(App): pmu_name = pmu.name().lower() pmu_node = pmus.add(pmu_name) try: - for event in sorted(pmu.events(), key=lambda x: x["name"]): + for event in sorted(pmu.events(), key=lambda x: x.get("name", "")): if "deprecated" in event: continue if "name" in event: @@ -460,7 +460,7 @@ class IListApp(App): data=PmuEvent(pmu_name, e)) else: pmu_node.add_leaf(e, data=PmuEvent(pmu_name, e)) - except: + except (OSError, ValueError, RuntimeError, KeyError, TypeError): # Reading events may fail with EPERM, ignore. pass metrics = tree.root.add("Metrics") From 2997739214ce6e0825f127e1a71d0f82526aa83a Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:31 -0700 Subject: [PATCH 179/425] perf python: Clean up pylint warnings in treport.py Clean up pylint warnings in treport.py so that it passes pylint without warning suppression comments: - Replace broad 'except Exception:' clauses (W0718: broad-exception-caught) with specific exception tuples. - Pass both pid and sample_tid to session.find_thread() so guest and process threads are resolved accurately. - Rename parameter 'args' to 'pos_args' in FlameGraph.__init__() to avoid shadowing the outer scope variable 'args' (W0621: redefined-outer-name). - Clear session = None in a finally block after session.process_events() to break the C perf_session reference cycle before launching the TUI. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/treport.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/perf/python/treport.py b/tools/perf/python/treport.py index 786b852f471c..0921b428b1aa 100755 --- a/tools/perf/python/treport.py +++ b/tools/perf/python/treport.py @@ -91,9 +91,9 @@ class ProfileNode: pid = sample.sample_pid try: assert session - thread = session.find_thread(sample.sample_tid) + thread = session.find_thread(pid, sample.sample_tid) comm = (thread.comm() if thread else None) or f"unknown ({pid})" - except Exception: + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError): comm = f"unknown ({pid})" period = sample.sample_period @@ -412,9 +412,9 @@ class FlameGraph(ScrollView): } """ - def __init__(self, root: ProfileNode, *args, **kwargs): + def __init__(self, root: ProfileNode, *pos_args, **kwargs): """Initialize the FlameGraph widget.""" - super().__init__(*args, **kwargs) + super().__init__(*pos_args, **kwargs) self.root = root self.cursor = root self.selected = root @@ -549,12 +549,15 @@ if __name__ == "__main__": profile = ProfileBuilder() try: session = perf.session(perf.data(input_file), sample=profile.process_event) - except Exception as e: + except (OSError, ValueError, RuntimeError) as e: print(f"Error opening session: {e}", file=sys.stderr) sys.exit(1) # profile.process_event is called for each perf event to build the profile. - session.process_events() + try: + session.process_events() + finally: + session = None # Visualize data. app = ReportApp(profile.root) From 62dfb55494e4cb62a2ab127224c8dcbd18499a0b Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:32 -0700 Subject: [PATCH 180/425] perf python: Clean up pylint warnings in tracepoint.py Clean up pylint warnings in tracepoint.py so that it passes pylint without warning suppression comments: - Catch ImportError instead of bare 'except:' (W0702: bare-except) when importing setproctitle. - Remove unnecessary trailing semicolon (W0301: unnecessary-semicolon) after evlist.enable(). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/tracepoint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/perf/python/tracepoint.py b/tools/perf/python/tracepoint.py index 15b0c8268996..9d49a25b3b1e 100755 --- a/tools/perf/python/tracepoint.py +++ b/tools/perf/python/tracepoint.py @@ -9,7 +9,7 @@ def change_proctitle(): try: import setproctitle setproctitle.setproctitle("tracepoint.py") - except: + except ImportError: print("Install the setproctitle python package to help with top and friends") def main(): @@ -29,7 +29,7 @@ def main(): evlist.open() evlist.mmap() - evlist.enable(); + evlist.enable() while True: evlist.poll(timeout = -1) From abbcb48f33e6eba5bb955c4af431d2920779c0f7 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:33 -0700 Subject: [PATCH 181/425] perf python: Clean up pylint warnings in twatch.py Clean up pylint warnings in twatch.py so that it passes pylint without warning suppression comments: - Convert tab indentation to 4 spaces (W0311: bad-indentation). - Remove unnecessary trailing semicolon (W0301: unnecessary-semicolon) after evsel.open(). - Convert floating multi-line string comments to '#' comments (W0105: pointless-string-statement). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/twatch.py | 76 ++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/tools/perf/python/twatch.py b/tools/perf/python/twatch.py index 04f3db29b9bc..ad35b1620781 100755 --- a/tools/perf/python/twatch.py +++ b/tools/perf/python/twatch.py @@ -9,53 +9,51 @@ import perf def main(context_switch = 0, thread = -1): - cpus = perf.cpu_map() - threads = perf.thread_map(thread) - evsel = perf.evsel(type = perf.TYPE_SOFTWARE, - config = perf.COUNT_SW_DUMMY, - task = 1, comm = 1, mmap = 0, freq = 0, - wakeup_events = 1, watermark = 1, - sample_id_all = 1, context_switch = context_switch, - sample_type = perf.SAMPLE_PERIOD | perf.SAMPLE_TID | perf.SAMPLE_CPU) + cpus = perf.cpu_map() + threads = perf.thread_map(thread) + evsel = perf.evsel(type = perf.TYPE_SOFTWARE, + config = perf.COUNT_SW_DUMMY, + task = 1, comm = 1, mmap = 0, freq = 0, + wakeup_events = 1, watermark = 1, + sample_id_all = 1, context_switch = context_switch, + sample_type = perf.SAMPLE_PERIOD | perf.SAMPLE_TID | perf.SAMPLE_CPU) - """What we want are just the PERF_RECORD_ lifetime events for threads, - using the default, PERF_TYPE_HARDWARE + PERF_COUNT_HW_CYCLES & freq=1 - (the default), makes perf reenable irq_vectors:local_timer_entry, when - disabling nohz, not good for some use cases where all we want is to get - threads comes and goes... So use (perf.TYPE_SOFTWARE, perf_COUNT_SW_DUMMY, - freq=0) instead.""" + # What we want are just the PERF_RECORD_ lifetime events for threads, + # using the default, PERF_TYPE_HARDWARE + PERF_COUNT_HW_CYCLES & freq=1 + # (the default), makes perf reenable irq_vectors:local_timer_entry, when + # disabling nohz, not good for some use cases where all we want is to get + # threads comes and goes... So use (perf.TYPE_SOFTWARE, perf_COUNT_SW_DUMMY, + # freq=0) instead. - evsel.open(cpus = cpus, threads = threads); - evlist = perf.evlist(cpus, threads) - evlist.add(evsel) - evlist.mmap() - while True: - evlist.poll(timeout = -1) - for cpu in cpus: - event = evlist.read_on_cpu(cpu) - if not event: - continue - print("cpu: {0}, pid: {1}, tid: {2} {3}".format(event.sample_cpu, + evsel.open(cpus = cpus, threads = threads) + evlist = perf.evlist(cpus, threads) + evlist.add(evsel) + evlist.mmap() + while True: + evlist.poll(timeout = -1) + for cpu in cpus: + event = evlist.read_on_cpu(cpu) + if not event: + continue + print("cpu: {0}, pid: {1}, tid: {2} {3}".format(event.sample_cpu, event.sample_pid, event.sample_tid, event)) if __name__ == '__main__': - """ - To test the PERF_RECORD_SWITCH record, pick a pid and replace - in the following line. + # To test the PERF_RECORD_SWITCH record, pick a pid and replace + # in the following line. - Example output: + # Example output: -cpu: 3, pid: 31463, tid: 31593 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31593, switch_out: 1 } -cpu: 1, pid: 31463, tid: 31489 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31489, switch_out: 1 } -cpu: 2, pid: 31463, tid: 31496 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31496, switch_out: 1 } -cpu: 3, pid: 31463, tid: 31491 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31491, switch_out: 0 } + # cpu: 3, pid: 31463, tid: 31593 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31593, switch_out: 1 } + # cpu: 1, pid: 31463, tid: 31489 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31489, switch_out: 1 } + # cpu: 2, pid: 31463, tid: 31496 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31496, switch_out: 1 } + # cpu: 3, pid: 31463, tid: 31491 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31491, switch_out: 0 } + # + # It is possible as well to use event.misc & perf.PERF_RECORD_MISC_SWITCH_OUT + # to figure out if this is a context switch in or out of the monitored threads. - It is possible as well to use event.misc & perf.PERF_RECORD_MISC_SWITCH_OUT - to figure out if this is a context switch in or out of the monitored threads. - - If bored, please add command line option parsing support for these options :-) - """ - # main(context_switch = 1, thread = 31463) + # If bored, please add command line option parsing support for these options :-) + # # main(context_switch = 1, thread = 31463) main() From 4b15226c14df50c8c9ca67e35295bf8c7ae71574 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:34 -0700 Subject: [PATCH 182/425] perf python: Improve perf script -l descriptions Add module docstrings to counting.py, tracepoint.py, and twatch.py. This satisfies pylint's missing-module-docstring check and prepares these existing standalone scripts in tools/perf/python/ for the later commit in this series ("perf script: Support standalone scripts and remove embedded scripting"), which updates 'perf script -l' to scan tools/perf/python/ and display each script's module docstring: ``` $ perf script -l List of available scripts: ... counting Example for counting perf events. ... tracepoint Example showing how to enable a tracepoint and access its fields. twatch Example to show how to enable a software event and track context switches. ... ``` Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/counting.py | 1 + tools/perf/python/tracepoint.py | 1 + tools/perf/python/twatch.py | 1 + 3 files changed, 3 insertions(+) diff --git a/tools/perf/python/counting.py b/tools/perf/python/counting.py index 02121d2bb11d..9adbbeccdacd 100755 --- a/tools/perf/python/counting.py +++ b/tools/perf/python/counting.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 +"""Example for counting perf events.""" # -*- python -*- # -*- coding: utf-8 -*- diff --git a/tools/perf/python/tracepoint.py b/tools/perf/python/tracepoint.py index 9d49a25b3b1e..faa5311b8418 100755 --- a/tools/perf/python/tracepoint.py +++ b/tools/perf/python/tracepoint.py @@ -1,5 +1,6 @@ #! /usr/bin/env python # SPDX-License-Identifier: GPL-2.0 +"""Example showing how to enable a tracepoint and access its fields.""" # -*- python -*- # -*- coding: utf-8 -*- diff --git a/tools/perf/python/twatch.py b/tools/perf/python/twatch.py index ad35b1620781..b0f1bf113162 100755 --- a/tools/perf/python/twatch.py +++ b/tools/perf/python/twatch.py @@ -1,5 +1,6 @@ #! /usr/bin/env python # SPDX-License-Identifier: GPL-2.0-only +"""Example to show how to enable a software event and track context switches.""" # -*- python -*- # -*- coding: utf-8 -*- # twatch - Experimental use of the perf python interface From 7a481f6c712a1505f67b97add508bb9d393c9d22 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:35 -0700 Subject: [PATCH 183/425] perf python: Expose addr location, transaction, and context_switch Expose destination address location components (addr_dso, addr_symbol, addr_sym_offset), branch/transaction metrics (branch_type, in_tx, flags, transaction), machine_pid, vcpu, sym_offset, and context_switch callbacks directly to the Python extension. Also include supporting fixes and infrastructure updates in util/python.c and perf.pyi: - Support fetching sym_offset on resolved sample symbols. - Populate sample machine_pid and vcpu from evlist__id2sid() when perf_guest is enabled, and resolve samples against the matching guest or host machine. - Expose next_prev_pid and next_prev_tid on switch_event only for PERF_RECORD_SWITCH_CPU_WIDE records (returning None otherwise). - Wire up tracing_data in pyrf_session__new() and update perf.pyi type stubs (including srccode() and insn() return types). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/perf.pyi | 23 ++- tools/perf/util/python.c | 289 ++++++++++++++++++++++++++++++++++--- 2 files changed, 288 insertions(+), 24 deletions(-) diff --git a/tools/perf/python/perf.pyi b/tools/perf/python/perf.pyi index 92aad84ca542..44c693761838 100644 --- a/tools/perf/python/perf.pyi +++ b/tools/perf/python/perf.pyi @@ -194,6 +194,8 @@ class _sample_members: sample_time: int sample_id: int sample_stream_id: int + machine_pid: int + vcpu: int sample_period: int sample_cpu: int @@ -218,10 +220,18 @@ class sample_event(_sample_members): symbol: str sym_start: int sym_end: int + sym_offset: Optional[int] + addr_dso: Optional[str] + addr_symbol: Optional[str] + addr_sym_offset: Optional[int] + branch_type: int + in_tx: int + flags: int + transaction: int brstack: Optional['branch_stack'] callchain: Optional['callchain'] - def srccode(self) -> str: ... - def insn(self) -> str: ... + def srccode(self) -> Optional[tuple[Optional[str], int, Optional[str]]]: ... + def insn(self) -> Optional[bytes]: ... def __getattr__(self, name: str) -> Any: ... class mmap_event(_sample_members): @@ -298,8 +308,9 @@ class read_event(_sample_members): class switch_event(_sample_members): """Represents a SWITCH or SWITCH_CPU_WIDE record.""" type: int - next_prev_pid: int - next_prev_tid: int + misc: int + next_prev_pid: Optional[int] + next_prev_tid: Optional[int] evsel: Optional['evsel'] class branch_entry: @@ -334,6 +345,7 @@ class callchain_node: ip: int symbol: str dso: str + sym_offset: Optional[int] class callchain: """Sequence of callchain frames.""" @@ -441,7 +453,8 @@ class session: self, data: data, sample: Optional[Callable[[sample_event], None]] = None, - stat: Optional[Callable[[Any, Optional[str]], None]] = None + stat: Optional[Callable[[Any, Optional[str]], None]] = None, + context_switch: Optional[Callable[[switch_event], None]] = None, ) -> None: """Initialize a perf session. diff --git a/tools/perf/util/python.c b/tools/perf/util/python.c index 40b327801b7d..526d0e197230 100644 --- a/tools/perf/util/python.c +++ b/tools/perf/util/python.c @@ -2,9 +2,11 @@ #define PY_SSIZE_T_CLEAN #include +#include #include #include #include +#include #include #include @@ -91,6 +93,8 @@ struct pyrf_event { PyObject_HEAD /** @sample: The parsed sample from the event. */ struct perf_sample sample; + /** @machine: The machine associated with the sample/event. */ + struct machine *machine; /** @al: The address location from machine__resolve, lazily computed. */ struct addr_location al; /** @al_resolved: True when machine__resolve been called. */ @@ -109,6 +113,8 @@ struct pyrf_event { sample_member_def(sample_time, time, T_ULONGLONG, "event timestamp"), \ sample_member_def(sample_id, id, T_ULONGLONG, "event id"), \ sample_member_def(sample_stream_id, stream_id, T_ULONGLONG, "event stream id"), \ + sample_member_def(machine_pid, machine_pid, T_UINT, "event machine pid"), \ + sample_member_def(vcpu, vcpu, T_UINT, "event vcpu"), \ sample_member_def(sample_period, period, T_ULONGLONG, "event period"), \ sample_member_def(sample_cpu, cpu, T_UINT, "event cpu"), @@ -584,6 +590,7 @@ static PyMemberDef pyrf_sample_event__members[] = { sample_member_def(sample_data_src, data_src, T_ULONGLONG, "event data source"), sample_member_def(sample_insn_count, insn_cnt, T_ULONGLONG, "event instruction count"), sample_member_def(sample_cyc_count, cyc_cnt, T_ULONGLONG, "event cycle count"), + sample_member_def(flags, flags, T_UINT, "event flags"), member_def(perf_event_header, type, T_UINT, "event type"), { .name = NULL, }, }; @@ -682,6 +689,7 @@ static int pyrf_sample_event__resolve_al(struct pyrf_event *pevent) struct evsel *evsel = pevent->sample.evsel; struct evlist *evlist = evsel ? evsel->evlist : NULL; struct perf_session *session = evlist ? evlist__session(evlist) : NULL; + struct machine *machine; if (pevent->al_resolved) return 0; @@ -689,8 +697,17 @@ static int pyrf_sample_event__resolve_al(struct pyrf_event *pevent) if (!session) return -1; + /* + * Use pevent->machine, which pyrf_event__new() initializes either from + * the machine resolved by perf_session (machines__find_for_cpumode(), + * preserving DEFAULT_GUEST_KERNEL_ID == 0 for default guests) or via + * sample.machine_pid when pyrf_event__new() is called with a NULL + * machine. + */ + machine = pevent->machine ? pevent->machine : &session->machines.host; + addr_location__init(&pevent->al); - if (machine__resolve(&session->machines.host, &pevent->al, &pevent->sample) < 0) { + if (machine__resolve(machine, &pevent->al, &pevent->sample) < 0) { addr_location__exit(&pevent->al); return -1; } @@ -774,6 +791,15 @@ static PyObject *pyrf_sample_event__get_sym_start(struct pyrf_event *pevent, return PyLong_FromUnsignedLongLong(pevent->al.sym->start); } +static PyObject *pyrf_sample_event__get_sym_offset(struct pyrf_event *pevent, + void *closure __maybe_unused) +{ + if (pyrf_sample_event__resolve_al(pevent) < 0 || !pevent->al.sym) + Py_RETURN_NONE; + + return PyLong_FromUnsignedLongLong(pevent->al.addr - pevent->al.sym->start); +} + static PyObject *pyrf_sample_event__get_sym_end(struct pyrf_event *pevent, void *closure __maybe_unused) { @@ -915,10 +941,23 @@ static PyObject *pyrf_callchain_node__get_dso(struct pyrf_callchain_node *pnode, return PyUnicode_FromString(dsoname); } +static PyObject *pyrf_callchain_node__get_sym_offset(struct pyrf_callchain_node *pnode, + void *closure __maybe_unused) +{ + u64 addr; + + if (!pnode->sym) + Py_RETURN_NONE; + + addr = pnode->map ? map__map_ip(pnode->map, pnode->ip) : pnode->ip; + return PyLong_FromUnsignedLongLong(addr - pnode->sym->start); +} + static PyGetSetDef pyrf_callchain_node__getset[] = { - { .name = "ip", .get = (getter)pyrf_callchain_node__get_ip, }, - { .name = "symbol", .get = (getter)pyrf_callchain_node__get_symbol, }, - { .name = "dso", .get = (getter)pyrf_callchain_node__get_dso, }, + { .name = "ip", .get = (getter)pyrf_callchain_node__get_ip, }, + { .name = "symbol", .get = (getter)pyrf_callchain_node__get_symbol, }, + { .name = "dso", .get = (getter)pyrf_callchain_node__get_dso, }, + { .name = "sym_offset", .get = (getter)pyrf_callchain_node__get_sym_offset, }, { .name = NULL, }, }; @@ -1169,7 +1208,114 @@ pyrf_sample_event__getattro(struct pyrf_event *pevent, PyObject *attr_name) return obj ?: PyObject_GenericGetAttr((PyObject *) pevent, attr_name); } +static int pyrf_sample_event__resolve_addr_al(struct pyrf_event *pevent, + struct addr_location *addr_al) +{ + addr_location__init(addr_al); + if (pyrf_sample_event__resolve_al(pevent) < 0 || !pevent->al.thread) + return -1; + + thread__find_symbol_fb(pevent->al.thread, pevent->sample.cpumode, + pevent->sample.addr, addr_al); + return 0; +} + +static PyObject *pyrf_sample_event__get_addr_dso(struct pyrf_event *pevent, + void *closure __maybe_unused) +{ + struct addr_location addr_al; + PyObject *ret = Py_None; + + if (pyrf_sample_event__resolve_addr_al(pevent, &addr_al) == 0 && addr_al.map) + ret = PyUnicode_FromString(dso__name(map__dso(addr_al.map))); + else + Py_INCREF(Py_None); + + addr_location__exit(&addr_al); + return ret; +} + +static PyObject *pyrf_sample_event__get_addr_symbol(struct pyrf_event *pevent, + void *closure __maybe_unused) +{ + struct addr_location addr_al; + PyObject *ret = Py_None; + + if (pyrf_sample_event__resolve_addr_al(pevent, &addr_al) == 0 && addr_al.sym) + ret = PyUnicode_FromString(addr_al.sym->name); + else + Py_INCREF(Py_None); + + addr_location__exit(&addr_al); + return ret; +} + +static PyObject *pyrf_sample_event__get_addr_sym_offset(struct pyrf_event *pevent, + void *closure __maybe_unused) +{ + struct addr_location addr_al; + PyObject *ret = Py_None; + + if (pyrf_sample_event__resolve_addr_al(pevent, &addr_al) == 0 && addr_al.sym) + ret = PyLong_FromUnsignedLongLong(addr_al.addr - addr_al.sym->start); + else + Py_INCREF(Py_None); + + addr_location__exit(&addr_al); + return ret; +} + +static PyObject *pyrf_sample_event__get_branch_type(struct pyrf_event *pevent, + void *closure __maybe_unused) +{ + return PyLong_FromUnsignedLong(pevent->sample.flags & PERF_BRANCH_MASK); +} + +static PyObject *pyrf_sample_event__get_in_tx(struct pyrf_event *pevent, + void *closure __maybe_unused) +{ + return PyLong_FromUnsignedLong(!!(pevent->sample.flags & PERF_IP_FLAG_IN_TX)); +} + +static PyObject *pyrf_sample_event__get_transaction(struct pyrf_event *pevent, + void *closure __maybe_unused) +{ + return PyLong_FromUnsignedLongLong(pevent->sample.transaction); +} + static PyGetSetDef pyrf_sample_event__getset[] = { + + { + .name = "addr_dso", + .get = (getter)pyrf_sample_event__get_addr_dso, + .doc = "event destination dso.", + }, + { + .name = "addr_symbol", + .get = (getter)pyrf_sample_event__get_addr_symbol, + .doc = "event destination symbol.", + }, + { + .name = "addr_sym_offset", + .get = (getter)pyrf_sample_event__get_addr_sym_offset, + .doc = "event destination symbol offset.", + }, + { + .name = "branch_type", + .get = (getter)pyrf_sample_event__get_branch_type, + .doc = "branch type.", + }, + { + .name = "in_tx", + .get = (getter)pyrf_sample_event__get_in_tx, + .doc = "in transaction flag.", + }, + { + .name = "transaction", + .get = (getter)pyrf_sample_event__get_transaction, + .doc = "transaction execution.", + }, + { .name = "callchain", .get = pyrf_sample_event__get_callchain, @@ -1230,6 +1376,12 @@ static PyGetSetDef pyrf_sample_event__getset[] = { .set = NULL, .doc = "event map page offset.", }, + { + .name = "sym_offset", + .get = (getter)pyrf_sample_event__get_sym_offset, + .set = NULL, + .doc = "event symbol offset.", + }, { .name = "symbol", .get = (getter)pyrf_sample_event__get_symbol, @@ -1286,8 +1438,45 @@ static const char pyrf_context_switch_event__doc[] = PyDoc_STR("perf context_swi static PyMemberDef pyrf_context_switch_event__members[] = { sample_members member_def(perf_event_header, type, T_UINT, "event type"), - member_def(perf_record_switch, next_prev_pid, T_UINT, "next/prev pid"), - member_def(perf_record_switch, next_prev_tid, T_UINT, "next/prev tid"), + member_def(perf_event_header, misc, T_USHORT, "event misc"), + { .name = NULL, }, +}; + +static PyObject *pyrf_context_switch_event__get_next_prev_pid(const struct pyrf_event *pevent, + void *closure __maybe_unused) +{ + if (pevent->event.header.type == PERF_RECORD_SWITCH_CPU_WIDE) + return PyLong_FromUnsignedLong(pevent->event.context_switch.next_prev_pid); + Py_RETURN_NONE; +} + +static PyObject *pyrf_context_switch_event__get_next_prev_tid(const struct pyrf_event *pevent, + void *closure __maybe_unused) +{ + if (pevent->event.header.type == PERF_RECORD_SWITCH_CPU_WIDE) + return PyLong_FromUnsignedLong(pevent->event.context_switch.next_prev_tid); + Py_RETURN_NONE; +} + +static PyGetSetDef pyrf_context_switch_event__getset[] = { + { + .name = "evsel", + .get = pyrf_event__get_evsel, + .set = NULL, + .doc = "tracking event.", + }, + { + .name = "next_prev_pid", + .get = (getter)pyrf_context_switch_event__get_next_prev_pid, + .set = NULL, + .doc = "next/prev pid for CPU-wide switch, or None.", + }, + { + .name = "next_prev_tid", + .get = (getter)pyrf_context_switch_event__get_next_prev_tid, + .set = NULL, + .doc = "next/prev tid for CPU-wide switch, or None.", + }, { .name = NULL, }, }; @@ -1295,11 +1484,19 @@ static PyObject *pyrf_context_switch_event__repr(const struct pyrf_event *pevent { PyObject *ret; char *s; + int res; - if (asprintf(&s, "{ type: context_switch, next_prev_pid: %u, next_prev_tid: %u, switch_out: %u }", - pevent->event.context_switch.next_prev_pid, - pevent->event.context_switch.next_prev_tid, - !!(pevent->event.header.misc & PERF_RECORD_MISC_SWITCH_OUT)) < 0) { + if (pevent->event.header.type == PERF_RECORD_SWITCH_CPU_WIDE) { + res = asprintf(&s, + "{ type: context_switch, next_prev_pid: %u, next_prev_tid: %u, switch_out: %u }", + pevent->event.context_switch.next_prev_pid, + pevent->event.context_switch.next_prev_tid, + !!(pevent->event.header.misc & PERF_RECORD_MISC_SWITCH_OUT)); + } else { + res = asprintf(&s, "{ type: context_switch, switch_out: %u }", + !!(pevent->event.header.misc & PERF_RECORD_MISC_SWITCH_OUT)); + } + if (res < 0) { ret = PyErr_NoMemory(); } else { ret = PyUnicode_FromString(s); @@ -1316,7 +1513,7 @@ static PyTypeObject pyrf_context_switch_event__type = { .tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, .tp_doc = pyrf_context_switch_event__doc, .tp_members = pyrf_context_switch_event__members, - .tp_getset = pyrf_event__getset, + .tp_getset = pyrf_context_switch_event__getset, .tp_repr = (reprfunc)pyrf_context_switch_event__repr, }; @@ -1400,9 +1597,6 @@ static PyObject *pyrf_event__new(const union perf_event *event, struct evsel *ev u32 min_size; bool needs_swap; - if (!machine) - machine = session ? &session->machines.host : NULL; - if (event->header.type >= ARRAY_SIZE(pyrf_event__type) || pyrf_event__type[event->header.type] == NULL) { return PyErr_Format(PyExc_TypeError, "Unexpected header type %u", @@ -1434,13 +1628,17 @@ static PyObject *pyrf_event__new(const union perf_event *event, struct evsel *ev pevent->event.mmap2.filename[sizeof(pevent->event.mmap2.filename) - 1] = '\0'; perf_sample__init(&pevent->sample, /*all=*/true); + pevent->machine = machine; pevent->callchain = NULL; pevent->brstack = NULL; pevent->al_resolved = false; addr_location__init(&pevent->al); - if (!evsel) + if (!evsel) { + if (!pevent->machine && session) + pevent->machine = &session->machines.host; return (PyObject *)pevent; + } /* Parse the sample again so that pointers are within the copied event. */ needs_swap = evsel->needs_swap; @@ -1453,6 +1651,24 @@ static PyObject *pyrf_event__new(const union perf_event *event, struct evsel *ev return PyErr_Format(PyExc_OSError, "perf: can't parse sample, err=%d", err); } + if (session && session->evlist && perf_guest && pevent->sample.id) { + struct perf_sample_id *sid = evlist__id2sid(session->evlist, pevent->sample.id); + + if (sid) { + pevent->sample.machine_pid = sid->machine_pid; + pevent->sample.vcpu = sid->vcpu.cpu; + } + } + if (!machine && session) { + machine = pevent->sample.machine_pid ? + machines__find(&session->machines, pevent->sample.machine_pid) : + &session->machines.host; + if (!machine) + machine = &session->machines.host; + } + pevent->machine = machine; + if (machine && machine->pid > 0 && !pevent->sample.machine_pid) + pevent->sample.machine_pid = machine->pid; sample = &pevent->sample; if (machine && sample->callchain) { struct addr_location al; @@ -3840,6 +4056,7 @@ struct pyrf_session { struct pyrf_data *pdata; PyObject *sample; PyObject *stat; + PyObject *context_switch; }; static int pyrf_session_tool__sample(const struct perf_tool *tool, @@ -3864,6 +4081,33 @@ 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) +{ + struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool); + PyObject *pyevent = pyrf_event__new(event, sample->evsel, psession->session, machine); + PyObject *ret; + + if (perf_event__process_switch(tool, event, sample, machine) < 0) { + Py_XDECREF(pyevent); + return -1; + } + + if (pyevent == NULL) + return -ENOMEM; + + ret = PyObject_CallFunction(psession->context_switch, "O", pyevent); + if (!ret) { + Py_DECREF(pyevent); + return -1; + } + Py_DECREF(ret); + Py_DECREF(pyevent); + return 0; +} + static int pyrf_session_tool__stat(const struct perf_tool *tool, struct perf_session *session, union perf_event *event) @@ -3946,13 +4190,13 @@ static PyObject *pyrf_session__find_thread(struct pyrf_session *psession, PyObje static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { struct pyrf_data *pdata; - PyObject *sample = NULL, *stat = NULL; - static char *kwlist[] = { "data", "sample", "stat", NULL }; + PyObject *sample = NULL, *stat = NULL, *context_switch = NULL; + static char *kwlist[] = { "data", "sample", "stat", "context_switch", NULL }; struct pyrf_session *psession; struct perf_session *session; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!|OO", kwlist, &pyrf_data__type, &pdata, - &sample, &stat)) + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!|OOO", kwlist, &pyrf_data__type, &pdata, + &sample, &stat, &context_switch)) return NULL; psession = PyObject_New(struct pyrf_session, type); @@ -3962,6 +4206,7 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject psession->session = NULL; psession->sample = NULL; psession->stat = NULL; + psession->context_switch = NULL; psession->pdata = NULL; Py_INCREF(pdata); @@ -3985,6 +4230,7 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject ADD_TOOL(sample); ADD_TOOL(stat); + ADD_TOOL(context_switch); #undef ADD_TOOL if (stat) @@ -4002,7 +4248,11 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject 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.event_update = perf_event__process_event_update; psession->tool.feature = perf_event__process_feature; +#ifdef HAVE_LIBTRACEEVENT + psession->tool.tracing_data = perf_event__process_tracing_data; +#endif session = perf_session__new(&pdata->data, &psession->tool); if (IS_ERR(session)) { @@ -4033,6 +4283,7 @@ static void pyrf_session__delete(struct pyrf_session *psession) Py_XDECREF(psession->pdata); Py_XDECREF(psession->sample); Py_XDECREF(psession->stat); + Py_XDECREF(psession->context_switch); Py_TYPE(psession)->tp_free((PyObject *)psession); } From f66944587d18a84504d9c9782295944fbd29036f Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:36 -0700 Subject: [PATCH 184/425] perf python: Add Intel PT call_return and itrace capability Add support for Intel PT call_return events and itrace options to the perf Python extension so standalone Python scripts can decode and inspect low-level instruction traces. Specifically: - Add a call_return callback parameter and itrace option string to perf.session, treating explicit None (Py_None) callbacks as omitted. - Expose the perf.call_return object with db_id, parent_id, insn_count, cyc_count, comm, pid, tid, call_time, return_time, branch_count, call_ref, return_ref, flags, and call_path (resolved as a perf.callchain ordered from outermost caller to callee, populating frame->map and frame->sym so call_path entries expose resolved symbol and DSO names). - Export perf.CALL_RETURN_NO_CALL, perf.CALL_RETURN_NO_RETURN, and perf.CALL_RETURN_NON_CALL flag constants. - Expose session.e_machine and session.is_64_bit properties, preferring recorded thread ELF machines (perf_session__e_machine) over perf_env so pipe-mode traces do not prematurely cache host bitness. - Support optional vmlinux, kallsyms, and symfs symbol configuration parameters (and PERF_SYMBOL_VMLINUX, PERF_SYMBOL_KALLSYMS, PERF_SYMBOL_SYMFS environment fallbacks) in perf.session, resetting symbol_conf pointers and freeing vm_tm_corr_args, cpu_bitmap, and ptime_range in pyrf_session__delete(). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/perf.pyi | 78 ++++- tools/perf/util/python.c | 700 ++++++++++++++++++++++++++++++++++--- 2 files changed, 712 insertions(+), 66 deletions(-) diff --git a/tools/perf/python/perf.pyi b/tools/perf/python/perf.pyi index 44c693761838..087da00728fd 100644 --- a/tools/perf/python/perf.pyi +++ b/tools/perf/python/perf.pyi @@ -96,7 +96,7 @@ class data: class thread: """Represents a thread in the system.""" - def comm(self) -> str: + def comm(self) -> Optional[str]: """Get the command name of the thread.""" ... pid: int @@ -165,7 +165,13 @@ class evsel: def __str__(self) -> str: """Return string representation of the event.""" ... - def open(self) -> None: + def open( + self, + cpus: Optional['cpu_map'] = None, + threads: Optional[thread_map] = None, + group: bool = False, + inherit: bool = False, + ) -> None: """Open the event selector file descriptor table.""" ... def read(self, cpu: int, thread: int) -> counts_values: @@ -211,15 +217,15 @@ class sample_event(_sample_members): sample_cyc_count: int type: int raw_buf: bytes - dso: str - dso_long_name: str - dso_bid: Optional[bytes] - map_start: int - map_end: int - map_pgoff: int - symbol: str - sym_start: int - sym_end: int + dso: Optional[str] + dso_long_name: Optional[str] + dso_bid: Optional[str] + map_start: Optional[int] + map_end: Optional[int] + map_pgoff: Optional[int] + symbol: Optional[str] + sym_start: Optional[int] + sym_end: Optional[int] sym_offset: Optional[int] addr_dso: Optional[str] addr_symbol: Optional[str] @@ -230,7 +236,7 @@ class sample_event(_sample_members): transaction: int brstack: Optional['branch_stack'] callchain: Optional['callchain'] - def srccode(self) -> Optional[tuple[Optional[str], int, Optional[str]]]: ... + def srccode(self, addr: int = ..., /) -> Optional[tuple[Optional[str], int, Optional[str]]]: ... def insn(self) -> Optional[bytes]: ... def __getattr__(self, name: str) -> Any: ... @@ -339,6 +345,7 @@ class branch_stack: """Sequence of branch entries in the branch stack.""" def __len__(self) -> int: ... def __getitem__(self, index: int) -> branch_entry: ... + def __iter__(self) -> Iterator[branch_entry]: ... class callchain_node: """Represents a frame in the callchain.""" @@ -387,10 +394,10 @@ class evlist: def close(self) -> None: """Close the events in the list.""" ... - def mmap(self) -> None: + def mmap(self, pages: int = 128, overwrite: bool = False) -> None: """Memory map the event buffers.""" ... - def poll(self, timeout: int) -> int: + def poll(self, timeout: int = -1) -> int: """Poll for events. Args: @@ -400,7 +407,7 @@ class evlist: Number of events ready. """ ... - def read_on_cpu(self, cpu: int) -> Optional[Any]: + def read_on_cpu(self, cpu: int, sample_id_all: bool = True) -> Optional[Any]: """Read a sample event from a specific CPU. Args: @@ -437,17 +444,38 @@ class evlist: def enable(self) -> None: """Enable all events in the list.""" ... - def get_pollfd(self) -> List[int]: + def get_pollfd(self) -> List[Any]: """Get a list of file descriptors for polling.""" ... - def add(self, evsel: evsel) -> int: + def add(self, evsel: evsel, /) -> int: """Add an event to the list.""" ... + def __len__(self) -> int: ... + def __getitem__(self, index: int) -> evsel: ... def __iter__(self) -> Iterator[evsel]: """Iterate over the events (evsel) in the list.""" ... +class call_return: + """Represents a call/return event from instruction trace decoding.""" + db_id: int + parent_id: int + insn_count: int + cyc_count: int + comm: Optional[str] + machine_pid: int + pid: Optional[int] + tid: Optional[int] + call_time: int + return_time: int + branch_count: int + call_ref: int + return_ref: int + flags: int + call_path: Optional[callchain] + + class session: def __init__( self, @@ -455,6 +483,11 @@ class session: sample: Optional[Callable[[sample_event], None]] = None, stat: Optional[Callable[[Any, Optional[str]], None]] = None, context_switch: Optional[Callable[[switch_event], None]] = None, + call_return: Optional[Callable[[call_return], None]] = None, + itrace: Optional[str] = None, + vmlinux: Optional[str] = None, + kallsyms: Optional[str] = None, + symfs: Optional[str] = None ) -> None: """Initialize a perf session. @@ -464,6 +497,8 @@ class session: stat: Callback for stat events. """ ... + e_machine: int + is_64_bit: bool def process_events(self) -> None: """Process all events in the session.""" ... @@ -696,3 +731,12 @@ RECORD_STAT_ROUND: int RECORD_MISC_SWITCH_OUT: int """MISC_SWITCH_OUT record.""" + +CALL_RETURN_NO_CALL: int +"""'return' but no matching 'call'.""" + +CALL_RETURN_NO_RETURN: int +"""'call' but no matching 'return'.""" + +CALL_RETURN_NON_CALL: int +"""A branch but not a 'call' to the start of a different symbol.""" diff --git a/tools/perf/util/python.c b/tools/perf/util/python.c index 526d0e197230..690a94fb4f62 100644 --- a/tools/perf/util/python.c +++ b/tools/perf/util/python.c @@ -2,15 +2,18 @@ #define PY_SSIZE_T_CLEAN #include +#include #include #include #include #include #include #include +#include #include #include +#include #include #include @@ -26,6 +29,9 @@ #include "counts.h" #include "data.h" #include "debug.h" +#include "call-path.h" +#include "db-export.h" +#include "thread-stack.h" #include "dso.h" #include "dwarf-regs.h" #include "event.h" @@ -33,6 +39,7 @@ #include "branch.h" #include "evlist.h" #include "evsel.h" +#include "symbol.h" #include "expr.h" #include "map.h" #include "metricgroup.h" @@ -830,6 +837,12 @@ static PyObject *pyrf_sample_event__srccode(PyObject *self, PyObject *args) PyObject *result; struct addr_location al; + /* + * addr defaults to pevent->sample.ip when omitted, and when + * addr != pevent->sample.ip a local addr_location is resolved so + * callers can inspect callchain/branch addresses without mutating + * the sample's cached pevent->al. + */ if (!PyArg_ParseTuple(args, "|K", &addr)) return NULL; @@ -858,6 +871,12 @@ static PyObject *pyrf_sample_event__srccode(PyObject *self, PyObject *args) addr_location__exit(&al); if (srcfile) { + /* + * PyUnicode_FromStringAndSize(line, len) (via Py_BuildValue "s#") + * must copy the source line before free_srccode() frees the + * buffer, and because the line returned by find_sourceline() is + * bounded by len rather than NUL-terminated. + */ srccode = find_sourceline(srcfile, line, &len); result = Py_BuildValue("(sIs#)", srcfile, line, srccode, (Py_ssize_t)len); free(srcfile); @@ -901,11 +920,13 @@ struct pyrf_callchain_node { u64 ip; struct map *map; struct symbol *sym; + char *sym_name; }; static void pyrf_callchain_node__delete(struct pyrf_callchain_node *pnode) { map__put(pnode->map); + free(pnode->sym_name); Py_TYPE(pnode)->tp_free((PyObject *)pnode); } @@ -918,6 +939,8 @@ static PyObject *pyrf_callchain_node__get_ip(struct pyrf_callchain_node *pnode, static PyObject *pyrf_callchain_node__get_symbol(struct pyrf_callchain_node *pnode, void *closure __maybe_unused) { + if (pnode->sym_name) + return PyUnicode_FromString(pnode->sym_name); if (pnode->sym) return PyUnicode_FromString(pnode->sym->name); return PyUnicode_FromString("[unknown]"); @@ -975,6 +998,7 @@ struct pyrf_callchain_frame { u64 ip; struct map *map; struct symbol *sym; + char *sym_name; }; struct pyrf_callchain { @@ -986,8 +1010,10 @@ struct pyrf_callchain { static void pyrf_callchain__delete(struct pyrf_callchain *pchain) { if (pchain->frames) { - for (u64 i = 0; i < pchain->nr_frames; i++) + for (u64 i = 0; i < pchain->nr_frames; i++) { map__put(pchain->frames[i].map); + free(pchain->frames[i].sym_name); + } free(pchain->frames); } Py_TYPE(pchain)->tp_free((PyObject *)pchain); @@ -1017,6 +1043,7 @@ static PyObject *pyrf_callchain__item(PyObject *obj, Py_ssize_t i) pnode->ip = pchain->frames[i].ip; pnode->map = map__get(pchain->frames[i].map); pnode->sym = pchain->frames[i].sym; + pnode->sym_name = pchain->frames[i].sym_name ? strdup(pchain->frames[i].sym_name) : NULL; return (PyObject *)pnode; } @@ -1588,8 +1615,8 @@ static PyTypeObject *pyrf_event__type[] = { }; static PyObject *pyrf_event__new(const union perf_event *event, struct evsel *evsel, - struct perf_session *session, - struct machine *machine) + struct perf_session *session, struct machine *machine, + struct perf_sample *sample_arg) { struct pyrf_event *pevent; struct perf_sample *sample; @@ -1647,9 +1674,26 @@ static PyObject *pyrf_event__new(const union perf_event *event, struct evsel *ev err = evsel__parse_sample(evsel, &pevent->event, &pevent->sample); evsel->needs_swap = needs_swap; if (err < 0) { - Py_DECREF(pevent); - return PyErr_Format(PyExc_OSError, - "perf: can't parse sample, err=%d", err); + /* + * Synthesized events (e.g. Intel PT itrace) may not have raw sample + * buffers for evsel__parse_sample(); use the pre-parsed sample_arg. + */ + if (sample_arg) { + perf_sample__exit(&pevent->sample); + pevent->sample = *sample_arg; + if (pevent->sample.evsel) + pevent->sample.evsel = evsel__get(pevent->sample.evsel); + pevent->sample.merged_callchain = false; + + pevent->sample.user_regs = NULL; + pevent->sample.intr_regs = NULL; + pevent->sample.raw_data = NULL; + pevent->sample.raw_size = 0; + } else { + Py_DECREF(pevent); + return PyErr_Format(PyExc_OSError, + "perf: can't parse sample, err=%d", err); + } } if (session && session->evlist && perf_guest && pevent->sample.id) { struct perf_sample_id *sid = evlist__id2sid(session->evlist, pevent->sample.id); @@ -2014,7 +2058,6 @@ static int pyrf_pmu__setup_types(void) return PyType_Ready(&pyrf_pmu__type); } - /** A python iterator for pmus that has no equivalent in the C code. */ struct pyrf_pmu_iterator { PyObject_HEAD @@ -2425,12 +2468,13 @@ static PyObject *pyrf_evsel__read(struct pyrf_evsel *pevsel, { struct evsel *evsel = pevsel->evsel; int cpu = 0, cpu_idx, thread = 0, thread_idx; + static char *kwlist[] = { "cpu", "thread", NULL }; struct perf_counts_values *old_count, *new_count; struct pyrf_counts_values *count_values; CHECK_INITIALIZED(evsel, "evsel"); - if (!PyArg_ParseTuple(args, "ii", &cpu, &thread)) + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii", kwlist, &cpu, &thread)) return NULL; cpu_idx = perf_cpu_map__idx(evsel->core.cpus, (struct perf_cpu){.cpu = cpu}); @@ -2955,10 +2999,6 @@ static int prepare_metric(const struct metric_expr *mexp, double val, ena, run; int ret, source_count = 0; struct perf_counts_values *old_count, *new_count; - char *n = strdup(evsel__metric_id(cur)); - - if (!n) - return -ENOMEM; /* * If there are multiple uncore PMUs and we're not reading the @@ -3000,9 +3040,15 @@ static int prepare_metric(const struct metric_expr *mexp, if (ena != run && run != 0) val = val * ena / run; + char *n = strdup(evsel__metric_id(cur)); + + if (!n) + return -ENOMEM; ret = expr__add_id_val_source_count(pctx, n, val, source_count); - if (ret) + if (ret) { + free(n); return ret; + } } for (int i = 0; metric_refs && metric_refs[i].metric_name; i++) { @@ -3020,6 +3066,7 @@ static PyObject *pyrf_evlist__compute_metric(struct pyrf_evlist *pevlist, { int ret, cpu = 0, cpu_idx = 0, thread = 0, thread_idx = 0; const char *metric; + static char *kwlist[] = { "metric", "cpu", "thread", NULL }; struct rb_node *node; struct metric_expr *mexp = NULL; struct expr_parse_ctx *pctx; @@ -3028,7 +3075,7 @@ static PyObject *pyrf_evlist__compute_metric(struct pyrf_evlist *pevlist, CHECK_INITIALIZED(pevlist->evlist, "evlist"); - if (!PyArg_ParseTuple(args, "sii", &metric, &cpu, &thread)) + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sii", kwlist, &metric, &cpu, &thread)) return NULL; for (node = rb_first_cached(&evlist__metric_events(pevlist->evlist)->entries); @@ -3170,10 +3217,7 @@ static PyObject *pyrf_evlist__get_pollfd(struct pyrf_evlist *pevlist, return PyErr_NoMemory(); } - -static PyObject *pyrf_evlist__add(struct pyrf_evlist *pevlist, - PyObject *args, - PyObject *kwargs __maybe_unused) +static PyObject *pyrf_evlist__add(struct pyrf_evlist *pevlist, PyObject *args) { struct evlist *evlist; PyObject *pevsel; @@ -3251,7 +3295,7 @@ static PyObject *pyrf_evlist__read_on_cpu(struct pyrf_evlist *pevlist, perf_mmap__consume(&md->core); Py_RETURN_NONE; } - pyevent = pyrf_event__new(event, evsel, evlist__session(evlist), /*machine=*/NULL); + pyevent = pyrf_event__new(event, evsel, evlist__session(evlist), /*machine=*/NULL, NULL); perf_mmap__consume(&md->core); if (pyevent == NULL) return PyErr_Occurred() ? NULL : PyErr_NoMemory(); @@ -3385,7 +3429,7 @@ static PyMethodDef pyrf_evlist__methods[] = { { .ml_name = "add", .ml_meth = (PyCFunction)pyrf_evlist__add, - .ml_flags = METH_VARARGS | METH_KEYWORDS, + .ml_flags = METH_VARARGS, .ml_doc = PyDoc_STR("adds an event selector to the list.") }, { @@ -3619,6 +3663,11 @@ static const struct perf_constant perf__constants[] = { PERF_CONST(RECORD_STAT_ROUND), PERF_CONST(RECORD_MISC_SWITCH_OUT), + + /* Values for the flags of a perf.call_return, see thread-stack.h. */ + { "CALL_RETURN_NO_CALL", CALL_RETURN_NO_CALL }, + { "CALL_RETURN_NO_RETURN", CALL_RETURN_NO_RETURN }, + { "CALL_RETURN_NON_CALL", CALL_RETURN_NON_CALL }, { .name = NULL, }, }; @@ -3629,7 +3678,7 @@ static PyObject *pyrf__tracepoint(struct pyrf_evsel *pevsel, char *sys = NULL; char *name = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ss", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ss", kwlist, &sys, &name)) return NULL; @@ -3897,6 +3946,10 @@ static int pyrf_data__init(struct pyrf_data *pdata, PyObject *args, PyObject *kw pdata->data.mode = PERF_DATA_MODE_READ; pdata->data.file.fd = fd; if (perf_data__open(&pdata->data) < 0) { + if (fd >= 0) { + close(fd); + pdata->data.file.fd = -1; + } PyErr_Format(PyExc_IOError, "Failed to open perf data: %s", pdata->data.path ? pdata->data.path : "perf.data"); return -1; @@ -4053,25 +4106,363 @@ struct pyrf_session { struct perf_session *session; struct perf_tool tool; + struct itrace_synth_opts itrace_opts; struct pyrf_data *pdata; PyObject *sample; PyObject *stat; PyObject *context_switch; + PyObject *call_return; + struct call_return_processor *crp; + /** + * @call_return_last_db_id: Counter used to give each perf.call_return a + * unique db_id. A call's db_id may be allocated before the call + * returns, so the identifiers cannot be generated by the Python script. + */ + u64 call_return_last_db_id; + u64 sample_last_db_id; + char *vmlinux_name; + char *kallsyms_name; + char *symfs; }; -static int pyrf_session_tool__sample(const struct perf_tool *tool, - union perf_event *event, - struct perf_sample *sample, - struct machine *machine) +struct pyrf_call_return { + PyObject_HEAD + struct call_return cr; + /** + * @call_path: perf.callchain of the call path, ordered from the + * outermost caller to the callee. Resolved eagerly as the underlying + * struct call_path is owned by the call return processor. + */ + PyObject *call_path; + char *comm; + pid_t machine_pid; + pid_t pid; + pid_t tid; +}; + +static PyObject *pyrf_call_return__repr(struct pyrf_call_return *pcr) { - struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool); - PyObject *pyevent = pyrf_event__new(event, sample->evsel, psession->session, machine); - PyObject *ret; + return PyUnicode_FromString("{ type: call_return }"); +} - if (pyevent == NULL) - return -ENOMEM; +static PyObject *pyrf_call_return__get_comm(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + if (pcr->comm) + return PyUnicode_FromString(pcr->comm); + Py_RETURN_NONE; +} - ret = PyObject_CallFunction(psession->sample, "O", pyevent); +static PyObject *pyrf_call_return__get_machine_pid(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + return PyLong_FromLong(pcr->machine_pid); +} + +static PyObject *pyrf_call_return__get_pid(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + if (pcr->pid != -1) + return PyLong_FromLong(pcr->pid); + Py_RETURN_NONE; +} + +static PyObject *pyrf_call_return__get_tid(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + if (pcr->tid != -1) + return PyLong_FromLong(pcr->tid); + Py_RETURN_NONE; +} + +static PyObject *pyrf_call_return__get_call_time(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + struct call_return *cr = &pcr->cr; + + return PyLong_FromUnsignedLongLong(cr->call_time); +} + +static PyObject *pyrf_call_return__get_return_time(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + struct call_return *cr = &pcr->cr; + + return PyLong_FromUnsignedLongLong(cr->return_time); +} + +static PyObject *pyrf_call_return__get_branch_count(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + struct call_return *cr = &pcr->cr; + + return PyLong_FromUnsignedLongLong(cr->branch_count); +} + +static PyObject *pyrf_call_return__get_call_ref(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + struct call_return *cr = &pcr->cr; + + return PyLong_FromUnsignedLongLong(cr->call_ref); +} + +static PyObject *pyrf_call_return__get_return_ref(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + struct call_return *cr = &pcr->cr; + + return PyLong_FromUnsignedLongLong(cr->return_ref); +} + +static PyObject *pyrf_call_return__get_flags(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + struct call_return *cr = &pcr->cr; + + return PyLong_FromLong(cr->flags); +} + +static PyObject *pyrf_call_return__get_call_path(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + PyObject *call_path = pcr->call_path ?: Py_None; + + Py_INCREF(call_path); + return call_path; +} + +static PyObject *pyrf_call_return__get_parent_id(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + return PyLong_FromUnsignedLongLong(pcr->cr.parent_db_id); +} + +static PyObject *pyrf_call_return__get_insn_count(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + return PyLong_FromUnsignedLongLong(pcr->cr.insn_count); +} + +static PyObject *pyrf_call_return__get_cyc_count(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + return PyLong_FromUnsignedLongLong(pcr->cr.cyc_count); +} + +static PyObject *pyrf_call_return__get_db_id(struct pyrf_call_return *pcr, + void *closure __maybe_unused) +{ + return PyLong_FromUnsignedLongLong(pcr->cr.db_id); +} + +static PyGetSetDef pyrf_call_return__getset[] = { + { + .name = "db_id", + .get = (getter)pyrf_call_return__get_db_id, + .doc = "Unique identifier of this call return.", + }, + { + .name = "parent_id", + .get = (getter)pyrf_call_return__get_parent_id, + .doc = "db_id of the calling call return, 0 if there is none.", + }, + { + .name = "insn_count", + .get = (getter)pyrf_call_return__get_insn_count, + .doc = "Instructions executed between the call and the return.", + }, + { + .name = "cyc_count", + .get = (getter)pyrf_call_return__get_cyc_count, + .doc = "Cycles elapsed between the call and the return.", + }, + { + .name = "comm", + .get = (getter)pyrf_call_return__get_comm, + .doc = "Thread comm at the time of the call/return, or None.", + }, + { + .name = "machine_pid", + .get = (getter)pyrf_call_return__get_machine_pid, + .doc = "Machine PID (0 for host).", + }, + { + .name = "pid", + .get = (getter)pyrf_call_return__get_pid, + .doc = "Process ID of the thread, or None.", + }, + { + .name = "tid", + .get = (getter)pyrf_call_return__get_tid, + .doc = "Thread identifier, None if unknown.", + }, + { + .name = "call_time", + .get = (getter)pyrf_call_return__get_call_time, + .doc = "Timestamp of the call.", + }, + { + .name = "return_time", + .get = (getter)pyrf_call_return__get_return_time, + .doc = "Timestamp of the return.", + }, + { + .name = "branch_count", + .get = (getter)pyrf_call_return__get_branch_count, + .doc = "Branches taken between the call and the return.", + }, + { + .name = "call_ref", + .get = (getter)pyrf_call_return__get_call_ref, + .doc = "Reference to the sample of the call.", + }, + { + .name = "return_ref", + .get = (getter)pyrf_call_return__get_return_ref, + .doc = "Reference to the sample of the return.", + }, + { + .name = "flags", + .get = (getter)pyrf_call_return__get_flags, + .doc = "Bitmask of perf.CALL_RETURN_* values.", + }, + { + .name = "call_path", + .get = (getter)pyrf_call_return__get_call_path, + .doc = "perf.callchain from outermost caller to callee.", + }, + { .name = NULL, }, +}; + +static void pyrf_call_return__delete(struct pyrf_call_return *pevent) +{ + Py_XDECREF(pevent->call_path); + free(pevent->comm); + Py_TYPE(pevent)->tp_free((PyObject *)pevent); +} + +static PyTypeObject pyrf_call_return__type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "perf.call_return", + .tp_basicsize = sizeof(struct pyrf_call_return), + .tp_dealloc = (destructor)pyrf_call_return__delete, + .tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, + .tp_doc = "perf call_return object.", + .tp_getset = pyrf_call_return__getset, + .tp_repr = (reprfunc)pyrf_call_return__repr, +}; + +/* + * Turn a struct call_path, that is a leaf with parent pointers up to the + * outermost caller, into a perf.callchain ordered from the outermost caller to + * the leaf. The struct call_path is owned by the call return processor and so + * must be resolved before returning to Python. + */ +static PyObject *pyrf_call_path__to_callchain(const struct call_path *cp, + struct thread *thread) +{ + struct pyrf_callchain *pchain; + const struct call_path *pos; + u64 nr_frames = 0; + + /* The root call path is a sentinel with no symbol, skip it. */ + for (pos = cp; pos && pos->parent; pos = pos->parent) + nr_frames++; + + pchain = PyObject_New(struct pyrf_callchain, &pyrf_callchain__type); + if (!pchain) + return NULL; + + pchain->nr_frames = nr_frames; + pchain->frames = nr_frames ? calloc(nr_frames, sizeof(*pchain->frames)) : NULL; + if (nr_frames && !pchain->frames) { + pchain->nr_frames = 0; + Py_DECREF(pchain); + return PyErr_NoMemory(); + } + /* Fill in reverse so that the outermost caller is first. */ + for (pos = cp; nr_frames > 0; pos = pos->parent) { + struct pyrf_callchain_frame *frame = &pchain->frames[--nr_frames]; + + frame->ip = pos->ip; + frame->sym = NULL; + frame->sym_name = pos->sym ? strdup(pos->sym->name) : NULL; + frame->map = NULL; + if (thread) { + struct addr_location al; + u8 cpumode = pos->in_kernel ? PERF_RECORD_MISC_KERNEL + : PERF_RECORD_MISC_USER; + + addr_location__init(&al); + thread__find_map(thread, cpumode, pos->ip, &al); + frame->map = map__get(al.map); + if (frame->map) + frame->sym = pos->sym; + addr_location__exit(&al); + } + } + return (PyObject *)pchain; +} + +static PyObject *pyrf_call_return__new(struct call_return *cr) +{ + struct pyrf_call_return *pevent; + + pevent = PyObject_New(struct pyrf_call_return, &pyrf_call_return__type); + if (!pevent) + return NULL; + + pevent->cr = *cr; + /* Avoid lifetime issues with thread by caching tid/pid/machine_pid */ + if (cr->thread) { + struct maps *maps = thread__maps(cr->thread); + + pevent->machine_pid = maps && maps__machine(maps) ? maps__machine(maps)->pid : 0; + pevent->pid = thread__pid(cr->thread); + pevent->tid = thread__tid(cr->thread); + } else { + pevent->machine_pid = 0; + pevent->pid = -1; + pevent->tid = -1; + } + pevent->cr.thread = NULL; + pevent->comm = cr->comm ? strdup(comm__str(cr->comm)) : NULL; + pevent->call_path = NULL; + if (cr->cp) { + pevent->call_path = pyrf_call_path__to_callchain(cr->cp, cr->thread); + if (!pevent->call_path) { + Py_DECREF(pevent); + return NULL; + } + } + return (PyObject *)pevent; +} + +static int pyrf_session__call_return_process(struct call_return *cr, + u64 *parent_db_id, + void *data) +{ + struct pyrf_session *psession = data; + PyObject *pyevent, *ret; + + if (!psession->call_return) + return 0; + + if (!cr->db_id) + cr->db_id = ++psession->call_return_last_db_id; + + if (parent_db_id) { + if (!*parent_db_id) + *parent_db_id = ++psession->call_return_last_db_id; + cr->parent_db_id = *parent_db_id; + } + + pyevent = pyrf_call_return__new(cr); + if (!pyevent) + return -1; + + ret = PyObject_CallFunctionObjArgs(psession->call_return, pyevent, NULL); if (!ret) { Py_DECREF(pyevent); return -1; @@ -4081,13 +4472,71 @@ static int pyrf_session_tool__sample(const struct perf_tool *tool, return 0; } +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; + + if (psession->crp) { + struct addr_location al, addr_al; + struct thread *thread = NULL; + + addr_location__init(&al); + addr_location__init(&addr_al); + + if (machine__resolve(machine, &al, sample) >= 0) { + thread = al.thread; + if (sample->flags & (PERF_IP_FLAG_CALL | + PERF_IP_FLAG_TRACE_BEGIN | + PERF_IP_FLAG_RETURN | + PERF_IP_FLAG_BRANCH | + PERF_IP_FLAG_TRACE_END)) { + thread__resolve(thread, &addr_al, sample); + } + + int err; + + err = thread_stack__process(thread, thread__comm(thread), sample, + &al, &addr_al, sample_db_id, psession->crp); + if (err) { + addr_location__exit(&addr_al); + addr_location__exit(&al); + return err; + } + } + + addr_location__exit(&addr_al); + addr_location__exit(&al); + } + + if (psession->sample) { + PyObject *pyevent = pyrf_event__new(event, sample->evsel, + psession->session, + machine, sample); + PyObject *ret; + + if (pyevent == NULL) + return -ENOMEM; + + ret = PyObject_CallFunction(psession->sample, "O", pyevent); + Py_DECREF(pyevent); + if (!ret) + return -1; + Py_DECREF(ret); + } + 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) { struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool); - PyObject *pyevent = pyrf_event__new(event, sample->evsel, psession->session, machine); + PyObject *pyevent = pyrf_event__new(event, sample->evsel, psession->session, machine, NULL); PyObject *ret; if (perf_event__process_switch(tool, event, sample, machine) < 0) { @@ -4115,7 +4564,7 @@ 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); + /*machine=*/NULL, NULL); const char *name = evsel ? evsel__name(evsel) : "unknown"; PyObject *ret; @@ -4138,7 +4587,7 @@ static int pyrf_session_tool__stat_round(const struct perf_tool *tool, { struct pyrf_session *psession = container_of(tool, struct pyrf_session, tool); PyObject *pyevent = pyrf_event__new(event, /*evsel=*/NULL, psession->session, - /*machine=*/NULL); + /*machine=*/NULL, NULL); PyObject *ret; if (pyevent == NULL) @@ -4166,17 +4615,28 @@ static PyObject *pyrf_session__find_thread(struct pyrf_session *psession, PyObje if (!PyArg_ParseTuple(args, "i|i", &pid, &tid)) return NULL; - if (tid == -1) + if (tid == -1) { tid = pid; + pid = -1; + } - /* Look up the thread in the host machine first, then fall back to guest machines. */ + /* + * When host lookup fails, guest machines (default_guest and the guests + * rb_root) are searched. + */ machine = &psession->session->machines.host; thread = machine__find_thread(machine, pid, tid); if (!thread) { - machine = perf_session__find_machine(psession->session, pid); + machine = perf_session__find_machine(psession->session, pid != -1 ? pid : tid); if (machine) thread = machine__find_thread(machine, pid, tid); + if (!thread) { + machine = machines__find(&psession->session->machines, + DEFAULT_GUEST_KERNEL_ID); + if (machine) + thread = machine__find_thread(machine, pid, tid); + } } /* Return None rather than raising TypeError when a PID/TID is not known. */ @@ -4191,23 +4651,40 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject { struct pyrf_data *pdata; PyObject *sample = NULL, *stat = NULL, *context_switch = NULL; - static char *kwlist[] = { "data", "sample", "stat", "context_switch", NULL }; + PyObject *call_return = NULL; + char *itrace_str = NULL; + char *vmlinux_str = NULL, *kallsyms_str = NULL, *symfs_str = NULL; + static char *kwlist[] = { "data", "sample", "stat", "context_switch", + "call_return", "itrace", "vmlinux", "kallsyms", + "symfs", NULL }; struct pyrf_session *psession; struct perf_session *session; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!|OOO", kwlist, &pyrf_data__type, &pdata, - &sample, &stat, &context_switch)) + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!|OOOOzzzz", kwlist, + &pyrf_data__type, &pdata, + &sample, &stat, &context_switch, + &call_return, &itrace_str, + &vmlinux_str, &kallsyms_str, &symfs_str)) return NULL; psession = PyObject_New(struct pyrf_session, type); if (!psession) return NULL; + /* PyObject_New doesn't zero the memory, initialize every member. */ psession->session = NULL; psession->sample = NULL; psession->stat = NULL; psession->context_switch = NULL; + psession->call_return = NULL; + psession->crp = NULL; + psession->call_return_last_db_id = 0; + psession->sample_last_db_id = 0; + psession->vmlinux_name = NULL; + psession->kallsyms_name = NULL; + psession->symfs = NULL; psession->pdata = NULL; + memset(&psession->itrace_opts, 0, sizeof(psession->itrace_opts)); Py_INCREF(pdata); psession->pdata = pdata; @@ -4217,7 +4694,7 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject #define ADD_TOOL(name) \ do { \ - if (name) { \ + if (name && name != Py_None) { \ if (!PyCallable_Check(name)) { \ PyErr_SetString(PyExc_TypeError, #name " must be callable"); \ goto err_out; \ @@ -4229,15 +4706,38 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject } while (0) ADD_TOOL(sample); + if (call_return && call_return != Py_None && (!sample || sample == Py_None)) + psession->tool.sample = pyrf_session_tool__sample; ADD_TOOL(stat); ADD_TOOL(context_switch); + + if (call_return && call_return != Py_None) { + if (!PyCallable_Check(call_return)) { + PyErr_SetString(PyExc_TypeError, "call_return must be callable"); + Py_DECREF(psession); + return NULL; + } + Py_INCREF(call_return); + psession->call_return = call_return; + psession->crp = call_return_processor__new(pyrf_session__call_return_process, + psession); + if (!psession->crp) { + PyErr_SetString(PyExc_MemoryError, + "Failed to allocate call_return_processor"); + Py_DECREF(psession); + return NULL; + } + } #undef ADD_TOOL - if (stat) + if (stat && stat != Py_None) psession->tool.stat_round = pyrf_session_tool__stat_round; - psession->tool.comm = perf_event__process_comm; + psession->tool.auxtrace_info = perf_event__process_auxtrace_info; + psession->tool.auxtrace = perf_event__process_auxtrace; + psession->tool.auxtrace_error = perf_event__process_auxtrace_error; + psession->tool.id_index = perf_event__process_id_index; psession->tool.mmap = perf_event__process_mmap; psession->tool.mmap2 = perf_event__process_mmap2; psession->tool.namespaces = perf_event__process_namespaces; @@ -4254,12 +4754,57 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject psession->tool.tracing_data = perf_event__process_tracing_data; #endif + if (!vmlinux_str) + vmlinux_str = getenv("PERF_SYMBOL_VMLINUX"); + if (vmlinux_str && vmlinux_str[0]) { + psession->vmlinux_name = strdup(vmlinux_str); + if (!psession->vmlinux_name) { + PyErr_NoMemory(); + goto err_out; + } + symbol_conf.vmlinux_name = psession->vmlinux_name; + } + + if (!kallsyms_str) + kallsyms_str = getenv("PERF_SYMBOL_KALLSYMS"); + if (kallsyms_str && kallsyms_str[0]) { + psession->kallsyms_name = strdup(kallsyms_str); + if (!psession->kallsyms_name) { + PyErr_NoMemory(); + goto err_out; + } + symbol_conf.kallsyms_name = psession->kallsyms_name; + } + + if (!symfs_str) + symfs_str = getenv("PERF_SYMBOL_SYMFS"); + if (symfs_str && symfs_str[0]) { + psession->symfs = strdup(symfs_str); + if (!psession->symfs) { + 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"); + if (itrace_str) { + 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; + } + } symbol_conf.use_callchain = true; symbol_conf.show_kernel_path = true; @@ -4269,8 +4814,6 @@ static PyObject *pyrf_session__new(PyTypeObject *type, PyObject *args, PyObject goto err_out; } - - return (PyObject *)psession; err_out: Py_DECREF(psession); @@ -4280,10 +4823,25 @@ 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; + 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); + free(psession->itrace_opts.ptime_range); Py_XDECREF(psession->pdata); Py_XDECREF(psession->sample); Py_XDECREF(psession->stat); Py_XDECREF(psession->context_switch); + Py_XDECREF(psession->call_return); + if (psession->crp) + call_return_processor__free(psession->crp); Py_TYPE(psession)->tp_free((PyObject *)psession); } @@ -4326,10 +4884,54 @@ static const char pyrf_session__doc[] = PyDoc_STR("perf session object."); static PyObject *pyrf_session__getattro(struct pyrf_session *psession, PyObject *attr_name) { + const char *name_str = PyUnicode_AsUTF8(attr_name); + + if (!name_str) + return NULL; if (!psession->session) { PyErr_SetString(PyExc_ValueError, "session not initialized"); return NULL; } + if (!strcmp(name_str, "e_machine")) + return PyLong_FromLong(perf_session__e_machine(psession->session, + /*e_flags=*/NULL)); + + if (!strcmp(name_str, "is_64_bit")) { + /* + * Prefer the machine of the recorded threads over the perf_env, + * as in pipe mode the perf_env may not be populated yet and + * perf_env__kernel_is_64_bit would cache the host's bitness. + */ + switch (perf_session__e_machine(psession->session, /*e_flags=*/NULL)) { + case EM_AARCH64: + case EM_ALPHA: + case EM_IA_64: + case EM_LOONGARCH: + case EM_PPC64: + case EM_SPARCV9: + case EM_X86_64: + Py_RETURN_TRUE; + case EM_386: + case EM_ARM: + case EM_PPC: + case EM_SPARC: + Py_RETURN_FALSE; + default: + /* + * Machines like EM_MIPS, EM_PARISC, EM_RISCV and + * EM_S390 are used for both word sizes, fall back on + * the recorded arch string if available. + */ + break; + } + { + struct perf_env *env = perf_session__env(psession->session); + + if (env && env->arch) + return PyBool_FromLong(perf_env__kernel_is_64_bit(env) == 1); + return PyBool_FromLong(sizeof(void *) == 8); + } + } return PyObject_GenericGetAttr((PyObject *) psession, attr_name); } @@ -4537,7 +5139,8 @@ PyMODINIT_FUNC PyInit_perf(void) if (module == NULL) return NULL; - if (pyrf_event__setup_types() < 0 || + if (PyType_Ready(&pyrf_call_return__type) < 0 || + pyrf_event__setup_types() < 0 || pyrf_evlist__setup_types() < 0 || pyrf_evsel__setup_types() < 0 || pyrf_thread_map__setup_types() < 0 || @@ -4588,9 +5191,6 @@ PyMODINIT_FUNC PyInit_perf(void) Py_INCREF(&pyrf_throttle_event__type); PyModule_AddObject(module, "throttle_event", (PyObject *)&pyrf_throttle_event__type); - Py_INCREF(&pyrf_task_event__type); - PyModule_AddObject(module, "task_event", (PyObject *)&pyrf_task_event__type); - Py_INCREF(&pyrf_read_event__type); PyModule_AddObject(module, "read_event", (PyObject *)&pyrf_read_event__type); @@ -4620,6 +5220,8 @@ PyMODINIT_FUNC PyInit_perf(void) Py_INCREF(&pyrf_session__type); PyModule_AddObject(module, "session", (PyObject *)&pyrf_session__type); + Py_INCREF(&pyrf_call_return__type); + PyModule_AddObject(module, "call_return", (PyObject *)&pyrf_call_return__type); Py_INCREF(&pyrf_branch_entry__type); if (PyModule_AddObject(module, "branch_entry", (PyObject *)&pyrf_branch_entry__type) < 0) { From ca23ecf8eedbcdb2ae366b38599722af31adef89 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:37 -0700 Subject: [PATCH 185/425] perf python: Allow KeyboardInterrupt to propagate in LiveSession LiveSession.run() synchronously blocks while polling evlist for events. When running interactive tools or scripts from the terminal, KeyboardInterrupt exceptions raised via Ctrl-C must propagate so the caller can break out of the polling loop. Previously, LiveSession caught KeyboardInterrupt with an empty pass statement, swallowing the exception and preventing calling scripts from detecting the interrupt cleanly. Remove the empty handler so calling scripts receive KeyboardInterrupt and can perform clean teardown. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/perf_live.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/perf/python/perf_live.py b/tools/perf/python/perf_live.py index 616b5f657463..99e73d58a96f 100755 --- a/tools/perf/python/perf_live.py +++ b/tools/perf/python/perf_live.py @@ -33,10 +33,16 @@ class LiveSession: # Poll for events with 100ms timeout try: self.evlist.poll(100) + # CPython's PyErr_SetFromErrno() invokes PyErr_CheckSignals() on + # EINTR, converting SIGINT into KeyboardInterrupt, which inherits + # from BaseException rather than InterruptedError/OSError, so + # Ctrl-C exits the loop and triggers finally: self.evlist.close() + # while non-fatal signals retry poll(). except InterruptedError: continue for cpu in self.cpus: - for _ in range(1000): # Limit to 1000 events per CPU per poll to prevent starvation + # Limit to 1000 events per CPU per poll to prevent starvation + for _ in range(1000): try: event = self.evlist.read_on_cpu(cpu) except TypeError as e: @@ -53,7 +59,5 @@ class LiveSession: if event.type == perf.RECORD_SAMPLE: self.sample_callback(event) - except KeyboardInterrupt: - pass finally: self.evlist.close() From db5a60750133d1789c1254fcae55dbf542ef0aa6 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:38 -0700 Subject: [PATCH 186/425] perf pmu-events: Clean up mypy and pylint issues Clean up mypy type errors and pylint warnings across the pmu-events Python scripts (amd_metrics.py, intel_metrics.py, jevents.py, make_legacy_cache.py, and metric.py) in preparation for making MYPY and PYLINT build tests opt-out. In addition to type annotations and unused import/variable cleanups, fix three bugs in intel_metrics.py exposed by static analysis: - Fix references to undefined 'args.model' (instead of '_args.model') inside try/except blocks in IntelL2() and UncoreMemBw(), which previously threw NameError and silently disabled Skylake/CascadeLake L2 silent eviction adjustments and uncore memory bandwidth JSON loading. - Define DC_WB_U and DC_WB_D before their first use in IntelL2(), fixing an UnboundLocalError that silently dropped the l2_out_rate, wbn, and isd metrics. - Fix Event-to-string comparison and addition in IntelFp() by comparing and modifying 'f_assist.name' instead of the Event object 'f_assist'. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/pmu-events/amd_metrics.py | 16 +- tools/perf/pmu-events/intel_metrics.py | 265 ++++++++++----------- tools/perf/pmu-events/jevents.py | 77 +++--- tools/perf/pmu-events/make_legacy_cache.py | 34 +-- tools/perf/pmu-events/metric.py | 60 +++-- 5 files changed, 217 insertions(+), 235 deletions(-) diff --git a/tools/perf/pmu-events/amd_metrics.py b/tools/perf/pmu-events/amd_metrics.py index dccfcacaf148..42569b9153f4 100755 --- a/tools/perf/pmu-events/amd_metrics.py +++ b/tools/perf/pmu-events/amd_metrics.py @@ -3,7 +3,7 @@ import argparse import math import os -from typing import Optional +from typing import Optional, Union from common_metrics import Cycles from metric import (d_ratio, has_event, max, Event, JsonEncodeMetric, JsonEncodeMetricGroupDescriptions, Literal, LoadEvents, @@ -70,7 +70,6 @@ def AmdBr(): ]) def Conditional() -> Optional[MetricGroup]: - global _zen_model br = Event("ex_ret_brn_cond", "ex_ret_cond") br_r = d_ratio(br, interval_sec) ins_r = d_ratio(ins, br) @@ -156,25 +155,24 @@ def AmdCtxSw() -> MetricGroup: def AmdDtlb() -> Optional[MetricGroup]: - global _zen_model if _zen_model >= 4: return None - d_dat = Event("ls_dc_accesses") if _zen_model <= 3 else None + d_dat = Event("ls_dc_accesses") d_h4k = Event("ls_l1_d_tlb_miss.tlb_reload_4k_l2_hit") - d_hcoal = Event( + d_hcoal: Union[Event, int] = Event( "ls_l1_d_tlb_miss.tlb_reload_coalesced_page_hit") if _zen_model >= 2 else 0 d_h2m = Event("ls_l1_d_tlb_miss.tlb_reload_2m_l2_hit") d_h1g = Event("ls_l1_d_tlb_miss.tlb_reload_1g_l2_hit") d_m4k = Event("ls_l1_d_tlb_miss.tlb_reload_4k_l2_miss") - d_mcoal = Event( + d_mcoal: Union[Event, int] = Event( "ls_l1_d_tlb_miss.tlb_reload_coalesced_page_miss") if _zen_model >= 2 else 0 d_m2m = Event("ls_l1_d_tlb_miss.tlb_reload_2m_l2_miss") d_m1g = Event("ls_l1_d_tlb_miss.tlb_reload_1g_l2_miss") - d_w0 = Event("ls_tablewalker.dc_type0") if _zen_model <= 3 else None - d_w1 = Event("ls_tablewalker.dc_type1") if _zen_model <= 3 else None + d_w0 = Event("ls_tablewalker.dc_type0") + d_w1 = Event("ls_tablewalker.dc_type1") walks = d_w0 + d_w1 walks_r = d_ratio(walks, interval_sec) ins_w = d_ratio(ins, walks) @@ -266,7 +264,6 @@ def AmdDtlb() -> Optional[MetricGroup]: def AmdIotlb() -> Optional[MetricGroup]: - global _zen_model if _zen_model < 2: return None @@ -322,7 +319,6 @@ def AmdIotlb() -> Optional[MetricGroup]: def AmdItlb(): - global _zen_model l2h = Event("bp_l1_tlb_miss_l2_tlb_hit", "bp_l1_tlb_miss_l2_hit") l2m = Event("bp_l1_tlb_miss_l2_tlb_miss.all", "l2_itlb_misses",) l2r = l2h + l2m diff --git a/tools/perf/pmu-events/intel_metrics.py b/tools/perf/pmu-events/intel_metrics.py index 9780bf978a41..4fd5c1de4333 100755 --- a/tools/perf/pmu-events/intel_metrics.py +++ b/tools/perf/pmu-events/intel_metrics.py @@ -5,10 +5,10 @@ import json import math import os import re -from typing import Optional +from typing import Optional, Union from common_metrics import Cycles -from metric import (d_ratio, has_event, max, aggr_nr, CheckPmu, Event, - JsonEncodeMetric, JsonEncodeMetricGroupDescriptions, +from metric import (d_ratio, has_event, max, aggr_nr, CheckPmu, Constant, Event, + Expression, JsonEncodeMetric, JsonEncodeMetricGroupDescriptions, Literal, LoadEvents, Metric, MetricConstraint, MetricGroup, MetricRef, Select) @@ -90,7 +90,7 @@ def Tsx() -> Optional[MetricGroup]: # sysfs version so that we can detect its presence at runtime. transaction_start = Event("RTM_RETIRED.START") transaction_start = Event(f'{pmu}/tx\\-start/') - except: + except ValueError: return None elision_start = None @@ -100,7 +100,7 @@ def Tsx() -> Optional[MetricGroup]: # case. Again, prefer the sysfs encoding of the event. elision_start = Event("HLE_RETIRED.START") elision_start = Event(f'{pmu}/el\\-start/') - except: + except ValueError: pass return MetricGroup('transaction', [ @@ -139,7 +139,7 @@ def IntelBr(): br_clr = None try: br_clr = Event("BACLEARS.ANY", "BACLEARS.ALL") - except: + except ValueError: pass br_r = d_ratio(br_all, interval_sec) @@ -171,7 +171,7 @@ def IntelBr(): br_m_tk = Event("BR_MISP_RETIRED.NEAR_TAKEN", "BR_MISP_RETIRED.TAKEN_JCC", "BR_INST_RETIRED.MISPRED_TAKEN") - except: + except ValueError: pass br_r = d_ratio(br_all, interval_sec) ins_r = d_ratio(ins, br_all) @@ -199,7 +199,7 @@ def IntelBr(): br_m_cond = Event("BR_MISP_RETIRED.COND", "BR_MISP_RETIRED.CONDITIONAL", "BR_MISP_RETIRED.TAKEN_JCC") - except: + except ValueError: return None br_cond_nt = None @@ -207,7 +207,7 @@ def IntelBr(): try: br_cond_nt = Event("BR_INST_RETIRED.COND_NTAKEN") br_m_cond_nt = Event("BR_MISP_RETIRED.COND_NTAKEN") - except: + except ValueError: pass br_r = d_ratio(br_cond, interval_sec) ins_r = d_ratio(ins, br_cond) @@ -222,7 +222,7 @@ def IntelBr(): "Retired conditional branch instructions mispredicted as a " "percentage of all conditional branches.", misp_r, "100%"), ] - if not br_m_cond_nt: + if not br_m_cond_nt or not br_cond_nt: return MetricGroup("lpm_br_cond", taken_metrics) br_r = d_ratio(br_cond_nt, interval_sec) @@ -247,7 +247,7 @@ def IntelBr(): def Far() -> Optional[MetricGroup]: try: br_far = Event("BR_INST_RETIRED.FAR_BRANCH") - except: + except ValueError: return None br_r = d_ratio(br_far, interval_sec) @@ -284,7 +284,7 @@ def IntelCtxSw() -> MetricGroup: ev = Event("MEM_INST_RETIRED.ALL_LOADS", "MEM_UOPS_RETIRED.ALL_LOADS") metrics.append(Metric("lpm_cs_loads", "Loads per context switch", d_ratio(ev, cs), "loads/cs")) - except: + except ValueError: pass try: @@ -292,14 +292,14 @@ def IntelCtxSw() -> MetricGroup: "MEM_UOPS_RETIRED.ALL_STORES") metrics.append(Metric("lpm_cs_stores", "Stores per context switch", d_ratio(ev, cs), "stores/cs")) - except: + except ValueError: pass try: ev = Event("BR_INST_RETIRED.NEAR_TAKEN", "BR_INST_RETIRED.TAKEN_JCC") metrics.append(Metric("lpm_cs_br_taken", "Branches taken per context switch", d_ratio(ev, cs), "br_taken/cs")) - except: + except ValueError: pass try: @@ -309,12 +309,12 @@ def IntelCtxSw() -> MetricGroup: try: l2_misses += Event("L2_RQSTS.HWPF_MISS", "L2_RQSTS.L2_PF_MISS", "L2_RQSTS.PF_MISS") - except: + except ValueError: pass metrics.append(Metric("lpm_cs_l2_misses", "L2 misses per context switch", d_ratio(l2_misses, cs), "l2_misses/cs")) - except: + except ValueError: pass return MetricGroup("lpm_cs", metrics, @@ -327,7 +327,7 @@ def IntelFpu() -> Optional[MetricGroup]: try: s_64 = Event("FP_ARITH_INST_RETIRED.SCALAR_SINGLE", "SIMD_INST_RETIRED.SCALAR_SINGLE") - except: + except ValueError: return None d_64 = Event("FP_ARITH_INST_RETIRED.SCALAR_DOUBLE", "SIMD_INST_RETIRED.SCALAR_DOUBLE") @@ -352,21 +352,21 @@ def IntelFpu() -> Optional[MetricGroup]: flop += 16 * s_512 d_512 = Event("FP_ARITH_INST_RETIRED.512B_PACKED_DOUBLE") flop += 8 * d_512 - except: + except ValueError: pass f_assist = Event("ASSISTS.FP", "FP_ASSIST.ANY", "FP_ASSIST.S") - if f_assist in [ - "ASSISTS.FP", - "FP_ASSIST.S", - ]: - f_assist += "/cmask=1/" - - flop_r = d_ratio(flop, interval_sec) - flop_c = d_ratio(flop, cyc) nmi_constraint = MetricConstraint.GROUPED_EVENTS if f_assist.name == "ASSISTS.FP": # Icelake+ nmi_constraint = MetricConstraint.NO_GROUP_EVENTS_NMI + if f_assist.name in [ + "ASSISTS.FP", + "FP_ASSIST.S", + ]: + f_assist = Event(f"{f_assist.name}/cmask=1/") + + flop_r = d_ratio(flop, interval_sec) + flop_c = d_ratio(flop, cyc) def FpuMetrics(group: str, fl: Optional[Event], mult: int, desc: str) -> Optional[MetricGroup]: if not fl: @@ -421,16 +421,17 @@ def IntelFpu() -> Optional[MetricGroup]: def IntelIlp() -> MetricGroup: tsc = Event("msr/tsc/") c0 = Event("msr/mperf/") - low = tsc - c0 + low = max(tsc - c0, 0) inst_ret = Event("INST_RETIRED.ANY_P") - inst_ret_c = [Event(f"{inst_ret.name}/cmask={x}/") for x in range(1, 6)] core_cycles = Event("CPU_CLK_UNHALTED.THREAD_P_ANY", "CPU_CLK_UNHALTED.DISTRIBUTED", "cycles") + inst_ret_c = [Event(f"{inst_ret.name}/cmask={x}/") for x in range(1, 6)] + ilp = [d_ratio(max(inst_ret_c[x] - inst_ret_c[x + 1], 0), core_cycles) for x in range(0, 4)] ilp.append(d_ratio(inst_ret_c[4], core_cycles)) - ilp0 = 1 + ilp0: Expression = Constant(1) for x in ilp: ilp0 -= x return MetricGroup("lpm_ilp", [ @@ -465,7 +466,7 @@ def IntelIotlb() -> Optional[MetricGroup]: + Event("UNC_IIO_IOMMU0.1G_HITS") ) total_miss = Event("UNC_IIO_IOMMU0.MISSES") - except: + except ValueError: return None miss_rate = d_ratio(total_miss, total_miss + total_hit) @@ -508,7 +509,7 @@ def IntelIotlb() -> Optional[MetricGroup]: "100%", ), ] - except: + except ValueError: pass return MetricGroup( @@ -519,59 +520,92 @@ def IntelIotlb() -> Optional[MetricGroup]: def IntelL2() -> Optional[MetricGroup]: + assert _args is not None try: DC_HIT = Event("L2_RQSTS.DEMAND_DATA_RD_HIT") - except: + except ValueError: return None try: DC_MISS = Event("L2_RQSTS.DEMAND_DATA_RD_MISS") - l2_dmnd_miss = DC_MISS - l2_dmnd_rd_all = DC_MISS + DC_HIT - except: + l2_dmnd_miss: Expression = DC_MISS + l2_dmnd_rd_all: Expression = DC_MISS + DC_HIT + except ValueError: DC_ALL = Event("L2_RQSTS.ALL_DEMAND_DATA_RD") l2_dmnd_miss = DC_ALL - DC_HIT l2_dmnd_rd_all = DC_ALL l2_dmnd_mrate = d_ratio(l2_dmnd_miss, interval_sec) l2_dmnd_rrate = d_ratio(l2_dmnd_rd_all, interval_sec) - DC_PFH = None - DC_PFM = None - l2_pf_all = None - l2_pf_mrate = None - l2_pf_rrate = None + l2_useless_rate = None + try: + DC_OUT_U = Event("L2_LINES_OUT.USELESS_HWPF") + l2_pf_useless = DC_OUT_U + l2_useless_rate = d_ratio(l2_pf_useless, interval_sec) + except ValueError: + pass + + hwpf_group = None try: DC_PFH = Event("L2_RQSTS.PF_HIT") DC_PFM = Event("L2_RQSTS.PF_MISS") l2_pf_all = DC_PFH + DC_PFM l2_pf_mrate = d_ratio(DC_PFM, interval_sec) l2_pf_rrate = d_ratio(l2_pf_all, interval_sec) - except: + hwpf_group = MetricGroup("lpm_l2_hwpf", [ + Metric("lpm_l2_hwpf_hits", "L2 cache hardware prefetcher hits", + d_ratio(DC_PFH, l2_pf_all), "100%"), + Metric("lpm_l2_hwpf_misses", "L2 cache hardware prefetcher misses", + d_ratio(DC_PFM, l2_pf_all), "100%"), + Metric("lpm_l2_hwpf_useless", "L2 cache hardware prefetcher useless prefetches per second", + l2_useless_rate, "100%") if l2_useless_rate else None, + Metric("lpm_l2_hwpf_requests", "L2 cache hardware prefetcher requests per second", + l2_pf_rrate, "100%"), + Metric("lpm_l2_hwpf_misses", "L2 cache hardware prefetcher misses per second", + l2_pf_mrate, "100%"), + ]) + except ValueError: pass - DC_RFOH = None - DC_RFOM = None - l2_rfo_all = None - l2_rfo_mrate = None - l2_rfo_rrate = None + rfo_group = None try: DC_RFOH = Event("L2_RQSTS.RFO_HIT") DC_RFOM = Event("L2_RQSTS.RFO_MISS") l2_rfo_all = DC_RFOH + DC_RFOM l2_rfo_mrate = d_ratio(DC_RFOM, interval_sec) l2_rfo_rrate = d_ratio(l2_rfo_all, interval_sec) - except: + rfo_group = MetricGroup("lpm_l2_rfo", [ + Metric("lpm_l2_rfo_hits", "L2 cache request for ownership (RFO) hits", + d_ratio(DC_RFOH, l2_rfo_all), "100%"), + Metric("lpm_l2_rfo_misses", "L2 cache request for ownership (RFO) misses", + d_ratio(DC_RFOM, l2_rfo_all), "100%"), + Metric("lpm_l2_rfo_requests", "L2 cache request for ownership (RFO) requests per second", + l2_rfo_rrate, "requests/s"), + Metric("lpm_l2_rfo_misses", "L2 cache request for ownership (RFO) misses per second", + l2_rfo_mrate, "misses/s"), + ]) + except ValueError: pass DC_CH = None try: DC_CH = Event("L2_RQSTS.CODE_RD_HIT") - except: + except ValueError: pass DC_CM = Event("L2_RQSTS.CODE_RD_MISS") DC_IN = Event("L2_LINES_IN.ALL") - DC_OUT_NS = None - DC_OUT_S = None - l2_lines_out = None + + DC_WB_U = None + DC_WB_D = None + wbu = None + wbd = None + try: + DC_WB_U = Event("IDI_MISC.WB_UPGRADE") + DC_WB_D = Event("IDI_MISC.WB_DOWNGRADE") + wbu = d_ratio(DC_WB_U, interval_sec) + wbd = d_ratio(DC_WB_D, interval_sec) + except ValueError: + pass + l2_out_rate = None wbn = None isd = None @@ -583,43 +617,24 @@ def IntelL2() -> Optional[MetricGroup]: "L2_LINES_OUT.DEMAND_CLEAN", "L2_LINES_IN.I") if DC_OUT_S.name == "L2_LINES_OUT.SILENT" and ( - args.model.startswith("skylake") or - args.model == "cascadelakex"): + _args.model.startswith("skylake") or + _args.model == "cascadelakex"): DC_OUT_S.name = "L2_LINES_OUT.SILENT/any/" # bring is back to per-CPU l2_s = Select(DC_OUT_S / 2, Literal("#smt_on"), DC_OUT_S) l2_ns = DC_OUT_NS l2_lines_out = l2_s + l2_ns l2_out_rate = d_ratio(l2_lines_out, interval_sec) - nlr = max(l2_ns - DC_WB_U - DC_WB_D, 0) - wbn = d_ratio(nlr, interval_sec) + if DC_WB_U and DC_WB_D: + nlr = max(l2_ns - DC_WB_U - DC_WB_D, 0) + wbn = d_ratio(nlr, interval_sec) isd = d_ratio(l2_s, interval_sec) - except: - pass - DC_OUT_U = None - l2_pf_useless = None - l2_useless_rate = None - try: - DC_OUT_U = Event("L2_LINES_OUT.USELESS_HWPF") - l2_pf_useless = DC_OUT_U - l2_useless_rate = d_ratio(l2_pf_useless, interval_sec) - except: - pass - DC_WB_U = None - DC_WB_D = None - wbu = None - wbd = None - try: - DC_WB_U = Event("IDI_MISC.WB_UPGRADE") - DC_WB_D = Event("IDI_MISC.WB_DOWNGRADE") - wbu = d_ratio(DC_WB_U, interval_sec) - wbd = d_ratio(DC_WB_D, interval_sec) - except: + except ValueError: pass l2_lines_in = DC_IN l2_code_all = (DC_CH + DC_CM) if DC_CH else None - l2_code_rate = d_ratio(l2_code_all, interval_sec) if DC_CH else None + l2_code_rate = d_ratio(l2_code_all, interval_sec) if l2_code_all else None l2_code_miss_rate = d_ratio(DC_CM, interval_sec) l2_in_rate = d_ratio(l2_lines_in, interval_sec) @@ -640,35 +655,15 @@ def IntelL2() -> Optional[MetricGroup]: Metric("lpm_l2_rd_misses", "L2 cache data read misses per second", l2_dmnd_mrate, "misses/s"), ]), - MetricGroup("lpm_l2_hwpf", [ - Metric("lpm_l2_hwpf_hits", "L2 cache hardware prefetcher hits", - d_ratio(DC_PFH, l2_pf_all), "100%"), - Metric("lpm_l2_hwpf_misses", "L2 cache hardware prefetcher misses", - d_ratio(DC_PFM, l2_pf_all), "100%"), - Metric("lpm_l2_hwpf_useless", "L2 cache hardware prefetcher useless prefetches per second", - l2_useless_rate, "100%") if l2_useless_rate else None, - Metric("lpm_l2_hwpf_requests", "L2 cache hardware prefetcher requests per second", - l2_pf_rrate, "100%"), - Metric("lpm_l2_hwpf_misses", "L2 cache hardware prefetcher misses per second", - l2_pf_mrate, "100%"), - ]) if DC_PFH else None, - MetricGroup("lpm_l2_rfo", [ - Metric("lpm_l2_rfo_hits", "L2 cache request for ownership (RFO) hits", - d_ratio(DC_RFOH, l2_rfo_all), "100%"), - Metric("lpm_l2_rfo_misses", "L2 cache request for ownership (RFO) misses", - d_ratio(DC_RFOM, l2_rfo_all), "100%"), - Metric("lpm_l2_rfo_requests", "L2 cache request for ownership (RFO) requests per second", - l2_rfo_rrate, "requests/s"), - Metric("lpm_l2_rfo_misses", "L2 cache request for ownership (RFO) misses per second", - l2_rfo_mrate, "misses/s"), - ]) if DC_RFOH else None, + hwpf_group, + rfo_group, MetricGroup("lpm_l2_code", [ Metric("lpm_l2_code_hits", "L2 cache code hits", - d_ratio(DC_CH, l2_code_all), "100%") if DC_CH else None, + d_ratio(DC_CH, l2_code_all), "100%") if DC_CH and l2_code_all else None, Metric("lpm_l2_code_misses", "L2 cache code misses", - d_ratio(DC_CM, l2_code_all), "100%") if DC_CH else None, + d_ratio(DC_CM, l2_code_all), "100%") if DC_CH and l2_code_all else None, Metric("lpm_l2_code_requests", "L2 cache code requests per second", - l2_code_rate, "requests/s") if DC_CH else None, + l2_code_rate, "requests/s") if l2_code_rate else None, Metric("lpm_l2_code_misses", "L2 cache code misses per second", l2_code_miss_rate, "misses/s"), ]), @@ -706,7 +701,7 @@ def IntelMissLat() -> Optional[MetricGroup]: "UNC_CHA_TOR_INSERTS.IA_MISS", "UNC_C_TOR_INSERTS.MISS_REMOTE_OPCODE", "UNC_C_TOR_INSERTS.NID_MISS_OPCODE") - except: + except ValueError: return None if (data_rd_loc_occ.name == "UNC_C_TOR_OCCUPANCY.MISS_LOCAL_OPCODE" or @@ -752,8 +747,8 @@ def IntelMissLat() -> Optional[MetricGroup]: def IntelMlp() -> Optional[Metric]: try: l1d = Event("L1D_PEND_MISS.PENDING") - l1dc = Event("L1D_PEND_MISS.PENDING_CYCLES") - except: + l1dc: Expression = Event("L1D_PEND_MISS.PENDING_CYCLES") + except ValueError: return None l1dc = Select(l1dc / 2, Literal("#smt_on"), l1dc) @@ -764,8 +759,9 @@ def IntelMlp() -> Optional[Metric]: def IntelPorts() -> Optional[MetricGroup]: - pipeline_events = json.load( - open(f"{_args.events_path}/x86/{_args.model}/pipeline.json")) + assert _args is not None + with open(f"{_args.events_path}/x86/{_args.model}/pipeline.json", encoding="utf-8") as f: + pipeline_events = json.load(f) core_cycles = Event("CPU_CLK_UNHALTED.THREAD_P_ANY", "CPU_CLK_UNHALTED.DISTRIBUTED", @@ -777,11 +773,10 @@ def IntelPorts() -> Optional[MetricGroup]: for x in pipeline_events: if "EventName" in x and re.search("^UOPS_DISPATCHED.PORT", x["EventName"]): name = x["EventName"] - port = re.search(r"(PORT_[0-9].*)", name).group(0).lower() - if name.endswith("_CORE"): - cyc = core_cycles - else: - cyc = smt_cycles + match = re.search(r"(PORT_[0-9].*)", name) + assert match is not None + port = match.group(0).lower() + cyc: Expression = core_cycles if name.endswith("_CORE") else smt_cycles metrics.append(Metric(f"lpm_{port}", f"{port} utilization (higher is better)", d_ratio(Event(name), cyc), "100%")) if len(metrics) == 0: @@ -800,7 +795,7 @@ def IntelSwpf() -> Optional[MetricGroup]: s_t0 = Event("SW_PREFETCH_ACCESS.T0") s_t1 = Event("SW_PREFETCH_ACCESS.T1_T2") s_w = Event("SW_PREFETCH_ACCESS.PREFETCHW") - except: + except ValueError: return None all_sw = s_nta + s_t0 + s_t1 + s_w @@ -857,6 +852,7 @@ def IntelSwpf() -> Optional[MetricGroup]: def IntelLdSt() -> Optional[MetricGroup]: + assert _args is not None if _args.model in [ "bonnell", "nehalemep", @@ -882,12 +878,12 @@ def IntelLdSt() -> Optional[MetricGroup]: LDST_PRE = None try: LDST_PRE = Event("LOAD_HIT_PREFETCH.SWPF", "LOAD_HIT_PRE.SW_PF") - except: + except ValueError: pass LDST_AT = None try: LDST_AT = Event("MEM_INST_RETIRED.LOCK_LOADS") - except: + except ValueError: pass cyc = LDST_CYC @@ -945,8 +941,8 @@ def UncoreCState() -> Optional[MetricGroup]: pcu_ticks = Event("UNC_P_CLOCKTICKS") c0 = Event("UNC_P_POWER_STATE_OCCUPANCY.CORES_C0") c3 = Event("UNC_P_POWER_STATE_OCCUPANCY.CORES_C3") - c6 = Event("UNC_P_POWER_STATE_OCCUPANCY.CORES_C6") - except: + c6: Expression = Event("UNC_P_POWER_STATE_OCCUPANCY.CORES_C6") + except ValueError: return None num_cores = Literal("#num_cores") / Literal("#num_packages") @@ -981,13 +977,10 @@ def UncoreDir() -> Optional[MetricGroup]: cha_upd = Event("UNC_CHA_DIR_UPDATE.HA") # Turn the umask into a ANY rather than HA filter. cha_upd.name += "/umask=3,name=UNC_CHA_DIR_UPDATE.ANY/" - except: + except ValueError: return None m2m_total = m2m_hits + m2m_miss - upd = m2m_upd + cha_upd # in cache lines - upd_r = upd / interval_sec - look_r = m2m_total / interval_sec scale = 64 / 1_000_000 # Cache lines to MB return MetricGroup("lpm_dir", [ @@ -1014,7 +1007,7 @@ def UncoreMem() -> Optional[MetricGroup]: "UNC_H_REQUESTS.WRITES_LOCAL") rem_wrs = Event("UNC_CHA_REQUESTS.WRITES_REMOTE", "UNC_H_REQUESTS.WRITES_REMOTE") - except: + except ValueError: return None scale = 64 / 1_000_000 @@ -1035,16 +1028,18 @@ def UncoreMem() -> Optional[MetricGroup]: def UncoreMemBw() -> Optional[MetricGroup]: + assert _args is not None mem_events = [] try: - mem_events = json.load(open(f"{os.path.dirname(os.path.realpath(__file__))}" - f"/arch/x86/{args.model}/uncore-memory.json")) - except: + with open(f"{os.path.dirname(os.path.realpath(__file__))}" + f"/arch/x86/{_args.model}/uncore-memory.json", encoding="utf-8") as f: + mem_events = json.load(f) + except (OSError, ValueError): pass - ddr_rds = 0 - ddr_wrs = 0 - ddr_total = 0 + ddr_rds: Union[int, Expression] = 0 + ddr_wrs: Union[int, Expression] = 0 + ddr_total: Union[int, Expression] = 0 for x in mem_events: if "EventName" in x: name = x["EventName"] @@ -1059,17 +1054,17 @@ def UncoreMemBw() -> Optional[MetricGroup]: try: ddr_rds = Event("UNC_M_CAS_COUNT.RD") ddr_wrs = Event("UNC_M_CAS_COUNT.WR") - except: + except ValueError: return None ddr_total = ddr_rds + ddr_wrs - pmm_rds = 0 - pmm_wrs = 0 + pmm_rds: Union[int, Expression] = 0 + pmm_wrs: Union[int, Expression] = 0 try: pmm_rds = Event("UNC_M_PMM_RPQ_INSERTS") pmm_wrs = Event("UNC_M_PMM_WPQ_INSERTS") - except: + except ValueError: pass pmm_total = pmm_rds + pmm_wrs @@ -1101,7 +1096,7 @@ def UncoreMemSat() -> Optional[Metric]: sat = Event("UNC_CHA_DISTRESS_ASSERTED.VERT", "UNC_CHA_FAST_ASSERTED.VERT", "UNC_C_FAST_ASSERTED", "UNC_CHA_DISTRESS_ASSERTED.DPT_ANY", "UNC_CHA_DISTRESS_ASSERTED.DPT_NONLOCAL") - except: + except ValueError: return None desc = ("Mesh Bandwidth saturation (% CBOX cycles with FAST signal asserted, " @@ -1116,11 +1111,9 @@ def UncoreUpiBw() -> Optional[MetricGroup]: try: upi_rds = Event("UNC_UPI_RxL_FLITS.ALL_DATA") upi_wrs = Event("UNC_UPI_TxL_FLITS.ALL_DATA") - except: + except ValueError: return None - upi_total = upi_rds + upi_wrs - # From "Uncore Performance Monitoring": When measuring the amount of # bandwidth consumed by transmission of the data (i.e. NOT including # the header), it should be .ALL_DATA / 9 * 64B. diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py index 296bf68d1081..cc143f7e7cdf 100755 --- a/tools/perf/pmu-events/jevents.py +++ b/tools/perf/pmu-events/jevents.py @@ -3,7 +3,6 @@ """Convert directories of JSON events to C code.""" import argparse import csv -from functools import lru_cache import json import metric import os @@ -22,7 +21,7 @@ _metric_tables: list[str] = [] # List of metric tables generated from "/sys" directories. _sys_metric_tables: list[str] = [] # Mapping between sys event table names and sys metric table names. -_sys_event_table_to_metric_table_mapping = {} +_sys_event_table_to_metric_table_mapping: Dict[str, str] = {} # Map from an event name to an architecture standard # JsonEvent. Architecture standard events are in json files in the top # f'{_args.starting_dir}/{_args.arch}' directory. @@ -38,7 +37,7 @@ _pending_metrics_tblname: Optional[str] = None # Global BigCString shared by all structures. _bcs = None # Map from the name of a metric group to a description of the group. -_metricgroups = {} +_metricgroups: Dict[str, str] = {} # Order specific JsonEvent attributes will be visited. _json_event_attributes = [ # cmp_sevent related attributes. @@ -115,17 +114,17 @@ class BigCString: def __init__(self): self.strings = set() - self.insert_number = 0; + self.insert_number = 0 self.insert_point = {} self.metrics = set() - def add(self, s: str, metric: bool) -> None: + def add(self, s: str, is_metric: bool) -> None: """Called to add to the big string.""" if s not in self.strings: self.strings.add(s) self.insert_point[s] = self.insert_number self.insert_number += 1 - if metric: + if is_metric: self.metrics.add(s) def compute(self) -> None: @@ -303,7 +302,7 @@ class JsonEvent: return int(val, 16) == 0 else: return int(val) == 0 - except: + except ValueError: return False def canonicalize_value(val: str) -> str: @@ -311,7 +310,7 @@ class JsonEvent: if val.startswith('0x'): return llx(int(val, 16)) return str(int(val)) - except: + except ValueError: return val eventcode = 0 @@ -426,15 +425,15 @@ class JsonEvent: s += f'\t{attr} = {value},\n' return s + '}' - def build_c_string(self, metric: bool) -> str: + def build_c_string(self, is_metric: bool) -> str: s = '' - for attr in _json_metric_attributes if metric else _json_event_attributes: + for attr in _json_metric_attributes if is_metric else _json_event_attributes: x = getattr(self, attr) - if metric and x and attr == 'metric_expr': + if is_metric and x and attr == 'metric_expr': # Convert parsed metric expressions into a string. Slashes # must be doubled in the file. x = x.ToPerfJson().replace('\\', '\\\\') - if metric and x and attr == 'metric_threshold': + if is_metric and x and attr == 'metric_threshold': x = x.replace('\\', '\\\\') if attr in _json_enum_attributes: s += x if x else '0' @@ -442,24 +441,25 @@ class JsonEvent: s += f'{x}\\000' if x else '\\000' return s - def to_c_string(self, metric: bool) -> str: + def to_c_string(self, is_metric: bool) -> str: """Representation of the event as a C struct initializer.""" def make_comment(s: str) -> str: s = s.replace('*/', r'\*\/') return f'\t/* {s} */\n' if len(s) < 80 else f'\t/* {s[0:80]}... */\n' - s = self.build_c_string(metric) + s = self.build_c_string(is_metric) assert _bcs is not None return f'{make_comment(s)}\t{{ { _bcs.offsets[s] } }},\n' -_json_cache = {} +_json_cache: Dict[Tuple[str, str], Sequence[JsonEvent]] = {} def _read_json_events_impl(path: str, topic: str) -> Sequence[JsonEvent]: """Read json events from the specified file.""" try: - events = json.load(open(path), object_hook=JsonEvent) - except BaseException as err: + with open(path, encoding='utf-8') as f: + events = json.load(f, object_hook=JsonEvent) + except BaseException: print(f"Exception processing {path}") raise metrics: list[Tuple[str, str, metric.Expression]] = [] @@ -483,7 +483,6 @@ def read_json_events(path: str, topic: str) -> Sequence[JsonEvent]: def preprocess_arch_std_files(archpath: str) -> None: """Read in all architecture standard events.""" - global _arch_std_events for item in os.scandir(archpath): if not item.is_file() or not item.name.endswith('.json'): continue @@ -523,13 +522,10 @@ def print_pending_events() -> None: if not _pending_events: return - global _pending_events_tblname assert _pending_events_tblname is not None if _pending_events_tblname.endswith('_sys'): - global _sys_event_tables _sys_event_tables.append(_pending_events_tblname) else: - global event_tables _event_tables.append(_pending_events_tblname) first = True @@ -550,7 +546,7 @@ def print_pending_events() -> None: last_pmu = event.pmu pmus.add((event.pmu, pmu_name)) - _args.output_file.write(event.to_c_string(metric=False)) + _args.output_file.write(event.to_c_string(is_metric=False)) last_name = event.name _pending_events = [] @@ -586,31 +582,28 @@ def print_pending_metrics() -> None: if not _pending_metrics: return - global _pending_metrics_tblname assert _pending_metrics_tblname is not None if _pending_metrics_tblname.endswith('_sys'): - global _sys_metric_tables _sys_metric_tables.append(_pending_metrics_tblname) else: - global metric_tables _metric_tables.append(_pending_metrics_tblname) first = True last_pmu = None pmus: Set[Tuple[str, str]] = set() assert _args is not None - for metric in sorted(_pending_metrics, key=metric_cmp_key): - if metric.pmu != last_pmu: + for m in sorted(_pending_metrics, key=metric_cmp_key): + if m.pmu != last_pmu: if not first: _args.output_file.write('};\n') - pmu_name = metric.pmu.replace(',', '_') + pmu_name = m.pmu.replace(',', '_') _args.output_file.write( f'static const struct compact_pmu_event {_pending_metrics_tblname}_{pmu_name}[] = {{\n') first = False - last_pmu = metric.pmu - pmus.add((metric.pmu, pmu_name)) + last_pmu = m.pmu + pmus.add((m.pmu, pmu_name)) - _args.output_file.write(metric.to_c_string(metric=True)) + _args.output_file.write(m.to_c_string(is_metric=True)) _pending_metrics = [] _args.output_file.write(f""" @@ -649,13 +642,14 @@ def preprocess_one_file(parents: Sequence[str], item: os.DirEntry) -> None: assert _bcs is not None if item.name.endswith('metricgroups.json'): - metricgroup_descriptions = json.load(open(item.path)) + with open(item.path, encoding='utf-8') as f: + metricgroup_descriptions = json.load(f) for mgroup in metricgroup_descriptions: assert len(mgroup) > 1, parents description = f"{metricgroup_descriptions[mgroup]}\\000" mgroup = f"{mgroup}\\000" - _bcs.add(mgroup, metric=True) - _bcs.add(description, metric=True) + _bcs.add(mgroup, is_metric=True) + _bcs.add(description, is_metric=True) _metricgroups[mgroup] = description return @@ -663,11 +657,12 @@ def preprocess_one_file(parents: Sequence[str], item: os.DirEntry) -> None: for event in read_json_events(item.path, topic): pmu_name = f"{event.pmu}\\000" if event.name: - _bcs.add(pmu_name, metric=False) - _bcs.add(event.build_c_string(metric=False), metric=False) + _bcs.add(pmu_name, is_metric=False) + _bcs.add(event.build_c_string(is_metric=False), is_metric=False) if event.metric_name: - _bcs.add(pmu_name, metric=True) - _bcs.add(event.build_c_string(metric=True), metric=True) + pmu_name = f"{event.pmu}\\000" + _bcs.add(pmu_name, is_metric=True) + _bcs.add(event.build_c_string(is_metric=True), is_metric=True) def process_one_file(parents: Sequence[str], item: os.DirEntry) -> None: """Process a JSON file during the main walk.""" @@ -777,7 +772,7 @@ static const struct pmu_events_map pmu_events_map[] = { }, """) else: - with open(f'{_args.starting_dir}/{arch}/mapfile.csv') as csvfile: + with open(f'{_args.starting_dir}/{arch}/mapfile.csv', encoding='utf-8') as csvfile: table = csv.reader(csvfile) first = True for row in table: @@ -871,8 +866,8 @@ int pmu_metrics_table__iterate_tables(pmu_metrics_table_iter_t fn, void *data) def print_system_mapping_table() -> None: - assert _args is not None """C struct mapping table array for tables from /sys directories.""" + assert _args is not None _args.output_file.write(""" struct pmu_sys_events { \tconst char *name; @@ -1481,7 +1476,7 @@ def main() -> None: except Exception as e: raise RuntimeError(f'Action failure for \'{item.name}\' in {parents}') from e if item.is_dir(): - ftw(item.path, parents + [item.name], action) + ftw(item.path, list(parents) + [item.name], action) ap = argparse.ArgumentParser() ap.add_argument('arch', help='Architecture name like x86') diff --git a/tools/perf/pmu-events/make_legacy_cache.py b/tools/perf/pmu-events/make_legacy_cache.py index 28a1ff804f86..b224544f85f6 100755 --- a/tools/perf/pmu-events/make_legacy_cache.py +++ b/tools/perf/pmu-events/make_legacy_cache.py @@ -61,7 +61,7 @@ hw_cache_result = [ "misses"), ] -events = [] +events: list[dict[str, str]] = [] def add_event(name: str, cache_id: int, cache_op: int, cache_result: int, desc: str, @@ -87,10 +87,10 @@ def add_event(name: str, event["Deprecated"] = "1" events.append(event) -for (cache_id, names, ops, cache_desc) in hw_cache_id: - for name in names: - add_event(name, - cache_id, +for (cid, names, ops, cache_desc) in hw_cache_id: + for cname in names: + add_event(cname, + cid, 0, # PERF_COUNT_HW_CACHE_OP_READ 0, # PERF_COUNT_HW_CACHE_RESULT_ACCESS f"{cache_desc} read accesses.", @@ -100,27 +100,29 @@ for (cache_id, names, ops, cache_desc) in hw_cache_id: if op not in ops: continue for op_name in op_names: - deprecated = (names[0] != name or op_names[1] != op_name) - add_event(f"{name}-{op_name}", - cache_id, + is_deprecated = (names[0] != cname or op_names[1] != op_name) + add_event(f"{cname}-{op_name}", + cid, op, 0, # PERF_COUNT_HW_CACHE_RESULT_ACCESS f"{cache_desc} {op_desc} accesses.", - deprecated) + is_deprecated) for (result, result_names, result_desc) in hw_cache_result: for result_name in result_names: - deprecated = ((names[0] != name or op_names[0] != op_name) or - (result == 0) or (result_names[0] != result_name)) - add_event(f"{name}-{op_name}-{result_name}", - cache_id, op, result, + is_deprecated = ((names[0] != cname or op_names[0] != op_name) or + (result == 0) or (result_names[0] != result_name)) + add_event(f"{cname}-{op_name}-{result_name}", + cid, + op, + result, f"{cache_desc} {op_desc} {result_desc}.", - deprecated) + is_deprecated) for (result, result_names, result_desc) in hw_cache_result: for result_name in result_names: - add_event(f"{name}-{result_name}", - cache_id, + add_event(f"{cname}-{result_name}", + cid, 0, # PERF_COUNT_HW_CACHE_OP_READ result, f"{cache_desc} read {result_desc}.", diff --git a/tools/perf/pmu-events/metric.py b/tools/perf/pmu-events/metric.py index ce025675898c..f5d81eccd5c3 100644 --- a/tools/perf/pmu-events/metric.py +++ b/tools/perf/pmu-events/metric.py @@ -6,18 +6,16 @@ import json import os import re from enum import Enum -from typing import Dict, List, Optional, Set, Tuple, Union +from typing import Dict, List, Optional, Sequence, Set, Tuple, Union -all_pmus = set() -all_events = set() -experimental_events = set() -all_events_all_models = set() +all_pmus: Set[str] = set() +all_events: Set[str] = set() +experimental_events: Set[str] = set() +all_events_all_models: Set[str] = set() def LoadEvents(directory: str) -> None: """Populate a global set of all known events for the purpose of validating Event names""" - global all_pmus global all_events - global experimental_events global all_events_all_models all_events = { "context\\-switches", @@ -30,29 +28,31 @@ def LoadEvents(directory: str) -> None: filename = os.fsdecode(file) if filename.endswith(".json"): try: - for x in json.load(open(f"{directory}/{filename}")): - if "Unit" in x: - all_pmus.add(x["Unit"]) - if "EventName" in x: - all_events.add(x["EventName"]) - if "Experimental" in x and x["Experimental"] == "1": - experimental_events.add(x["EventName"]) - elif "ArchStdEvent" in x: - all_events.add(x["ArchStdEvent"]) + with open(f"{directory}/{filename}", encoding="utf-8") as f: + for x in json.load(f): + if "Unit" in x: + all_pmus.add(x["Unit"]) + if "EventName" in x: + all_events.add(x["EventName"]) + if "Experimental" in x and x["Experimental"] == "1": + experimental_events.add(x["EventName"]) + elif "ArchStdEvent" in x: + all_events.add(x["ArchStdEvent"]) except json.decoder.JSONDecodeError: # The generated directory may be the same as the input, which # causes partial json files. Ignore errors. pass all_events_all_models = all_events.copy() - for root, dirs, files in os.walk(directory + ".."): + for root, _, files in os.walk(directory + ".."): for filename in files: if filename.endswith(".json"): try: - for x in json.load(open(f"{root}/{filename}")): - if "EventName" in x: - all_events_all_models.add(x["EventName"]) - elif "ArchStdEvent" in x: - all_events_all_models.add(x["ArchStdEvent"]) + with open(f"{root}/{filename}", encoding="utf-8") as f: + for x in json.load(f): + if "EventName" in x: + all_events_all_models.add(x["EventName"]) + elif "ArchStdEvent" in x: + all_events_all_models.add(x["ArchStdEvent"]) except json.decoder.JSONDecodeError: # The generated directory may be the same as the input, which # causes partial json files. Ignore errors. @@ -65,7 +65,6 @@ def CheckPmu(name: str) -> bool: def CheckEvent(name: str) -> bool: """Check the event name exists in the set of all loaded events""" - global all_events if len(all_events) == 0: # No events loaded so assume any event is good. return True @@ -81,7 +80,6 @@ def CheckEvent(name: str) -> bool: def CheckEveryEvent(*names: str) -> None: """Check all the events exist in at least one json file""" - global all_events_all_models if len(all_events_all_models) == 0: assert len(names) == 1, f"Cannot determine valid events in {names}" # No events loaded so assume any event is good. @@ -96,11 +94,10 @@ def CheckEveryEvent(*names: str) -> None: if any([name.startswith(x) for x in ['amd', 'arm', 'cpu', 'msr', 'power', 'cha', 'uncore']]): continue if name not in all_events_all_models: - raise Exception(f"Is {name} a named json event?") + raise ValueError(f"Is {name} a named json event?") def IsExperimentalEvent(name: str) -> bool: - global experimental_events if ':' in name: # Remove trailing modifier. name = name[:name.find(':')] @@ -447,14 +444,13 @@ class Event(Expression): error += " or " + name else: error = name - global all_events - raise Exception(f"No event {error} in:\n{all_events}") + raise ValueError(f"No event {error} in:\n{all_events}") def HasExperimentalEvents(self) -> bool: return IsExperimentalEvent(self.name) def ToPerfJson(self): - result = re.sub('/', '@', self.name) + result = re.sub('/', '@', _FixEscapes(self.name)) return result def ToPython(self): @@ -655,7 +651,7 @@ class Metric: return result - def ToMetricGroupDescriptions(self, root: bool = True) -> Dict[str, str]: + def ToMetricGroupDescriptions(self, _root: bool = True) -> Dict[str, str]: return {} class MetricGroup: @@ -667,7 +663,7 @@ class MetricGroup: """ def __init__(self, name: str, - metric_list: List[Union[Optional[Metric], Optional['MetricGroup']]], + metric_list: Sequence[Union[Optional[Metric], Optional['MetricGroup']]], description: Optional[str] = None): self.name = name self.metric_list = [] @@ -696,7 +692,7 @@ class MetricGroup: result.append(x.ToPerfJson()) return result - def ToMetricGroupDescriptions(self, root: bool = True) -> Dict[str, str]: + def ToMetricGroupDescriptions(self, _root: bool = True) -> Dict[str, str]: result = {self.name: self.description} if self.description else {} for x in self.metric_list: result.update(x.ToMetricGroupDescriptions(False)) From 1331150d7d68f447fce2e209f316a1f83c5fe9f7 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:39 -0700 Subject: [PATCH 187/425] perf test: Clean up mypy and pylint issues in shell test libraries Clean up mypy type errors and pylint errors/warnings in tools/perf/tests/shell/lib/ (attr.py, perf_json_output_lint.py, and perf_metric_validation.py): - Fix E0606 (possibly-used-before-assignment) in perf_metric_validation.py by importing sys at module level. - Add type annotations for stack, second_results, collectlist, get_bounds(), and main() in perf_metric_validation.py. - Avoid assigning -1 to list[int] expected_items in perf_json_output_lint.py, rename shadowing variables, and remove redundant lambdas. - Remove unnecessary semicolons, use lazy logging arguments, specify exception types and file encodings, and initialize module-level logger in attr.py. Also update tools/perf/tests/shell/lib/setup_python.sh to: - Avoid x-prefix string comparisons. - Export PYTHON and resolve PYTHONPATH and PERF_EXEC_PATH from BASH_SOURCE[0], ../../../python (for nested test directories such as coresight/), and the directory containing the perf binary so out-of-tree builds and standalone perf python scripts work seamlessly. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/tests/shell/lib/attr.py | 86 ++++++++++--------- .../tests/shell/lib/perf_json_output_lint.py | 36 ++++---- .../tests/shell/lib/perf_metric_validation.py | 53 ++++++------ tools/perf/tests/shell/lib/setup_python.sh | 29 ++++++- 4 files changed, 113 insertions(+), 91 deletions(-) diff --git a/tools/perf/tests/shell/lib/attr.py b/tools/perf/tests/shell/lib/attr.py index bfccc727d9b2..7f3d5b64b00d 100644 --- a/tools/perf/tests/shell/lib/attr.py +++ b/tools/perf/tests/shell/lib/attr.py @@ -12,6 +12,8 @@ import re import shutil import subprocess +log = logging.getLogger('test') + def data_equal(a, b): # Allow multiple values in assignment separated by '|' a_list = a.split('|') @@ -89,19 +91,19 @@ class Event(dict): def add(self, data): for key, val in data: - log.debug(" %s = %s" % (key, val)) + log.debug(" %s = %s", key, val) self[key] = val def __init__(self, name, data, base): - log.debug(" Event %s" % name); - self.name = name; + log.debug(" Event %s", name) + self.name = name self.group = '' self.add(base) self.add(data) def equal(self, other): for t in Event.terms: - log.debug(" [%s] %s %s" % (t, self[t], other[t])); + log.debug(" [%s] %s %s", t, self[t], other[t]) if t not in self or t not in other: return False if not data_equal(self[t], other[t]): @@ -118,7 +120,7 @@ class Event(dict): if t not in self or t not in other: continue if not data_equal(self[t], other[t]): - log.warning("expected %s=%s, got %s" % (t, self[t], other[t])) + log.warning("expected %s=%s, got %s", t, self[t], other[t]) def parse_version(version): if not version: @@ -149,7 +151,7 @@ class Test(object): parser = configparser.ConfigParser() parser.read(path) - log.warning("running '%s'" % path) + log.warning("running '%s'", path) self.path = path self.test_dir = options.test_dir @@ -159,15 +161,15 @@ class Test(object): try: self.ret = parser.get('config', 'ret') - except: + except Exception: self.ret = 0 self.test_ret = parser.getboolean('config', 'test_ret', fallback=False) try: self.arch = parser.get('config', 'arch') - log.warning("test limitation '%s'" % self.arch) - except: + log.warning("test limitation '%s'", self.arch) + except Exception: self.arch = '' self.auxv = parser.get('config', 'auxv', fallback=None) @@ -175,7 +177,7 @@ class Test(object): self.kernel_until = parse_version(parser.get('config', 'kernel_until', fallback=None)) self.expect = {} self.result = {} - log.debug(" loading expected events"); + log.debug(" loading expected events") self.load_events(path, self.expect) def is_event(self, name): @@ -203,7 +205,7 @@ class Test(object): else: try: value = int(items[-1], 0) - except: + except ValueError: value = items[-1] return (items[0], value) @@ -227,7 +229,7 @@ class Test(object): # Handle negated list such as !s390x,ppc if arch_list[0][0] == '!': arch_list[0] = arch_list[0][1:] - log.warning("excluded architecture list %s" % arch_list) + log.warning("excluded architecture list %s", arch_list) for arch_item in arch_list: # log.warning("test for %s arch is %s" % (arch_item, myarch)) if arch_item == myarch: @@ -243,19 +245,19 @@ class Test(object): def restore_sample_rate(self, value=10000): try: # Check value of sample_rate - with open("/proc/sys/kernel/perf_event_max_sample_rate", "r") as fIn: + with open("/proc/sys/kernel/perf_event_max_sample_rate", "r", encoding="utf-8") as fIn: curr_value = fIn.readline() # If too low restore to reasonable value if not curr_value or int(curr_value) < int(value): - with open("/proc/sys/kernel/perf_event_max_sample_rate", "w") as fOut: + with open("/proc/sys/kernel/perf_event_max_sample_rate", "w", encoding="utf-8") as fOut: fOut.write(str(value)) except IOError as e: - log.warning("couldn't restore sample_rate value: I/O error %s" % e) + log.warning("couldn't restore sample_rate value: I/O error %s", e) except ValueError as e: - log.warning("couldn't restore sample_rate value: Value error %s" % e) + log.warning("couldn't restore sample_rate value: Value error %s", e) except TypeError as e: - log.warning("couldn't restore sample_rate value: Type error %s" % e) + log.warning("couldn't restore sample_rate value: Type error %s", e) def load_events(self, path, events): parser_event = configparser.ConfigParser() @@ -266,7 +268,7 @@ class Test(object): # event' first as a base for section in filter(self.is_event, parser_event.sections()): - parser_items = parser_event.items(section); + parser_items = parser_event.items(section) base_items = {} # Read parent event if there's any @@ -280,7 +282,7 @@ class Test(object): events[section] = e def run_cmd(self, tempdir): - junk1, junk2, junk3, junk4, myarch = (os.uname()) + _junk1, _junk2, _junk3, _junk4, myarch = (os.uname()) if self.skip_test_arch(myarch): raise Notest(self, myarch) @@ -299,7 +301,7 @@ class Test(object): self.perf, self.command, tempdir, self.args) ret = os.WEXITSTATUS(os.system(cmd)) - log.info(" '%s' ret '%s', expected '%s'" % (cmd, str(ret), str(self.ret))) + log.info(" '%s' ret '%s', expected '%s'", cmd, str(ret), str(self.ret)) if not data_equal(str(ret), str(self.ret)): if self.test_ret: @@ -310,34 +312,34 @@ class Test(object): def compare(self, expect, result): match = {} - log.debug(" compare"); + log.debug(" compare") # For each expected event find all matching # events in result. Fail if there's not any. for exp_name, exp_event in expect.items(): exp_list = [] res_event = {} - log.debug(" matching [%s]" % exp_name) + log.debug(" matching [%s]", exp_name) for res_name, res_event in result.items(): - log.debug(" to [%s]" % res_name) + log.debug(" to [%s]", res_name) if (exp_event.equal(res_event)): exp_list.append(res_name) log.debug(" ->OK") else: - log.debug(" ->FAIL"); + log.debug(" ->FAIL") - log.debug(" match: [%s] matches %s" % (exp_name, str(exp_list))) + log.debug(" match: [%s] matches %s", exp_name, str(exp_list)) # we did not any matching event - fail if not exp_list: if exp_event.optional(): - log.debug(" %s does not match, but is optional" % exp_name) + log.debug(" %s does not match, but is optional", exp_name) else: if not res_event: - log.debug(" res_event is empty"); + log.debug(" res_event is empty") else: exp_event.diff(res_event) - raise Fail(self, 'match failure'); + raise Fail(self, 'match failure') match[exp_name] = exp_list @@ -354,38 +356,38 @@ class Test(object): if res_group not in match[group]: raise Fail(self, 'group failure') - log.debug(" group: [%s] matches group leader %s" % - (exp_name, str(match[group]))) + log.debug(" group: [%s] matches group leader %s", + exp_name, str(match[group])) log.debug(" matched") def resolve_groups(self, events): for name, event in events.items(): - group_fd = event['group_fd']; + group_fd = event['group_fd'] if group_fd == '-1': - continue; + continue for iname, ievent in events.items(): if (ievent['fd'] == group_fd): event.group = iname - log.debug('[%s] has group leader [%s]' % (name, iname)) - break; + log.debug('[%s] has group leader [%s]', name, iname) + break def run(self): - tempdir = tempfile.mkdtemp(); + tempdir = tempfile.mkdtemp() try: # run the test script - self.run_cmd(tempdir); + self.run_cmd(tempdir) # load events expectation for the test - log.debug(" loading result events"); + log.debug(" loading result events") for f in glob.glob(tempdir + '/event*'): - self.load_events(f, self.result); + self.load_events(f, self.result) # resolve group_fd to event names - self.resolve_groups(self.expect); - self.resolve_groups(self.result); + self.resolve_groups(self.expect) + self.resolve_groups(self.result) # do the expectation - results matching - both ways self.compare(self.expect, self.result) @@ -401,9 +403,9 @@ def run_tests(options): try: Test(f, options).run() except Unsup as obj: - log.warning("unsupp %s" % obj.getMsg()) + log.warning("unsupp %s", obj.getMsg()) except Notest as obj: - log.warning("skipped %s" % obj.getMsg()) + log.warning("skipped %s", obj.getMsg()) def setup_log(verbose): global log diff --git a/tools/perf/tests/shell/lib/perf_json_output_lint.py b/tools/perf/tests/shell/lib/perf_json_output_lint.py index dafbde56cc76..8cde22ac9d97 100644 --- a/tools/perf/tests/shell/lib/perf_json_output_lint.py +++ b/tools/perf/tests/shell/lib/perf_json_output_lint.py @@ -46,46 +46,46 @@ def is_counter_value(num): def is_metric_value(num): return isfloat(num) or num == 'none' -def check_json_output(expected_items): +def check_json_output(expected_count): checks = { - 'counters': lambda x: isfloat(x), + 'counters': isfloat, 'core': lambda x: True, - 'counter-value': lambda x: is_counter_value(x), + 'counter-value': is_counter_value, 'cgroup': lambda x: True, - 'cpu': lambda x: isint(x), + 'cpu': isint, 'cache': lambda x: True, 'cluster': lambda x: True, 'die': lambda x: True, 'event': lambda x: True, - 'event-runtime': lambda x: isfloat(x), - 'interval': lambda x: isfloat(x), + 'event-runtime': isfloat, + 'interval': isfloat, 'metric-unit': lambda x: True, - 'metric-value': lambda x: is_metric_value(x), + 'metric-value': is_metric_value, 'metric-threshold': lambda x: x in ['unknown', 'good', 'less good', 'nearly bad', 'bad'], 'metricgroup': lambda x: True, 'node': lambda x: True, - 'pcnt-running': lambda x: isfloat(x), + 'pcnt-running': isfloat, 'socket': lambda x: True, 'thread': lambda x: True, 'unit': lambda x: True, } - input = '[\n' + ','.join(Lines) + '\n]' - for item in json.loads(input): - if expected_items != -1: + json_input = '[\n' + ','.join(Lines) + '\n]' + for item in json.loads(json_input): + if expected_count: count = len(item) - if count not in expected_items and count >= 1 and count <= 7 and 'metric-value' in item: + if count not in expected_count and count >= 1 and count <= 7 and 'metric-value' in item: # Events that generate >1 metric may have isolated metric # values and possibly other prefixes like interval, core, # counters, or event-runtime/pcnt-running from multiplexing. pass - elif count not in expected_items and count >= 1 and count <= 5 and 'metricgroup' in item: + elif count not in expected_count and count >= 1 and count <= 5 and 'metricgroup' in item: pass - elif count - 1 in expected_items and 'metric-threshold' in item: + elif count - 1 in expected_count and 'metric-threshold' in item: pass - elif count in expected_items and 'insn per cycle' in item: + elif count in expected_count and 'insn per cycle' in item: pass - elif count not in expected_items: - raise RuntimeError(f'wrong number of fields. counted {count} expected {expected_items}' + elif count not in expected_count: + raise RuntimeError(f'wrong number of fields. counted {count} expected {expected_count}' f' in \'{item}\'') for key, value in item.items(): if key not in checks: @@ -107,7 +107,7 @@ try: expected_items = [1, 2] else: # If no option is specified, don't check the number of items. - expected_items = -1 + expected_items = [] check_json_output(expected_items) except: print('Test failed for input:\n' + '\n'.join(Lines)) diff --git a/tools/perf/tests/shell/lib/perf_metric_validation.py b/tools/perf/tests/shell/lib/perf_metric_validation.py index 3d52f94f22b9..dff60337c030 100644 --- a/tools/perf/tests/shell/lib/perf_metric_validation.py +++ b/tools/perf/tests/shell/lib/perf_metric_validation.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: GPL-2.0 -import re -import csv -import json import argparse +import json from pathlib import Path import subprocess +import sys +from typing import Any class TestError: @@ -77,11 +77,11 @@ class Validator: def read_json(self, filename: str) -> dict: try: - with open(Path(filename).resolve(), "r") as f: + with open(Path(filename).resolve(), "r", encoding="utf-8") as f: data = json.loads(f.read()) except OSError as e: print(f"Error when reading file {e}") - sys.exit() + sys.exit(1) return data @@ -90,16 +90,16 @@ class Validator: if not parent.exists(): parent.mkdir(parents=True) - with open(output_file, "w+") as output_file: + with open(output_file, "w+", encoding="utf-8") as out_f: json.dump(data, - output_file, + out_f, ensure_ascii=True, indent=4) def get_results(self, idx: int = 0): return self.results.get(idx) - def get_bounds(self, lb, ub, error, alias={}, ridx: int = 0) -> list: + def get_bounds(self, lb, ub, error, alias=None, ridx: int = 0) -> tuple: """ Get bounds and tolerance from lb, ub, and error. If missing lb, use 0.0; missing ub, use float('inf); missing error, use self.tolerance. @@ -111,6 +111,9 @@ class Validator: upper bound, return -1 if the upper bound is a metric value and is not collected tolerance, denormalized base on upper bound value """ + if alias is None: + alias = {} + # init ubv and lbv to invalid values def get_bound_value(bound, initval, ridx): val = initval @@ -120,7 +123,7 @@ class Validator: if bound == '': val = float("inf") elif bound in alias: - vall = self.get_value(alias[ub], ridx) + vall = self.get_value(alias[bound], ridx) if vall: val = vall[0] elif bound.replace('.', '1').isdigit(): @@ -213,7 +216,7 @@ class Validator: @param alias: the dict has alias to metric name mapping @returns: value of the formula is success; -1 if the one or more metric value not provided """ - stack = [] + stack: list[Any] = [] b = 0 errs = [] sign = "+" @@ -230,9 +233,9 @@ class Validator: else: f = f + "{0}(={1:.4f})".format(s, v[0]) if sign == "*": - stack[-1] = stack[-1] * v + stack[-1] = stack[-1] * v[0] elif sign == "/": - stack[-1] = stack[-1] / v + stack[-1] = stack[-1] / v[0] elif sign == '-': stack.append(-v[0]) else: @@ -263,7 +266,7 @@ class Validator: alias[m['Alias']] = m['Name'] lbv, ubv, t = self.get_bounds( rule['RangeLower'], rule['RangeUpper'], rule['ErrorThreshold'], alias, ridx=rule['RuleIndex']) - val, f = self.evaluate_formula( + val, _ = self.evaluate_formula( rule['Formula'], alias, ridx=rule['RuleIndex']) lb = rule['RangeLower'] @@ -316,7 +319,7 @@ class Validator: rerun.append(m['Name']) if len(rerun) > 0 and len(rerun) < 20: - second_results = dict() + second_results: dict[Any, Any] = dict() self.second_test(rerun, second_results) for name, val in second_results.items(): if name not in failures: @@ -373,7 +376,7 @@ class Validator: name = result["metric-unit"].split(" ")[1] if len(result["metric-unit"].split(" ")) > 1 \ else result["metric-unit"] metricvalues[name.lower()] = float(result["metric-value"]) - except ValueError as error: + except ValueError: continue return @@ -383,7 +386,7 @@ class Validator: wl = workload.split() command.extend(wl) print(" ".join(command)) - cmd = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + cmd = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', check=False) lines = cmd.stderr.splitlines() + cmd.stdout.splitlines() data = [] for line in lines: @@ -397,9 +400,9 @@ class Validator: Collect metric data with "perf stat -M" on given workload with -a and -j. """ self.results = dict() - print(f"Starting perf collection") + print("Starting perf collection") print(f"Long workload: {workload}") - collectlist = dict() + collectlist: dict[int, Any] = dict() if self.collectlist != "": collectlist[0] = {x for x in self.collectlist.split(",")} else: @@ -441,7 +444,7 @@ class Validator: """ command = ['perf', 'list', '-j', '--details', 'metrics'] cmd = subprocess.run(command, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, encoding='utf-8') + stderr=subprocess.PIPE, encoding='utf-8', check=False) try: data = json.loads(cmd.stdout) for m in data: @@ -454,9 +457,9 @@ class Validator: self.metrics.add(name) if 'ScaleUnit' in m and (m['ScaleUnit'] == '1%' or m['ScaleUnit'] == '100%'): self.pctgmetrics.add(name.lower()) - except ValueError as error: - print(f"Error when parsing metric data") - sys.exit() + except ValueError: + print("Error when parsing metric data") + sys.exit(1) return @@ -519,9 +522,6 @@ class Validator: # Initialize data structures before data validation of each workload def _init_data(self): - - testtypes = ['PositiveValueTest', - 'RelationshipTest', 'SingleMetricTest'] self.results = dict() self.ignoremetrics = set() self.errlist = list() @@ -572,7 +572,7 @@ class Validator: # End of Class Validator -def main() -> None: +def main() -> int: parser = argparse.ArgumentParser( description="Launch metric value validation") @@ -602,5 +602,4 @@ def main() -> None: if __name__ == "__main__": - import sys sys.exit(main()) diff --git a/tools/perf/tests/shell/lib/setup_python.sh b/tools/perf/tests/shell/lib/setup_python.sh index 2173215a0517..758834f8dcf8 100644 --- a/tools/perf/tests/shell/lib/setup_python.sh +++ b/tools/perf/tests/shell/lib/setup_python.sh @@ -1,29 +1,50 @@ #!/bin/bash # SPDX-License-Identifier: GPL-2.0 -if [ "x$PYTHON" = "x" ] +if [ -z "$PYTHON" ] then python3 --version >/dev/null 2>&1 && PYTHON=python3 fi -if [ "x$PYTHON" = "x" ] +if [ -z "$PYTHON" ] then python --version >/dev/null 2>&1 && PYTHON=python fi -if [ "x$PYTHON" = "x" ] +if [ -z "$PYTHON" ] then echo Skipping test, python not detected please set environment variable PYTHON. exit 2 fi +export PYTHON -# Set PYTHONPATH to find the in-tree built perf.so first, avoiding system-wide perf.so +# Set PYTHONPATH to find the built perf.so and standalone scripts first, +# avoiding system-wide perf.so if [ -n "$PERF_EXEC_PATH" ] && [ -d "$PERF_EXEC_PATH/python" ]; then PYTHONPATH_DIR="$PERF_EXEC_PATH/python" +elif [ -n "${BASH_SOURCE[0]}" ] && [ -d "$(dirname "${BASH_SOURCE[0]}")/../../../python" ]; then + PYTHONPATH_DIR="$(dirname "${BASH_SOURCE[0]}")/../../../python" +elif [ -d "$(dirname "$0")/../../../python" ]; then + PYTHONPATH_DIR="$(dirname "$0")/../../../python" elif [ -d "$(dirname "$0")/../../python" ]; then PYTHONPATH_DIR="$(dirname "$0")/../../python" elif [ -d "$(dirname "$0")/../python" ]; then PYTHONPATH_DIR="$(dirname "$0")/../python" fi +if [ -n "${BASH_SOURCE[0]}" ] && [ -d "$(dirname "${BASH_SOURCE[0]}")/../../../python" ]; then + SRC_PYTHON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../python" && pwd)" + export PYTHONPATH="$SRC_PYTHON_DIR${PYTHONPATH:+:$PYTHONPATH}" + if [ -z "$PERF_EXEC_PATH" ] || [ ! -f "$PERF_EXEC_PATH/python/perf_live.py" ]; then + PERF_EXEC_PATH="$(dirname "$SRC_PYTHON_DIR")" + export PERF_EXEC_PATH + fi +fi + if [ -n "$PYTHONPATH_DIR" ]; then export PYTHONPATH="$PYTHONPATH_DIR${PYTHONPATH:+:$PYTHONPATH}" fi + +PERF_BIN=$(which perf 2>/dev/null || true) +if [ -n "$PERF_BIN" ] && [ -d "$(dirname "$PERF_BIN")/python" ]; then + PERF_BIN_PYTHON="$(dirname "$PERF_BIN")/python" + export PYTHONPATH="$PERF_BIN_PYTHON${PYTHONPATH:+:$PYTHONPATH}" +fi From b3e9c396d0acab27e6fd52edbb67f01ef0465782 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:40 -0700 Subject: [PATCH 188/425] perf build: Make mypy build test opt-out (NO_MYPY=1) Commit 168910d0f937 ("perf build: Add mypy build tests") added a compile-time mypy type check for Python files in tools/perf, but left it opt-in via MYPY=1. Now that mypy issues across tools/perf/python, tools/perf/pmu-events, and tools/perf/tests/shell/lib have been cleaned up, enable the mypy check by default when mypy is installed on the system, with an opt-out via NO_MYPY=1 (matching NO_SHELLCHECK=1 and other perf build feature flags). Also configure MYPYPATH in Makefile.perf so scripts can resolve perf.pyi type stubs and local modules, pass --ignore-missing-imports so optional third-party packages do not fail default builds, and remove *.mypy_log files during `make clean`. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Build | 2 +- tools/perf/Makefile.perf | 26 ++++++++++++++++++++++---- tools/perf/pmu-events/Build | 2 +- tools/perf/tests/Build | 2 +- tools/perf/util/Build | 2 +- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/tools/perf/Build b/tools/perf/Build index e18c80a5c1bc..37d603faea6d 100644 --- a/tools/perf/Build +++ b/tools/perf/Build @@ -88,7 +88,7 @@ endif $(OUTPUT)%.mypy_log: % $(call rule_mkdir) - $(Q)$(call echo-cmd,test)mypy "$<" > $@ || (cat $@ && rm $@ && false) + $(Q)$(call echo-cmd,test)$(MYPY) "$<" > $@ || (cat $@ && rm $@ && false) perf-y += $(MYPY_TEST_LOGS) diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf index e91d6138330e..fbb5dfb67089 100644 --- a/tools/perf/Makefile.perf +++ b/tools/perf/Makefile.perf @@ -126,6 +126,8 @@ include ../scripts/utilities.mak # Define GEN_VMLINUX_H to generate vmlinux.h from the BTF. # # Define NO_SHELLCHECK if you do not want to run shellcheck during build +# +# Define NO_MYPY if you do not want to run mypy during build # As per kernel Makefile, avoid funny character set dependencies unexport LC_ALL @@ -259,9 +261,24 @@ ifneq ($(SHELLCHECK),) endif endif -# Runs mypy on perf python files -ifeq ($(MYPY),1) - MYPY := $(shell which mypy 2> /dev/null) +# Runs mypy on perf python files. +# Wait until after the fixdep bootstrap sub-make (force_fixdep != 1) so +# command-line overrides (MYPY=1, MYPY=/custom/path, NO_MYPY=1) reach the +# main Makefile.perf pass intact, then strip MYPY=% from MAKEOVERRIDES +# (substituting escaped spaces so multi-word overrides like MYPY="mypy --strict" +# are removed as a single token) so child Makefile.build sub-makes inherit the +# MYPYPATH-wrapped export MYPY environment variable. +ifneq ($(force_fixdep),1) + MAKEOVERRIDES := $(subst __SPACE__,\ ,$(filter-out MYPY=%,$(subst \ ,__SPACE__,$(MAKEOVERRIDES)))) + ifeq ($(NO_MYPY),1) + override MYPY := + else ifeq ($(filter-out 1,$(MYPY)),) + override MYPY := $(shell which mypy 2> /dev/null) + endif + + ifneq ($(MYPY),) + override MYPY := MYPYPATH=$(srctree)/tools/perf/python $(MYPY) --ignore-missing-imports + endif endif # Runs pylint on perf python files @@ -945,7 +962,8 @@ clean:: $(LIBAPI)-clean $(LIBBPF)-clean $(LIBSUBCMD)-clean $(LIBSYMBOL)-clean $( $(call QUIET_CLEAN, core-objs) $(RM) $(LIBPERF_A) $(OUTPUT)perf-archive \ $(OUTPUT)perf-iostat $(LANG_BINDINGS) $(Q)find $(or $(OUTPUT),.) -name '*.o' -delete -o -name '*.a' -delete -o \ - -name '\.*.cmd' -delete -o -name '\.*.d' -delete -o -name '*.shellcheck_log' -delete + -name '\.*.cmd' -delete -o -name '\.*.d' -delete -o -name '*.shellcheck_log' -delete -o \ + -name '*.mypy_log' -delete $(Q)$(RM) $(OUTPUT).config-detected $(call QUIET_CLEAN, core-progs) $(RM) $(ALL_PROGRAMS) perf perf-read-vdso32 \ perf-read-vdsox32 $(OUTPUT)$(LIBJVMTI).so diff --git a/tools/perf/pmu-events/Build b/tools/perf/pmu-events/Build index 372773b998e4..c3542e5ae46f 100644 --- a/tools/perf/pmu-events/Build +++ b/tools/perf/pmu-events/Build @@ -187,7 +187,7 @@ JEVENTS_DEPS := $(OUT_JSON) $(GEN_JSON) $(JEVENTS_PY) $(METRIC_PY) $(EMPTY_PMU_E ifdef MYPY define MYPY_RULE $(2): $(1) - $$(Q)$$(call echo-cmd,test)mypy $(1) > $(2) || (cat $(2) && rm $(2) && false) + $$(Q)$$(call echo-cmd,test)$$(MYPY) $(1) > $(2) || (cat $(2) && rm $(2) && false) endef $(foreach src,$(wildcard pmu-events/*.py), \ $(eval dest := $(patsubst pmu-events/%,$(OUTPUT)pmu-events/%.mypy_log,$(src))) \ diff --git a/tools/perf/tests/Build b/tools/perf/tests/Build index 81c311b131b7..cd7b62a6c1b8 100644 --- a/tools/perf/tests/Build +++ b/tools/perf/tests/Build @@ -104,7 +104,7 @@ endif $(OUTPUT)%.mypy_log: % $(call rule_mkdir) - $(Q)$(call echo-cmd,test)mypy "$<" > $@ || (cat $@ && rm $@ && false) + $(Q)$(call echo-cmd,test)$(MYPY) "$<" > $@ || (cat $@ && rm $@ && false) perf-test-y += $(MYPY_TEST_LOGS) diff --git a/tools/perf/util/Build b/tools/perf/util/Build index a4618e4e4848..f922794e8108 100644 --- a/tools/perf/util/Build +++ b/tools/perf/util/Build @@ -426,7 +426,7 @@ endif $(OUTPUT)%.mypy_log: % $(call rule_mkdir) - $(Q)$(call echo-cmd,test)mypy "$<" > $@ || (cat $@ && rm $@ && false) + $(Q)$(call echo-cmd,test)$(MYPY) "$<" > $@ || (cat $@ && rm $@ && false) perf-util-y += $(MYPY_TEST_LOGS) From 5fe83fb352c8d50fadd814228ae1022d34073d60 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:41 -0700 Subject: [PATCH 189/425] perf build: Make pylint build test opt-out (NO_PYLINT=1) Commit 8a54784e708b ("perf build: Add pylint build tests") added a compile-time pylint check for Python files in tools/perf, but left it opt-in via PYLINT=1. Now that pylint issues across tools/perf/python, tools/perf/pmu-events, and tools/perf/tests/shell/lib have been cleaned up, enable the pylint check by default when pylint is installed on the system, with an opt-out via NO_PYLINT=1 (matching NO_SHELLCHECK=1, NO_MYPY=1, and other perf build feature flags). Configure PYTHONPATH in Makefile.perf so pylint resolves perf.pyi type stubs and pmu-events modules, enable error and warning categories (--disable=all --enable=E,W --disable=W0123,W0311,W0511,W0603,W0622,W0718 --ignored-modules=textual,setproctitle) appropriate for perf's standalone CLI and test scripts, and require pylint >= 2.16.0 (skipping with a warning if an older version is installed). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Build | 2 +- tools/perf/Makefile.perf | 40 +++++++++++++++++++++++++++++++++---- tools/perf/pmu-events/Build | 2 +- tools/perf/tests/Build | 2 +- tools/perf/util/Build | 2 +- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/tools/perf/Build b/tools/perf/Build index 37d603faea6d..5bd0e58d7cb0 100644 --- a/tools/perf/Build +++ b/tools/perf/Build @@ -101,6 +101,6 @@ endif $(OUTPUT)%.pylint_log: % $(call rule_mkdir) - $(Q)$(call echo-cmd,test)pylint "$<" > $@ || (cat $@ && rm $@ && false) + $(Q)$(call echo-cmd,test)$(PYLINT) "$<" > $@ || (cat $@ && rm $@ && false) perf-y += $(PYLINT_TEST_LOGS) diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf index fbb5dfb67089..168b286770f3 100644 --- a/tools/perf/Makefile.perf +++ b/tools/perf/Makefile.perf @@ -128,6 +128,8 @@ include ../scripts/utilities.mak # Define NO_SHELLCHECK if you do not want to run shellcheck during build # # Define NO_MYPY if you do not want to run mypy during build +# +# Define NO_PYLINT if you do not want to run pylint during build # As per kernel Makefile, avoid funny character set dependencies unexport LC_ALL @@ -281,9 +283,39 @@ ifneq ($(force_fixdep),1) endif endif -# Runs pylint on perf python files -ifeq ($(PYLINT),1) - PYLINT := $(shell which pylint 2> /dev/null) +# Runs pylint on perf python files. +# Require pylint >= 2.16.0 (which introduced W0718 broad-exception-caught and +# astroid .pyi stub support for perf.pyi). +# Disabled warnings: +# W0123 (eval-used): Use of eval() +# W0311 (bad-indentation): Non-4-space indentation +# W0511 (fixme): Presence of FIXME/TODO comments +# W0603 (global-statement): Use of the global statement +# W0622 (redefined-builtin): Redefining a built-in name (e.g. id, type, dir) +# W0718 (broad-exception-caught): Catching broad Exception +# Wait until after the fixdep bootstrap sub-make (force_fixdep != 1) before +# resolving PYLINT and stripping PYLINT=% from MAKEOVERRIDES (substituting +# escaped spaces so multi-word overrides are removed as a single token) so child +# Makefile.build sub-makes inherit the PYTHONPATH- and flag-wrapped export. +ifneq ($(force_fixdep),1) + MAKEOVERRIDES := $(subst __SPACE__,\ ,$(filter-out PYLINT=%,$(subst \ ,__SPACE__,$(MAKEOVERRIDES)))) + ifeq ($(NO_PYLINT),1) + override PYLINT := + else ifeq ($(filter-out 1,$(PYLINT)),) + override PYLINT := $(shell which pylint 2> /dev/null) + endif + + ifneq ($(PYLINT),) + ifneq ($(shell $(PYLINT) --version 2>/dev/null | \ + awk '/^pylint / { split($$2, v, "."); print (v[1]+0 > 2 || (v[1]+0 == 2 && v[2]+0 >= 16)) }'), 1) + $(warning Warning: pylint version is older than 2.16.0, skipping pylint checks.) + override PYLINT := + else + override PYLINT := PYTHONPATH=$(srctree)/tools/perf/python:$(srctree)/tools/perf/pmu-events \ + $(PYLINT) --disable=all --enable=E,W --disable=W0123,W0311,W0511,W0603,W0622,W0718 \ + --ignored-modules=textual,setproctitle + endif + endif endif export srctree OUTPUT RM CC CXX RUSTC CLANG LD AR CFLAGS CXXFLAGS RUST_FLAGS V BISON FLEX AWK LIBBPF READELF @@ -963,7 +995,7 @@ clean:: $(LIBAPI)-clean $(LIBBPF)-clean $(LIBSUBCMD)-clean $(LIBSYMBOL)-clean $( $(OUTPUT)perf-iostat $(LANG_BINDINGS) $(Q)find $(or $(OUTPUT),.) -name '*.o' -delete -o -name '*.a' -delete -o \ -name '\.*.cmd' -delete -o -name '\.*.d' -delete -o -name '*.shellcheck_log' -delete -o \ - -name '*.mypy_log' -delete + -name '*.mypy_log' -delete -o -name '*.pylint_log' -delete $(Q)$(RM) $(OUTPUT).config-detected $(call QUIET_CLEAN, core-progs) $(RM) $(ALL_PROGRAMS) perf perf-read-vdso32 \ perf-read-vdsox32 $(OUTPUT)$(LIBJVMTI).so diff --git a/tools/perf/pmu-events/Build b/tools/perf/pmu-events/Build index c3542e5ae46f..1ac067be9d34 100644 --- a/tools/perf/pmu-events/Build +++ b/tools/perf/pmu-events/Build @@ -203,7 +203,7 @@ endif ifdef PYLINT define PYLINT_RULE $(2): $(1) - $$(Q)$$(call echo-cmd,test)pylint $(1) > $(2) || (cat $(2) && rm $(2) && false) + $$(Q)$$(call echo-cmd,test)$$(PYLINT) $(1) > $(2) || (cat $(2) && rm $(2) && false) endef $(foreach src,$(wildcard pmu-events/*.py), \ $(eval dest := $(patsubst pmu-events/%,$(OUTPUT)pmu-events/%.pylint_log,$(src))) \ diff --git a/tools/perf/tests/Build b/tools/perf/tests/Build index cd7b62a6c1b8..8fecd5390124 100644 --- a/tools/perf/tests/Build +++ b/tools/perf/tests/Build @@ -117,6 +117,6 @@ endif $(OUTPUT)%.pylint_log: % $(call rule_mkdir) - $(Q)$(call echo-cmd,test)pylint "$<" > $@ || (cat $@ && rm $@ && false) + $(Q)$(call echo-cmd,test)$(PYLINT) "$<" > $@ || (cat $@ && rm $@ && false) perf-test-y += $(PYLINT_TEST_LOGS) diff --git a/tools/perf/util/Build b/tools/perf/util/Build index f922794e8108..afcf98dc5006 100644 --- a/tools/perf/util/Build +++ b/tools/perf/util/Build @@ -438,7 +438,7 @@ endif $(OUTPUT)%.pylint_log: % $(call rule_mkdir) - $(Q)$(call echo-cmd,test)pylint "$<" > $@ || (cat $@ && rm $@ && false) + $(Q)$(call echo-cmd,test)$(PYLINT) "$<" > $@ || (cat $@ && rm $@ && false) perf-util-y += $(PYLINT_TEST_LOGS) From af2f226e8c29fc4dfa381043b7de2c145532bbae Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:42 -0700 Subject: [PATCH 190/425] perf Makefile: Install standalone Python scripts during transition Install the newly ported standalone Python scripts (python/*.py), type stubs (python/perf.pyi), and compiled perf Python extension module (python/perf*.so, when built) into $(perfexec_instdir_SQ)/python alongside the legacy scripts during the transition phase so that newly ported scripts can import perf directly from the installation directory before legacy support is removed. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Makefile.perf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf index 168b286770f3..89edf2720fc1 100644 --- a/tools/perf/Makefile.perf +++ b/tools/perf/Makefile.perf @@ -907,6 +907,13 @@ ifndef NO_LIBPYTHON $(INSTALL) scripts/python/Perf-Trace-Util/lib/Perf/Trace/* -m 644 -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/python/Perf-Trace-Util/lib/Perf/Trace'; \ $(INSTALL) scripts/python/*.py -m 644 -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/python'; \ $(INSTALL) scripts/python/bin/* -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/python/bin' + $(call QUIET_INSTALL, python-scripts-standalone) \ + $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/python' && \ + $(INSTALL) python/*.py -m 755 -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/python' && \ + $(INSTALL) python/perf.pyi -m 644 -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/python' && \ + if [ -f "$(OUTPUT)python/perf$(PYTHON_EXTENSION_SUFFIX)" ]; then \ + $(INSTALL) "$(OUTPUT)python/perf$(PYTHON_EXTENSION_SUFFIX)" -m 644 -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/python'; \ + fi endif $(call QUIET_INSTALL, dlfilters) \ $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/dlfilters'; \ From 4425182d426b2180e98d0b537f46cdb03078876d Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:43 -0700 Subject: [PATCH 191/425] perf python: Port stat-cpi to perf module Port stat-cpi.py from the legacy embedded scripting framework to a standalone Python script in tools/perf/python/ to calculate Cycles Per Instruction (CPI) per interval per CPU or thread. Improvements compared to the legacy script: - Support both perf.data file mode (via perf.session stat callbacks) and live counter collection mode (using perf.parse_events, evlist.open, and evsel.read across intervals), with automatic fallback to user-space (:u) and self-process monitoring when perf_event_paranoid restricts system-wide events (EACCES). - Compute per-interval counter deltas (val, ena, run) keyed by raw event name so cumulative PERF_RECORD_STAT snapshots and hybrid PMU events (e.g. cpu_core/cycles/, cpu_atom/cycles/) are accumulated accurately, and scale counts by time_enabled / time_running when multiplexed. - Replace hard-coded CPU ([0, 1]) and thread ([0]) arrays with dynamic CPU and thread discovery so arbitrary system topologies work automatically. - Add CLI option handling (-i, -I, -p) via argparse and type annotations passing mypy and pylint. Add a shell test (test_stat_cpi_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/stat-cpi.py | 219 ++++++++++++++++++ .../perf/tests/shell/test_stat_cpi_python.sh | 115 +++++++++ 2 files changed, 334 insertions(+) create mode 100755 tools/perf/python/stat-cpi.py create mode 100755 tools/perf/tests/shell/test_stat_cpi_python.sh diff --git a/tools/perf/python/stat-cpi.py b/tools/perf/python/stat-cpi.py new file mode 100755 index 000000000000..0b7d76876a6c --- /dev/null +++ b/tools/perf/python/stat-cpi.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +"""Calculate CPI from perf stat data or live.""" +from __future__ import annotations + +import argparse +import os +import signal +import sys +import time +from typing import Any, Optional +import perf + +class StatCpiAnalyzer: + """Accumulates cycles and instructions and calculates CPI.""" + + def __init__(self, args: argparse.Namespace) -> None: + self.args = args + self.data: dict[str, float] = {} + self.prev_data: dict[str, tuple[int, int, int]] = {} + self.recorded_pairs: set[tuple[int, int]] = set() + + def get_key(self, event: str, cpu: int, thread: int) -> str: + """Get key for data dictionary.""" + return f"{event}-{cpu}-{thread}" + + def store_key(self, cpu: int, thread: int) -> None: + """Store CPU and thread IDs.""" + self.recorded_pairs.add((cpu, thread)) + + def store(self, event: str, cpu: int, thread: int, + counts: tuple[int, int, int], is_delta: bool = False, + raw_name: Optional[str] = None) -> None: + """Store counter values, computing difference from previous + absolute values if not already deltas.""" + self.store_key(cpu, thread) + key = self.get_key(event, cpu, thread) + prev_key = self.get_key(raw_name or event, cpu, thread) + + val, ena, run = counts + if is_delta: + # counts are already deltas + cur_val = val + cur_ena = ena + cur_run = run + else: + if prev_key in self.prev_data: + prev_val, prev_ena, prev_run = self.prev_data[prev_key] + cur_val = val - prev_val + cur_ena = ena - prev_ena + cur_run = run - prev_run + else: + cur_val = val + cur_ena = ena + cur_run = run + self.prev_data[prev_key] = counts # Store absolute value for next time + + # Scale each raw event's delta by its own multiplexing ratio before + # summing across PMUs (e.g. cpu_core and cpu_atom on hybrid systems) so + # enabled time from an idle PMU does not inflate the multiplier of an + # active PMU. + scaled_val = cur_val * (cur_ena / float(cur_run)) if cur_run > 0 else float(cur_val) + self.data[key] = self.data.get(key, 0.0) + scaled_val + + def get(self, event: str, cpu: int, thread: int) -> float: + """Get scaled counter value.""" + key = self.get_key(event, cpu, thread) + return self.data.get(key, 0.0) + + @staticmethod + def _classify_event(name: str) -> Optional[str]: + """Classify an event name as 'cycles' or 'instructions'.""" + ev = name[6:-1] if name.startswith("evsel(") and name.endswith(")") else name + ev = ev.split(":", 1)[0] + if "/" in ev: + parts = [p for p in ev.split("/") if p] + if len(parts) >= 2: + ev = parts[1] + if ev in ("cycles", "cpu-cycles"): + return "cycles" + if ev == "instructions": + return "instructions" + return None + + def process_stat_event(self, event: Any, name: Optional[str] = None) -> None: + """Process PERF_RECORD_STAT and PERF_RECORD_STAT_ROUND events.""" + if event.type == perf.RECORD_STAT: + if name: + event_name = self._classify_event(name) + if not event_name: + return + self.store(event_name, event.cpu, event.thread, + (event.val, event.ena, event.run), raw_name=name) + elif event.type == perf.RECORD_STAT_ROUND: + timestamp = getattr(event, "time", 0) + self.print_interval(timestamp) + self.data.clear() + self.recorded_pairs.clear() + + def print_interval(self, timestamp: int) -> None: + """Print CPI for the current interval.""" + for cpu, thread in sorted(self.recorded_pairs): + cyc = self.get("cycles", cpu, thread) + ins = self.get("instructions", cpu, thread) + cpi = 0.0 + if ins != 0: + cpi = cyc / float(ins) + t_sec = timestamp / 1000000000.0 + print(f"{t_sec:15f}: cpu {cpu}, thread {thread} -> cpi {cpi:f} ({cyc:.0f}/{ins:.0f})") + + def read_counters(self, evlist: Any) -> None: + """Read counters live.""" + for evsel in evlist: + name = str(evsel) + event_name = self._classify_event(name) + if not event_name: + continue + + for cpu in evsel.cpus(): + for thread in evsel.threads(): + try: + counts = evsel.read(cpu, thread) + self.store(event_name, cpu, thread, + (counts.val, counts.ena, counts.run), + is_delta=True, raw_name=name) + except OSError: + pass + + def run_file(self) -> None: + """Process events from file.""" + session: Optional[perf.session] = perf.session( + perf.data(self.args.input), stat=self.process_stat_event + ) + try: + assert session is not None + session.process_events() + finally: + session = None + + def _open_live_evlist(self) -> Any: + """Open evlist for live mode, falling back to user-space or process scope on EACCES.""" + threads = perf.thread_map(self.args.pid) if self.args.pid else None + candidates = [ + ("cycles,instructions", threads), + ("cycles:u,instructions:u", threads), + ] + if threads is None: + self_threads = perf.thread_map(os.getpid()) + candidates.append(("cycles,instructions", self_threads)) + candidates.append(("cycles:u,instructions:u", self_threads)) + + last_err: Optional[OSError] = None + for events, tmap in candidates: + try: + evlist = perf.parse_events(events, None, tmap) + for evsel in evlist: + evsel.read_format |= ( + perf.FORMAT_TOTAL_TIME_ENABLED | perf.FORMAT_TOTAL_TIME_RUNNING + ) + evlist.open() + evlist.enable() + return evlist + except PermissionError as e: + last_err = e + except OSError as e: + if e.errno == 13: + last_err = e + else: + raise + if last_err is not None: + raise last_err + raise RuntimeError("Failed to open events") + + def run_live(self) -> None: + """Read counters live.""" + try: + evlist = self._open_live_evlist() + except OSError as e: + print(f"Failed to open events: {e}", file=sys.stderr) + sys.exit(1) + + def handle_signal(_signum: int, _frame: Any) -> None: + raise KeyboardInterrupt + + signal.signal(signal.SIGINT, signal.default_int_handler) + signal.signal(signal.SIGTERM, handle_signal) + + print("Live mode started. Press Ctrl+C to stop.") + try: + while True: + time.sleep(self.args.interval) + timestamp = time.time_ns() + self.read_counters(evlist) + self.print_interval(timestamp) + self.data.clear() + self.recorded_pairs.clear() + except KeyboardInterrupt: + print("\nStopped.") + finally: + evlist.close() + +def main() -> None: + """Main function.""" + ap = argparse.ArgumentParser(description="Calculate CPI from perf stat data or live") + ap.add_argument("-i", "--input", help="Input file name (enables file mode)") + ap.add_argument("-I", "--interval", type=float, default=1.0, + help="Interval in seconds for live mode") + ap.add_argument("-p", "--pid", type=int, + help="Monitor specific process ID in live mode") + args = ap.parse_args() + + analyzer = StatCpiAnalyzer(args) + if args.input: + analyzer.run_file() + else: + analyzer.run_live() + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_stat_cpi_python.sh b/tools/perf/tests/shell/test_stat_cpi_python.sh new file mode 100755 index 000000000000..0960afa4c797 --- /dev/null +++ b/tools/perf/tests/shell/test_stat_cpi_python.sh @@ -0,0 +1,115 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# stat-cpi python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + return 2 2>/dev/null || exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/stat-cpi.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, stat-cpi.py not found at $script_path" + return 2 2>/dev/null || exit 2 +fi + +err=0 +ran=0 +temp_data="" +temp_out="" + +cleanup() { + [ -n "${pid}" ] && kill "$pid" 2>/dev/null || true + [ -n "${workload_pid}" ] && kill "$workload_pid" 2>/dev/null || true + rm -f "${temp_data}" "${temp_out}" + trap - exit term int +} + +trap_cleanup() { + cleanup + exit 1 +} +trap trap_cleanup exit term int + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +test_live_mode() { + echo "Testing stat-cpi.py live mode..." + if ! perf stat -e cycles,instructions -- sleep 0.1 2>/dev/null && \ + ! perf stat -e cycles:u,instructions:u -- sleep 0.1 2>/dev/null; then + echo "perf stat failed (permissions?), skipping live mode test." + return 0 + fi + perf test -w noploop & + workload_pid=$! + if ! perf stat -e cycles,instructions -p "$workload_pid" -- sleep 0.05 2>/dev/null && \ + ! perf stat -e cycles:u,instructions:u -p "$workload_pid" -- sleep 0.05 2>/dev/null; then + kill "$workload_pid" 2>/dev/null || true + workload_pid="" + echo "perf stat -p failed (ptrace_scope?), skipping live mode test." + return 0 + fi + ran=1 + + # Run live mode for 1 interval in the background, give it a tiny sleep, then interrupt + "$PYTHON" "$script_path" -I 0.1 -p "$workload_pid" > "${temp_out}" & + pid=$! + sleep 0.5 + kill -INT "$pid" 2>/dev/null || true + set +e + wait "$pid" + res=$? + set -e + pid="" + kill "$workload_pid" 2>/dev/null || true + workload_pid="" + if [ $res -ne 0 ] && [ $res -ne 130 ] && [ $res -ne 143 ]; then + echo "Live mode failed or crashed" + err=1 + elif ! grep -q "cpi" "${temp_out}"; then + echo "Live mode produced no cpi output" + err=1 + else + echo "Live mode test passed." + fi +} + +test_file_mode() { + echo "Testing stat-cpi.py file mode..." + # Generate some stat events - perf stat -I represents interval reporting + if ! perf stat -e cycles,instructions -I 100 record -o "${temp_data}" \ + -- sleep 0.5 2>/dev/null && \ + ! perf stat -e cycles:u,instructions:u -I 100 record -o "${temp_data}" \ + -- sleep 0.5 2>/dev/null; then + echo "perf stat failed (permissions?), skipping file mode test." + return + fi + ran=1 + + out=$("$PYTHON" "$script_path" -i "${temp_data}") + if ! echo "$out" | grep -q "cpi"; then + echo "File mode test failed." + err=1 + else + echo "File mode test passed." + fi +} + +test_live_mode +test_file_mode + +cleanup +if [ $ran -eq 0 ]; then + exit 2 +fi +exit $err From 6ceccefad0751966ec63713e2855095caf296c54 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:44 -0700 Subject: [PATCH 192/425] perf python: Port mem-phys-addr to perf module Port mem-phys-addr.py to a standalone script in tools/perf/python/ using the perf.session API to read perf.data files and profile physical memory access types against /proc/iomem. Improvements compared to the legacy script: - Parse the full indentation hierarchy of /proc/iomem into a parent-child tree of frozen IomemEntry dataclasses (instead of only top-level indent-0 ranges), resolving physical addresses to the most specific sub-range (such as Kernel code/data/bss inside System RAM) and rolling child counts up into parent totals. - Support profiling multiple memory events in a single perf.data session (keyed by evsel name) instead of assuming a single global event. - Add argparse CLI options (-i/--input and --iomem to allow supplying an offline /proc/iomem snapshot from a target system). Add a shell test (test_mem_phys_addr_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/mem-phys-addr.py | 136 ++++++++++++++++++ .../tests/shell/test_mem_phys_addr_python.sh | 102 +++++++++++++ 2 files changed, 238 insertions(+) create mode 100755 tools/perf/python/mem-phys-addr.py create mode 100755 tools/perf/tests/shell/test_mem_phys_addr_python.sh diff --git a/tools/perf/python/mem-phys-addr.py b/tools/perf/python/mem-phys-addr.py new file mode 100755 index 000000000000..53ef7e9a14b5 --- /dev/null +++ b/tools/perf/python/mem-phys-addr.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +"""mem-phys-addr.py: Resolve physical address samples""" +from __future__ import annotations +import argparse +import bisect +import collections +from dataclasses import dataclass +import re +from typing import (Dict, List, Optional) + +import perf + +@dataclass(frozen=True) +class IomemEntry: + """Read from a line in /proc/iomem""" + begin: int + end: int + indent: int + label: str + + def __lt__(self, other) -> bool: + if isinstance(other, int): + return self.begin < other + return self.begin < other.begin + + def __gt__(self, other) -> bool: + if isinstance(other, int): + return self.begin > other + return self.begin > other.begin + +# Physical memory layout from /proc/iomem. Key is the indent and then +# a list of ranges. +iomem: Dict[int, List[IomemEntry]] = collections.defaultdict(list) +# Child nodes from the iomem parent. +children: Dict[IomemEntry, List[IomemEntry]] = collections.defaultdict(list) +# Maximum indent seen before an entry in the iomem file. +_STATE: Dict[str, int] = {"max_indent": 0} +# Per-event counts for each range of memory. +event_counts: Dict[str, collections.Counter] = collections.defaultdict(collections.Counter) + +def parse_iomem(iomem_path: str): + """Populate iomem from iomem file""" + with open(iomem_path, 'r', encoding='ascii') as f: + for line in f: + line = line.rstrip('\n') + if not line or line.isspace(): + continue + indent = 0 + while indent < len(line) and line[indent] == ' ': + indent += 1 + _STATE["max_indent"] = max(_STATE["max_indent"], indent) + m = re.split('-|:', line, maxsplit=2) + if len(m) < 3: + continue + begin = int(m[0].strip(), 16) + end = int(m[1].strip(), 16) + label = m[2].strip() + entry = IomemEntry(begin, end, indent, label) + # Before adding entry, search for a parent node using its begin. + if indent > 0: + parent = find_memory_type(begin) + assert parent, f"Given indent expected a parent for {label}" + children[parent].append(entry) + iomem[indent].append(entry) + +def find_memory_type(phys_addr) -> Optional[IomemEntry]: + """Search iomem for the range containing phys_addr with the maximum indent""" + for i in range(_STATE["max_indent"], -1, -1): + if i not in iomem: + continue + position = bisect.bisect_right(iomem[i], phys_addr) + if position == 0: + continue + iomem_entry = iomem[i][position-1] + if iomem_entry.begin <= phys_addr <= iomem_entry.end: + return iomem_entry + return None + +def _print_entries(entries, load_mem_type_cnt, total): + """Print counts from parents down to their children""" + for entry in sorted(entries, + key=lambda e: (-load_mem_type_cnt[e], e.begin)): + count = load_mem_type_cnt[entry] + if count > 0: + mem_type = ' ' * entry.indent + f"{entry.begin:x}-{entry.end:x} : {entry.label}" + percent = 100 * count / total + print(f"{mem_type:<40} {count:>10} {percent:>10.1f}") + _print_entries(children[entry], load_mem_type_cnt, total) + +def print_memory_type(): + """Print the resolved memory types and their counts.""" + if not event_counts: + print("No valid physical address samples found in perf data.") + return + + for event_name, load_mem_type_cnt in event_counts.items(): + print(f"Event: {event_name}") + print(f"{'Memory type':<40} {'count':>10} {'percentage':>10}") + print(f"{'-' * 40:<40} {'-' * 10:>10} {'-' * 10:>10}") + total = sum(load_mem_type_cnt.values()) + if total == 0: + continue + + # Add count from children into the parent. + for i in range(_STATE["max_indent"], -1, -1): + if i not in iomem: + continue + for entry in iomem[i]: + for child in children[entry]: + if load_mem_type_cnt[child] > 0: + load_mem_type_cnt[entry] += load_mem_type_cnt[child] + + _print_entries(iomem[0], load_mem_type_cnt, total) + print() +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Resolve physical address samples") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + ap.add_argument("--iomem", default="/proc/iomem", help="Path to iomem file") + args = ap.parse_args() + + def process_event(sample): + """Process a single sample event.""" + phys_addr = sample.sample_phys_addr or 0 + if not phys_addr: + return + entry = find_memory_type(phys_addr) + if entry: + event_name = str(sample.evsel) + if event_name.startswith("evsel(") and event_name.endswith(")"): + event_name = event_name[6:-1] + event_counts[event_name][entry] += 1 + + parse_iomem(args.iomem) + perf.session(perf.data(args.input), sample=process_event).process_events() + print_memory_type() diff --git a/tools/perf/tests/shell/test_mem_phys_addr_python.sh b/tools/perf/tests/shell/test_mem_phys_addr_python.sh new file mode 100755 index 000000000000..d094a0611a24 --- /dev/null +++ b/tools/perf/tests/shell/test_mem_phys_addr_python.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# mem-phys-addr python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/mem-phys-addr.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, mem-phys-addr.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_iomem="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_iomem}" "${temp_out}" +} + +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_iomem=$(mktemp /tmp/perf.iomem.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +cat << 'EOF' > "${temp_iomem}" +00000000-ffffffffffffffff : System RAM + 00000000-7fffffffffffffff : Low RAM + 00001000-00ffffff : Kernel code + 8000000000000000-ffffffffffffffff : High RAM +EOF + +test_iomem_hierarchy() { + echo "Testing mem-phys-addr.py hierarchical iomem resolution..." + "$PYTHON" - "$script_path" "${temp_iomem}" << 'PYEOF' > "${temp_out}" +import importlib.util +import sys + +spec = importlib.util.spec_from_file_location("mem_phys_addr", sys.argv[1]) +mod = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = mod +spec.loader.exec_module(mod) + +mod.parse_iomem(sys.argv[2]) +entry_kernel = mod.find_memory_type(0x100000) +entry_high = mod.find_memory_type(0x9000000000000000) +assert entry_kernel is not None and entry_kernel.label == "Kernel code" +assert entry_high is not None and entry_high.label == "High RAM" +mod.event_counts["cpu/mem-loads/"][entry_kernel] += 3 +mod.event_counts["cpu/mem-loads/"][entry_high] += 1 +mod.print_memory_type() +PYEOF + if ! grep -q "System RAM" "${temp_out}" || \ + ! grep -q "Kernel code" "${temp_out}" || \ + ! grep -q "High RAM" "${temp_out}"; then + echo "Hierarchical iomem resolution test failed." + err=1 + else + echo "Hierarchical iomem resolution test passed." + fi +} + +test_file_mode() { + echo "Testing mem-phys-addr.py file mode..." + + # Generate memory access events (try unprivileged user-space first, then system-wide) + if ! perf record --phys-data -d -o "${temp_data}" \ + -- perf test -w datasym >/dev/null 2>&1 && \ + ! perf record -d -o "${temp_data}" -- perf test -w datasym >/dev/null 2>&1 && \ + ! perf record -d -a -o "${temp_data}" -- sleep 0.2 >/dev/null 2>&1; then + echo "Skipping file mode record test, perf record -d not supported" + return 0 + fi + + # Run the script with custom --iomem + if ! "$PYTHON" "$script_path" -i "${temp_data}" --iomem "${temp_iomem}" > "${temp_out}" || \ + [ ! -s "${temp_out}" ]; then + echo "File mode test failed." + err=1 + else + echo "File mode test passed." + fi +} + +test_iomem_hierarchy +test_file_mode + +exit $err From c3034e70c6d4a2883a51e852a60b2c0ffa64703d Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:45 -0700 Subject: [PATCH 193/425] perf python: Port stackcollapse to perf module Port stackcollapse.py from tools/perf/scripts/python/ to a standalone script in tools/perf/python/ refactored into a StackCollapseAnalyzer class. Improvements compared to the legacy script: - Traverse sample.callchain directly from perf.session without allocating per-event dictionaries, and fall back to sample.symbol when a sample has no callchain. - Replace deprecated optparse with argparse, adding -i/--input alongside --include-tid, --include-pid, --no-comm, --tidy-java, and --kernel. - Handle BrokenPipeError cleanly when output is piped into downstream tools (such as head or flamegraph.pl). Add a shell test (test_stackcollapse_python.sh) using a CPU workload (perf test -w noploop) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/stackcollapse.py | 145 ++++++++++++++++++ .../tests/shell/test_stackcollapse_python.sh | 78 ++++++++++ 2 files changed, 223 insertions(+) create mode 100755 tools/perf/python/stackcollapse.py create mode 100755 tools/perf/tests/shell/test_stackcollapse_python.sh diff --git a/tools/perf/python/stackcollapse.py b/tools/perf/python/stackcollapse.py new file mode 100755 index 000000000000..0e8a65969db3 --- /dev/null +++ b/tools/perf/python/stackcollapse.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +stackcollapse.py - format perf samples with one line per distinct call stack + +This script's output has two space-separated fields. The first is a semicolon +separated stack including the program name (from the "comm" field) and the +function names from the call stack. The second is a count: + + swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 2 + +The file is sorted according to the first field. + +Ported from tools/perf/scripts/python/stackcollapse.py +""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import os +import sys +import perf + + +class StackCollapseAnalyzer: + """Accumulates call stacks and prints them collapsed.""" + + def __init__(self, args: argparse.Namespace) -> None: + self.args = args + self.lines: dict[str, int] = defaultdict(int) + self.session: perf.session | None = None + + def tidy_function_name(self, sym: str, dso: str) -> str: + """Beautify function names based on options.""" + if sym is None: + sym = "[unknown]" + + sym = sym.replace(";", ":") + if self.args.tidy_java: + # Beautify Java signatures + sym = sym.replace("<", "") + sym = sym.replace(">", "") + if sym.startswith("L") and "/" in sym: + sym = sym[1:] + try: + sym = sym[:sym.index("(")] + except ValueError: + pass + + if self.args.annotate_kernel and dso == "[kernel.kallsyms]": + return sym + "_[k]" + return sym + + def process_event(self, sample: perf.sample_event) -> None: + """Collect call stack for each sample.""" + stack = [] + callchain = sample.callchain + if callchain is not None: + for node in callchain: + stack.append(self.tidy_function_name(node.symbol, node.dso)) + else: + # Fallback if no callchain + sym = (sample.symbol or '[unknown]') + dso = (sample.dso or '[unknown]') + stack.append(self.tidy_function_name(sym, dso)) + + if self.args.include_comm: + comm = "Unknown" + if self.session is not None: + try: + proc = self.session.find_thread( + sample.sample_pid, sample.sample_tid + ) + if proc: + proc_comm = proc.comm() + if proc_comm is not None: + comm = proc_comm + except TypeError: + pass + comm = str(comm).replace(" ", "_") + sep = "-" + if self.args.include_pid: + comm = f"{comm}{sep}{(sample.sample_pid or 0)}" + sep = "/" + if self.args.include_tid: + comm = f"{comm}{sep}{(sample.sample_tid or 0)}" + stack.append(comm) + + stack_string = ";".join(reversed(stack)) + self.lines[stack_string] += 1 + + def print_totals(self) -> None: + """Print sorted collapsed stacks.""" + try: + for stack in sorted(self.lines): + print(f"{stack} {self.lines[stack]}") + sys.stdout.flush() + except BrokenPipeError: + devnull = os.open(os.devnull, os.O_WRONLY) + os.dup2(devnull, sys.stdout.fileno()) + os.close(devnull) + + +def main(): + """Main function.""" + ap = argparse.ArgumentParser( + description="Format perf samples with one line per distinct call stack" + ) + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + ap.add_argument("--include-tid", action="store_true", help="include thread id in stack") + ap.add_argument("--include-pid", action="store_true", help="include process id in stack") + ap.add_argument("--no-comm", dest="include_comm", action="store_false", default=True, + help="do not separate stacks according to comm") + ap.add_argument("--tidy-java", action="store_true", help="beautify Java signatures") + ap.add_argument("--kernel", dest="annotate_kernel", action="store_true", + help="annotate kernel functions with _[k]") + + args = ap.parse_args() + + if args.include_tid and not args.include_comm: + print("requesting tid but not comm is invalid", file=sys.stderr) + sys.exit(1) + if args.include_pid and not args.include_comm: + print("requesting pid but not comm is invalid", file=sys.stderr) + sys.exit(1) + + analyzer = StackCollapseAnalyzer(args) + + try: + session = perf.session(perf.data(args.input), sample=analyzer.process_event) + analyzer.session = session + session.process_events() + except IOError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + pass + finally: + analyzer.session = None + + analyzer.print_totals() + + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_stackcollapse_python.sh b/tools/perf/tests/shell/test_stackcollapse_python.sh new file mode 100755 index 000000000000..a3b6c917e842 --- /dev/null +++ b/tools/perf/tests/shell/test_stackcollapse_python.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# stackcollapse python test + +set -e -o pipefail + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/stackcollapse.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, stackcollapse.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing stackcollapse.py..." + +# Create a perf.data file with callchains. Use a busy workload rather than +# sleep, as an idle system may not generate any samples at all. +perf record -g -o "${temp_data}" \ + -- perf test -w noploop >/dev/null 2>&1 || \ + { echo "Skipping test, perf record failed"; exit 2; } + +if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 +fi + +# Check that the script executes with default options +if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "stackcollapse.py test failed" + err=1 +else + # It outputs stacks like: swapper;...;... 2 + if [ ! -s "${temp_out}" ]; then + echo "Expected stack traces in output, but output is empty." + err=1 + else + echo "stackcollapse default test passed." + fi +fi + +# Test CLI flags (--include-pid, --include-tid, --tidy-java, --kernel) and BrokenPipeError +if ! "$PYTHON" "$script_path" -i "${temp_data}" \ + --include-pid --include-tid --tidy-java --kernel | head -n 1 > "${temp_out}" || \ + [ ! -s "${temp_out}" ]; then + echo "stackcollapse.py options/pipe test failed" + err=1 +elif ! "$PYTHON" "$script_path" -i "${temp_data}" --no-comm > "${temp_out}" || \ + [ ! -s "${temp_out}" ]; then + echo "stackcollapse.py --no-comm test failed" + err=1 +else + echo "stackcollapse options test passed." +fi +rm -f "${temp_out}" + +exit $err From eb08738a7f65fbb629f7acf499a742f1a708f1ad Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:46 -0700 Subject: [PATCH 194/425] perf python: Port flamegraph to perf module Port flamegraph.py to a standalone script in tools/perf/python/ that uses the perf module directly, avoiding intermediate dictionary allocations for event fields. Improvements compared to the legacy script: - Add Subresource Integrity (integrity="sha256-...") and crossorigin="anonymous" attributes to external CDN stylesheet and script tags in MINIMAL_HTML. - Upgrade CDN HTML template hash verification from weak MD5 (hashlib.md5) to cryptographic SHA-256 (hashlib.sha256). - Escape '<', '>', and '&' ('\u003c', '\u003e', '\u0026') in embedded JSON payloads (stacks_json and options_json) to prevent HTML script injection / XSS when rendering untrusted symbol or command names. - Skip invoking 'perf report --header-only' when the input is stdin ('-'), a FIFO pipe, or a character device (S_ISFIFO / S_ISCHR) so non-seekable streams do not hang or fail. Add a shell test (test_flamegraph_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/flamegraph.py | 283 ++++++++++++++++++ .../tests/shell/test_flamegraph_python.sh | 122 ++++++++ 2 files changed, 405 insertions(+) create mode 100755 tools/perf/python/flamegraph.py create mode 100755 tools/perf/tests/shell/test_flamegraph_python.sh diff --git a/tools/perf/python/flamegraph.py b/tools/perf/python/flamegraph.py new file mode 100755 index 000000000000..1b06a11fa86c --- /dev/null +++ b/tools/perf/python/flamegraph.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +flamegraph.py - create flame graphs from perf samples using perf python module +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import subprocess +import sys +import urllib.request +from typing import Dict, Optional, Union +import perf + +MINIMAL_HTML = """ + + + +
+ + + + +""" + +class Node: + """A node in the flame graph tree.""" + def __init__(self, name: str, libtype: str): + self.name = name + self.libtype = libtype + self.value: int = 0 + self.children: dict[str, Node] = {} + + def to_json(self) -> Dict[str, Union[str, int, list[Dict]]]: + """Convert the node to a JSON-serializable dictionary.""" + return { + "n": self.name, + "l": self.libtype, + "v": self.value, + "c": [x.to_json() for x in self.children.values()] + } + + +class FlameGraphCLI: + """Command-line interface for generating flame graphs.""" + def __init__(self, args): + self.args = args + self.stack = Node("all", "root") + self.session = None + + @staticmethod + def get_libtype_from_dso(dso: Optional[str]) -> str: + """Determine the library type from the DSO name.""" + if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux") or dso == "[kernel]"): + return "kernel" + return "" + + @staticmethod + def find_or_create_node(node: Node, name: str, libtype: str) -> Node: + """Find a child node with the given name or create a new one.""" + if name in node.children: + return node.children[name] + child = Node(name, libtype) + node.children[name] = child + return child + + def process_event(self, sample) -> None: + """Process a single perf sample event.""" + if self.args.event_name and self.args.event_name not in str(sample.evsel): + return + + pid = sample.sample_pid + dso_type = "" + try: + thread = self.session.find_thread(sample.sample_pid, sample.sample_tid) + comm = (thread.comm() if thread else None) or "[unknown]" + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError): + comm = "[unknown]" + + if pid == 0: + comm = comm if comm != "[unknown]" else "swapper" + dso_type = "kernel" + else: + comm = f"{comm} ({pid})" + + node = self.find_or_create_node(self.stack, comm, dso_type) + + callchain = sample.callchain + if callchain: + # We want to traverse from root to leaf. + # perf callchain iterator gives leaf to root. + # We collect them and reverse. + frames = list(callchain) + for entry in reversed(frames): + name = entry.symbol or "[unknown]" + libtype = self.get_libtype_from_dso(entry.dso) + node = self.find_or_create_node(node, name, libtype) + else: + # Fallback if no callchain + name = (sample.symbol or '[unknown]') + libtype = self.get_libtype_from_dso((sample.dso or '[unknown]')) + node = self.find_or_create_node(node, name, libtype) + + node.value += 1 + + def get_report_header(self) -> str: + """Get the header from the perf report.""" + try: + input_file = self.args.input or "perf.data" + if input_file == "-": + return "" + mode = os.stat(input_file).st_mode + import stat + if stat.S_ISFIFO(mode) or stat.S_ISCHR(mode): + return "" + output = subprocess.check_output(["perf", "report", "--header-only", "-i", input_file]) + result = output.decode("utf-8") + if self.args.event_name: + result += "\nFocused event: " + self.args.event_name + return result + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError, + subprocess.CalledProcessError): + return "" + + def run(self) -> None: + """Run the flame graph generation.""" + input_file = self.args.input or "perf.data" + if input_file != "-" and not os.path.exists(input_file): + print(f"Error: {input_file} not found. (try 'perf record' first)", file=sys.stderr) + sys.exit(1) + + try: + self.session = perf.session(perf.data(input_file), + sample=self.process_event) + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e: + print(f"Error opening session: {e}", file=sys.stderr) + sys.exit(1) + + try: + self.session.process_events() + finally: + # perf.session is a C extension type that holds a strong reference to the + # bound method self.process_event (which references self) and does not + # implement cyclic GC (tp_traverse). Explicitly clear self.session to break + # the reference cycle and free the underlying session resources deterministically. + self.session = None + + stacks_json = json.dumps(self.stack, default=lambda x: x.to_json()) + # Escape HTML special characters to prevent XSS + stacks_json = stacks_json.replace("<", "\\u003c") \ + .replace(">", "\\u003e").replace("&", "\\u0026") + + if self.args.format == "html": + report_header = self.get_report_header() + options = { + "colorscheme": self.args.colorscheme, + "context": report_header + } + options_json = json.dumps(options) + options_json = options_json.replace("<", "\\u003c") \ + .replace(">", "\\u003e").replace("&", "\\u0026") + + template = self.args.template + template_sha256sum = None + output_str = None + + if not os.path.isfile(template): + if template.startswith("http://") or template.startswith("https://"): + if not self.args.allow_download: + print("Warning: Downloading templates is disabled. " + "Use --allow-download.", file=sys.stderr) + template = None + else: + print(f"Warning: Template file '{template}' not found.", file=sys.stderr) + if self.args.allow_download: + print("Using default CDN template.", file=sys.stderr) + template = ( + "https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/" + "d3-flamegraph-base.html" + ) + template_sha256sum = ( + "f6a4aa7edffda4fb9bd71eb0eb75bc44d0bb34cd9efbd053f6095bc5c28d702b" + ) + else: + template = None + + use_minimal = False + try: + if not template: + use_minimal = True + elif template.startswith(("http://", "https://")): + with urllib.request.urlopen(template) as url_template: + output_str = "".join([l.decode("utf-8") for l in url_template.readlines()]) + else: + with open(template, "r", encoding="utf-8") as f: + output_str = f.read() + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as err: + print(f"Error reading template {template}: {err}\n", file=sys.stderr) + use_minimal = True + + if use_minimal: + print("Using internal minimal HTML that refers to d3's web site. JavaScript " + + "loaded this way from a local file may be blocked unless your " + + "browser has relaxed permissions. Run with '--allow-download' to fetch " + + "the full D3 HTML template.", file=sys.stderr) + output_str = MINIMAL_HTML + + elif template_sha256sum: + assert output_str is not None + download_sha256sum = hashlib.sha256( + output_str.encode("utf-8") + ).hexdigest() + if download_sha256sum != template_sha256sum: + s = None + while s not in ["y", "n"]: + try: + s = input(f"""Unexpected template sha256sum. +{download_sha256sum} != {template_sha256sum}, for: +{template} +continue?[yn] """).lower() + except EOFError: + s = "n" + if s == "n": + sys.exit(1) + + assert output_str is not None + replacements = { + "/** @options_json **/": options_json, + "/** @flamegraph_json **/": stacks_json, + } + # Use a lambda replacement so re.sub does not interpret backslashes + # in the JSON strings (such as \n or \u003c) as regex escape sequences. + output_str = re.sub( + r"/\*\* @(?:options_json|flamegraph_json) \*\*/", + lambda m: replacements[m.group(0)], + output_str, + ) + output_fn = self.args.output or "flamegraph.html" + else: + output_str = stacks_json + output_fn = self.args.output or "stacks.json" + + if output_fn == "-": + with open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out: + out.write(output_str) + else: + print(f"dumping data to {output_fn}") + with open(output_fn, "w", encoding="utf-8") as out: + out.write(output_str) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Create flame graphs using perf python module.") + parser.add_argument("-f", "--format", default="html", choices=["json", "html"], + help="output file format") + parser.add_argument("-o", "--output", help="output file name") + parser.add_argument("--template", + default="/usr/share/d3-flame-graph/d3-flamegraph-base.html", + help="path to flame graph HTML template") + parser.add_argument("--colorscheme", default="blue-green", + help="flame graph color scheme", choices=["blue-green", "orange"]) + parser.add_argument("-i", "--input", help="input perf.data file") + parser.add_argument("--allow-download", default=False, action="store_true", + help="allow unprompted downloading of HTML template") + parser.add_argument("-e", "--event", default="", dest="event_name", type=str, + help="specify the event to generate flamegraph for") + + cli_args = parser.parse_args() + cli = FlameGraphCLI(cli_args) + cli.run() diff --git a/tools/perf/tests/shell/test_flamegraph_python.sh b/tools/perf/tests/shell/test_flamegraph_python.sh new file mode 100755 index 000000000000..c6f5cf80b79f --- /dev/null +++ b/tools/perf/tests/shell/test_flamegraph_python.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# flamegraph python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/flamegraph.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, flamegraph.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_json="" +temp_html="" +temp_tpl="" + +cleanup() { + rm -f "${temp_data}" "${temp_json}" "${temp_html}" "${temp_tpl}" +} + +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_json=$(mktemp /tmp/perf.flamegraph.json.XXXXXX) +temp_html=$(mktemp /tmp/perf.flamegraph.html.XXXXXX) +temp_tpl=$(mktemp /tmp/perf.flamegraph.tpl.XXXXXX) + +validate_json() { + local json_file="$1" + local label="$2" + + if ! "$PYTHON" -m json.tool "${json_file}" /dev/null >/dev/null 2>&1; then + echo "${label} JSON validation failed." + err=1 + elif ! "$PYTHON" -c "import json, sys +d = json.load(open(sys.argv[1])) +assert d.get('v', 0) > 0 or len(d.get('c', [])) > 0" "${json_file}" >/dev/null 2>&1; then + echo "${label} JSON flamegraph structure is empty." + err=1 + else + echo "${label} JSON test passed." + fi +} + +test_file_mode() { + echo "Testing flamegraph.py..." + + # Generate some events with callchains + if ! perf record -g -o "${temp_data}" -- perf test -w noploop >/dev/null 2>&1; then + echo "Skipping test, perf record -g failed (permissions or lack of support)" + exit 2 + fi + + # Run the script in file mode and validate JSON output + if ! "$PYTHON" "$script_path" -i "${temp_data}" -f json -o "${temp_json}" >/dev/null; then + echo "File mode JSON test failed." + err=1 + else + validate_json "${temp_json}" "File mode" + fi + + # Run the script in pipe ('-') mode and validate JSON output + rm -f "${temp_json}" + if ! perf record -g -o - -- perf test -w noploop 2>/dev/null | \ + "$PYTHON" "$script_path" -i - -f json -o "${temp_json}" >/dev/null; then + echo "Pipe stdin JSON mode test failed." + err=1 + else + validate_json "${temp_json}" "Pipe mode" + fi + + # Run the script, dump as html to temp_html using MINIMAL_HTML fallback + if ! "$PYTHON" "$script_path" -i "${temp_data}" -f html \ + --template /nonexistent/template.html -o "${temp_html}" >/dev/null 2>&1; then + echo "File mode HTML test failed." + err=1 + else + if ! grep -q "" "${temp_html}" || \ + ! grep -q 'crossorigin="anonymous"' "${temp_html}"; then + echo "HTML validation failed." + err=1 + else + echo "File mode HTML test passed." + fi + fi + + # Test custom local HTML template and --colorscheme option + cat << 'EOF' > "${temp_tpl}" + +EOF + if ! "$PYTHON" "$script_path" -i "${temp_data}" -f html --template "${temp_tpl}" \ + --colorscheme blue-green -o "${temp_html}" >/dev/null 2>&1; then + echo "Custom template HTML test failed." + err=1 + elif ! grep -q "blue-green" "${temp_html}"; then + echo "Custom template colorscheme substitution failed." + err=1 + else + echo "Custom template HTML test passed." + fi +} + +test_file_mode + +exit $err From e1e4bd6f8b6bddeafd3d1b5d3f756981b4b3dde2 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:47 -0700 Subject: [PATCH 195/425] perf python: Port gecko to perf module Port gecko.py to a standalone script in tools/perf/python/ that uses the perf module directly to convert perf.data profiles into Firefox Gecko profile format. Improvements compared to the legacy script: - Encapsulate profiler state in GeckoCLI and CategoryData classes with full type annotations, removing global variables. - Harden the local HTTP server in _write_and_launch(): write the temporary profile to an isolated tempfile.TemporaryDirectory() with a randomized UUID filename instead of the current working directory, bind HTTPServer exclusively to 127.0.0.1 on an OS-assigned ephemeral port (0), restrict CORS Access-Control-Allow-Origin to 'https://profiler.firefox.com' instead of '*', and restrict HTTP GET requests exclusively to the randomized profile filename (returning HTTP 403 for directory listings and any other path). - Add -i/--input and -e/--event CLI options via argparse. Add a shell test (test_gecko_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/gecko.py | 411 ++++++++++++++++++++ tools/perf/tests/shell/test_gecko_python.sh | 95 +++++ 2 files changed, 506 insertions(+) create mode 100755 tools/perf/python/gecko.py create mode 100755 tools/perf/tests/shell/test_gecko_python.sh diff --git a/tools/perf/python/gecko.py b/tools/perf/python/gecko.py new file mode 100755 index 000000000000..3427e0770156 --- /dev/null +++ b/tools/perf/python/gecko.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +gecko.py - Convert perf record output to Firefox's gecko profile format +""" +from __future__ import annotations + +import argparse +import functools +import json +import os +import sys +import tempfile +import threading +import urllib.parse +import uuid +import webbrowser +from dataclasses import dataclass, field +from http.server import HTTPServer, SimpleHTTPRequestHandler +from typing import Dict, List, NamedTuple, Optional, Tuple + +import perf + + +# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 +class Frame(NamedTuple): + """A single stack frame in the gecko profile format.""" + string_id: int + relevantForJS: bool + innerWindowID: int + implementation: None + optimizations: None + line: None + column: None + category: int + subcategory: Optional[int] + + +# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 +class Stack(NamedTuple): + """A single stack in the gecko profile format.""" + prefix_id: Optional[int] + frame_id: int + + +# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 +class Sample(NamedTuple): + """A single sample in the gecko profile format.""" + stack_id: Optional[int] + time_ms: float + responsiveness: int + + +@dataclass +class Tables: + """Interned tables for the gecko profile format.""" + frame_table: List[Frame] = field(default_factory=list) + string_table: List[str] = field(default_factory=list) + string_map: Dict[str, int] = field(default_factory=dict) + stack_table: List[Stack] = field(default_factory=list) + stack_map: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) + frame_map: Dict[str, int] = field(default_factory=dict) + + +@dataclass +class Thread: + """A builder for a profile of the thread.""" + comm: str + pid: int + tid: int + user_category: int + kernel_category: int + samples: List[Sample] = field(default_factory=list) + tables: Tables = field(default_factory=Tables) + + def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int: + """Gets a matching stack, or saves the new stack. Returns a Stack ID.""" + key = (prefix_id, frame_id) + stack_id = self.tables.stack_map.get(key) + if stack_id is None: + stack_id = len(self.tables.stack_table) + self.tables.stack_table.append(Stack(prefix_id=prefix_id, frame_id=frame_id)) + self.tables.stack_map[key] = stack_id + return stack_id + + def _intern_string(self, string: str) -> int: + """Gets a matching string, or saves the new string. Returns a String ID.""" + string_id = self.tables.string_map.get(string) + if string_id is not None: + return string_id + string_id = len(self.tables.string_table) + self.tables.string_table.append(string) + self.tables.string_map[string] = string_id + return string_id + + def _intern_frame(self, frame_str: str) -> int: + """Gets a matching stack frame, or saves the new frame. Returns a Frame ID.""" + frame_id = self.tables.frame_map.get(frame_str) + if frame_id is not None: + return frame_id + frame_id = len(self.tables.frame_table) + self.tables.frame_map[frame_str] = frame_id + string_id = self._intern_string(frame_str) + + category = self.user_category + if (frame_str.find('kallsyms') != -1 or + frame_str.find('/vmlinux') != -1 or + frame_str.endswith('.ko)')): + category = self.kernel_category + + self.tables.frame_table.append(Frame( + string_id=string_id, + relevantForJS=False, + innerWindowID=0, + implementation=None, + optimizations=None, + line=None, + column=None, + category=category, + subcategory=None, + )) + return frame_id + + def add_sample(self, comm: str, stack: List[str], time_ms: float) -> None: + """Add a timestamped stack trace sample to the thread builder.""" + if self.comm != comm: + self.comm = comm + + prefix_stack_id: Optional[int] = None + for frame in stack: + frame_id = self._intern_frame(frame) + prefix_stack_id = self._intern_stack(frame_id, prefix_stack_id) + + if prefix_stack_id is not None: + self.samples.append(Sample(stack_id=prefix_stack_id, + time_ms=time_ms, + responsiveness=0)) + + def to_json_dict(self) -> Dict: + """Converts current Thread to GeckoThread JSON format.""" + return { + "tid": self.tid, + "pid": self.pid, + "name": self.comm, + "markers": { + "schema": { + "name": 0, + "startTime": 1, + "endTime": 2, + "phase": 3, + "category": 4, + "data": 5, + }, + "data": [], + }, + "samples": { + "schema": { + "stack": 0, + "time": 1, + "responsiveness": 2, + }, + "data": self.samples + }, + "frameTable": { + "schema": { + "location": 0, + "relevantForJS": 1, + "innerWindowID": 2, + "implementation": 3, + "optimizations": 4, + "line": 5, + "column": 6, + "category": 7, + "subcategory": 8, + }, + "data": self.tables.frame_table, + }, + "stackTable": { + "schema": { + "prefix": 0, + "frame": 1, + }, + "data": self.tables.stack_table, + }, + "stringTable": self.tables.string_table, + "registerTime": 0, + "unregisterTime": None, + "processType": "default", + } + + +class CORSRequestHandler(SimpleHTTPRequestHandler): + """Enable CORS for requests from profiler.firefox.com.""" + def __init__(self, *args, allowed_file: Optional[str] = None, **kwargs): + self.allowed_file = allowed_file + super().__init__(*args, **kwargs) + + def end_headers(self): + self.send_header('Access-Control-Allow-Origin', 'https://profiler.firefox.com') + super().end_headers() + + def do_GET(self): + if self.allowed_file and self.path.split('?')[0] != f'/{self.allowed_file}': + self.send_error(403, "Access denied") + return + super().do_GET() + + def list_directory(self, path): + self.send_error(403, "Directory listing forbidden") + return None + + +@dataclass +class CategoryData: + """Category configuration for the gecko profile.""" + user_index: int = 0 + kernel_index: int = 1 + categories: List[Dict] = field(default_factory=list) + + +class GeckoCLI: + """Command-line interface for converting perf data to Gecko format.""" + def __init__(self, args: argparse.Namespace) -> None: + self.args = args + self.tid_to_thread: Dict[int, Thread] = {} + self.start_time_ms: Optional[float] = None + self.session: Optional[perf.session] = None + self.product = args.product + self.cat_data = CategoryData( + categories=[ + { + "name": 'User', + "color": args.user_color, + "subcategories": ['Other'] + }, + { + "name": 'Kernel', + "color": args.kernel_color, + "subcategories": ['Other'] + }, + ] + ) + + def process_event(self, sample) -> None: + """Process a single perf sample event.""" + if self.args.event_name and self.args.event_name not in str(sample.evsel): + return + + # sample_time is in nanoseconds. Gecko wants milliseconds. + time_ms = sample.sample_time / 1000000.0 + pid = sample.sample_pid + tid = sample.sample_tid + + if self.start_time_ms is None: + self.start_time_ms = time_ms + + try: + thread_info = self.session.find_thread(sample.sample_pid, sample.sample_tid) if self.session else None + comm = (thread_info.comm() if thread_info is not None else None) or "[unknown]" + except AttributeError: + comm = "[unknown]" + + stack = [] + callchain = sample.callchain + if callchain: + for entry in callchain: + symbol = entry.symbol or "[unknown]" + dso = entry.dso or "[unknown]" + stack.append(f"{symbol} (in {dso})") + # Reverse because Gecko wants root first. + stack.reverse() + else: + symbol = sample.symbol or "[unknown]" + dso = sample.dso or "[unknown]" + stack.append(f"{symbol} (in {dso})") + + thread = self.tid_to_thread.get(tid) + if thread is None: + thread = Thread(comm=comm, pid=pid, tid=tid, + user_category=self.cat_data.user_index, + kernel_category=self.cat_data.kernel_index) + self.tid_to_thread[tid] = thread + thread.add_sample(comm=comm, stack=stack, time_ms=time_ms) + + def run(self) -> None: + """Run the conversion process.""" + input_file = self.args.input or "perf.data" + if input_file != "-" and not os.path.exists(input_file): + print(f"Error: {input_file} not found.", file=sys.stderr) + sys.exit(1) + + try: + self.session = perf.session(perf.data(input_file), sample=self.process_event) + except (OSError, RuntimeError, ValueError) as e: + print(f"Error opening session: {e}", file=sys.stderr) + sys.exit(1) + + if self.session: + try: + self.session.process_events() + finally: + # Break the reference cycle between perf.session and self.process_event + # because perf.session lacks cyclic GC support (tp_traverse). + self.session = None + + threads = [t.to_json_dict() for t in self.tid_to_thread.values()] + + gecko_profile = { + "meta": { + "interval": 1, + "processType": 0, + "product": self.product, + "stackwalk": 1, + "debug": 0, + "gcpoison": 0, + "asyncstack": 1, + "startTime": self.start_time_ms or 0, + "shutdownTime": None, + "version": 24, + "presymbolicated": True, + "categories": self.cat_data.categories, + "markerSchema": [], + }, + "libs": [], + "threads": threads, + "processes": [], + "pausedRanges": [], + } + + output_file = self.args.save_only + if output_file is None: + self._write_and_launch(gecko_profile) + else: + print(f'[ perf gecko: Captured and wrote into {output_file} ]') + with open(output_file, 'w', encoding='utf-8') as f: + json.dump(gecko_profile, f, indent=2) + + def _write_and_launch(self, profile: Dict) -> None: + """Write the profile to a file and launch the Firefox profiler.""" + print("Starting Firefox Profiler on your default browser...") + + with tempfile.TemporaryDirectory() as tmp_dir_name: + profile_name = f'gecko_profile_{uuid.uuid4().hex}.json' + filename = os.path.join(tmp_dir_name, profile_name) + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(profile, f, indent=2) + + handler = functools.partial( + CORSRequestHandler, directory=tmp_dir_name, allowed_file=profile_name + ) + try: + httpd = HTTPServer(('127.0.0.1', 0), handler) + except OSError as e: + print(f"Error starting HTTP server: {e}", file=sys.stderr) + sys.exit(1) + + port = httpd.server_port + + def start_server(): + httpd.serve_forever() + + thread = threading.Thread(target=start_server, daemon=True) + thread.start() + + safe_string = urllib.parse.quote_plus(f'http://127.0.0.1:{port}/{profile_name}') + url = f'https://profiler.firefox.com/from-url/{safe_string}' + print(f"Please open the following URL in your browser to view the profile:\n\n{url}\n") + launch_html = os.path.join(tmp_dir_name, 'launch.html') + with open(launch_html, 'w', encoding='utf-8') as f: + f.write(f'' + f'' + f'' + f'Redirecting to Firefox Profiler...') + if not webbrowser.open(f'file://{launch_html}'): + print("Failed to open browser, please open the URL manually.") + + print(f'[ perf gecko: Captured and wrote into {filename} ]') + print("Press Ctrl+C to stop the local server.") + try: + # Keep the main thread alive so the daemon thread can serve requests + stop_event = threading.Event() + while True: + stop_event.wait(1) + except KeyboardInterrupt: + print("\nStopping server...") + httpd.shutdown() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Convert perf.data to Firefox's Gecko Profile format" + ) + parser.add_argument('--product', default='perf', help='Product name (e.g. perf)') + parser.add_argument('--user-color', default='yellow', + help='Color for the User category', + choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red', + 'grey', 'magenta']) + parser.add_argument('--kernel-color', default='orange', + help='Color for the Kernel category', + choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red', + 'grey', 'magenta']) + parser.add_argument('--save-only', + help='Save the output to a file instead of opening Firefox\'s profiler') + parser.add_argument("-i", "--input", help="input perf.data file") + parser.add_argument("-e", "--event", default="", dest="event_name", type=str, + help="specify the event to generate gecko profile for") + + cli_args = parser.parse_args() + cli = GeckoCLI(cli_args) + cli.run() diff --git a/tools/perf/tests/shell/test_gecko_python.sh b/tools/perf/tests/shell/test_gecko_python.sh new file mode 100755 index 000000000000..eac9b2f8e3c7 --- /dev/null +++ b/tools/perf/tests/shell/test_gecko_python.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# gecko python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/gecko.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, gecko.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_json="" + +cleanup() { + rm -f "${temp_data}" "${temp_json}" +} + +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_json=$(mktemp /tmp/perf.gecko.json.XXXXXX) + +test_file_mode() { + echo "Testing gecko.py..." + + # Generate some events with callchains + if ! perf record -g -o "${temp_data}" -- perf test -w noploop >/dev/null 2>&1; then + echo "Skipping test, perf record -g failed (permissions or lack of support)" + exit 2 + fi + + # Run the script in save-only mode with custom product and category colors + if ! "$PYTHON" "$script_path" -i "${temp_data}" \ + --product "perf-test-product" --user-color blue --kernel-color red \ + --save-only "${temp_json}" >/dev/null; then + echo "File mode test failed." + err=1 + else + # Validate JSON schema and custom CLI option values + if ! "$PYTHON" - "$script_path" "${temp_json}" << 'PYEOF' +import importlib.util +import json +import sys + +with open(sys.argv[2], encoding="utf-8") as f: + data = json.load(f) + +assert data["meta"]["product"] == "perf-test-product" +assert data["meta"]["version"] == 24 +assert isinstance(data["threads"], list) and len(data["threads"]) > 0 + +spec = importlib.util.spec_from_file_location("gecko", sys.argv[1]) +mod = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = mod +spec.loader.exec_module(mod) + +class DummyHandler: + headers = [] + error_code = None + def send_header(self, k, v): + self.headers.append((k, v)) + def send_error(self, code, _msg): + self.error_code = code + +dummy = DummyHandler() +mod.CORSRequestHandler.list_directory(dummy, "/tmp") +assert dummy.error_code == 403 +PYEOF + then + echo "Gecko JSON and CORSRequestHandler validation failed." + err=1 + else + echo "File mode JSON and CORSRequestHandler test passed." + fi + fi +} + +test_file_mode + +exit $err From eeb70645a8097437c06cebccda4115f366b928c3 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:48 -0700 Subject: [PATCH 196/425] perf python: Port event_analyzing_sample to perf module Port event_analyzing_sample.py to a standalone script in tools/perf/python/ using the perf module and standard library sqlite3 module. Improvements compared to the legacy script: - Encapsulate database state in a _DB container instead of mutating module-level globals, and ensure temporary SQLite database files are cleaned up on exit. - Add argparse CLI options (-i/--input and -d/--db) while preserving PerfEvent, PebsEvent, and PebsNHM binary raw_buf unpacking and symbol/DSO histogram reporting. - Remove Python 2 compatibility code and add type annotations. Add a shell test (test_event_analyzing_sample_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/event_analyzing_sample.py | 332 ++++++++++++++++++ .../test_event_analyzing_sample_python.sh | 62 ++++ 2 files changed, 394 insertions(+) create mode 100755 tools/perf/python/event_analyzing_sample.py create mode 100755 tools/perf/tests/shell/test_event_analyzing_sample_python.sh diff --git a/tools/perf/python/event_analyzing_sample.py b/tools/perf/python/event_analyzing_sample.py new file mode 100755 index 000000000000..15b47cff9fa3 --- /dev/null +++ b/tools/perf/python/event_analyzing_sample.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +General event handler in Python, using SQLite to analyze events. + +The 2 database related functions in this script just show how to gather +the basic information, and users can modify and write their own functions +according to their specific requirement. + +The first function "show_general_events" just does a basic grouping for all +generic events with the help of sqlite, and the 2nd one "show_pebs_ll" is +for a x86 HW PMU event: PEBS with load latency data. + +Ported from tools/perf/scripts/python/event_analyzing_sample.py +""" +from __future__ import annotations + +import argparse +import math +import os +import sqlite3 +import struct +import tempfile +from typing import Any +import perf + +# Event types, user could add more here +EVTYPE_GENERIC = 0 +EVTYPE_PEBS = 1 # Basic PEBS event +EVTYPE_PEBS_LL = 2 # PEBS event with load latency info +EVTYPE_IBS = 3 + +# +# Currently we don't have good way to tell the event type, but by +# the size of raw buffer, raw PEBS event with load latency data's +# size is 176 bytes, while the pure PEBS event's size is 144 bytes. +# +def create_event(name, comm, dso, symbol, raw_buf): + """Create an event object based on raw buffer size.""" + if len(raw_buf) == 144: + event = PebsEvent(name, comm, dso, symbol, raw_buf) + elif len(raw_buf) == 176: + event = PebsNHM(name, comm, dso, symbol, raw_buf) + else: + event = PerfEvent(name, comm, dso, symbol, raw_buf) + + return event + +class PerfEvent: + """Base class for all perf event samples.""" + event_num = 0 + def __init__(self, name, comm, dso, symbol, raw_buf, ev_type=EVTYPE_GENERIC): + self.name = name + self.comm = comm + self.dso = dso + self.symbol = symbol + self.raw_buf = raw_buf + self.ev_type = ev_type + PerfEvent.event_num += 1 + + def show(self): + """Display PMU event info.""" + print(f"PMU event: name={self.name:12s}, symbol={self.symbol:24s}, " + f"comm={self.comm:8s}, dso={self.dso:12s}") + +# +# Basic Intel PEBS (Precise Event-based Sampling) event, whose raw buffer +# contains the context info when that event happened: the EFLAGS and +# linear IP info, as well as all the registers. +# +class PebsEvent(PerfEvent): + """Intel PEBS event.""" + pebs_num = 0 + def __init__(self, name, comm, dso, symbol, raw_buf, ev_type=EVTYPE_PEBS): + tmp_buf = raw_buf[0:80] + flags, ip, ax, bx, cx, dx, si, di, bp, sp = struct.unpack(' None: + """Initialize database tables.""" + print("In trace_begin:\n") + if not db_path: + fd, db_path = tempfile.mkstemp(prefix="perf_events_", suffix=".db") + os.close(fd) + _DB.temp_path = db_path + con = sqlite3.connect(db_path) + try: + # Drop any pre-existing tables so repeated runs do not accumulate duplicate events. + con.execute("drop table if exists gen_events;") + con.execute("drop table if exists pebs_ll;") + + # Will create several tables at the start, pebs_ll is for PEBS data with + # load latency info, while gen_events is for general event. + con.execute(""" + create table if not exists gen_events ( + name text, + symbol text, + comm text, + dso text + );""") + con.execute(""" + create table if not exists pebs_ll ( + name text, + symbol text, + comm text, + dso text, + flags integer, + ip integer, + status integer, + dse integer, + dla integer, + lat integer + );""") + except BaseException: + con.close() + raise + _DB.con = con + +def insert_db(event: Any) -> None: + """Insert event into database.""" + con = _DB.con + assert con is not None + if event.ev_type == EVTYPE_GENERIC: + con.execute("insert into gen_events values(?, ?, ?, ?)", + (event.name, event.symbol, event.comm, event.dso)) + elif event.ev_type == EVTYPE_PEBS_LL: + # Python's sqlite3 module only accepts signed 64-bit integers (-2^63 to 2^63 - 1) + # and raises OverflowError for unsigned 64-bit kernel pointers (> 0x7fffffffffffffff). + # Convert unsigned 64-bit addresses to signed 64-bit two's-complement integers. + ip = event.ip - 0x10000000000000000 if event.ip > 0x7fffffffffffffff else event.ip + status = (event.status - 0x10000000000000000 + if event.status > 0x7fffffffffffffff else event.status) + dla = event.dla - 0x10000000000000000 if event.dla > 0x7fffffffffffffff else event.dla + con.execute("insert into pebs_ll values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (event.name, event.symbol, event.comm, event.dso, event.flags, + ip, status, event.dse, dla, event.lat)) + +def process_event(sample: perf.sample_event) -> None: + """Callback for processing events.""" + # Create and insert event object to a database so that user could + # do more analysis with simple database commands. + + # Resolve comm, symbol, dso + comm = "Unknown_comm" + try: + if session is not None: + proc = session.find_thread(sample.sample_pid, sample.sample_tid) + if proc: + comm = proc.comm() or "Unknown_comm" + except TypeError: + pass + + # Symbol and dso info are not always resolved + dso = sample.dso if hasattr(sample, 'dso') and sample.dso else "Unknown_dso" + symbol = sample.symbol if hasattr(sample, 'symbol') and sample.symbol else "Unknown_symbol" + name = str(sample.evsel) + if name.startswith("evsel("): + name = name[6:-1] + + # Create the event object and insert it to the right table in database + try: + event = create_event(name, comm, dso, symbol, sample.raw_buf) + insert_db(event) + except (sqlite3.Error, ValueError, TypeError, OverflowError) as e: + print(f"Error creating/inserting event: {e}") + +def num2sym(num: int) -> str: + """Convert number to a histogram symbol (log2).""" + # As the event number may be very big, so we can't use linear way + # to show the histogram in real number, but use a log2 algorithm. + if num <= 0: + return "" + snum = '#' * (int(math.log(num, 2)) + 1) + return snum + +def show_general_events() -> None: + """Display statistics for general events.""" + con = _DB.con + assert con is not None + count = con.execute("select count(*) from gen_events") + for t in count: + print(f"There is {t[0]} records in gen_events table") + if t[0] == 0: + return + + print("Statistics about the general events grouped by thread/symbol/dso: \n") + + # Group by thread + commq = con.execute(""" + select comm, count(comm) from gen_events + group by comm order by -count(comm) + """) + print(f"\n{ 'comm':>16} {'number':>8} {'histogram':>16}\n{'='*42}") + for row in commq: + print(f"{row[0]:>16} {row[1]:>8} {num2sym(row[1])}") + + # Group by symbol + print(f"\n{'symbol':>32} {'number':>8} {'histogram':>16}\n{'='*58}") + symbolq = con.execute(""" + select symbol, count(symbol) from gen_events + group by symbol order by -count(symbol) + """) + for row in symbolq: + print(f"{row[0]:>32} {row[1]:>8} {num2sym(row[1])}") + + # Group by dso + print(f"\n{'dso':>40} {'number':>8} {'histogram':>16}\n{'='*74}") + dsoq = con.execute("select dso, count(dso) from gen_events group by dso order by -count(dso)") + for row in dsoq: + print(f"{row[0]:>40} {row[1]:>8} {num2sym(row[1])}") + +def show_pebs_ll() -> None: + """Display statistics for PEBS load latency events.""" + con = _DB.con + assert con is not None + # This function just shows the basic info, and we could do more with the + # data in the tables, like checking the function parameters when some + # big latency events happen. + count = con.execute("select count(*) from pebs_ll") + for t in count: + print(f"There is {t[0]} records in pebs_ll table") + if t[0] == 0: + return + + print("Statistics about the PEBS Load Latency events grouped by thread/symbol/dse/latency: \n") + + # Group by thread + commq = con.execute("select comm, count(comm) from pebs_ll group by comm order by -count(comm)") + print(f"\n{'comm':>16} {'number':>8} {'histogram':>16}\n{'='*42}") + for row in commq: + print(f"{row[0]:>16} {row[1]:>8} {num2sym(row[1])}") + + # Group by symbol + print(f"\n{'symbol':>32} {'number':>8} {'histogram':>16}\n{'='*58}") + symbolq = con.execute(""" + select symbol, count(symbol) from pebs_ll + group by symbol order by -count(symbol) + """) + for row in symbolq: + print(f"{row[0]:>32} {row[1]:>8} {num2sym(row[1])}") + + # Group by dse + dseq = con.execute("select dse, count(dse) from pebs_ll group by dse order by -count(dse)") + print(f"\n{'dse':>32} {'number':>8} {'histogram':>16}\n{'='*58}") + for row in dseq: + print(f"{row[0]:>32} {row[1]:>8} {num2sym(row[1])}") + + # Group by latency + latq = con.execute("select lat, count(lat) from pebs_ll group by lat order by lat") + print(f"\n{'latency':>32} {'number':>8} {'histogram':>16}\n{'='*58}") + for row in latq: + print(f"{str(row[0]):>32} {row[1]:>8} {num2sym(row[1])}") + +def trace_end() -> None: + """Called at the end of trace processing.""" + print("In trace_end:\n") + try: + if _DB.con: + try: + _DB.con.commit() + show_general_events() + show_pebs_ll() + finally: + _DB.con.close() + _DB.con = None + finally: + if _DB.temp_path and os.path.exists(_DB.temp_path): + try: + os.remove(_DB.temp_path) + except OSError: + pass + _DB.temp_path = None + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Analyze events with SQLite") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + ap.add_argument("-d", "--db", "--database", dest="database", default=None, + help="Database file name (defaults to a temporary file cleaned up on exit)") + args = ap.parse_args() + + try: + trace_begin(args.database) + session = perf.session(perf.data(args.input), sample=process_event) + session.process_events() + finally: + # Break the reference cycle between session and process_event (whose module + # globals reference session) so the C session object is deterministically freed. + session = None + trace_end() diff --git a/tools/perf/tests/shell/test_event_analyzing_sample_python.sh b/tools/perf/tests/shell/test_event_analyzing_sample_python.sh new file mode 100755 index 000000000000..4678edfd9e5b --- /dev/null +++ b/tools/perf/tests/shell/test_event_analyzing_sample_python.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# event_analyzing_sample python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/event_analyzing_sample.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, event_analyzing_sample.py not found at $script_path" + exit 2 +fi + +err=0 +temp_dir="" + +cleanup() { + rm -rf "${temp_dir}" +} + +trap 'cleanup' EXIT TERM INT + +temp_dir=$(mktemp -d /tmp/perf.event_analyzing.XXXXXX) +temp_data="${temp_dir}/perf.data" +temp_db="${temp_dir}/perf.db" + +test_file_mode() { + echo "Testing event_analyzing_sample.py..." + + # Generate some events + if ! perf record -o "${temp_data}" -- perf test -w noploop >/dev/null 2>&1; then + echo "Skipping test, perf record failed" + exit 2 + fi + + # Run the script + if ! "$PYTHON" "$script_path" -i "${temp_data}" -d "${temp_db}" > "${temp_dir}/perf.out" 2>&1; then + echo "File mode test failed." + err=1 + elif ! grep -q "Statistics about the general events" "${temp_dir}/perf.out" || \ + grep -q "Error creating/inserting event" "${temp_dir}/perf.out"; then + echo "Event analysis output validation failed." + err=1 + else + echo "File mode test passed." + fi +} + +test_file_mode + +exit $err From 3d69731960a5e55beecd9b395001b24a3c0b542e Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:49 -0700 Subject: [PATCH 197/425] perf python: Port syscall-counts to perf module Port tools/perf/scripts/python/syscall-counts.py to a standalone script in tools/perf/python/ using the perf module. Avoiding the embedded interpreter and per-event dictionary allocation overhead improves execution speed by ~4x: ``` $ perf record -e raw_syscalls:sys_enter -a sleep 1 ... $ time perf script tools/perf/scripts/python/syscall-counts.py perf ... real 0m3.887s user 0m3.578s sys 0m0.308s $ time python3 tools/perf/python/syscall-counts.py perf ... real 0m0.953s user 0m0.905s sys 0m0.048s ``` Additional improvements compared to the legacy script: - Resolve syscall names using perf.syscall_name(id, session.e_machine) instead of host python-audit / Util.py tables, enabling accurate cross-architecture perf.data analysis without external dependencies. - Support both raw_syscalls:sys_enter (sample.id) and individual syscalls:sys_enter_* tracepoints (sample.__syscall_nr / sample.nr), filtering out invalid/corrupt (> 0xffff or negative) syscall numbers. - Add argparse CLI options (-i/--input and optional comm filter). Add a shell test (test_syscall_counts_python.sh) to verify the standalone script. The legacy script and its bin wrapper are retained temporarily during the transition to maintain bisectability and are removed once all scripts are migrated. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/syscall-counts.py | 95 +++++++++++++++++++ .../tests/shell/test_syscall_counts_python.sh | 81 ++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100755 tools/perf/python/syscall-counts.py create mode 100755 tools/perf/tests/shell/test_syscall_counts_python.sh diff --git a/tools/perf/python/syscall-counts.py b/tools/perf/python/syscall-counts.py new file mode 100755 index 000000000000..3ddb1bcc27e0 --- /dev/null +++ b/tools/perf/python/syscall-counts.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Displays system-wide system call totals, broken down by syscall. + +If a [comm] arg is specified, only syscalls called by [comm] are displayed. +""" +from __future__ import annotations + +import argparse +from collections import defaultdict +from typing import DefaultDict +import perf + +syscalls: DefaultDict[int, int] = defaultdict(int) +for_comm = None +session = None + + +def print_syscall_totals(): + """Print aggregated statistics.""" + if for_comm is not None: + print(f"\nsyscall events for {for_comm}:\n") + else: + print("\nsyscall events:\n") + + print(f"{'event':<40} {'count':>10}") + print("---------------------------------------- -----------") + + for sc_id, val in sorted(syscalls.items(), + key=lambda kv: (-kv[1], kv[0])): + e_machine = getattr(session, "e_machine", 0) or 0 + # Mask out the x86_64 x32 ABI bit (__X32_SYSCALL_BIT = 0x40000000) before + # resolving the syscall number in the architecture's syscall table. + raw_sc_id = sc_id & ~0x40000000 + if e_machine: + name = perf.syscall_name(raw_sc_id, e_machine) or str(sc_id) + else: + name = perf.syscall_name(raw_sc_id) or str(sc_id) + print(f"{name:<40} {val:>10}") + + +def process_event(sample): + """Process a single sample event.""" + event_name = str(sample.evsel) + # Per-syscall syscalls:sys_enter_* tracepoints expose '__syscall_nr' (or 'nr') + # and may have an unrelated syscall argument named 'id' (e.g. sys_enter_clock_gettime), + # whereas raw_syscalls:sys_enter (and legacy pre-2.6.35 syscalls:sys_enter) expose 'id'. + if event_name.startswith("evsel(syscalls:sys_enter_"): + sc_id = getattr(sample, "__syscall_nr", None) + if sc_id is not None and not (0 <= (sc_id & ~0x40000000) <= 0xffff): + sc_id = None + if sc_id is None: + sc_id = getattr(sample, "nr", -1) + elif event_name.startswith(("evsel(raw_syscalls:sys_enter", "evsel(syscalls:sys_enter")): + sc_id = getattr(sample, "id", -1) + if not (0 <= (sc_id & ~0x40000000) <= 0xffff): + sc_id = getattr(sample, "__syscall_nr", -1) + if not (0 <= (sc_id & ~0x40000000) <= 0xffff): + sc_id = getattr(sample, "nr", -1) + else: + return + + # Mask out __X32_SYSCALL_BIT (0x40000000) when validating the syscall ID range. + if not (0 <= (sc_id & ~0x40000000) <= 0xffff): + return + + comm = "unknown" + try: + if session: + proc = session.find_thread(sample.sample_pid, sample.sample_tid) + if proc: + comm = proc.comm() or "unknown" + except (TypeError, AttributeError): + pass + + if for_comm and comm != for_comm: + return + syscalls[sc_id] += 1 + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("comm", nargs="?", help="Only report syscalls for comm") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + args = ap.parse_args() + for_comm = args.comm + try: + session = perf.session(perf.data(args.input), sample=process_event) + session.process_events() + print_syscall_totals() + finally: + # Break the reference cycle between session and process_event (whose module + # globals reference session) since perf.session lacks cyclic GC (tp_traverse). + session = None diff --git a/tools/perf/tests/shell/test_syscall_counts_python.sh b/tools/perf/tests/shell/test_syscall_counts_python.sh new file mode 100755 index 000000000000..0c4b75499209 --- /dev/null +++ b/tools/perf/tests/shell/test_syscall_counts_python.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# syscall-counts python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/syscall-counts.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, syscall-counts.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} + +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +test_file_mode() { + echo "Testing syscall-counts.py..." + # Some systems might not have raw_syscalls:sys_enter (e.g. stripped kernels or permissions) + if ! perf list | grep -q raw_syscalls:sys_enter; then + echo "Skipping test, raw_syscalls:sys_enter not found" + exit 2 + fi + + # Generate some syscall events + if ! perf record -e raw_syscalls:sys_enter -o "${temp_data}" -- sleep 0.5 2>/dev/null; then + echo "perf record failed (permissions?), skipping file mode test." + exit 2 + fi + + if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "File mode test failed." + err=1 + elif ! grep -E -q "^[a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then + echo "File mode output validation failed." + err=1 + else + echo "File mode test passed." + fi + + # Test with a comm argument + if ! "$PYTHON" "$script_path" -i "${temp_data}" "sleep" > "${temp_out}"; then + echo "Comm filter test failed." + err=1 + elif ! grep -E -q "^[a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then + echo "Comm filter output validation failed." + err=1 + else + echo "Comm filter test passed." + fi +} + +test_file_mode + +exit $err From 712af96a53eddfba4ec93bd59e69a4b5a4815890 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:50 -0700 Subject: [PATCH 198/425] perf python: Port syscall-counts-by-pid to perf module Port tools/perf/scripts/python/syscall-counts-by-pid.py to a standalone script in tools/perf/python/ using the perf module. Avoiding the embedded interpreter and per-event dictionary overhead improves execution speed by ~3.8x: ``` $ perf record -e raw_syscalls:sys_enter -a sleep 1 ... $ time perf script tools/perf/scripts/python/syscall-counts-by-pid.py perf ... real 0m3.852s user 0m3.512s sys 0m0.336s $ time python3 tools/perf/python/syscall-counts-by-pid.py perf ... real 0m1.011s user 0m0.963s sys 0m0.048s ``` Additional improvements compared to the legacy script: - Resolve architecture-specific syscall names via perf.syscall_name(id, session.e_machine) instead of host python-audit tables. - Support both raw_syscalls:sys_enter and individual syscalls:sys_enter_* tracepoints, and filter out invalid (> 0xffff or negative) syscall IDs. - Support filtering by numeric PID as well as command name (comm), and resolve process command names via session.find_thread(pid). Add a shell test (test_syscall_counts_by_pid_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/syscall-counts-by-pid.py | 112 ++++++++++++++++++ .../test_syscall_counts_by_pid_python.sh | 92 ++++++++++++++ 2 files changed, 204 insertions(+) create mode 100755 tools/perf/python/syscall-counts-by-pid.py create mode 100755 tools/perf/tests/shell/test_syscall_counts_by_pid_python.sh diff --git a/tools/perf/python/syscall-counts-by-pid.py b/tools/perf/python/syscall-counts-by-pid.py new file mode 100755 index 000000000000..fc0ec1bf7ca1 --- /dev/null +++ b/tools/perf/python/syscall-counts-by-pid.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Displays system-wide system call totals, broken down by syscall. +If a [comm] arg is specified, only syscalls called by [comm] are displayed. +""" +from __future__ import annotations + +import argparse +from collections import defaultdict +from typing import (Dict, Tuple) +import perf + +syscalls: Dict[Tuple[str, int, int], int] = defaultdict(int) +for_comm = None +for_pid = None +session = None + + +def print_syscall_totals(): + """Print aggregated statistics.""" + if for_comm is not None: + print(f"\nsyscall events for {for_comm}:\n") + elif for_pid is not None: + print(f"\nsyscall events for PID {for_pid}:\n") + else: + print("\nsyscall events:\n") + + print(f"{'comm [pid]/syscalls':<40} {'count':>10}") + print("---------------------------------------- -----------") + + sorted_keys = sorted(syscalls.keys(), key=lambda k: (k[0], k[1], -syscalls[k], k[2])) + current_comm_pid = None + for comm, pid, sc_id in sorted_keys: + if current_comm_pid != (comm, pid): + print(f"\n{comm} [{pid}]") + current_comm_pid = (comm, pid) + e_machine = getattr(session, "e_machine", 0) or 0 + # Mask out the x86_64 x32 ABI bit (__X32_SYSCALL_BIT = 0x40000000) before + # resolving the syscall number in the architecture's syscall table. + raw_sc_id = sc_id & ~0x40000000 + if e_machine: + name = perf.syscall_name(raw_sc_id, e_machine) or str(sc_id) + else: + name = perf.syscall_name(raw_sc_id) or str(sc_id) + print(f" {name:<38} {syscalls[(comm, pid, sc_id)]:>10}") + + +def process_event(sample): + """Process a single sample event.""" + event_name = str(sample.evsel) + # Per-syscall syscalls:sys_enter_* tracepoints expose '__syscall_nr' (or 'nr') + # and may have an unrelated syscall argument named 'id', whereas + # raw_syscalls:sys_enter (and legacy pre-2.6.35 syscalls:sys_enter) expose 'id'. + if event_name.startswith("evsel(syscalls:sys_enter_"): + sc_id = getattr(sample, "__syscall_nr", None) + if sc_id is not None and not (0 <= (sc_id & ~0x40000000) <= 0xffff): + sc_id = None + if sc_id is None: + sc_id = getattr(sample, "nr", -1) + elif event_name.startswith(("evsel(raw_syscalls:sys_enter", "evsel(syscalls:sys_enter")): + sc_id = getattr(sample, "id", -1) + if not (0 <= (sc_id & ~0x40000000) <= 0xffff): + sc_id = getattr(sample, "__syscall_nr", -1) + if not (0 <= (sc_id & ~0x40000000) <= 0xffff): + sc_id = getattr(sample, "nr", -1) + else: + return + + # Mask out __X32_SYSCALL_BIT (0x40000000) when validating the syscall ID range. + if not (0 <= (sc_id & ~0x40000000) <= 0xffff): + return + + pid = sample.sample_pid + + if for_pid is not None and pid != for_pid: + return + + comm = "unknown" + try: + if session: + proc = session.find_thread(sample.sample_pid, sample.sample_tid) + if proc: + comm = proc.comm() or "unknown" + except (TypeError, AttributeError): + pass + + if for_comm and comm != for_comm: + return + syscalls[(comm, pid, sc_id)] += 1 + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("filter", nargs="?", help="COMM or PID to filter by") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + args = ap.parse_args() + + if args.filter: + try: + for_pid = int(args.filter) + except ValueError: + for_comm = args.filter + + try: + session = perf.session(perf.data(args.input), sample=process_event) + session.process_events() + print_syscall_totals() + finally: + # Break the reference cycle between session and process_event (whose module + # globals reference session) since perf.session lacks cyclic GC (tp_traverse). + session = None diff --git a/tools/perf/tests/shell/test_syscall_counts_by_pid_python.sh b/tools/perf/tests/shell/test_syscall_counts_by_pid_python.sh new file mode 100755 index 000000000000..9f2ad27751a2 --- /dev/null +++ b/tools/perf/tests/shell/test_syscall_counts_by_pid_python.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# syscall-counts-by-pid python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/syscall-counts-by-pid.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, syscall-counts-by-pid.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} + +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +test_file_mode() { + echo "Testing syscall-counts-by-pid.py..." + # Some systems might not have raw_syscalls:sys_enter + if ! perf list | grep -q raw_syscalls:sys_enter; then + echo "Skipping test, raw_syscalls:sys_enter not found" + exit 2 + fi + + # Generate some syscall events + perf record -e raw_syscalls:sys_enter -a -o "${temp_data}" \ + -- sleep 0.5 >/dev/null 2>&1 || \ + { echo "Skipping test, perf record failed"; exit 2; } + + if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "File mode test failed." + err=1 + elif ! grep -E -q "^ [a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then + echo "File mode output validation failed." + err=1 + else + echo "File mode test passed." + fi + + # Test with a comm argument + if ! "$PYTHON" "$script_path" -i "${temp_data}" "sleep" > "${temp_out}"; then + echo "Comm filter test failed." + err=1 + elif ! grep -E -q "^ [a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then + echo "Comm filter output validation failed." + err=1 + else + echo "Comm filter test passed." + fi + + # Extract sleep PID from "sleep []" header and test with a numeric PID filter argument + sleep_pid=$(sed -n 's/^sleep \[\([0-9]\+\)\]$/\1/p' "${temp_out}" | head -n 1) + if [ -z "${sleep_pid}" ] || ! "$PYTHON" "$script_path" -i "${temp_data}" "${sleep_pid}" > "${temp_out}"; then + echo "PID filter test failed." + err=1 + elif ! grep -E -q "^ [a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then + echo "PID filter output validation failed." + err=1 + else + echo "PID filter test passed." + fi +} + +test_file_mode + +exit $err From b76c43d09b06da1bd8ff5be1ecfb532f6bf0a444 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:51 -0700 Subject: [PATCH 199/425] perf python: Port failed-syscalls-by-pid to perf module Port failed-syscalls-by-pid.py to a standalone script in tools/perf/python/ using the perf module: - Resolve architecture-aware syscall names using perf.syscall_name(id, session.e_machine) instead of host python-audit tables, supporting cross-architecture perf.data files without external dependencies. - Translate negative syscall return values into symbolic E* error names using perf.arch_strerrno(ret, session.e_machine), leveraging the auto-generated trace beauty architecture errno tables. - Encapsulate aggregation in a SyscallAnalyzer class using collections.defaultdict and add argparse filtering by comm, PID, and -i/--input. Add a shell test (test_failed_syscalls_by_pid_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/failed-syscalls-by-pid.py | 161 ++++++++++++++++++ .../test_failed_syscalls_by_pid_python.sh | 95 +++++++++++ 2 files changed, 256 insertions(+) create mode 100755 tools/perf/python/failed-syscalls-by-pid.py create mode 100755 tools/perf/tests/shell/test_failed_syscalls_by_pid_python.sh diff --git a/tools/perf/python/failed-syscalls-by-pid.py b/tools/perf/python/failed-syscalls-by-pid.py new file mode 100755 index 000000000000..51824a929ea4 --- /dev/null +++ b/tools/perf/python/failed-syscalls-by-pid.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Displays system-wide failed system call totals, broken down by pid. +If a [comm] or [pid] arg is specified, only syscalls called by it are displayed. + +Ported from tools/perf/scripts/python/failed-syscalls-by-pid.py +""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import os +import sys +from typing import Optional +import perf + + +def strerror(nr: int, e_machine: Optional[int] = None) -> str: + """Return error string for a given errno, accounting for target e_machine.""" + return perf.arch_strerrno(nr, e_machine) or f"Unknown {nr} errno" + + +class SyscallAnalyzer: + """Analyzes failed syscalls and aggregates counts.""" + + def __init__(self, for_comm: Optional[str] = None, for_pid: Optional[int] = None): + self.for_comm = for_comm + self.for_pid = for_pid + self.session: Optional[perf.session] = None + self.syscalls: dict[tuple[str, int, int, int], int] = defaultdict(int) + machine = os.uname().machine + self.host_64_bit = ("64" in machine or machine in ("s390x", "alpha") + or sys.maxsize > 0xffffffff) + + def process_event(self, sample: perf.sample_event) -> None: + """Process raw_syscalls:sys_exit and syscalls:sys_exit events.""" + event_name = str(sample.evsel) + if "raw_syscalls:sys_exit" not in event_name and "syscalls:sys_exit" not in event_name: + return + + pid = sample.sample_tid + comm = "Unknown" + if hasattr(self, 'session') and self.session: + try: + thread = self.session.find_thread(sample.sample_pid, pid) + if thread: + comm = thread.comm() or "Unknown" + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError): + pass + + if self.for_comm is not None and comm != self.for_comm: + return + if self.for_pid is not None and pid != self.for_pid: + return + + ret = getattr(sample, "ret", 0) + is_64_bit = (getattr(self.session, "is_64_bit", self.host_64_bit) + if self.session else self.host_64_bit) + # Tracepoint fields may be exposed as unsigned 32-bit or 64-bit integers. + # Guard the 32-bit 0xfffff000..0xffffffff conversion with 'not is_64_bit' so valid + # 64-bit user-space addresses in that range (e.g. mmap/brk) are not misclassified. + if ret > 0: + if not is_64_bit and 0xfffff000 <= ret <= 0xffffffff: + ret -= 0x100000000 + elif ret >= 0xfffffffffffff000: + ret -= 0x10000000000000000 + + # Linux kernel syscalls only return error codes in the [-4095, -1] (-MAX_ERRNO) + # range; other negative signed values are valid unsigned user-space pointers. + if -4095 <= ret < 0: + syscall_id = getattr(sample, "__syscall_nr", -1) + if not (0 <= (syscall_id & ~0x40000000) <= 0xffff): + syscall_id = getattr(sample, "nr", -1) + if not (0 <= (syscall_id & ~0x40000000) <= 0xffff): + syscall_id = getattr(sample, "sys_id", -1) + if not (0 <= (syscall_id & ~0x40000000) <= 0xffff): + syscall_id = getattr(sample, "id", -1) + + # Mask out the x86_64 x32 ABI bit (__X32_SYSCALL_BIT = 0x40000000). + raw_sc_id = syscall_id & ~0x40000000 + if 0 <= raw_sc_id <= 0xffff: + self.syscalls[(comm, pid, syscall_id, ret)] += 1 + + def print_summary(self) -> None: + """Print aggregated statistics.""" + if self.for_comm is not None: + print(f"\nsyscall errors for {self.for_comm}:\n") + elif self.for_pid is not None: + print(f"\nsyscall errors for PID {self.for_pid}:\n") + else: + print("\nsyscall errors:\n") + + print(f"{'comm [pid]':<30} {'count':>10}") + print(f"{'-' * 30:<30} {'-' * 10:>10}") + + sorted_keys = sorted( + self.syscalls.keys(), + key=lambda k: (k[0], k[1], k[2], -self.syscalls[k]) + ) + current_comm_pid = None + current_syscall = None + emach = getattr(self.session, "e_machine", 0) or 0 + for comm, pid, syscall_id, ret in sorted_keys: + if current_comm_pid != (comm, pid): + print(f"\n{comm} [{pid}]") + current_comm_pid = (comm, pid) + current_syscall = None + if current_syscall != syscall_id: + raw_sc_id = syscall_id & ~0x40000000 + try: + if emach: + name = perf.syscall_name(raw_sc_id, emach) or str(syscall_id) + else: + name = perf.syscall_name(raw_sc_id) or str(syscall_id) + except AttributeError: + name = str(syscall_id) + print(f" syscall: {name:<16}") + current_syscall = syscall_id + err_str = strerror(ret, emach) + count = self.syscalls[(comm, pid, syscall_id, ret)] + print(f" err = {err_str:<20} {count:10d}") + + +if __name__ == "__main__": + ap = argparse.ArgumentParser( + description="Displays system-wide failed system call totals, " + "broken down by pid.") + ap.add_argument("-i", "--input", default="perf.data", + help="Input file name") + ap.add_argument("filter", nargs="?", help="COMM or PID to filter by") + args = ap.parse_args() + + F_COMM = None + F_PID = None + + if args.filter: + try: + F_PID = int(args.filter) + except ValueError: + F_COMM = args.filter + + analyzer = SyscallAnalyzer(F_COMM, F_PID) + session = None + + try: + print("Press control+C to stop and show the summary") + session = perf.session(perf.data(args.input), sample=analyzer.process_event) + analyzer.session = session + session.process_events() + analyzer.print_summary() + except KeyboardInterrupt: + analyzer.print_summary() + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e: + print(f"Error processing events: {e}") + sys.exit(1) + finally: + # Break reference cycles between perf.session and analyzer.process_event + # because perf.session is a C extension type without cyclic GC (tp_traverse). + analyzer.session = None + session = None diff --git a/tools/perf/tests/shell/test_failed_syscalls_by_pid_python.sh b/tools/perf/tests/shell/test_failed_syscalls_by_pid_python.sh new file mode 100755 index 000000000000..5bbd1a0adb1e --- /dev/null +++ b/tools/perf/tests/shell/test_failed_syscalls_by_pid_python.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# failed-syscalls-by-pid python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/failed-syscalls-by-pid.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, failed-syscalls-by-pid.py not found at $script_path" + exit 2 +fi + +err=0 +temp_dir=$(mktemp -d /tmp/perf-failed-syscalls-by-pid-XXXXXX) +temp_data="${temp_dir}/perf.data" +temp_out="${temp_dir}/perf.out" + +cleanup() { + rm -rf "${temp_dir}" +} + +trap 'cleanup' EXIT TERM INT + +test_file_mode() { + echo "Testing failed-syscalls-by-pid.py..." + + # Check if syscalls:sys_exit is supported/readable + if ! perf record -e syscalls:sys_exit -o /dev/null -- true >/dev/null 2>&1; then + if ! perf record -e raw_syscalls:sys_exit -o /dev/null -- true >/dev/null 2>&1; then + echo "Skipping test, no syscalls:sys_exit or raw_syscalls:sys_exit event" + exit 2 + else + EVENT="raw_syscalls:sys_exit" + fi + else + EVENT="syscalls:sys_exit" + fi + + # Generate some events by running a command that should fail at least some syscall + # (e.g. failing stat on non-existent file). + # Using '|| true' because 'perf record' returns the exit code of 'ls', + # which fails with ENOENT + perf record -e "${EVENT}" -o "${temp_data}" -- ls /does_not_exist >/dev/null 2>&1 || true + if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 + fi + + # Run the script and check output + if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "failed-syscalls-by-pid test failed." + err=1 + elif ! grep -n -q "err = ENOENT" "${temp_out}"; then + echo "Failed to find expected failed syscalls" + cat "${temp_out}" + err=1 + elif ! "$PYTHON" "$script_path" -i "${temp_data}" "ls" > "${temp_out}.comm" || \ + ! grep -q "err = ENOENT" "${temp_out}.comm"; then + echo "failed-syscalls-by-pid comm filter test failed." + cat "${temp_out}.comm" + err=1 + else + ls_pid=$(sed -n 's/^ls \[\([0-9][0-9]*\)\].*/\1/p' "${temp_out}.comm" | head -n 1) + if [ -z "${ls_pid}" ] || \ + ! "$PYTHON" "$script_path" -i "${temp_data}" "${ls_pid}" > "${temp_out}.pid" || \ + ! grep -q "err = ENOENT" "${temp_out}.pid"; then + echo "failed-syscalls-by-pid PID filter test failed." + cat "${temp_out}.pid" 2>/dev/null || true + err=1 + else + echo "failed-syscalls-by-pid test passed." + fi + fi + rm -f "${temp_out}" "${temp_out}.comm" "${temp_out}.pid" +} + +test_file_mode + +exit $err From 4e3fe6987cbad8ab1c97815f4f27b2417d303233 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:52 -0700 Subject: [PATCH 200/425] perf python: Port failed-syscalls from Perl to perf module Replace the legacy Perl script failed-syscalls.pl with a standalone Python script in tools/perf/python/failed-syscalls.py using the perf Python module. Improvements compared to the legacy Perl script: - Remove the dependency on libperl and Perf::Trace::Util. - Support both syscalls:sys_exit_* and raw_syscalls:sys_exit events (the legacy script only handled raw_syscalls::sys_exit). - Use session.is_64_bit to distinguish 32-bit vs 64-bit unsigned error return ranges (0xfffff000..0xffffffff vs >= 0xfffffffffffff000) so valid 64-bit syscalls returning ~4GB values are not misclassified as 32-bit negative errors. - Add argparse CLI support (-i/--input and optional comm filter) and full type annotations. Add a shell test (test_failed_syscalls_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/failed-syscalls.py | 94 +++++++++++++++++++ .../shell/test_failed_syscalls_python.sh | 80 ++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100755 tools/perf/python/failed-syscalls.py create mode 100755 tools/perf/tests/shell/test_failed_syscalls_python.sh diff --git a/tools/perf/python/failed-syscalls.py b/tools/perf/python/failed-syscalls.py new file mode 100755 index 000000000000..6d1fa4e5ef89 --- /dev/null +++ b/tools/perf/python/failed-syscalls.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +"""Failed system call counts.""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import sys +from typing import Optional +import perf + +class FailedSyscalls: + """Tracks and displays failed system call totals.""" + def __init__(self, comm: Optional[str] = None) -> None: + self.failed_syscalls: dict[str, int] = defaultdict(int) + self.for_comm = comm + self.session: Optional[perf.session] = None + + def process_event(self, sample: perf.sample_event) -> None: + """Process sys_exit events.""" + event_name = str(sample.evsel) + if not event_name.startswith("evsel(syscalls:sys_exit") and \ + not event_name.startswith("evsel(raw_syscalls:sys_exit"): + return + + try: + ret = sample.ret + except AttributeError: + print("ERROR: tracepoint fields missed", file=sys.stderr) + sys.exit(1) + + if ret > 0: + if ret >= 0xfffffffffffff000: # 64-bit negative errors + ret -= 0x10000000000000000 + elif 0xfffff000 <= ret <= 0xffffffff: # 32-bit negative errors + assert self.session is not None + if not self.session.is_64_bit: + ret -= 0x100000000 + + # Linux kernel syscalls only return error codes in the [-4095, -1] (-MAX_ERRNO) + # range; other negative signed values are valid unsigned user-space addresses. + if not -4095 <= ret < 0: + return + + assert self.session is not None + try: + thread = self.session.find_thread(sample.sample_pid, sample.sample_tid) + comm = (thread.comm() if thread else None) or "unknown" + except (TypeError, AttributeError): + # find_thread returns None when the thread isn't known. + comm = "unknown" + + if self.for_comm and comm != self.for_comm: + return + + self.failed_syscalls[comm] += 1 + + def print_totals(self) -> None: + """Print summary table.""" + print("\nfailed syscalls by comm:\n") + print(f"{'comm':<20s} {'# errors':>10s}") + print(f"{'-'*20} {'-'*10}") + + for comm, val in sorted(self.failed_syscalls.items(), + key=lambda kv: (-kv[1], kv[0])): + print(f"{comm:<20s} {val:10d}") + + def run(self, input_file: str) -> None: + """Run the session.""" + self.session = perf.session(perf.data(input_file), sample=self.process_event) + try: + self.session.process_events() + finally: + # Break the reference cycle between perf.session and self.process_event + # because perf.session lacks cyclic GC support (tp_traverse). + self.session = None + self.print_totals() + +def main() -> None: + """Main function.""" + parser = argparse.ArgumentParser(description="Trace failed syscalls") + parser.add_argument("comm", nargs="?", help="Filter by command name") + parser.add_argument("-i", "--input", default="perf.data", help="Input file") + args = parser.parse_args() + + analyzer = FailedSyscalls(args.comm) + try: + analyzer.run(args.input) + except IOError as e: + print(e, file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_failed_syscalls_python.sh b/tools/perf/tests/shell/test_failed_syscalls_python.sh new file mode 100755 index 000000000000..0cabb127d7e9 --- /dev/null +++ b/tools/perf/tests/shell/test_failed_syscalls_python.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# failed-syscalls python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/failed-syscalls.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, failed-syscalls.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing failed-syscalls.py..." + +# Check if sys_exit event can be recorded +if ! perf record -e raw_syscalls:sys_exit -o /dev/null -- true >/dev/null 2>&1; then + if ! perf record -e syscalls:sys_exit -o /dev/null -- true >/dev/null 2>&1; then + echo "Skipping test, no permission or support for sys_exit event" + exit 2 + else + EVENT="syscalls:sys_exit" + fi +else + EVENT="raw_syscalls:sys_exit" +fi + +# Run perf record with a command that fails a syscall (ls non-existent file). +# ls exits with non-zero, so perf record returns non-zero exit code of the workload. +perf record -e "${EVENT}" -o "${temp_data}" \ + -- ls /nonexistent_file_for_test >/dev/null 2>&1 || true + +if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 +fi + +# Check that the script executes +if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "failed-syscalls.py test failed" + err=1 +else + if ! grep -q "failed syscalls by comm" "${temp_out}" || \ + ! grep -Eq '^ls[[:space:]]+[0-9]+' "${temp_out}"; then + echo "Failed to find the metrics table header or expected error" + err=1 + else + echo "failed-syscalls test passed." + fi +fi +rm -f "${temp_out}" + +exit $err From b83f0bacf5e4f9369257f5c1191df02ab1be5602 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:53 -0700 Subject: [PATCH 201/425] perf python: Port sctop to perf module Port sctop.py from tools/perf/scripts/python/ to a standalone script in tools/perf/python/ using an SCTopAnalyzer class structure. Improvements compared to the legacy script: - Support both offline perf.data analysis (via perf.session, advancing display intervals deterministically using event timestamps) and live monitoring (via LiveSession with automatic tracepoint fallback from raw_syscalls:sys_enter to syscalls:sys_enter_*). - Resolve architecture-aware syscall names via perf.syscall_name(id, session.e_machine) without requiring python-audit. - Replace unsafe signal.SIGALRM dictionary mutation and os.popen("clear") subshell spawning with a synchronized threading.Lock / threading.Event timer and direct ANSI terminal escape sequences ('\x1b[2J\x1b[H'). Add a shell test (test_sctop_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/sctop.py | 250 ++++++++++++++++++++ tools/perf/tests/shell/test_sctop_python.sh | 78 ++++++ 2 files changed, 328 insertions(+) create mode 100755 tools/perf/python/sctop.py create mode 100755 tools/perf/tests/shell/test_sctop_python.sh diff --git a/tools/perf/python/sctop.py b/tools/perf/python/sctop.py new file mode 100755 index 000000000000..42e95ecfffad --- /dev/null +++ b/tools/perf/python/sctop.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +System call top + +Periodically displays system-wide system call totals, broken down by +syscall. If a [comm] arg is specified, only syscalls called by +[comm] are displayed. If an [interval] arg is specified, the display +will be refreshed every [interval] seconds. The default interval is +3 seconds. + +Ported from tools/perf/scripts/python/sctop.py +""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import os +import sys +import threading +from typing import Optional +import perf +from perf_live import LiveSession + + +class SCTopAnalyzer: + """Periodically displays system-wide system call totals.""" + + def __init__(self, for_comm: Optional[str], interval: int, offline: bool = False): + self.for_comm = for_comm + self.interval = interval + self.syscalls: dict[int, int] = defaultdict(int) + self.comm_cache: dict[int, str] = {} + self.lock = threading.Lock() + self.stop_event = threading.Event() + self.thread = threading.Thread(target=self.print_syscall_totals) + self.offline = offline + self.own_pid = os.getpid() + self.last_print_time: Optional[int] = None + self.session: Optional[perf.session] = None + self.e_machine: Optional[int] = None + + def syscall_name(self, syscall_id: int) -> str: + """Lookup syscall name by ID.""" + # Mask out the x86_64 x32 ABI bit (__X32_SYSCALL_BIT = 0x40000000) before + # resolving the syscall number in the architecture's syscall table. + raw_sc_id = syscall_id & ~0x40000000 + try: + e_machine = getattr(self.session, "e_machine", self.e_machine) + if e_machine is not None: + name = perf.syscall_name(raw_sc_id, e_machine) + else: + name = perf.syscall_name(raw_sc_id) + if name is not None: + return name + except (TypeError, OverflowError): + pass + return str(syscall_id) + + def process_event(self, sample: perf.sample_event) -> None: + """Collect syscall events.""" + if not self.offline and sample.sample_pid == self.own_pid: + return + + name = str(sample.evsel) + # Per-syscall syscalls:sys_enter_* tracepoints expose '__syscall_nr' (or 'nr') + # and may have an unrelated syscall argument named 'id', whereas + # raw_syscalls:sys_enter (and legacy pre-2.6.35 syscalls:sys_enter) expose 'id'. + if name.startswith("evsel(syscalls:sys_enter_"): + syscall_id = getattr(sample, "__syscall_nr", -1) + if not (0 <= (syscall_id & ~0x40000000) <= 0xffff): + syscall_id = getattr(sample, "nr", -1) + elif name.startswith(("evsel(raw_syscalls:sys_enter", "evsel(syscalls:sys_enter")): + syscall_id = getattr(sample, "id", -1) + if not (0 <= (syscall_id & ~0x40000000) <= 0xffff): + syscall_id = getattr(sample, "__syscall_nr", -1) + if not (0 <= (syscall_id & ~0x40000000) <= 0xffff): + syscall_id = getattr(sample, "nr", -1) + else: + syscall_id = -1 + + skip = False + with self.lock: + if self.for_comm is not None: + is_execve = (0 <= (syscall_id & ~0x40000000) <= 0xffff and + self.syscall_name(syscall_id) in ("execve", "execveat")) + if is_execve: + self.comm_cache.pop(sample.sample_pid, None) + + comm = "Unknown" + if hasattr(self, 'session') and self.session: + # In offline perf.data mode, query session.find_thread() directly + # so PERF_RECORD_COMM updates after execve (e.g. perf -> sleep) + # are reflected immediately rather than returning a stale cached comm. + try: + proc = self.session.find_thread(sample.sample_pid, sample.sample_tid) + if proc: + comm = proc.comm() or "Unknown" + except TypeError: + pass + if comm != "Unknown" and not is_execve: + self.comm_cache[sample.sample_pid] = comm + elif sample.sample_pid in self.comm_cache: + comm = self.comm_cache[sample.sample_pid] + elif sample.sample_pid in self.comm_cache: + comm = self.comm_cache[sample.sample_pid] + else: + try: + with open(f"/proc/{sample.sample_pid}/comm", "r", + encoding="utf-8", errors="replace") as f: + comm = f.read().strip() + except OSError: + comm = "Unknown" + # Cache both matching and non-matching comms (including "Unknown" + # when a PID has exited or is inaccessible) so live system-wide + # tracing does not re-open /proc//comm on every syscall. + # Do not cache during sys_enter(execve/execveat) since /proc//comm + # still holds the pre-exec command name until the syscall completes. + if not is_execve: + self.comm_cache[sample.sample_pid] = comm + + if comm != self.for_comm: + skip = True + + is_enter = (name.startswith("evsel(raw_syscalls:sys_enter") or + name.startswith("evsel(syscalls:sys_enter")) + if not skip and is_enter and 0 <= (syscall_id & ~0x40000000) <= 0xffff: + self.syscalls[syscall_id] += 1 + + if self.offline and hasattr(sample, "sample_time"): + interval_ns = self.interval * (10 ** 9) + if self.last_print_time is None: + self.last_print_time = sample.sample_time + elif sample.sample_time - self.last_print_time >= interval_ns: + self.print_current_totals() + self.last_print_time = sample.sample_time + + def print_current_totals(self): + """Print current syscall totals.""" + # Clear terminal + if not self.offline: + print("\x1b[2J\x1b[H", end="") + else: + print() + + with self.lock: + for_comm = self.for_comm + if for_comm is not None: + print(f"\nsyscall events for {for_comm}:\n") + else: + print("\nsyscall events:\n") + + print(f"{'event':40s} {'count':10s}") + print(f"{'-' * 40:40s} {'-' * 10:10s}") + + with self.lock: + current_syscalls = list(self.syscalls.items()) + self.syscalls.clear() + self.comm_cache.clear() + + current_syscalls.sort(key=lambda kv: (-kv[1], kv[0])) + + for syscall_id, val in current_syscalls: + print(f"{self.syscall_name(syscall_id):<40s} {val:10d}") + + def print_syscall_totals(self): + """Periodically print syscall totals.""" + while not self.stop_event.is_set(): + self.print_current_totals() + self.stop_event.wait(self.interval) + # Print final batch + self.print_current_totals() + + def start(self): + """Start the background thread.""" + self.thread.start() + + def stop(self): + """Stop the background thread.""" + self.stop_event.set() + self.thread.join() + + +def main(): + """Main function.""" + ap = argparse.ArgumentParser(description="System call top") + ap.add_argument("args", nargs="*", help="[comm] [interval] or [interval]") + ap.add_argument("-i", "--input", help="Input file name") + args = ap.parse_args() + + for_comm = None + default_interval = 3 + interval = default_interval + + if len(args.args) > 2: + print("Usage: python sctop.py [comm] [interval]") + sys.exit(1) + + if len(args.args) > 1: + for_comm = args.args[0] + try: + interval = int(args.args[1]) + except ValueError: + print(f"Invalid interval: {args.args[1]}") + sys.exit(1) + elif len(args.args) > 0: + try: + interval = int(args.args[0]) + except ValueError: + for_comm = args.args[0] + interval = default_interval + + analyzer = SCTopAnalyzer(for_comm, interval, offline=bool(args.input)) + session = None + + try: + if args.input: + session = perf.session(perf.data(args.input), sample=analyzer.process_event) + analyzer.session = session + session.process_events() + analyzer.e_machine = getattr(session, "e_machine", None) + else: + try: + live_session = LiveSession( + "raw_syscalls:sys_enter", sample_callback=analyzer.process_event + ) + except OSError: + live_session = LiveSession( + "syscalls:sys_enter_*", sample_callback=analyzer.process_event + ) + analyzer.start() + live_session.run() + except KeyboardInterrupt: + pass + except (OSError, IOError) as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + finally: + if args.input: + analyzer.print_current_totals() + # Break the reference cycle between perf.session and analyzer.process_event + # because perf.session lacks cyclic GC support (tp_traverse). + analyzer.session = None + session = None + elif analyzer.thread.is_alive(): + analyzer.stop() + + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_sctop_python.sh b/tools/perf/tests/shell/test_sctop_python.sh new file mode 100755 index 000000000000..cd38cdd4794c --- /dev/null +++ b/tools/perf/tests/shell/test_sctop_python.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# sctop python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/sctop.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, sctop.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing sctop.py..." + +# Create a perf.data file. +if perf list | grep -q "raw_syscalls:sys_enter"; then + perf record -e raw_syscalls:sys_enter -a -o "${temp_data}" \ + -- sleep 0.1 >/dev/null 2>&1 || \ + { echo "Skipping test, perf record failed"; exit 2; } +else + echo "Skipping test, no raw_syscalls:sys_enter event" + exit 2 +fi + +if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 +fi + +# Check that the script executes +if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "sctop.py test failed" + err=1 +elif ! grep -E -q "[0-9]+$" "${temp_out}"; then + echo "Failed to find metric data rows in default run" + err=1 +elif ! "$PYTHON" "$script_path" -i "${temp_data}" sleep 1 > "${temp_out}"; then + echo "sctop.py comm+interval test failed" + err=1 +else + if ! grep -E -q "[0-9]+$" "${temp_out}"; then + echo "Failed to find metric data rows" + err=1 + else + echo "sctop test passed." + fi +fi +rm -f "${temp_out}" + +exit $err From 47b00c38780d27bedb91b8090f74e0523ccbc95e Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:54 -0700 Subject: [PATCH 202/425] perf python: Port rw-by-file from Perl to perf module Replace the legacy Perl script rw-by-file.pl with a standalone Python script in tools/perf/python/rw-by-file.py using the perf Python module. Improvements compared to the legacy Perl script: - Remove the dependency on libperl and Perf::Trace::Util. - Encapsulate per-file-descriptor read/write byte and call count aggregation in an RwByFile class using perf.session and resolve thread command names via session.find_thread(pid, sample_tid). - Add argparse CLI support (-i/--input and target program filter) and full type annotations. Add a shell test (test_rw_by_file_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/rw-by-file.py | 113 ++++++++++++++++++ .../tests/shell/test_rw_by_file_python.sh | 68 +++++++++++ 2 files changed, 181 insertions(+) create mode 100755 tools/perf/python/rw-by-file.py create mode 100755 tools/perf/tests/shell/test_rw_by_file_python.sh diff --git a/tools/perf/python/rw-by-file.py b/tools/perf/python/rw-by-file.py new file mode 100755 index 000000000000..b33330261606 --- /dev/null +++ b/tools/perf/python/rw-by-file.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Display r/w activity for files read/written to for a given program.""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import sys +from typing import Optional, Dict +import perf + +class RwByFile: + """Tracks and displays read/write activity by file descriptor.""" + def __init__(self, comm: str) -> None: + self.for_comm = comm + self.reads: Dict[int, Dict[str, int]] = defaultdict( + lambda: {"bytes_requested": 0, "total_reads": 0} + ) + self.writes: Dict[int, Dict[str, int]] = defaultdict( + lambda: {"bytes_written": 0, "total_writes": 0} + ) + self.unhandled: Dict[str, int] = defaultdict(int) + self.session: Optional[perf.session] = None + + def process_event(self, sample: perf.sample_event) -> None: + """Process events.""" + event_name = str(sample.evsel) + if event_name.startswith("evsel(") and event_name.endswith(")"): + event_name = event_name[6:-1] + event_name = "".join(c if c.isprintable() else "?" for c in event_name) + + pid = sample.sample_pid + assert self.session is not None + try: + thread = self.session.find_thread(pid, sample.sample_tid) + comm = (thread.comm() if thread else None) or "unknown" + except (TypeError, AttributeError): + comm = "unknown" + + if comm != self.for_comm: + return + + if event_name == "syscalls:sys_enter_read": + try: + fd = sample.fd + count = sample.count + self.reads[fd]["bytes_requested"] += count + self.reads[fd]["total_reads"] += 1 + except AttributeError: + self.unhandled[event_name] += 1 + elif event_name == "syscalls:sys_enter_write": + try: + fd = sample.fd + count = sample.count + self.writes[fd]["bytes_written"] += count + self.writes[fd]["total_writes"] += 1 + except AttributeError: + self.unhandled[event_name] += 1 + else: + self.unhandled[event_name] += 1 + + def print_totals(self) -> None: + """Print summary tables.""" + print(f"file read counts for {self.for_comm}:\n") + print(f"{'fd':>6s} {'# reads':>10s} {'bytes_requested':>15s}") + print(f"{'-'*6} {'-'*10} {'-'*15}") + + for fd, data in sorted(self.reads.items(), + key=lambda kv: kv[1]["bytes_requested"], reverse=True): + print(f"{fd:6d} {data['total_reads']:10d} {data['bytes_requested']:15d}") + + print(f"\nfile write counts for {self.for_comm}:\n") + print(f"{'fd':>6s} {'# writes':>10s} {'bytes_written':>15s}") + print(f"{'-'*6} {'-'*10} {'-'*15}") + + for fd, data in sorted(self.writes.items(), + key=lambda kv: kv[1]["bytes_written"], reverse=True): + print(f"{fd:6d} {data['total_writes']:10d} {data['bytes_written']:15d}") + + if self.unhandled: + print("\nunhandled events:\n") + print(f"{'event':<40s} {'count':>10s}") + print(f"{'-'*40} {'-'*10}") + for event_name, count in self.unhandled.items(): + print(f"{event_name:<40s} {count:10d}") + + def run(self, input_file: str) -> None: + """Run the session.""" + self.session = perf.session(perf.data(input_file), sample=self.process_event) + try: + self.session.process_events() + finally: + # Break the reference cycle between perf.session and self.process_event + # because perf.session lacks cyclic GC support (tp_traverse). + self.session = None + self.print_totals() + +def main() -> None: + """Main function.""" + parser = argparse.ArgumentParser(description="Trace r/w activity by file") + parser.add_argument("comm", help="Filter by command name") + parser.add_argument("-i", "--input", default="perf.data", help="Input file") + args = parser.parse_args() + + analyzer = RwByFile(args.comm) + try: + analyzer.run(args.input) + except IOError as e: + print(e, file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_rw_by_file_python.sh b/tools/perf/tests/shell/test_rw_by_file_python.sh new file mode 100755 index 000000000000..98218bf8a39e --- /dev/null +++ b/tools/perf/tests/shell/test_rw_by_file_python.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# rw-by-file python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/rw-by-file.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, rw-by-file.py not found at $script_path" + exit 2 +fi + +err=0 +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing rw-by-file.py..." + +# Create a perf.data file. Try to get tracepoint data. +if perf list | grep -q "syscalls:sys_enter_read"; then + perf record -e syscalls:sys_enter_read,syscalls:sys_enter_write -a -o "${temp_data}" \ + -- dd if=/dev/urandom of=/dev/null bs=1M count=10 >/dev/null 2>&1 || \ + { echo "Skipping test, perf record failed"; exit 2; } +else + echo "Skipping test, no syscalls:sys_enter_read event" + exit 2 +fi + +if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 +fi + +# Check that the script executes - filtering for "dd" since that's what we ran +if ! "$PYTHON" "$script_path" -i "${temp_data}" "dd" > "${temp_out}"; then + echo "rw-by-file.py test failed" + err=1 +else + if ! grep -E -q "^ *[0-9]+" "${temp_out}"; then + echo "Failed to find metric data rows" + err=1 + else + echo "rw-by-file test passed." + fi +fi +rm -f "${temp_out}" + +exit $err From d935308817b348443acad0a7dbb82689f0e48475 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:55 -0700 Subject: [PATCH 203/425] perf python: Port rw-by-pid from Perl to perf module Replace the legacy Perl script rw-by-pid.pl with a standalone Python script in tools/perf/python/rw-by-pid.py using the perf Python module. Improvements compared to the legacy Perl script: - Remove the dependency on libperl and Perf::Trace::Util. - Convert unsigned 32-bit and 64-bit error return values (0xfffff000..0xffffffff and >= 0x8000000000000000) to negative errnos so failed read/write syscalls are recorded in the error table rather than inflating bytes_read / bytes_written. - Add argparse CLI support (-i/--input) and full type annotations. Add a shell test (test_rw_by_pid_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/rw-by-pid.py | 205 ++++++++++++++++++ .../perf/tests/shell/test_rw_by_pid_python.sh | 75 +++++++ 2 files changed, 280 insertions(+) create mode 100755 tools/perf/python/rw-by-pid.py create mode 100755 tools/perf/tests/shell/test_rw_by_pid_python.sh diff --git a/tools/perf/python/rw-by-pid.py b/tools/perf/python/rw-by-pid.py new file mode 100755 index 000000000000..8e2d1dbd9a08 --- /dev/null +++ b/tools/perf/python/rw-by-pid.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Display r/w activity for all processes.""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import sys +from typing import Optional, Dict, List, Tuple, Any +import perf + +class RwByPid: + """Tracks and displays read/write activity by PID.""" + def __init__(self) -> None: + self.reads: Dict[int, Dict[str, Any]] = defaultdict( + lambda: { + "bytes_requested": 0, + "bytes_read": 0, + "total_reads": 0, + "comm": "", + "errors": defaultdict(int), + } + ) + self.writes: Dict[int, Dict[str, Any]] = defaultdict( + lambda: { + "bytes_requested": 0, + "bytes_written": 0, + "total_writes": 0, + "comm": "", + "errors": defaultdict(int), + } + ) + self.unhandled: Dict[str, int] = defaultdict(int) + self.session: Optional[perf.session] = None + + def _handle_sys_enter_read(self, pid: int, comm: str, sample: perf.sample_event) -> None: + try: + count = sample.count + self.reads[pid]["bytes_requested"] += count + self.reads[pid]["total_reads"] += 1 + self.reads[pid]["comm"] = comm + except AttributeError: + self.unhandled["sys_enter_read_attr_err"] += 1 + + def _handle_sys_exit_read(self, pid: int, sample: perf.sample_event, comm: str = "") -> None: + try: + ret = sample.ret + # Convert unsigned 64-bit or 32-bit tracepoint return values to signed integers. + # Because kernel read/write transfers are bounded by MAX_RW_COUNT (0x7ffff000 < 2 GiB), + # 0xfffff000..0xffffffff is always a 32-bit negative errno even on 64-bit hosts. + if ret >= 0x8000000000000000: + ret -= 0x10000000000000000 + elif 0xfffff000 <= ret <= 0xffffffff: + ret -= 0x100000000 + if not self.reads[pid]["comm"] and comm: + self.reads[pid]["comm"] = comm + if ret >= 0: + self.reads[pid]["bytes_read"] += ret + else: + self.reads[pid]["errors"][ret] += 1 + except AttributeError: + self.unhandled["sys_exit_read_attr_err"] += 1 + + def _handle_sys_enter_write(self, pid: int, comm: str, sample: perf.sample_event) -> None: + try: + count = sample.count + self.writes[pid]["bytes_requested"] += count + self.writes[pid]["total_writes"] += 1 + self.writes[pid]["comm"] = comm + except AttributeError: + self.unhandled["sys_enter_write_attr_err"] += 1 + + def _handle_sys_exit_write(self, pid: int, sample: perf.sample_event, comm: str = "") -> None: + try: + ret = sample.ret + # Convert unsigned 64-bit or 32-bit tracepoint return values to signed integers. + # Because kernel read/write transfers are bounded by MAX_RW_COUNT (0x7ffff000 < 2 GiB), + # 0xfffff000..0xffffffff is always a 32-bit negative errno even on 64-bit hosts. + if ret >= 0x8000000000000000: + ret -= 0x10000000000000000 + elif 0xfffff000 <= ret <= 0xffffffff: + ret -= 0x100000000 + if not self.writes[pid]["comm"] and comm: + self.writes[pid]["comm"] = comm + if ret >= 0: + self.writes[pid]["bytes_written"] += ret + else: + self.writes[pid]["errors"][ret] += 1 + except AttributeError: + self.unhandled["sys_exit_write_attr_err"] += 1 + + def process_event(self, sample: perf.sample_event) -> None: + """Process events.""" + event_name = str(sample.evsel) + if event_name.startswith("evsel(") and event_name.endswith(")"): + event_name = event_name[6:-1] + event_name = "".join(c if c.isprintable() else "?" for c in event_name) + pid = sample.sample_pid + + assert self.session is not None + try: + thread = self.session.find_thread(pid, sample.sample_tid) + comm = (thread.comm() if thread else None) or "unknown" + except (TypeError, AttributeError): + comm = "unknown" + comm = "".join(c if c.isprintable() else "?" for c in comm) + + if event_name == "syscalls:sys_enter_read": + self._handle_sys_enter_read(pid, comm, sample) + elif event_name == "syscalls:sys_exit_read": + self._handle_sys_exit_read(pid, sample, comm) + elif event_name == "syscalls:sys_enter_write": + self._handle_sys_enter_write(pid, comm, sample) + elif event_name == "syscalls:sys_exit_write": + self._handle_sys_exit_write(pid, sample, comm) + else: + self.unhandled[event_name] += 1 + + def print_totals(self) -> None: + """Print summary tables.""" + print("read counts by pid:\n") + print( + f"{'pid':>6s} {'comm':<20s} {'# reads':>10s} " + f"{'bytes_requested':>15s} {'bytes_read':>10s}" + ) + print(f"{'-'*6} {'-'*20} {'-'*10} {'-'*15} {'-'*10}") + + for pid, data in sorted(self.reads.items(), + key=lambda kv: kv[1]["bytes_read"], reverse=True): + print( + f"{pid:6d} {data['comm']:<20s} {data['total_reads']:10d} " + f"{data['bytes_requested']:15d} {data['bytes_read']:10d}" + ) + + print("\nfailed reads by pid:\n") + print(f"{'pid':>6s} {'comm':<20s} {'error #':>7s} {'# errors':>10s}") + print(f"{'-'*6} {'-'*20} {'-'*7} {'-'*10}") + + errcounts: List[Tuple[int, str, int, int]] = [] + for pid, data in self.reads.items(): + for error, count in data["errors"].items(): + errcounts.append((pid, data["comm"], error, count)) + + for pid, comm, error, count in sorted(errcounts, key=lambda x: x[3], reverse=True): + print(f"{pid:6d} {comm:<20s} {error:7d} {count:10d}") + + print("\nwrite counts by pid:\n") + print( + f"{'pid':>6s} {'comm':<20s} {'# writes':>10s} " + f"{'bytes_requested':>15s} {'bytes_written':>15s}" + ) + print(f"{'-'*6} {'-'*20} {'-'*10} {'-'*15} {'-'*15}") + + for pid, data in sorted(self.writes.items(), + key=lambda kv: kv[1]["bytes_written"], reverse=True): + print( + f"{pid:6d} {data['comm']:<20s} {data['total_writes']:10d} " + f"{data['bytes_requested']:15d} {data['bytes_written']:15d}" + ) + + print("\nfailed writes by pid:\n") + print(f"{'pid':>6s} {'comm':<20s} {'error #':>7s} {'# errors':>10s}") + print(f"{'-'*6} {'-'*20} {'-'*7} {'-'*10}") + + errcounts = [] + for pid, data in self.writes.items(): + for error, count in data["errors"].items(): + errcounts.append((pid, data["comm"], error, count)) + + for pid, comm, error, count in sorted(errcounts, key=lambda x: x[3], reverse=True): + print(f"{pid:6d} {comm:<20s} {error:7d} {count:10d}") + + if self.unhandled: + print("\nunhandled events:\n") + print(f"{'event':<40s} {'count':>10s}") + print(f"{'-'*40} {'-'*10}") + for event_name, count in self.unhandled.items(): + print(f"{event_name:<40s} {count:10d}") + + def run(self, input_file: str) -> None: + """Run the session.""" + self.session = perf.session(perf.data(input_file), sample=self.process_event) + try: + self.session.process_events() + finally: + # Break the reference cycle between perf.session and self.process_event + # because perf.session lacks cyclic GC support (tp_traverse). + self.session = None + self.print_totals() + +def main() -> None: + """Main function.""" + parser = argparse.ArgumentParser(description="Trace r/w activity by PID") + parser.add_argument("-i", "--input", default="perf.data", help="Input file") + args = parser.parse_args() + + analyzer = RwByPid() + try: + analyzer.run(args.input) + except IOError as e: + print(e, file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_rw_by_pid_python.sh b/tools/perf/tests/shell/test_rw_by_pid_python.sh new file mode 100755 index 000000000000..da8b065d43de --- /dev/null +++ b/tools/perf/tests/shell/test_rw_by_pid_python.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# rw-by-pid python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/rw-by-pid.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, rw-by-pid.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing rw-by-pid.py..." + +# Create a perf.data file. Try to get tracepoint data. +if perf list | grep -q "syscalls:sys_enter_read"; then + ev="syscalls:sys_enter_read,syscalls:sys_exit_read" + ev="${ev},syscalls:sys_enter_write,syscalls:sys_exit_write" + perf record -e "$ev" -a -o "${temp_data}" \ + -- dd if=/dev/urandom of=/dev/null bs=1M count=10 >/dev/null 2>&1 || \ + { echo "Skipping test, perf record failed"; exit 2; } +else + echo "Skipping test, no syscalls:sys_enter_read event" + exit 2 +fi + +if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 +fi + +# Check that the script executes +if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "rw-by-pid.py test failed" + err=1 +else + if ! grep -E -q "^ *[0-9]+" "${temp_out}"; then + echo "Failed to find metric data rows in output" + cat "${temp_out}" + err=1 + else + echo "rw-by-pid test passed." + fi +fi +rm -f "${temp_out}" + +exit $err From 605fd1c8b7e6677428cd8395b8191d0846e18adf Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:56 -0700 Subject: [PATCH 204/425] perf python: Port rwtop from Perl to perf module Replace the legacy Perl script rwtop.pl with a standalone Python script in tools/perf/python/rwtop.py using the perf Python module. Improvements compared to the legacy Perl script: - Remove the dependency on libperl and Perf::Trace::Util. - Support both offline perf.data files (via perf.session) and live recording (via LiveSession with --live), driving periodic interval summaries from event timestamps rather than wall-clock SIGALRM timers so both live and offline runs produce deterministic output. - Convert unsigned 32-bit and 64-bit error return values (0xfffff000..0xffffffff and >= 0x8000000000000000) to negative errnos so failed read/write syscalls are recorded in the error table rather than inflating bytes_read / bytes_written. - Sanitize non-printable characters in /proc//comm to prevent terminal control sequence injection. Add a shell test (test_rwtop_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/rwtop.py | 256 ++++++++++++++++++++ tools/perf/tests/shell/test_rwtop_python.sh | 74 ++++++ 2 files changed, 330 insertions(+) create mode 100755 tools/perf/python/rwtop.py create mode 100755 tools/perf/tests/shell/test_rwtop_python.sh diff --git a/tools/perf/python/rwtop.py b/tools/perf/python/rwtop.py new file mode 100755 index 000000000000..86674d7c8f6f --- /dev/null +++ b/tools/perf/python/rwtop.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Periodically displays system-wide r/w call activity, broken down by pid.""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import os +import sys +from typing import Optional, Dict, Any +import perf +from perf_live import LiveSession + +class RwTop: + """Periodically displays system-wide r/w call activity.""" + def __init__(self, interval: int = 3, nlines: int = 20) -> None: + self.offline = False + self.interval_ns = interval * 1000000000 + self.nlines = nlines + self.reads: Dict[int, Dict[str, Any]] = defaultdict( + lambda: { + "bytes_requested": 0, + "bytes_read": 0, + "total_reads": 0, + "comm": "", + "errors": defaultdict(int), + } + ) + self.writes: Dict[int, Dict[str, Any]] = defaultdict( + lambda: { + "bytes_requested": 0, + "bytes_written": 0, + "total_writes": 0, + "comm": "", + "errors": defaultdict(int), + } + ) + self.unhandled: Dict[str, int] = defaultdict(int) + self.comm_cache: Dict[int, str] = {} + self.session: Optional[perf.session] = None + self.last_print_time: int = 0 + + def get_comm(self, pid: int, tid: Optional[int] = None) -> str: + """Resolve and cache the comm(and) of a pid.""" + comm = None + if self.session: + # In offline mode, query session.find_thread() directly so PERF_RECORD_COMM + # updates after execve are reflected immediately instead of returning a + # stale pre-exec comm from comm_cache. + try: + thread = (self.session.find_thread(pid, tid) + if tid is not None else self.session.find_thread(pid)) + comm = thread.comm() if thread else None + except (TypeError, AttributeError): + pass + if not comm: + comm = self.comm_cache.get(pid) + else: + comm = self.comm_cache.get(pid) + if comm: + return comm + try: + with open(f"/proc/{pid}/comm", "r", encoding="utf-8", errors="replace") as f: + comm = f.read().strip() + except OSError: + # The thread may have exited before /proc could be read. + pass + if not comm: + comm = f"PID({pid})" + comm = ''.join(c if c.isprintable() else '?' for c in comm) + self.comm_cache[pid] = comm + return comm + + def process_event(self, sample: perf.sample_event) -> None: + """Process events.""" + event_name = str(sample.evsel) + if event_name.startswith("evsel(") and event_name.endswith(")"): + event_name = event_name[6:-1] + event_name = "".join(c if c.isprintable() else "?" for c in event_name) + pid = sample.sample_pid + sample_time = getattr(sample, "sample_time", 0) or 0 + + if sample_time > 0: + if self.last_print_time == 0: + self.last_print_time = sample_time + elif (sample_time > self.last_print_time and + sample_time - self.last_print_time >= self.interval_ns): + self.print_totals() + self.last_print_time = sample_time + + # Map each event onto the totals it updates. "enter" events count the + # requested bytes, "exit" events the transferred bytes or the error. + handlers = { + "syscalls:sys_enter_read": (self.reads, "total_reads", None), + "syscalls:sys_exit_read": (self.reads, None, "bytes_read"), + "syscalls:sys_enter_write": (self.writes, "total_writes", None), + "syscalls:sys_exit_write": (self.writes, None, "bytes_written"), + } + handler = handlers.get(event_name) + if not handler: + self.unhandled[event_name] += 1 + return + + totals, count_key, bytes_key = handler + try: + value = sample.count if count_key else sample.ret + except AttributeError: + self.unhandled[event_name] += 1 + return + + data = totals[pid] + data["comm"] = self.get_comm(pid, getattr(sample, "sample_tid", None)) + if count_key: + data["bytes_requested"] += value + data[count_key] += 1 + elif bytes_key: + # Convert unsigned 32-bit or 64-bit kernel error return values to signed integers. + # Because kernel read/write transfers are bounded by MAX_RW_COUNT (0x7ffff000 < 2 GiB), + # 0xfffff000..0xffffffff is always a 32-bit negative errno even on 64-bit hosts. + if value > 0: + if 0xfffff000 <= value <= 0xffffffff: + value -= 0x100000000 + elif value >= 0x8000000000000000: + value -= 0x10000000000000000 + if value >= 0: + data[bytes_key] += value + else: + data["errors"][value] += 1 + + def print_totals(self) -> None: + """Print summary tables.""" + if not self.offline: + print('\x1b[H\x1b[2J', end='') + print("read counts by pid:\n") + print( + f"{'pid':>6s} {'comm':<20s} {'# reads':>10s} " + f"{'bytes_req':>10s} {'bytes_read':>10s}" + ) + print(f"{'-'*6} {'-'*20} {'-'*10} {'-'*10} {'-'*10}") + + count = 0 + for pid, data in sorted(self.reads.items(), + key=lambda kv: kv[1]["bytes_read"], reverse=True): + print( + f"{pid:6d} {data['comm']:<20s} {data['total_reads']:10d} " + f"{data['bytes_requested']:10d} {data['bytes_read']:10d}" + ) + count += 1 + if count >= self.nlines: + break + + print("\nfailed reads by pid:\n") + print(f"{'pid':>6s} {'comm':<20s} {'error #':>7s} {'# errors':>10s}") + print(f"{'-'*6} {'-'*20} {'-'*7} {'-'*10}") + + errcounts = [] + for pid, data in self.reads.items(): + for error, cnt in data["errors"].items(): + errcounts.append((pid, data["comm"], error, cnt)) + + sorted_errcounts = sorted(errcounts, key=lambda x: x[3], reverse=True) + for pid, comm, error, cnt in sorted_errcounts[:self.nlines]: + print(f"{pid:6d} {comm:<20s} {error:7d} {cnt:10d}") + + print("\nwrite counts by pid:\n") + print( + f"{'pid':>6s} {'comm':<20s} {'# writes':>10s} " + f"{'bytes_req':>10s} {'bytes_written':>13s}" + ) + print(f"{'-'*6} {'-'*20} {'-'*10} {'-'*10} {'-'*13}") + + count = 0 + for pid, data in sorted(self.writes.items(), + key=lambda kv: kv[1]["bytes_written"], reverse=True): + print( + f"{pid:6d} {data['comm']:<20s} {data['total_writes']:10d} " + f"{data['bytes_requested']:10d} {data['bytes_written']:13d}" + ) + count += 1 + if count >= self.nlines: + break + + print("\nfailed writes by pid:\n") + print(f"{'pid':>6s} {'comm':<20s} {'error #':>7s} {'# errors':>10s}") + print(f"{'-'*6} {'-'*20} {'-'*7} {'-'*10}") + + errcounts = [] + for pid, data in self.writes.items(): + for error, cnt in data["errors"].items(): + errcounts.append((pid, data["comm"], error, cnt)) + + sorted_errcounts = sorted(errcounts, key=lambda x: x[3], reverse=True) + for pid, comm, error, cnt in sorted_errcounts[:self.nlines]: + print(f"{pid:6d} {comm:<20s} {error:7d} {cnt:10d}") + + # Reset counts + self.reads.clear() + self.writes.clear() + self.comm_cache.clear() + + def run(self, input_file: str) -> None: + """Run the session.""" + self.session = perf.session(perf.data(input_file), sample=self.process_event) + try: + self.session.process_events() + finally: + # Break the reference cycle between perf.session and self.process_event + # because perf.session lacks cyclic GC support (tp_traverse). + self.session = None + + # Print final totals if there are any left + if self.reads or self.writes: + self.print_totals() + + if self.unhandled: + print("\nunhandled events:\n") + print(f"{'event':<40s} {'count':>10s}") + print(f"{'-'*40} {'-'*10}") + for event_name, count in self.unhandled.items(): + print(f"{event_name:<40s} {count:10d}") + +def main() -> None: + """Main function.""" + parser = argparse.ArgumentParser(description="Trace r/w activity by PID") + parser.add_argument( + "interval", type=int, nargs="?", default=3, help="Refresh interval in seconds" + ) + parser.add_argument("-i", "--input", default="perf.data", help="Input file") + parser.add_argument("-l", "--live", action="store_true", help="Run in live mode") + args = parser.parse_args() + + analyzer = RwTop(args.interval) + try: + if args.live or (not os.path.exists(args.input) and args.input == "perf.data"): + # Live mode + events = ( + "syscalls:sys_enter_read,syscalls:sys_exit_read," + "syscalls:sys_enter_write,syscalls:sys_exit_write" + ) + live_session = LiveSession(events, sample_callback=analyzer.process_event) + print("Live mode started. Press Ctrl+C to stop.", file=sys.stderr) + live_session.run() + else: + analyzer.offline = True + analyzer.run(args.input) + except IOError as e: + print(e, file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + if not analyzer.offline: + print("\nStopping live mode...", file=sys.stderr) + if analyzer.reads or analyzer.writes: + analyzer.print_totals() + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_rwtop_python.sh b/tools/perf/tests/shell/test_rwtop_python.sh new file mode 100755 index 000000000000..062fca2dad14 --- /dev/null +++ b/tools/perf/tests/shell/test_rwtop_python.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# rwtop python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/rwtop.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, rwtop.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing rwtop.py..." + +# Create a perf.data file. Try to get tracepoint data. +if perf list | grep -q "syscalls:sys_enter_read"; then + ev="syscalls:sys_enter_read,syscalls:sys_exit_read" + ev="${ev},syscalls:sys_enter_write,syscalls:sys_exit_write" + perf record -e "$ev" -a -o "${temp_data}" \ + -- dd if=/dev/urandom of=/dev/null bs=1M count=10 >/dev/null 2>&1 || \ + { echo "Skipping test, perf record failed"; exit 2; } +else + echo "Skipping test, no syscalls:sys_enter_read event" + exit 2 +fi + +if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 +fi + +# Check that the script executes +if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "rwtop.py test failed" + err=1 +else + if ! grep -E -q "^ *[0-9]+" "${temp_out}"; then + echo "Failed to find metric data rows" + err=1 + else + echo "rwtop test passed." + fi +fi +rm -f "${temp_out}" + +exit $err From b103b3338b99c3edfe982ef1e6198db8aaa039b6 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:57 -0700 Subject: [PATCH 205/425] perf python: Port futex-contention to perf module Port tools/perf/scripts/python/futex-contention.py to a standalone script in tools/perf/python/ using the perf module. Avoiding the embedded interpreter overhead improves execution speed by ~3.2x: ``` $ perf record -e syscalls:sys_*_futex -a sleep 1 ... $ time perf script tools/perf/scripts/python/futex-contention.py ... real 0m1.007s user 0m0.935s sys 0m0.072s $ time python3 tools/perf/python/futex-contention.py ... real 0m0.314s user 0m0.259s sys 0m0.056s ``` Additional improvements compared to the legacy script: - Consolidate per-(tid, uaddr) contention count, total_time, min_time, and max_time into a single LockStats class instead of maintaining three separate dictionaries. - Validate that uaddr and op tracepoint attributes are present on syscalls:sys_enter_futex samples rather than silently attributing missing fields to uaddr=0, op=0 (FUTEX_WAIT). - Add -i/--input CLI option support via argparse and full type annotations. Add a shell test (test_futex_contention_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/futex-contention.py | 107 ++++++++++++++++ .../shell/test_futex_contention_python.sh | 115 ++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100755 tools/perf/python/futex-contention.py create mode 100755 tools/perf/tests/shell/test_futex_contention_python.sh diff --git a/tools/perf/python/futex-contention.py b/tools/perf/python/futex-contention.py new file mode 100755 index 000000000000..097663d929b7 --- /dev/null +++ b/tools/perf/python/futex-contention.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +"""Measures futex contention.""" +from __future__ import annotations + +import argparse +from collections import defaultdict +from typing import Dict, Tuple +import perf + +class LockStats: + """Aggregate lock contention information.""" + def __init__(self) -> None: + self.count = 0 + self.total_time = 0 + self.min_time = 0 + self.max_time = 0 + + def add(self, duration: int) -> None: + """Add a new duration measurement.""" + self.count += 1 + self.total_time += duration + if self.count == 1: + self.min_time = duration + self.max_time = duration + else: + self.min_time = min(self.min_time, duration) + self.max_time = max(self.max_time, duration) + + def avg(self) -> float: + """Return average duration.""" + return self.total_time / self.count if self.count > 0 else 0.0 + +process_names: Dict[int, str] = {} +start_times: Dict[int, Tuple[int, int]] = {} +session = None +durations: Dict[Tuple[int, int], LockStats] = defaultdict(LockStats) + +FUTEX_WAIT = 0 +FUTEX_PRIVATE_FLAG = 128 +FUTEX_CLOCK_REALTIME = 256 +# Mask out FUTEX_PRIVATE_FLAG and FUTEX_CLOCK_REALTIME so variants such as +# FUTEX_WAIT_PRIVATE (0 | 128) match the base FUTEX_WAIT command. +FUTEX_CMD_MASK = ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME) + + +def handle_start(pid: int, tid: int, uaddr: int, op: int, start_time: int) -> None: + """Handle a futex sys_enter event.""" + if (op & FUTEX_CMD_MASK) != FUTEX_WAIT: + return + try: + if session: + process = session.find_thread(pid, tid) + if process: + process_names[tid] = process.comm() or "unknown" + except (TypeError, AttributeError): + pass + if tid not in process_names: + process_names[tid] = "unknown" + + start_times[tid] = (uaddr, start_time) + +def handle_end(tid: int, end_time: int) -> None: + """Handle a futex sys_exit event.""" + if tid not in start_times: + return + (uaddr, start_time) = start_times[tid] + del start_times[tid] + durations[(tid, uaddr)].add(end_time - start_time) + +def process_event(sample: perf.sample_event) -> None: + """Process a single sample event.""" + event_name = str(sample.evsel) + if event_name.startswith("evsel(") and event_name.endswith(")"): + event_name = event_name[6:-1] + if event_name.startswith("syscalls:sys_enter_futex"): + uaddr = getattr(sample, "uaddr", None) + op = getattr(sample, "op", None) + if uaddr is None or op is None: + return # Tracepoint fields missing, skip silent attribution to 0 + handle_start(getattr(sample, "sample_pid", sample.sample_tid), + sample.sample_tid, uaddr, op, sample.sample_time) + elif event_name.startswith("syscalls:sys_exit_futex"): + handle_end(sample.sample_tid, sample.sample_time) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Measure futex contention") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + args = ap.parse_args() + + try: + session = perf.session(perf.data(args.input), sample=process_event) + try: + session.process_events() + except KeyboardInterrupt: + pass + + for ((t, u), stats) in sorted(durations.items()): + avg_ns = stats.avg() + print(f"{process_names.get(t, 'unknown')}[{t}] lock {u:x} contended " + f"{stats.count} times, {avg_ns:.0f} avg ns " + f"[max: {stats.max_time} ns, min {stats.min_time} ns]") + finally: + # Break the reference cycle between the global session and the + # process_event callback so the C perf.session object is freed. + session = None diff --git a/tools/perf/tests/shell/test_futex_contention_python.sh b/tools/perf/tests/shell/test_futex_contention_python.sh new file mode 100755 index 000000000000..26b13d263d20 --- /dev/null +++ b/tools/perf/tests/shell/test_futex_contention_python.sh @@ -0,0 +1,115 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# futex-contention python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/futex-contention.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, futex-contention.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} + +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +test_synthetic() { + echo "Testing futex-contention.py synthetic events..." + if ! "$PYTHON" - "$script_path" << 'EOF' +import importlib.util +import sys +from types import SimpleNamespace + +spec = importlib.util.spec_from_file_location("futex_contention", sys.argv[1]) +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) + +enter_sample = SimpleNamespace( + evsel="evsel(syscalls:sys_enter_futex:k)", + sample_pid=1234, + sample_tid=1234, + uaddr=0xdeadbeef, + op=mod.FUTEX_WAIT | mod.FUTEX_PRIVATE_FLAG, + sample_time=1000, +) +exit_sample = SimpleNamespace( + evsel="evsel(syscalls:sys_exit_futex:k)", + sample_pid=1234, + sample_tid=1234, + sample_time=2500, +) + +mod.process_event(enter_sample) +mod.process_event(exit_sample) + +stats = mod.durations.get((1234, 0xdeadbeef)) +assert stats is not None and stats.count == 1, f"Unexpected stats: {stats}" +assert stats.min_time == 1500 and stats.max_time == 1500 and stats.avg() == 1500.0 +line = (f"{mod.process_names.get(1234, 'unknown')}[1234] lock {0xdeadbeef:x} " + f"contended {stats.count} times, {stats.avg():.0f} avg ns " + f"[max: {stats.max_time} ns, min {stats.min_time} ns]") +expected = "unknown[1234] lock deadbeef contended 1 times, 1500 avg ns [max: 1500 ns, min 1500 ns]" +assert line == expected, line +EOF + then + echo "Synthetic test failed." + err=1 + else + echo "Synthetic test passed." + fi +} + +test_file_mode() { + echo "Testing futex-contention.py..." + # Some systems might not have syscalls:sys_enter_futex + if ! perf list | grep -q syscalls:sys_enter_futex; then + echo "Skipping file mode test, syscalls:sys_enter_futex not found" + return + fi + + # Generate some futex events + if ! perf record -e syscalls:sys_enter_futex,syscalls:sys_exit_futex -a -o "${temp_data}" \ + -- sleep 0.5 2>/dev/null; then + echo "Skipping file mode test (record failed)" + return + fi + + if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "File mode test failed." + err=1 + else + echo "File mode test passed." + fi +} + +test_synthetic +test_file_mode + +exit $err From 164c7edd80e10f40aaf2a7817f1a23015f2fc9dc Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:58 -0700 Subject: [PATCH 206/425] perf python: Port task-analyzer to perf module Port task-analyzer.py from tools/perf/scripts/python/ to a standalone script in tools/perf/python/ refactored into a class-based architecture. Improvements compared to the legacy script: - Support both offline perf.data file analysis (using perf.session) and live trace capture (using evlist.read_on_cpu), accessing sched:sched_switch tracepoint fields directly from sample objects. - Automatically disable ANSI terminal color escape sequences when --csv or --csv-summary is enabled so CSV column headers ('Comm,', 'Time Out-Out,', etc.) are never polluted by color codes when running interactively on a TTY (e.g. as root). - Sanitize non-printable characters and leading CSV formula characters (=, +, -, @) in task comm strings. - Emit a one-time warning to stderr if sched:sched_switch samples are missing tracepoint fields (such as when recorded without libtraceevent support). Update test_task_analyzer.sh to invoke the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/task-analyzer.py | 908 +++++++++++++++++++ tools/perf/tests/shell/test_task_analyzer.sh | 96 +- 2 files changed, 971 insertions(+), 33 deletions(-) create mode 100755 tools/perf/python/task-analyzer.py diff --git a/tools/perf/python/task-analyzer.py b/tools/perf/python/task-analyzer.py new file mode 100755 index 000000000000..fedba36b5ced --- /dev/null +++ b/tools/perf/python/task-analyzer.py @@ -0,0 +1,908 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +# task-analyzer.py - comprehensive perf tasks analysis +# Copyright (c) 2022, Hagen Paul Pfeifer +# Licensed under the terms of the GNU GPL License version 2 +# +# Usage: +# +# perf record -e sched:sched_switch -a -- sleep 10 +# ./task-analyzer.py +# +"""Comprehensive perf tasks analysis.""" +from __future__ import annotations + +import argparse +from contextlib import contextmanager +import decimal + +from typing import List, Dict, Union + +def _median(numbers: List[decimal.Decimal]) -> decimal.Decimal: + """phython3 hat statistics module - we have nothing""" + n = len(numbers) + index = n // 2 + if n % 2: + return sorted(numbers)[index] + return sum(sorted(numbers)[index - 1 : index + 1]) / decimal.Decimal(2) + +def _mean(numbers: List[decimal.Decimal]) -> decimal.Decimal: + return sum(numbers) / decimal.Decimal(len(numbers)) + +import os +import string +import sys +from typing import Any, Optional +import perf + + +# Columns will have a static size to align everything properly +# Support of 116 days of active update with nano precision +LEN_SWITCHED_IN = len("9999999.999999999") +LEN_SWITCHED_OUT = len("9999999.999999999") +LEN_CPU = len("000") +LEN_PID = len("maxvalue") +LEN_TID = len("maxvalue") +LEN_COMM = len("max-comms-length") +LEN_RUNTIME = len("999999.999") +# Support of 3.45 hours of timespans +LEN_OUT_IN = len("99999999999.999") +LEN_OUT_OUT = len("99999999999.999") +LEN_IN_IN = len("99999999999.999") +LEN_IN_OUT = len("99999999999.999") + +class Timespans: + """Tracks elapsed time between occurrences of the same task.""" + def __init__(self, args: argparse.Namespace, time_unit: str) -> None: + self.args = args + self.time_unit = time_unit + self._last_start: Optional[decimal.Decimal] = None + self._last_finish: Optional[decimal.Decimal] = None + self.current = { + 'out_out': decimal.Decimal(-1), + 'in_out': decimal.Decimal(-1), + 'out_in': decimal.Decimal(-1), + 'in_in': decimal.Decimal(-1) + } + if args.summary_extended: + self._time_in: decimal.Decimal = decimal.Decimal(-1) + self.max_vals = { + 'out_in': decimal.Decimal(-1), + 'at': decimal.Decimal(-1), + 'in_out': decimal.Decimal(-1), + 'in_in': decimal.Decimal(-1), + 'out_out': decimal.Decimal(-1) + } + + def feed(self, task: 'Task') -> None: + """Calculate timespans from chronological task occurrences.""" + if not self._last_finish: + self._last_start = task.time_in(self.time_unit) + self._last_finish = task.time_out(self.time_unit) + return + assert self._last_start is not None + assert self._last_finish is not None + self._time_in = task.time_in() + time_in = task.time_in(self.time_unit) + time_out = task.time_out(self.time_unit) + self.current['in_in'] = time_in - self._last_start + self.current['out_in'] = time_in - self._last_finish + self.current['in_out'] = time_out - self._last_start + self.current['out_out'] = time_out - self._last_finish + if self.args.summary_extended: + self.update_max_entries() + self._last_finish = task.time_out(self.time_unit) + self._last_start = task.time_in(self.time_unit) + + def update_max_entries(self) -> None: + """Update maximum timespans.""" + self.max_vals['in_in'] = max(self.max_vals['in_in'], self.current['in_in']) + self.max_vals['out_out'] = max(self.max_vals['out_out'], self.current['out_out']) + self.max_vals['in_out'] = max(self.max_vals['in_out'], self.current['in_out']) + if self.current['out_in'] > self.max_vals['out_in']: + self.max_vals['out_in'] = self.current['out_in'] + self.max_vals['at'] = self._time_in + +class Task: + """Handles information of a given task.""" + def __init__(self, task_id: str, tid: int, cpu: int, comm: str) -> None: + self.id = task_id + self.tid = tid + self.cpu = cpu + self.comm = comm + self.pid: Optional[int] = None + self._time_in: Optional[decimal.Decimal] = None + self._time_out: Optional[decimal.Decimal] = None + + def schedule_in_at(self, time_ns: int) -> None: + """Set schedule in time.""" + self._time_in = decimal.Decimal(time_ns) / decimal.Decimal(1e9) + + def schedule_out_at(self, time_ns: int) -> None: + """Set schedule out time.""" + self._time_out = decimal.Decimal(time_ns) / decimal.Decimal(1e9) + + def time_out(self, unit: str = "s") -> decimal.Decimal: + """Return schedule out time.""" + factor = TaskAnalyzer.time_uniter(unit) + return self._time_out * decimal.Decimal(factor) if self._time_out else decimal.Decimal(0) + + def time_in(self, unit: str = "s") -> decimal.Decimal: + """Return schedule in time.""" + factor = TaskAnalyzer.time_uniter(unit) + return self._time_in * decimal.Decimal(factor) if self._time_in else decimal.Decimal(0) + + def runtime(self, unit: str = "us") -> decimal.Decimal: + """Return runtime.""" + factor = TaskAnalyzer.time_uniter(unit) + if self._time_out is not None and self._time_in is not None: + return (self._time_out - self._time_in) * decimal.Decimal(factor) + return decimal.Decimal(0) + + def update_pid(self, pid: int) -> None: + """Update PID.""" + self.pid = pid + +class Summary: + """ + Primary instance for calculating the summary output. Processes the whole trace to + find and memorize relevant data such as mean, max et cetera. This instance handles + dynamic alignment aspects for summary output. + """ + + def __init__(self, analyzer): + self.analyzer = analyzer + self.args = analyzer.args + self.db = analyzer.db + self.time_unit = analyzer.time_unit + self.fd_sum = analyzer.fd_sum + self._body = [] + + class AlignmentHelper: + """ + Used to calculated the alignment for the output of the summary. + """ + def __init__(self, pid, tid, comm, runs, acc, mean, + median, min_val, max_val, max_at): + self.pid = pid + self.tid = tid + self.comm = comm + self.runs = runs + self.acc = acc + self.mean = mean + self.median = median + self.min = min_val + self.max = max_val + self.max_at = max_at + self.out_in = None + self.inter_at = None + self.out_out = None + self.in_in = None + self.in_out = None + + def _print_header(self): + ''' + Output is aligned using the formatted column widths in self.db. + ''' + len_tasks = max( + len("Task Information"), + sum(self.db["task_info"].values()) + len(self.db["task_info"]) - 1, + ) + fmt = "{{:^{}}}".format(len_tasks) + fmt += " {{:^{}}}".format( + sum(self.db["runtime_info"].values()) + len(self.db["runtime_info"]) - 1 + ) + _header = ("Task Information", "Runtime Information") + + if self.args.summary_extended: + fmt += " {{:^{}}}".format( + sum(self.db["inter_times"].values()) + len(self.db["inter_times"]) - 1 + ) + _header += ("Max Inter Task Times",) + self.fd_sum.write(fmt.format(*_header) + "\n") + + def _column_titles(self): + """ + Cells are being processed and displayed in different way so an alignment adjust + is implemented depeding on the choice of the timeunit. The positions of the max + values are being displayed in grey. Thus in their format two additional {}, + are placed for color set and reset. + """ + separator, fix_csv_align = self.analyzer.prepare_fmt_sep(is_summary=True) + fmt = "{{:>{}}}".format(self.db["task_info"]["pid"] * fix_csv_align) + fmt += "{}{{:>{}}}".format(separator, self.db["task_info"]["tid"] * fix_csv_align) + fmt += "{}{{:>{}}}".format(separator, self.db["task_info"]["comm"] * fix_csv_align) + fmt += "{}{{:>{}}}".format(separator, self.db["runtime_info"]["runs"] * fix_csv_align) + fmt += "{}{{:>{}}}".format(separator, self.db["runtime_info"]["acc"] * fix_csv_align) + fmt += "{}{{:>{}}}".format(separator, self.db["runtime_info"]["mean"] * fix_csv_align) + fmt += "{}{{:>{}}}".format( + separator, self.db["runtime_info"]["median"] * fix_csv_align + ) + fmt += "{}{{:>{}}}".format( + separator, self.db["runtime_info"]["min"] * fix_csv_align + ) + fmt += "{}{{:>{}}}".format( + separator, self.db["runtime_info"]["max"] * fix_csv_align + ) + fmt += "{}{{}}{{:>{}}}{{}}".format( + separator, self.db["runtime_info"]["max_at"] * fix_csv_align + ) + + grey = "" if self.args.csv_summary else TaskAnalyzer.COLORS["grey"] + reset = "" if self.args.csv_summary else TaskAnalyzer.COLORS["reset"] + column_titles = ("PID", "TID", "Comm") + column_titles += ("Runs", "Accumulated", "Mean", "Median", "Min", "Max") + column_titles += (grey, "Max At", reset) + + if self.args.summary_extended: + fmt += "{}{{:>{}}}".format( + separator, + self.db["inter_times"]["out_in"] * fix_csv_align + ) + fmt += "{}{{}}{{:>{}}}{{}}".format( + separator, + self.db["inter_times"]["inter_at"] * fix_csv_align + ) + fmt += "{}{{:>{}}}".format( + separator, + self.db["inter_times"]["out_out"] * fix_csv_align + ) + fmt += "{}{{:>{}}}".format( + separator, + self.db["inter_times"]["in_in"] * fix_csv_align + ) + fmt += "{}{{:>{}}}".format( + separator, + self.db["inter_times"]["in_out"] * fix_csv_align + ) + + column_titles += ( + "Out-In", grey, "Max At", + reset, "Out-Out", "In-In", "In-Out" + ) + + self.fd_sum.write(fmt.format(*column_titles) + "\n") + + def _task_stats(self): + """calculates the stats of every task and constructs the printable summary""" + grey = "" if self.args.csv_summary else TaskAnalyzer.COLORS["grey"] + reset = "" if self.args.csv_summary else TaskAnalyzer.COLORS["reset"] + for tid in sorted(self.db["tid"]): + color_one_sample = grey + color_reset = reset + no_executed = 0 + runtimes = [] + time_in = [] + timespans = Timespans(self.args, self.time_unit) + for task in self.db["tid"][tid]: + pid = task.pid + comm = task.comm + no_executed += 1 + runtimes.append(task.runtime(self.time_unit)) + time_in.append(task.time_in()) + timespans.feed(task) + if len(runtimes) > 1: + color_one_sample = "" + color_reset = "" + time_max = max(runtimes) + time_min = min(runtimes) + max_at = time_in[runtimes.index(max(runtimes))] + + # The size of the decimal after sum,mean and median varies, thus we cut + # the decimal number, by rounding it. It has no impact on the output, + # because we have a precision of the decimal points at the output. + time_sum = round(sum(runtimes), 3) + time_mean = round(_mean(runtimes), 3) + time_median = round(_median(runtimes), 3) + + align_helper = self.AlignmentHelper(pid, tid, comm, no_executed, time_sum, + time_mean, time_median, time_min, time_max, max_at) + self._body.append([ + pid, tid, comm, no_executed, time_sum, color_one_sample, + time_mean, time_median, time_min, time_max, + grey, max_at, + reset, color_reset + ]) + if self.args.summary_extended: + self._body[-1].extend([timespans.max_vals['out_in'], + grey, timespans.max_vals['at'], + reset, timespans.max_vals['out_out'], + timespans.max_vals['in_in'], + timespans.max_vals['in_out']]) + align_helper.out_in = timespans.max_vals['out_in'] + align_helper.inter_at = timespans.max_vals['at'] + align_helper.out_out = timespans.max_vals['out_out'] + align_helper.in_in = timespans.max_vals['in_in'] + align_helper.in_out = timespans.max_vals['in_out'] + self._calc_alignments_summary(align_helper) + + def _format_stats(self): + separator, fix_csv_align = self.analyzer.prepare_fmt_sep(is_summary=True) + decimal_precision, time_precision = self.analyzer.prepare_fmt_precision() + len_pid = self.db["task_info"]["pid"] * fix_csv_align + len_tid = self.db["task_info"]["tid"] * fix_csv_align + len_comm = self.db["task_info"]["comm"] * fix_csv_align + len_runs = self.db["runtime_info"]["runs"] * fix_csv_align + len_acc = self.db["runtime_info"]["acc"] * fix_csv_align + len_mean = self.db["runtime_info"]["mean"] * fix_csv_align + len_median = self.db["runtime_info"]["median"] * fix_csv_align + len_min = self.db["runtime_info"]["min"] * fix_csv_align + len_max = self.db["runtime_info"]["max"] * fix_csv_align + len_max_at = self.db["runtime_info"]["max_at"] * fix_csv_align + if self.args.summary_extended: + len_out_in = self.db["inter_times"]["out_in"] * fix_csv_align + len_inter_at = self.db["inter_times"]["inter_at"] * fix_csv_align + len_out_out = self.db["inter_times"]["out_out"] * fix_csv_align + len_in_in = self.db["inter_times"]["in_in"] * fix_csv_align + len_in_out = self.db["inter_times"]["in_out"] * fix_csv_align + + fmt = "{{:{}d}}".format(len_pid) + fmt += "{}{{:{}d}}".format(separator, len_tid) + fmt += "{}{{:>{}}}".format(separator, len_comm) + fmt += "{}{{:{}d}}".format(separator, len_runs) + fmt += "{}{{:{}.{}f}}".format(separator, len_acc, time_precision) + fmt += "{}{{}}{{:{}.{}f}}".format(separator, len_mean, time_precision) + fmt += "{}{{:{}.{}f}}".format(separator, len_median, time_precision) + fmt += "{}{{:{}.{}f}}".format(separator, len_min, time_precision) + fmt += "{}{{:{}.{}f}}".format(separator, len_max, time_precision) + fmt += "{}{{}}{{:{}.{}f}}{{}}{{}}".format( + separator, len_max_at, decimal_precision + ) + if self.args.summary_extended: + fmt += "{}{{:{}.{}f}}".format(separator, len_out_in, time_precision) + fmt += "{}{{}}{{:{}.{}f}}{{}}".format( + separator, len_inter_at, decimal_precision + ) + fmt += "{}{{:{}.{}f}}".format(separator, len_out_out, time_precision) + fmt += "{}{{:{}.{}f}}".format(separator, len_in_in, time_precision) + fmt += "{}{{:{}.{}f}}".format(separator, len_in_out, time_precision) + return fmt + + def _calc_alignments_summary(self, align_helper): + # Measure the formatted string widths (e.g. f"{val:.{decimal_precision}f}" + # and f"{val:.{time_precision}f}") rather than str(val), because Decimal's + # default string representation may include unrounded fractional digits or + # omit trailing zeros that _format_stats() renders with fixed precision. + decimal_precision, time_precision = self.analyzer.prepare_fmt_precision() + for key in self.db["task_info"]: + val_len = len(str(getattr(align_helper, key))) + if val_len > self.db["task_info"][key]: + self.db["task_info"][key] = val_len + for key in self.db["runtime_info"]: + val = getattr(align_helper, key) + if key == "runs": + val_len = len(str(val)) + elif key == "max_at": + val_len = len(f"{val:.{decimal_precision}f}") + else: + val_len = len(f"{val:.{time_precision}f}") + if val_len > self.db["runtime_info"][key]: + self.db["runtime_info"][key] = val_len + if self.args.summary_extended: + for key in self.db["inter_times"]: + val = getattr(align_helper, key) + if key == "inter_at": + val_len = len(f"{val:.{decimal_precision}f}") + else: + val_len = len(f"{val:.{time_precision}f}") + if val_len > self.db["inter_times"][key]: + self.db["inter_times"][key] = val_len + + def print(self): + self._task_stats() + fmt = self._format_stats() + + if not self.args.csv_summary: + print("\nSummary", file=self.fd_sum) + self._print_header() + self._column_titles() + for i in range(len(self._body)): + self.fd_sum.write(fmt.format(*tuple(self._body[i])) + "\n") + + +class TaskAnalyzer: + + """Main class for task analysis.""" + + COLORS = { + "grey": "\033[90m", + "red": "\033[91m", + "green": "\033[92m", + "yellow": "\033[93m", + "blue": "\033[94m", + "violet": "\033[95m", + "reset": "\033[0m", + } + + def __init__(self, args: argparse.Namespace) -> None: + self.args = args + self.db: Dict[str, Any] = {} + self.session: Optional[perf.session] = None + self._tgid_cache: Dict[int, int] = {} + self.time_unit = "us" + if args.ns: + self.time_unit = "ns" + elif args.ms: + self.time_unit = "ms" + self._init_db() + self._check_color() + self.fd_task = sys.stdout + self.fd_sum = sys.stdout + + @contextmanager + def open_output(self, filename: str, default: Any): + """Context manager for file or stdout.""" + if filename: + with open(filename, "w", encoding="utf-8") as f: + yield f + else: + yield default + + def _init_db(self) -> None: + self.db["running"] = {} + self.db["tid"] = {} + self.db["global"] = [] + if (self.args.summary or self.args.summary_extended or + self.args.summary_only or self.args.csv_summary): + self.db["task_info"] = {} + self.db["runtime_info"] = {} + self.db["task_info"]["pid"] = len("PID") + self.db["task_info"]["tid"] = len("TID") + self.db["task_info"]["comm"] = len("Comm") + self.db["runtime_info"]["runs"] = len("Runs") + self.db["runtime_info"]["acc"] = len("Accumulated") + self.db["runtime_info"]["max"] = len("Max") + self.db["runtime_info"]["max_at"] = len("Max At") + self.db["runtime_info"]["min"] = len("Min") + self.db["runtime_info"]["mean"] = len("Mean") + self.db["runtime_info"]["median"] = len("Median") + if self.args.summary_extended: + self.db["inter_times"] = {} + self.db["inter_times"]["out_in"] = len("Out-In") + self.db["inter_times"]["inter_at"] = len("Max At") + self.db["inter_times"]["out_out"] = len("Out-Out") + self.db["inter_times"]["in_in"] = len("In-In") + self.db["inter_times"]["in_out"] = len("In-Out") + + def _check_color(self) -> None: + """Check if color should be enabled.""" + if self.args.csv: + TaskAnalyzer.COLORS = {k: "" for k in TaskAnalyzer.COLORS} + return + if sys.stdout.isatty() and self.args.stdio_color != "never": + return + if self.args.stdio_color == "always": + return + TaskAnalyzer.COLORS = {k: "" for k in TaskAnalyzer.COLORS} + + @staticmethod + def time_uniter(unit: str) -> float: + """Return time unit factor.""" + picker = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9} + return picker[unit] + + def _task_id(self, pid: int, cpu: int) -> str: + return f"{pid}-{cpu}" + + def _filter_non_printable(self, unfiltered: Union[str, bytearray]) -> str: + # Strip non-printable characters, whitespace control characters + # (\r\n\t\x0b\x0c), quotes (", '), and ';' because --csv and + # --csv-summary emit unquoted ';'-delimited columns. Also replace a + # leading '=', '+', '-', or '@' with '_' to mitigate CSV formula / DDE + # injection attacks if an attacker-controlled process comm is opened in + # a spreadsheet program. + if isinstance(unfiltered, (bytearray, bytes)): + unfiltered = unfiltered.decode('utf-8', 'ignore') + filtered = "" + for char in unfiltered: + if char in string.printable and char not in "\r\n\t\x0b\x0c;\"'": + filtered += char + stripped = filtered.lstrip() + if stripped and stripped[0] in "=+-@": + filtered = "_" + stripped[1:] + return filtered + + def prepare_fmt_precision(self) -> tuple[int, int]: + if self.args.ns: + return 9, 0 + return 6, 3 + + def prepare_fmt_sep(self, is_summary: bool = False) -> tuple[str, int]: + # fix_csv_align multiplies format field widths: 0 in CSV mode collapses + # {:>N} padding to {:>0} (no extra spaces around ';'), while 1 in + # standard text mode preserves fixed-width column alignment. + if (is_summary and self.args.csv_summary) or (not is_summary and self.args.csv): + return ";", 0 + return " ", 1 + + def _fmt_header(self) -> str: + separator, fix_csv_align = self.prepare_fmt_sep() + fmt = f"{{:>{LEN_SWITCHED_IN*fix_csv_align}}}" + fmt += f"{separator}{{:>{LEN_SWITCHED_OUT*fix_csv_align}}}" + fmt += f"{separator}{{:>{LEN_CPU*fix_csv_align}}}" + fmt += f"{separator}{{:>{LEN_PID*fix_csv_align}}}" + fmt += f"{separator}{{:>{LEN_TID*fix_csv_align}}}" + fmt += f"{separator}{{:>{LEN_COMM*fix_csv_align}}}" + fmt += f"{separator}{{:>{LEN_RUNTIME*fix_csv_align}}}" + fmt += f"{separator}{{:>{LEN_OUT_IN*fix_csv_align}}}" + if self.args.extended_times: + fmt += f"{separator}{{:>{LEN_OUT_OUT*fix_csv_align}}}" + fmt += f"{separator}{{:>{LEN_IN_IN*fix_csv_align}}}" + fmt += f"{separator}{{:>{LEN_IN_OUT*fix_csv_align}}}" + return fmt + + def _fmt_body(self) -> str: + separator, fix_csv_align = self.prepare_fmt_sep() + decimal_precision, time_precision = self.prepare_fmt_precision() + fmt = f"{{}}{{:{LEN_SWITCHED_IN*fix_csv_align}.{decimal_precision}f}}" + fmt += f"{separator}{{:{LEN_SWITCHED_OUT*fix_csv_align}.{decimal_precision}f}}" + fmt += f"{separator}{{:{LEN_CPU*fix_csv_align}d}}" + fmt += f"{separator}{{:{LEN_PID*fix_csv_align}d}}" + fmt += f"{separator}{{}}{{:{LEN_TID*fix_csv_align}d}}{{}}" + fmt += f"{separator}{{}}{{:>{LEN_COMM*fix_csv_align}}}" + fmt += f"{separator}{{:{LEN_RUNTIME*fix_csv_align}.{time_precision}f}}" + if self.args.extended_times: + fmt += f"{separator}{{:{LEN_OUT_IN*fix_csv_align}.{time_precision}f}}" + fmt += f"{separator}{{:{LEN_OUT_OUT*fix_csv_align}.{time_precision}f}}" + fmt += f"{separator}{{:{LEN_IN_IN*fix_csv_align}.{time_precision}f}}" + fmt += f"{separator}{{:{LEN_IN_OUT*fix_csv_align}.{time_precision}f}}{{}}" + else: + fmt += f"{separator}{{:{LEN_OUT_IN*fix_csv_align}.{time_precision}f}}{{}}" + return fmt + + def _print_header(self) -> None: + fmt = self._fmt_header() + header = ["Switched-In", "Switched-Out", "CPU", "PID", "TID", "Comm", + "Runtime", "Time Out-In"] + if self.args.extended_times: + header += ["Time Out-Out", "Time In-In", "Time In-Out"] + self.fd_task.write(fmt.format(*header) + "\n") + + def _print_task_finish(self, task: Task) -> None: + c_row_set = "" + c_row_reset = "" + out_in: Any = -1 + out_out: Any = -1 + in_in: Any = -1 + in_out: Any = -1 + fmt = self._fmt_body() + + if str(task.tid) in self.args.highlight_tasks_map: + c_row_set = TaskAnalyzer.COLORS.get(self.args.highlight_tasks_map[str(task.tid)], '') + c_row_reset = TaskAnalyzer.COLORS["reset"] + if task.comm in self.args.highlight_tasks_map: + c_row_set = TaskAnalyzer.COLORS.get(self.args.highlight_tasks_map[task.comm], '') + c_row_reset = TaskAnalyzer.COLORS["reset"] + + c_tid_set = "" + c_tid_reset = "" + if task.pid == task.tid: + c_tid_set = TaskAnalyzer.COLORS["grey"] + c_tid_reset = TaskAnalyzer.COLORS["reset"] + + if task.tid in self.db["tid"]: + last_tid_task = self.db["tid"][task.tid][-1] + timespan_gap_tid = Timespans(self.args, self.time_unit) + timespan_gap_tid.feed(last_tid_task) + timespan_gap_tid.feed(task) + out_in = timespan_gap_tid.current['out_in'] + out_out = timespan_gap_tid.current['out_out'] + in_in = timespan_gap_tid.current['in_in'] + in_out = timespan_gap_tid.current['in_out'] + + if self.args.extended_times: + line_out = fmt.format(c_row_set, task.time_in(), task.time_out(), task.cpu, + task.pid, c_tid_set, task.tid, c_tid_reset, c_row_set, task.comm, + task.runtime(self.time_unit), out_in, out_out, in_in, in_out, + c_row_reset) + "\n" + else: + line_out = fmt.format(c_row_set, task.time_in(), task.time_out(), task.cpu, + task.pid, c_tid_set, task.tid, c_tid_reset, c_row_set, task.comm, + task.runtime(self.time_unit), out_in, c_row_reset) + "\n" + self.fd_task.write(line_out) + + def _record_cleanup(self, _list: list[Any]) -> list[Any]: + need_summary = (self.args.summary or self.args.summary_extended or + self.args.summary_only or self.args.csv_summary) + if not need_summary and len(_list) > 1: + return _list[len(_list) - 1:] + return _list + + def _record_by_tid(self, task: Task) -> None: + tid = task.tid + if tid not in self.db["tid"]: + self.db["tid"][tid] = [] + self.db["tid"][tid].append(task) + self.db["tid"][tid] = self._record_cleanup(self.db["tid"][tid]) + + def _record_global(self, task: Task) -> None: + self.db["global"].append(task) + self.db["global"] = self._record_cleanup(self.db["global"]) + + def _handle_task_finish(self, tid: int, cpu: int, time_ns: int, pid: int) -> None: + if tid == 0 and (not self.args.tid_renames or 0 not in self.args.tid_renames): + return + _id = self._task_id(tid, cpu) + if _id not in self.db["running"]: + return + task = self.db["running"][_id] + task.schedule_out_at(time_ns) + task.update_pid(pid) + del self.db["running"][_id] + + if not self._limit_filtered(tid, pid, task.comm): + if not self.args.summary_only: + self._print_task_finish(task) + self._record_by_tid(task) + self._record_global(task) + + def _handle_task_start(self, tid: int, cpu: int, comm: str, time_ns: int) -> None: + if tid == 0 and (not self.args.tid_renames or 0 not in self.args.tid_renames): + return + if tid in self.args.tid_renames: + comm = self._filter_non_printable(self.args.tid_renames[tid]) + _id = self._task_id(tid, cpu) + if _id in self.db["running"]: + return + task = Task(_id, tid, cpu, comm) + task.schedule_in_at(time_ns) + self.db["running"][_id] = task + + def _limit_filtered(self, tid: int, pid: int, comm: str) -> bool: + """Filter tasks based on CLI arguments.""" + match_filter = False + if self.args.filter_tasks: + if (str(tid) in self.args.filter_tasks or + str(pid) in self.args.filter_tasks or + comm in self.args.filter_tasks): + match_filter = True + + match_limit = False + if self.args.limit_to_tasks: + if (str(tid) in self.args.limit_to_tasks or + str(pid) in self.args.limit_to_tasks or + comm in self.args.limit_to_tasks): + match_limit = True + + if self.args.filter_tasks and match_filter: + return True + if self.args.limit_to_tasks and not match_limit: + return True + return False + + def _is_within_timelimit(self, time_ns: int) -> bool: + if not self.args.time_limit: + return True + time_s = decimal.Decimal(time_ns) / decimal.Decimal(1e9) + bounds = self.args.time_limit.split(":") + lower_bound = bounds[0] if len(bounds) > 0 else "" + upper_bound = bounds[1] if len(bounds) > 1 else "" + if lower_bound and time_s < decimal.Decimal(lower_bound): + return False + if upper_bound and time_s > decimal.Decimal(upper_bound): + return False + return True + + def process_event(self, sample: perf.sample_event) -> None: + """Process sched:sched_switch events.""" + if "sched:sched_switch" not in str(sample.evsel): + return + + time_ns = sample.sample_time + if not self._is_within_timelimit(time_ns): + return + + # Access tracepoint fields directly from sample object + try: + prev_pid = sample.prev_pid + next_pid = sample.next_pid + next_comm = sample.next_comm + common_cpu = sample.sample_cpu + except AttributeError: + if not self.db.get("warned_missing_fields"): + self.db["warned_missing_fields"] = True + print("Warning: sched:sched_switch sample missing tracepoint fields " + "(is libtraceevent support enabled?).", file=sys.stderr) + return + + next_comm = self._filter_non_printable(next_comm) + + # Task finish for previous task. Resolve TGID (process ID) from TID: + # in file mode we query perf.session.find_thread() (checking thread.pid > 0 + # so an unresolved -1 does not overwrite the prev_pid fallback), whereas in + # live mode (self.session is None) we read /proc//status and cache + # the lookup in _tgid_cache. + prev_tgid = prev_pid # Fallback + if prev_pid != 0: + if self.session: + try: + thread = self.session.find_thread(-1, prev_pid) + if thread and thread.pid > 0: + prev_tgid = thread.pid + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError): + pass + elif prev_pid in self._tgid_cache: + prev_tgid = self._tgid_cache[prev_pid] + else: + if len(self._tgid_cache) >= 4096: + self._tgid_cache.pop(next(iter(self._tgid_cache))) + self._tgid_cache[prev_pid] = prev_tgid + try: + with open(f"/proc/{prev_pid}/status", encoding="utf-8") as f: + for line in f: + if line.startswith("Tgid:"): + prev_tgid = int(line.split()[1]) + self._tgid_cache[prev_pid] = prev_tgid + break + except (OSError, ValueError, KeyError, RuntimeError): + pass + self._handle_task_finish(prev_pid, common_cpu, time_ns, prev_tgid) + # Task start for next task + self._handle_task_start(next_pid, common_cpu, next_comm, time_ns) + + def print_summary(self) -> None: + """Calculate and print summary.""" + need_summary = (self.args.summary or self.args.summary_extended or + self.args.summary_only or self.args.csv_summary) + if not need_summary: + return + + Summary(self).print() + + def _run_file(self) -> None: + if not self.args.summary_only: + self._print_header() + + session = perf.session(perf.data(self.args.input), sample=self.process_event) + self.session = session + try: + session.process_events() + except KeyboardInterrupt: + pass + finally: + # Break the reference cycle between self.session and the bound + # self.process_event callback so the C perf.session object is freed. + self.session = None + + if not self.db["global"]: + print(f"Warning: No sched:sched_switch trace events found in '{self.args.input}'.", + file=sys.stderr) + + self.print_summary() + + def _run_live(self) -> None: + if not self.args.summary_only: + self._print_header() + + cpus = perf.cpu_map() + threads = perf.thread_map(-1) + evlist = perf.parse_events("sched:sched_switch", cpus, threads) + evlist.config() + + evlist.open() + evlist.mmap() + evlist.enable() + + # Per-CPU perf ring buffers are drained sequentially in a loop, so events + # read from CPU N may have timestamps slightly earlier than events just + # read from CPU 0. Buffer events in pending_events and only dispatch those + # older than a 50 ms watermark behind max_ts so cross-CPU migrations and + # switches are processed in global timestamp order. + pending_events: list[perf.sample_event] = [] + print("Live mode started. Press Ctrl+C to stop.", file=sys.stderr) + try: + while True: + try: + evlist.poll(timeout=100) + except InterruptedError: + continue + for cpu in cpus: + while True: + event = evlist.read_on_cpu(cpu) + if not event: + break + if not isinstance(event, perf.sample_event): + continue + pending_events.append(event) + if pending_events: + pending_events.sort( + key=lambda e: getattr(e, 'sample_time', getattr(e, 'time', 0)) + ) + max_ts = getattr( + pending_events[-1], 'sample_time', getattr(pending_events[-1], 'time', 0) + ) + cutoff_ts = max_ts - 50_000_000 + ready_idx = 0 + for event in pending_events: + ev_ts = getattr(event, 'sample_time', getattr(event, 'time', 0)) + if ev_ts <= cutoff_ts: + self.process_event(event) + ready_idx += 1 + else: + break + del pending_events[:ready_idx] + except KeyboardInterrupt: + print("\nStopping live mode...", file=sys.stderr) + finally: + pending_events.sort(key=lambda e: getattr(e, 'sample_time', getattr(e, 'time', 0))) + for event in pending_events: + self.process_event(event) + evlist.close() + self.print_summary() + + def run(self) -> None: + """Run the session.""" + is_live = (self.args.live or + (not os.path.exists(self.args.input) and self.args.input == "perf.data")) + with self.open_output(self.args.csv, sys.stdout) as fd_task: + if self.args.csv and self.args.csv == self.args.csv_summary: + self.fd_task = fd_task + self.fd_sum = fd_task + if is_live: + self._run_live() + else: + self._run_file() + else: + with self.open_output(self.args.csv_summary, sys.stdout) as fd_sum: + self.fd_task = fd_task + self.fd_sum = fd_sum + if is_live: + self._run_live() + else: + self._run_file() + +def main() -> None: + """Main function.""" + parser = argparse.ArgumentParser(description="Analyze tasks behavior") + parser.add_argument("-i", "--input", default="perf.data", help="Input file name") + parser.add_argument("--time-limit", default="", help="print tasks only in time window") + parser.add_argument("--summary", action="store_true", + help="print additional runtime information") + parser.add_argument("--summary-only", action="store_true", + help="print only summary without traces") + parser.add_argument("--summary-extended", action="store_true", + help="print extended summary") + parser.add_argument("--ns", action="store_true", help="show timestamps in nanoseconds") + parser.add_argument("--ms", action="store_true", help="show timestamps in milliseconds") + parser.add_argument("--live", action="store_true", + help="force live mode (ignores -i/perf.data)") + parser.add_argument("--extended-times", action="store_true", + help="Show elapsed times between schedule in/out") + parser.add_argument("--filter-tasks", default="", help="filter tasks by tid, pid or comm") + parser.add_argument("--limit-to-tasks", default="", help="limit output to selected tasks") + parser.add_argument("--highlight-tasks", default="", help="colorize special tasks") + parser.add_argument("--rename-comms-by-tids", default="", help="rename task names by using tid") + parser.add_argument("--stdio-color", default="auto", choices=["always", "never", "auto"], + help="configure color output") + parser.add_argument("--csv", default="", help="Write trace to file") + parser.add_argument("--csv-summary", default="", help="Write summary to file") + + args = parser.parse_args() + args.tid_renames = {} + args.highlight_tasks_map = {} + args.filter_tasks = args.filter_tasks.split(",") if args.filter_tasks else [] + args.limit_to_tasks = args.limit_to_tasks.split(",") if args.limit_to_tasks else [] + + if args.rename_comms_by_tids: + for item in args.rename_comms_by_tids.split(","): + try: + tid, name = item.split(":", 1) + args.tid_renames[int(tid)] = name + except ValueError: + print(f"Error: Invalid format for --rename-comms-by-tids '{item}', " + "expected tid:name", file=sys.stderr) + sys.exit(1) + + if args.highlight_tasks: + for item in args.highlight_tasks.split(","): + parts = item.split(":") + if len(parts) == 1: + parts.append("red") + key, color = parts[0], parts[1] + args.highlight_tasks_map[key] = color + + analyzer = TaskAnalyzer(args) + analyzer.run() + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_task_analyzer.sh b/tools/perf/tests/shell/test_task_analyzer.sh index 0314412e63b4..3a97d58eec74 100755 --- a/tools/perf/tests/shell/test_task_analyzer.sh +++ b/tools/perf/tests/shell/test_task_analyzer.sh @@ -1,19 +1,24 @@ #!/bin/bash -# perf script task-analyzer tests (exclusive) # SPDX-License-Identifier: GPL-2.0 +# task-analyzer python test + +# shellcheck source=lib/setup_python.sh +. "$(dirname "$0")"/lib/setup_python.sh tmpdir=$(mktemp -d /tmp/perf-script-task-analyzer-XXXXX) -# TODO: perf script report only supports input from the CWD perf.data file, make -# it support input from any file. -perfdata="perf.data" +perfdata="$tmpdir/perf.data" csv="$tmpdir/csv" csvsummary="$tmpdir/csvsummary" err=0 -# set PERF_EXEC_PATH to find scripts in the source directory -perfdir=$(dirname "$0")/../.. -if [ -e "$perfdir/scripts/python/Perf-Trace-Util" ]; then - export PERF_EXEC_PATH=$perfdir +# Set up perfdir and PERF_EXEC_PATH +if [ "x$PERF_EXEC_PATH" = "x" ]; then + perfdir="$(dirname "$0")/../.." + if [ -f "$perfdir/python/task-analyzer.py" ]; then + export PERF_EXEC_PATH="$perfdir" + fi +else + perfdir="$PERF_EXEC_PATH" fi # Disable lsan to avoid warnings about python memory leaks. @@ -76,85 +81,106 @@ prepare_perf_data() { # check standard inkvokation with no arguments test_basic() { out="$tmpdir/perf.out" - perf script report task-analyzer > "$out" - check_exec_0 "perf script report task-analyzer" + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" > "$out" + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata}" find_str_or_fail "Comm" "$out" "${FUNCNAME[0]}" } test_ns_rename(){ out="$tmpdir/perf.out" - perf script report task-analyzer --ns --rename-comms-by-tids 0:random > "$out" - check_exec_0 "perf script report task-analyzer --ns --rename-comms-by-tids 0:random" + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + --ns \ + --rename-comms-by-tids 0:random > "$out" + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + --ns \ + --rename-comms-by-tids 0:random" find_str_or_fail "Comm" "$out" "${FUNCNAME[0]}" } test_ms_filtertasks_highlight(){ out="$tmpdir/perf.out" - perf script report task-analyzer --ms --filter-tasks perf --highlight-tasks perf \ + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + --ms --filter-tasks perf --highlight-tasks perf \ > "$out" - check_exec_0 "perf script report task-analyzer --ms --filter-tasks perf --highlight-tasks perf" + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + --ms --filter-tasks perf --highlight-tasks perf" find_str_or_fail "Comm" "$out" "${FUNCNAME[0]}" } test_extended_times_timelimit_limittasks() { out="$tmpdir/perf.out" - perf script report task-analyzer --extended-times --time-limit :99999 \ + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --extended-times \ + --time-limit :99999 \ --limit-to-tasks perf > "$out" - check_exec_0 "perf script report task-analyzer --extended-times --time-limit :99999 --limit-to-tasks perf" + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + --extended-times \ + --time-limit :99999 --limit-to-tasks perf" find_str_or_fail "Out-Out" "$out" "${FUNCNAME[0]}" } test_summary() { out="$tmpdir/perf.out" - perf script report task-analyzer --summary > "$out" - check_exec_0 "perf script report task-analyzer --summary" + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --summary > "$out" + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} --summary" find_str_or_fail "Summary" "$out" "${FUNCNAME[0]}" } test_summaryextended() { out="$tmpdir/perf.out" - perf script report task-analyzer --summary-extended > "$out" - check_exec_0 "perf script report task-analyzer --summary-extended" + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --summary-extended > "$out" + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} --summary-extended" find_str_or_fail "Inter Task Times" "$out" "${FUNCNAME[0]}" } test_summaryonly() { out="$tmpdir/perf.out" - perf script report task-analyzer --summary-only > "$out" - check_exec_0 "perf script report task-analyzer --summary-only" + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --summary-only > "$out" + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} --summary-only" find_str_or_fail "Summary" "$out" "${FUNCNAME[0]}" } test_extended_times_summary_ns() { out="$tmpdir/perf.out" - perf script report task-analyzer --extended-times --summary --ns > "$out" - check_exec_0 "perf script report task-analyzer --extended-times --summary --ns" + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --extended-times \ + --summary --ns > "$out" + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + --extended-times \ + --summary \ + --ns" find_str_or_fail "Out-Out" "$out" "${FUNCNAME[0]}" find_str_or_fail "Summary" "$out" "${FUNCNAME[0]}" } test_csv() { - perf script report task-analyzer --csv "${csv}" > /dev/null - check_exec_0 "perf script report task-analyzer --csv ${csv}" + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --csv "${csv}" > /dev/null + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} --csv ${csv}" find_str_or_fail "Comm;" "${csv}" "${FUNCNAME[0]}" } test_csv_extended_times() { - perf script report task-analyzer --csv "${csv}" --extended-times > /dev/null - check_exec_0 "perf script report task-analyzer --csv ${csv} --extended-times" + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + --csv "${csv}" \ + --extended-times > /dev/null + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + --csv ${csv} \ + --extended-times" find_str_or_fail "Out-Out;" "${csv}" "${FUNCNAME[0]}" } test_csvsummary() { - perf script report task-analyzer --csv-summary "${csvsummary}" > /dev/null - check_exec_0 "perf script report task-analyzer --csv-summary ${csvsummary}" + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + --csv-summary "${csvsummary}" > /dev/null + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + --csv-summary ${csvsummary}" find_str_or_fail "Comm;" "${csvsummary}" "${FUNCNAME[0]}" } test_csvsummary_extended() { - perf script report task-analyzer --csv-summary "${csvsummary}" --summary-extended \ + $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + --csv-summary "${csvsummary}" --summary-extended \ >/dev/null - check_exec_0 "perf script report task-analyzer --csv-summary ${csvsummary} --summary-extended" + check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + --csv-summary ${csvsummary} --summary-extended" find_str_or_fail "Out-Out;" "${csvsummary}" "${FUNCNAME[0]}" } @@ -165,7 +191,11 @@ if [ $err -ne 0 ]; then cleanup exit $err fi -prepare_perf_data +prepare_perf_data || { + echo "Skipping tests, failed to prepare perf.data" + cleanup + exit 2 +} test_basic test_ns_rename test_ms_filtertasks_highlight From 56d9f05dce1dd091c0ca2683aaa2d70fdc1884a3 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:19:59 -0700 Subject: [PATCH 207/425] perf python: Port sched-migration and SchedGui to perf module Port sched-migration.py and SchedGui.py from tools/perf/scripts/python/ to standalone modules in tools/perf/python/: - Refactor sched-migration.py into a SchedMigrationAnalyzer class using perf.session for event processing and add argparse CLI support (-i/--input, -v/--verbose, --gui, --no-gui). - Port SchedGui.py to tools/perf/python/ as a local module dependency. - Load wxPython dynamically via importlib only when GUI mode (--gui) is requested so text-mode analysis, testing on headless systems, and static analysis (mypy/pylint) succeed without wx installed. - Remove Python 2 compatibility code. Add a shell test (test_sched_migration_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/SchedGui.py | 246 +++++++++ tools/perf/python/sched-migration.py | 496 ++++++++++++++++++ .../shell/test_sched_migration_python.sh | 72 +++ 3 files changed, 814 insertions(+) create mode 100755 tools/perf/python/SchedGui.py create mode 100755 tools/perf/python/sched-migration.py create mode 100755 tools/perf/tests/shell/test_sched_migration_python.sh diff --git a/tools/perf/python/SchedGui.py b/tools/perf/python/SchedGui.py new file mode 100755 index 000000000000..fa0d010fe528 --- /dev/null +++ b/tools/perf/python/SchedGui.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +# SchedGui.py - Python extension for perf script, basic GUI code for +# traces drawing and overview. +# +# Copyright (C) 2010 by Frederic Weisbecker +# +# Ported to modern directory structure. + +"""SchedGui.py - Python extension for perf script, basic GUI code for traces drawing and overview.""" +from __future__ import annotations + +import importlib +from typing import Any + +class _DummyWx: + """Dummy wx module fallback when wxPython is not installed.""" + Frame = object + + +try: + wx: Any = importlib.import_module("wx") + WX_AVAILABLE = True +except ImportError: + wx = _DummyWx + WX_AVAILABLE = False + + +class RootFrame(wx.Frame): + """Main window frame for scheduling trace visualization.""" + Y_OFFSET = 100 + RECT_HEIGHT = 100 + RECT_SPACE = 50 + EVENT_MARKING_WIDTH = 5 + + def __init__(self, sched_tracer, title, parent=None, win_id=-1): + if not WX_AVAILABLE: + raise ImportError("You need to install the wxpython lib for this script") + wx.Frame.__init__(self, parent, win_id, title) + + self.dc = None + (self.screen_width, self.screen_height) = wx.GetDisplaySize() + self.screen_width -= 10 + self.screen_height -= 10 + self.zoom = 0.5 + self.scroll_scale = 20 + self.sched_tracer = sched_tracer + self.sched_tracer.set_root_win(self) + (self.ts_start, self.ts_end) = sched_tracer.interval() + self.update_width_virtual() + self.nr_rects = sched_tracer.nr_rectangles() + 1 + self.height_virtual = RootFrame.Y_OFFSET + \ + (self.nr_rects * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE)) + + # whole window panel + self.panel = wx.Panel(self, size=(self.screen_width, self.screen_height)) + + # scrollable container + # Create SplitterWindow + self.splitter = wx.SplitterWindow(self.panel, style=wx.SP_3D) + + # scrollable container (Top) + self.scroll = wx.ScrolledWindow(self.splitter) + self.scroll.SetScrollbars(self.scroll_scale, self.scroll_scale, + int(self.width_virtual // self.scroll_scale), + int(self.height_virtual // self.scroll_scale)) + self.scroll.EnableScrolling(True, True) + self.scroll.SetFocus() + + # scrollable drawing area + self.scroll_panel = wx.Panel(self.scroll, + size=(self.screen_width - 15, self.screen_height // 2)) + self.scroll_panel.Bind(wx.EVT_PAINT, self.on_paint) + self.scroll_panel.Bind(wx.EVT_KEY_DOWN, self.on_key_press) + self.scroll_panel.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down) + self.scroll.Bind(wx.EVT_KEY_DOWN, self.on_key_press) + self.scroll.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down) + + self.scroll_panel.SetSize(int(self.width_virtual), int(self.height_virtual)) + + # Create a separate panel for text (Bottom) + self.text_panel = wx.Panel(self.splitter) + self.text_sizer = wx.BoxSizer(wx.VERTICAL) + self.txt = wx.TextCtrl(self.text_panel, -1, "Click a bar to see details", + style=wx.TE_MULTILINE) + self.text_sizer.Add(self.txt, 1, wx.EXPAND | wx.ALL, 5) + self.text_panel.SetSizer(self.text_sizer) + + # Split the window + self.splitter.SplitHorizontally(self.scroll, self.text_panel, (self.screen_height * 3) // 4) + + # Main sizer to layout splitter + self.main_sizer = wx.BoxSizer(wx.VERTICAL) + self.main_sizer.Add(self.splitter, 1, wx.EXPAND) + self.panel.SetSizer(self.main_sizer) + + self.scroll.Fit() + self.Fit() + + self.Show(True) + + def us_to_px(self, val): + """Convert microseconds to pixels.""" + return val / (10 ** 3) * self.zoom + + def px_to_us(self, val): + """Convert pixels to microseconds.""" + return (val / self.zoom) * (10 ** 3) + + def scroll_start(self): + """Get scroll start position in pixels.""" + (x, y) = self.scroll.GetViewStart() + return (x * self.scroll_scale, y * self.scroll_scale) + + def scroll_start_us(self): + """Get scroll start position in microseconds.""" + (x, _) = self.scroll_start() + return self.px_to_us(x) + + def paint_rectangle_zone(self, nr, color, top_color, start, end): + """Draw a rectangle zone for a CPU.""" + offset_px = self.us_to_px(start - self.ts_start) + width_px = self.us_to_px(end - start) + + offset_py = RootFrame.Y_OFFSET + (nr * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE)) + width_py = RootFrame.RECT_HEIGHT + + dc = self.dc + + if top_color is not None: + (r, g, b) = top_color + top_color = wx.Colour(r, g, b) + brush = wx.Brush(top_color, wx.SOLID) + dc.SetBrush(brush) + dc.DrawRectangle(int(offset_px), int(offset_py), + int(width_px), RootFrame.EVENT_MARKING_WIDTH) + width_py -= RootFrame.EVENT_MARKING_WIDTH + offset_py += RootFrame.EVENT_MARKING_WIDTH + + (r, g, b) = color + color = wx.Colour(r, g, b) + brush = wx.Brush(color, wx.SOLID) + dc.SetBrush(brush) + dc.DrawRectangle(int(offset_px), int(offset_py), int(width_px), int(width_py)) + + def update_rectangles(self, start, end): + """Update rectangles in the given time window.""" + start += self.ts_start + end += self.ts_start + self.sched_tracer.fill_zone(start, end) + + def on_paint(self, event): + """Handle paint event.""" + window = event.GetEventObject() + dc = wx.PaintDC(window) + + # Clear background to avoid ghosting + dc.SetBackground(wx.Brush(window.GetBackgroundColour())) + dc.Clear() + + self.dc = dc + try: + width = min(self.width_virtual, self.screen_width) + (x, _) = self.scroll_start() + start = self.px_to_us(x) + end = self.px_to_us(x + width) + self.update_rectangles(start, end) + + # Draw CPU labels at the left edge of the visible area + (x_scroll, _) = self.scroll_start() + for nr in range(self.nr_rects): + offset_py = RootFrame.Y_OFFSET + (nr * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE)) + dc.DrawText(f"CPU {nr}", x_scroll + 10, offset_py + 10) + finally: + self.dc = None + + def rect_from_ypixel(self, y): + y -= RootFrame.Y_OFFSET + rect = y // (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE) + height = y % (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE) + + if rect < 0 or rect > self.nr_rects - 1 or height > RootFrame.RECT_HEIGHT: + return -1 + + return rect + + def update_summary(self, txt): + self.txt.SetValue(txt) + self.text_panel.Layout() + self.splitter.Layout() + self.text_panel.Refresh() + + def on_mouse_down(self, event): + pos = event.GetPosition() + x, y = pos.x, pos.y + rect = self.rect_from_ypixel(y) + if rect == -1: + return + + t = self.px_to_us(x) + self.ts_start + + self.sched_tracer.mouse_down(rect, t) + + def update_width_virtual(self): + self.width_virtual = self.us_to_px(self.ts_end - self.ts_start) + + def __zoom(self, x): + self.update_width_virtual() + (xpos, ypos) = self.scroll.GetViewStart() + xpos = int(self.us_to_px(x) // self.scroll_scale) + self.scroll_panel.SetSize((int(self.width_virtual), int(self.height_virtual))) + self.scroll.SetScrollbars(self.scroll_scale, self.scroll_scale, + int(self.width_virtual // self.scroll_scale), + int(self.height_virtual // self.scroll_scale), + xpos, ypos) + self.Refresh() + + def zoom_in(self): + x = self.scroll_start_us() + self.zoom *= 2 + self.__zoom(x) + + def zoom_out(self): + x = self.scroll_start_us() + self.zoom /= 2 + self.__zoom(x) + + def on_key_press(self, event): + key = event.GetRawKeyCode() + if key == ord("+"): + self.zoom_in() + return + if key == ord("-"): + self.zoom_out() + return + + key = event.GetKeyCode() + (x, y) = self.scroll.GetViewStart() + if key == wx.WXK_RIGHT: + self.scroll.Scroll(x + 1, y) + elif key == wx.WXK_LEFT: + self.scroll.Scroll(x - 1, y) + elif key == wx.WXK_DOWN: + self.scroll.Scroll(x, y + 1) + elif key == wx.WXK_UP: + self.scroll.Scroll(x, y - 1) diff --git a/tools/perf/python/sched-migration.py b/tools/perf/python/sched-migration.py new file mode 100755 index 000000000000..d92c1a80d42f --- /dev/null +++ b/tools/perf/python/sched-migration.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Cpu task migration overview toy + +Copyright (C) 2010 Frederic Weisbecker +Ported to modern directory structure and refactored to use class. +""" +from __future__ import annotations + +import argparse +from collections import defaultdict, UserList +import importlib +import sys +from typing import Any +import perf + + +# Global threads dictionary +threads = defaultdict(lambda: "unknown") +threads[0] = "idle" + + +def thread_name(pid: int) -> str: + """Return thread name formatted with pid.""" + return f"{threads[pid]}:{pid}" + + +def task_state(state: int) -> str: + """Map task state integer to string.""" + states = { + 0: "R", + 1: "S", + 2: "D", + 64: "DEAD" + } + return states.get(state, "Unknown") + + +class RunqueueEventUnknown: + """Unknown runqueue event.""" + @staticmethod + def color(): + """Return color for event.""" + return None + + def __repr__(self): + return "unknown" + + +class RunqueueEventSleep: + """Sleep runqueue event.""" + @staticmethod + def color(): + """Return color for event.""" + return 0, 0, 0xff + + def __init__(self, sleeper: int): + self.sleeper = sleeper + + def __repr__(self): + return f"{thread_name(self.sleeper)} gone to sleep" + + +class RunqueueEventWakeup: + """Wakeup runqueue event.""" + @staticmethod + def color(): + """Return color for event.""" + return 0xff, 0xff, 0 + + def __init__(self, wakee: int): + self.wakee = wakee + + def __repr__(self): + return f"{thread_name(self.wakee)} woke up" + + +class RunqueueEventFork: + """Fork runqueue event.""" + @staticmethod + def color(): + """Return color for event.""" + return 0, 0xff, 0 + + def __init__(self, child: int): + self.child = child + + def __repr__(self): + return f"new forked task {thread_name(self.child)}" + + +class RunqueueMigrateIn: + """Migrate in runqueue event.""" + @staticmethod + def color(): + """Return color for event.""" + return 0, 0xf0, 0xff + + def __init__(self, new: int): + self.new = new + + def __repr__(self): + return f"task migrated in {thread_name(self.new)}" + + +class RunqueueMigrateOut: + """Migrate out runqueue event.""" + @staticmethod + def color(): + """Return color for event.""" + return 0xff, 0, 0xff + + def __init__(self, old: int): + self.old = old + + def __repr__(self): + return f"task migrated out {thread_name(self.old)}" + + +class RunqueueSnapshot: + """Snapshot of runqueue state.""" + + def __init__(self, tasks=None, event=None): + if tasks is None: + tasks = (0,) + if event is None: + event = RunqueueEventUnknown() + self.tasks = tuple(tasks) + self.event = event + + def sched_switch(self, prev: int, prev_state: int, next_pid: int): + """Handle sched switch in snapshot.""" + if task_state(prev_state) == "R" and next_pid in self.tasks \ + and prev in self.tasks: + return self + + event = ( + RunqueueEventSleep(prev) + if task_state(prev_state) != "R" + else RunqueueEventUnknown() + ) + + next_tasks = list(self.tasks[:]) + if prev in self.tasks: + if task_state(prev_state) != "R": + next_tasks.remove(prev) + elif task_state(prev_state) == "R": + next_tasks.append(prev) + + if next_pid not in next_tasks: + next_tasks.append(next_pid) + + return RunqueueSnapshot(next_tasks, event) + + def migrate_out(self, old: int): + """Handle task migrate out in snapshot.""" + if old not in self.tasks: + return self + next_tasks = [task for task in self.tasks if task != old] + + return RunqueueSnapshot(next_tasks, RunqueueMigrateOut(old)) + + def __migrate_in(self, new: int, event): + if new in self.tasks: + return RunqueueSnapshot(self.tasks, event) + next_tasks = self.tasks + tuple([new]) + + return RunqueueSnapshot(next_tasks, event) + + def migrate_in(self, new: int): + """Handle task migrate in snapshot.""" + return self.__migrate_in(new, RunqueueMigrateIn(new)) + + def wake_up(self, new: int): + """Handle task wakeup in snapshot.""" + return self.__migrate_in(new, RunqueueEventWakeup(new)) + + def wake_up_new(self, new: int): + """Handle task fork in snapshot.""" + return self.__migrate_in(new, RunqueueEventFork(new)) + + def load(self) -> int: + """Provide the number of tasks on the runqueue. Don't count idle""" + return len(self.tasks) - 1 + + def __repr__(self): + return self.tasks.__repr__() + + +class TimeSlice: + """Represents a time slice of execution.""" + + def __init__(self, start: int, prev): + self.start = start + self.prev = prev + self.end = start + # cpus that triggered the event + self.event_cpus: list[int] = [] + if prev is not None: + self.total_load = prev.total_load + self.rqs = prev.rqs.copy() + else: + self.rqs = defaultdict(RunqueueSnapshot) + self.total_load = 0 + + def __update_total_load(self, old_rq: RunqueueSnapshot, new_rq: RunqueueSnapshot): + diff = new_rq.load() - old_rq.load() + self.total_load += diff + + def sched_switch(self, ts_list, prev: int, prev_state: int, next_pid: int, cpu: int): + """Process sched_switch in time slice.""" + old_rq = self.prev.rqs[cpu] + new_rq = old_rq.sched_switch(prev, prev_state, next_pid) + + if old_rq is new_rq: + return + + self.rqs[cpu] = new_rq + self.__update_total_load(old_rq, new_rq) + ts_list.append(self) + self.event_cpus = [cpu] + + def migrate(self, ts_list, new: int, old_cpu: int, new_cpu: int): + """Process task migration in time slice.""" + if old_cpu == new_cpu: + return + old_rq = self.prev.rqs[old_cpu] + out_rq = old_rq.migrate_out(new) + self.rqs[old_cpu] = out_rq + self.__update_total_load(old_rq, out_rq) + + new_rq = self.prev.rqs[new_cpu] + in_rq = new_rq.migrate_in(new) + self.rqs[new_cpu] = in_rq + self.__update_total_load(new_rq, in_rq) + + ts_list.append(self) + + if old_rq is not out_rq: + self.event_cpus.append(old_cpu) + self.event_cpus.append(new_cpu) + + def wake_up(self, ts_list, pid: int, cpu: int, fork: bool): + """Process wakeup in time slice.""" + old_rq = self.prev.rqs[cpu] + if fork: + new_rq = old_rq.wake_up_new(pid) + else: + new_rq = old_rq.wake_up(pid) + + if new_rq is old_rq: + return + self.rqs[cpu] = new_rq + self.__update_total_load(old_rq, new_rq) + ts_list.append(self) + self.event_cpus = [cpu] + + def next(self, t: int): + """Create next time slice.""" + self.end = t + return TimeSlice(t, self) + + +class TimeSliceList(UserList): + """List of time slices with search capabilities.""" + + def __init__(self, arg=None): + super().__init__(arg if arg is not None else []) + self.root_win = None + + def get_time_slice(self, ts: int) -> TimeSlice: + """Get or create time slice for timestamp.""" + if len(self.data) == 0: + ts_slice = TimeSlice(ts, TimeSlice(-1, None)) + else: + ts_slice = self.data[-1].next(ts) + return ts_slice + + def find_time_slice(self, ts: int) -> int: + """Binary search for time slice containing timestamp.""" + if not self.data: + return -1 + start = 0 + end = len(self.data) + found = -1 + searching = True + while searching: + if start in (end, end - 1): + searching = False + + i = (end + start) // 2 + if self.data[i].start <= ts <= self.data[i].end: + found = i + break + + if self.data[i].end < ts: + start = i + elif self.data[i].start > ts: + end = i + + return found + + def set_root_win(self, win): + """Set root window for GUI.""" + self.root_win = win + + def mouse_down(self, cpu: int, t: int): + """Handle mouse down event from GUI.""" + idx = self.find_time_slice(t) + if idx == -1: + return + + ts = self[idx] + rq = ts.rqs[cpu] + raw = f"CPU: {cpu}\n" + raw += f"Last event : {repr(rq.event)}\n" + raw += f"Timestamp : {ts.start // (10 ** 9)}.{ts.start % (10 ** 9) // 1000:06d}\n" + raw += f"Duration : {(ts.end - ts.start) // (10 ** 3):6d} us\n" + raw += f"Load = {rq.load()}\n" + for task in rq.tasks: + raw += f"{thread_name(task)} \n" + + if self.root_win: + self.root_win.update_summary(raw) + + def update_rectangle_cpu(self, slice_obj: TimeSlice, cpu: int): + """Update rectangle for CPU in GUI.""" + rq = slice_obj.rqs[cpu] + + if slice_obj.total_load != 0: + load_rate = rq.load() / float(slice_obj.total_load) + else: + load_rate = 0 + + red_power = int(0xff - (0xff * load_rate)) + color = (0xff, red_power, red_power) + + top_color = None + if cpu in slice_obj.event_cpus: + top_color = rq.event.color() + + if self.root_win: + self.root_win.paint_rectangle_zone(cpu, color, top_color, + slice_obj.start, slice_obj.end) + + def fill_zone(self, start: int, end: int): + """Fill zone in GUI.""" + i = self.find_time_slice(start) + if i == -1: + return + + for idx in range(i, len(self.data)): + timeslice = self.data[idx] + if timeslice.start > end: + return + + for cpu in timeslice.rqs: + self.update_rectangle_cpu(timeslice, cpu) + + def interval(self) -> tuple[int, int]: + """Return start and end timestamps.""" + if len(self.data) == 0: + return 0, 0 + return self.data[0].start, self.data[-1].end + + def nr_rectangles(self) -> int: + """Return maximum CPU number.""" + if not self.data: + return 0 + last_ts = self.data[-1] + max_cpu = 0 + for cpu in last_ts.rqs: + max_cpu = max(max_cpu, cpu) + return max_cpu + + +class SchedMigrationAnalyzer: + """Analyzes task migrations and manages time slices.""" + + def __init__(self): + self.current_tsk = defaultdict(lambda: -1) + self.timeslices = TimeSliceList() + + def sched_switch(self, time: int, cpu: int, prev_comm: str, prev_pid: int, prev_state: int, + next_comm: str, next_pid: int): + """Handle sched_switch event.""" + on_cpu_task = self.current_tsk[cpu] + + if on_cpu_task not in (-1, prev_pid): + print(f"Sched switch event rejected ts: {time} cpu: {cpu} " + f"prev: {prev_comm}({prev_pid}) next: {next_comm}({next_pid})") + + threads[prev_pid] = prev_comm + threads[next_pid] = next_comm + self.current_tsk[cpu] = next_pid + + ts = self.timeslices.get_time_slice(time) + ts.sched_switch(self.timeslices, prev_pid, prev_state, next_pid, cpu) + + def migrate(self, time: int, pid: int, orig_cpu: int, dest_cpu: int): + """Handle sched_migrate_task event.""" + ts = self.timeslices.get_time_slice(time) + ts.migrate(self.timeslices, pid, orig_cpu, dest_cpu) + + def wake_up(self, time: int, pid: int, success: int, target_cpu: int, fork: bool): + """Handle wakeup event.""" + if success == 0: + return + ts = self.timeslices.get_time_slice(time) + ts.wake_up(self.timeslices, pid, target_cpu, fork) + + def process_event(self, sample: perf.sample_event) -> None: + """Collect events and pass to analyzer.""" + name = str(sample.evsel) + if name.startswith("evsel(") and name.endswith(")"): + name = name[6:-1] + if name.count(":") >= 2: + name = ":".join(name.split(":")[:2]) + time = sample.sample_time + cpu = sample.sample_cpu + _pid = sample.sample_pid + _comm = "Unknown" + + if name.startswith("sched:sched_switch"): + prev_comm = getattr(sample, "prev_comm", "Unknown") + prev_pid = getattr(sample, "prev_pid", -1) + prev_state = getattr(sample, "prev_state", 0) + next_comm = getattr(sample, "next_comm", "Unknown") + next_pid = getattr(sample, "next_pid", -1) + self.sched_switch(time, cpu, prev_comm, prev_pid, prev_state, next_comm, next_pid) + elif name.startswith("sched:sched_migrate_task"): + task_pid = getattr(sample, "pid", -1) + orig_cpu = getattr(sample, "orig_cpu", -1) + dest_cpu = getattr(sample, "dest_cpu", -1) + self.migrate(time, task_pid, orig_cpu, dest_cpu) + elif name.startswith("sched:sched_wakeup_new"): + task_pid = getattr(sample, "pid", -1) + success = getattr(sample, "success", 1) + target_cpu = getattr(sample, "target_cpu", -1) + self.wake_up(time, task_pid, success, target_cpu, True) + elif name.startswith("sched:sched_wakeup"): + task_pid = getattr(sample, "pid", -1) + success = getattr(sample, "success", 1) + target_cpu = getattr(sample, "target_cpu", -1) + self.wake_up(time, task_pid, success, target_cpu, False) + + def run_gui(self): + """Start wxPython GUI.""" + try: + wx_mod: Any = importlib.import_module("wx") + sched_gui: Any = importlib.import_module("SchedGui") + except ImportError: + print("wxPython is not available. Cannot start GUI.") + return + app = wx_mod.App(False) + _frame = sched_gui.RootFrame(self.timeslices, "Migration") + app.MainLoop() + + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Cpu task migration overview toy") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + ap.add_argument("-v", "--verbose", action="store_true", + help="Print parsed migration summary") + ap.add_argument("--gui", action="store_true", default=True, + help="Start the GUI (default)") + ap.add_argument("--no-gui", action="store_true", help="Do not start the GUI") + args = ap.parse_args() + + analyzer = SchedMigrationAnalyzer() + session = None + + try: + try: + session = perf.session(perf.data(args.input), sample=analyzer.process_event) + try: + session.process_events() + except KeyboardInterrupt: + pass + finally: + # Release the perf.session reference before launching the long-running + # wxPython GUI event loop so the underlying C session and callback + # reference cycle are freed immediately. + session = None + if args.verbose: + start_ts, end_ts = analyzer.timeslices.interval() + print(f"Timeslices: {len(analyzer.timeslices.data)} " + f"(interval: {start_ts}..{end_ts})") + if args.gui and not args.no_gui: + analyzer.run_gui() + except KeyboardInterrupt: + pass + except OSError as e: + print(f"Error processing events: {e}") + sys.exit(1) diff --git a/tools/perf/tests/shell/test_sched_migration_python.sh b/tools/perf/tests/shell/test_sched_migration_python.sh new file mode 100755 index 000000000000..f243e86c6700 --- /dev/null +++ b/tools/perf/tests/shell/test_sched_migration_python.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# sched-migration python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/sched-migration.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, sched-migration.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing sched-migration.py..." + +# Create a perf.data file. Force dropping a packet if tracepoint is available! +ev="sched:sched_switch,sched:sched_migrate_task" +ev="${ev},sched:sched_wakeup_new,sched:sched_wakeup" +has_sched=1 +if ! perf record -e "$ev" -a -o "${temp_data}" \ + -- sleep 0.1 >/dev/null 2>&1; then + has_sched=0 + perf record -e cycles -o "${temp_data}" \ + -- perf test -w noploop >/dev/null 2>&1 || \ + { echo "Skipping test, perf record failed"; exit 2; } +fi + +if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 +fi + +# Check that the script executes +if ! "$PYTHON" "$script_path" -v --no-gui -i "${temp_data}" > "${temp_out}"; then + echo "sched-migration.py test failed" + err=1 +elif [ "$has_sched" -eq 1 ] && ! grep -q "Timeslices:" "${temp_out}"; then + echo "sched-migration.py missing Timeslices output" + err=1 +else + echo "sched-migration test passed." +fi +rm -f "${temp_out}" + +exit $err From 97c70711fc5b7446e66340dcf8b40dd31c27b5d3 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:00 -0700 Subject: [PATCH 208/425] perf python: Port wakeup-latency from Perl to perf module Replace the legacy Perl script wakeup-latency.pl with a standalone Python script in tools/perf/python/wakeup-latency.py using the perf Python module. Improvements compared to the legacy Perl script: - Remove the dependency on libperl and Perf::Trace::Util. - Track wakeup timestamps per-task (keyed by the wakee PID on sched:sched_wakeup and matched against next_pid on sched:sched_switch) rather than per-CPU, avoiding mixed latencies when multiple wakeups occur on a CPU before a context switch. - Guard print_totals() when total_wakeups == 0 (printing 'N/A' instead of dividing by zero when a trace contains no matched wakeup/switch pairs). - Add argparse CLI options (-i/--input) and full type annotations. Add a shell test (test_wakeup_latency_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/wakeup-latency.py | 103 ++++++++++++++++++ .../tests/shell/test_wakeup_latency_python.sh | 82 ++++++++++++++ 2 files changed, 185 insertions(+) create mode 100755 tools/perf/python/wakeup-latency.py create mode 100755 tools/perf/tests/shell/test_wakeup_latency_python.sh diff --git a/tools/perf/python/wakeup-latency.py b/tools/perf/python/wakeup-latency.py new file mode 100755 index 000000000000..f77045d02734 --- /dev/null +++ b/tools/perf/python/wakeup-latency.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Display avg/min/max wakeup latency.""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import sys +from typing import Optional, Dict +import perf + +class WakeupLatency: + """Tracks and displays wakeup latency statistics.""" + def __init__(self) -> None: + self.last_wakeup: Dict[int, int] = defaultdict(int) + self.max_wakeup_latency: int = 0 + self.min_wakeup_latency: Optional[int] = None + self.total_wakeup_latency = 0 + self.total_wakeups = 0 + self.unhandled: Dict[str, int] = defaultdict(int) + self.session: Optional[perf.session] = None + + def process_event(self, sample: perf.sample_event) -> None: + """Process events.""" + event_name = str(sample.evsel) + if event_name.startswith("evsel(") and event_name.endswith(")"): + event_name = event_name[6:-1] + sample_time = sample.sample_time + + if "sched:sched_wakeup" in event_name: + try: + pid = sample.pid + self.last_wakeup[pid] = sample_time + except AttributeError: + self.unhandled[event_name] += 1 + elif "sched:sched_switch" in event_name: + try: + next_pid = sample.next_pid + wakeup_ts = self.last_wakeup.get(next_pid, 0) + if wakeup_ts: + latency = sample_time - wakeup_ts + self.max_wakeup_latency = max(self.max_wakeup_latency, latency) + if self.min_wakeup_latency is None: + self.min_wakeup_latency = latency + else: + self.min_wakeup_latency = min(self.min_wakeup_latency, latency) + self.total_wakeup_latency += latency + self.total_wakeups += 1 + del self.last_wakeup[next_pid] + except AttributeError: + self.unhandled[event_name] += 1 + else: + self.unhandled[event_name] += 1 + + def print_totals(self) -> None: + """Print summary statistics.""" + print("wakeup_latency stats:\n") + print(f"total_wakeups: {self.total_wakeups}") + if self.total_wakeups: + avg = self.total_wakeup_latency // self.total_wakeups + print(f"avg_wakeup_latency (ns): {avg}") + print(f"min_wakeup_latency (ns): {self.min_wakeup_latency}") + print(f"max_wakeup_latency (ns): {self.max_wakeup_latency}") + else: + print("avg_wakeup_latency (ns): N/A") + print("min_wakeup_latency (ns): N/A") + print("max_wakeup_latency (ns): N/A") + + if self.unhandled: + print("\nunhandled events:\n") + print(f"{'event':<40s} {'count':>10s}") + print(f"{'-'*40} {'-'*10}") + for event_name, count in self.unhandled.items(): + print(f"{event_name:<40s} {count:10d}") + + def run(self, input_file: str) -> None: + """Run the session.""" + self.session = perf.session(perf.data(input_file), sample=self.process_event) + try: + self.session.process_events() + except KeyboardInterrupt: + pass + finally: + # Break the reference cycle between self.session and the bound + # self.process_event callback so the C perf.session object is freed. + self.session = None + self.print_totals() + +def main() -> None: + """Main function.""" + parser = argparse.ArgumentParser(description="Trace wakeup latency") + parser.add_argument("-i", "--input", default="perf.data", help="Input file") + args = parser.parse_args() + + analyzer = WakeupLatency() + try: + analyzer.run(args.input) + except IOError as e: + print(e, file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_wakeup_latency_python.sh b/tools/perf/tests/shell/test_wakeup_latency_python.sh new file mode 100755 index 000000000000..fc39ac117609 --- /dev/null +++ b/tools/perf/tests/shell/test_wakeup_latency_python.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# wakeup-latency python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +if ! perf check feature -q libtraceevent; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/wakeup-latency.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, wakeup-latency.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing wakeup-latency.py..." + +# Create a perf.data file. Try to get tracepoint data. +if perf list | grep -q "sched:sched_wakeup"; then + ev="sched:sched_wakeup,sched:sched_wakeup_new,sched:sched_switch" + perf record -e "$ev" -a -o "${temp_data}" \ + -- sleep 0.1 >/dev/null 2>&1 || \ + { echo "Skipping test, perf record failed"; exit 2; } +else + echo "Skipping test, no sched:sched_wakeup event" + exit 2 +fi + +if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 +fi + +# Check that the script executes +if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "wakeup-latency.py test failed" + err=1 +else + if ! grep -E -q "avg_wakeup_latency.*[0-9]+" "${temp_out}"; then + echo "Failed to find metric data rows" + err=1 + else + echo "wakeup-latency test passed." + fi +fi + +# Also test zero-wakeups / unhandled events path to verify division-by-zero protection +if perf record -e cycles -o "${temp_data}" -- perf test -w noploop >/dev/null 2>&1; then + if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}" || \ + ! grep -q "avg_wakeup_latency (ns): N/A" "${temp_out}"; then + echo "wakeup-latency zero-wakeups guard test failed" + err=1 + fi +fi +rm -f "${temp_out}" + +exit $err From 45266bbeead70f2d4f02f5255ed4cb35fd74778a Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:01 -0700 Subject: [PATCH 209/425] perf python: Port compaction-times to perf module Port compaction-times.py to a standalone script in tools/perf/python/ using the perf module directly to analyze mm_compaction tracepoints. Improvements compared to the legacy script: - Replace Python 2 constructs (such as sys.maxint and raw integer bitmasks) with Python 3 enum.IntEnum (Popt) and enum.IntFlag (Topt) types. - Access tracepoint fields directly on perf.sample_event and add -i/--input CLI option support via argparse. - Add full type annotations passing mypy and pylint without suppression comments. Add a shell test (test_compaction_times_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/compaction-times.py | 362 ++++++++++++++++++ .../shell/test_compaction_times_python.sh | 127 ++++++ 2 files changed, 489 insertions(+) create mode 100755 tools/perf/python/compaction-times.py create mode 100755 tools/perf/tests/shell/test_compaction_times_python.sh diff --git a/tools/perf/python/compaction-times.py b/tools/perf/python/compaction-times.py new file mode 100755 index 000000000000..48d6af005c46 --- /dev/null +++ b/tools/perf/python/compaction-times.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Report time spent in memory compaction. + +Memory compaction is a feature in the Linux kernel that defragments memory +by moving used pages to create larger contiguous blocks of free memory. This +is particularly useful for allocating huge pages. + +This script processes trace events related to memory compaction and reports: +- Total time spent in compaction (stall time). +- Statistics for page migration (moved vs. failed). +- Statistics for the free scanner (scanned vs. isolated pages). +- Statistics for the migration scanner (scanned vs. isolated pages). + +Definitions: +- **Compaction**: Defragmenting memory by moving allocated pages. +- **Migration**: Moving pages from their current location to free pages found by the free scanner. +- **Free Scanner**: Scans memory (typically from the end of a zone) to find free pages. +- **Migration Scanner**: Scans memory (typically from the beginning of a zone) + to find pages to move. +- **Isolated Pages**: Pages that have been temporarily removed from the buddy + system for migration or as migration targets. + +Ported from tools/perf/scripts/python/compaction-times.py to the modern perf Python module. +""" +from __future__ import annotations + +import argparse +import enum +import re +import signal +import sys +from typing import Callable, Dict, List, Optional, Any +import perf + +if hasattr(signal, "SIGPIPE"): + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + +class Popt(enum.IntEnum): + """Process display options.""" + DISP_DFL = 0 + DISP_PROC = 1 + DISP_PROC_VERBOSE = 2 + +class Topt(enum.IntFlag): + """Trace display options.""" + DISP_TIME = 0 + DISP_MIG = 1 + DISP_ISOLFREE = 2 + DISP_ISOLMIG = 4 + DISP_ALL = DISP_MIG | DISP_ISOLFREE | DISP_ISOLMIG + +# Globals to satisfy pylint when accessed in functions before assignment in main. +OPT_NS = True +opt_disp = Topt.DISP_ALL +opt_proc = Popt.DISP_DFL +session = None + +def get_comm_filter(regex: re.Pattern) -> Callable[[int, str], bool]: + """Returns a filter function based on command regex.""" + def filter_func(_pid: int, comm: str) -> bool: + regex_match = regex.search(comm) + return regex_match is None or regex_match.group() == "" + return filter_func + +def get_pid_filter(low_str: str, high_str: str) -> Callable[[int, str], bool]: + """Returns a filter function based on PID range.""" + low = 0 if low_str == "" else int(low_str) + high = None if high_str == "" else int(high_str) + + def filter_func(pid: int, _comm: str) -> bool: + return not (pid >= low and (high is None or pid <= high)) + return filter_func + +def ns_to_time(ns: int) -> str: + """Format nanoseconds to string based on options.""" + return f"{ns}ns" if OPT_NS else f"{round(ns, -3) // 1000}us" + +class Pair: + """Represents a pair of related counters (e.g., scanned vs isolated, moved vs failed).""" + def __init__(self, aval: int, bval: int, + alabel: Optional[str] = None, blabel: Optional[str] = None): + self.alabel = alabel + self.blabel = blabel + self.aval = aval + self.bval = bval + + def __add__(self, rhs: 'Pair') -> 'Pair': + return Pair(self.aval + rhs.aval, self.bval + rhs.bval, self.alabel, self.blabel) + + def __iadd__(self, rhs: 'Pair') -> 'Pair': + self.aval += rhs.aval + self.bval += rhs.bval + return self + + def __str__(self) -> str: + return f"{self.alabel}={self.aval} {self.blabel}={self.bval}" + +class Cnode: + """Holds statistics for a single compaction event or an aggregated set of events.""" + def __init__(self, ns: int): + self.ns = ns + self.migrated = Pair(0, 0, "moved", "failed") + self.fscan = Pair(0, 0, "scanned", "isolated") + self.mscan = Pair(0, 0, "scanned", "isolated") + + def __add__(self, rhs: 'Cnode') -> 'Cnode': + res = Cnode(self.ns + rhs.ns) + res.migrated = self.migrated + rhs.migrated + res.fscan = self.fscan + rhs.fscan + res.mscan = self.mscan + rhs.mscan + return res + + def __iadd__(self, rhs: 'Cnode') -> 'Cnode': + self.ns += rhs.ns + self.migrated += rhs.migrated + self.fscan += rhs.fscan + self.mscan += rhs.mscan + return self + + def __str__(self) -> str: + prev = False + s = f"{ns_to_time(self.ns)} " + if opt_disp & Topt.DISP_MIG: + s += f"migration: {self.migrated}" + prev = True + if opt_disp & Topt.DISP_ISOLFREE: + s += f"{' ' if prev else ''}free_scanner: {self.fscan}" + prev = True + if opt_disp & Topt.DISP_ISOLMIG: + s += f"{' ' if prev else ''}migration_scanner: {self.mscan}" + return s + + def complete(self, secs: int, nsecs: int) -> None: + """Complete the node with duration.""" + self.ns = (secs * 1000000000 + nsecs) - self.ns + + def increment(self, migrated: Optional[Pair], fscan: Optional[Pair], + mscan: Optional[Pair]) -> None: + """Increment statistics.""" + if migrated is not None: + self.migrated += migrated + if fscan is not None: + self.fscan += fscan + if mscan is not None: + self.mscan += mscan + +class Chead: + """Aggregates compaction statistics per process (PID) and maintains total statistics.""" + heads: Dict[int, 'Chead'] = {} + val = Cnode(0) + fobj: Optional[Any] = None + + @classmethod + def add_filter(cls, fobj: Any) -> None: + """Add a filter object.""" + cls.fobj = fobj + + @classmethod + def create_pending(cls, pid: int, comm: str, start_secs: int, start_nsecs: int) -> None: + """Create a pending node for a process.""" + filtered = False + try: + head = cls.heads[pid] + filtered = head.is_filtered() + except KeyError: + if cls.fobj is not None: + filtered = cls.fobj(pid, comm) + head = cls.heads[pid] = Chead(comm, pid, filtered) + + if not filtered: + head.mark_pending(start_secs, start_nsecs) + + @classmethod + def increment_pending(cls, pid: int, migrated: Optional[Pair], + fscan: Optional[Pair], mscan: Optional[Pair]) -> None: + """Increment pending stats for a process.""" + if pid not in cls.heads: + return + head = cls.heads[pid] + if not head.is_filtered(): + if head.is_pending(): + head.do_increment(migrated, fscan, mscan) + else: + sys.stderr.write(f"missing start compaction event for pid {pid}\n") + + @classmethod + def complete_pending(cls, pid: int, secs: int, nsecs: int) -> None: + """Complete pending stats for a process.""" + if pid not in cls.heads: + return + head = cls.heads[pid] + if not head.is_filtered(): + if head.is_pending(): + head.make_complete(secs, nsecs) + else: + sys.stderr.write(f"missing start compaction event for pid {pid}\n") + + @classmethod + def gen(cls): + """Generate heads for display.""" + if opt_proc != Popt.DISP_DFL: + yield from cls.heads.values() + + @classmethod + def get_total(cls) -> Cnode: + """Get total statistics.""" + return cls.val + + def __init__(self, comm: str, pid: int, filtered: bool): + self.comm = comm + self.pid = pid + self.val = Cnode(0) + self.pending: Optional[Cnode] = None + self.filtered = filtered + self.list: List[Cnode] = [] + + def mark_pending(self, secs: int, nsecs: int) -> None: + """Mark node as pending.""" + self.pending = Cnode(secs * 1000000000 + nsecs) + + def do_increment(self, migrated: Optional[Pair], fscan: Optional[Pair], + mscan: Optional[Pair]) -> None: + """Increment pending stats.""" + if self.pending is not None: + self.pending.increment(migrated, fscan, mscan) + + def make_complete(self, secs: int, nsecs: int) -> None: + """Make pending stats complete.""" + if self.pending is not None: + self.pending.complete(secs, nsecs) + Chead.val += self.pending + + if opt_proc != Popt.DISP_DFL: + self.val += self.pending + + if opt_proc == Popt.DISP_PROC_VERBOSE: + self.list.append(self.pending) + self.pending = None + + def enumerate(self) -> None: + """Enumerate verbose stats.""" + if opt_proc == Popt.DISP_PROC_VERBOSE and not self.is_filtered(): + for i, pelem in enumerate(self.list): + sys.stdout.write(f"{self.pid}[{self.comm}].{i+1}: {pelem}\n") + + def is_pending(self) -> bool: + """Check if node is pending.""" + return self.pending is not None + + def is_filtered(self) -> bool: + """Check if node is filtered.""" + return self.filtered + + def display(self) -> None: + """Display stats.""" + if not self.is_filtered(): + sys.stdout.write(f"{self.pid}[{self.comm}]: {self.val}\n") + +def trace_end() -> None: + """Called at the end of trace processing.""" + sys.stdout.write(f"total: {Chead.get_total()}\n") + for i in Chead.gen(): + i.display() + i.enumerate() + +def process_event(sample: perf.sample_event) -> None: + """Callback for processing events.""" + event_name = str(sample.evsel) + pid = sample.sample_tid + comm = "[unknown]" + try: + if session: + thread = session.find_thread(sample.sample_pid, pid) + if thread: + comm = thread.comm() or "[unknown]" + except (TypeError, AttributeError): + pass + secs = sample.sample_time // 1000000000 + nsecs = sample.sample_time % 1000000000 + + if "evsel(compaction:mm_compaction_begin)" in event_name: + Chead.create_pending(pid, comm, secs, nsecs) + elif "evsel(compaction:mm_compaction_end)" in event_name: + Chead.complete_pending(pid, secs, nsecs) + elif "evsel(compaction:mm_compaction_migratepages)" in event_name: + nr_migrated = getattr(sample, "nr_migrated", 0) + nr_failed = getattr(sample, "nr_failed", 0) + Chead.increment_pending(pid, Pair(nr_migrated, nr_failed), None, None) + elif "evsel(compaction:mm_compaction_isolate_freepages)" in event_name: + nr_scanned = getattr(sample, "nr_scanned", 0) + nr_taken = getattr(sample, "nr_taken", 0) + Chead.increment_pending(pid, None, Pair(nr_scanned, nr_taken), None) + elif "evsel(compaction:mm_compaction_isolate_migratepages)" in event_name: + nr_scanned = getattr(sample, "nr_scanned", 0) + nr_taken = getattr(sample, "nr_taken", 0) + Chead.increment_pending(pid, None, None, Pair(nr_scanned, nr_taken)) + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Report time spent in compaction") + ap.add_argument("-p", action="store_true", help="display by process") + ap.add_argument("-pv", action="store_true", help="display by process (verbose)") + ap.add_argument("-u", action="store_true", help="display results in microseconds") + ap.add_argument("-t", action="store_true", help="display stall times only") + ap.add_argument("-m", action="store_true", help="display stats for migration") + ap.add_argument("-fs", action="store_true", help="display stats for free scanner") + ap.add_argument("-ms", action="store_true", help="display stats for migration scanner") + ap.add_argument("filter", nargs="?", help="pid|pid-range|comm-regex") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + args = ap.parse_args() + + opt_proc = Popt.DISP_DFL + if args.pv: + opt_proc = Popt.DISP_PROC_VERBOSE + elif args.p: + opt_proc = Popt.DISP_PROC + + OPT_NS = not args.u + + opt_disp = Topt.DISP_ALL + if args.t or args.m or args.fs or args.ms: + opt_disp = Topt(0) + if args.t: + opt_disp |= Topt.DISP_TIME + if args.m: + opt_disp |= Topt.DISP_MIG + if args.fs: + opt_disp |= Topt.DISP_ISOLFREE + if args.ms: + opt_disp |= Topt.DISP_ISOLMIG + + if args.filter: + PID_PATTERN = r"^(\d*)-(\d*)$|^(\d*)$" + pid_re = re.compile(PID_PATTERN) + match = pid_re.search(args.filter) + filter_obj: Any = None + if match is not None and match.group() != "": + if match.group(3) is not None: + filter_obj = get_pid_filter(match.group(3), match.group(3)) + else: + filter_obj = get_pid_filter(match.group(1), match.group(2)) + else: + try: + comm_re = re.compile(args.filter) + except re.error: + sys.stderr.write(f"invalid regex '{args.filter}'\n") + sys.exit(1) + filter_obj = get_comm_filter(comm_re) + Chead.add_filter(filter_obj) + + try: + session = perf.session(perf.data(args.input), sample=process_event) + try: + session.process_events() + except KeyboardInterrupt: + pass + trace_end() + finally: + # Break the reference cycle between the global session and the + # process_event callback so the C perf.session object is freed. + session = None diff --git a/tools/perf/tests/shell/test_compaction_times_python.sh b/tools/perf/tests/shell/test_compaction_times_python.sh new file mode 100755 index 000000000000..ad950f3602b5 --- /dev/null +++ b/tools/perf/tests/shell/test_compaction_times_python.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# compaction-times python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +if ! perf check feature -q libtraceevent; then + echo "Skipping test, perf built without libtraceevent" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/compaction-times.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, compaction-times.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} + +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +test_file_mode() { + echo "Testing compaction-times.py..." + + # Check for any compaction events to see if kernel supports it + if ! perf list | grep -q "compaction:mm_compaction_begin"; then + echo "Skipping test, compaction tracepoints not found" + exit 2 + fi + + # Generate some events + # We might not naturally trigger compaction in 0.5s sleep, but the script + # should parse the empty or sparse file correctly without crashing. + if ! perf record -e "compaction:*" -a -o "${temp_data}" -- sleep 0.5 >/dev/null 2>&1; then + echo "Skipping test, perf record failed" + exit 2 + fi + + # Run the script with some filters to validate filtering logic + if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "File mode default test failed." + err=1 + elif ! grep -q "total:" "${temp_out}"; then + echo "File mode default test missing 'total:' output." + err=1 + fi + + if ! "$PYTHON" "$script_path" -i "${temp_data}" "0-0" > "${temp_out}"; then + echo "File mode strict PID filter test failed." + err=1 + elif ! grep -q "total:" "${temp_out}"; then + echo "File mode strict PID filter test missing 'total:' output." + err=1 + fi + + if ! "$PYTHON" "$script_path" -i "${temp_data}" "sleep" > "${temp_out}"; then + echo "File mode comm filter test failed." + err=1 + elif ! grep -q "total:" "${temp_out}"; then + echo "File mode comm filter test missing 'total:' output." + err=1 + fi + + # Deterministic unit test of comm filtering and non-zero duration aggregation + if ! "$PYTHON" -c " +import re, sys, importlib.util +spec = importlib.util.spec_from_file_location('compaction_times', sys.argv[1]) +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) + +class DummyThread: + def comm(self): return 'sleep' + +class DummySession: + def find_thread(self, pid, tid): return DummyThread() + +class DummySample: + def __init__(self, evsel, time, **kw): + self.evsel = evsel + self.sample_time = time + self.sample_pid = 100 + self.sample_tid = 100 + for k, v in kw.items(): + setattr(self, k, v) + +mod.Chead.add_filter(mod.get_comm_filter(re.compile('sleep'))) +mod.session = DummySession() +mod.process_event(DummySample('evsel(compaction:mm_compaction_begin)', 1000)) +mod.process_event(DummySample('evsel(compaction:mm_compaction_end)', 2500)) +mod.trace_end() +" "$script_path" > "${temp_out}"; then + echo "Deterministic comm filter unit test failed." + err=1 + elif ! grep -q "total: 1500ns" "${temp_out}"; then + echo "Deterministic comm filter unit test did not report expected 1500ns total." + err=1 + fi + + if [ $err -eq 0 ]; then + echo "File mode test passed." + fi +} + +test_file_mode + +exit $err From c1864af54c89adb9f32737a07831398f5eea4981 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:02 -0700 Subject: [PATCH 210/425] perf python: Port net_dropmonitor to perf module Port net_dropmonitor.py from tools/perf/scripts/python/ to a standalone script in tools/perf/python/: - Refactor the script into a DropMonitor class with full type annotations to encapsulate state. - Use perf.session for skb:kfree_skb event processing and add argparse CLI support (-i/--input and -k/--kallsyms). - Resolve kernel drop addresses via perf.session symbols/callchains and binary search over /proc/kallsyms, ignoring zeroed kptr_restrict addresses with graceful fallback when kallsyms is unavailable. - Remove Python 2 compatibility code. Add a shell test (test_net_dropmonitor_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/net_dropmonitor.py | 173 ++++++++++++++++++ .../shell/test_net_dropmonitor_python.sh | 102 +++++++++++ 2 files changed, 275 insertions(+) create mode 100755 tools/perf/python/net_dropmonitor.py create mode 100755 tools/perf/tests/shell/test_net_dropmonitor_python.sh diff --git a/tools/perf/python/net_dropmonitor.py b/tools/perf/python/net_dropmonitor.py new file mode 100755 index 000000000000..fff637b5c7e9 --- /dev/null +++ b/tools/perf/python/net_dropmonitor.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Monitor the system for dropped packets and produce a report of drop locations and counts. +Ported from tools/perf/scripts/python/net_dropmonitor.py +""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import os +import sys +from typing import Tuple +import perf + + +class DropMonitor: + """Monitors dropped packets and aggregates counts by location.""" + + def __init__(self, kallsyms_path: str | None = None) -> None: + self.drop_log: dict[int, int] = defaultdict(int) + self.kallsyms: list[Tuple[int, str]] = [] + self.kallsyms_parsed = False + self.resolved_syms: dict[int, Tuple[str, int]] = {} + self.callchain_syms: dict[int, str] = {} + self.kallsyms_path = ( + kallsyms_path + or os.environ.get("PERF_SYMBOL_KALLSYMS") + or "/proc/kallsyms" + ) + + def _parse_kallsyms(self) -> None: + """Parse the kallsyms file and map kernel addresses to function symbols.""" + self.kallsyms.clear() + self.kallsyms_parsed = True + try: + with open(self.kallsyms_path, "r", encoding="utf-8") as f: + for line in f: + parts = line.split() + if len(parts) >= 3 and parts[1] in ('t', 'T', 'w', 'W'): + addr = int(parts[0], 16) + if addr > 0: + self.kallsyms.append((addr, parts[2])) + self.kallsyms.sort(key=lambda x: x[0]) + except (FileNotFoundError, PermissionError): + print(f"Failed to read {self.kallsyms_path}. Symbols will not be resolved.") + + def _get_sym(self, loc: int) -> Tuple[str, int]: + """Resolve a memory location using session symbols or the kallsyms map.""" + # Priority order: + # 1. Exact symbols with offsets resolved directly from the perf.data session + # (resolved_syms). + # 2. Symbols captured from the sample's callchain in the trace file + # (callchain_syms) before falling back to self.kallsyms, so offline + # trace symbols are not overwritten by the live host's /proc/kallsyms. + # 3. Binary search in self.kallsyms (from --kallsyms or /proc/kallsyms). + if loc in self.resolved_syms: + return self.resolved_syms[loc] + if loc in self.callchain_syms: + res = (self.callchain_syms[loc], 0) + self.resolved_syms[loc] = res + return res + if not self.kallsyms: + return f"{loc:#x}", 0 + + start = 0 + end = len(self.kallsyms) - 1 + while start < end: + mid = (start + end) // 2 + if self.kallsyms[mid][0] <= loc < self.kallsyms[mid+1][0]: + start = mid + break + if loc < self.kallsyms[mid][0]: + end = mid - 1 + else: + start = mid + 1 + + sym_addr, sym_name = self.kallsyms[start] + if loc >= sym_addr: + res = (sym_name, loc - sym_addr) + self.resolved_syms[loc] = res + return res + return f"{loc:#x}", 0 + + def print_drop_table(self) -> None: + """Print aggregated results.""" + if not self.drop_log: + print(f"{'LOCATION':>25} {'OFFSET':>25} {'COUNT':>25}") + return + + if (not self.kallsyms_parsed + and any(loc not in self.resolved_syms and loc not in self.callchain_syms + for loc in self.drop_log)): + print("Gathering kallsyms data") + self._parse_kallsyms() + + print(f"{'LOCATION':>25} {'OFFSET':>25} {'COUNT':>25}") + sorted_keys = sorted(self.drop_log.keys()) + for sloc in sorted_keys: + sym, off = self._get_sym(sloc) + print(f"{sym:>25} {off:>25d} {self.drop_log[sloc]:>25d}") + + def process_event(self, sample: perf.sample_event) -> None: + """Process a single sample event.""" + if "skb:kfree_skb" not in str(sample.evsel): + return + + location = getattr(sample, "location", None) + if location is not None: + self.drop_log[location] += 1 + if location not in self.resolved_syms: + sym = getattr(sample, "symbol", None) + if getattr(sample, "sample_ip", 0) == location and sym and sym != "[unknown]": + self.resolved_syms[location] = ( + sym, + getattr(sample, "sym_offset", 0) or 0, + ) + self.callchain_syms.pop(location, None) + else: + for entry in getattr(sample, "callchain", []) or []: + if isinstance(entry, dict): + entry_ip = entry.get("ip") + sym_info = entry.get("sym") + sym_name = sym_info.get("name") if isinstance(sym_info, dict) else None + sym_start = sym_info.get("start") if isinstance(sym_info, dict) else None + sym_off = (max(0, location - sym_start) + if sym_start is not None else None) + else: + entry_ip = getattr(entry, "ip", None) + entry_sym = getattr(entry, "sym", None) + sym_name = ( + getattr(entry_sym, "name", None) + or getattr(entry, "symbol", None) + ) + sym_off = getattr(entry, "sym_offset", None) + if sym_off is None: + sym_start = getattr(entry_sym, "start", None) + if sym_start is not None: + sym_off = max(0, location - sym_start) + if entry_ip == location and sym_name and sym_name != "[unknown]": + if sym_off is not None: + self.resolved_syms[location] = (sym_name, sym_off) + self.callchain_syms.pop(location, None) + else: + self.callchain_syms[location] = sym_name + break + + +if __name__ == "__main__": + ap = argparse.ArgumentParser( + description="Monitor the system for dropped packets and produce a " + "report of drop locations and counts.") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + ap.add_argument("-k", "--kallsyms", default=None, + help="Path to kallsyms file for offline symbol resolution") + args = ap.parse_args() + + monitor = DropMonitor(kallsyms_path=args.kallsyms) + session = None + + try: + session = perf.session(perf.data(args.input), sample=monitor.process_event, + kallsyms=args.kallsyms) + session.process_events() + except KeyboardInterrupt: + print("\nStopping trace...") + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e: + print(f"Error processing events: {e}") + sys.exit(1) + finally: + session = None + + monitor.print_drop_table() diff --git a/tools/perf/tests/shell/test_net_dropmonitor_python.sh b/tools/perf/tests/shell/test_net_dropmonitor_python.sh new file mode 100755 index 000000000000..d00be255d259 --- /dev/null +++ b/tools/perf/tests/shell/test_net_dropmonitor_python.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# net_dropmonitor python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/net_dropmonitor.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, net_dropmonitor.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT +trap 'cleanup; exit 1' TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing net_dropmonitor.py..." + +# Create a perf.data file. Force dropping a packet if tracepoint is available! +if ! perf record -e skb:kfree_skb -o "${temp_data}" -a \ + -- ping -c 1 255.255.255.255 >/dev/null 2>&1; then + if ! perf record -e skb:kfree_skb -o "${temp_data}" \ + -- sleep 0.1 >/dev/null 2>&1; then + if ! perf record -o "${temp_data}" -- uname >/dev/null 2>&1; then + echo "Skipping test, cannot record perf events" + exit 2 + fi + fi +fi + +if [ ! -s "${temp_data}" ]; then + echo "Skipping test, perf record failed to create data" + exit 2 +fi + +# Check that the script executes and outputs table header +if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "net_dropmonitor.py test failed" + err=1 +else + if ! grep -q "LOCATION.*OFFSET.*COUNT" "${temp_out}"; then + echo "Failed to find the metrics table header" + err=1 + fi +fi + +# Verify DropMonitor event processing and symbol resolution +if [ $err -eq 0 ]; then + if ! "$PYTHON" -c " +import sys +sys.path.insert(0, sys.argv[1]) +import net_dropmonitor + +class DummySample: + evsel = 'skb:kfree_skb' + location = 0xffffffff81001010 + sample_ip = 0xffffffff81001010 + symbol = 'ip_rcv_finish' + sym_offset = 16 + callchain = [] + +dm = net_dropmonitor.DropMonitor() +dm.process_event(DummySample()) +dm.print_drop_table() +" "${script_dir}" > "${temp_out}"; then + echo "net_dropmonitor.py unit test failed" + err=1 + elif ! grep -q "ip_rcv_finish.*16.*1" "${temp_out}"; then + echo "Failed to find expected symbol resolution in net_dropmonitor.py" + err=1 + else + echo "net_dropmonitor test passed." + fi +fi +rm -f "${temp_out}" + +exit $err From b08e799dee7356902db12d3d516125531ad5485d Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:03 -0700 Subject: [PATCH 211/425] perf python: Port netdev-times to perf module Port netdev-times.py from tools/perf/scripts/python/ to a standalone script in tools/perf/python/: - Refactor the script into a NetDevTimesAnalyzer class with full type annotations to encapsulate state. - Collect events via perf.session and sort them in timestamp order before analysis so multi-CPU TX and RX packet timelines are reconstructed deterministically. - Replace custom argument parsing with argparse (-i/--input, --tx, --rx, --dev, --debug), extract tracepoint fields directly from sample attributes, and remove Python 2 compatibility artifacts. Add a shell test (test_netdev_times_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/netdev-times.py | 494 ++++++++++++++++++ .../tests/shell/test_netdev_times_python.sh | 104 ++++ 2 files changed, 598 insertions(+) create mode 100755 tools/perf/python/netdev-times.py create mode 100755 tools/perf/tests/shell/test_netdev_times_python.sh diff --git a/tools/perf/python/netdev-times.py b/tools/perf/python/netdev-times.py new file mode 100755 index 000000000000..f0f829987447 --- /dev/null +++ b/tools/perf/python/netdev-times.py @@ -0,0 +1,494 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Display a process of packets and processed time. +It helps us to investigate networking or network device. + +Ported from tools/perf/scripts/python/netdev-times.py +""" + +from __future__ import annotations +import argparse +from collections import defaultdict +import sys +from typing import Optional +import perf + +# Format for displaying rx packet processing +PF_IRQ_ENTRY = " irq_entry(+%.3fmsec irq=%d:%s)" +PF_SOFT_ENTRY = " softirq_entry(+%.3fmsec)" +PF_NAPI_POLL = " napi_poll_exit(+%.3fmsec %s)" +PF_JOINT = " |" +PF_WJOINT = " | |" +PF_NET_RECV = " |---netif_receive_skb(+%.3fmsec skb=%x len=%d)" +PF_NET_RX = " |---netif_rx(+%.3fmsec skb=%x)" +PF_CPY_DGRAM = " | skb_copy_datagram_iovec(+%.3fmsec %d:%s)" +PF_KFREE_SKB = " | kfree_skb(+%.3fmsec location=%x)" +PF_CONS_SKB = " | consume_skb(+%.3fmsec)" + + +class NetDevTimesAnalyzer: + """Analyzes network device events and prints charts.""" + + def __init__(self, cfg: argparse.Namespace): + self.args = cfg + self.session: Optional[perf.session] = None + self.show_tx = cfg.tx or (not cfg.tx and not cfg.rx) + self.show_rx = cfg.rx or (not cfg.tx and not cfg.rx) + self.dev = cfg.dev + self.debug = cfg.debug + self.buffer_budget = 65536 + self.irq_dic: dict[int, list[dict]] = defaultdict(list) + self.net_rx_dic: dict[int, dict] = {} + self.receive_hunk_list: list[dict] = [] + self.rx_skb_list: list[dict] = [] + self.tx_queue_list: list[dict] = [] + self.tx_xmit_list: list[dict] = [] + self.tx_free_list: list[dict] = [] + + self.buffer_budget = 65536 + self.of_count_rx_skb_list = 0 + self.of_count_tx_queue_list = 0 + self.of_count_tx_xmit_list = 0 + + def diff_msec(self, src: int, dst: int) -> float: + """Calculate a time interval(msec) from src(nsec) to dst(nsec).""" + return (dst - src) / 1000000.0 + + def print_transmit(self, hunk: dict) -> None: + """Display a process of transmitting a packet.""" + if self.dev and hunk['dev'].find(self.dev) < 0: + return + queue_t_sec = hunk['queue_t'] // 1000000000 + queue_t_usec = hunk['queue_t'] % 1000000000 // 1000 + print(f"{hunk['dev']:7s} {hunk['len']:5d} " + f"{queue_t_sec:6d}.{queue_t_usec:06d}sec " + f"{self.diff_msec(hunk['queue_t'], hunk['xmit_t']):12.3f}msec " + f"{self.diff_msec(hunk['xmit_t'], hunk['free_t']):12.3f}msec") + + def print_receive(self, hunk: dict) -> None: + """Display a process of received packets and interrupts.""" + show_hunk = False + irq_list = hunk['irq_list'] + if not irq_list: + return + cpu = irq_list[0]['cpu'] + base_t = irq_list[0]['irq_ent_t'] + + if self.dev: + for irq in irq_list: + if irq['name'].find(self.dev) >= 0: + show_hunk = True + break + else: + show_hunk = True + + if not show_hunk: + return + + base_t_sec = base_t // 1000000000 + base_t_usec = base_t % 1000000000 // 1000 + print(f"{base_t_sec}.{base_t_usec:06d}sec cpu={cpu}") + for irq in irq_list: + print(PF_IRQ_ENTRY % + (self.diff_msec(base_t, irq['irq_ent_t']), + irq['irq'], irq['name'])) + print(PF_JOINT) + irq_event_list = irq['event_list'] + for irq_event in irq_event_list: + if irq_event['event'] == 'netif_rx': + print(PF_NET_RX % + (self.diff_msec(base_t, irq_event['time']), + irq_event['skbaddr'])) + print(PF_JOINT) + + print(PF_SOFT_ENTRY % self.diff_msec(base_t, hunk['sirq_ent_t'])) + print(PF_JOINT) + event_list = hunk['event_list'] + for i, event in enumerate(event_list): + if event['event_name'] == 'napi_poll': + print(PF_NAPI_POLL % + (self.diff_msec(base_t, event['event_t']), + event['dev'])) + if i == len(event_list) - 1: + print("") + else: + print(PF_JOINT) + else: + print(PF_NET_RECV % + (self.diff_msec(base_t, event['event_t']), + event['skbaddr'], + event['len'])) + if 'handle' in event: + print(PF_WJOINT) + if event['handle'] == "kfree_skb": + print(PF_KFREE_SKB % + (self.diff_msec(base_t, event['comm_t']), + event['location'])) + elif event['handle'] == "consume_skb": + print(PF_CONS_SKB % + self.diff_msec(base_t, event['comm_t'])) + elif event['handle'] == "skb_copy_datagram_iovec": + print(PF_CPY_DGRAM % + (self.diff_msec(base_t, event['comm_t']), + event['pid'], event['comm'])) + print(PF_JOINT) + + def handle_irq_handler_entry(self, event: dict) -> None: + """Handle irq:irq_handler_entry event.""" + time = event['time'] + cpu = event['cpu'] + irq = event['irq'] + irq_name = event['irq_name'] + irq_record = {'irq': irq, 'name': irq_name, 'cpu': cpu, + 'irq_ent_t': time, 'event_list': []} + self.irq_dic[cpu].append(irq_record) + + def handle_irq_handler_exit(self, event: dict) -> None: + """Handle irq:irq_handler_exit event.""" + time = event['time'] + cpu = event['cpu'] + irq = event['irq'] + if cpu not in self.irq_dic or not self.irq_dic[cpu]: + return + if irq != self.irq_dic[cpu][-1]['irq']: + return + irq_record = self.irq_dic[cpu].pop() + irq_record['irq_ext_t'] = time + # if an irq doesn't include NET_RX softirq, drop. + if irq_record['event_list']: + self.irq_dic[cpu].append(irq_record) + + def handle_irq_softirq_raise(self, event: dict) -> None: + """Handle irq:softirq_raise event.""" + time = event['time'] + cpu = event['cpu'] + if cpu not in self.irq_dic or not self.irq_dic[cpu]: + return + irq_record = self.irq_dic[cpu].pop() + irq_record['event_list'].append({'time': time, 'event': 'sirq_raise'}) + self.irq_dic[cpu].append(irq_record) + + def handle_irq_softirq_entry(self, event: dict) -> None: + """Handle irq:softirq_entry event.""" + time = event['time'] + cpu = event['cpu'] + self.net_rx_dic[cpu] = {'sirq_ent_t': time, 'event_list': []} + + def handle_irq_softirq_exit(self, event: dict) -> None: + """Handle irq:softirq_exit event.""" + time = event['time'] + cpu = event['cpu'] + irq_list = None + event_list = None + sirq_ent_t = None + + if cpu in self.irq_dic: + irq_list = self.irq_dic[cpu] + del self.irq_dic[cpu] + if cpu in self.net_rx_dic: + sirq_ent_t = self.net_rx_dic[cpu]['sirq_ent_t'] + event_list = self.net_rx_dic[cpu]['event_list'] + del self.net_rx_dic[cpu] + if not irq_list or event_list is None or sirq_ent_t is None: + return + rec_data = {'sirq_ent_t': sirq_ent_t, 'sirq_ext_t': time, + 'irq_list': irq_list, 'event_list': event_list} + self.receive_hunk_list.append(rec_data) + + def handle_napi_poll(self, event: dict) -> None: + """Handle napi:napi_poll event.""" + time = event['time'] + cpu = event['cpu'] + dev_name = event['dev_name'] + work = event['work'] + budget = event['budget'] + if cpu in self.net_rx_dic: + event_list = self.net_rx_dic[cpu]['event_list'] + rec_data = {'event_name': 'napi_poll', + 'dev': dev_name, 'event_t': time, + 'work': work, 'budget': budget} + event_list.append(rec_data) + + def handle_netif_rx(self, event: dict) -> None: + """Handle net:netif_rx event.""" + time = event['time'] + cpu = event['cpu'] + skbaddr = event['skbaddr'] + skblen = event['skblen'] + dev_name = event['dev_name'] + if cpu not in self.irq_dic or not self.irq_dic[cpu]: + return + irq_record = self.irq_dic[cpu].pop() + irq_record['event_list'].append({'time': time, 'event': 'netif_rx', + 'skbaddr': skbaddr, 'skblen': skblen, + 'dev_name': dev_name}) + self.irq_dic[cpu].append(irq_record) + + def handle_netif_receive_skb(self, event: dict) -> None: + """Handle net:netif_receive_skb event.""" + time = event['time'] + cpu = event['cpu'] + skbaddr = event['skbaddr'] + skblen = event['skblen'] + if cpu in self.net_rx_dic: + rec_data = {'event_name': 'netif_receive_skb', + 'event_t': time, 'skbaddr': skbaddr, 'len': skblen} + event_list = self.net_rx_dic[cpu]['event_list'] + event_list.append(rec_data) + self.rx_skb_list.insert(0, rec_data) + if len(self.rx_skb_list) > self.buffer_budget: + self.rx_skb_list.pop() + self.of_count_rx_skb_list += 1 + + def handle_net_dev_queue(self, event: dict) -> None: + """Handle net:net_dev_queue event.""" + time = event['time'] + skbaddr = event['skbaddr'] + skblen = event['skblen'] + dev_name = event['dev_name'] + skb = {'dev': dev_name, 'skbaddr': skbaddr, 'len': skblen, 'queue_t': time} + self.tx_queue_list.insert(0, skb) + if len(self.tx_queue_list) > self.buffer_budget: + self.tx_queue_list.pop() + self.of_count_tx_queue_list += 1 + + def handle_net_dev_xmit(self, event: dict) -> None: + """Handle net:net_dev_xmit event.""" + time = event['time'] + skbaddr = event['skbaddr'] + rc = event['rc'] + if rc == 0: # NETDEV_TX_OK + for i, skb in enumerate(self.tx_queue_list): + if skb['skbaddr'] == skbaddr: + skb['xmit_t'] = time + self.tx_xmit_list.insert(0, skb) + del self.tx_queue_list[i] + if len(self.tx_xmit_list) > self.buffer_budget: + self.tx_xmit_list.pop() + self.of_count_tx_xmit_list += 1 + return + + def handle_kfree_skb(self, event: dict) -> None: + """Handle skb:kfree_skb event.""" + time = event['time'] + skbaddr = event['skbaddr'] + comm = event['comm'] + pid = event['pid'] + location = event['location'] + for i, skb in enumerate(self.tx_queue_list): + if skb['skbaddr'] == skbaddr: + del self.tx_queue_list[i] + return + for i, skb in enumerate(self.tx_xmit_list): + if skb['skbaddr'] == skbaddr: + skb['free_t'] = time + self.tx_free_list.append(skb) + del self.tx_xmit_list[i] + return + for i, rec_data in enumerate(self.rx_skb_list): + if rec_data['skbaddr'] == skbaddr: + rec_data.update({'handle': "kfree_skb", + 'comm': comm, 'pid': pid, 'comm_t': time, 'location': location}) + del self.rx_skb_list[i] + return + + def handle_consume_skb(self, event: dict) -> None: + """Handle skb:consume_skb event.""" + time = event['time'] + skbaddr = event['skbaddr'] + for i, skb in enumerate(self.tx_xmit_list): + if skb['skbaddr'] == skbaddr: + skb['free_t'] = time + self.tx_free_list.append(skb) + del self.tx_xmit_list[i] + return + for i, rec_data in enumerate(self.rx_skb_list): + if rec_data['skbaddr'] == skbaddr: + rec_data.update({'handle': "consume_skb", 'comm_t': time}) + del self.rx_skb_list[i] + return + + def handle_skb_copy_datagram_iovec(self, event: dict) -> None: + """Handle skb:skb_copy_datagram_iovec event.""" + time = event['time'] + skbaddr = event['skbaddr'] + comm = event['comm'] + pid = event['pid'] + for i, rec_data in enumerate(self.rx_skb_list): + if skbaddr == rec_data['skbaddr']: + rec_data.update({'handle': "skb_copy_datagram_iovec", + 'comm': comm, 'pid': pid, 'comm_t': time}) + del self.rx_skb_list[i] + return + + def print_summary(self) -> None: + """Print charts.""" + + # display receive hunks + if self.show_rx: + for hunk in self.receive_hunk_list: + self.print_receive(hunk) + + # display transmit hunks + if self.show_tx: + print(" dev len Qdisc " + " netdevice free") + for hunk in self.tx_free_list: + self.print_transmit(hunk) + + if self.debug: + print("debug buffer status") + print("----------------------------") + print(f"xmit Qdisc:remain:{len(self.tx_queue_list)} " + f"overflow:{self.of_count_tx_queue_list}") + print(f"xmit netdevice:remain:{len(self.tx_xmit_list)} " + f"overflow:{self.of_count_tx_xmit_list}") + print(f"receive:remain:{len(self.rx_skb_list)} " + f"overflow:{self.of_count_rx_skb_list}") + + def handle_single_event(self, event: dict) -> None: + """Handle a single processed event.""" + name = event['name'] + if name == 'irq:softirq_exit': + self.handle_irq_softirq_exit(event) + elif name == 'irq:softirq_entry': + self.handle_irq_softirq_entry(event) + elif name == 'irq:softirq_raise': + self.handle_irq_softirq_raise(event) + elif name == 'irq:irq_handler_entry': + self.handle_irq_handler_entry(event) + elif name == 'irq:irq_handler_exit': + self.handle_irq_handler_exit(event) + elif name == 'napi:napi_poll': + self.handle_napi_poll(event) + elif name == 'net:netif_receive_skb': + self.handle_netif_receive_skb(event) + elif name == 'net:netif_rx': + self.handle_netif_rx(event) + elif name == 'skb:skb_copy_datagram_iovec': + self.handle_skb_copy_datagram_iovec(event) + elif name == 'net:net_dev_queue': + self.handle_net_dev_queue(event) + elif name == 'net:net_dev_xmit': + self.handle_net_dev_xmit(event) + elif name == 'skb:kfree_skb': + self.handle_kfree_skb(event) + elif name == 'skb:consume_skb': + self.handle_consume_skb(event) + + def process_event(self, sample: perf.sample_event) -> None: + """Process events directly on-the-fly.""" + name = str(sample.evsel) + ev_name = name[6:-1] if name.startswith("evsel(") else name + pid = sample.sample_pid + if hasattr(self, 'session') and self.session: + try: + thread = self.session.find_thread(sample.sample_pid, sample.sample_tid) + comm = (thread.comm() if thread else None) or "[unknown]" + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError): + comm = "[unknown]" + else: + comm = "Unknown" + event_data = { + 'name': ev_name, + 'time': sample.sample_time, + 'cpu': sample.sample_cpu, + 'pid': pid, + 'comm': comm, + } + + # Extract specific fields based on event type + if ev_name.startswith("irq:softirq_"): + event_data['vec'] = getattr(sample, "vec", 0) + # Filter for NET_RX + if event_data['vec'] != 3: # NET_RX_SOFTIRQ is usually 3 + return + elif ev_name == "irq:irq_handler_entry": + event_data['irq'] = getattr(sample, "irq", -1) + event_data['irq_name'] = getattr(sample, "name", "[unknown]") + elif ev_name == "irq:irq_handler_exit": + event_data['irq'] = getattr(sample, "irq", -1) + event_data['ret'] = getattr(sample, "ret", 0) + elif ev_name == "napi:napi_poll": + event_data['napi'] = getattr(sample, "napi", 0) + event_data['dev_name'] = getattr(sample, "dev_name", "[unknown]") + event_data['work'] = getattr(sample, "work", 0) + event_data['budget'] = getattr(sample, "budget", 0) + elif ev_name in ("net:netif_receive_skb", "net:netif_rx", + "net:net_dev_queue"): + event_data['skbaddr'] = getattr(sample, "skbaddr", 0) + event_data['skblen'] = getattr(sample, "len", 0) + event_data['dev_name'] = getattr(sample, "name", "[unknown]") + elif ev_name == "net:net_dev_xmit": + event_data['skbaddr'] = getattr(sample, "skbaddr", 0) + event_data['skblen'] = getattr(sample, "len", 0) + event_data['rc'] = getattr(sample, "rc", 0) + event_data['dev_name'] = getattr(sample, "name", "[unknown]") + elif ev_name == "skb:kfree_skb": + event_data['skbaddr'] = getattr(sample, "skbaddr", 0) + event_data['location'] = getattr(sample, "location", 0) + event_data['protocol'] = getattr(sample, "protocol", 0) + event_data['reason'] = getattr(sample, "reason", 0) + elif ev_name == "skb:consume_skb": + event_data['skbaddr'] = getattr(sample, "skbaddr", 0) + event_data['location'] = getattr(sample, "location", 0) + elif ev_name == "skb:skb_copy_datagram_iovec": + event_data['skbaddr'] = getattr(sample, "skbaddr", 0) + event_data['skblen'] = getattr(sample, "len", 0) + + self.handle_single_event(event_data) + + +NetdevTimes = NetDevTimesAnalyzer + + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Display a process of packets and processed time.") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + ap.add_argument("--tx", action="store_true", help="show only tx chart") + ap.add_argument("--rx", action="store_true", help="show only rx chart") + ap.add_argument("--dev", default=None, help="show only specified device") + ap.add_argument("--debug", action="store_true", + help="work with debug mode. It shows buffer status.") + ap.add_argument("positionals", nargs="*", + help="optional positional arguments (tx, rx, dev=, debug)") + args, unknown_args = ap.parse_known_args() + + parsed_args = argparse.Namespace( + tx=args.tx, rx=args.rx, dev=args.dev, debug=args.debug, input=args.input + ) + + for arg in list(args.positionals) + unknown_args: + if not arg or not isinstance(arg, str): + continue + if arg in ('tx', '--tx'): + parsed_args.tx = True + elif arg in ('rx', '--rx'): + parsed_args.rx = True + elif arg.startswith('dev='): + parsed_args.dev = arg[4:] + elif arg.startswith('--dev='): + parsed_args.dev = arg[6:] + elif arg in ('debug', '--debug'): + parsed_args.debug = True + + analyzer = NetDevTimesAnalyzer(parsed_args) + + try: + try: + analyzer.session = perf.session( + perf.data(parsed_args.input), sample=analyzer.process_event + ) + try: + analyzer.session.process_events() + except KeyboardInterrupt: + pass + finally: + # Break the reference cycle between analyzer.session and the bound + # analyzer.process_event callback so the C perf.session object is freed. + analyzer.session = None + analyzer.print_summary() + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e: + print(f"Error processing events: {e}") + sys.exit(1) diff --git a/tools/perf/tests/shell/test_netdev_times_python.sh b/tools/perf/tests/shell/test_netdev_times_python.sh new file mode 100755 index 000000000000..bb0f8c30207f --- /dev/null +++ b/tools/perf/tests/shell/test_netdev_times_python.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# netdev_times python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/netdev-times.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, netdev-times.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing netdev-times.py..." + +if ! "$PYTHON" - "$script_path" << 'EOF' +import argparse +import contextlib +import importlib.util +import io +import sys + +spec = importlib.util.spec_from_file_location("netdev_times", sys.argv[1]) +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) + +cfg = argparse.Namespace(tx=True, rx=False, dev=None, debug=False, input="perf.data") +analyzer = mod.NetdevTimes(cfg) +analyzer.handle_net_dev_queue({ + "time": 1_000_000_000, + "skbaddr": 0xdeadbeef, + "skblen": 1500, + "dev_name": "eth0", +}) +analyzer.handle_net_dev_xmit({ + "time": 1_000_500_000, + "skbaddr": 0xdeadbeef, + "rc": 0, +}) +analyzer.handle_kfree_skb({ + "time": 1_000_750_000, + "skbaddr": 0xdeadbeef, + "comm": "ping", + "pid": 42, + "location": 0xffffffff81000000, +}) + +buf = io.StringIO() +with contextlib.redirect_stdout(buf): + analyzer.print_summary() +out = buf.getvalue() +assert "eth0" in out and "1500" in out and "1.000000sec" in out, out +assert "0.500msec" in out and "0.250msec" in out, out +EOF +then + echo "netdev-times.py synthetic unit test failed" + exit 1 +fi + +# Create a perf.data file. Force dropping a packet if tracepoint is available! +if ! perf record -e skb:kfree_skb -a -o "${temp_data}" \ + -- ping -c 1 127.0.0.1 >/dev/null 2>&1; then + perf record -e cycles -o "${temp_data}" \ + -- perf test -w noploop >/dev/null 2>&1 || \ + { echo "Skipping test, perf record failed"; exit 2; } +fi + +# Check that the script executes +if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "netdev-times.py test failed" + err=1 +else + echo "netdev-times test passed." +fi +rm -f "${temp_out}" + +exit $err From 8c302c91402a3f543e661ace28bacf49f803b88b Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:04 -0700 Subject: [PATCH 212/425] perf python: Port check-perf-trace to perf module Port check-perf-trace.py to a standalone script in tools/perf/python/ using the perf module directly. Improvements compared to the legacy script: - Access tracepoint fields directly as attributes on perf.sample_event instead of per-event dictionaries and legacy Perf-Trace-Util helpers. - Decode symbolic flag and enum masks for irq:softirq_entry and kmem:kmalloc directly in Python and add -i/--input CLI support via argparse. - Add full type annotations and clean up Python 2 idioms. Add a shell test (test_check_perf_trace_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/check-perf-trace.py | 223 ++++++++++++++++++ .../shell/test_check_perf_trace_python.sh | 85 +++++++ 2 files changed, 308 insertions(+) create mode 100755 tools/perf/python/check-perf-trace.py create mode 100755 tools/perf/tests/shell/test_check_perf_trace_python.sh diff --git a/tools/perf/python/check-perf-trace.py b/tools/perf/python/check-perf-trace.py new file mode 100755 index 000000000000..a7679a897cc7 --- /dev/null +++ b/tools/perf/python/check-perf-trace.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Basic test of Python scripting support for perf. +Ported from tools/perf/scripts/python/check-perf-trace.py +""" +from __future__ import annotations + +import argparse +import collections +import perf + +unhandled: collections.defaultdict[str, int] = collections.defaultdict(int) +session = None + +softirq_vecs = { + 0: "HI_SOFTIRQ", + 1: "TIMER_SOFTIRQ", + 2: "NET_TX_SOFTIRQ", + 3: "NET_RX_SOFTIRQ", + 4: "BLOCK_SOFTIRQ", + 5: "IRQ_POLL_SOFTIRQ", + 6: "TASKLET_SOFTIRQ", + 7: "SCHED_SOFTIRQ", + 8: "HRTIMER_SOFTIRQ", + 9: "RCU_SOFTIRQ", +} + +_GFP_DMA = 1 << 0 +_GFP_HIGHMEM = 1 << 1 +_GFP_DMA32 = 1 << 2 +_GFP_MOVABLE = 1 << 3 +_GFP_RECLAIMABLE = 1 << 4 +_GFP_HIGH = 1 << 5 +_GFP_IO = 1 << 6 +_GFP_FS = 1 << 7 +_GFP_ZERO = 1 << 8 +_GFP_DIRECT_RECLAIM = 1 << 10 +_GFP_KSWAPD_RECLAIM = 1 << 11 +_GFP_WRITE = 1 << 12 +_GFP_NOWARN = 1 << 13 +_GFP_RETRY_MAYFAIL = 1 << 14 +_GFP_NOFAIL = 1 << 15 +_GFP_NORETRY = 1 << 16 +_GFP_MEMALLOC = 1 << 17 +_GFP_COMP = 1 << 18 +_GFP_NOMEMALLOC = 1 << 19 +_GFP_HARDWALL = 1 << 20 +_GFP_THISNODE = 1 << 21 +_GFP_ACCOUNT = 1 << 22 +_GFP_ZEROTAGS = 1 << 23 + +_GFP_RECLAIM = _GFP_DIRECT_RECLAIM | _GFP_KSWAPD_RECLAIM +_GFP_KERNEL = _GFP_RECLAIM | _GFP_IO | _GFP_FS +_GFP_USER = _GFP_KERNEL | _GFP_HARDWALL +_GFP_HIGHUSER = _GFP_USER | _GFP_HIGHMEM +_GFP_HIGHUSER_MOVABLE = _GFP_HIGHUSER | _GFP_MOVABLE +_GFP_TRANSHUGE_LIGHT = ( + _GFP_HIGHUSER_MOVABLE | _GFP_COMP | _GFP_NOMEMALLOC | _GFP_NOWARN +) & ~_GFP_RECLAIM +_GFP_TRANSHUGE = _GFP_TRANSHUGE_LIGHT | _GFP_DIRECT_RECLAIM + +GFP_FLAG_NAMES = [ + (_GFP_TRANSHUGE, "GFP_TRANSHUGE"), + (_GFP_TRANSHUGE_LIGHT, "GFP_TRANSHUGE_LIGHT"), + (_GFP_HIGHUSER_MOVABLE, "GFP_HIGHUSER_MOVABLE"), + (_GFP_HIGHUSER, "GFP_HIGHUSER"), + (_GFP_USER, "GFP_USER"), + (_GFP_KERNEL | _GFP_ACCOUNT, "GFP_KERNEL_ACCOUNT"), + (_GFP_KERNEL, "GFP_KERNEL"), + (_GFP_RECLAIM | _GFP_IO, "GFP_NOFS"), + (_GFP_HIGH | _GFP_KSWAPD_RECLAIM, "GFP_ATOMIC"), + (_GFP_RECLAIM, "GFP_NOIO"), + (_GFP_KSWAPD_RECLAIM | _GFP_NOWARN, "GFP_NOWAIT"), + (_GFP_DMA, "GFP_DMA"), + (_GFP_DMA32, "GFP_DMA32"), + (_GFP_RECLAIM, "__GFP_RECLAIM"), + (_GFP_DMA, "__GFP_DMA"), + (_GFP_HIGHMEM, "__GFP_HIGHMEM"), + (_GFP_DMA32, "__GFP_DMA32"), + (_GFP_MOVABLE, "__GFP_MOVABLE"), + (_GFP_RECLAIMABLE, "__GFP_RECLAIMABLE"), + (_GFP_HIGH, "__GFP_HIGH"), + (_GFP_IO, "__GFP_IO"), + (_GFP_FS, "__GFP_FS"), + (_GFP_ZERO, "__GFP_ZERO"), + (_GFP_DIRECT_RECLAIM, "__GFP_DIRECT_RECLAIM"), + (_GFP_KSWAPD_RECLAIM, "__GFP_KSWAPD_RECLAIM"), + (_GFP_WRITE, "__GFP_WRITE"), + (_GFP_NOWARN, "__GFP_NOWARN"), + (_GFP_RETRY_MAYFAIL, "__GFP_RETRY_MAYFAIL"), + (_GFP_NOFAIL, "__GFP_NOFAIL"), + (_GFP_NORETRY, "__GFP_NORETRY"), + (_GFP_MEMALLOC, "__GFP_MEMALLOC"), + (_GFP_COMP, "__GFP_COMP"), + (_GFP_NOMEMALLOC, "__GFP_NOMEMALLOC"), + (_GFP_HARDWALL, "__GFP_HARDWALL"), + (_GFP_THISNODE, "__GFP_THISNODE"), + (_GFP_ACCOUNT, "__GFP_ACCOUNT"), + (_GFP_ZEROTAGS, "__GFP_ZEROTAGS"), +] + + +def trace_begin() -> None: + """Called at the start of trace processing.""" + print("in trace_begin") + +def trace_end() -> None: + """Called at the end of trace processing.""" + print_unhandled() + print("in trace_end") + +def symbol_str(event_name: str, field_name: str, value: int) -> str: + """Resolves symbol values to strings.""" + # Note: The standalone Python API currently lacks dynamic libtraceevent + # formatting (equivalent to _perf_trace_context.symbol_str()) + if event_name == "irq__softirq_entry" and field_name == "vec": + return softirq_vecs.get(value, str(value)) + return str(value) + +def flag_str(event_name: str, field_name: str, value: int) -> str: + """Resolves flag values to strings.""" + # Note: The standalone Python API currently lacks dynamic libtraceevent + # formatting (equivalent to _perf_trace_context.flag_str()) + if event_name == "kmem__kmalloc" and field_name == "gfp_flags": + if value == 0: + return "none" + names = [] + rem = value + for mask, name in GFP_FLAG_NAMES: + if (rem & mask) == mask: + names.append(name) + rem &= ~mask + if rem: + names.append(f"0x{rem:x}") + return "|".join(names) + return str(value) + +def print_header(event_name: str, sample: perf.sample_event) -> None: + """Prints common header for events.""" + secs = sample.sample_time // 1000000000 + nsecs = sample.sample_time % 1000000000 + comm = "[unknown]" + try: + if session: + thread = session.find_thread(sample.sample_pid, sample.sample_tid) + if thread: + comm = thread.comm() or "[unknown]" + except (TypeError, AttributeError): + pass + print(f"{event_name:<20} {sample.sample_cpu:5} {secs:05}.{nsecs:09} " + f"{sample.sample_tid:8} {comm:<20} ", end=' ') + +def print_uncommon(sample: perf.sample_event) -> None: + """Prints uncommon fields for tracepoints.""" + # Fallback to 0 if field not found (e.g. on older kernels or if not tracepoint) + pc = getattr(sample, 'common_preempt_count', 0) + flags = getattr(sample, 'common_flags', 0) + lock_depth = getattr(sample, 'common_lock_depth', 0) + + print(f"common_preempt_count={pc}, common_flags={flags}, " + f"common_lock_depth={lock_depth}, ", end='') + +def irq__softirq_entry(sample: perf.sample_event) -> None: + """Handles irq:softirq_entry events.""" + print_header("irq__softirq_entry", sample) + print_uncommon(sample) + print(f"vec={symbol_str('irq__softirq_entry', 'vec', getattr(sample, 'vec', 0))}") + +def kmem__kmalloc(sample: perf.sample_event) -> None: + """Handles kmem:kmalloc events.""" + print_header("kmem__kmalloc", sample) + print_uncommon(sample) + + print(f"call_site={getattr(sample, 'call_site', 0):#x}, " + f"ptr={getattr(sample, 'ptr', 0):#x}, " + f"bytes_req={getattr(sample, 'bytes_req', 0):d}, " + f"bytes_alloc={getattr(sample, 'bytes_alloc', 0):d}, " + f"gfp_flags={flag_str('kmem__kmalloc', 'gfp_flags', getattr(sample, 'gfp_flags', 0))}") + +def trace_unhandled(event_name: str) -> None: + """Tracks unhandled events.""" + unhandled[event_name] += 1 + +def print_unhandled() -> None: + """Prints summary of unhandled events.""" + if not unhandled: + return + print("\nunhandled events:\n") + print(f"{'event':<40} {'count':>10}") + print("---------------------------------------- -----------") + for event_name, count in unhandled.items(): + print(f"{event_name:<40} {count:10}") + +def process_event(sample: perf.sample_event) -> None: + """Callback for processing events.""" + event_name = str(sample.evsel) + if event_name.startswith("evsel(") and event_name.endswith(")"): + event_name = event_name[6:-1] + if event_name.startswith("irq:softirq_entry"): + irq__softirq_entry(sample) + elif event_name.startswith("kmem:kmalloc"): + kmem__kmalloc(sample) + else: + trace_unhandled(event_name) + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + args = ap.parse_args() + + trace_begin() + try: + session = perf.session(perf.data(args.input), sample=process_event) + try: + session.process_events() + except KeyboardInterrupt: + pass + trace_end() + finally: + # Break the reference cycle between the global session and the + # process_event callback so the C perf.session object is freed. + session = None diff --git a/tools/perf/tests/shell/test_check_perf_trace_python.sh b/tools/perf/tests/shell/test_check_perf_trace_python.sh new file mode 100755 index 000000000000..a346e45eb18c --- /dev/null +++ b/tools/perf/tests/shell/test_check_perf_trace_python.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# check-perf-trace python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +# If we don't have the perf python module, we can't test +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +if ! perf check feature -q libtraceevent; then + echo "Skipping test, perf built without libtraceevent" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/check-perf-trace.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, check-perf-trace.py not found at $script_path" + exit 2 +fi + +err=0 +temp_data="" +temp_out="" + +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} + +trap 'cleanup' EXIT +trap 'cleanup; exit 1' TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +test_file_mode() { + echo "Testing check-perf-trace.py..." + + events="" + if perf list | grep -q "irq:softirq_entry"; then + events="irq:softirq_entry" + fi + if perf list | grep -q "kmem:kmalloc"; then + if [ -n "$events" ]; then + events="$events,kmem:kmalloc,kmem:kfree" + else + events="kmem:kmalloc,kmem:kfree" + fi + fi + + if [ -z "$events" ]; then + echo "Skipping test, no required tracepoints found" + exit 2 + fi + + # Generate events + if ! perf record -e "$events" -a -o "${temp_data}" -- sleep 0.5 >/dev/null 2>&1; then + echo "Skipping test, perf record failed" + exit 2 + fi + + # Run the script + if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "File mode test failed." + err=1 + elif ! grep -q "in trace_begin" "${temp_out}" || \ + ! grep -q "in trace_end" "${temp_out}"; then + echo "File mode test missing trace_begin/trace_end markers." + err=1 + else + echo "File mode test passed." + fi +} + +test_file_mode + +exit $err From 279b402e5143214c077f1a95132134c27d907e4c Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:05 -0700 Subject: [PATCH 213/425] perf python: Port arm-cs-trace-disasm to perf module Port arm-cs-trace-disasm.py to a standalone script in tools/perf/python/ using the perf module directly. Improvements compared to the legacy script: - Encapsulate trace disassembly state in a TraceDisasm class. - Default to itrace="b" when creating perf.session() (overridable via --itrace or PERF_ITRACE) so all CoreSight branch samples are synthesized without requiring --itrace=b on the command line. - Automatically search standard kernel debug paths (find_vmlinux()) when -k/--vmlinux is not specified, and query perf.config_get("annotate.objdump") for the default objdump binary. - Bound DISASM_CACHE memory consumption by evicting the cache at 1024 entries and skipping caching of oversized (> 512 lines) objdump outputs. - Use sample.srccode() from the perf extension module to annotate disassembly output with source filenames, line numbers, and source lines. Update the ARM CoreSight disassembly shell test (test_arm_coresight_disasm.sh) to invoke the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Reviewed-by: James Clark Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/arm-cs-trace-disasm.py | 442 ++++++++++++++++++ .../coresight/test_arm_coresight_disasm.sh | 25 +- 2 files changed, 460 insertions(+), 7 deletions(-) create mode 100755 tools/perf/python/arm-cs-trace-disasm.py diff --git a/tools/perf/python/arm-cs-trace-disasm.py b/tools/perf/python/arm-cs-trace-disasm.py new file mode 100755 index 000000000000..cf126ac79c18 --- /dev/null +++ b/tools/perf/python/arm-cs-trace-disasm.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +arm-cs-trace-disasm.py: ARM CoreSight Trace Dump With Disassembler + +Author: Tor Jeremiassen + Mathieu Poirier + Leo Yan + Al Grant + +Below are some example commands for using this script. +Note a --kcore recording is required for accurate decode +due to the alternatives patching mechanism. In addition to this, +source line info comes from Perf, and when using kcore there is +no debug info. The following lists the supported features in each mode: + ++-----------+-----------------+------------------+------------------+ +| Recording | Accurate decode | Source line dump | Disassembly dump | ++-----------+-----------------+------------------+------------------+ +| --kcore | yes | no | yes | +| normal | no | yes (inaccurate) | yes (inaccurate) | ++-----------+-----------------+------------------+------------------+ + +Output disassembly with objdump and auto detect vmlinux +(when running on same machine): + perf script arm-cs-trace-disasm -- -d + +Output disassembly with llvm-objdump: + perf script arm-cs-trace-disasm -- -d llvm-objdump-11 -k path/to/vmlinux + +Output accurate disassembly by passing kcore to script: + perf script arm-cs-trace-disasm -- -d -k perf.data/kcore_dir/kcore + +Output only source line and symbols: + perf script arm-cs-trace-disasm +""" +from __future__ import annotations + +import os +from os import path +import re +from subprocess import CalledProcessError, check_output +import argparse +import platform +import sys +from typing import Dict, List, Optional + +import perf + +# Initialize global dicts and regular expression +DISASM_CACHE: Dict[str, List[str]] = {} +DISASM_RE = re.compile(r"^\s*([0-9a-fA-F]+):") +DISASM_FUNC_RE = re.compile(r"^\s*([0-9a-fA-F]+)\s.*:") +CACHE_SIZE = 1024 +class _State: + sample_idx: int = -1 + source_file_name: Optional[str] = None + line_number: Optional[int] = None + dso: Optional[str] = None + +_STATE = _State() + +KVER = platform.release() +VMLINUX_PATHS = [ + f"/usr/lib/debug/boot/vmlinux-{KVER}.debug", + f"/usr/lib/debug/lib/modules/{KVER}/vmlinux", + f"/lib/modules/{KVER}/build/vmlinux", + f"/usr/lib/debug/boot/vmlinux-{KVER}", + f"/boot/vmlinux-{KVER}", + "/boot/vmlinux", + "vmlinux", +] + +def default_objdump() -> str: + """Return the default objdump path from perf config or 'objdump'.""" + try: + config = perf.config_get("annotate.objdump") + return str(config) if config else "objdump" + except (AttributeError, TypeError): + return "objdump" + +def find_vmlinux() -> Optional[str]: + """Find the vmlinux file in standard paths.""" + if hasattr(find_vmlinux, "path"): + return getattr(find_vmlinux, "path") + + for v in VMLINUX_PATHS: + if os.access(v, os.R_OK): + setattr(find_vmlinux, "path", v) + return v + setattr(find_vmlinux, "path", None) + return None + +def get_dso_file_path(dso_name: str, dso_build_id: str, vmlinux: Optional[str]) -> str: + """Return the path to the DSO file.""" + # Locate DSO binaries in the perf build-id cache (~/.debug or PERF_BUILDID_DIR). + # Perf stores cached binaries under both ///elf + # and the canonical symlink/file layout /.build-id///elf. + buildid_dir = os.environ.get('PERF_BUILDID_DIR') + if not buildid_dir: + buildid_dir = os.path.join(os.environ.get('HOME', ''), '.debug') + + if (dso_name in ("[kernel.kallsyms]", "vmlinux", "kcore") + or dso_name.endswith("/vmlinux") or dso_name.endswith("/kcore")): + if vmlinux: + return vmlinux + if dso_build_id and dso_build_id != "[unknown]" and len(dso_build_id) > 2: + bid_candidate = os.path.join( + buildid_dir, ".build-id", dso_build_id[:2], dso_build_id[2:], "elf" + ) + if os.access(bid_candidate, os.R_OK): + return bid_candidate + for kname in (dso_name, "vmlinux", "[kernel.kallsyms]"): + candidate = os.path.join(buildid_dir, kname.lstrip("/"), dso_build_id, "elf") + if os.access(candidate, os.R_OK): + return candidate + return find_vmlinux() or dso_name + + if dso_name == "[vdso]": + append = "/vdso" + else: + append = "/elf" + + if dso_build_id and dso_build_id != "[unknown]" and len(dso_build_id) > 2: + bid_path = (buildid_dir + "/.build-id/" + dso_build_id[:2] + "/" + + dso_build_id[2:] + append).replace('//', '/') + if os.access(bid_path, os.R_OK): + return bid_path + + dso_path = buildid_dir + "/" + dso_name + "/" + dso_build_id + append + # Replace duplicate slash chars to single slash char + dso_path = dso_path.replace('//', '/') + return dso_path + +def read_disam(dso_fname: str, dso_start: int, start_addr: int, + stop_addr: int, objdump: str) -> List[str]: + """Read disassembly from a DSO file using objdump.""" + if stop_addr <= start_addr or (stop_addr - start_addr) > 0x100000: + return [] + addr_range = f"{start_addr}:{stop_addr}:{dso_start}:{dso_fname}" + + # Don't let the cache get too big, clear it when it hits max size + if len(DISASM_CACHE) > CACHE_SIZE: + DISASM_CACHE.clear() + + if addr_range in DISASM_CACHE: + disasm_output = DISASM_CACHE[addr_range] + else: + start_addr = start_addr - dso_start + stop_addr = stop_addr - dso_start + disasm = [objdump, "-d", "-z", + f"--start-address={start_addr:#x}", + f"--stop-address={stop_addr:#x}"] + disasm += [dso_fname] + try: + disasm_output = check_output(disasm).decode('utf-8', errors='replace').split('\n') + except (CalledProcessError, OSError): + return [] + if len(disasm_output) <= 512: + DISASM_CACHE[addr_range] = disasm_output + + return disasm_output + +def print_disam(dso_fname: str, dso_start: int, start_addr: int, + stop_addr: int, objdump: str) -> None: + """Print disassembly for a given address range.""" + for line in read_disam(dso_fname, dso_start, start_addr, stop_addr, objdump): + m = DISASM_FUNC_RE.search(line) + if m is None: + m = DISASM_RE.search(line) + if m is None: + continue + print(f"\t{line}") + +def print_sample(sample: perf.sample_event) -> None: + """Print sample details.""" + print(f"Sample = {{ cpu: {sample.sample_cpu:04d} addr: {sample.sample_addr:016x} " + f"phys_addr: {sample.sample_phys_addr:016x} ip: {sample.sample_ip:016x} " + f"pid: {sample.sample_pid} tid: {sample.sample_tid} period: {sample.sample_period} " + f"time: {sample.sample_time} index: {_STATE.sample_idx}}}") + +def common_start_str(comm: str, sample: perf.sample_event) -> str: + """Return common start string for sample output.""" + sec = int(sample.sample_time / 1000000000) + ns = sample.sample_time % 1000000000 + cpu = sample.sample_cpu + pid = sample.sample_pid + tid = sample.sample_tid + return f"{comm:>16s} {pid:5d}/{tid:<5d} [{cpu:04d}] {sec:9d}.{ns:09d} " + +def print_srccode(comm: str, sample: perf.sample_event, symbol: str, dso: str) -> None: + """Print source code and symbols for a sample.""" + ip = sample.sample_ip + if symbol == "[unknown]": + start_str = common_start_str(comm, sample) + f"{ip:x}".rjust(16).ljust(40) + else: + symoff = 0 + symoff = getattr(sample, 'sym_offset', 0) or 0 + offs = f"+{symoff:#x}" if symoff != 0 else "" + start_str = common_start_str(comm, sample) + (symbol + offs).ljust(40) + + source_file_name, line_number, source_line = sample.srccode() or (None, 0, None) + if source_file_name: + if _STATE.line_number == line_number and _STATE.source_file_name == source_file_name: + src_str = "" + else: + if len(source_file_name) > 40: + src_file = f"...{source_file_name[-37:]} " + else: + src_file = source_file_name.ljust(41) + + if source_line is None: + src_str = f"{src_file}{line_number:>4d} " + else: + src_str = f"{src_file}{line_number:>4d} {source_line}" + _STATE.dso = None + elif dso == _STATE.dso: + src_str = "" + else: + src_str = dso + _STATE.dso = dso + + _STATE.line_number = line_number + _STATE.source_file_name = source_file_name + + print(start_str, src_str) + +class TraceDisasm: + """Class to handle trace disassembly.""" + def __init__(self, cli_options: argparse.Namespace): + self.options = cli_options + self.sample_idx = -1 + self.cpu_data: Dict[str, int] = {} + self.session: Optional[perf.session] = None + + def process_event(self, sample: perf.sample_event) -> None: + """Process a single perf event.""" + self.sample_idx += 1 + _STATE.sample_idx = self.sample_idx + + if self.options.start_time is not None and sample.sample_time < self.options.start_time: + return + if self.options.stop_time is not None and sample.sample_time > self.options.stop_time: + sys.exit(0) + if self.options.start_sample is not None and self.sample_idx < self.options.start_sample: + return + if self.options.stop_sample is not None and self.sample_idx > self.options.stop_sample: + sys.exit(0) + + ev_name = str(sample.evsel) + if self.options.verbose: + print(f"Event type: {ev_name}") + print_sample(sample) + + # Initialize CPU data if it's empty, and directly return back + # if this is the first tracing event for this CPU. This must happen + # before the dso == '[unknown]' check because the initial + # CS_ETM_TRACE_ON sample has ip == 0 (dso == '[unknown]') and + # carries the starting target address in sample.sample_addr. + if self.cpu_data.get(f"{sample.sample_cpu}addr") is None: + self.cpu_data[f"{sample.sample_cpu}addr"] = sample.sample_addr + return + + dso = getattr(sample, 'dso_long_name', None) or sample.dso or '[unknown]' + symbol = sample.symbol or '[unknown]' + dso_bid = (sample.dso_bid.decode('utf-8') + if isinstance(sample.dso_bid, bytes) + else str(sample.dso_bid or '[unknown]')) + dso_start = sample.map_start + dso_end = sample.map_end + map_pgoff = sample.map_pgoff or 0 + + comm = "[unknown]" + try: + if self.session: + thread_info = self.session.find_thread(sample.sample_pid, sample.sample_tid) + if thread_info: + comm = thread_info.comm() or "[unknown]" + except (TypeError, AttributeError): + pass + + if dso == '[unknown]': + return + + if dso_start is None or dso_end is None: + print(f"Failed to find valid dso map for dso {dso}") + return + + if "instructions" in ev_name: + print_srccode(comm, sample, symbol, dso) + return + + if "branches" not in ev_name: + return + + self._process_branch(sample, comm, symbol, dso, dso_bid, dso_start, dso_end, map_pgoff) + + def _process_branch(self, sample: perf.sample_event, comm: str, symbol: str, dso: str, + dso_bid: str, dso_start: int, dso_end: int, map_pgoff: int) -> None: + """Helper to process branch events.""" + cpu = sample.sample_cpu + ip = sample.sample_ip + addr = sample.sample_addr + + # The format for packet is: + # + # +------------+------------+------------+ + # sample_prev: | addr | ip | cpu | + # +------------+------------+------------+ + # sample_next: | addr | ip | cpu | + # +------------+------------+------------+ + # + # Combine the two continuous packets to get the instruction range for + # sample_prev::cpu: + # + # [ sample_prev::addr .. sample_next::ip ] + # + # sample_prev::addr is stored into cpu_data and read back as + # 'start_addr' when the new packet arrives, and sample_next::ip + 4 is + # used for 'stop_addr' so objdump includes the branch instruction at + # sample_next::ip. + start_addr = self.cpu_data[str(cpu) + 'addr'] + stop_addr = ip + 4 + + # Record for previous sample packet + self.cpu_data[str(cpu) + 'addr'] = addr + + # Filter out zero start_address. Optionally identify CS_ETM_TRACE_ON packet + if start_addr == 0: + if stop_addr == 4 and self.options.verbose: + print(f"CPU{cpu}: CS_ETM_TRACE_ON packet is inserted") + return + + if start_addr < dso_start or start_addr > dso_end: + print(f"Start address {start_addr:#x} is out of range [ {dso_start:#x} .. " + f"{dso_end:#x} ] for dso {dso}") + return + + if stop_addr < dso_start or stop_addr > dso_end: + print(f"Stop address {stop_addr:#x} is out of range [ {dso_start:#x} .. " + f"{dso_end:#x} ] for dso {dso}") + return + + if self.options.objdump is not None: + # Kernel images (vmlinux / kcore) and non-PIE executables linked at + # the default 0x400000 base already have their virtual load addresses + # baked into the ELF symbol/section headers read by objdump, so + # dso_vm_start and map_pgoff_local must be 0 to avoid subtracting + # map_start from virtual sample addresses. + if (dso in ("[kernel.kallsyms]", "vmlinux", "kcore") + or dso.endswith("/vmlinux") or dso.endswith("/kcore")): + dso_vm_start = 0 + map_pgoff_local = 0 + elif dso_start == 0x400000: + dso_vm_start = 0 + map_pgoff_local = 0 + else: + dso_vm_start = dso_start + map_pgoff_local = map_pgoff + + dso_fname = get_dso_file_path(dso, dso_bid, self.options.vmlinux) + if path.exists(dso_fname): + print_disam(dso_fname, dso_vm_start, start_addr + map_pgoff_local, + stop_addr + map_pgoff_local, self.options.objdump) + else: + print(f"Failed to find dso {dso} for address range [ " + f"{start_addr + map_pgoff_local:#x} .. {stop_addr + map_pgoff_local:#x} ]") + + print_srccode(comm, sample, symbol, dso) + + def run(self) -> None: + """Run the trace disassembly session.""" + input_file = self.options.input or "perf.data" + if input_file != "-" and not os.path.exists(input_file): + print(f"Error: {input_file} not found.", file=sys.stderr) + sys.exit(1) + + print('ARM CoreSight Trace Data Assembler Dump') + try: + session_kwargs = { + "sample": self.process_event, + "itrace": self.options.itrace, + } + if self.options.vmlinux: + session_kwargs["vmlinux"] = self.options.vmlinux + try: + self.session = perf.session(perf.data(input_file), **session_kwargs) + except TypeError: + session_kwargs.pop("vmlinux", None) + self.session = perf.session(perf.data(input_file), **session_kwargs) + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e: + print(f"Error opening session: {e}", file=sys.stderr) + sys.exit(1) + + try: + self.session.process_events() + except (KeyboardInterrupt, SystemExit): + pass + finally: + # Break the reference cycle between self.session and the bound + # self.process_event callback so the C perf.session object is freed. + self.session = None + print('End') + +if __name__ == "__main__": + def int_arg(v: str) -> int: + """Helper for integer command line arguments.""" + val = int(v) + if val < 0: + raise argparse.ArgumentTypeError("Argument must be a positive integer") + return val + + arg_parser = argparse.ArgumentParser(description="ARM CoreSight Trace Dump With Disassembler") + arg_parser.add_argument("-i", "--input", help="input perf.data file") + arg_parser.add_argument("-k", "--vmlinux", default=os.environ.get("PERF_SYMBOL_VMLINUX"), + help="Set path to vmlinux file. Omit to autodetect") + arg_parser.add_argument("-d", "--objdump", nargs="?", const=default_objdump(), + help="Show disassembly. Can also be used to change the objdump path") + arg_parser.add_argument("-v", "--verbose", action="store_true", help="Enable debugging log") + arg_parser.add_argument("--start-time", type=int_arg, + help="Monotonic clock time of sample to start from.") + arg_parser.add_argument("--stop-time", type=int_arg, + help="Monotonic clock time of sample to stop at.") + arg_parser.add_argument("--itrace", default=os.environ.get("PERF_ITRACE") or "b", + help="Instruction tracing options.") + arg_parser.add_argument("--start-sample", type=int_arg, + help="Index of sample to start from.") + arg_parser.add_argument("--stop-sample", type=int_arg, + help="Index of sample to stop at.") + + parsed_options = arg_parser.parse_args() + if (parsed_options.start_time is not None and parsed_options.stop_time is not None and + parsed_options.start_time >= parsed_options.stop_time): + print("--start-time must less than --stop-time") + sys.exit(2) + if (parsed_options.start_sample is not None and parsed_options.stop_sample is not None and + parsed_options.start_sample >= parsed_options.stop_sample): + print("--start-sample must less than --stop-sample") + sys.exit(2) + + td = TraceDisasm(parsed_options) + td.run() diff --git a/tools/perf/tests/shell/coresight/test_arm_coresight_disasm.sh b/tools/perf/tests/shell/coresight/test_arm_coresight_disasm.sh index f3ebad596378..82cdb1762285 100755 --- a/tools/perf/tests/shell/coresight/test_arm_coresight_disasm.sh +++ b/tools/perf/tests/shell/coresight/test_arm_coresight_disasm.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Check Arm CoreSight disassembly script completes without errors (exclusive) # SPDX-License-Identifier: GPL-2.0 +# Check Arm CoreSight disassembly script completes without errors (exclusive) # The disassembly script reconstructs ranges of instructions and gives these to objdump to # decode. objdump doesn't like ranges that go backwards, but these are a good indication @@ -20,11 +20,23 @@ skip_if_no_cs_etm_event || exit 2 set -e glb_err=1 +# Relative path works whether it's installed or running from repo +if [ -n "$PERF_EXEC_PATH" ] && [ -e "$PERF_EXEC_PATH/python/arm-cs-trace-disasm.py" ]; then + script_path="$PERF_EXEC_PATH/python/arm-cs-trace-disasm.py" +else + script_path=$(dirname "$0")/../../../python/arm-cs-trace-disasm.py +fi + +# shellcheck source=lib/setup_python.sh +. "$(dirname "$0")"/../lib/setup_python.sh +$PYTHON -c "import perf" 2>/dev/null || { + echo "Skipping test, perf python module not found" + exit 2 +} + perfdata_dir=$(mktemp -d /tmp/__perf_test.perf.data.XXXXX) perfdata=${perfdata_dir}/perf.data file=$(mktemp /tmp/temporary_file.XXXXX) -# Relative path works whether it's installed or running from repo -script_path=$(dirname "$0")/../../../scripts/python/arm-cs-trace-disasm.py cleanup_files() { @@ -44,8 +56,8 @@ branch_search='[[:space:]](bl|b(\.(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al) if [ "$(id -u)" == 0 ] && [ -e /proc/kcore ]; then echo "Testing kernel disassembly" perf record -o ${perfdata} -e cs_etm//k --kcore -Se -m,64K -- touch $file > /dev/null 2>&1 - perf script -i ${perfdata} --itrace=b -s python:${script_path} -- \ - -d --stop-sample=2 -k ${perfdata}/kcore_dir/kcore 2> /dev/null > ${file} + $PYTHON ${script_path} -i ${perfdata} -d --stop-sample=2 \ + -k ${perfdata}/kcore_dir/kcore 2> /dev/null > ${file} grep -q -E ${branch_search} ${file} echo "Found kernel branches" else @@ -56,8 +68,7 @@ fi ## Test user ## echo "Testing userspace disassembly" perf record -o ${perfdata} -e cs_etm//u -Se -m,64K -- touch $file > /dev/null 2>&1 -perf script -i ${perfdata} --itrace=b -s python:${script_path} -- \ - -d --stop-sample=2 2> /dev/null > ${file} +$PYTHON ${script_path} -i ${perfdata} -d --stop-sample=2 2> /dev/null > ${file} grep -q -E ${branch_search} ${file} echo "Found userspace branches" From d5934ca319a08e41498e47210285b73970cf074a Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:06 -0700 Subject: [PATCH 214/425] perf python: Port powerpc-hcalls to perf module Port powerpc-hcalls.py from tools/perf/scripts/python/ to a standalone script in tools/perf/python/: - Refactor the script into an HCallAnalyzer class with full type annotations to encapsulate per-CPU hypervisor call entry/exit state. - Use perf.session for event processing to track hypervisor call entry and exit timestamps and aggregate min/max/average duration statistics against HCALL_TABLE. - Add argparse CLI support (-i/--input) and remove Python 2 compatibility code. Add a shell test (test_powerpc_hcalls_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/powerpc-hcalls.py | 334 ++++++++++++++++++ .../tests/shell/test_powerpc_hcalls_python.sh | 95 +++++ 2 files changed, 429 insertions(+) create mode 100755 tools/perf/python/powerpc-hcalls.py create mode 100755 tools/perf/tests/shell/test_powerpc_hcalls_python.sh diff --git a/tools/perf/python/powerpc-hcalls.py b/tools/perf/python/powerpc-hcalls.py new file mode 100755 index 000000000000..ab5867c05d3e --- /dev/null +++ b/tools/perf/python/powerpc-hcalls.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0+ +""" +Hypervisor call statistics + +Copyright (C) 2018 Ravi Bangoria, IBM Corporation +Ported from tools/perf/scripts/python/powerpc-hcalls.py +""" +from __future__ import annotations + +import argparse +from collections import defaultdict +import sys +import perf + +# Hypervisor call table +HCALL_TABLE = { + 4: 'H_REMOVE', + 8: 'H_ENTER', + 12: 'H_READ', + 16: 'H_CLEAR_MOD', + 20: 'H_CLEAR_REF', + 24: 'H_PROTECT', + 28: 'H_GET_TCE', + 32: 'H_PUT_TCE', + 36: 'H_SET_SPRG0', + 40: 'H_SET_DABR', + 44: 'H_PAGE_INIT', + 48: 'H_SET_ASR', + 52: 'H_ASR_ON', + 56: 'H_ASR_OFF', + 60: 'H_LOGICAL_CI_LOAD', + 64: 'H_LOGICAL_CI_STORE', + 68: 'H_LOGICAL_CACHE_LOAD', + 72: 'H_LOGICAL_CACHE_STORE', + 76: 'H_LOGICAL_ICBI', + 80: 'H_LOGICAL_DCBF', + 84: 'H_GET_TERM_CHAR', + 88: 'H_PUT_TERM_CHAR', + 92: 'H_REAL_TO_LOGICAL', + 96: 'H_HYPERVISOR_DATA', + 100: 'H_EOI', + 104: 'H_CPPR', + 108: 'H_IPI', + 112: 'H_IPOLL', + 116: 'H_XIRR', + 120: 'H_MIGRATE_DMA', + 124: 'H_PERFMON', + 220: 'H_REGISTER_VPA', + 224: 'H_CEDE', + 228: 'H_CONFER', + 232: 'H_PROD', + 236: 'H_GET_PPP', + 240: 'H_SET_PPP', + 244: 'H_PURR', + 248: 'H_PIC', + 252: 'H_REG_CRQ', + 256: 'H_FREE_CRQ', + 260: 'H_VIO_SIGNAL', + 264: 'H_SEND_CRQ', + 272: 'H_COPY_RDMA', + 276: 'H_REGISTER_LOGICAL_LAN', + 280: 'H_FREE_LOGICAL_LAN', + 284: 'H_ADD_LOGICAL_LAN_BUFFER', + 288: 'H_SEND_LOGICAL_LAN', + 292: 'H_BULK_REMOVE', + 304: 'H_MULTICAST_CTRL', + 308: 'H_SET_XDABR', + 312: 'H_STUFF_TCE', + 316: 'H_PUT_TCE_INDIRECT', + 332: 'H_CHANGE_LOGICAL_LAN_MAC', + 336: 'H_VTERM_PARTNER_INFO', + 340: 'H_REGISTER_VTERM', + 344: 'H_FREE_VTERM', + 348: 'H_RESET_EVENTS', + 352: 'H_ALLOC_RESOURCE', + 356: 'H_FREE_RESOURCE', + 360: 'H_MODIFY_QP', + 364: 'H_QUERY_QP', + 368: 'H_REREGISTER_PMR', + 372: 'H_REGISTER_SMR', + 376: 'H_QUERY_MR', + 380: 'H_QUERY_MW', + 384: 'H_QUERY_HCA', + 388: 'H_QUERY_PORT', + 392: 'H_MODIFY_PORT', + 396: 'H_DEFINE_AQP1', + 400: 'H_GET_TRACE_BUFFER', + 404: 'H_DEFINE_AQP0', + 408: 'H_RESIZE_MR', + 412: 'H_ATTACH_MCQP', + 416: 'H_DETACH_MCQP', + 420: 'H_CREATE_RPT', + 424: 'H_REMOVE_RPT', + 428: 'H_REGISTER_RPAGES', + 432: 'H_DISABLE_AND_GET', + 436: 'H_ERROR_DATA', + 440: 'H_GET_HCA_INFO', + 444: 'H_GET_PERF_COUNT', + 448: 'H_MANAGE_TRACE', + 456: 'H_GET_CPU_CHARACTERISTICS', + 468: 'H_FREE_LOGICAL_LAN_BUFFER', + 472: 'H_POLL_PENDING', + 484: 'H_QUERY_INT_STATE', + 580: 'H_ILLAN_ATTRIBUTES', + 584: 'H_ADD_LOGICAL_LAN_BUFFERS', + 592: 'H_MODIFY_HEA_QP', + 596: 'H_QUERY_HEA_QP', + 600: 'H_QUERY_HEA', + 604: 'H_QUERY_HEA_PORT', + 608: 'H_MODIFY_HEA_PORT', + 612: 'H_REG_BCMC', + 616: 'H_DEREG_BCMC', + 620: 'H_REGISTER_HEA_RPAGES', + 624: 'H_DISABLE_AND_GET_HEA', + 628: 'H_GET_HEA_INFO', + 632: 'H_ALLOC_HEA_RESOURCE', + 644: 'H_ADD_CONN', + 648: 'H_DEL_CONN', + 664: 'H_JOIN', + 672: 'H_VASI_SIGNAL', + 676: 'H_VASI_STATE', + 680: 'H_VIOCTL', + 688: 'H_ENABLE_CRQ', + 696: 'H_GET_EM_PARMS', + 720: 'H_SET_MPP', + 724: 'H_GET_MPP', + 732: 'H_REG_SUB_CRQ', + 736: 'H_FREE_SUB_CRQ', + 740: 'H_SEND_SUB_CRQ', + 744: 'H_SEND_SUB_CRQ_INDIRECT', + 748: 'H_HOME_NODE_ASSOCIATIVITY', + 756: 'H_BEST_ENERGY', + 764: 'H_XIRR_X', + 768: 'H_RANDOM', + 772: 'H_COP', + 788: 'H_GET_MPP_X', + 796: 'H_SET_MODE', + 808: 'H_BLOCK_REMOVE', + 856: 'H_CLEAR_HPT', + 864: 'H_REQUEST_VMC', + 876: 'H_RESIZE_HPT_PREPARE', + 880: 'H_RESIZE_HPT_COMMIT', + 892: 'H_REGISTER_PROC_TBL', + 896: 'H_SIGNAL_SYS_RESET', + 904: 'H_ALLOCATE_VAS_WINDOW', + 908: 'H_MODIFY_VAS_WINDOW', + 912: 'H_DEALLOCATE_VAS_WINDOW', + 916: 'H_QUERY_VAS_WINDOW', + 920: 'H_QUERY_VAS_CAPABILITIES', + 924: 'H_QUERY_NX_CAPABILITIES', + 928: 'H_GET_NX_FAULT', + 936: 'H_INT_GET_SOURCE_INFO', + 940: 'H_INT_SET_SOURCE_CONFIG', + 944: 'H_INT_GET_SOURCE_CONFIG', + 948: 'H_INT_GET_QUEUE_INFO', + 952: 'H_INT_SET_QUEUE_CONFIG', + 956: 'H_INT_GET_QUEUE_CONFIG', + 960: 'H_INT_SET_OS_REPORTING_LINE', + 964: 'H_INT_GET_OS_REPORTING_LINE', + 968: 'H_INT_ESB', + 972: 'H_INT_SYNC', + 976: 'H_INT_RESET', + 996: 'H_SCM_READ_METADATA', + 1000: 'H_SCM_WRITE_METADATA', + 1004: 'H_SCM_BIND_MEM', + 1008: 'H_SCM_UNBIND_MEM', + 1012: 'H_SCM_QUERY_BLOCK_MEM_BINDING', + 1016: 'H_SCM_QUERY_LOGICAL_MEM_BINDING', + 1020: 'H_SCM_UNBIND_ALL', + 1024: 'H_SCM_HEALTH', + 1048: 'H_SCM_PERFORMANCE_STATS', + 1052: 'H_PKS_GET_CONFIG', + 1056: 'H_PKS_SET_PASSWORD', + 1060: 'H_PKS_GEN_PASSWORD', + 1068: 'H_PKS_WRITE_OBJECT', + 1072: 'H_PKS_GEN_KEY', + 1076: 'H_PKS_READ_OBJECT', + 1080: 'H_PKS_REMOVE_OBJECT', + 1084: 'H_PKS_CONFIRM_OBJECT_FLUSHED', + 1096: 'H_RPT_INVALIDATE', + 1100: 'H_SCM_FLUSH', + 1104: 'H_GET_ENERGY_SCALE_INFO', + 1108: 'H_PKS_SIGNED_UPDATE', + 1112: 'H_HTM', + 1116: 'H_WATCHDOG', + # Platform specific hcalls used by KVM on PowerVM + 1120: 'H_GUEST_GET_CAPABILITIES', + 1124: 'H_GUEST_SET_CAPABILITIES', + 1136: 'H_GUEST_CREATE', + 1140: 'H_GUEST_CREATE_VCPU', + 1144: 'H_GUEST_GET_STATE', + 1148: 'H_GUEST_SET_STATE', + 1152: 'H_GUEST_RUN_VCPU', + 1156: 'H_GUEST_COPY_MEMORY', + 1160: 'H_GUEST_DELETE', + # Key wrapping hcalls + 1168: 'H_PKS_WRAP_OBJECT', + 1172: 'H_PKS_UNWRAP_OBJECT', + # Platform-specific hcalls used by the Ultravisor + 61184: 'H_SVM_PAGE_IN', + 61188: 'H_SVM_PAGE_OUT', + 61192: 'H_SVM_INIT_START', + 61196: 'H_SVM_INIT_DONE', + 61204: 'H_SVM_INIT_ABORT', + # Platform specific hcalls used by KVM + 61440: 'H_RTAS', + # Platform specific hcalls used by QEMU/SLOF + 61441: 'H_LOGICAL_MEMOP', + 61442: 'H_CAS', + 61443: 'H_UPDATE_DT', + # Platform specific hcalls provided by PHYP + 61560: 'H_GET_24X7_CATALOG_PAGE', + 61564: 'H_GET_24X7_DATA', + 61568: 'H_GET_PERF_COUNTER_INFO', + # Platform-specific hcalls used for nested HV KVM + 63488: 'H_SET_PARTITION_TABLE', + 63492: 'H_ENTER_NESTED', + 63496: 'H_TLB_INVALIDATE', + 63500: 'H_COPY_TOFROM_GUEST', +} + + +class HCallAnalyzer: + """Analyzes hypervisor calls and aggregates statistics.""" + + def __init__(self, cmd_args) -> None: + self.args = cmd_args + # output: {opcode: {'min': min, 'max': max, 'time': time, 'cnt': cnt}} + self.output: dict[int, dict[str, 'int | float']] = \ + defaultdict(lambda: {'time': 0, 'cnt': 0, 'min': float('inf'), 'max': 0}) + # d_enter: {cpu: {opcode: nsec}} + self.d_enter: dict[int, dict[int, int]] = {} + self.print_ptrn = '%-28s%10s%10s%10s%10s' + + def hcall_table_lookup(self, opcode: int) -> str: + """Lookup hcall name by opcode.""" + return HCALL_TABLE.get(opcode, str(opcode)) + + def process_event(self, sample: perf.sample_event) -> None: + """Process a single sample event.""" + name = str(sample.evsel) + if name not in ("evsel(powerpc:hcall_entry)", "evsel(powerpc:hcall_exit)"): + return + + try: + cpu = sample.sample_cpu + opcode = sample.opcode + except AttributeError: + print("ERROR: tracepoint fields missed (is libtraceevent enabled?)", + file=sys.stderr) + sys.exit(1) + + # sample.sample_time represents the nsecs + time = sample.sample_time + + if opcode < 0 or cpu < 0: + return + + if name == "evsel(powerpc:hcall_entry)": + if cpu not in self.d_enter: + self.d_enter[cpu] = {} + self.d_enter[cpu][opcode] = time + elif name == "evsel(powerpc:hcall_exit)": + if cpu in self.d_enter and opcode in self.d_enter[cpu]: + time_entry = self.d_enter[cpu][opcode] + diff = time - time_entry + del self.d_enter[cpu][opcode] + + stats = self.output[opcode] + stats['time'] += diff + stats['cnt'] += 1 + if diff < stats['min']: + stats['min'] = diff + if diff > stats['max']: + stats['max'] = diff + + def print_summary(self) -> None: + """Print aggregated statistics.""" + print(self.print_ptrn % ('hcall', 'count', 'min(ns)', 'max(ns)', 'avg(ns)')) + print('-' * 68) + + def sort_output(opcode): + stats = self.output[opcode] + sort_by = getattr(self.args, 'sort', None) + if sort_by == 'min': + return stats['min'] + if sort_by == 'max': + return stats['max'] + if sort_by == 'avg': + return stats['time'] // stats['cnt'] + return stats['cnt'] + + for opcode in sorted(self.output.keys(), key=sort_output, reverse=True): + h_name = self.hcall_table_lookup(opcode) + stats = self.output[opcode] + time = stats['time'] + cnt = stats['cnt'] + min_t = stats['min'] + max_t = stats['max'] + + # Avoid float representation for large integers if possible, + # or use formatted strings. Legacy used time//cnt. + avg_t = time // cnt if cnt > 0 else 0 + + # If min was not updated, it remains inf, but cnt should be > 0 if in output + if min_t == float('inf'): + min_t = 0 + + print(self.print_ptrn % (h_name, cnt, int(min_t), int(max_t), avg_t)) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Hypervisor call statistics") + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + ap.add_argument("-s", "--sort", default="count", + choices=["count", "min", "max", "avg"], help="Sort key") + args = ap.parse_args() + + analyzer = HCallAnalyzer(args) + + session = None + try: + session = perf.session(perf.data(args.input), sample=analyzer.process_event) + session.process_events() + analyzer.print_summary() + except KeyboardInterrupt: + analyzer.print_summary() + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e: + print(f"Error processing events: {e}") + sys.exit(1) + finally: + # Break the reference cycle between perf.session and analyzer.process_event. + session = None diff --git a/tools/perf/tests/shell/test_powerpc_hcalls_python.sh b/tools/perf/tests/shell/test_powerpc_hcalls_python.sh new file mode 100755 index 000000000000..5316c28f243f --- /dev/null +++ b/tools/perf/tests/shell/test_powerpc_hcalls_python.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# powerpc-hcalls python test + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/powerpc-hcalls.py" + +if ! perf check feature -q libtraceevent > /dev/null 2>&1; then + echo "Skipping test, libtraceevent is disabled" + exit 2 +fi + +if [ ! -f "$script_path" ]; then + echo "Skipping test, powerpc-hcalls.py not found at $script_path" + exit 2 +fi + +err=0 +cleanup() { + rm -f "${temp_data}" "${temp_out}" +} +trap 'cleanup' EXIT TERM INT + +temp_data=$(mktemp /tmp/perf.data.XXXXXX) +temp_out=$(mktemp /tmp/perf.out.XXXXXX) + +echo "Testing powerpc-hcalls.py..." + +# Verify HCallAnalyzer aggregation logic on synthetic hcall_entry/hcall_exit samples +if ! "$PYTHON" - "$script_path" > "${temp_out}" <<'EOF' +import argparse +import importlib.util +import sys +from types import SimpleNamespace + +spec = importlib.util.spec_from_file_location("powerpc_hcalls", sys.argv[1]) +assert spec and spec.loader +mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(mod) + +analyzer = mod.HCallAnalyzer(argparse.Namespace(sort='count')) +analyzer.process_event(SimpleNamespace( + evsel="evsel(powerpc:hcall_entry)", sample_cpu=0, sample_time=1000, opcode=4)) +analyzer.process_event(SimpleNamespace( + evsel="evsel(powerpc:hcall_exit)", sample_cpu=0, sample_time=2500, opcode=4)) +analyzer.print_summary() +EOF +then + echo "powerpc-hcalls.py synthetic aggregation test failed" + exit 1 +fi + +if ! grep -q "H_REMOVE.*1.*1500.*1500.*1500" "${temp_out}"; then + echo "Failed to find aggregated H_REMOVE metrics in output" + exit 1 +fi + +# Create a perf.data file if powerpc hcall tracepoints are available on this host. +if ! perf record -e powerpc:hcall_entry,powerpc:hcall_exit -a -o "${temp_data}" \ + -- perf test -w noploop >/dev/null 2>&1; then + echo "Skipping live record test, powerpc hcall tracepoints not available" + exit 0 +fi + +if [ ! -s "${temp_data}" ]; then + echo "Skipping live record test, perf record failed to create data" + exit 0 +fi + +# Check that the script executes on recorded perf.data +if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "powerpc-hcalls.py test failed" + err=1 +else + if ! grep -q "hcall.*count.*min.*max.*avg" "${temp_out}"; then + echo "Failed to find the metrics table header" + err=1 + else + echo "powerpc-hcalls test passed." + fi +fi +rm -f "${temp_out}" + +exit $err From d4ce72e9e238fd703306b66a33eb41391e6e5a5a Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:07 -0700 Subject: [PATCH 215/425] perf python: Port intel-pt-events and libxed to perf module Port intel-pt-events.py and libxed.py from tools/perf/scripts/python/ to standalone modules in tools/perf/python/: - Refactor intel-pt-events.py into an IntelPTAnalyzer class with full type annotations to encapsulate trace state and stashed output. - Configure instruction trace decoding directly via perf.session's itrace option and context_switch callback. - Dynamically select 32-bit vs 64-bit disassembly mode in libxed.py using session.is_64_bit and annotate instructions with source lines via sample.srccode(). - Rename methods in libxed.py to snake_case (instruction, set_mode, disassemble_one) and remove Python 2 compatibility code. Add a shell test (test_intel_pt_events_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/intel-pt-events.py | 632 ++++++++++++++++++ tools/perf/python/libxed.py | 123 ++++ .../shell/test_intel_pt_events_python.sh | 69 ++ 3 files changed, 824 insertions(+) create mode 100755 tools/perf/python/intel-pt-events.py create mode 100755 tools/perf/python/libxed.py create mode 100755 tools/perf/tests/shell/test_intel_pt_events_python.sh diff --git a/tools/perf/python/intel-pt-events.py b/tools/perf/python/intel-pt-events.py new file mode 100755 index 000000000000..c233a29eb8f9 --- /dev/null +++ b/tools/perf/python/intel-pt-events.py @@ -0,0 +1,632 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Print Intel PT Events including Power Events and PTWRITE. +Ported from tools/perf/scripts/python/intel-pt-events.py +""" +from __future__ import annotations + +import argparse +from typing import Dict, List, Tuple +import contextlib +from ctypes import addressof, create_string_buffer +import io +import os +import struct +import sys +from typing import Any, Optional +import typing +import perf + +try: + from libxed import LibXED as _LibXED + LibXED: Optional[type[_LibXED]] = _LibXED +except ImportError: + LibXED = None + + +def sample_flags_to_name(flags: int) -> str: + """Implement perf's sample_flags_to_name.""" + if not isinstance(flags, int) or flags == 0: + return "".ljust(21) + # PERF_IP_FLAG_* bit positions (from tools/perf/util/event.h): + # bit 0: PERF_IP_FLAG_BRANCH + # bit 1: PERF_IP_FLAG_CALL + # bit 2: PERF_IP_FLAG_RETURN + # bit 3: PERF_IP_FLAG_CONDITIONAL + # bit 4: PERF_IP_FLAG_SYSCALLRET + # bit 5: PERF_IP_FLAG_ASYNC + # bit 6: PERF_IP_FLAG_INTERRUPT + # bit 7: PERF_IP_FLAG_TX_ABORT + # bit 8: PERF_IP_FLAG_TRACE_BEGIN + # bit 9: PERF_IP_FLAG_TRACE_END + # bit 10: PERF_IP_FLAG_IN_TX + # bit 11: PERF_IP_FLAG_VMENTRY + # bit 12: PERF_IP_FLAG_VMEXIT + # bit 13: PERF_IP_FLAG_INTR_DISABLE + # bit 14: PERF_IP_FLAG_INTR_TOGGLE + # bit 15: PERF_IP_FLAG_BRANCH_MISS + # bit 16: PERF_IP_FLAG_NOT_TAKEN + sample_flags = [ + ((1 << 0) | (1 << 1), "call"), + ((1 << 0) | (1 << 2), "return"), + ((1 << 0) | (1 << 3), "jcc"), + ((1 << 0), "jmp"), + ((1 << 0) | (1 << 1) | (1 << 6), "int"), + ((1 << 0) | (1 << 2) | (1 << 6), "iret"), + ((1 << 0) | (1 << 1) | (1 << 4), "syscall"), + ((1 << 0) | (1 << 2) | (1 << 4), "sysret"), + ((1 << 0) | (1 << 5), "async"), + ((1 << 0) | (1 << 1) | (1 << 5) | (1 << 6), "hw int"), + ((1 << 0) | (1 << 7), "tx abrt"), + ((1 << 0) | (1 << 8), "tr strt"), + ((1 << 0) | (1 << 9), "tr end"), + ((1 << 0) | (1 << 1) | (1 << 11), "vmentry"), + ((1 << 0) | (1 << 1) | (1 << 12), "vmexit"), + ] + additional_mask = (1 << 10) | (1 << 13) | (1 << 14) + branch_event_mask = (1 << 15) | (1 << 16) + xf = flags & additional_mask + rem_flags = flags & ~additional_mask + + if rem_flags & (1 << 8): + prefix = "tr strt " + elif rem_flags & (1 << 9): + prefix = "tr end " + else: + prefix = "" + + rem_flags &= ~((1 << 8) | (1 << 9)) + types = rem_flags & ~branch_event_mask + type_name = "" + for f_mask, f_name in sample_flags: + if f_mask == types: + type_name = f_name + break + + s = prefix + type_name + ev_parts = [] + if rem_flags & (1 << 15): + ev_parts.append("miss") + if rem_flags & (1 << 16): + ev_parts.append("not_taken") + if ev_parts: + s += "/" + ",".join(ev_parts) + "/" + + if xf: + xs = "(" + if xf & (1 << 10): + xs += "x" + if xf & (1 << 13): + xs += "D" + if xf & (1 << 14): + xs += "t" + xs += ")" + if len(s) + len(xs) < 21: + s = s + xs.rjust(21 - len(s)) + else: + s = s + " " + xs + return s.ljust(21) + +class IntelPTAnalyzer: + """Analyzes Intel PT events and prints details.""" + + def __init__(self, cfg: argparse.Namespace): + self.args = cfg + self.session: Optional[perf.session] = None + self.insn = False + self.src = False + self.source_file_name: Optional[str] = None + self.line_number: int = 0 + self.dso: Optional[str] = None + self.stash_dict: Dict[int, List[str]] = {} + self.output: Any = None + self.output_pos: int = 0 + self.cpu: int = -1 + self.time: int = 0 + self.switch_str: Dict[int, str] = {} + + if cfg.insn_trace: + print("Intel PT Instruction Trace") + self.insn = True + elif cfg.src_trace: + print("Intel PT Source Trace") + self.insn = True + self.src = True + else: + print("Intel PT Branch Trace, Power Events, Event Trace and PTWRITE") + + self.disassembler: Any = None + self.inst: Any = None + if self.insn and LibXED is not None: + try: + self.disassembler = LibXED() + self.inst = self.disassembler.instruction() + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError, + ImportError) as e: + print(f"Failed to initialize LibXED: {e}") + self.disassembler = None + self.inst = None + + def print_ptwrite(self, raw_buf: bytes) -> None: + """Print PTWRITE data.""" + if not raw_buf: + return + try: + data = struct.unpack_from(" None: + """Print CBR data.""" + if len(raw_buf) < 12: + return + try: + data = struct.unpack_from(" None: + """Print MWAIT data.""" + try: + data = struct.unpack_from("> 32) & 0x3 + print(f"hints: {hints:#x} extensions: {extensions:#x}", end=' ') + + def print_pwre(self, raw_buf: bytes) -> None: + """Print PWRE data.""" + try: + data = struct.unpack_from("> 7) & 1 + cstate = (payload >> 12) & 0xf + subcstate = (payload >> 8) & 0xf + print(f"hw: {hw} cstate: {cstate} sub-cstate: {subcstate}", end=' ') + + def print_exstop(self, raw_buf: bytes) -> None: + """Print EXSTOP data.""" + try: + data = struct.unpack_from(" None: + """Print PWRX data.""" + try: + data = struct.unpack_from("> 4) & 0xf + wake_reason = (payload >> 8) & 0xf + print(f"deepest cstate: {deepest_cstate} last cstate: {last_cstate} " + f"wake reason: {wake_reason:#x}", end=' ') + + def print_psb(self, raw_buf: bytes) -> None: + """Print PSB data.""" + try: + data = struct.unpack_from(" None: + """Print EVT data.""" + glb_cfe = ["", "INTR", "IRET", "SMI", "RSM", "SIPI", "INIT", "VMENTRY", "VMEXIT", + "VMEXIT_INTR", "SHUTDOWN", "", "UINT", "UIRET"] + [""] * 18 + glb_evd = ["", "PFA", "VMXQ", "VMXR"] + [""] * 60 + + try: + data = struct.unpack_from("> 7 + vector = data[1] + evd_cnt = data[2] + s = glb_cfe[typ] + if s: + print(f" cfe: {s} IP: {ip_flag} vector: {vector}", end=' ') + else: + print(f" cfe: {typ} IP: {ip_flag} vector: {vector}", end=' ') + pos = 4 + for _ in range(evd_cnt): + if len(raw_buf) < pos + 16: + break + try: + data = struct.unpack_from(" None: + """Print IFLAG data.""" + try: + data = struct.unpack_from("{iflag} {s} branch", end=' ') + + def common_start_str(self, comm: str, sample: perf.sample_event) -> str: + """Return common start string for display.""" + ts = sample.sample_time + cpu = sample.sample_cpu + pid = sample.sample_pid + tid = sample.sample_tid + machine_pid = getattr(sample, "machine_pid", 0) + if machine_pid: + vcpu = getattr(sample, "vcpu", -1) + return (f"VM:{machine_pid:5d} VCPU:{vcpu:03d} {comm:>16s} {pid:5d}/{tid:<5d} " + f"[{cpu:03d}] {ts // 1000000000:9d}.{ts % 1000000000:09d} ") + return (f"{comm:>16s} {pid:5d}/{tid:<5d} [{cpu:03d}] " + f"{ts // 1000000000:9d}.{ts % 1000000000:09d} ") + + def print_common_start(self, comm: str, sample: perf.sample_event, name: str) -> None: + """Print common start info.""" + flags_disp = sample_flags_to_name(getattr(sample, "flags", 0)) + print(self.common_start_str(comm, sample) + f"{name:>8s} {flags_disp:>21s}", end=' ') + + def print_instructions_start(self, comm: str, sample: perf.sample_event) -> None: + """Print instructions start info.""" + raw_flags = getattr(sample, "flags", 0) + if isinstance(raw_flags, int) and (raw_flags & (1 << 10)): + print(self.common_start_str(comm, sample) + "x", end=' ') + else: + print(self.common_start_str(comm, sample), end=' ') + + def disassem(self, insn: bytes, ip: int) -> Tuple[int, str]: + """Disassemble instruction using LibXED.""" + inst = self.inst if self.inst is not None else self.disassembler.instruction() + is_64_bit = getattr(self.session, "is_64_bit", True) if self.session else True + self.disassembler.set_mode(inst, 0 if is_64_bit else 1) + buf = create_string_buffer(insn) + return self.disassembler.disassemble_one(inst, addressof(buf), len(insn), ip) + + def print_common_ip(self, sample: perf.sample_event, symbol: str, dso: str) -> None: + """Print IP and symbol info.""" + ip = sample.sample_ip + offs = f"+{sample.sym_offset:#x}" if getattr(sample, "sym_offset", None) is not None else "" + cyc_cnt = getattr(sample, "sample_cyc_count", 0) + if cyc_cnt: + insn_cnt = getattr(sample, "sample_insn_count", 0) + ipc_str = f" IPC: {insn_cnt / cyc_cnt:#.2f} ({insn_cnt}/{cyc_cnt})" + else: + ipc_str = "" + + if self.insn and self.disassembler is not None: + try: + insn = sample.insn() + except AttributeError: + insn = None + if insn: + cnt, text = self.disassem(insn, ip) + byte_str = (f"{ip:x}").rjust(16) + for k in range(cnt): + byte_str += f" {insn[k]:02x}" + print(f"{byte_str:<40s} {text:<30s}", end=' ') + else: + print(f"{ip:16x}", end=' ') + print(f"{symbol}{offs} ({dso})", end=' ') + else: + print(f"{ip:16x} {symbol}{offs} ({dso})", end=' ') + + addr = getattr(sample, "sample_addr", getattr(sample, "addr", 0)) + addr_correlates_sym = ( + bool(addr) + or getattr(sample, "addr_symbol", None) is not None + or getattr(sample, "addr_dso", None) is not None + ) + if addr_correlates_sym: + addr_dso = (sample.addr_dso or '[unknown]') + addr_symbol = (sample.addr_symbol or '[unknown]') + addr_offs = (f"+{sample.addr_sym_offset:#x}" + if getattr(sample, "addr_sym_offset", None) is not None else "") + print(f"=> {addr:x} {addr_symbol}{addr_offs} ({addr_dso}){ipc_str}") + else: + print(ipc_str) + + def print_srccode(self, comm: str, sample: perf.sample_event, + symbol: str, dso: str, with_insn: bool) -> None: + """Print source code info.""" + ip = sample.sample_ip + if symbol == "[unknown]": + start_str = self.common_start_str(comm, sample) + (f"{ip:x}").rjust(16).ljust(40) + else: + offs = (f"+{sample.sym_offset:#x}" + if getattr(sample, "sym_offset", None) is not None else "") + start_str = self.common_start_str(comm, sample) + (symbol + offs).ljust(40) + + if with_insn and self.insn and self.disassembler is not None: + try: + insn = sample.insn() + except AttributeError: + insn = None + if insn: + _, text = self.disassem(insn, ip) + start_str += text.ljust(30) + + source_file_name: typing.Any = None + line_number: typing.Any = 0 + source_line: typing.Any = None + try: + res_srcc = sample.srccode() + if res_srcc: + source_file_name, line_number, source_line = res_srcc + except (AttributeError, ValueError, TypeError): + pass + + if source_file_name: + if self.line_number == line_number and self.source_file_name == source_file_name: + src_str = "" + else: + if len(source_file_name) > 40: + src_file = ("..." + source_file_name[-37:]) + " " + else: + src_file = source_file_name.ljust(41) + if source_line is None: + src_str = src_file + str(line_number).rjust(4) + " " + else: + src_str = src_file + str(line_number).rjust(4) + " " + source_line + self.dso = None + elif dso == self.dso: + src_str = "" + else: + src_str = dso + self.dso = dso + + self.line_number = line_number + self.source_file_name = source_file_name + print(start_str, src_str) + + def do_process_event(self, sample: perf.sample_event) -> None: + """Process event and print info.""" + cpu = getattr(sample, "sample_cpu", getattr(sample, "cpu", 0)) + if cpu in self.switch_str: + print(self.switch_str[cpu]) + del self.switch_str[cpu] + + comm = "Unknown" + if hasattr(self, 'session') and self.session: + try: + thread = self.session.find_thread(sample.sample_pid, sample.sample_tid) + if thread: + comm = thread.comm() or "Unknown" + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError): + pass + # Python < 3.9 compatibility + name = str(sample.evsel) + if name.startswith("evsel("): + name = name[6:-1] + dso = (sample.dso or '[unknown]') + symbol = (sample.symbol or '[unknown]') + + raw_buf = getattr(sample, 'raw_buf', b'') or b'' + + if name.startswith("instructions"): + if self.src: + self.print_srccode(comm, sample, symbol, dso, True) + else: + self.print_instructions_start(comm, sample) + self.print_common_ip(sample, symbol, dso) + elif name.startswith("branches"): + if self.src: + self.print_srccode(comm, sample, symbol, dso, False) + else: + self.print_common_start(comm, sample, name) + self.print_common_ip(sample, symbol, dso) + elif name == "ptwrite": + self.print_common_start(comm, sample, name) + self.print_ptwrite(raw_buf) + self.print_common_ip(sample, symbol, dso) + elif name == "cbr": + self.print_common_start(comm, sample, name) + self.print_cbr(raw_buf) + self.print_common_ip(sample, symbol, dso) + elif name == "mwait": + self.print_common_start(comm, sample, name) + self.print_mwait(raw_buf) + self.print_common_ip(sample, symbol, dso) + elif name == "pwre": + self.print_common_start(comm, sample, name) + self.print_pwre(raw_buf) + self.print_common_ip(sample, symbol, dso) + elif name == "exstop": + self.print_common_start(comm, sample, name) + self.print_exstop(raw_buf) + self.print_common_ip(sample, symbol, dso) + elif name == "pwrx": + self.print_common_start(comm, sample, name) + self.print_pwrx(raw_buf) + self.print_common_ip(sample, symbol, dso) + elif name == "psb": + self.print_common_start(comm, sample, name) + self.print_psb(raw_buf) + self.print_common_ip(sample, symbol, dso) + elif name == "evt": + self.print_common_start(comm, sample, name) + self.print_evt(raw_buf) + self.print_common_ip(sample, symbol, dso) + elif name == "iflag": + self.print_common_start(comm, sample, name) + self.print_iflag(raw_buf) + self.print_common_ip(sample, symbol, dso) + else: + self.print_common_start(comm, sample, name) + self.print_common_ip(sample, symbol, dso) + + def interleave_events(self, sample: perf.sample_event) -> None: + """Interleave output to avoid garbled lines from different CPUs.""" + self.cpu = sample.sample_cpu + ts = sample.sample_time + + if self.time != ts: + self.time = ts + self.flush_stashed_output() + + self.output_pos = 0 + with contextlib.redirect_stdout(io.StringIO()) as self.output: + self.do_process_event(sample) + + self.stash_output() + + def stash_output(self) -> None: + """Stash output for later flushing.""" + output_str = self.output.getvalue()[self.output_pos:] + n = len(output_str) + if n: + self.output_pos += n + if self.cpu not in self.stash_dict: + self.stash_dict[self.cpu] = [] + self.stash_dict[self.cpu].append(output_str) + if len(self.stash_dict[self.cpu]) > 1000: + self.flush_stashed_output() + + def flush_stashed_output(self) -> None: + """Flush stashed output.""" + while self.stash_dict: + cpus = list(self.stash_dict.keys()) + for cpu in cpus: + items = self.stash_dict[cpu] + countdown = self.args.interleave + while len(items) and countdown: + sys.stdout.write(items[0]) + del items[0] + countdown -= 1 + if not items: + del self.stash_dict[cpu] + + def process_context_switch(self, event: perf.switch_event) -> None: + """Process context switch.""" + cpu = getattr(event, "sample_cpu", getattr(event, "cpu", 0)) + pid = getattr(event, "sample_pid", getattr(event, "pid", 0)) + tid = getattr(event, "sample_tid", getattr(event, "tid", 0)) + ts = getattr(event, "sample_time", getattr(event, "time", 0)) + np_pid = getattr(event, "next_prev_pid", None) + np_tid = getattr(event, "next_prev_tid", None) + machine_pid = getattr(event, "machine_pid", -1) + vcpu = getattr(event, "vcpu", -1) + misc = getattr(event, "misc", 0) + out = bool(misc & (1 << 13)) + out_preempt = bool(misc & (1 << 14)) + + if self.args.interleave: + self.flush_stashed_output() + + if out: + out_str = "Switch out " + else: + out_str = "Switch In " + + preempt_str = "preempt" if out_preempt else "" + + if machine_pid == -1: + machine_str = "" + elif vcpu == -1: + machine_str = f"machine PID {machine_pid}" + else: + machine_str = f"machine PID {machine_pid} VCPU {vcpu}" + + np_str = f"{np_pid:5d}/{np_tid:<5d} " if np_pid is not None and np_tid is not None else "" + c_str = (f"{out_str:>16s} {pid:5d}/{tid:<5d} [{cpu:03d}] " + f"{ts // 1000000000:9d}.{ts % 1000000000:09d} " + f"{np_str}{machine_str} {preempt_str}") + + if self.args.all_switch_events: + print(c_str) + else: + self.switch_str[cpu] = c_str + def process_event(self, sample: perf.sample_event) -> None: + """Wrapper to handle interleaving and exceptions.""" + try: + if self.args.interleave: + self.interleave_events(sample) + else: + self.do_process_event(sample) + except BrokenPipeError: + # Stop python printing broken pipe errors and traceback + sys.stdout = open(os.devnull, 'w', encoding='utf-8') + sys.exit(1) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + ap.add_argument("--insn-trace", action='store_true') + ap.add_argument("--src-trace", action='store_true') + ap.add_argument("--all-switch-events", action='store_true') + ap.add_argument("--interleave", type=int, nargs='?', const=4, default=0) + ap.add_argument("--itrace", default=os.environ.get("PERF_ITRACE"), help="itrace options") + args = ap.parse_args() + + if args.itrace is None: + if args.insn_trace or args.src_trace: + args.itrace = "i0nsepwxI" + else: + args.itrace = "bepwxI" + analyzer = IntelPTAnalyzer(args) + + try: + # Note: Python API currently lacks auxtrace_error callbacks affecting + # chronological interleaving + try: + analyzer.session = perf.session( + perf.data(args.input), + sample=analyzer.process_event, + context_switch=analyzer.process_context_switch, + itrace=args.itrace + ) + try: + analyzer.session.process_events() + except KeyboardInterrupt: + pass + finally: + # Break the reference cycle between analyzer.session and the bound + # analyzer.process_event / process_context_switch callbacks so the + # underlying C perf.session object is freed. + analyzer.session = None + if args.interleave: + analyzer.flush_stashed_output() + print("End") + except BrokenPipeError: + sys.exit(0) + except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError): + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tools/perf/python/libxed.py b/tools/perf/python/libxed.py new file mode 100755 index 000000000000..900bd3e0184b --- /dev/null +++ b/tools/perf/python/libxed.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Python wrapper for libxed.so +Ported from tools/perf/scripts/python/libxed.py +""" +from __future__ import annotations + +from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \ + c_void_p, c_byte, c_int, c_uint, c_ulonglong + +# To use Intel XED, libxed.so must be present. To build and install +# libxed.so: +# git clone https://github.com/intelxed/mbuild.git mbuild +# git clone https://github.com/intelxed/xed +# cd xed +# ./mfile.py --share +# sudo ./mfile.py --prefix=/usr/local install +# sudo ldconfig +# + + +class XedStateT(Structure): + """xed_state_t structure.""" + _fields_ = [ + ("mode", c_int), + ("width", c_int) + ] + + +class XEDInstruction(): + """Represents a decoded instruction.""" + + def __init__(self, libxed): + # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion + xedd_t = c_byte * 512 + self.xedd = xedd_t() + self.xedp = addressof(self.xedd) + libxed.xed_decoded_inst_zero(self.xedp) + self.state = XedStateT() + self.statep = addressof(self.state) + # Buffer for disassembled instruction text + self.buffer = create_string_buffer(256) + self.bufferp = addressof(self.buffer) + + +class LibXED(): + """Wrapper for libxed.so.""" + + def __init__(self): + try: + self.libxed = CDLL("libxed.so") + except OSError: + self.libxed = None + if not self.libxed: + try: + self.libxed = CDLL("/usr/local/lib/libxed.so") + except OSError: + self.libxed = None + + if not self.libxed: + raise ImportError("libxed.so not found. Please install Intel XED.") + + self.xed_tables_init = self.libxed.xed_tables_init + self.xed_tables_init.restype = None + self.xed_tables_init.argtypes = [] + + self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero + self.xed_decoded_inst_zero.restype = None + self.xed_decoded_inst_zero.argtypes = [c_void_p] + + self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode + self.xed_operand_values_set_mode.restype = None + self.xed_operand_values_set_mode.argtypes = [c_void_p, c_void_p] + + self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode + self.xed_decoded_inst_zero_keep_mode.restype = None + self.xed_decoded_inst_zero_keep_mode.argtypes = [c_void_p] + + self.xed_decode = self.libxed.xed_decode + self.xed_decode.restype = c_int + self.xed_decode.argtypes = [c_void_p, c_void_p, c_uint] + + self.xed_format_context = self.libxed.xed_format_context + self.xed_format_context.restype = c_uint + self.xed_format_context.argtypes = [ + c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p + ] + + self.xed_decoded_inst_get_length = self.libxed.xed_decoded_inst_get_length + self.xed_decoded_inst_get_length.restype = c_uint + self.xed_decoded_inst_get_length.argtypes = [c_void_p] + + self.xed_tables_init() + + def instruction(self): + """Create a new XEDInstruction.""" + return XEDInstruction(self) + + def set_mode(self, inst, mode): + """Set 32-bit or 64-bit mode.""" + if mode: + inst.state.mode = 4 # 32-bit + inst.state.width = 4 # 4 bytes + else: + inst.state.mode = 1 # 64-bit + inst.state.width = 8 # 8 bytes + self.xed_operand_values_set_mode(inst.xedp, inst.statep) + + def disassemble_one(self, inst, bytes_ptr, bytes_cnt, ip): + """Disassemble one instruction.""" + self.xed_decoded_inst_zero_keep_mode(inst.xedp) + err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt) + if err: + return 0, "" + # Use AT&T mode (2), alternative is Intel (3) + ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0) + if not ok: + return 0, "" + + result = inst.buffer.value.decode('utf-8') + # Return instruction length and the disassembled instruction text + return self.xed_decoded_inst_get_length(inst.xedp), result diff --git a/tools/perf/tests/shell/test_intel_pt_events_python.sh b/tools/perf/tests/shell/test_intel_pt_events_python.sh new file mode 100755 index 000000000000..0d397792ced8 --- /dev/null +++ b/tools/perf/tests/shell/test_intel_pt_events_python.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# intel-pt-events python test (exclusive) + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/intel-pt-events.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, intel-pt-events.py not found at $script_path" + exit 2 +fi + +err=0 +temp_dir="" +temp_data="" +temp_out="" + +cleanup() { + [ -n "${temp_dir}" ] && rm -rf "${temp_dir}" +} + +trap 'cleanup' EXIT TERM INT + +temp_dir=$(mktemp -d /tmp/perf.ipt.XXXXXX) +temp_data="${temp_dir}/perf.data" +temp_out="${temp_dir}/perf.out" + +test_intel_pt() { + echo "Testing intel-pt-events.py with intel_pt..." + + rm -f "${temp_data}" "${temp_out}" + # Generate some intel_pt events; use a subshell that waits for uname so + # uname's AUX buffer is flushed before the parent workload exits. + if ! perf record -B -N --no-bpf-event -e intel_pt//u -o "${temp_data}" \ + -- sh -c "uname; true" >/dev/null 2>&1; then + echo "Skipping intel_pt test, intel_pt not available." + exit 2 + fi + + # Run the script and check output + if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + echo "intel-pt-events.py test failed." + err=1 + else + if ! grep -q "Intel PT Branch Trace" "${temp_out}" || \ + ! grep -q "uname" "${temp_out}"; then + echo "Failed to find expected output: $(cat "${temp_out}")" + err=1 + else + echo "intel-pt-events test passed." + fi + fi + rm -f "${temp_out}" +} + +test_intel_pt + +exit $err From 915d6b14dc0c8971ce33f27c527d21e1ea63d906 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:08 -0700 Subject: [PATCH 216/425] perf test: Migrate Intel PT virtual LBR test to Python API Migrate the Intel PT virtual LBR test from generating an inline legacy perf script callback to using a standalone Python script (perf_brstack_max.py) with the brstack iterator API in the perf Python module. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- .../perf/tests/shell/lib/perf_brstack_max.py | 40 +++++++++++++++++ tools/perf/tests/shell/test_intel_pt.sh | 45 ++++++++----------- 2 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 tools/perf/tests/shell/lib/perf_brstack_max.py diff --git a/tools/perf/tests/shell/lib/perf_brstack_max.py b/tools/perf/tests/shell/lib/perf_brstack_max.py new file mode 100644 index 000000000000..cc065f07cfed --- /dev/null +++ b/tools/perf/tests/shell/lib/perf_brstack_max.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# SPDX-License-Identifier: GPL-2.0 +# Determine the maximum size of branch stacks in a perf.data file. + +import argparse +import sys + +import perf + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("-i", "--input", default="perf.data", help="Input file name") + args = ap.parse_args() + + bmax = 0 + + def process_event(sample): + nonlocal bmax + try: + brstack = sample.brstack + if brstack: + n = len(list(brstack)) + if n > bmax: + bmax = n + except AttributeError: + pass + + session = None + try: + session = perf.session(perf.data(args.input), sample=process_event) + session.process_events() + print("max brstack", bmax) + except Exception as e: + print(f"Error processing events: {e}", file=sys.stderr) + sys.exit(1) + finally: + session = None + +if __name__ == "__main__": + main() diff --git a/tools/perf/tests/shell/test_intel_pt.sh b/tools/perf/tests/shell/test_intel_pt.sh index a2e87ef09593..c0ecece0a825 100755 --- a/tools/perf/tests/shell/test_intel_pt.sh +++ b/tools/perf/tests/shell/test_intel_pt.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Miscellaneous Intel PT testing (exclusive) # SPDX-License-Identifier: GPL-2.0 +# Miscellaneous Intel PT testing (exclusive) set -e @@ -21,7 +21,6 @@ perfdatafile="${temp_dir}/test-perf.data" outfile="${temp_dir}/test-out.txt" errfile="${temp_dir}/test-err.txt" awkscript="${temp_dir}/awkscript" -maxbrstack="${temp_dir}/maxbrstack.py" cleanup() { @@ -375,34 +374,28 @@ test_kernel_trace() test_virtual_lbr() { echo "--- Test virtual LBR ---" - # Check if python script is supported - libpython=$(perf version --build-options | grep python | grep -cv OFF) - if [ "${libpython}" != "1" ] ; then - echo "SKIP: python scripting is not supported" + # Run setup_python.sh in a subshell first so its 'exit 2' when Python is + # not detected skips only this subtest rather than exiting test_intel_pt.sh + # (which would trigger trap_cleanup and fail the suite). + # shellcheck source=lib/setup_python.sh + if ! ( . "${shelldir}"/lib/setup_python.sh ) ; then + return 2 + fi + # shellcheck source=lib/setup_python.sh + . "${shelldir}"/lib/setup_python.sh + + if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "SKIP: Python perf module not found" return 2 fi - # Python script to determine the maximum size of branch stacks - cat << "_end_of_file_" > "${maxbrstack}" -from __future__ import print_function - -bmax = 0 - -def process_event(param_dict): - if "brstack" in param_dict: - brstack = param_dict["brstack"] - n = len(brstack) - global bmax - if n > bmax: - bmax = n - -def trace_end(): - print("max brstack", bmax) -_end_of_file_ - # Check if virtual lbr is working - perf_record_no_bpf -o "${perfdatafile}" --aux-sample -e '{intel_pt//,cycles}:u' uname - times_val=$(perf script -i "${perfdatafile}" --itrace=L -s "${maxbrstack}" 2>/dev/null | grep "max brstack " | cut -d " " -f 3) + perf_record_no_bpf -o "${tmpfile}" --aux-sample \ + -e '{intel_pt//,cycles}:u' perf test -w brstack || return 1 + perf inject --itrace=L -i "${tmpfile}" -o "${perfdatafile}" || return 1 + output=$("$PYTHON" "${shelldir}/lib/perf_brstack_max.py" -i "${perfdatafile}") || return 1 + echo "Debug: perf_brstack_max.py output: $output" + times_val=$(echo "$output" | grep "max brstack " | cut -d " " -f 3) case "${times_val}" in [0-9]*) ;; *) times_val=0;; From 62d350135e676a11775bf7b85f1ed75a476d811c Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:09 -0700 Subject: [PATCH 217/425] perf python: Port export-to-sqlite to perf module Port export-to-sqlite.py to a standalone script in tools/perf/python/ using the perf module and Python's standard library sqlite3 module. Improvements compared to the legacy script: - Remove the dependency on PySide/QtSql by using Python's built-in sqlite3 module in DatabaseExporter, allowing SQLite export on headless and minimal systems without Qt installed. - Support Intel PT and hardware instruction trace export via perf.session(itrace=...) and perf.call_return callbacks, reconstructing relational call_paths and calls tables and decoding synthesized PT payloads (ptwrite, cbr, mwait, pwre, exstop, pwrx). - Export context_switches via perf.session's context_switch callback and map thread and comm IDs to their relational database keys. - Manage temporary staging files inside an isolated tempfile.mkdtemp() directory with guaranteed cleanup in a finally block. Update Documentation/db-export.txt and add a shell test (test_export_to_sqlite_python.sh) to verify the standalone exporter. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/db-export.txt | 2 +- tools/perf/python/export-to-sqlite.py | 946 ++++++++++++++++++ .../shell/test_export_to_sqlite_python.sh | 109 ++ 3 files changed, 1056 insertions(+), 1 deletion(-) create mode 100755 tools/perf/python/export-to-sqlite.py create mode 100755 tools/perf/tests/shell/test_export_to_sqlite_python.sh diff --git a/tools/perf/Documentation/db-export.txt b/tools/perf/Documentation/db-export.txt index 52ffccb02d55..b43f12b96973 100644 --- a/tools/perf/Documentation/db-export.txt +++ b/tools/perf/Documentation/db-export.txt @@ -7,7 +7,7 @@ perf tool's python scripting engine: supports scripts: - tools/perf/scripts/python/export-to-sqlite.py + tools/perf/python/export-to-sqlite.py tools/perf/scripts/python/export-to-postgresql.py which export data to a SQLite3 or PostgreSQL database. diff --git a/tools/perf/python/export-to-sqlite.py b/tools/perf/python/export-to-sqlite.py new file mode 100755 index 000000000000..0a8087e51037 --- /dev/null +++ b/tools/perf/python/export-to-sqlite.py @@ -0,0 +1,946 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +Export perf data to a sqlite3 database. + +This script has been ported to use the modern perf Python module and the +standard library sqlite3 module. It no longer requires PySide2 or QtSql +for exporting. + +Examples of using this script with Intel PT: + + $ perf record -e intel_pt//u ls + $ python export-to-sqlite.py -i perf.data -o pt_example + +To browse the database, sqlite3 can be used e.g. + + $ sqlite3 pt_example + sqlite> .header on + sqlite> select * from samples_view where id < 10; + sqlite> .mode column + sqlite> select * from samples_view where id < 10; + sqlite> .tables + sqlite> .schema samples_view + sqlite> .quit + +An example of using the database is provided by the script +exported-sql-viewer.py. Refer to that script for details. + +Ported from tools/perf/scripts/python/export-to-sqlite.py +""" + +from __future__ import annotations +import typing +import argparse +import os +import shutil +import sqlite3 +import struct +import sys +import tempfile +from typing import Dict, Optional +import perf + + +PERF_IP_FLAG_BRANCH = 1 << 0 +PERF_IP_FLAG_CALL = 1 << 1 +PERF_IP_FLAG_RETURN = 1 << 2 +PERF_IP_FLAG_CONDITIONAL = 1 << 3 +PERF_IP_FLAG_SYSCALLRET = 1 << 4 +PERF_IP_FLAG_ASYNC = 1 << 5 +PERF_IP_FLAG_INTERRUPT = 1 << 6 +PERF_IP_FLAG_TX_ABORT = 1 << 7 +PERF_IP_FLAG_TRACE_BEGIN = 1 << 8 +PERF_IP_FLAG_TRACE_END = 1 << 9 +PERF_IP_FLAG_IN_TX = 1 << 10 +PERF_IP_FLAG_VMENTRY = 1 << 11 +PERF_IP_FLAG_VMEXIT = 1 << 12 + +BRANCH_TYPES = [ + (0, "no branch"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL, "call"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN, "return"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CONDITIONAL, "conditional jump"), + (PERF_IP_FLAG_BRANCH, "unconditional jump"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_INTERRUPT, "software interrupt"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_INTERRUPT, "return from interrupt"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_SYSCALLRET, "system call"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_SYSCALLRET, + "return from system call"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_ASYNC, "asynchronous branch"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_ASYNC | PERF_IP_FLAG_INTERRUPT, + "hardware interrupt"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TX_ABORT, "transaction abort"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_BEGIN, "trace begin"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_END, "trace end"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMENTRY, "vm entry"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMEXIT, "vm exit"), +] +for _b_type, _b_name in list(BRANCH_TYPES): + if (_b_type == PERF_IP_FLAG_BRANCH or + (_b_type & (PERF_IP_FLAG_TRACE_BEGIN | PERF_IP_FLAG_TRACE_END))): + continue + BRANCH_TYPES.append((_b_type | PERF_IP_FLAG_TRACE_BEGIN, f"trace begin / {_b_name}")) + BRANCH_TYPES.append((_b_type | PERF_IP_FLAG_TRACE_END, f"{_b_name} / trace end")) + + +class DatabaseExporter: + """Handles database connection and exporting of perf events.""" + + def __init__(self, db_path: str): + self.con = sqlite3.connect(db_path) + self.con.execute("PRAGMA journal_mode = MEMORY") + self.session: Optional[perf.session] = None + self.sample_count = 0 + + # Caches and counters grouped to reduce instance attributes + self.caches: Dict[str, dict] = { + 'machines': {}, + 'threads': {}, + 'comms': {}, + 'dsos': {}, + 'symbols': {}, + 'events': {}, + 'branch_types': {}, + 'call_paths': {} + } + + self.next_id = { + 'machine': 1, + 'thread': 1, + 'comm': 1, + 'dso': 1, + 'symbol': 1, + 'event': 1, + 'branch_type': 1, + 'call_path': 1 + } + + self.create_tables() + + def create_tables(self) -> None: + """Create database tables.""" + self.con.execute(""" + CREATE TABLE IF NOT EXISTS selected_events ( + id INTEGER NOT NULL PRIMARY KEY, + name VARCHAR(80)) + """) + self.con.execute(""" + CREATE TABLE IF NOT EXISTS machines ( + id INTEGER NOT NULL PRIMARY KEY, + pid INTEGER, + root_dir VARCHAR(4096)) + """) + self.con.execute(""" + CREATE TABLE IF NOT EXISTS threads ( + id INTEGER NOT NULL PRIMARY KEY, + machine_id BIGINT, + process_id BIGINT, + pid INTEGER, + tid INTEGER) + """) + self.con.execute(""" + CREATE TABLE IF NOT EXISTS comms ( + id INTEGER NOT NULL PRIMARY KEY, + comm VARCHAR(16), + c_thread_id BIGINT, + c_time BIGINT, + exec_flag BOOLEAN) + """) + self.con.execute(""" + CREATE TABLE IF NOT EXISTS comm_threads ( + id INTEGER NOT NULL PRIMARY KEY, + comm_id BIGINT, + thread_id BIGINT) + """) + self.con.execute(""" + CREATE TABLE IF NOT EXISTS dsos ( + id INTEGER NOT NULL PRIMARY KEY, + machine_id BIGINT, + short_name VARCHAR(256), + long_name VARCHAR(4096), + build_id VARCHAR(64)) + """) + self.con.execute(""" + CREATE TABLE IF NOT EXISTS symbols ( + id INTEGER NOT NULL PRIMARY KEY, + dso_id BIGINT, + sym_start BIGINT, + sym_end BIGINT, + binding INTEGER, + name VARCHAR(2048)) + """) + self.con.execute(""" + CREATE TABLE IF NOT EXISTS branch_types ( + id INTEGER NOT NULL PRIMARY KEY, + name VARCHAR(80)) + """) + self.con.execute(""" + CREATE TABLE IF NOT EXISTS samples ( + id INTEGER NOT NULL PRIMARY KEY, + evsel_id BIGINT, + machine_id BIGINT, + thread_id BIGINT, + comm_id BIGINT, + dso_id BIGINT, + symbol_id BIGINT, + sym_offset BIGINT, + ip BIGINT, + time BIGINT, + cpu INTEGER, + to_dso_id BIGINT, + to_symbol_id BIGINT, + to_sym_offset BIGINT, + to_ip BIGINT, + period BIGINT, + weight BIGINT, + transaction_ BIGINT, + data_src BIGINT, + branch_type INTEGER, + in_tx BOOLEAN, + call_path_id BIGINT, + insn_count BIGINT, + cyc_count BIGINT, + flags INTEGER) + """) + self.con.execute(''' + CREATE TABLE IF NOT EXISTS calls ( + id INTEGER NOT NULL PRIMARY KEY, + thread_id BIGINT, + comm_id BIGINT, + call_path_id BIGINT, + call_time BIGINT, + return_time BIGINT, + branch_count BIGINT, + call_id BIGINT, + return_id BIGINT, + parent_call_path_id BIGINT, + flags INTEGER, + parent_id BIGINT, + insn_count BIGINT, + cyc_count BIGINT + ) + ''') + self.con.execute(''' + CREATE TABLE IF NOT EXISTS call_paths ( + id INTEGER NOT NULL PRIMARY KEY, + parent_id BIGINT, + symbol_id BIGINT, + ip BIGINT + ) + ''') + + self.con.execute(''' + CREATE TABLE IF NOT EXISTS cbr ( + id integer primary key, + cbr integer, + mhz integer, + percent integer + ) + ''') + self.con.execute(''' + CREATE TABLE IF NOT EXISTS mwait ( + id integer primary key, + hints integer, + extensions integer + ) + ''') + + self.con.execute(''' + CREATE TABLE IF NOT EXISTS context_switches ( + id integer primary key, + machine_id bigint, + time bigint, + cpu integer, + thread_out_id bigint, + comm_out_id bigint, + thread_in_id bigint, + comm_in_id bigint, + flags integer + ) + ''') + self.con.execute( + "CREATE TABLE IF NOT EXISTS ptwrite (" + "id integer primary key, payload integer, exact_ip integer" + ")" + ) + self.con.execute( + "CREATE TABLE IF NOT EXISTS pwre (" + "id integer primary key, hw integer, cstate integer, subcstate integer, " + "hw_name text, cstate_name text" + ")" + ) + self.con.execute( + "CREATE TABLE IF NOT EXISTS exstop (" + "id integer primary key, exact_ip integer" + ")" + ) + self.con.execute( + "CREATE TABLE IF NOT EXISTS pwrx (" + "id integer primary key, deepest_cstate integer, last_cstate integer, " + "wake_reason integer" + ")" + ) + + self.con.execute( + "CREATE VIEW IF NOT EXISTS machines_view AS " + "SELECT id, pid, root_dir, " + "CASE WHEN id=0 THEN 'unknown' WHEN pid=-1 THEN 'host' ELSE 'guest' END " + "AS host_or_guest FROM machines" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS dsos_view AS " + "SELECT id, machine_id, " + "(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest, " + "short_name, long_name, build_id FROM dsos" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS symbols_view AS " + "SELECT id, name, (SELECT short_name FROM dsos WHERE id=dso_id) AS dso, " + "dso_id, sym_start, sym_end, " + "CASE WHEN binding=0 THEN 'local' WHEN binding=1 THEN 'global' ELSE 'weak' END " + "AS binding FROM symbols" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS threads_view AS " + "SELECT id, machine_id, " + "(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest, " + "process_id, pid, tid FROM threads" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS comm_threads_view AS " + "SELECT comm_id, (SELECT comm FROM comms WHERE id = comm_id) AS command, " + "thread_id, (SELECT pid FROM threads WHERE id = thread_id) AS pid, " + "(SELECT tid FROM threads WHERE id = thread_id) AS tid FROM comm_threads" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS call_paths_view AS " + "SELECT c.id, printf('%x', c.ip) AS ip, c.symbol_id, " + "(SELECT name FROM symbols WHERE id = c.symbol_id) AS symbol, " + "(SELECT dso_id FROM symbols WHERE id = c.symbol_id) AS dso_id, " + "(SELECT dso FROM symbols_view WHERE id = c.symbol_id) AS dso_short_name, " + "c.parent_id, printf('%x', p.ip) AS parent_ip, p.symbol_id AS parent_symbol_id, " + "(SELECT name FROM symbols WHERE id = p.symbol_id) AS parent_symbol, " + "(SELECT dso_id FROM symbols WHERE id = p.symbol_id) AS parent_dso_id, " + "(SELECT dso FROM symbols_view WHERE id = p.symbol_id) AS parent_dso_short_name " + "FROM call_paths c LEFT JOIN call_paths p ON p.id = c.parent_id" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS calls_view AS " + "SELECT calls.id, thread_id, " + "(SELECT pid FROM threads WHERE id = thread_id) AS pid, " + "(SELECT tid FROM threads WHERE id = thread_id) AS tid, " + "(SELECT comm FROM comms WHERE id = comm_id) AS command, " + "call_path_id, printf('%x', ip) AS ip, symbol_id, " + "(SELECT name FROM symbols WHERE id = symbol_id) AS symbol, " + "call_time, return_time, return_time - call_time AS elapsed_time, " + "branch_count, insn_count, cyc_count, " + "CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) " + "ELSE CAST(insn_count AS FLOAT) / cyc_count END AS IPC, " + "call_id, return_id, " + "CASE WHEN flags=0 THEN '' WHEN flags=1 THEN 'no call' " + "WHEN flags=2 THEN 'no return' WHEN flags=3 THEN 'no call/return' " + "WHEN flags IN (4, 6) THEN 'jmp' ELSE flags END AS flags, " + "parent_call_path_id, calls.parent_id " + "FROM calls INNER JOIN call_paths ON call_paths.id = call_path_id" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS samples_view AS " + "SELECT id, time, cpu, " + "(SELECT pid FROM threads WHERE id = thread_id) AS pid, " + "(SELECT tid FROM threads WHERE id = thread_id) AS tid, " + "(SELECT comm FROM comms WHERE id = comm_id) AS command, " + "(SELECT name FROM selected_events WHERE id = evsel_id) AS event, " + "printf('%x', ip) AS ip_hex, " + "(SELECT name FROM symbols WHERE id = symbol_id) AS symbol, sym_offset, " + "(SELECT short_name FROM dsos WHERE id = dso_id) AS dso_short_name, " + "printf('%x', to_ip) AS to_ip_hex, " + "(SELECT name FROM symbols WHERE id = to_symbol_id) AS to_symbol, to_sym_offset, " + "(SELECT short_name FROM dsos WHERE id = to_dso_id) AS to_dso_short_name, " + "(SELECT name FROM branch_types WHERE id = branch_type) AS branch_type_name, " + "in_tx, call_path_id, insn_count, cyc_count, " + "CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) " + "ELSE CAST(insn_count AS FLOAT) / cyc_count END AS IPC, flags FROM samples" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS ptwrite_view AS " + "SELECT ptwrite.id, time, cpu, printf('%x', payload) AS payload_hex, " + "CASE WHEN exact_ip=0 THEN 'False' ELSE 'True' END AS exact_ip " + "FROM ptwrite INNER JOIN samples ON samples.id = ptwrite.id" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS cbr_view AS " + "SELECT cbr.id, time, cpu, cbr, mhz, percent " + "FROM cbr INNER JOIN samples ON samples.id = cbr.id" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS mwait_view AS " + "SELECT mwait.id, time, cpu, printf('%x', hints) AS hints_hex, " + "printf('%x', extensions) AS extensions_hex " + "FROM mwait INNER JOIN samples ON samples.id = mwait.id" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS pwre_view AS " + "SELECT pwre.id, time, cpu, cstate, subcstate, " + "CASE WHEN hw=0 THEN 'False' ELSE 'True' END AS hw " + "FROM pwre INNER JOIN samples ON samples.id = pwre.id" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS exstop_view AS " + "SELECT exstop.id, time, cpu, " + "CASE WHEN exact_ip=0 THEN 'False' ELSE 'True' END AS exact_ip " + "FROM exstop INNER JOIN samples ON samples.id = exstop.id" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS pwrx_view AS " + "SELECT pwrx.id, time, cpu, deepest_cstate, last_cstate, " + "CASE WHEN wake_reason=1 THEN 'Interrupt' " + "WHEN wake_reason=2 THEN 'Timer Deadline' " + "WHEN wake_reason=4 THEN 'Monitored Address' " + "WHEN wake_reason=8 THEN 'HW' WHEN wake_reason=16 THEN 'Other' " + "ELSE wake_reason END AS wake_reason " + "FROM pwrx INNER JOIN samples ON samples.id = pwrx.id" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS power_events_view AS " + "SELECT samples.id, time, cpu, selected_events.name AS event, " + "CASE WHEN selected_events.name='cbr' THEN " + "(SELECT cbr FROM cbr WHERE cbr.id = samples.id) ELSE \"\" END AS cbr, " + "CASE WHEN selected_events.name='cbr' THEN " + "(SELECT mhz FROM cbr WHERE cbr.id = samples.id) ELSE \"\" END AS mhz, " + "CASE WHEN selected_events.name='cbr' THEN " + "(SELECT percent FROM cbr WHERE cbr.id = samples.id) ELSE \"\" END AS percent, " + "CASE WHEN selected_events.name='mwait' THEN " + "(SELECT printf('%x', hints) FROM mwait WHERE mwait.id = samples.id) " + "ELSE \"\" END AS hints_hex, " + "CASE WHEN selected_events.name='mwait' THEN " + "(SELECT printf('%x', extensions) FROM mwait WHERE mwait.id = samples.id) " + "ELSE \"\" END AS extensions_hex, " + "CASE WHEN selected_events.name='pwre' THEN " + "(SELECT cstate FROM pwre WHERE pwre.id = samples.id) ELSE \"\" END AS cstate, " + "CASE WHEN selected_events.name='pwre' THEN " + "(SELECT subcstate FROM pwre WHERE pwre.id = samples.id) ELSE \"\" END AS subcstate, " + "CASE WHEN selected_events.name='pwre' THEN " + "(SELECT hw FROM pwre WHERE pwre.id = samples.id) ELSE \"\" END AS hw, " + "CASE WHEN selected_events.name='exstop' THEN " + "(SELECT exact_ip FROM exstop WHERE exstop.id = samples.id) " + "ELSE \"\" END AS exact_ip, " + "CASE WHEN selected_events.name='pwrx' THEN " + "(SELECT deepest_cstate FROM pwrx WHERE pwrx.id = samples.id) " + "ELSE \"\" END AS deepest_cstate, " + "CASE WHEN selected_events.name='pwrx' THEN " + "(SELECT last_cstate FROM pwrx WHERE pwrx.id = samples.id) " + "ELSE \"\" END AS last_cstate, " + "CASE WHEN selected_events.name='pwrx' THEN " + "(SELECT wake_reason FROM pwrx WHERE pwrx.id = samples.id) " + "ELSE \"\" END AS wake_reason " + "FROM samples INNER JOIN selected_events ON selected_events.id = evsel_id " + "WHERE selected_events.name IN ('cbr','mwait','pwre','exstop','pwrx')" + ) + self.con.execute( + "CREATE VIEW IF NOT EXISTS context_switches_view AS " + "SELECT context_switches.id, context_switches.machine_id, " + "context_switches.time, context_switches.cpu, " + "th_out.pid AS pid_out, th_out.tid AS tid_out, comm_out.comm AS comm_out, " + "th_in.pid AS pid_in, th_in.tid AS tid_in, comm_in.comm AS comm_in, " + "CASE WHEN flags=0 THEN 'in' WHEN flags=1 THEN 'out' " + "WHEN flags=3 THEN 'out preempt' ELSE flags END AS flags " + "FROM context_switches " + "INNER JOIN threads AS th_out ON th_out.id = context_switches.thread_out_id " + "INNER JOIN threads AS th_in ON th_in.id = context_switches.thread_in_id " + "INNER JOIN comms AS comm_out ON comm_out.id = context_switches.comm_out_id " + "INNER JOIN comms AS comm_in ON comm_in.id = context_switches.comm_in_id" + ) + + # id == 0 means unknown. It is easier to create records for them than + # replace the zeroes with NULLs + self.con.execute("INSERT OR IGNORE INTO selected_events VALUES (0, 'unknown')") + self.con.execute("INSERT OR IGNORE INTO machines VALUES (0, 0, 'unknown')") + self.con.execute("INSERT OR IGNORE INTO threads VALUES (0, 0, 0, -1, -1)") + self.con.execute("INSERT OR IGNORE INTO comms VALUES (0, 'unknown', 0, 0, 0)") + self.con.execute("INSERT OR IGNORE INTO dsos VALUES (0, 0, 'unknown', 'unknown', '')") + self.con.execute("INSERT OR IGNORE INTO call_paths VALUES (0, 0, 0, 0)") + self.con.execute("INSERT OR IGNORE INTO symbols VALUES (0, 0, 0, 0, 0, 'unknown')") + for b_type, b_name in BRANCH_TYPES: + self.con.execute("INSERT OR IGNORE INTO branch_types VALUES (?, ?)", (b_type, b_name)) + self.caches['branch_types'][b_type] = b_name + + self.caches['events']['unknown'] = 0 + self.caches['threads'][(0, -1, -1)] = 0 + self.caches['threads'][(0, 0, 0)] = 0 + self.caches['comms'][('unknown', 0)] = 0 + self.caches['dsos'][(0, 'unknown', 'unknown', '')] = 0 + self.caches['symbols'][(0, 'unknown')] = (0, 0, 0) + # Initialize comm_threads mapping + self.comm_threads_cache: set[tuple[int, int]] = set() + self.next_comm_thread_id = 1 + + def _exec(self, sql: str, params: tuple[typing.Any, ...] = ()) -> sqlite3.Cursor: + """Execute SQL statement converting unsigned 64-bit ints to signed 64-bit. + + Python's sqlite3 module only binds signed 64-bit integers (int64_t) and + raises OverflowError for unsigned 64-bit kernel addresses (>= 2^63). + Convert values in [2^63, 2^64) into two's-complement signed 64-bit ints. + """ + conv_params = tuple( + p - 0x10000000000000000 if isinstance(p, int) and p >= 0x8000000000000000 else p + for p in params + ) + return self.con.execute(sql, conv_params) + + def get_machine_id(self, machine_pid: Optional[int]) -> int: + """Get or create machine ID.""" + if machine_pid is None or machine_pid <= 0 or machine_pid > 0x7fffffff: + machine_pid = -1 + if machine_pid in self.caches['machines']: + return self.caches['machines'][machine_pid] + machine_id = self.next_id['machine'] + self.next_id['machine'] += 1 + self._exec("INSERT INTO machines VALUES (?, ?, ?)", + (machine_id, machine_pid, "")) + self.caches['machines'][machine_pid] = machine_id + return machine_id + + def get_event_id(self, name: str) -> int: + """Get or create event ID.""" + if name in self.caches['events']: + return self.caches['events'][name] + event_id = self.next_id['event'] + self._exec("INSERT INTO selected_events VALUES (?, ?)", + (event_id, name)) + self.caches['events'][name] = event_id + self.next_id['event'] += 1 + return event_id + + def get_thread_id(self, machine_id: int, pid: Optional[int], tid: Optional[int]) -> int: + """Get or create thread ID.""" + if pid is None: + pid = -1 + elif pid > 0x7fffffff: + pid -= 0x100000000 + if tid is None: + tid = -1 + elif tid > 0x7fffffff: + tid -= 0x100000000 + key = (machine_id, pid, tid) + if key in self.caches['threads']: + return self.caches['threads'][key] + thread_id = self.next_id['thread'] + self.next_id['thread'] += 1 + self.caches['threads'][key] = thread_id + process_id = thread_id if pid == tid else self.get_thread_id(machine_id, pid, pid) + self._exec("INSERT INTO threads VALUES (?, ?, ?, ?, ?)", + (thread_id, machine_id, process_id, pid, tid)) + return thread_id + + def get_comm_id(self, comm: str, thread_id: int) -> int: + """Get or create comm ID.""" + key = (comm, thread_id) + if key not in self.caches['comms']: + comm_id = self.next_id['comm'] + self._exec("INSERT INTO comms VALUES (?, ?, ?, ?, ?)", + (comm_id, comm, thread_id, 0, 0)) + self.caches['comms'][key] = comm_id + self.next_id['comm'] += 1 + comm_id = self.caches['comms'][key] + mapping_key = (comm_id, thread_id) + if mapping_key not in self.comm_threads_cache: + comm_thread_id = self.next_comm_thread_id + self._exec("INSERT INTO comm_threads VALUES (?, ?, ?)", + (comm_thread_id, comm_id, thread_id)) + self.comm_threads_cache.add(mapping_key) + self.next_comm_thread_id += 1 + return comm_id + + def get_dso_id(self, short_name: str, long_name: str, + build_id: str, machine_id: int = 0) -> int: + """Get or create DSO ID.""" + key = (machine_id, short_name, long_name, build_id) + if key in self.caches['dsos']: + return self.caches['dsos'][key] + short_key = (machine_id, short_name) + if not build_id and short_key in self.caches['dsos']: + return self.caches['dsos'][short_key] + dso_id = self.next_id['dso'] + self._exec("INSERT INTO dsos VALUES (?, ?, ?, ?, ?)", + (dso_id, machine_id, short_name, long_name, build_id)) + self.caches['dsos'][key] = dso_id + self.caches['dsos'][short_key] = dso_id + self.next_id['dso'] += 1 + return dso_id + + def get_symbol_id(self, dso_id: int, name: str, start: int, + end: int) -> int: + """Get or create symbol ID.""" + key = (dso_id, name) + if key in self.caches['symbols']: + symbol_id, cached_start, cached_end = self.caches['symbols'][key] + if cached_start == 0 and cached_end == 0 and (start != 0 or end != 0): + self._exec("UPDATE symbols SET sym_start = ?, sym_end = ? WHERE id = ?", + (start, end, symbol_id)) + self.caches['symbols'][key] = (symbol_id, start, end) + return symbol_id + symbol_id = self.next_id['symbol'] + self._exec("INSERT INTO symbols VALUES (?, ?, ?, ?, ?, ?)", + (symbol_id, dso_id, start, end, 0, name)) + self.caches['symbols'][key] = (symbol_id, start, end) + self.next_id['symbol'] += 1 + return symbol_id + + def get_call_path_id(self, parent_id: int, symbol_id: int, + ip: int) -> int: + """Get or create call path ID.""" + key = (parent_id, symbol_id, ip) + if key in self.caches['call_paths']: + return self.caches['call_paths'][key] + call_path_id = self.next_id['call_path'] + self._exec("INSERT INTO call_paths VALUES (?, ?, ?, ?)", + (call_path_id, parent_id, symbol_id, ip)) + self.caches['call_paths'][key] = call_path_id + self.next_id['call_path'] += 1 + return call_path_id + + def process_event(self, sample: perf.sample_event) -> None: + """Callback for processing events.""" + + machine_pid = getattr(sample, 'machine_pid', 0) + machine_id = self.get_machine_id(machine_pid) + thread_id = self.get_thread_id(machine_id, sample.sample_pid, sample.sample_tid) + + comm = "Unknown_comm" + try: + if self.session is not None: + proc = self.session.find_thread(sample.sample_pid, sample.sample_tid) + if proc: + comm_name = proc.comm() + if comm_name: + comm = comm_name + except TypeError: + pass + comm_id = self.get_comm_id(comm, thread_id) + + build_id = sample.dso_bid or "" + dso_id = self.get_dso_id( + sample.dso or "Unknown_dso", + sample.dso_long_name or "Unknown_dso_long", + build_id, + machine_id + ) + + symbol_id = self.get_symbol_id( + dso_id, + sample.symbol or "Unknown_symbol", + sample.sym_start or 0, + sample.sym_end or 0 + ) + + # Handle callchain + call_path_id = 0 + if hasattr(sample, 'callchain') and sample.callchain: + parent_id = 0 + for node in reversed(sample.callchain): + dso_name = node.dso or "Unknown_dso" + symbol_name = node.symbol or "Unknown_symbol" + + node_dso_id = self.get_dso_id(dso_name, dso_name, "", machine_id) + node_symbol_id = self.get_symbol_id(node_dso_id, symbol_name, 0, 0) + + parent_id = self.get_call_path_id(parent_id, node_symbol_id, node.ip) + call_path_id = parent_id + else: + call_path_id = 0 + + # Insert sample + event_name_str = str(sample.evsel) + if event_name_str.startswith("evsel(") and event_name_str.endswith(")"): + event_name_str = event_name_str[6:-1] + cursor = self._exec(""" + INSERT INTO samples VALUES ( + NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + """, ( + self.get_event_id(event_name_str), + machine_id, thread_id, comm_id, + dso_id, symbol_id, + getattr(sample, 'sym_offset', 0) or 0, + sample.sample_ip, sample.sample_time, sample.sample_cpu, + self.get_dso_id(sample.addr_dso or "Unknown_dso", + sample.addr_dso or "Unknown_dso_long", "", machine_id), + self.get_symbol_id( + self.get_dso_id(sample.addr_dso or "Unknown_dso", + sample.addr_dso or "Unknown_dso_long", "", machine_id), + sample.addr_symbol or "Unknown_symbol", 0, 0 + ), + sample.addr_sym_offset or 0, + sample.sample_addr or 0, + sample.sample_period or 0, + sample.sample_weight or 0, + sample.transaction or 0, + sample.sample_data_src, + sample.branch_type or 0, + sample.in_tx or 0, + call_path_id, + sample.sample_insn_count, + sample.sample_cyc_count, + getattr(sample, "flags", 0) # flags + )) + sample_id = cursor.lastrowid + + # Handle Intel PT specific raw payloads mathematically equivalent to + # legacy synth_data unpacking + + if event_name_str == "ptwrite" and hasattr(sample, "raw_buf"): + try: + flags, payload = struct.unpack_from("> 32) & 0x3 + self._exec("INSERT INTO mwait VALUES (?, ?, ?)", + (sample_id, hints, extensions)) + except struct.error: + pass + + elif event_name_str == "pwre" and hasattr(sample, "raw_buf"): + try: + flags, payload = struct.unpack_from("> 7) & 1 + cstate = (payload >> 12) & 0xf + subcstate = (payload >> 8) & 0xf + self._exec("INSERT INTO pwre (id, cstate, subcstate, hw) VALUES (?, ?, ?, ?)", + (sample_id, cstate, subcstate, hw)) + except struct.error: + pass + + elif event_name_str == "exstop" and hasattr(sample, "raw_buf"): + try: + flags = struct.unpack_from("> 4) & 0xf + wake_reason = (payload >> 8) & 0xf + self._exec("INSERT INTO pwrx VALUES (?, ?, ?, ?)", + (sample_id, deepest_cstate, last_cstate, wake_reason)) + except struct.error: + pass + + self.sample_count += 1 + if self.sample_count % 10000 == 0: + self.commit() + + def get_call_path_ids(self, call_path: typing.Any, machine_id: int = 0) -> tuple[int, int]: + """Add a perf.callchain as call_paths rows, return its id and parent's.""" + parent_id = 0 + call_path_id = 0 + for node in call_path: + dso_name = node.dso or "Unknown_dso" + symbol_name = node.symbol or "Unknown_symbol" + dso_id = self.get_dso_id(dso_name, dso_name, "", machine_id) + symbol_id = self.get_symbol_id(dso_id, symbol_name, 0, 0) + parent_id = call_path_id + call_path_id = self.get_call_path_id(parent_id, symbol_id, node.ip) + return call_path_id, parent_id + + def process_call_return(self, cr: perf.call_return) -> None: + """Callback for processing call_return events.""" + machine_id = self.get_machine_id(getattr(cr, "machine_pid", None)) + thread_id = self.get_thread_id(machine_id, cr.pid, cr.tid) + comm_id = self.get_comm_id(cr.comm or "Unknown_comm", thread_id) + call_path_id, parent_call_path_id = self.get_call_path_ids(cr.call_path or [], machine_id) + # calls.id and calls.parent_id use the db_id of the perf module, as a + # call is given an id before it returns its parent cannot be numbered + # here. + self._exec( + "INSERT INTO calls VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (cr.db_id, thread_id, comm_id, call_path_id, + cr.call_time, cr.return_time, + cr.branch_count, cr.call_ref, + cr.return_ref, parent_call_path_id, + cr.flags, cr.parent_id, + cr.insn_count, cr.cyc_count) + ) + + def process_context_switch(self, event: typing.Any) -> None: + """Callback for processing context switch events.""" + misc = getattr(event, 'misc', 0) + out = bool(misc & (1 << 13)) # PERF_RECORD_MISC_SWITCH_OUT + out_preempt = bool(misc & (1 << 14)) # PERF_RECORD_MISC_SWITCH_OUT_PREEMPT + flags = (1 if out else 0) | ((1 if out_preempt else 0) << 1) + machine_pid = getattr(event, 'machine_pid', None) + machine_id = self.get_machine_id(machine_pid) + + sample_pid = getattr(event, 'sample_pid', None) + if sample_pid is None: + sample_pid = -1 + elif sample_pid > 0x7fffffff: + sample_pid -= 0x100000000 + sample_tid = getattr(event, 'sample_tid', None) + if sample_tid is None: + sample_tid = -1 + elif sample_tid > 0x7fffffff: + sample_tid -= 0x100000000 + next_prev_pid = getattr(event, 'next_prev_pid', None) + if next_prev_pid is None: + next_prev_pid = -1 + elif next_prev_pid > 0x7fffffff: + next_prev_pid -= 0x100000000 + next_prev_tid = getattr(event, 'next_prev_tid', None) + if next_prev_tid is None: + next_prev_tid = -1 + elif next_prev_tid > 0x7fffffff: + next_prev_tid -= 0x100000000 + + th_a_id = self.get_thread_id(machine_id, sample_pid, sample_tid) + comm_a = "Unknown_comm" + if self.session: + try: + proc = self.session.find_thread(sample_pid, sample_tid) + if proc: + comm_a_name = proc.comm() + if comm_a_name: + comm_a = comm_a_name + except TypeError: + pass + comm_a_id = self.get_comm_id(comm_a, th_a_id) + + th_b_id = self.get_thread_id(machine_id, next_prev_pid, next_prev_tid) + comm_b = "Unknown_comm" + if self.session: + try: + proc = self.session.find_thread(next_prev_pid, next_prev_tid) + if proc: + comm_b_name = proc.comm() + if comm_b_name: + comm_b = comm_b_name + except TypeError: + pass + comm_b_id = self.get_comm_id(comm_b, th_b_id) + + if out: + th_out_id = th_a_id + comm_out_id = comm_a_id + th_in_id = th_b_id + comm_in_id = comm_b_id + else: + th_out_id = th_b_id + comm_out_id = comm_b_id + th_in_id = th_a_id + comm_in_id = comm_a_id + + self._exec( + "INSERT INTO context_switches VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)", + (machine_id, getattr(event, 'sample_time', 0), getattr(event, 'sample_cpu', 0) or 0, + th_out_id, comm_out_id, th_in_id, comm_in_id, flags) + ) + + def finish(self) -> None: + """Create indexes and update summary metadata after processing events. + + pcpid_idx, pid_idx, and comms.has_calls are required by + exported-sql-viewer.py to filter the command dropdown to threads with + call graphs and to expand parent/child call trees efficiently. + """ + self.con.execute("CREATE INDEX IF NOT EXISTS pcpid_idx ON calls (parent_call_path_id)") + self.con.execute("CREATE INDEX IF NOT EXISTS pid_idx ON calls (parent_id)") + self.con.execute("ALTER TABLE comms ADD has_calls boolean") + self.con.execute("UPDATE comms SET has_calls = 1 WHERE comms.id IN " + "(SELECT DISTINCT comm_id FROM calls)") + + def commit(self) -> None: + """Commit transaction.""" + self.con.commit() + + def close(self) -> None: + """Close connection.""" + self.con.close() + + +if __name__ == "__main__": + ap = argparse.ArgumentParser( + description="Export perf data to a sqlite3 database") + ap.add_argument("-i", "--input", default="perf.data", + help="Input file name") + ap.add_argument("-o", "--output", default="perf.db", + help="Output database name") + ap.add_argument("--itrace", help="Instruction Tracing options (e.g. crpt)") + args = ap.parse_args() + + try: + fd = os.open(args.output, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) + except FileExistsError: + print(f"Error: {args.output} already exists") + sys.exit(1) + + tmp_dir = None + exporter = None + succeeded = False + try: + tmp_dir = tempfile.mkdtemp(prefix="perf-export-sqlite-") + tmp_db = os.path.join(tmp_dir, "perf.db") + exporter = DatabaseExporter(tmp_db) + session = perf.session(perf.data(args.input), + sample=exporter.process_event, + context_switch=exporter.process_context_switch, + call_return=exporter.process_call_return, + itrace=args.itrace) + exporter.session = session + try: + session.process_events() + finally: + # Break the C perf.session -> bound method -> exporter reference cycle. + exporter.session = None + exporter.finish() + exporter.commit() + exporter.close() + exporter = None + with open(tmp_db, "rb") as src, os.fdopen(fd, "wb", closefd=False) as dst: + shutil.copyfileobj(src, dst) + succeeded = True + print(f"Successfully exported to {args.output}") + except (OSError, RuntimeError, ValueError, sqlite3.Error): + import traceback + traceback.print_exc() + finally: + if exporter is not None: + exporter.session = None + try: + exporter.close() + except (OSError, sqlite3.Error): + pass + try: + os.close(fd) + except OSError: + pass + if tmp_dir is not None: + shutil.rmtree(tmp_dir, ignore_errors=True) + if not succeeded: + try: + os.remove(args.output) + except OSError: + pass + if not succeeded: + sys.exit(1) diff --git a/tools/perf/tests/shell/test_export_to_sqlite_python.sh b/tools/perf/tests/shell/test_export_to_sqlite_python.sh new file mode 100755 index 000000000000..59280d372399 --- /dev/null +++ b/tools/perf/tests/shell/test_export_to_sqlite_python.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# export-to-sqlite python test (exclusive) + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +# If we don't have sqlite3, we can't test +if ! "$PYTHON" -c 'import sqlite3' > /dev/null 2>&1; then + echo "Skipping test, sqlite3 module not found" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/export-to-sqlite.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, export-to-sqlite.py not found at $script_path" + exit 2 +fi + +err=0 +temp_dir="" +temp_data="" +temp_db="" + +cleanup() { + [ -n "${temp_dir}" ] && rm -rf "${temp_dir}" +} + +trap 'cleanup' EXIT +trap 'cleanup; exit 1' TERM INT + +temp_dir=$(mktemp -d /tmp/perf.sqlite.XXXXXX) +temp_data="${temp_dir}/perf.data" +temp_db="${temp_dir}/perf.export.db" + +test_file_mode() { + echo "Testing export-to-sqlite.py..." + + # Generate events with callchains and context switches if supported + if ! perf record -g --switch-events -o "${temp_data}" \ + -- perf test -w noploop >/dev/null 2>&1 && \ + ! perf record -g -o "${temp_data}" -- perf test -w noploop >/dev/null 2>&1; then + echo "Skipping test, perf record failed" + exit 2 + fi + + # Run the script + if ! "$PYTHON" "$script_path" -i "${temp_data}" -o "${temp_db}" >/dev/null; then + echo "File mode test failed." + err=1 + else + # Check DB tables (samples, call_paths, threads, comms) + query="import sqlite3; c = sqlite3.connect('${temp_db}'); " + query="${query}s = c.execute('SELECT COUNT(*) FROM samples').fetchone()[0]; " + query="${query}q = 'SELECT COUNT(*) FROM call_paths WHERE id > 0'; " + query="${query}cp = c.execute(q).fetchone()[0]; " + query="${query}exit(0 if s > 0 and cp > 0 else 1)" + if ! "$PYTHON" -c "$query" >/dev/null 2>&1; then + echo "SQLite validation failed." + err=1 + else + echo "File mode test passed." + fi + fi +} + +test_intel_pt() { + echo "Testing export-to-sqlite.py with intel_pt..." + + rm -f "${temp_db}" "${temp_data}" + # Generate some intel_pt events; use a subshell that waits for uname + if ! perf record -B -N --no-bpf-event -e intel_pt//u -o "${temp_data}" \ + -- sh -c "uname; true" >/dev/null 2>&1; then + echo "Skipping intel_pt test, intel_pt not available." + return 0 + fi + + # Run the script with --itrace cr to synthesize call_returns + if ! "$PYTHON" "$script_path" -i "${temp_data}" -o "${temp_db}" --itrace cr; then + echo "intel_pt file mode test failed." + err=1 + else + # Check DB for calls + query="import sqlite3; c = sqlite3.connect('${temp_db}'); " + query="${query}r = c.execute('SELECT COUNT(*) FROM calls').fetchone()[0]; " + query="${query}exit(1 if r == 0 else 0)" + if ! "$PYTHON" -c "$query" >/dev/null 2>&1; then + echo "SQLite intel_pt validation failed (no calls found)." + err=1 + else + echo "intel_pt test passed (cr validated)." + fi + fi +} + +test_file_mode +test_intel_pt + +exit $err From b1f968c9656a8a874f566340ae501f888e700ad0 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:10 -0700 Subject: [PATCH 218/425] perf python: Port export-to-postgresql to perf module Port export-to-postgresql.py to a standalone script in tools/perf/python/ using the perf module and libpq via ctypes. Improvements compared to the legacy script: - Remove the dependency on PySide/QtSql for database creation and DDL execution by driving libpq directly via ctypes (PQconnectdb, PQexec, PQputCopyData, PQputCopyEnd) and streaming binary PostgreSQL COPY files in PostgresExporter, enabling export on headless servers without Qt installed. - Harden database connection and SQL identifier handling by rejecting URI ('://') and connection-parameter ('=') injection strings in setup_db() and connect(), escaping single quotes and backslashes in connection strings, and quoting SQL identifiers (quote_ident). - Support Intel PT and instruction trace export via perf.session(itrace=...) and perf.call_return callbacks, reconstructing relational call_paths and calls tables (including id=0 placeholder rows and callfk/returnfk foreign keys) and unpacking synthesized PT payloads (ptwrite, cbr, mwait, pwre, exstop, pwrx). - Use a dedicated context_switch ID counter so context-switch exports do not advance sample database IDs out of sync with call_return references. Update Documentation/db-export.txt and add a shell test (test_export_to_postgresql_python.sh) to verify the standalone exporter. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/db-export.txt | 2 +- tools/perf/python/export-to-postgresql.py | 1328 +++++++++++++++++ .../shell/test_export_to_postgresql_python.sh | 122 ++ 3 files changed, 1451 insertions(+), 1 deletion(-) create mode 100755 tools/perf/python/export-to-postgresql.py create mode 100755 tools/perf/tests/shell/test_export_to_postgresql_python.sh diff --git a/tools/perf/Documentation/db-export.txt b/tools/perf/Documentation/db-export.txt index b43f12b96973..20024e1f9164 100644 --- a/tools/perf/Documentation/db-export.txt +++ b/tools/perf/Documentation/db-export.txt @@ -8,7 +8,7 @@ perf tool's python scripting engine: supports scripts: tools/perf/python/export-to-sqlite.py - tools/perf/scripts/python/export-to-postgresql.py + tools/perf/python/export-to-postgresql.py which export data to a SQLite3 or PostgreSQL database. diff --git a/tools/perf/python/export-to-postgresql.py b/tools/perf/python/export-to-postgresql.py new file mode 100755 index 000000000000..b6782129354e --- /dev/null +++ b/tools/perf/python/export-to-postgresql.py @@ -0,0 +1,1328 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +r""" +Export perf data to a postgresql database. + +This script has been ported to use the modern perf Python module and +libpq via ctypes. It no longer requires PySide2 or QtSql for exporting. + +The script assumes postgresql is running on the local machine and that the +user has postgresql permissions to create databases. + +An example of using this script with Intel PT: + + $ perf record -e intel_pt//u ls + $ python export-to-postgresql.py -i perf.data -o pt_example + +To browse the database, psql can be used e.g. + + $ psql pt_example + pt_example=# select * from samples_view where id < 100; + pt_example=# \d+ + pt_example=# \d+ samples_view + pt_example=# \q + +An example of using the database is provided by the script +exported-sql-viewer.py. Refer to that script for details. + +Tables: + + The tables largely correspond to perf tools' data structures. They are + largely self-explanatory. + + samples + 'samples' is the main table. It represents what instruction was + executing at a point in time when something (a selected event) + happened. The memory address is the instruction pointer or 'ip'. + + branch_types + 'branch_types' provides descriptions for each type of branch. + + comm_threads + 'comm_threads' shows how 'comms' relates to 'threads'. + + comms + 'comms' contains a record for each 'comm' - the name given to the + executable that is running. + + dsos + 'dsos' contains a record for each executable file or library. + + machines + 'machines' can be used to distinguish virtual machines if + virtualization is supported. + + selected_events + 'selected_events' contains a record for each kind of event that + has been sampled. + + symbols + 'symbols' contains a record for each symbol. Only symbols that + have samples are present. + + threads + 'threads' contains a record for each thread. + +Views: + + Most of the tables have views for more friendly display. The views are: + + comm_threads_view + dsos_view + machines_view + samples_view + symbols_view + threads_view + +Ported from tools/perf/scripts/python/export-to-postgresql.py +""" + +from __future__ import annotations +import typing +import argparse +from ctypes import CDLL, c_char_p, c_int, c_void_p, c_ubyte +import ctypes.util +import os +import shutil +import struct +import tempfile +import sys +from typing import Any, Dict, Optional +import perf + +# Need to access PostgreSQL C library directly to use COPY FROM STDIN +libpq_name = ctypes.util.find_library("pq") +if not libpq_name: + libpq_name = "libpq.so.5" + +try: + libpq = CDLL(libpq_name) +except OSError as e: + print(f"Error loading {libpq_name}: {e}") + print("Please ensure PostgreSQL client library is installed.") + sys.exit(1) + +PQconnectdb = libpq.PQconnectdb +PQconnectdb.restype = c_void_p +PQconnectdb.argtypes = [c_char_p] +PQfinish = libpq.PQfinish +PQfinish.argtypes = [c_void_p] +PQstatus = libpq.PQstatus +PQstatus.restype = c_int +PQstatus.argtypes = [c_void_p] +libpq.PQerrorMessage.restype = c_char_p +libpq.PQerrorMessage.argtypes = [c_void_p] +PQexec = libpq.PQexec +PQexec.restype = c_void_p +PQexec.argtypes = [c_void_p, c_char_p] +PQresultStatus = libpq.PQresultStatus +PQresultStatus.restype = c_int +PQresultStatus.argtypes = [c_void_p] +PQputCopyData = libpq.PQputCopyData +PQputCopyData.restype = c_int +PQputCopyData.argtypes = [c_void_p, c_void_p, c_int] +PQputCopyEnd = libpq.PQputCopyEnd +PQputCopyEnd.restype = c_int +PQputCopyEnd.argtypes = [c_void_p, c_void_p] +PQgetResult = libpq.PQgetResult +PQgetResult.restype = c_void_p +PQgetResult.argtypes = [c_void_p] +PQclear = libpq.PQclear +PQclear.argtypes = [c_void_p] + + +def toserverstr(s: str) -> bytes: + """Convert string to server encoding (UTF-8).""" + return bytes(s, "UTF_8") + + +def toclientstr(s: str) -> bytes: + """Convert string to client encoding (UTF-8).""" + return bytes(s, "UTF_8") + +PERF_IP_FLAG_BRANCH = 1 << 0 +PERF_IP_FLAG_CALL = 1 << 1 +PERF_IP_FLAG_RETURN = 1 << 2 +PERF_IP_FLAG_CONDITIONAL = 1 << 3 +PERF_IP_FLAG_SYSCALLRET = 1 << 4 +PERF_IP_FLAG_ASYNC = 1 << 5 +PERF_IP_FLAG_INTERRUPT = 1 << 6 +PERF_IP_FLAG_TX_ABORT = 1 << 7 +PERF_IP_FLAG_TRACE_BEGIN = 1 << 8 +PERF_IP_FLAG_TRACE_END = 1 << 9 +PERF_IP_FLAG_IN_TX = 1 << 10 +PERF_IP_FLAG_VMENTRY = 1 << 11 +PERF_IP_FLAG_VMEXIT = 1 << 12 + +BRANCH_TYPES = [ + (0, "no branch"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL, "call"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN, "return"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CONDITIONAL, "conditional jump"), + (PERF_IP_FLAG_BRANCH, "unconditional jump"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_INTERRUPT, "software interrupt"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_INTERRUPT, "return from interrupt"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_SYSCALLRET, "system call"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_SYSCALLRET, + "return from system call"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_ASYNC, "asynchronous branch"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_ASYNC | PERF_IP_FLAG_INTERRUPT, + "hardware interrupt"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TX_ABORT, "transaction abort"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_BEGIN, "trace begin"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_END, "trace end"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMENTRY, "vm entry"), + (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMEXIT, "vm exit"), +] +for _b_type, _b_name in list(BRANCH_TYPES): + if (_b_type == PERF_IP_FLAG_BRANCH or + (_b_type & (PERF_IP_FLAG_TRACE_BEGIN | PERF_IP_FLAG_TRACE_END))): + continue + BRANCH_TYPES.append((_b_type | PERF_IP_FLAG_TRACE_BEGIN, f"trace begin / {_b_name}")) + BRANCH_TYPES.append((_b_type | PERF_IP_FLAG_TRACE_END, f"{_b_name} / trace end")) + +class PostgresExporter: + """Handles PostgreSQL connection and exporting of perf events.""" + + def __init__(self, dbname: str): + self.dbname = dbname + self.conn = None + self.session: Optional[perf.session] = None + self.output_dir_name = tempfile.mkdtemp(prefix=dbname + "-perf-data-") + self.created_output_dir = True + + self.file_header = struct.pack("!11sii", b"PGCOPY\n\377\r\n\0", 0, 0) + self.file_trailer = b"\377\377" + + # Caches and counters grouped to reduce instance attributes + self.caches: Dict[str, dict] = { + 'machines': {}, + 'threads': {}, + 'comms': {}, + 'dsos': {}, + 'symbols': {}, + 'events': {}, + 'branch_types': {}, + 'call_paths': {} + } + self.symbol_rows: Dict[int, tuple[int, int, int, int, str]] = {} + + self.next_id = { + 'machine': 1, + 'thread': 1, + 'comm': 1, + 'dso': 1, + 'symbol': 1, + 'event': 1, + 'branch_type': 1, + 'comm_thread': 1, + 'call_path': 1, + 'sample': 1, + 'call': 1, + 'context_switch': 1 + } + + self.files: Dict[str, Any] = {} + self.unhandled_count = 0 + + def connect(self, db_to_use: str) -> None: + """Connect to database. + + Reject URI schemes ('://') and key=value pairs ('=') to prevent + connection parameter injection, and escape backslashes and single + quotes per libpq single-quoted conninfo string rules. + """ + if "://" in db_to_use or "=" in db_to_use: + raise ValueError(f"Invalid database name: {db_to_use}") + safe_db = db_to_use.replace('\\', '\\\\').replace("'", "\\'") + conn_str = toclientstr(f"dbname='{safe_db}'") + self.conn = PQconnectdb(conn_str) + if PQstatus(self.conn) != 0: + PQfinish(self.conn) + self.conn = None + raise RuntimeError(f"PQconnectdb failed for {db_to_use}") + + def disconnect(self) -> None: + """Disconnect from database.""" + if self.conn: + PQfinish(self.conn) + self.conn = None + + def do_query(self, sql: str) -> None: + """Execute a query and check status.""" + res = PQexec(self.conn, toserverstr(sql)) + status = PQresultStatus(res) + PQclear(res) + if status not in (1, 2): # PGRES_COMMAND_OK, PGRES_TUPLES_OK + error_msg = libpq.PQerrorMessage(self.conn).decode('utf-8') + raise RuntimeError(f"Query failed: {sql}. Error: {error_msg}") + + def open_output_file(self, file_name: str): + """Open intermediate binary file.""" + path_name = self.output_dir_name + "/" + file_name + f = open(path_name, "wb+") + f.write(self.file_header) + return f + + def close_output_file(self, f): + """Close intermediate binary file.""" + f.write(self.file_trailer) + f.close() + + def copy_output_file(self, path_name: str, table_name: str): + """Copy intermediate file to database.""" + sql = f"COPY {table_name} FROM STDIN (FORMAT 'binary')" + res = PQexec(self.conn, toserverstr(sql)) + if PQresultStatus(res) != 4: # PGRES_COPY_IN + PQclear(res) + err = libpq.PQerrorMessage(self.conn).decode() + raise RuntimeError(f"COPY FROM STDIN PQexec failed for {table_name}: {err}") + PQclear(res) + + with open(path_name, "rb") as f: + data = f.read(65536) + while len(data) > 0: + c_data = (c_ubyte * len(data)).from_buffer_copy(data) + ret = PQputCopyData(self.conn, c_data, len(data)) + if ret != 1: + raise RuntimeError(f"PQputCopyData failed for {table_name}") + data = f.read(65536) + + ret = PQputCopyEnd(self.conn, None) + if ret != 1: + err = libpq.PQerrorMessage(self.conn).decode() + raise RuntimeError(f"PQputCopyEnd failed for {table_name}: {err}") + + res = PQgetResult(self.conn) + while res: + status = PQresultStatus(res) + if status != 1: # PGRES_COMMAND_OK + error_msg = libpq.PQerrorMessage(self.conn).decode('utf-8') + PQclear(res) + raise RuntimeError( + f"COPY completion failed for {table_name}. Status {status}: {error_msg}" + ) + PQclear(res) + res = PQgetResult(self.conn) + + def setup_db(self) -> None: + """Create database and tables. MUST be called after init.""" + if "://" in self.dbname or "=" in self.dbname: + raise ValueError(f"Invalid database name: {self.dbname}") + self.created_output_dir = True + + self.connect('postgres') + try: + # SQL identifiers are double-quoted; escape embedded double quotes by doubling them. + db_name = self.dbname.replace('"', '""') + self.do_query(f'CREATE DATABASE "{db_name}"') + except Exception as e: + shutil.rmtree(self.output_dir_name, ignore_errors=True) + raise e + self.disconnect() + + self.connect(self.dbname) + self.do_query("SET client_min_messages TO WARNING") + + self.do_query(""" + CREATE TABLE selected_events ( + id bigint NOT NULL, + name varchar(80)) + """) + self.do_query(""" + CREATE TABLE machines ( + id bigint NOT NULL, + pid integer, + root_dir varchar(4096)) + """) + self.do_query(""" + CREATE TABLE threads ( + id bigint NOT NULL, + machine_id bigint, + process_id bigint, + pid integer, + tid integer) + """) + self.do_query(""" + CREATE TABLE comms ( + id bigint NOT NULL, + comm varchar(16), + c_thread_id bigint, + c_time bigint, + exec_flag boolean) + """) + self.do_query(""" + CREATE TABLE comm_threads ( + id bigint NOT NULL, + comm_id bigint, + thread_id bigint) + """) + self.do_query(""" + CREATE TABLE dsos ( + id bigint NOT NULL, + machine_id bigint, + short_name varchar(256), + long_name varchar(4096), + build_id varchar(64)) + """) + self.do_query(""" + CREATE TABLE symbols ( + id bigint NOT NULL, + dso_id bigint, + sym_start bigint, + sym_end bigint, + binding integer, + name varchar(2048)) + """) + self.do_query(""" + CREATE TABLE branch_types ( + id integer NOT NULL, + name varchar(80)) + """) + self.do_query(""" + CREATE TABLE samples ( + id bigint NOT NULL, + evsel_id bigint, + machine_id bigint, + thread_id bigint, + comm_id bigint, + dso_id bigint, + symbol_id bigint, + sym_offset bigint, + ip bigint, + time bigint, + cpu integer, + to_dso_id bigint, + to_symbol_id bigint, + to_sym_offset bigint, + to_ip bigint, + period bigint, + weight bigint, + transaction_ bigint, + data_src bigint, + branch_type integer, + in_tx boolean, + call_path_id bigint, + insn_count bigint, + cyc_count bigint, + flags integer) + """) + self.do_query(''' + CREATE TABLE context_switches ( + id bigint, + machine_id bigint, + time bigint, + cpu integer, + thread_out_id bigint, + comm_out_id bigint, + thread_in_id bigint, + comm_in_id bigint, + flags integer + ) + ''') + self.do_query(''' + CREATE TABLE call_paths ( + id bigint NOT NULL, + parent_id bigint, + symbol_id bigint, + ip bigint) + ''') + self.do_query(''' + CREATE TABLE calls ( + id bigint NOT NULL, + thread_id bigint, + comm_id bigint, + call_path_id bigint, + call_time bigint, + return_time bigint, + branch_count bigint, + call_id bigint, + return_id bigint, + parent_call_path_id bigint, + flags integer, + parent_id bigint, + insn_count bigint, + cyc_count bigint + ) + ''') + self.do_query(''' + CREATE TABLE ptwrite ( + id bigint NOT NULL, + payload bigint, + exact_ip boolean + ) + ''') + self.do_query(''' + CREATE TABLE cbr ( + id bigint NOT NULL, + cbr integer, + mhz integer, + percent integer + ) + ''') + self.do_query(''' + CREATE TABLE mwait ( + id bigint NOT NULL, + hints integer, + extensions integer + ) + ''') + self.do_query(''' + CREATE TABLE pwre ( + id bigint NOT NULL, + cstate integer, + subcstate integer, + hw boolean + ) + ''') + self.do_query(''' + CREATE TABLE exstop ( + id bigint NOT NULL, + exact_ip boolean + ) + ''') + self.do_query(''' + CREATE TABLE pwrx ( + id bigint NOT NULL, + deepest_cstate integer, + last_cstate integer, + wake_reason integer + ) + ''') + + self.files['evsel'] = self.open_output_file("evsel_table.bin") + self.files['machine'] = self.open_output_file("machine_table.bin") + self.files['thread'] = self.open_output_file("thread_table.bin") + self.files['comm'] = self.open_output_file("comm_table.bin") + self.files['comm_thread'] = self.open_output_file("comm_thread_table.bin") + self.files['dso'] = self.open_output_file("dso_table.bin") + self.files['symbol'] = self.open_output_file("symbol_table.bin") + self.files['branch_type'] = self.open_output_file("branch_type_table.bin") + self.files['sample'] = self.open_output_file("sample_table.bin") + self.files['context_switches'] = self.open_output_file("context_switches_table.bin") + self.files['call'] = self.open_output_file("call_table.bin") + self.files['call_path'] = self.open_output_file("call_path_table.bin") + self.files['ptwrite'] = self.open_output_file("ptwrite_table.bin") + self.files['cbr'] = self.open_output_file("cbr_table.bin") + self.files['mwait'] = self.open_output_file("mwait_table.bin") + self.files['pwre'] = self.open_output_file("pwre_table.bin") + self.files['exstop'] = self.open_output_file("exstop_table.bin") + self.files['pwrx'] = self.open_output_file("pwrx_table.bin") + + self.write_evsel(0, "unknown") + self.write_machine(0, 0, "unknown") + self.write_thread(0, 0, 0, -1, -1) + self.write_comm(0, "unknown", 0, 0, 0) + self.write_dso(0, 0, "unknown", "unknown", "") + self.write_symbol(0, 0, 0, 0, 0, "unknown") + self.write_call_path(0, 0, 0, 0) + # Sentinel id = 0 rows in sample_table.bin and call_table.bin satisfy + # PostgreSQL FOREIGN KEY constraints (call_id/return_id -> samples.id + # and parent references) when a call or return event has no matching + # sample reference (0). + self.files['sample'].write(struct.pack( + "!hiqiqiqiqiqiqiqiQiQiQiIiqiqiQiQiQiQiQiQiiiBiqiQiQii", + 25, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 4, 0, + 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 4, 0, 1, 0, 8, 0, 8, 0, 8, 0, 4, 0 + )) + self.files['call'].write(struct.pack( + "!hiqiqiqiqiqiqiqiqiqiqiiiqiqiq", + 14, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 4, 0, 8, 0, 8, 0, 8, 0 + )) + for b_type, b_name in BRANCH_TYPES: + self.write_branch_type(b_type, b_name) + + def write_branch_type(self, branch_type: int, name: str) -> None: + name_bytes = toserverstr(name) + n = len(name_bytes) + fmt = "!hiii" + str(n) + "s" + value = struct.pack(fmt, 2, 4, branch_type, n, name_bytes) + self.files['branch_type'].write(value) + + def write_ptwrite(self, id_: int, raw_buf: bytes) -> None: + data = struct.unpack_from(" None: + data = struct.unpack_from(" None: + data = struct.unpack_from("> 32) & 0x3 + value = struct.pack("!hiqiiii", 3, 8, id_, 4, hints, 4, extensions) + self.files['mwait'].write(value) + + def write_pwre(self, id_: int, raw_buf: bytes) -> None: + data = struct.unpack_from("> 7) & 1 + cstate = (payload >> 12) & 0xf + subcstate = (payload >> 8) & 0xf + value = struct.pack("!hiqiiiiiB", 4, 8, id_, 4, cstate, 4, subcstate, 1, hw) + self.files['pwre'].write(value) + + def write_exstop(self, id_: int, raw_buf: bytes) -> None: + data = struct.unpack_from(" None: + data = struct.unpack_from("> 4) & 0xf + wake = (payload >> 8) & 0xf + value = struct.pack("!hiqiiiiii", 4, 8, id_, 4, deepest, 4, last, 4, wake) + self.files['pwrx'].write(value) + + def write_evsel(self, evsel_id: int, name: str) -> None: + """Write event to binary file.""" + name_bytes = toserverstr(name) + n = len(name_bytes) + fmt = "!hiqi" + str(n) + "s" + value = struct.pack(fmt, 2, 8, evsel_id, n, name_bytes) + self.files['evsel'].write(value) + + def write_machine(self, machine_id: int, pid: int, root_dir: str) -> None: + """Write machine to binary file.""" + rd_bytes = toserverstr(root_dir) + n = len(rd_bytes) + fmt = "!hiqiii" + str(n) + "s" + value = struct.pack(fmt, 3, 8, machine_id, 4, pid, n, rd_bytes) + self.files['machine'].write(value) + + def write_thread(self, thread_id: int, machine_id: int, process_id: int, + pid: int, tid: int) -> None: + """Write thread to binary file.""" + value = struct.pack("!hiqiqiqiIiI", 5, 8, thread_id, 8, machine_id, + 8, process_id, 4, pid & 0xffffffff, 4, tid & 0xffffffff) + self.files['thread'].write(value) + + def write_comm(self, comm_id: int, comm_str: str, thread_id: int, + time: int, exec_flag: int) -> None: + """Write comm to binary file.""" + comm_bytes = toserverstr(comm_str) + n = len(comm_bytes) + fmt = "!hiqi" + str(n) + "s" + "iqiqiB" + value = struct.pack(fmt, 5, 8, comm_id, n, comm_bytes, 8, + thread_id, 8, time, 1, exec_flag) + self.files['comm'].write(value) + + def write_comm_thread(self, comm_thread_id: int, comm_id: int, + thread_id: int) -> None: + """Write comm_thread to binary file.""" + fmt = "!hiqiqiq" + value = struct.pack(fmt, 3, 8, comm_thread_id, 8, comm_id, 8, thread_id) + self.files['comm_thread'].write(value) + + def write_dso(self, dso_id: int, machine_id: int, short_name: str, + long_name: str, build_id: str) -> None: + """Write DSO to binary file.""" + sn_bytes = toserverstr(short_name) + ln_bytes = toserverstr(long_name) + bi_bytes = toserverstr(build_id) + n1, n2, n3 = len(sn_bytes), len(ln_bytes), len(bi_bytes) + fmt = "!hiqiqi" + str(n1) + "si" + str(n2) + "si" + str(n3) + "s" + value = struct.pack(fmt, 5, 8, dso_id, 8, machine_id, n1, + sn_bytes, n2, ln_bytes, n3, bi_bytes) + self.files['dso'].write(value) + + def write_symbol(self, symbol_id: int, dso_id: int, sym_start: int, + sym_end: int, binding: int, symbol_name: str) -> None: + """Write symbol to binary file.""" + name_bytes = toserverstr(symbol_name) + n = len(name_bytes) + fmt = "!hiqiqiQiQiii" + str(n) + "s" + value = struct.pack(fmt, 6, 8, symbol_id, 8, dso_id, 8, + sym_start, 8, sym_end, 4, binding, n, name_bytes) + self.files['symbol'].write(value) + + def write_call_path(self, cp_id: int, parent_id: int, symbol_id: int, + ip: int) -> None: + """Write call path to binary file.""" + fmt = "!hiqiqiqiQ" + value = struct.pack(fmt, 4, 8, cp_id, 8, parent_id, 8, symbol_id, 8, ip) + self.files['call_path'].write(value) + + def write_sample(self, sample_id: int, evsel_id: int, machine_id: int, thread_id: int, + comm_id: int, dso_id: int, symbol_id: int, + sample: perf.sample_event, call_path_id: int) -> None: + """Write sample to binary file.""" + addr_dso_id = self.get_dso_id( + sample.addr_dso or "Unknown_dso", sample.addr_dso or "Unknown_dso_long", "", + machine_id + ) + addr_symbol_id = self.get_symbol_id( + addr_dso_id, sample.addr_symbol or "Unknown_symbol", 0, 0 + ) + value = struct.pack( + "!hiqiqiqiqiqiqiqiQiQiQiIiqiqiQiQiQiQiQiQiiiBiqiQiQii", + 25, 8, sample_id, 8, evsel_id, 8, machine_id, 8, thread_id, 8, comm_id, + 8, dso_id, 8, symbol_id, 8, getattr(sample, 'sym_offset', 0) or 0, + 8, getattr(sample, 'sample_ip', 0) or 0, + 8, getattr(sample, 'sample_time', 0) or 0, + 4, getattr(sample, 'sample_cpu', 0) & 0xffffffff, + 8, addr_dso_id, + 8, addr_symbol_id, + 8, getattr(sample, 'addr_sym_offset', 0) or 0, + 8, getattr(sample, 'sample_addr', 0) or 0, + 8, getattr(sample, 'sample_period', 0) or 0, + 8, getattr(sample, 'sample_weight', 0) or 0, + 8, getattr(sample, 'transaction', 0) or 0, + 8, getattr(sample, 'sample_data_src', 0) or 0 or 0, + 4, getattr(sample, 'branch_type', 0) or 0, + 1, getattr(sample, 'in_tx', 0) or 0, + 8, call_path_id, + 8, getattr(sample, 'sample_insn_count', 0) or 0 or 0, + 8, getattr(sample, 'sample_cyc_count', 0) or 0 or 0, + 4, getattr(sample, 'flags', 0) or 0 + ) + self.files['sample'].write(value) + + def get_machine_id(self, machine_pid: Optional[int]) -> int: + """Get or create machine ID.""" + if machine_pid is None or machine_pid <= 0 or machine_pid > 0x7fffffff: + machine_pid = -1 + if machine_pid in self.caches['machines']: + return self.caches['machines'][machine_pid] + machine_id = self.next_id['machine'] + self.next_id['machine'] += 1 + self.write_machine(machine_id, machine_pid, "") + self.caches['machines'][machine_pid] = machine_id + return machine_id + + def get_event_id(self, name: str) -> int: + """Get or create event ID.""" + if name in self.caches['events']: + return self.caches['events'][name] + event_id = self.next_id['event'] + self.write_evsel(event_id, name) + self.caches['events'][name] = event_id + self.next_id['event'] += 1 + return event_id + + def get_thread_id(self, machine_id: int, pid: Optional[int], tid: Optional[int]) -> int: + """Get or create thread ID.""" + if pid is None: + pid = -1 + elif pid > 0x7fffffff: + pid -= 0x100000000 + if tid is None: + tid = -1 + elif tid > 0x7fffffff: + tid -= 0x100000000 + key = (machine_id, pid, tid) + if key in self.caches['threads']: + return self.caches['threads'][key] + process_id = self.get_thread_id(machine_id, pid, pid) if tid != pid else -1 + thread_id = self.next_id['thread'] + if process_id == -1: process_id = thread_id + self.write_thread(thread_id, machine_id, process_id, pid, tid) + self.caches['threads'][key] = thread_id + self.next_id['thread'] += 1 + return thread_id + + def get_comm_id(self, comm: str, thread_id: int) -> int: + """Get or create comm ID.""" + c_key = (comm, thread_id) + if c_key in self.caches['comms']: + comm_id = self.caches['comms'][c_key] + else: + comm_id = self.next_id['comm'] + self.write_comm(comm_id, comm, thread_id, 0, 0) + self.caches['comms'][c_key] = comm_id + self.next_id['comm'] += 1 + + key = (comm_id, thread_id) + if 'comm_threads' not in self.caches: + self.caches['comm_threads'] = {} + if key not in self.caches['comm_threads']: + comm_thread_id = self.next_id['comm_thread'] + self.write_comm_thread(comm_thread_id, comm_id, thread_id) + self.caches['comm_threads'][key] = True + self.next_id['comm_thread'] += 1 + + return comm_id + + def get_dso_id(self, short_name: str, long_name: str, + build_id: str, machine_id: int = 0) -> int: + """Get or create DSO ID.""" + key = (machine_id, short_name, long_name, build_id) + if key in self.caches['dsos']: + return self.caches['dsos'][key] + short_key = (machine_id, short_name) + if not build_id and short_key in self.caches['dsos']: + return self.caches['dsos'][short_key] + dso_id = self.next_id['dso'] + self.write_dso(dso_id, machine_id, short_name, long_name, build_id) + self.caches['dsos'][key] = dso_id + self.caches['dsos'][short_key] = dso_id + self.next_id['dso'] += 1 + return dso_id + + def get_symbol_id(self, dso_id: int, name: str, start: int, + end: int) -> int: + """Get or create symbol ID.""" + key = (dso_id, name) + if key in self.caches['symbols']: + symbol_id, cached_start, cached_end = self.caches['symbols'][key] + if cached_start == 0 and cached_end == 0 and (start != 0 or end != 0): + self.caches['symbols'][key] = (symbol_id, start, end) + self.symbol_rows[symbol_id] = (dso_id, start, end, 0, name) + return symbol_id + symbol_id = self.next_id['symbol'] + self.caches['symbols'][key] = (symbol_id, start, end) + self.symbol_rows[symbol_id] = (dso_id, start, end, 0, name) + self.next_id['symbol'] += 1 + return symbol_id + + def get_call_path_id(self, parent_id: int, symbol_id: int, + ip: int) -> int: + """Get or create call path ID.""" + key = (parent_id, symbol_id, ip) + if key in self.caches['call_paths']: + return self.caches['call_paths'][key] + call_path_id = self.next_id['call_path'] + self.write_call_path(call_path_id, parent_id, symbol_id, ip) + self.caches['call_paths'][key] = call_path_id + self.next_id['call_path'] += 1 + return call_path_id + + def process_context_switch(self, event: typing.Any) -> None: + """Callback for processing context switch events.""" + misc = getattr(event, 'misc', 0) + out = bool(misc & (1 << 13)) + out_preempt = bool(misc & (1 << 14)) + flags = (1 if out else 0) | ((1 if out_preempt else 0) << 1) + machine_id = self.get_machine_id(getattr(event, 'machine_pid', None)) + + sample_pid = getattr(event, 'sample_pid', None) + if sample_pid is None: + sample_pid = -1 + elif sample_pid > 0x7fffffff: + sample_pid -= 0x100000000 + sample_tid = getattr(event, 'sample_tid', None) + if sample_tid is None: + sample_tid = -1 + elif sample_tid > 0x7fffffff: + sample_tid -= 0x100000000 + next_prev_pid = getattr(event, 'next_prev_pid', None) + if next_prev_pid is None: + next_prev_pid = -1 + elif next_prev_pid > 0x7fffffff: + next_prev_pid -= 0x100000000 + next_prev_tid = getattr(event, 'next_prev_tid', None) + if next_prev_tid is None: + next_prev_tid = -1 + elif next_prev_tid > 0x7fffffff: + next_prev_tid -= 0x100000000 + + th_a_id = self.get_thread_id(machine_id, sample_pid, sample_tid) + comm_a = "Unknown_comm" + if self.session: + try: + proc = self.session.find_thread(sample_pid, sample_tid) + if proc: + comm_a_name = proc.comm() or "Unknown_comm" + if comm_a_name: + comm_a = comm_a_name + except TypeError: + pass + comm_a_id = self.get_comm_id(comm_a, th_a_id) + + th_b_id = self.get_thread_id(machine_id, next_prev_pid, next_prev_tid) + comm_b = "Unknown_comm" + if self.session: + try: + proc = self.session.find_thread(next_prev_pid, next_prev_tid) + if proc: + comm_b_name = proc.comm() or "Unknown_comm" + if comm_b_name: + comm_b = comm_b_name + except TypeError: + pass + comm_b_id = self.get_comm_id(comm_b, th_b_id) + + if out: + th_out_id = th_a_id + comm_out_id = comm_a_id + th_in_id = th_b_id + comm_in_id = comm_b_id + else: + th_out_id = th_b_id + comm_out_id = comm_b_id + th_in_id = th_a_id + comm_in_id = comm_a_id + + cs_id = self.next_id['context_switch'] + self.next_id['context_switch'] += 1 + time = getattr(event, "time", getattr(event, "sample_time", 0)) + cpu = getattr(event, "sample_cpu", 0) or 0 + if cpu > 0x7fffffff: + cpu -= 0x100000000 + + # 9 columns total: + fmt = "!hiQiqiQiiiqiqiqiqii" + value = struct.pack( + fmt, 9, 8, cs_id, 8, machine_id, 8, time, 4, cpu, + 8, th_out_id, 8, comm_out_id, 8, th_in_id, 8, comm_in_id, 4, flags + ) + self.files["context_switches"].write(value) + + def get_call_path_ids(self, call_path: typing.Any, machine_id: int = 0) -> tuple[int, int]: + """Add a perf.callchain as call_path rows, return its id and parent's.""" + parent_id = 0 + call_path_id = 0 + for node in call_path: + dso_name = node.dso or "Unknown_dso" + symbol_name = node.symbol or "Unknown_symbol" + dso_id = self.get_dso_id(dso_name, dso_name, "", machine_id) + symbol_id = self.get_symbol_id(dso_id, symbol_name, 0, 0) + parent_id = call_path_id + call_path_id = self.get_call_path_id(parent_id, symbol_id, node.ip) + return call_path_id, parent_id + + def process_call_return(self, cr: perf.call_return) -> None: + """Callback for processing call_return events.""" + machine_id = self.get_machine_id(getattr(cr, "machine_pid", 0) or 0) + thread_id = self.get_thread_id(machine_id, + cr.pid if cr.pid is not None else -1, + cr.tid if cr.tid is not None else -1) + comm_id = self.get_comm_id(cr.comm or "Unknown_comm", thread_id) + call_path_id, parent_call_path_id = self.get_call_path_ids(cr.call_path or [], machine_id) + + fmt = "!hiqiqiqiqiqiqiqiqiqiqiiiqiqiq" + # call.id and call.parent_id use the db_id of the perf module, as a call + # is given an id before it returns its parent cannot be numbered here. + value = struct.pack( + fmt, 14, 8, cr.db_id, 8, thread_id, 8, comm_id, 8, call_path_id, + 8, cr.call_time, 8, cr.return_time, 8, cr.branch_count, + 8, cr.call_ref, 8, cr.return_ref, + 8, parent_call_path_id, 4, cr.flags, 8, cr.parent_id, + 8, cr.insn_count, 8, cr.cyc_count + ) + self.files['call'].write(value) + + def process_event(self, sample: typing.Any) -> None: + """Callback for processing events.""" + + machine_db_id = getattr(sample, 'machine_pid', 0) + machine_id = self.get_machine_id(machine_db_id) + thread_id = self.get_thread_id(machine_id, sample.sample_pid, sample.sample_tid) + + comm = "Unknown_comm" + try: + if self.session is not None: + proc = self.session.find_thread(sample.sample_pid, sample.sample_tid) + if proc: + comm = proc.comm() or "Unknown_comm" + except TypeError: + pass + comm_id = self.get_comm_id(comm, thread_id) + + dso_id = self.get_dso_id( + sample.dso or "Unknown_dso", + sample.dso_long_name or "Unknown_dso_long", + sample.dso_bid or "", + machine_id + ) + + symbol_id = self.get_symbol_id( + dso_id, + sample.symbol or "Unknown_symbol", + sample.sym_start or 0, + sample.sym_end or 0 + ) + + call_path_id = 0 + if hasattr(sample, 'callchain') and sample.callchain: + parent_id = 0 + for node in reversed(sample.callchain): + node_dso = getattr(node, 'dso', None) or getattr(node, 'map', None) + node_symbol = getattr(node, 'symbol', None) or getattr(node, 'sym', None) + + dso_name = "Unknown_dso" + if node_dso: + dso_name = (node_dso if isinstance(node_dso, str) + else getattr(node_dso, 'name', "Unknown_dso") or "Unknown_dso") + + symbol_name = "Unknown_symbol" + if node_symbol: + symbol_name = (node_symbol if isinstance(node_symbol, str) + else getattr(node_symbol, 'name', "Unknown_symbol") + or "Unknown_symbol") + + node_dso_id = self.get_dso_id(dso_name, dso_name, "", machine_id) + node_symbol_id = self.get_symbol_id(node_dso_id, symbol_name, 0, 0) + + parent_id = self.get_call_path_id(parent_id, node_symbol_id, node.ip) + call_path_id = parent_id + + sample_id = self.next_id['sample'] + evsel_name = str(sample.evsel) + if evsel_name.startswith("evsel(") and evsel_name.endswith(")"): + evsel_name = evsel_name[6:-1] + self.write_sample(sample_id, + self.get_event_id(evsel_name), + machine_id, thread_id, comm_id, dso_id, symbol_id, sample, + call_path_id) + + event_name_str = evsel_name + if hasattr(sample, "raw_buf"): + if event_name_str == "ptwrite": + self.write_ptwrite(sample_id, sample.raw_buf) + elif event_name_str == "cbr": + self.write_cbr(sample_id, sample.raw_buf) + elif event_name_str == "mwait": + self.write_mwait(sample_id, sample.raw_buf) + elif event_name_str == "pwre": + self.write_pwre(sample_id, sample.raw_buf) + elif event_name_str == "exstop": + self.write_exstop(sample_id, sample.raw_buf) + elif event_name_str == "pwrx": + self.write_pwrx(sample_id, sample.raw_buf) + + self.next_id['sample'] += 1 + + def finalize(self) -> None: + """Copy files to database and add keys/views.""" + print("Copying to database...") + for symbol_id, (dso_id, sym_start, sym_end, binding, name) in self.symbol_rows.items(): + self.write_symbol(symbol_id, dso_id, sym_start, sym_end, binding, name) + for name, f in self.files.items(): + self.close_output_file(f) + + table_mapping = { + 'evsel': 'selected_events', + 'machine': 'machines', + 'thread': 'threads', + 'comm': 'comms', + 'comm_thread': 'comm_threads', + 'dso': 'dsos', + 'symbol': 'symbols', + 'branch_type': 'branch_types', + 'sample': 'samples', + 'call': 'calls', + 'call_path': 'call_paths', + 'context_switches': 'context_switches', + 'ptwrite': 'ptwrite', + 'cbr': 'cbr', + 'mwait': 'mwait', + 'pwre': 'pwre', + 'exstop': 'exstop', + 'pwrx': 'pwrx' + } + for name, f in self.files.items(): + table_name = table_mapping.get(name, name + "s") + self.copy_output_file(f.name, table_name) + + print("Removing intermediate files...") + for name, f in self.files.items(): + os.unlink(f.name) + shutil.rmtree(self.output_dir_name, ignore_errors=True) + + print("Adding primary keys") + self.do_query("ALTER TABLE selected_events ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE machines ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE threads ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE comms ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE comm_threads ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE dsos ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE symbols ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE branch_types ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE samples ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE call_paths ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE calls ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE ptwrite ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE cbr ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE mwait ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE pwre ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE exstop ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE pwrx ADD PRIMARY KEY (id)") + self.do_query("ALTER TABLE context_switches ADD PRIMARY KEY (id)") + + print("Adding foreign keys") + self.do_query('ALTER TABLE threads ' + 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id),' + 'ADD CONSTRAINT processfk FOREIGN KEY (process_id) REFERENCES threads (id)') + self.do_query('ALTER TABLE comms ' + 'ADD CONSTRAINT threadfk FOREIGN KEY (c_thread_id) REFERENCES threads (id)') + self.do_query('ALTER TABLE comm_threads ' + 'ADD CONSTRAINT commfk FOREIGN KEY (comm_id) REFERENCES comms (id),' + 'ADD CONSTRAINT threadfk FOREIGN KEY (thread_id) REFERENCES threads (id)') + self.do_query('ALTER TABLE dsos ' + 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id)') + self.do_query('ALTER TABLE symbols ' + 'ADD CONSTRAINT dsofk FOREIGN KEY (dso_id) REFERENCES dsos (id)') + self.do_query('ALTER TABLE samples ' + 'ADD CONSTRAINT evselfk FOREIGN KEY (evsel_id) ' + 'REFERENCES selected_events (id),' + 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id),' + 'ADD CONSTRAINT threadfk FOREIGN KEY (thread_id) REFERENCES threads (id),' + 'ADD CONSTRAINT commfk FOREIGN KEY (comm_id) REFERENCES comms (id),' + 'ADD CONSTRAINT dsofk FOREIGN KEY (dso_id) REFERENCES dsos (id),' + 'ADD CONSTRAINT symbolfk FOREIGN KEY (symbol_id) REFERENCES symbols (id),' + 'ADD CONSTRAINT todsofk FOREIGN KEY (to_dso_id) REFERENCES dsos (id),' + 'ADD CONSTRAINT tosymbolfk FOREIGN KEY (to_symbol_id) ' + 'REFERENCES symbols (id)') + self.do_query('ALTER TABLE call_paths ' + 'ADD CONSTRAINT parentfk FOREIGN KEY (parent_id) REFERENCES call_paths (id),' + 'ADD CONSTRAINT symbolfk FOREIGN KEY (symbol_id) REFERENCES symbols (id)') + self.do_query('ALTER TABLE calls ' + 'ADD CONSTRAINT threadfk FOREIGN KEY (thread_id) REFERENCES threads (id),' + 'ADD CONSTRAINT commfk FOREIGN KEY (comm_id) REFERENCES comms (id),' + 'ADD CONSTRAINT call_pathfk FOREIGN KEY (call_path_id) ' + 'REFERENCES call_paths (id),' + 'ADD CONSTRAINT callfk FOREIGN KEY (call_id) REFERENCES samples (id),' + 'ADD CONSTRAINT returnfk FOREIGN KEY (return_id) REFERENCES samples (id),' + 'ADD CONSTRAINT parent_call_pathfk FOREIGN KEY (parent_call_path_id) ' + 'REFERENCES call_paths (id)') + # pcpid_idx, pid_idx, and comms.has_calls are required by + # exported-sql-viewer.py to filter the command dropdown to threads with + # call graphs and to expand parent/child call trees efficiently. + self.do_query('CREATE INDEX pcpid_idx ON calls (parent_call_path_id)') + self.do_query('CREATE INDEX pid_idx ON calls (parent_id)') + self.do_query('ALTER TABLE comms ADD has_calls boolean') + self.do_query('UPDATE comms SET has_calls = TRUE WHERE comms.id IN ' + '(SELECT DISTINCT comm_id FROM calls)') + self.do_query('ALTER TABLE context_switches ' + 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id),' + 'ADD CONSTRAINT toutfk FOREIGN KEY (thread_out_id) REFERENCES threads (id),' + 'ADD CONSTRAINT tinfk FOREIGN KEY (thread_in_id) REFERENCES threads (id),' + 'ADD CONSTRAINT coutfk FOREIGN KEY (comm_out_id) REFERENCES comms (id),' + 'ADD CONSTRAINT cinfk FOREIGN KEY (comm_in_id) REFERENCES comms (id)') + self.do_query('ALTER TABLE ptwrite ' + 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') + self.do_query('ALTER TABLE cbr ' + 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') + self.do_query('ALTER TABLE mwait ' + 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') + self.do_query('ALTER TABLE pwre ' + 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') + self.do_query('ALTER TABLE exstop ' + 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') + self.do_query('ALTER TABLE pwrx ' + 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') + + print("Creating views...") + self.do_query( + "CREATE VIEW machines_view AS " + "SELECT id, pid, root_dir, " + "CASE WHEN id=0 THEN 'unknown' WHEN pid=-1 THEN 'host' ELSE 'guest' END " + "AS host_or_guest FROM machines" + ) + self.do_query( + "CREATE VIEW dsos_view AS " + "SELECT id, machine_id, " + "(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest, " + "short_name, long_name, build_id FROM dsos" + ) + self.do_query( + "CREATE VIEW symbols_view AS " + "SELECT id, name, (SELECT short_name FROM dsos WHERE id=dso_id) AS dso, " + "dso_id, sym_start, sym_end, " + "CASE WHEN binding=0 THEN 'local' WHEN binding=1 THEN 'global' ELSE 'weak' END " + "AS binding FROM symbols" + ) + self.do_query( + "CREATE VIEW threads_view AS " + "SELECT id, machine_id, " + "(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest, " + "process_id, pid, tid FROM threads" + ) + self.do_query( + "CREATE VIEW comm_threads_view AS " + "SELECT comm_id, (SELECT comm FROM comms WHERE id = comm_id) AS command, " + "thread_id, (SELECT pid FROM threads WHERE id = thread_id) AS pid, " + "(SELECT tid FROM threads WHERE id = thread_id) AS tid FROM comm_threads" + ) + self.do_query( + "CREATE VIEW call_paths_view AS " + "SELECT c.id, to_hex(c.ip) AS ip, c.symbol_id, " + "(SELECT name FROM symbols WHERE id = c.symbol_id) AS symbol, " + "(SELECT dso_id FROM symbols WHERE id = c.symbol_id) AS dso_id, " + "(SELECT dso FROM symbols_view WHERE id = c.symbol_id) AS dso_short_name, " + "c.parent_id, to_hex(p.ip) AS parent_ip, p.symbol_id AS parent_symbol_id, " + "(SELECT name FROM symbols WHERE id = p.symbol_id) AS parent_symbol, " + "(SELECT dso_id FROM symbols WHERE id = p.symbol_id) AS parent_dso_id, " + "(SELECT dso FROM symbols_view WHERE id = p.symbol_id) AS parent_dso_short_name " + "FROM call_paths c LEFT JOIN call_paths p ON p.id = c.parent_id" + ) + self.do_query( + "CREATE VIEW calls_view AS " + "SELECT calls.id, thread_id, " + "(SELECT pid FROM threads WHERE id = thread_id) AS pid, " + "(SELECT tid FROM threads WHERE id = thread_id) AS tid, " + "(SELECT comm FROM comms WHERE id = comm_id) AS command, " + "call_path_id, to_hex(ip) AS ip, symbol_id, " + "(SELECT name FROM symbols WHERE id = symbol_id) AS symbol, " + "call_time, return_time, return_time - call_time AS elapsed_time, " + "branch_count, insn_count, cyc_count, " + "CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) " + "ELSE CAST(insn_count AS FLOAT) / cyc_count END AS IPC, " + "call_id, return_id, " + "CASE WHEN flags=0 THEN '' WHEN flags=1 THEN 'no call' " + "WHEN flags=2 THEN 'no return' WHEN flags=3 THEN 'no call/return' " + "WHEN flags IN (4, 6) THEN 'jmp' ELSE CAST(flags AS VARCHAR(6)) END AS flags, " + "parent_call_path_id, calls.parent_id " + "FROM calls INNER JOIN call_paths ON call_paths.id = call_path_id" + ) + self.do_query( + "CREATE VIEW samples_view AS " + "SELECT id, time, cpu, " + "(SELECT pid FROM threads WHERE id = thread_id) AS pid, " + "(SELECT tid FROM threads WHERE id = thread_id) AS tid, " + "(SELECT comm FROM comms WHERE id = comm_id) AS command, " + "(SELECT name FROM selected_events WHERE id = evsel_id) AS event, " + "to_hex(ip) AS ip_hex, " + "(SELECT name FROM symbols WHERE id = symbol_id) AS symbol, sym_offset, " + "(SELECT short_name FROM dsos WHERE id = dso_id) AS dso_short_name, " + "to_hex(to_ip) AS to_ip_hex, " + "(SELECT name FROM symbols WHERE id = to_symbol_id) AS to_symbol, to_sym_offset, " + "(SELECT short_name FROM dsos WHERE id = to_dso_id) AS to_dso_short_name, " + "(SELECT name FROM branch_types WHERE id = branch_type) AS branch_type_name, " + "in_tx, call_path_id, insn_count, cyc_count, " + "CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) " + "ELSE CAST(insn_count AS FLOAT) / cyc_count END AS IPC, flags FROM samples" + ) + self.do_query( + "CREATE VIEW context_switches_view AS " + "SELECT context_switches.id, context_switches.machine_id, " + "context_switches.time, context_switches.cpu, " + "th_out.pid AS pid_out, th_out.tid AS tid_out, comm_out.comm AS comm_out, " + "th_in.pid AS pid_in, th_in.tid AS tid_in, comm_in.comm AS comm_in, " + "CASE WHEN flags=0 THEN 'in' WHEN flags=1 THEN 'out' " + "WHEN flags=3 THEN 'out preempt' ELSE CAST(flags AS VARCHAR(6)) END AS flags " + "FROM context_switches " + "INNER JOIN threads AS th_out ON th_out.id = context_switches.thread_out_id " + "INNER JOIN threads AS th_in ON th_in.id = context_switches.thread_in_id " + "INNER JOIN comms AS comm_out ON comm_out.id = context_switches.comm_out_id " + "INNER JOIN comms AS comm_in ON comm_in.id = context_switches.comm_in_id" + ) + self.do_query( + "CREATE VIEW ptwrite_view AS " + "SELECT ptwrite.id, time, cpu, to_hex(payload) AS payload_hex, " + "CASE WHEN exact_ip=FALSE THEN 'False' ELSE 'True' END AS exact_ip " + "FROM ptwrite INNER JOIN samples ON samples.id = ptwrite.id" + ) + self.do_query( + "CREATE VIEW cbr_view AS " + "SELECT cbr.id, time, cpu, cbr, mhz, percent " + "FROM cbr INNER JOIN samples ON samples.id = cbr.id" + ) + self.do_query( + "CREATE VIEW mwait_view AS " + "SELECT mwait.id, time, cpu, to_hex(hints) AS hints_hex, " + "to_hex(extensions) AS extensions_hex " + "FROM mwait INNER JOIN samples ON samples.id = mwait.id" + ) + self.do_query( + "CREATE VIEW pwre_view AS " + "SELECT pwre.id, time, cpu, cstate, subcstate, " + "CASE WHEN hw=FALSE THEN 'False' ELSE 'True' END AS hw " + "FROM pwre INNER JOIN samples ON samples.id = pwre.id" + ) + self.do_query( + "CREATE VIEW exstop_view AS " + "SELECT exstop.id, time, cpu, " + "CASE WHEN exact_ip=FALSE THEN 'False' ELSE 'True' END AS exact_ip " + "FROM exstop INNER JOIN samples ON samples.id = exstop.id" + ) + self.do_query( + "CREATE VIEW pwrx_view AS " + "SELECT pwrx.id, time, cpu, deepest_cstate, last_cstate, " + "CASE WHEN wake_reason=1 THEN 'Interrupt' " + "WHEN wake_reason=2 THEN 'Timer Deadline' " + "WHEN wake_reason=4 THEN 'Monitored Address' " + "WHEN wake_reason=8 THEN 'HW' WHEN wake_reason=16 THEN 'Other' " + "ELSE CAST ( wake_reason AS VARCHAR(2) ) END AS wake_reason " + "FROM pwrx INNER JOIN samples ON samples.id = pwrx.id" + ) + self.do_query( + "CREATE VIEW power_events_view AS " + "SELECT samples.id, samples.time, samples.cpu, selected_events.name AS event, " + "FORMAT('%6s', cbr.cbr) AS cbr, FORMAT('%6s', cbr.mhz) AS MHz, " + "FORMAT('%5s', cbr.percent) AS percent, to_hex(mwait.hints) AS hints_hex, " + "to_hex(mwait.extensions) AS extensions_hex, FORMAT('%3s', pwre.cstate) AS cstate, " + "FORMAT('%3s', pwre.subcstate) AS subcstate, " + "CASE WHEN pwre.hw=FALSE THEN 'False' WHEN pwre.hw=TRUE THEN 'True' " + "ELSE NULL END AS hw, " + "CASE WHEN exstop.exact_ip=FALSE THEN 'False' WHEN exstop.exact_ip=TRUE THEN 'True' " + "ELSE NULL END AS exact_ip, FORMAT('%3s', pwrx.deepest_cstate) AS deepest_cstate, " + "FORMAT('%3s', pwrx.last_cstate) AS last_cstate, " + "CASE WHEN pwrx.wake_reason=1 THEN 'Interrupt' " + "WHEN wake_reason=2 THEN 'Timer Deadline' " + "WHEN wake_reason=4 THEN 'Monitored Address' " + "WHEN wake_reason=8 THEN 'HW' WHEN wake_reason=16 THEN 'Other' " + "ELSE FORMAT('%2s', pwrx.wake_reason) END AS wake_reason " + "FROM cbr FULL JOIN mwait ON mwait.id = cbr.id " + "FULL JOIN pwre ON pwre.id = cbr.id FULL JOIN exstop ON exstop.id = cbr.id " + "FULL JOIN pwrx ON pwrx.id = cbr.id INNER JOIN samples ON samples.id = " + "coalesce(cbr.id, mwait.id, pwre.id, exstop.id, pwrx.id) " + "INNER JOIN selected_events ON selected_events.id = samples.evsel_id " + "ORDER BY samples.id" + ) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser( + description="Export perf data to a postgresql database") + ap.add_argument("-i", "--input", default="perf.data", + help="Input file name") + ap.add_argument("-o", "--output", required=True, + help="Output database name") + ap.add_argument("--itrace", default=None, + help="itrace options, e.g. cr") + args = ap.parse_args() + + exporter = PostgresExporter(args.output) + + session = None + succeeded = False + caught_error = False + try: + exporter.setup_db() + session = perf.session(perf.data(args.input), + context_switch=exporter.process_context_switch, + sample=exporter.process_event, + call_return=exporter.process_call_return, + itrace=args.itrace) + exporter.session = session + try: + session.process_events() + finally: + # Break the C perf.session -> bound method -> exporter reference cycle. + exporter.session = None + exporter.finalize() + print(f"Successfully exported to {args.output}") + succeeded = True + except (OSError, RuntimeError, ValueError, KeyboardInterrupt) as e: + if not isinstance(e, (KeyboardInterrupt, SystemExit)): + import traceback + traceback.print_exc() + caught_error = True + finally: + exporter.session = None + for out_file in exporter.files.values(): + try: + if not out_file.closed: + out_file.close() + except OSError: + pass + exporter.disconnect() + if not succeeded: + if (getattr(exporter, 'created_output_dir', False) and + os.path.exists(exporter.output_dir_name)): + shutil.rmtree(exporter.output_dir_name, ignore_errors=True) + if caught_error: + sys.exit(1) diff --git a/tools/perf/tests/shell/test_export_to_postgresql_python.sh b/tools/perf/tests/shell/test_export_to_postgresql_python.sh new file mode 100755 index 000000000000..ce13a1aa7cd7 --- /dev/null +++ b/tools/perf/tests/shell/test_export_to_postgresql_python.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# export-to-postgresql python test (exclusive) + +set -e + +shelldir=$(dirname "$0") +# shellcheck source=lib/setup_python.sh +. "${shelldir}"/lib/setup_python.sh + +if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then + echo "Skipping test, perf python module not found" + exit 2 +fi + +# If we don't have psql, we can't test +if ! command -v psql >/dev/null 2>&1; then + echo "Skipping test, psql not found" + exit 2 +fi + +# Check if we can connect to postgres and create a database +if ! psql -c "SELECT 1" postgres >/dev/null 2>&1; then + echo "Skipping test, cannot connect to postgres" + exit 2 +fi + +script_dir="$(dirname "$0")/../../python" +script_path="${script_dir}/export-to-postgresql.py" + +if [ ! -f "$script_path" ]; then + echo "Skipping test, export-to-postgresql.py not found at $script_path" + exit 2 +fi + +err=0 +temp_dir="" +temp_data="" +temp_db="" + +cleanup() { + [ -n "${temp_dir}" ] && rm -rf "${temp_dir}" + psql -c "DROP DATABASE IF EXISTS ${temp_db}" postgres >/dev/null 2>&1 || true +} + +trap 'cleanup' EXIT +trap 'cleanup; exit 1' TERM INT + +temp_dir=$(mktemp -d /tmp/perf.pg.XXXXXX) +temp_data="${temp_dir}/perf.data" +temp_db="perf_test_db_$$" + +test_file_mode() { + echo "Testing export-to-postgresql.py..." + + # Verify that invalid URI / key-value database names are rejected + if "$PYTHON" "$script_path" -i /dev/null -o "dbname=foo host=evil" >/dev/null 2>&1; then + echo "Connection parameter injection check failed." + err=1 + fi + + # Generate events with callchains and context switches + if ! perf record -g --switch-events -o "${temp_data}" \ + -- perf test -w noploop >/dev/null 2>&1 && \ + ! perf record -g -o "${temp_data}" -- perf test -w noploop >/dev/null 2>&1; then + echo "Skipping test, perf record failed" + exit 2 + fi + + # Ensure clean db start + psql -c "DROP DATABASE IF EXISTS ${temp_db}" postgres >/dev/null 2>&1 || true + + # Run the script + if ! "$PYTHON" "$script_path" -i "${temp_data}" -o "${temp_db}" >/dev/null; then + echo "File mode test failed." + err=1 + else + # Check DB + if ! psql -d "${temp_db}" -t -c 'SELECT COUNT(*) FROM samples WHERE id > 0;' | \ + grep -q '[1-9]' || \ + ! psql -d "${temp_db}" -t -c 'SELECT COUNT(*) FROM call_paths WHERE id > 0;' | \ + grep -q '[1-9]'; then + echo "PostgreSQL validation failed." + err=1 + else + echo "File mode test passed." + fi + fi +} + +test_intel_pt() { + echo "Testing export-to-postgresql.py with intel_pt..." + + psql -c "DROP DATABASE IF EXISTS ${temp_db}" postgres >/dev/null 2>&1 || true + rm -f "${temp_data}" + # Generate some intel_pt events; use a subshell that waits for uname + if ! perf record -B -N --no-bpf-event -e intel_pt//u -o "${temp_data}" \ + -- sh -c "uname; true" >/dev/null 2>&1; then + echo "Skipping intel_pt test, intel_pt not available." + return 0 + fi + + # Run the script with --itrace cr to synthesize call_returns + if ! "$PYTHON" "$script_path" -i "${temp_data}" -o "${temp_db}" --itrace cr >/dev/null; then + echo "intel_pt file mode test failed." + err=1 + else + # Check DB for calls + if ! psql -d "${temp_db}" -t -c 'SELECT COUNT(*) FROM calls WHERE id > 0;' | \ + grep -q '[1-9]'; then + echo "PostgreSQL intel_pt validation failed (no calls found)." + err=1 + else + echo "intel_pt test passed (cr validated)." + fi + fi +} + +test_file_mode +test_intel_pt + +exit $err From 3df7d85a242fb93cfa63466b8fc9fc44aad19b92 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:11 -0700 Subject: [PATCH 219/425] perf python: Move and clean up exported-sql-viewer.py Move exported-sql-viewer.py from tools/perf/scripts/python/ to tools/perf/python/ as it is a standalone PySide/PyQt GUI application for viewing SQLite and PostgreSQL databases exported by perf script. Improvements and cleanups compared to the legacy script: - Add support for PySide6 (falling back to PySide2 and PySide) loaded dynamically via importlib, and provide stub base classes (_QtBase, _QtWidget) so static analysis (mypy and pylint) and --help-only execution succeed when Qt bindings are not installed. - Fix a Python 3 NameError crash in HBoxLayout where expanded_mark and not_expanded_mark called Python 2's removed unicode() builtin instead of chr(0x25BC) and chr(0x25B6). - Replace instance-level method monkey-patching (self.mousePressEvent = self.MousePressEvent) on SwitchGraphView with proper class methods gated by self.rb_enabled so Qt virtual method dispatch works cleanly across PySide2 and PySide6. - Remove Python 2 constructs (cPickle, xrange), wildcard imports, bare 'except:' clauses, and variable names shadowing Python builtins. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/exported-sql-viewer.py | 5103 +++++++++++++++++ .../scripts/python/exported-sql-viewer.py | 5030 ---------------- 2 files changed, 5103 insertions(+), 5030 deletions(-) create mode 100755 tools/perf/python/exported-sql-viewer.py delete mode 100755 tools/perf/scripts/python/exported-sql-viewer.py diff --git a/tools/perf/python/exported-sql-viewer.py b/tools/perf/python/exported-sql-viewer.py new file mode 100755 index 000000000000..f31aa45d9a54 --- /dev/null +++ b/tools/perf/python/exported-sql-viewer.py @@ -0,0 +1,5103 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: GPL-2.0 +"""exported-sql-viewer.py: view data from sql database.""" +# Copyright (c) 2014-2018, Intel Corporation. + +# To use this script you will need to have exported data using either the +# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those +# scripts for details. +# +# Following on from the example in the export scripts, a +# call-graph can be displayed for the pt_example database like this: +# +# python exported-sql-viewer.py pt_example +# +# Note that for PostgreSQL, this script supports connecting to remote databases +# by setting hostname, port, username, password, and dbname e.g. +# +# python exported-sql-viewer.py \ +# "hostname=myhost username=myuser password=mypassword dbname=pt_example" +# +# The result is a GUI window with a tree representing a context-sensitive +# call-graph. Expanding a couple of levels of the tree and adjusting column +# widths to suit will display something like: +# +# Call Graph: pt_example +# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) +# v- ls +# v- 2638:2638 +# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 +# |- unknown unknown 1 13198 0.1 1 0.0 +# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 +# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 +# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 +# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 +# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 +# >- __libc_csu_init ls 1 10354 0.1 10 0.0 +# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 +# v- main ls 1 8182043 99.6 180254 99.9 +# +# Points to note: +# The top level is a command name (comm) +# The next level is a thread (pid:tid) +# Subsequent levels are functions +# 'Count' is the number of calls +# 'Time' is the elapsed time until the function returns +# Percentages are relative to the level above +# 'Branch Count' is the total number of branches for that function and all +# functions that it calls + +# There is also a "All branches" report, which displays branches and +# possibly disassembly. However, presently, the only supported disassembler is +# Intel XED, and additionally the object code must be present in perf build ID +# cache. To use Intel XED, libxed.so must be present. To build and install +# libxed.so: +# git clone https://github.com/intelxed/mbuild.git mbuild +# git clone https://github.com/intelxed/xed +# cd xed +# ./mfile.py --share +# sudo ./mfile.py --prefix=/usr/local install +# sudo ldconfig +# +# Example report: +# +# Time CPU Command PID TID Branch Type In Tx Branch +# 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) +# 7fab593ea260 48 89 e7 mov %rsp, %rdi +# 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) +# 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) +# 7fab593ea260 48 89 e7 mov %rsp, %rdi +# 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930 +# 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so) +# 7fab593ea930 55 pushq %rbp +# 7fab593ea931 48 89 e5 mov %rsp, %rbp +# 7fab593ea934 41 57 pushq %r15 +# 7fab593ea936 41 56 pushq %r14 +# 7fab593ea938 41 55 pushq %r13 +# 7fab593ea93a 41 54 pushq %r12 +# 7fab593ea93c 53 pushq %rbx +# 7fab593ea93d 48 89 fb mov %rdi, %rbx +# 7fab593ea940 48 83 ec 68 sub $0x68, %rsp +# 7fab593ea944 0f 31 rdtsc +# 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx +# 7fab593ea94a 89 c0 mov %eax, %eax +# 7fab593ea94c 48 09 c2 or %rax, %rdx +# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax +# 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) +# 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so) +# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax +# 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip) +# 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) + +from __future__ import print_function + +import sys +# Only change warnings if the python -W option was not used +if not sys.warnoptions: + import warnings + # PySide2 causes deprecation warnings, ignore them. + warnings.filterwarnings("ignore", category=DeprecationWarning) +import argparse +import weakref +import threading +import pickle +glb_nsz = 16 +import re +import os +import random +import copy +import math +from libxed import LibXED + +import importlib +from typing import Any + +class _QtBase: + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + def __call__(self, *args: Any, **kwargs: Any) -> Any: + return self + def __getattr__(self, name: str) -> Any: + return _QtBase() + def mousePressEvent(self, event: Any) -> None: + pass + def mouseMoveEvent(self, event: Any) -> None: + pass + def mouseReleaseEvent(self, event: Any) -> None: + pass + def resizeEvent(self, event: Any) -> None: + pass + def changeEvent(self, event: Any) -> None: + pass + +class _QtWidget(_QtBase): + pass + +class _QtMdiSubWindow(_QtWidget): + pass + +QAbstractItemModel: Any = _QtBase +QAbstractItemView: Any = _QtBase +QAbstractTableModel: Any = _QtBase +QAction: Any = _QtBase +QApplication: Any = _QtBase +QCheckBox: Any = _QtBase +QColor: Any = _QtBase +QComboBox: Any = _QtBase +QDialog: Any = _QtWidget +QEvent: Any = _QtBase +QFont: Any = _QtBase +QFontMetrics: Any = _QtBase +QGraphicsItem: Any = _QtBase +QGraphicsLineItem: Any = _QtBase +QGraphicsScene: Any = _QtBase +QGraphicsSimpleTextItem: Any = _QtBase +QGraphicsView: Any = _QtBase +QGridLayout: Any = _QtBase +QHBoxLayout: Any = _QtBase +QKeySequence: Any = _QtBase +QLabel: Any = _QtBase +QLineEdit: Any = _QtBase +QMainWindow: Any = _QtWidget +QMdiArea: Any = _QtBase +QMdiSubWindow: Any = _QtMdiSubWindow +QMenu: Any = _QtBase +QMessageBox: Any = _QtBase +QModelIndex: Any = _QtBase +QObject: Any = _QtBase +QPalette: Any = _QtBase +QPoint: Any = _QtBase +QPointF: Any = _QtBase +QProgressBar: Any = _QtBase +QPushButton: Any = _QtBase +QRect: Any = _QtBase +QRectF: Any = _QtBase +QRubberBand: Any = _QtBase +QSize: Any = _QtBase +QSizePolicy: Any = _QtBase +QSortFilterProxyModel: Any = _QtBase +QSpinBox: Any = _QtBase +QSplitter: Any = _QtWidget +QSqlDatabase: Any = _QtBase +QSqlQuery: Any = _QtBase +QStyle: Any = _QtBase +Qt: Any = _QtBase +QTableView: Any = _QtWidget +QTextBrowser: Any = _QtWidget +QThread: Any = _QtBase +QToolButton: Any = _QtBase +QTreeView: Any = _QtWidget +QVBoxLayout: Any = _QtBase +qVersion: Any = _QtBase +QWidget: Any = _QtWidget +Signal: Any = _QtBase + +pyside_version_1 = False +for _pkg in ("PySide6", "PySide2"): + try: + for _mod_name in ("QtCore", "QtGui", "QtSql", "QtWidgets"): + _mod = importlib.import_module(f"{_pkg}.{_mod_name}") + for _sym in dir(_mod): + if not _sym.startswith("_"): + globals()[_sym] = getattr(_mod, _sym) + break + except ImportError: + pass + + +from decimal import Decimal, ROUND_HALF_UP +from ctypes import create_string_buffer, addressof, sizeof, \ + c_void_p, c_bool, c_char, c_longlong +from multiprocessing import Process, Array, Value, Event + + +def printerr(*args, **keyword_args): + print(*args, file=sys.stderr, **keyword_args) + +# Data formatting helpers + +def tohex(ip): + if ip < 0: + ip += 1 << 64 + return "%x" % ip + +def offstr(offset): + if offset: + return "+0x%x" % offset + return "" + +def dsoname(name): + if name == "[kernel.kallsyms]": + return "[kernel]" + return name + +def findnth(s, sub, n, offs=0): + pos = s.find(sub) + if pos < 0: + return pos + if n <= 1: + return offs + pos + return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1) + +# Percent to one decimal place + +def PercentToOneDP(n, d): + if not d: + return "0.0" + x = (n * Decimal(100)) / d + return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) + +# Helper for queries that must not fail + +def QueryExec(query, stmt): + ret = query.exec_(stmt) + if not ret: + raise RuntimeError("Query failed: " + query.lastError().text()) + +# Background thread + +class Thread(QThread): + + done = Signal(object) + + def __init__(self, task, param=None, parent=None): + super(Thread, self).__init__(parent) + self.task = task + self.param = param + + def run(self): + while True: + if self.param is None: + done, result = self.task() + else: + done, result = self.task(self.param) + self.done.emit(result) + if done: + break + +# Tree data model + +class TreeModel(QAbstractItemModel): + + def __init__(self, glb, params, parent=None): + super(TreeModel, self).__init__(parent) + self.glb = glb + self.params = params + self.root = self.GetRoot() + self.last_row_read = 0 + + def Item(self, parent): + if parent.isValid(): + return parent.internalPointer() + else: + return self.root + + def rowCount(self, parent): + result = self.Item(parent).childCount() + if result < 0: + result = 0 + self.dataChanged.emit(parent, parent) + return result + + def hasChildren(self, parent): + return self.Item(parent).hasChildren() + + def headerData(self, section, orientation, role): + if role == Qt.TextAlignmentRole: + return self.columnAlignment(section) + if role != Qt.DisplayRole: + return None + if orientation != Qt.Horizontal: + return None + return self.columnHeader(section) + + def parent(self, child): + child_item = child.internalPointer() + if child_item is self.root: + return QModelIndex() + parent_item = child_item.getParentItem() + return self.createIndex(parent_item.getRow(), 0, parent_item) + + def index(self, row, column, parent): + child_item = self.Item(parent).getChildItem(row) + return self.createIndex(row, column, child_item) + + def DisplayData(self, item, index): + return item.getData(index.column()) + + def FetchIfNeeded(self, row): + if row > self.last_row_read: + self.last_row_read = row + if row + 10 >= self.root.child_count: + self.fetcher.Fetch(glb_chunk_sz) + + def columnAlignment(self, _column): + return Qt.AlignLeft + + def columnFont(self, _column): + return None + + def data(self, index, role): + if role == Qt.TextAlignmentRole: + return self.columnAlignment(index.column()) + if role == Qt.FontRole: + return self.columnFont(index.column()) + if role != Qt.DisplayRole: + return None + item = index.internalPointer() + return self.DisplayData(item, index) + +# Table data model + +class TableModel(QAbstractTableModel): + + def __init__(self, parent=None): + super(TableModel, self).__init__(parent) + self.child_count = 0 + self.child_items = [] + self.last_row_read = 0 + + def Item(self, parent): + if parent.isValid(): + return parent.internalPointer() + else: + return self + + def rowCount(self, _parent): + return self.child_count + + def headerData(self, section, orientation, role): + if role == Qt.TextAlignmentRole: + return self.columnAlignment(section) + if role != Qt.DisplayRole: + return None + if orientation != Qt.Horizontal: + return None + return self.columnHeader(section) + + def index(self, row, column, _parent): + return self.createIndex(row, column, self.child_items[row]) + + def DisplayData(self, item, index): + return item.getData(index.column()) + + def FetchIfNeeded(self, row): + if row > self.last_row_read: + self.last_row_read = row + if row + 10 >= self.child_count: + self.fetcher.Fetch(glb_chunk_sz) + + def columnAlignment(self, _column): + return Qt.AlignLeft + + def columnFont(self, _column): + return None + + def data(self, index, role): + if role == Qt.TextAlignmentRole: + return self.columnAlignment(index.column()) + if role == Qt.FontRole: + return self.columnFont(index.column()) + if role != Qt.DisplayRole: + return None + item = index.internalPointer() + return self.DisplayData(item, index) + +# Model cache + +model_cache: Any = weakref.WeakValueDictionary() +model_cache_lock = threading.Lock() + +def LookupCreateModel(model_name, create_fn): + model_cache_lock.acquire() + try: + model = model_cache[model_name] + except KeyError: + model = None + if model is None: + model = create_fn() + model_cache[model_name] = model + model_cache_lock.release() + return model + +def LookupModel(model_name): + model_cache_lock.acquire() + try: + model = model_cache[model_name] + except KeyError: + model = None + model_cache_lock.release() + return model + +# Find bar + +class FindBar(): + + def __init__(self, parent, finder, is_reg_expr=False): + self.finder = finder + self.context = [] + self.last_value = None + self.last_pattern = None + + label = QLabel("Find:") + label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self.textbox = QComboBox() + self.textbox.setEditable(True) + self.textbox.currentIndexChanged.connect(self.ValueChanged) + + self.progress = QProgressBar() + self.progress.setRange(0, 0) + self.progress.hide() + + if is_reg_expr: + self.pattern = QCheckBox("Regular Expression") + else: + self.pattern = QCheckBox("Pattern") + self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self.next_button = QToolButton() + self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown)) + self.next_button.released.connect(lambda: self.NextPrev(1)) + + self.prev_button = QToolButton() + self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp)) + self.prev_button.released.connect(lambda: self.NextPrev(-1)) + + self.close_button = QToolButton() + self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) + self.close_button.released.connect(self.Deactivate) + + self.hbox = QHBoxLayout() + self.hbox.setContentsMargins(0, 0, 0, 0) + + self.hbox.addWidget(label) + self.hbox.addWidget(self.textbox) + self.hbox.addWidget(self.progress) + self.hbox.addWidget(self.pattern) + self.hbox.addWidget(self.next_button) + self.hbox.addWidget(self.prev_button) + self.hbox.addWidget(self.close_button) + + self.bar = QWidget() + self.bar.setLayout(self.hbox) + self.bar.hide() + + def Widget(self): + return self.bar + + def Activate(self): + self.bar.show() + self.textbox.lineEdit().selectAll() + self.textbox.setFocus() + + def Deactivate(self): + self.bar.hide() + + def Busy(self): + self.textbox.setEnabled(False) + self.pattern.hide() + self.next_button.hide() + self.prev_button.hide() + self.progress.show() + + def Idle(self): + self.textbox.setEnabled(True) + self.progress.hide() + self.pattern.show() + self.next_button.show() + self.prev_button.show() + + def Find(self, direction): + value = self.textbox.currentText() + pattern = self.pattern.isChecked() + self.last_value = value + self.last_pattern = pattern + self.finder.Find(value, direction, pattern, self.context) + + def ValueChanged(self): + _value = self.textbox.currentText() + pattern = self.pattern.isChecked() + index = self.textbox.currentIndex() + data = self.textbox.itemData(index) + # Store the pattern in the combo box to keep it with the text value + if data == None: + self.textbox.setItemData(index, pattern) + else: + self.pattern.setChecked(data) + self.Find(0) + + def NextPrev(self, direction): + value = self.textbox.currentText() + pattern = self.pattern.isChecked() + if value != self.last_value: + index = self.textbox.findText(value) + # Allow for a button press before the value has been added to the combo box + if index < 0: + index = self.textbox.count() + self.textbox.addItem(value, pattern) + self.textbox.setCurrentIndex(index) + return + else: + self.textbox.setItemData(index, pattern) + elif pattern != self.last_pattern: + # Keep the pattern recorded in the combo box up to date + index = self.textbox.currentIndex() + self.textbox.setItemData(index, pattern) + self.Find(direction) + + def NotFound(self): + QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found") + +# Context-sensitive call graph data model item base + +class CallGraphLevelItemBase(object): + def Select(self) -> None: + pass + def data(self, _column: int, _role: int) -> Any: + return None + + def __init__(self, glb, params, row, parent_item): + self.glb = glb + self.params = params + self.row = row + self.parent_item = parent_item + self.query_done = False + self.child_count = 0 + self.child_items = [] + if parent_item: + self.level = parent_item.level + 1 + else: + self.level = 0 + + def getChildItem(self, row): + return self.child_items[row] + + def getParentItem(self): + return self.parent_item + + def getRow(self): + return self.row + + def childCount(self): + if not self.query_done: + self.Select() + if not self.child_count: + return -1 + return self.child_count + + def hasChildren(self): + if not self.query_done: + return True + return self.child_count > 0 + + def getData(self, column): + return self.data[column] + +# Context-sensitive call graph data model level 2+ item base + +class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): + + def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item): + super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) + self.comm_id = comm_id + self.thread_id = thread_id + self.call_path_id = call_path_id + self.insn_cnt = insn_cnt + self.cyc_cnt = cyc_cnt + self.branch_count = branch_count + self.time = time + + def Select(self): + self.query_done = True + query = QSqlQuery(self.glb.db) + if self.params.have_ipc: + ipc_str = ", SUM(insn_count), SUM(cyc_count)" + else: + ipc_str = "" + QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)" + " FROM calls" + " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" + " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" + " INNER JOIN dsos ON symbols.dso_id = dsos.id" + " WHERE parent_call_path_id = " + str(self.call_path_id) + + " AND comm_id = " + str(self.comm_id) + + " AND thread_id = " + str(self.thread_id) + + " GROUP BY call_path_id, name, short_name" + " ORDER BY call_path_id") + while query.next(): + if self.params.have_ipc: + insn_cnt = int(query.value(5)) + cyc_cnt = int(query.value(6)) + branch_count = int(query.value(7)) + else: + insn_cnt = 0 + cyc_cnt = 0 + branch_count = int(query.value(5)) + child_item = CallGraphLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self) + self.child_items.append(child_item) + self.child_count += 1 + +# Context-sensitive call graph data model level three item + +class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): + + def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item): + super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item) + dso = dsoname(dso) + if self.params.have_ipc: + insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) + cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) + br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) + ipc = CalcIPC(cyc_cnt, insn_cnt) + self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ] + else: + self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] + self.dbid = call_path_id + +# Context-sensitive call graph data model level two item + +class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): + + def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): + super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item) + if self.params.have_ipc: + self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] + else: + self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] + self.dbid = thread_id + + def Select(self): + super(CallGraphLevelTwoItem, self).Select() + for child_item in self.child_items: + self.time += child_item.time + self.insn_cnt += child_item.insn_cnt + self.cyc_cnt += child_item.cyc_cnt + self.branch_count += child_item.branch_count + for child_item in self.child_items: + child_item.data[4] = PercentToOneDP(child_item.time, self.time) + if self.params.have_ipc: + child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) + child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) + child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) + else: + child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) + +# Context-sensitive call graph data model level one item + +class CallGraphLevelOneItem(CallGraphLevelItemBase): + + def __init__(self, glb, params, row, comm_id, comm, parent_item): + super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item) + if self.params.have_ipc: + self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] + else: + self.data = [comm, "", "", "", "", "", ""] + self.dbid = comm_id + + def Select(self): + self.query_done = True + query = QSqlQuery(self.glb.db) + QueryExec(query, "SELECT thread_id, pid, tid" + " FROM comm_threads" + " INNER JOIN threads ON thread_id = threads.id" + " WHERE comm_id = " + str(self.dbid)) + while query.next(): + child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) + self.child_items.append(child_item) + self.child_count += 1 + +# Context-sensitive call graph data model root item + +class CallGraphRootItem(CallGraphLevelItemBase): + + def __init__(self, glb, params): + super(CallGraphRootItem, self).__init__(glb, params, 0, None) + self.dbid = 0 + self.query_done = True + if_has_calls = "" + if IsSelectable(glb.db, "comms", columns = "has_calls"): + if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE + query = QSqlQuery(glb.db) + QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) + while query.next(): + if not query.value(0): + continue + child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) + self.child_items.append(child_item) + self.child_count += 1 + +# Call graph model parameters + +class CallGraphModelParams(): + + def __init__(self, glb, _parent=None): + self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count") + +# Context-sensitive call graph data model base + +class CallGraphModelBase(TreeModel): + + def __init__(self, glb, parent=None): + super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent) + + def FindSelect(self, value, pattern, query): + if pattern: + # postgresql and sqlite pattern patching differences: + # postgresql LIKE is case sensitive but sqlite LIKE is not + # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not + # postgresql supports ILIKE which is case insensitive + # sqlite supports GLOB (text only) which uses * and ? and is case sensitive + if not self.glb.dbref.is_sqlite3: + # Escape % and _ + s = value.replace("%", "\\%") + s = s.replace("_", "\\_") + # Translate * and ? into SQL LIKE pattern characters % and _ + if sys.version_info[0] == 3: + trans = str.maketrans("*?", "%_") + else: + trans = bytes.maketrans("*?", "%_") + match = " LIKE '" + str(s).translate(trans) + "'" + else: + match = " GLOB '" + str(value) + "'" + else: + match = " = '" + str(value) + "'" + self.DoFindSelect(query, match) + + def Found(self, query, found): + if found: + return self.FindPath(query) + return [] + + def FindValue(self, value, pattern, query, last_value, last_pattern): + if last_value == value and pattern == last_pattern: + found = query.first() + else: + self.FindSelect(value, pattern, query) + found = query.next() + return self.Found(query, found) + + def FindNext(self, query): + found = query.next() + if not found: + found = query.first() + return self.Found(query, found) + + def FindPrev(self, query): + found = query.previous() + if not found: + found = query.last() + return self.Found(query, found) + + def FindThread(self, c): + if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern: + ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern) + elif c.direction > 0: + ids = self.FindNext(c.query) + else: + ids = self.FindPrev(c.query) + return (True, ids) + + def Find(self, value, direction, pattern, context, callback): + class Context(): + def __init__(self, *x): + self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x + def Update(self, *x): + self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern) + if len(context): + context[0].Update(value, direction, pattern) + else: + context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None)) + # Use a thread so the UI is not blocked during the SELECT + thread = Thread(self.FindThread, context[0]) + thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection) + thread.start() + + def FindDone(self, _thread, callback, ids): + callback(ids) + +# Context-sensitive call graph data model + +class CallGraphModel(CallGraphModelBase): + + def GetRoot(self): + return CallGraphRootItem(self.glb, self.params) + + def columnCount(self, _parent=None): + if self.params.have_ipc: + return 12 + else: + return 7 + + def columnHeader(self, column): + if self.params.have_ipc: + headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] + else: + headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] + return headers[column] + + def columnAlignment(self, column): + if self.params.have_ipc: + alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] + else: + alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] + return alignment[column] + + def DoFindSelect(self, query, match): + QueryExec(query, "SELECT call_path_id, comm_id, thread_id" + " FROM calls" + " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" + " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" + " WHERE calls.id <> 0" + " AND symbols.name" + match + + " GROUP BY comm_id, thread_id, call_path_id" + " ORDER BY comm_id, thread_id, call_path_id") + + def FindPath(self, query): + # Turn the query result into a list of ids that the tree view can walk + # to open the tree at the right place. + ids = [] + parent_id = query.value(0) + while parent_id: + ids.insert(0, parent_id) + q2 = QSqlQuery(self.glb.db) + QueryExec(q2, "SELECT parent_id" + " FROM call_paths" + " WHERE id = " + str(parent_id)) + if not q2.next(): + break + parent_id = q2.value(0) + # The call path root is not used + if ids[0] == 1: + del ids[0] + ids.insert(0, query.value(2)) + ids.insert(0, query.value(1)) + return ids + +# Call tree data model level 2+ item base + +class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase): + + def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item): + super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) + self.comm_id = comm_id + self.thread_id = thread_id + self.calls_id = calls_id + self.call_time = call_time + self.time = time + self.insn_cnt = insn_cnt + self.cyc_cnt = cyc_cnt + self.branch_count = branch_count + + def Select(self): + self.query_done = True + if self.calls_id == 0: + comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id) + else: + comm_thread = "" + if self.params.have_ipc: + ipc_str = ", insn_count, cyc_count" + else: + ipc_str = "" + query = QSqlQuery(self.glb.db) + QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count" + " FROM calls" + " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" + " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" + " INNER JOIN dsos ON symbols.dso_id = dsos.id" + " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread + + " ORDER BY call_time, calls.id") + while query.next(): + if self.params.have_ipc: + insn_cnt = int(query.value(5)) + cyc_cnt = int(query.value(6)) + branch_count = int(query.value(7)) + else: + insn_cnt = 0 + cyc_cnt = 0 + branch_count = int(query.value(5)) + child_item = CallTreeLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self) + self.child_items.append(child_item) + self.child_count += 1 + +# Call tree data model level three item + +class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase): + + def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item): + super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item) + dso = dsoname(dso) + if self.params.have_ipc: + insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) + cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) + br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) + ipc = CalcIPC(cyc_cnt, insn_cnt) + self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ] + else: + self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] + self.dbid = calls_id + +# Call tree data model level two item + +class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase): + + def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): + super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item) + if self.params.have_ipc: + self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] + else: + self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] + self.dbid = thread_id + + def Select(self): + super(CallTreeLevelTwoItem, self).Select() + for child_item in self.child_items: + self.time += child_item.time + self.insn_cnt += child_item.insn_cnt + self.cyc_cnt += child_item.cyc_cnt + self.branch_count += child_item.branch_count + for child_item in self.child_items: + child_item.data[4] = PercentToOneDP(child_item.time, self.time) + if self.params.have_ipc: + child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) + child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) + child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) + else: + child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) + +# Call tree data model level one item + +class CallTreeLevelOneItem(CallGraphLevelItemBase): + + def __init__(self, glb, params, row, comm_id, comm, parent_item): + super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item) + if self.params.have_ipc: + self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] + else: + self.data = [comm, "", "", "", "", "", ""] + self.dbid = comm_id + + def Select(self): + self.query_done = True + query = QSqlQuery(self.glb.db) + QueryExec(query, "SELECT thread_id, pid, tid" + " FROM comm_threads" + " INNER JOIN threads ON thread_id = threads.id" + " WHERE comm_id = " + str(self.dbid)) + while query.next(): + child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) + self.child_items.append(child_item) + self.child_count += 1 + +# Call tree data model root item + +class CallTreeRootItem(CallGraphLevelItemBase): + + def __init__(self, glb, params): + super(CallTreeRootItem, self).__init__(glb, params, 0, None) + self.dbid = 0 + self.query_done = True + if_has_calls = "" + if IsSelectable(glb.db, "comms", columns = "has_calls"): + if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE + query = QSqlQuery(glb.db) + QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) + while query.next(): + if not query.value(0): + continue + child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) + self.child_items.append(child_item) + self.child_count += 1 + +# Call Tree data model + +class CallTreeModel(CallGraphModelBase): + + def GetRoot(self): + return CallTreeRootItem(self.glb, self.params) + + def columnCount(self, _parent=None): + if self.params.have_ipc: + return 12 + else: + return 7 + + def columnHeader(self, column): + if self.params.have_ipc: + headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] + else: + headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] + return headers[column] + + def columnAlignment(self, column): + if self.params.have_ipc: + alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] + else: + alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] + return alignment[column] + + def DoFindSelect(self, query, match): + QueryExec(query, "SELECT calls.id, comm_id, thread_id" + " FROM calls" + " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" + " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" + " WHERE calls.id <> 0" + " AND symbols.name" + match + + " ORDER BY comm_id, thread_id, call_time, calls.id") + + def FindPath(self, query): + # Turn the query result into a list of ids that the tree view can walk + # to open the tree at the right place. + ids = [] + parent_id = query.value(0) + while parent_id: + ids.insert(0, parent_id) + q2 = QSqlQuery(self.glb.db) + QueryExec(q2, "SELECT parent_id" + " FROM calls" + " WHERE id = " + str(parent_id)) + if not q2.next(): + break + parent_id = q2.value(0) + ids.insert(0, query.value(2)) + ids.insert(0, query.value(1)) + return ids + +# Vertical layout + +class HBoxLayout(QHBoxLayout): + + def __init__(self, *children): + super(HBoxLayout, self).__init__() + + self.layout().setContentsMargins(0, 0, 0, 0) + for child in children: + if child.isWidgetType(): + self.layout().addWidget(child) + else: + self.layout().addLayout(child) + +# Horizontal layout + +class VBoxLayout(QVBoxLayout): + + def __init__(self, *children): + super(VBoxLayout, self).__init__() + + self.layout().setContentsMargins(0, 0, 0, 0) + for child in children: + if child.isWidgetType(): + self.layout().addWidget(child) + else: + self.layout().addLayout(child) + +# Vertical layout widget + +class VBox(): + + def __init__(self, *children): + self.vbox = QWidget() + self.vbox.setLayout(VBoxLayout(*children)) + + def Widget(self): + return self.vbox + +# Tree window base + +class TreeWindowBase(QMdiSubWindow): + + def __init__(self, parent=None): + super(TreeWindowBase, self).__init__(parent) + + self.model = None + self.find_bar = None + + self.view = QTreeView() + self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) + self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard + + self.context_menu = TreeContextMenu(self.view) + + def DisplayFound(self, ids): + if not len(ids): + return False + parent = QModelIndex() + for dbid in ids: + found = False + n = self.model.rowCount(parent) + for row in range(n): + child = self.model.index(row, 0, parent) + if child.internalPointer().dbid == dbid: + found = True + self.view.setExpanded(parent, True) + self.view.setCurrentIndex(child) + parent = child + break + if not found: + break + return found + + def Find(self, value, direction, pattern, context): + self.view.setFocus() + self.find_bar.Busy() + self.model.Find(value, direction, pattern, context, self.FindDone) + + def FindDone(self, ids): + found = True + if not self.DisplayFound(ids): + found = False + self.find_bar.Idle() + if not found: + self.find_bar.NotFound() + + +# Context-sensitive call graph window + +class CallGraphWindow(TreeWindowBase): + + def __init__(self, glb, parent=None): + super(CallGraphWindow, self).__init__(parent) + + self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) + + self.view.setModel(self.model) + + for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): + self.view.setColumnWidth(c, w) + + self.find_bar = FindBar(self, self) + + self.vbox = VBox(self.view, self.find_bar.Widget()) + + self.setWidget(self.vbox.Widget()) + + AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") + +# Call tree window + +class CallTreeWindow(TreeWindowBase): + + def __init__(self, glb, parent=None, thread_at_time=None): + super(CallTreeWindow, self).__init__(parent) + + self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x)) + + self.view.setModel(self.model) + + for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)): + self.view.setColumnWidth(c, w) + + self.find_bar = FindBar(self, self) + + self.vbox = VBox(self.view, self.find_bar.Widget()) + + self.setWidget(self.vbox.Widget()) + + AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree") + + if thread_at_time: + self.DisplayThreadAtTime(*thread_at_time) + + def DisplayThreadAtTime(self, comm_id, thread_id, time): + parent = QModelIndex() + for dbid in (comm_id, thread_id): + found = False + n = self.model.rowCount(parent) + for row in range(n): + child = self.model.index(row, 0, parent) + if child.internalPointer().dbid == dbid: + found = True + self.view.setExpanded(parent, True) + self.view.setCurrentIndex(child) + parent = child + break + if not found: + return + found = False + while True: + n = self.model.rowCount(parent) + if not n: + return + last_child = None + for row in range(n): + self.view.setExpanded(parent, True) + child = self.model.index(row, 0, parent) + child_call_time = child.internalPointer().call_time + if child_call_time < time: + last_child = child + elif child_call_time == time: + self.view.setCurrentIndex(child) + return + elif child_call_time > time: + break + if not last_child: + if not found: + child = self.model.index(0, 0, parent) + self.view.setExpanded(parent, True) + self.view.setCurrentIndex(child) + return + found = True + self.view.setExpanded(parent, True) + self.view.setCurrentIndex(last_child) + parent = last_child + +# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name + +def ExecComm(db, thread_id, time): + query = QSqlQuery(db) + QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag" + " FROM comm_threads" + " INNER JOIN comms ON comms.id = comm_threads.comm_id" + " WHERE comm_threads.thread_id = " + str(thread_id) + + " ORDER BY comms.c_time, comms.id") + first = None + last = None + while query.next(): + if first is None: + first = query.value(0) + if query.value(2) and Decimal(query.value(1)) <= Decimal(time): + last = query.value(0) + if not(last is None): + return last + return first + +# Container for (x, y) data + +class XY(): + def __init__(self, x=0, y=0): + self.x = x + self.y = y + + def __str__(self): + return "XY({}, {})".format(str(self.x), str(self.y)) + +# Container for sub-range data + +class Subrange(): + def __init__(self, lo=0, hi=0): + self.lo = lo + self.hi = hi + + def __str__(self): + return "Subrange({}, {})".format(str(self.lo), str(self.hi)) + +# Graph data region base class + +class GraphDataRegion(object): + + def __init__(self, key, title = "", ordinal = ""): + self.key = key + self.title = title + self.ordinal = ordinal + +# Function to sort GraphDataRegion + +def GraphDataRegionOrdinal(data_region): + return data_region.ordinal + +# Attributes for a graph region + +class GraphRegionAttribute(): + + def __init__(self, colour): + self.colour = colour + +# Switch graph data region represents a task + +class SwitchGraphDataRegion(GraphDataRegion): + + def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id): + super(SwitchGraphDataRegion, self).__init__(key) + + self.title = str(pid) + " / " + str(tid) + " " + comm + # Order graph legend within exec comm by pid / tid / time + self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16) + self.exec_comm_id = exec_comm_id + self.pid = pid + self.tid = tid + self.comm = comm + self.thread_id = thread_id + self.comm_id = comm_id + +# Graph data point + +class GraphDataPoint(): + + def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None): + self.data = data + self.index = index + self.x = x + self.y = y + self.altx = altx + self.alty = alty + self.hregion = hregion + self.vregion = vregion + +# Graph data (single graph) base class + +class GraphData(object): + + def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)): + self.collection = collection + self.points = [] + self.xbase = xbase + self.ybase = ybase + self.title = "" + + def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None): + index = len(self.points) + + x = float(Decimal(x) - self.xbase) + y = float(Decimal(y) - self.ybase) + + self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion)) + + def XToData(self, x): + return Decimal(x) + self.xbase + + def YToData(self, y): + return Decimal(y) + self.ybase + +# Switch graph data (for one CPU) + +class SwitchGraphData(GraphData): + + def __init__(self, db, collection, cpu, xbase): + super(SwitchGraphData, self).__init__(collection, xbase) + + self.cpu = cpu + self.title = "CPU " + str(cpu) + self.SelectSwitches(db) + + def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time): + query = QSqlQuery(db) + QueryExec(query, "SELECT id, c_time" + " FROM comms" + " WHERE c_thread_id = " + str(thread_id) + + " AND exec_flag = " + self.collection.glb.dbref.TRUE + + " AND c_time >= " + str(start_time) + + " AND c_time <= " + str(end_time) + + " ORDER BY c_time, id") + while query.next(): + comm_id = query.value(0) + if comm_id == last_comm_id: + continue + time = query.value(1) + hregion = self.HRegion(db, thread_id, comm_id, time) + self.AddPoint(time, 1000, None, None, hregion) + + def SelectSwitches(self, db): + last_time = None + last_comm_id = None + last_thread_id = None + query = QSqlQuery(db) + QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags" + " FROM context_switches" + " WHERE machine_id = " + str(self.collection.machine_id) + + " AND cpu = " + str(self.cpu) + + " ORDER BY time, id") + while query.next(): + flags = int(query.value(5)) + if flags & 1: + # Schedule-out: detect and add exec's + if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3): + self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0)) + continue + # Schedule-in: add data point + if len(self.points) == 0: + start_time = self.collection.glb.StartTime(self.collection.machine_id) + hregion = self.HRegion(db, query.value(1), query.value(3), start_time) + self.AddPoint(start_time, 1000, None, None, hregion) + time = query.value(0) + comm_id = query.value(4) + thread_id = query.value(2) + hregion = self.HRegion(db, thread_id, comm_id, time) + self.AddPoint(time, 1000, None, None, hregion) + last_time = time + last_comm_id = comm_id + last_thread_id = thread_id + + def NewHRegion(self, db, key, thread_id, comm_id, time): + exec_comm_id = ExecComm(db, thread_id, time) + query = QSqlQuery(db) + QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id)) + if query.next(): + pid = query.value(0) + tid = query.value(1) + else: + pid = -1 + tid = -1 + query = QSqlQuery(db) + QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id)) + if query.next(): + comm = query.value(0) + else: + comm = "" + return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id) + + def HRegion(self, db, thread_id, comm_id, time): + key = str(thread_id) + ":" + str(comm_id) + hregion = self.collection.LookupHRegion(key) + if hregion is None: + hregion = self.NewHRegion(db, key, thread_id, comm_id, time) + self.collection.AddHRegion(key, hregion) + return hregion + +# Graph data collection (multiple related graphs) base class + +class GraphDataCollection(object): + + def __init__(self, glb): + self.glb = glb + self.data = [] + self.hregions = {} + self.xrangelo = None + self.xrangehi = None + self.yrangelo = None + self.yrangehi = None + self.dp = XY(0, 0) + + def AddGraphData(self, data): + self.data.append(data) + + def LookupHRegion(self, key): + if key in self.hregions: + return self.hregions[key] + return None + + def AddHRegion(self, key, hregion): + self.hregions[key] = hregion + +# Switch graph data collection (SwitchGraphData for each CPU) + +class SwitchGraphDataCollection(GraphDataCollection): + + def __init__(self, glb, db, machine_id): + super(SwitchGraphDataCollection, self).__init__(glb) + + self.machine_id = machine_id + self.cpus = self.SelectCPUs(db) + + self.xrangelo = glb.StartTime(machine_id) + self.xrangehi = glb.FinishTime(machine_id) + + self.yrangelo = Decimal(0) + self.yrangehi = Decimal(1000) + + for cpu in self.cpus: + self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo)) + + def SelectCPUs(self, db): + cpus = [] + query = QSqlQuery(db) + QueryExec(query, "SELECT DISTINCT cpu" + " FROM context_switches" + " WHERE machine_id = " + str(self.machine_id)) + while query.next(): + cpus.append(int(query.value(0))) + return sorted(cpus) + +# Switch graph data graphics item displays the graphed data + +class SwitchGraphDataGraphicsItem(QGraphicsItem): + + def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None): + super(SwitchGraphDataGraphicsItem, self).__init__(parent) + + self.data = data + self.graph_width = graph_width + self.graph_height = graph_height + self.attrs = attrs + self.event_handler = event_handler + self.setAcceptHoverEvents(True) + + def boundingRect(self): + return QRectF(0, 0, self.graph_width, self.graph_height) + + def PaintPoint(self, painter, last, x): + if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo): + if last.x < self.attrs.subrange.x.lo: + x0 = self.attrs.subrange.x.lo + else: + x0 = last.x + if x > self.attrs.subrange.x.hi: + x1 = self.attrs.subrange.x.hi + else: + x1 = x - 1 + x0 = self.attrs.XToPixel(x0) + x1 = self.attrs.XToPixel(x1) + + y0 = self.attrs.YToPixel(last.y) + + colour = self.attrs.region_attributes[last.hregion.key].colour + + width = x1 - x0 + 1 + if width < 2: + painter.setPen(colour) + painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height) + else: + painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour) + + def paint(self, painter, _option, _widget): + last = None + for point in self.data.points: + self.PaintPoint(painter, last, point.x) + if point.x > self.attrs.subrange.x.hi: + break + last = point + self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1) + + def BinarySearchPoint(self, target): + lower_pos = 0 + higher_pos = len(self.data.points) + while True: + pos = int((lower_pos + higher_pos) / 2) + val = self.data.points[pos].x + if target >= val: + lower_pos = pos + else: + higher_pos = pos + if higher_pos <= lower_pos + 1: + return lower_pos + + def XPixelToData(self, x): + x = self.attrs.PixelToX(x) + if x < self.data.points[0].x: + x = 0 + pos = 0 + low = True + else: + pos = self.BinarySearchPoint(x) + low = False + return (low, pos, self.data.XToData(x)) + + def EventToData(self, event): + no_data = (None,) * 4 + if len(self.data.points) < 1: + return no_data + x = event.pos().x() + if x < 0: + return no_data + _low0, pos0, time_from = self.XPixelToData(x) + low1, pos1, time_to = self.XPixelToData(x + 1) + hregions = set() + hregion_times = [] + if not low1: + for i in range(pos0, pos1 + 1): + hregion = self.data.points[i].hregion + hregions.add(hregion) + if i == pos0: + time = time_from + else: + time = self.data.XToData(self.data.points[i].x) + hregion_times.append((hregion, time)) + return (time_from, time_to, hregions, hregion_times) + + def hoverMoveEvent(self, event): + time_from, time_to, hregions, _hregion_times = self.EventToData(event) + if time_from is not None: + self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions) + + def hoverLeaveEvent(self, _event): + self.event_handler.NoPointEvent() + + def mousePressEvent(self, event): + if event.button() != Qt.RightButton: + super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event) + return + _time_from, _time_to, _hregions, hregion_times = self.EventToData(event) + if hregion_times: + self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos()) + +# X-axis graphics item + +class XAxisGraphicsItem(QGraphicsItem): + + def __init__(self, width, parent=None): + super(XAxisGraphicsItem, self).__init__(parent) + + self.width = width + self.max_mark_sz = 4 + self.height = self.max_mark_sz + 1 + + def boundingRect(self): + return QRectF(0, 0, self.width, self.height) + + def Step(self): + attrs = self.parentItem().attrs + subrange = attrs.subrange.x + t = subrange.hi - subrange.lo + s = (3.0 * t) / self.width + n = 1.0 + while s > n: + n = n * 10.0 + return n + + def PaintMarks(self, painter, at_y, lo, hi, step, i): + attrs = self.parentItem().attrs + x = lo + while x <= hi: + xp = attrs.XToPixel(x) + if i % 10: + if i % 5: + sz = 1 + else: + sz = 2 + else: + sz = self.max_mark_sz + i = 0 + painter.drawLine(xp, at_y, xp, at_y + sz) + x += step + i += 1 + + def paint(self, painter, _option, _widget): + # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1 + painter.drawLine(0, 0, self.width - 1, 0) + n = self.Step() + attrs = self.parentItem().attrs + subrange = attrs.subrange.x + if subrange.lo: + x_offset = n - (subrange.lo % n) + else: + x_offset = 0.0 + x = subrange.lo + x_offset + i = (x / n) % 10 + self.PaintMarks(painter, 0, x, subrange.hi, n, i) + + def ScaleDimensions(self): + n = self.Step() + attrs = self.parentItem().attrs + lo = attrs.subrange.x.lo + hi = (n * 10.0) + lo + width = attrs.XToPixel(hi) + if width > 500: + width = 0 + return (n, lo, hi, width) + + def PaintScale(self, painter, at_x, at_y): + n, lo, hi, width = self.ScaleDimensions() + if not width: + return + painter.drawLine(at_x, at_y, at_x + width, at_y) + self.PaintMarks(painter, at_y, lo, hi, n, 0) + + def ScaleWidth(self): + _n, _lo, _hi, width = self.ScaleDimensions() + return width + + def ScaleHeight(self): + return self.height + + def ScaleUnit(self): + return self.Step() * 10 + +# Scale graphics item base class + +class ScaleGraphicsItem(QGraphicsItem): + + def __init__(self, axis, parent=None): + super(ScaleGraphicsItem, self).__init__(parent) + self.axis = axis + + def boundingRect(self): + scale_width = self.axis.ScaleWidth() + if not scale_width: + return QRectF() + return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight()) + + def paint(self, painter, _option, _widget): + scale_width = self.axis.ScaleWidth() + if not scale_width: + return + self.axis.PaintScale(painter, 0, 5) + x = scale_width + 4 + painter.drawText(QPointF(x, 10), self.Text()) + + def Unit(self): + return self.axis.ScaleUnit() + + def Text(self): + return "" + +# Switch graph scale graphics item + +class SwitchScaleGraphicsItem(ScaleGraphicsItem): + + def Text(self): + unit = self.Unit() + if unit >= 1000000000: + unit = int(unit / 1000000000) + us = "s" + elif unit >= 1000000: + unit = int(unit / 1000000) + us = "ms" + elif unit >= 1000: + unit = int(unit / 1000) + us = "us" + else: + unit = int(unit) + us = "ns" + return " = " + str(unit) + " " + us + +# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data + +class SwitchGraphGraphicsItem(QGraphicsItem): + + def __init__(self, collection, data, attrs, event_handler, first, parent=None): + super(SwitchGraphGraphicsItem, self).__init__(parent) + self.collection = collection + self.data = data + self.attrs = attrs + self.event_handler = event_handler + + margin = 20 + title_width = 50 + + self.title_graphics = QGraphicsSimpleTextItem(data.title, self) + + self.title_graphics.setPos(margin, margin) + graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1 + graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1 + + self.graph_origin_x = margin + title_width + margin + self.graph_origin_y = graph_height + margin + + _x_axis_size = 1 + y_axis_size = 1 + self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self) + + self.x_axis = XAxisGraphicsItem(graph_width, self) + self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1) + + if first: + self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self) + self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10) + + self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height) + + self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self) + self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1) + + self.width = self.graph_origin_x + graph_width + margin + self.height = self.graph_origin_y + margin + + self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self) + self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height) + + if parent and 'EnableRubberBand' in dir(parent): + parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self) + + def boundingRect(self): + return QRectF(0, 0, self.width, self.height) + + def paint(self, painter, option, widget): + pass + + def RBXToPixel(self, x): + return self.attrs.PixelToX(x - self.graph_origin_x) + + def RBXRangeToPixel(self, x0, x1): + return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1)) + + def RBPixelToTime(self, x): + if x < self.data.points[0].x: + return self.data.XToData(0) + return self.data.XToData(x) + + def RBEventTimes(self, x0, x1): + x0, x1 = self.RBXRangeToPixel(x0, x1) + time_from = self.RBPixelToTime(x0) + time_to = self.RBPixelToTime(x1) + return (time_from, time_to) + + def RBEvent(self, x0, x1): + time_from, time_to = self.RBEventTimes(x0, x1) + self.event_handler.RangeEvent(time_from, time_to) + + def RBMoveEvent(self, x0, x1): + if x1 < x0: + x0, x1 = x1, x0 + self.RBEvent(x0, x1) + + def RBReleaseEvent(self, x0, x1, selection_state): + if x1 < x0: + x0, x1 = x1, x0 + x0, x1 = self.RBXRangeToPixel(x0, x1) + self.event_handler.SelectEvent(x0, x1, selection_state) + +# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range) + +class VerticalBracketGraphicsItem(QGraphicsItem): + + def __init__(self, parent=None): + super(VerticalBracketGraphicsItem, self).__init__(parent) + + self.width = 0 + self.height = 0 + self.hide() + + def SetSize(self, width, height): + self.width = width + 1 + self.height = height + 1 + + def boundingRect(self): + return QRectF(0, 0, self.width, self.height) + + def paint(self, painter, _option, _widget): + colour = QColor(255, 255, 0, 32) + painter.fillRect(0, 0, self.width, self.height, colour) + x1 = self.width - 1 + y1 = self.height - 1 + painter.drawLine(0, 0, x1, 0) + painter.drawLine(0, 0, 0, 3) + painter.drawLine(x1, 0, x1, 3) + painter.drawLine(0, y1, x1, y1) + painter.drawLine(0, y1, 0, y1 - 3) + painter.drawLine(x1, y1, x1, y1 - 3) + +# Graphics item to contain graphs arranged vertically + +class VertcalGraphSetGraphicsItem(QGraphicsItem): + + def __init__(self, collection, attrs, event_handler, child_class, parent=None): + super(VertcalGraphSetGraphicsItem, self).__init__(parent) + + self.collection = collection + + self.top = 10 + + self.width = 0 + self.height = self.top + + self.rubber_band = None + self.rb_enabled = False + self.rb_in_view = False + self.rb_xlo = 0 + self.rb_xhi = 0 + self.rb_event_handler = None + self.rb_origin = None + + first = True + for data in collection.data: + child = child_class(collection, data, attrs, event_handler, first, self) + child.setPos(0, self.height + 1) + rect = child.boundingRect() + if rect.right() > self.width: + self.width = rect.right() + self.height = self.height + rect.bottom() + 1 + first = False + + self.bracket = VerticalBracketGraphicsItem(self) + + def EnableRubberBand(self, xlo, xhi, rb_event_handler): + if self.rb_enabled: + return + self.rb_enabled = True + self.rb_in_view = False + self.setAcceptedMouseButtons(Qt.LeftButton) + self.rb_xlo = xlo + self.rb_xhi = xhi + self.rb_event_handler = rb_event_handler + + def mousePressEvent(self, event): + if self.rb_enabled: + self.MousePressEvent(event) + + def mouseMoveEvent(self, event): + if self.rb_enabled: + self.MouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + if self.rb_enabled: + self.MouseReleaseEvent(event) + + def boundingRect(self): + return QRectF(0, 0, self.width, self.height) + + def paint(self, painter, option, widget): + pass + + def RubberBandParent(self): + scene = self.scene() + view = scene.views()[0] + viewport = view.viewport() + return viewport + + def RubberBandSetGeometry(self, rect): + scene_rectf = self.mapRectToScene(QRectF(rect)) + scene = self.scene() + view = scene.views()[0] + poly = view.mapFromScene(scene_rectf) + self.rubber_band.setGeometry(poly.boundingRect()) + + def SetSelection(self, selection_state): + if self.rubber_band: + if selection_state: + self.RubberBandSetGeometry(selection_state) + self.rubber_band.show() + else: + self.rubber_band.hide() + + def SetBracket(self, rect): + if rect: + x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height() + self.bracket.setPos(x, y) + self.bracket.SetSize(width, height) + self.bracket.show() + else: + self.bracket.hide() + + def RubberBandX(self, event): + x = event.pos().toPoint().x() + if x < self.rb_xlo: + x = self.rb_xlo + elif x > self.rb_xhi: + x = self.rb_xhi + else: + self.rb_in_view = True + return x + + def RubberBandRect(self, x): + if self.rb_origin.x() <= x: + width = x - self.rb_origin.x() + rect = QRect(self.rb_origin, QSize(width, self.height)) + else: + width = self.rb_origin.x() - x + top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y()) + rect = QRect(top_left, QSize(width, self.height)) + return rect + + def MousePressEvent(self, event): + self.rb_in_view = False + x = self.RubberBandX(event) + self.rb_origin = QPoint(x, self.top) + if self.rubber_band is None: + self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent()) + self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height))) + if self.rb_in_view: + self.rubber_band.show() + self.rb_event_handler.RBMoveEvent(x, x) + else: + self.rubber_band.hide() + + def MouseMoveEvent(self, event): + x = self.RubberBandX(event) + rect = self.RubberBandRect(x) + self.RubberBandSetGeometry(rect) + if self.rb_in_view: + self.rubber_band.show() + self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x) + + def MouseReleaseEvent(self, event): + x = self.RubberBandX(event) + if self.rb_in_view: + selection_state = self.RubberBandRect(x) + else: + selection_state = None + self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state) + +# Switch graph legend data model + +class SwitchGraphLegendModel(QAbstractTableModel): + + def __init__(self, collection, region_attributes, parent=None): + super(SwitchGraphLegendModel, self).__init__(parent) + + self.region_attributes = region_attributes + + self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal) + self.child_count = len(self.child_items) + + self.highlight_set = set() + + self.column_headers = ("pid", "tid", "comm") + + def rowCount(self, _parent): + return self.child_count + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return None + if orientation != Qt.Horizontal: + return None + return self.columnHeader(section) + + def index(self, row, column, _parent): + return self.createIndex(row, column, self.child_items[row]) + + def columnCount(self, _parent=None): + return len(self.column_headers) + + def columnHeader(self, column): + return self.column_headers[column] + + def data(self, index, role): + if role == Qt.BackgroundRole: + child = self.child_items[index.row()] + if child in self.highlight_set: + return self.region_attributes[child.key].colour + return None + if role == Qt.ForegroundRole: + child = self.child_items[index.row()] + if child in self.highlight_set: + return QColor(255, 255, 255) + return self.region_attributes[child.key].colour + if role != Qt.DisplayRole: + return None + hregion = self.child_items[index.row()] + col = index.column() + if col == 0: + return hregion.pid + if col == 1: + return hregion.tid + if col == 2: + return hregion.comm + return None + + def SetHighlight(self, row, _set_highlight): + child = self.child_items[row] + top_left = self.createIndex(row, 0, child) + bottom_right = self.createIndex(row, len(self.column_headers) - 1, child) + self.dataChanged.emit(top_left, bottom_right) + + def Highlight(self, highlight_set): + for row in range(self.child_count): + child = self.child_items[row] + if child in self.highlight_set: + if child not in highlight_set: + self.SetHighlight(row, False) + elif child in highlight_set: + self.SetHighlight(row, True) + self.highlight_set = highlight_set + +# Switch graph legend is a table + +class SwitchGraphLegend(QWidget): + + def __init__(self, collection, region_attributes, parent=None): + super(SwitchGraphLegend, self).__init__(parent) + + self.data_model = SwitchGraphLegendModel(collection, region_attributes) + + self.model = QSortFilterProxyModel() + self.model.setSourceModel(self.data_model) + + self.view = QTableView() + self.view.setModel(self.model) + self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.view.verticalHeader().setVisible(False) + self.view.sortByColumn(-1, Qt.AscendingOrder) + self.view.setSortingEnabled(True) + self.view.resizeColumnsToContents() + self.view.resizeRowsToContents() + + self.vbox = VBoxLayout(self.view) + self.setLayout(self.vbox) + + sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2 + sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width() + self.saved_size = sz1 + + def resizeEvent(self, event): + self.saved_size = self.size().width() + super(SwitchGraphLegend, self).resizeEvent(event) + + def Highlight(self, highlight_set): + self.data_model.Highlight(highlight_set) + self.update() + + def changeEvent(self, event): + if event.type() == QEvent.FontChange: + self.view.resizeRowsToContents() + self.view.resizeColumnsToContents() + # Need to resize rows again after column resize + self.view.resizeRowsToContents() + super(SwitchGraphLegend, self).changeEvent(event) + +# Random colour generation + +def RGBColourTooLight(r, g, _b): + if g > 230: + return True + if g <= 160: + return False + if r <= 180 and g <= 180: + return False + if r < 60: + return False + return True + +def GenerateColours(x): + cs = [0] + for i in range(1, x): + cs.append(int((255.0 / i) + 0.5)) + colours = [] + for r in cs: + for g in cs: + for b in cs: + # Exclude black and colours that look too light against a white background + if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b): + continue + colours.append(QColor(r, g, b)) + return colours + +def GenerateNColours(n): + for x in range(2, n + 2): + colours = GenerateColours(x) + if len(colours) >= n: + return colours + return [] + +def GenerateNRandomColours(n, seed): + colours = GenerateNColours(n) + random.seed(seed) + random.shuffle(colours) + return colours + +# Graph attributes, in particular the scale and subrange that change when zooming + +class GraphAttributes(): + + def __init__(self, scale, subrange, region_attributes, dp): + self.scale = scale + self.subrange = subrange + self.region_attributes = region_attributes + # Rounding avoids errors due to finite floating point precision + self.dp = dp # data decimal places + self.Update() + + def XToPixel(self, x): + return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x)) + + def YToPixel(self, y): + return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y)) + + def PixelToXRounded(self, px): + return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo + + def PixelToYRounded(self, py): + return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo + + def PixelToX(self, px): + x = self.PixelToXRounded(px) + if self.pdp.x == 0: + rt = self.XToPixel(x) + if rt > px: + return x - 1 + return x + + def PixelToY(self, py): + y = self.PixelToYRounded(py) + if self.pdp.y == 0: + rt = self.YToPixel(y) + if rt > py: + return y - 1 + return y + + def ToPDP(self, dp, scale): + # Calculate pixel decimal places: + # (10 ** dp) is the minimum delta in the data + # scale it to get the minimum delta in pixels + # log10 gives the number of decimals places negatively + # subtrace 1 to divide by 10 + # round to the lower negative number + # change the sign to get the number of decimals positively + x = math.log10((10 ** dp) * scale) + if x < 0: + x -= 1 + x = -int(math.floor(x) - 0.1) + else: + x = 0 + return x + + def Update(self): + x = self.ToPDP(self.dp.x, self.scale.x) + y = self.ToPDP(self.dp.y, self.scale.y) + self.pdp = XY(x, y) # pixel decimal places + +# Switch graph splitter which divides the CPU graphs from the legend + +class SwitchGraphSplitter(QSplitter): + + def __init__(self, parent=None): + super(SwitchGraphSplitter, self).__init__(parent) + + self.first_time = False + + def resizeEvent(self, event): + if self.first_time: + self.first_time = False + sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2 + sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width() + sz0 = self.size().width() - self.handleWidth() - sz1 + self.setSizes([sz0, sz1]) + elif not(self.widget(1).saved_size is None): + sz1 = self.widget(1).saved_size + sz0 = self.size().width() - self.handleWidth() - sz1 + self.setSizes([sz0, sz1]) + super(SwitchGraphSplitter, self).resizeEvent(event) + +# Graph widget base class + +class GraphWidget(QWidget): + + graph_title_changed = Signal(object) + + def __init__(self, parent=None): + super(GraphWidget, self).__init__(parent) + + def GraphTitleChanged(self, title): + self.graph_title_changed.emit(title) + + def Title(self): + return "" + +# Display time in s, ms, us or ns + +def ToTimeStr(val): + val = Decimal(val) + if val >= 1000000000: + return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001"))) + if val >= 1000000: + return "{} ms".format((val / 1000000).quantize(Decimal("0.000001"))) + if val >= 1000: + return "{} us".format((val / 1000).quantize(Decimal("0.001"))) + return "{} ns".format(val.quantize(Decimal("1"))) + +# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons + +class SwitchGraphWidget(GraphWidget): + + def __init__(self, glb, collection, parent=None): + super(SwitchGraphWidget, self).__init__(parent) + + self.glb = glb + self.collection = collection + + self.back_state = [] + self.forward_state = [] + self.selection_state = (None, None) + self.fwd_rect = None + self.start_time = self.glb.StartTime(collection.machine_id) + + i = 0 + hregions = collection.hregions.values() + colours = GenerateNRandomColours(len(hregions), 1013) + region_attributes = {} + for hregion in hregions: + if hregion.pid == 0 and hregion.tid == 0: + region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0)) + else: + region_attributes[hregion.key] = GraphRegionAttribute(colours[i]) + i = i + 1 + + # Default to entire range + xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0) + ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0) + subrange = XY(xsubrange, ysubrange) + + scale = self.GetScaleForRange(subrange) + + self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp) + + self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem) + + self.scene = QGraphicsScene() + self.scene.addItem(self.item) + + self.view = QGraphicsView(self.scene) + self.view.centerOn(0, 0) + self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop) + + self.legend = SwitchGraphLegend(collection, region_attributes) + + self.splitter = SwitchGraphSplitter() + self.splitter.addWidget(self.view) + self.splitter.addWidget(self.legend) + + self.point_label = QLabel("") + self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) + + self.back_button = QToolButton() + self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft)) + self.back_button.setDisabled(True) + self.back_button.released.connect(self.Back) + + self.forward_button = QToolButton() + self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight)) + self.forward_button.setDisabled(True) + self.forward_button.released.connect(self.Forward) + + self.zoom_button = QToolButton() + self.zoom_button.setText("Zoom") + self.zoom_button.setDisabled(True) + self.zoom_button.released.connect(self.Zoom) + + self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label) + + self.vbox = VBoxLayout(self.splitter, self.hbox) + + self.setLayout(self.vbox) + + def GetScaleForRangeX(self, xsubrange): + # Default graph 1000 pixels wide + dflt = 1000.0 + r = xsubrange.hi - xsubrange.lo + return dflt / r + + def GetScaleForRangeY(self, ysubrange): + # Default graph 50 pixels high + dflt = 50.0 + r = ysubrange.hi - ysubrange.lo + return dflt / r + + def GetScaleForRange(self, subrange): + # Default graph 1000 pixels wide, 50 pixels high + xscale = self.GetScaleForRangeX(subrange.x) + yscale = self.GetScaleForRangeY(subrange.y) + return XY(xscale, yscale) + + def PointEvent(self, cpu, time_from, _time_to, hregions): + text = "CPU: " + str(cpu) + time_from = time_from.quantize(Decimal(1)) + rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id) + text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")" + self.point_label.setText(text) + self.legend.Highlight(hregions) + + def RightClickEvent(self, _cpu, hregion_times, pos): + if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"): + return + menu = QMenu(self.view) + for hregion, time in hregion_times: + thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time) + menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time) + menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view)) + menu.exec_(pos) + + def RightClickSelect(self, args): + CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args) + + def NoPointEvent(self): + self.point_label.setText("") + self.legend.Highlight({}) + + def RangeEvent(self, time_from, time_to): + time_from = time_from.quantize(Decimal(1)) + time_to = time_to.quantize(Decimal(1)) + if time_to <= time_from: + self.point_label.setText("") + return + rel_time_from = time_from - self.start_time + rel_time_to = time_to - self.start_time + text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")" + text = text + " duration: " + ToTimeStr(time_to - time_from) + self.point_label.setText(text) + + def BackState(self): + return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect) + + def PushBackState(self): + state = copy.deepcopy(self.BackState()) + self.back_state.append(state) + self.back_button.setEnabled(True) + + def PopBackState(self): + self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop() + self.attrs.Update() + if not self.back_state: + self.back_button.setDisabled(True) + + def PushForwardState(self): + state = copy.deepcopy(self.BackState()) + self.forward_state.append(state) + self.forward_button.setEnabled(True) + + def PopForwardState(self): + self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop() + self.attrs.Update() + if not self.forward_state: + self.forward_button.setDisabled(True) + + def Title(self): + time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo) + time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi) + rel_time_from = time_from - self.start_time + rel_time_to = time_to - self.start_time + title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to) + title = title + " (" + ToTimeStr(time_to - time_from) + ")" + return title + + def Update(self): + selected_subrange, selection_state = self.selection_state + self.item.SetSelection(selection_state) + self.item.SetBracket(self.fwd_rect) + self.zoom_button.setDisabled(selected_subrange is None) + self.GraphTitleChanged(self.Title()) + self.item.update(self.item.boundingRect()) + + def Back(self): + if not self.back_state: + return + self.PushForwardState() + self.PopBackState() + self.Update() + + def Forward(self): + if not self.forward_state: + return + self.PushBackState() + self.PopForwardState() + self.Update() + + def SelectEvent(self, x0, x1, selection_state): + if selection_state is None: + selected_subrange = None + else: + if x1 - x0 < 1.0: + x1 += 1.0 + selected_subrange = Subrange(x0, x1) + self.selection_state = (selected_subrange, selection_state) + self.zoom_button.setDisabled(selected_subrange is None) + + def Zoom(self): + selected_subrange, selection_state = self.selection_state + if selected_subrange is None: + return + self.fwd_rect = selection_state + self.item.SetSelection(None) + self.PushBackState() + self.attrs.subrange.x = selected_subrange + self.forward_state = [] + self.forward_button.setDisabled(True) + self.selection_state = (None, None) + self.fwd_rect = None + self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x) + self.attrs.Update() + self.Update() + +# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting + +class SlowInitClass(): + + def __init__(self, glb, title, init_fn): + self.init_fn = init_fn + self.done = False + self.result = None + + self.msg_box = QMessageBox(glb.mainwindow) + self.msg_box.setText("Initializing " + title + ". Please wait.") + self.msg_box.setWindowTitle("Initializing " + title) + self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation)) + + self.init_thread = Thread(self.ThreadFn, glb) + self.init_thread.done.connect(self.Done, Qt.QueuedConnection) + + self.init_thread.start() + + def Done(self): + self.msg_box.done(0) + + def ThreadFn(self, glb): + conn_name = "SlowInitClass" + str(os.getpid()) + db, _dbname = glb.dbref.Open(conn_name) + self.result = self.init_fn(db) + self.done = True + return (True, 0) + + def Result(self): + while not self.done: + self.msg_box.exec_() + self.init_thread.wait() + return self.result + +def SlowInit(glb, title, init_fn): + init = SlowInitClass(glb, title, init_fn) + return init.Result() + +# Time chart by CPU window + +class TimeChartByCPUWindow(QMdiSubWindow): + + def __init__(self, glb, parent=None): + super(TimeChartByCPUWindow, self).__init__(parent) + + self.glb = glb + self.machine_id = glb.HostMachineId() + self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id) + + collection = LookupModel(self.collection_name) + if collection is None: + collection = SlowInit(glb, "Time Chart", self.Init) + + self.widget = SwitchGraphWidget(glb, collection, self) + self.view = self.widget + + self.base_title = "Time Chart by CPU" + self.setWindowTitle(self.base_title + self.widget.Title()) + self.widget.graph_title_changed.connect(self.GraphTitleChanged) + + self.setWidget(self.widget) + + AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle()) + + def Init(self, db): + return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id)) + + def GraphTitleChanged(self, title): + self.setWindowTitle(self.base_title + " : " + title) + +# Child data item finder + +class ChildDataItemFinder(): + + def __init__(self, root): + self.root = root + self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5 + self.rows = [] + self.pos = 0 + + def FindSelect(self): + self.rows = [] + if self.pattern: + pattern = re.compile(self.value) + for child in self.root.child_items: + for column_data in child.data: + if re.search(pattern, str(column_data)) is not None: + self.rows.append(child.row) + break + else: + for child in self.root.child_items: + for column_data in child.data: + if self.value in str(column_data): + self.rows.append(child.row) + break + + def FindValue(self): + self.pos = 0 + if self.last_value != self.value or self.pattern != self.last_pattern: + self.FindSelect() + if not len(self.rows): + return -1 + return self.rows[self.pos] + + def FindThread(self): + if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern: + row = self.FindValue() + elif len(self.rows): + if self.direction > 0: + self.pos += 1 + if self.pos >= len(self.rows): + self.pos = 0 + else: + self.pos -= 1 + if self.pos < 0: + self.pos = len(self.rows) - 1 + row = self.rows[self.pos] + else: + row = -1 + return (True, row) + + def Find(self, value, direction, pattern, _context, callback): + self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern) + # Use a thread so the UI is not blocked + thread = Thread(self.FindThread) + thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection) + thread.start() + + def FindDone(self, _thread, callback, row): + callback(row) + +# Number of database records to fetch in one go + +glb_chunk_sz = 10000 + +# Background process for SQL data fetcher + +class SQLFetcherProcess(): + + def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep): + # Need a unique connection name + conn_name = "SQLFetcher" + str(os.getpid()) + self.db, _dbname = dbref.Open(conn_name) + self.sql = sql + self.buffer = buffer + self.head = head + self.tail = tail + self.fetch_count = fetch_count + self.fetching_done = fetching_done + self.process_target = process_target + self.wait_event = wait_event + self.fetched_event = fetched_event + self.prep = prep + self.query = QSqlQuery(self.db) + self.query_limit = 0 if "$$last_id$$" in sql else 2 + self.last_id = -1 + self.fetched = 0 + self.more = True + self.local_head = self.head.value + self.local_tail = self.tail.value + + def Select(self): + if self.query_limit: + if self.query_limit == 1: + return + self.query_limit -= 1 + stmt = self.sql.replace("$$last_id$$", str(self.last_id)) + QueryExec(self.query, stmt) + + def Next(self): + if not self.query.next(): + self.Select() + if not self.query.next(): + return None + self.last_id = self.query.value(0) + return self.prep(self.query) + + def WaitForTarget(self): + while True: + self.wait_event.clear() + target = self.process_target.value + if target > self.fetched or target < 0: + break + self.wait_event.wait() + return target + + def HasSpace(self, sz): + if self.local_tail <= self.local_head: + space = len(self.buffer) - self.local_head + if space > sz: + return True + if space >= glb_nsz: + # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer + nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL) + self.buffer[self.local_head : self.local_head + len(nd)] = nd + self.local_head = 0 + if self.local_tail - self.local_head > sz: + return True + return False + + def WaitForSpace(self, sz): + if self.HasSpace(sz): + return + while True: + self.wait_event.clear() + self.local_tail = self.tail.value + if self.HasSpace(sz): + return + self.wait_event.wait() + + def AddToBuffer(self, obj): + d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) + n = len(d) + nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL) + sz = n + glb_nsz + self.WaitForSpace(sz) + pos = self.local_head + self.buffer[pos : pos + len(nd)] = nd + self.buffer[pos + glb_nsz : pos + sz] = d + self.local_head += sz + + def FetchBatch(self, batch_size): + fetched = 0 + while batch_size > fetched: + obj = self.Next() + if obj is None: + self.more = False + break + self.AddToBuffer(obj) + fetched += 1 + if fetched: + self.fetched += fetched + with self.fetch_count.get_lock(): + self.fetch_count.value += fetched + self.head.value = self.local_head + self.fetched_event.set() + + def Run(self): + while self.more: + target = self.WaitForTarget() + if target < 0: + break + batch_size = min(glb_chunk_sz, target - self.fetched) + self.FetchBatch(batch_size) + self.fetching_done.value = True + self.fetched_event.set() + +def SQLFetcherFn(*x): + process = SQLFetcherProcess(*x) + process.Run() + +# SQL data fetcher + +class SQLFetcher(QObject): + + done = Signal(object) + + def __init__(self, glb, sql, prep, process_data, parent=None): + super(SQLFetcher, self).__init__(parent) + self.process_data = process_data + self.more = True + self.target = 0 + self.last_target = 0 + self.fetched = 0 + self.buffer_size = 16 * 1024 * 1024 + self.buffer = Array(c_char, self.buffer_size, lock=False) + self.head = Value(c_longlong) + self.tail = Value(c_longlong) + self.local_tail = 0 + self.fetch_count = Value(c_longlong) + self.fetching_done = Value(c_bool) + self.last_count = 0 + self.process_target = Value(c_longlong) + self.wait_event = Event() + self.fetched_event = Event() + glb.AddInstanceToShutdownOnExit(self) + self.process = Process(target=SQLFetcherFn, args=(glb.dbref, sql, self.buffer, self.head, self.tail, self.fetch_count, self.fetching_done, self.process_target, self.wait_event, self.fetched_event, prep)) + self.process.start() + self.thread = Thread(self.Thread) + self.thread.done.connect(self.ProcessData, Qt.QueuedConnection) + self.thread.start() + + def Shutdown(self): + # Tell the thread and process to exit + self.process_target.value = -1 + self.wait_event.set() + self.more = False + self.fetching_done.value = True + self.fetched_event.set() + + def Thread(self): + if not self.more: + return True, 0 + while True: + self.fetched_event.clear() + fetch_count = self.fetch_count.value + if fetch_count != self.last_count: + break + if self.fetching_done.value: + self.more = False + return True, 0 + self.fetched_event.wait() + count = fetch_count - self.last_count + self.last_count = fetch_count + self.fetched += count + return False, count + + def Fetch(self, nr): + if not self.more: + # -1 inidcates there are no more + return -1 + result = self.fetched + extra = result + nr - self.target + if extra > 0: + self.target += extra + # process_target < 0 indicates shutting down + if self.process_target.value >= 0: + self.process_target.value = self.target + self.wait_event.set() + return result + + def RemoveFromBuffer(self): + pos = self.local_tail + if len(self.buffer) - pos < glb_nsz: + pos = 0 + n = pickle.loads(self.buffer[pos : pos + glb_nsz]) + if n == 0: + pos = 0 + n = pickle.loads(self.buffer[0 : glb_nsz]) + pos += glb_nsz + obj = pickle.loads(self.buffer[pos : pos + n]) + self.local_tail = pos + n + return obj + + def ProcessData(self, count): + for _i in range(count): + obj = self.RemoveFromBuffer() + self.process_data(obj) + self.tail.value = self.local_tail + self.wait_event.set() + self.done.emit(count) + +# Fetch more records bar + +class FetchMoreRecordsBar(): + + def __init__(self, model, parent): + self.model = model + + self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:") + self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self.fetch_count = QSpinBox() + self.fetch_count.setRange(1, 1000000) + self.fetch_count.setValue(10) + self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self.fetch = QPushButton("Go!") + self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.fetch.released.connect(self.FetchMoreRecords) + + self.progress = QProgressBar() + self.progress.setRange(0, 100) + self.progress.hide() + + self.done_label = QLabel("All records fetched") + self.done_label.hide() + + self.spacer = QLabel("") + + self.close_button = QToolButton() + self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) + self.close_button.released.connect(self.Deactivate) + + self.hbox = QHBoxLayout() + self.hbox.setContentsMargins(0, 0, 0, 0) + + self.hbox.addWidget(self.label) + self.hbox.addWidget(self.fetch_count) + self.hbox.addWidget(self.fetch) + self.hbox.addWidget(self.spacer) + self.hbox.addWidget(self.progress) + self.hbox.addWidget(self.done_label) + self.hbox.addWidget(self.close_button) + + self.bar = QWidget() + self.bar.setLayout(self.hbox) + self.bar.show() + + self.in_progress = False + self.start = 0 + self.model.progress.connect(self.Progress) + + self.done = False + + if not model.HasMoreRecords(): + self.Done() + + def Widget(self): + return self.bar + + def Activate(self): + self.bar.show() + self.fetch.setFocus() + + def Deactivate(self): + self.bar.hide() + + def Enable(self, enable): + self.fetch.setEnabled(enable) + self.fetch_count.setEnabled(enable) + + def Busy(self): + self.Enable(False) + self.fetch.hide() + self.spacer.hide() + self.progress.show() + + def Idle(self): + self.in_progress = False + self.Enable(True) + self.progress.hide() + self.fetch.show() + self.spacer.show() + + def Target(self): + return self.fetch_count.value() * glb_chunk_sz + + def Done(self): + self.done = True + self.Idle() + self.label.hide() + self.fetch_count.hide() + self.fetch.hide() + self.spacer.hide() + self.done_label.show() + + def Progress(self, count): + if self.in_progress: + if count: + percent = ((count - self.start) * 100) / self.Target() + if percent >= 100: + self.Idle() + else: + self.progress.setValue(percent) + if not count: + # Count value of zero means no more records + self.Done() + + def FetchMoreRecords(self): + if self.done: + return + self.progress.setValue(0) + self.Busy() + self.in_progress = True + self.start = self.model.FetchMoreRecords(self.Target()) + +# Brance data model level two item + +class BranchLevelTwoItem(): + + def __init__(self, row, col, text, parent_item): + self.row = row + self.parent_item = parent_item + self.data = [""] * (col + 1) + self.data[col] = text + self.level = 2 + + def getParentItem(self): + return self.parent_item + + def getRow(self): + return self.row + + def childCount(self): + return 0 + + def hasChildren(self): + return False + + def getData(self, column): + return self.data[column] + +# Brance data model level one item + +class BranchLevelOneItem(): + + def __init__(self, glb, row, data, parent_item): + self.glb = glb + self.row = row + self.parent_item = parent_item + self.child_count = 0 + self.child_items = [] + self.data = data[1:] + self.dbid = data[0] + self.level = 1 + self.query_done = False + self.br_col = len(self.data) - 1 + + def getChildItem(self, row): + return self.child_items[row] + + def getParentItem(self): + return self.parent_item + + def getRow(self): + return self.row + + def Select(self): + self.query_done = True + + if not self.glb.have_disassembler: + return + + query = QSqlQuery(self.glb.db) + + QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip" + " FROM samples" + " INNER JOIN dsos ON samples.to_dso_id = dsos.id" + " INNER JOIN symbols ON samples.to_symbol_id = symbols.id" + " WHERE samples.id = " + str(self.dbid)) + if not query.next(): + return + cpu = query.value(0) + dso = query.value(1) + sym = query.value(2) + if dso == 0 or sym == 0: + return + off = query.value(3) + short_name = query.value(4) + long_name = query.value(5) + build_id = query.value(6) + sym_start = query.value(7) + ip = query.value(8) + + QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start" + " FROM samples" + " INNER JOIN symbols ON samples.symbol_id = symbols.id" + " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) + + " ORDER BY samples.id" + " LIMIT 1") + if not query.next(): + return + if query.value(0) != dso: + # Cannot disassemble from one dso to another + return + bsym = query.value(1) + boff = query.value(2) + bsym_start = query.value(3) + if bsym == 0: + return + tot = bsym_start + boff + 1 - sym_start - off + if tot <= 0 or tot > 16384: + return + + inst = self.glb.disassembler.Instruction() + f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id) + if not f: + return + mode = 0 if Is64Bit(f) else 1 + self.glb.disassembler.SetMode(inst, mode) + + buf_sz = tot + 16 + buf = create_string_buffer(tot + 16) + f.seek(sym_start + off) + buf.value = f.read(buf_sz) + buf_ptr = addressof(buf) + i = 0 + while tot > 0: + cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip) + if cnt: + byte_str = tohex(ip).rjust(16) + for k in range(cnt): + byte_str += " %02x" % ord(buf[i]) + i += 1 + while k < 15: + byte_str += " " + k += 1 + self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self)) + self.child_count += 1 + else: + return + buf_ptr += cnt + tot -= cnt + buf_sz -= cnt + ip += cnt + + def childCount(self): + if not self.query_done: + self.Select() + if not self.child_count: + return -1 + return self.child_count + + def hasChildren(self): + if not self.query_done: + return True + return self.child_count > 0 + + def getData(self, column): + return self.data[column] + +# Brance data model root item + +class BranchRootItem(): + + def __init__(self): + self.child_count = 0 + self.child_items = [] + self.level = 0 + + def getChildItem(self, row): + return self.child_items[row] + + def getParentItem(self): + return None + + def getRow(self): + return 0 + + def childCount(self): + return self.child_count + + def hasChildren(self): + return self.child_count > 0 + + def getData(self, _column): + return "" + +# Calculate instructions per cycle + +def CalcIPC(cyc_cnt, insn_cnt): + if cyc_cnt and insn_cnt: + ipc = Decimal(float(insn_cnt) / cyc_cnt) + ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP)) + else: + ipc = "0" + return ipc + +# Branch data preparation + +def BranchDataPrepBr(query, data): + data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) + + " (" + dsoname(query.value(11)) + ")" + " -> " + + tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) + + " (" + dsoname(query.value(15)) + ")") + +def BranchDataPrepIPC(query, data): + insn_cnt = query.value(16) + cyc_cnt = query.value(17) + ipc = CalcIPC(cyc_cnt, insn_cnt) + data.append(insn_cnt) + data.append(cyc_cnt) + data.append(ipc) + +def BranchDataPrep(query): + data = [] + for i in range(0, 8): + data.append(query.value(i)) + BranchDataPrepBr(query, data) + return data + +def BranchDataPrepWA(query): + data = [] + data.append(query.value(0)) + # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string + data.append("{:>19}".format(query.value(1))) + for i in range(2, 8): + data.append(query.value(i)) + BranchDataPrepBr(query, data) + return data + +def BranchDataWithIPCPrep(query): + data = [] + for i in range(0, 8): + data.append(query.value(i)) + BranchDataPrepIPC(query, data) + BranchDataPrepBr(query, data) + return data + +def BranchDataWithIPCPrepWA(query): + data = [] + data.append(query.value(0)) + # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string + data.append("{:>19}".format(query.value(1))) + for i in range(2, 8): + data.append(query.value(i)) + BranchDataPrepIPC(query, data) + BranchDataPrepBr(query, data) + return data + +# Branch data model + +class BranchModel(TreeModel): + + progress = Signal(object) + + def __init__(self, glb, event_id, where_clause, parent=None): + super(BranchModel, self).__init__(glb, None, parent) + self.event_id = event_id + self.more = True + self.populated = 0 + self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count") + if self.have_ipc: + select_ipc = ", insn_count, cyc_count" + prep_fn = BranchDataWithIPCPrep + prep_wa_fn = BranchDataWithIPCPrepWA + else: + select_ipc = "" + prep_fn = BranchDataPrep + prep_wa_fn = BranchDataPrepWA + sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name," + " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END," + " ip, symbols.name, sym_offset, dsos.short_name," + " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name" + + select_ipc + + " FROM samples" + " INNER JOIN comms ON comm_id = comms.id" + " INNER JOIN threads ON thread_id = threads.id" + " INNER JOIN branch_types ON branch_type = branch_types.id" + " INNER JOIN symbols ON symbol_id = symbols.id" + " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id" + " INNER JOIN dsos ON samples.dso_id = dsos.id" + " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id" + " WHERE samples.id > $$last_id$$" + where_clause + + " AND evsel_id = " + str(self.event_id) + + " ORDER BY samples.id" + " LIMIT " + str(glb_chunk_sz)) + if pyside_version_1 and sys.version_info[0] == 3: + prep = prep_fn + else: + prep = prep_wa_fn + self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample) + self.fetcher.done.connect(self.Update) + self.fetcher.Fetch(glb_chunk_sz) + + def GetRoot(self): + return BranchRootItem() + + def columnCount(self, _parent=None): + if self.have_ipc: + return 11 + else: + return 8 + + def columnHeader(self, column): + if self.have_ipc: + return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column] + else: + return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column] + + def columnFont(self, column): + if self.have_ipc: + br_col = 10 + else: + br_col = 7 + if column != br_col: + return None + return QFont("Monospace") + + def DisplayData(self, item, index): + if item.level == 1: + self.FetchIfNeeded(item.row) + return item.getData(index.column()) + + def AddSample(self, data): + child = BranchLevelOneItem(self.glb, self.populated, data, self.root) + self.root.child_items.append(child) + self.populated += 1 + + def Update(self, fetched): + if not fetched: + self.more = False + self.progress.emit(0) + child_count = self.root.child_count + count = self.populated - child_count + if count > 0: + parent = QModelIndex() + self.beginInsertRows(parent, child_count, child_count + count - 1) + self.insertRows(child_count, count, parent) + self.root.child_count += count + self.endInsertRows() + self.progress.emit(self.root.child_count) + + def FetchMoreRecords(self, count): + current = self.root.child_count + if self.more: + self.fetcher.Fetch(count) + else: + self.progress.emit(0) + return current + + def HasMoreRecords(self): + return self.more + +# Report Variables + +class ReportVars(): + + def __init__(self, name = "", where_clause = "", limit = ""): + self.name = name + self.where_clause = where_clause + self.limit = limit + + def UniqueId(self): + return str(self.where_clause + ";" + self.limit) + +# Branch window + +class BranchWindow(QMdiSubWindow): + + def __init__(self, glb, event_id, report_vars, parent=None): + super(BranchWindow, self).__init__(parent) + + model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId() + + self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause)) + + self.view = QTreeView() + self.view.setUniformRowHeights(True) + self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) + self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard + self.view.setModel(self.model) + + self.ResizeColumnsToContents() + + self.context_menu = TreeContextMenu(self.view) + + self.find_bar = FindBar(self, self, True) + + self.finder = ChildDataItemFinder(self.model.root) + + self.fetch_bar = FetchMoreRecordsBar(self.model, self) + + self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) + + self.setWidget(self.vbox.Widget()) + + AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events") + + def ResizeColumnToContents(self, column, n): + # Using the view's resizeColumnToContents() here is extrememly slow + # so implement a crude alternative + mm = "MM" if column else "MMMM" + font = self.view.font() + metrics = QFontMetrics(font) + max_w = 0 + for row in range(n): + val = self.model.root.child_items[row].data[column] + width = metrics.width(str(val) + mm) + max_w = width if width > max_w else max_w + val = self.model.columnHeader(column) + width = metrics.width(str(val) + mm) + max_w = width if width > max_w else max_w + self.view.setColumnWidth(column, max_w) + + def ResizeColumnsToContents(self): + n = min(self.model.root.child_count, 100) + if n < 1: + # No data yet, so connect a signal to notify when there is + self.model.rowsInserted.connect(self.UpdateColumnWidths) + return + columns = self.model.columnCount() + for i in range(columns): + self.ResizeColumnToContents(i, n) + + def UpdateColumnWidths(self, *_x): + # This only needs to be done once, so disconnect the signal now + self.model.rowsInserted.disconnect(self.UpdateColumnWidths) + self.ResizeColumnsToContents() + + def Find(self, value, direction, pattern, context): + self.view.setFocus() + self.find_bar.Busy() + self.finder.Find(value, direction, pattern, context, self.FindDone) + + def FindDone(self, row): + self.find_bar.Idle() + if row >= 0: + self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) + else: + self.find_bar.NotFound() + +# Line edit data item + +class LineEditDataItem(object): + + def __init__(self, glb, label, placeholder_text, parent, item_id = "", default = ""): + self.glb = glb + self.label = label + self.placeholder_text = placeholder_text + self.parent = parent + self.id = item_id + + self.value = default + + self.widget = QLineEdit(default) + self.widget.editingFinished.connect(self.Validate) + self.widget.textChanged.connect(self.Invalidate) + self.red = False + self.error = "" + self.validated = True + + if placeholder_text: + self.widget.setPlaceholderText(placeholder_text) + + def TurnTextRed(self): + if not self.red: + palette = QPalette() + palette.setColor(QPalette.Text,Qt.red) + self.widget.setPalette(palette) + self.red = True + + def TurnTextNormal(self): + if self.red: + palette = QPalette() + self.widget.setPalette(palette) + self.red = False + + def InvalidValue(self, value): + self.value = "" + self.TurnTextRed() + self.error = self.label + " invalid value '" + value + "'" + self.parent.ShowMessage(self.error) + + def Invalidate(self): + self.validated = False + + def DoValidate(self, input_string): + self.value = input_string.strip() + + def Validate(self): + self.validated = True + self.error = "" + self.TurnTextNormal() + self.parent.ClearMessage() + input_string = self.widget.text() + if not len(input_string.strip()): + self.value = "" + return + self.DoValidate(input_string) + + def IsValid(self): + if not self.validated: + self.Validate() + if len(self.error): + self.parent.ShowMessage(self.error) + return False + return True + + def IsNumber(self, value): + try: + x = int(value) + except ValueError: + x = 0 + return str(x) == value + +# Non-negative integer ranges dialog data item + +class NonNegativeIntegerRangesDataItem(LineEditDataItem): + + def __init__(self, glb, label, placeholder_text, column_name, parent): + super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent) + + self.column_name = column_name + + def DoValidate(self, input_string): + singles = [] + ranges = [] + for value in [x.strip() for x in input_string.split(",")]: + if "-" in value: + vrange = value.split("-") + if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): + return self.InvalidValue(value) + ranges.append(vrange) + else: + if not self.IsNumber(value): + return self.InvalidValue(value) + singles.append(value) + ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] + if len(singles): + ranges.append(self.column_name + " IN (" + ",".join(singles) + ")") + self.value = " OR ".join(ranges) + +# Positive integer dialog data item + +class PositiveIntegerDataItem(LineEditDataItem): + + def DoValidate(self, input_string): + if not self.IsNumber(input_string.strip()): + return self.InvalidValue(input_string) + value = int(input_string.strip()) + if value <= 0: + return self.InvalidValue(input_string) + self.value = str(value) + +# Dialog data item converted and validated using a SQL table + +class SQLTableDataItem(LineEditDataItem): + + def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent): + super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent) + + self.table_name = table_name + self.match_column = match_column + self.column_name1 = column_name1 + self.column_name2 = column_name2 + + def ValueToIds(self, value): + ids = [] + query = QSqlQuery(self.glb.db) + stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'" + ret = query.exec_(stmt) + if ret: + while query.next(): + ids.append(str(query.value(0))) + return ids + + def DoValidate(self, input_string): + all_ids = [] + for value in [x.strip() for x in input_string.split(",")]: + ids = self.ValueToIds(value) + if len(ids): + all_ids.extend(ids) + else: + return self.InvalidValue(value) + self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")" + if self.column_name2: + self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )" + +# Sample time ranges dialog data item converted and validated using 'samples' SQL table + +class SampleTimeRangesDataItem(LineEditDataItem): + + def __init__(self, glb, label, placeholder_text, column_name, parent): + self.column_name = column_name + + self.last_id = 0 + self.first_time = 0 + self.last_time = 2 ** 64 + + query = QSqlQuery(glb.db) + QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1") + if query.next(): + self.last_id = int(query.value(0)) + self.first_time = int(glb.HostStartTime()) + self.last_time = int(glb.HostFinishTime()) + if placeholder_text: + placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time) + + super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent) + + def IdBetween(self, query, lower_id, higher_id, order): + QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1") + if query.next(): + return True, int(query.value(0)) + else: + return False, 0 + + def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor): + query = QSqlQuery(self.glb.db) + while True: + next_id = int((lower_id + higher_id) / 2) + QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) + if not query.next(): + ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC") + if not ok: + ok, dbid = self.IdBetween(query, next_id, higher_id, "") + if not ok: + return str(higher_id) + next_id = dbid + QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) + next_time = int(query.value(0)) + if get_floor: + if target_time > next_time: + lower_id = next_id + else: + higher_id = next_id + if higher_id <= lower_id + 1: + return str(higher_id) + else: + if target_time >= next_time: + lower_id = next_id + else: + higher_id = next_id + if higher_id <= lower_id + 1: + return str(lower_id) + + def ConvertRelativeTime(self, val): + mult = 1 + suffix = val[-2:] + if suffix == "ms": + mult = 1000000 + elif suffix == "us": + mult = 1000 + elif suffix == "ns": + mult = 1 + else: + return val + val = val[:-2].strip() + if not self.IsNumber(val): + return val + val = int(val) * mult + if val >= 0: + val += self.first_time + else: + val += self.last_time + return str(val) + + def ConvertTimeRange(self, vrange): + if vrange[0] == "": + vrange[0] = str(self.first_time) + if vrange[1] == "": + vrange[1] = str(self.last_time) + vrange[0] = self.ConvertRelativeTime(vrange[0]) + vrange[1] = self.ConvertRelativeTime(vrange[1]) + if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): + return False + beg_range = max(int(vrange[0]), self.first_time) + end_range = min(int(vrange[1]), self.last_time) + if beg_range > self.last_time or end_range < self.first_time: + return False + vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True) + vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False) + return True + + def AddTimeRange(self, value, ranges): + n = value.count("-") + if n == 1: + pass + elif n == 2: + if value.split("-")[1].strip() == "": + n = 1 + elif n == 3: + n = 2 + else: + return False + pos = findnth(value, "-", n) + vrange = [value[:pos].strip() ,value[pos+1:].strip()] + if self.ConvertTimeRange(vrange): + ranges.append(vrange) + return True + return False + + def DoValidate(self, input_string): + ranges = [] + for value in [x.strip() for x in input_string.split(",")]: + if not self.AddTimeRange(value, ranges): + return self.InvalidValue(value) + ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] + self.value = " OR ".join(ranges) + +# Report Dialog Base + +class ReportDialogBase(QDialog): + + def __init__(self, glb, title, items, partial, parent=None): + super(ReportDialogBase, self).__init__(parent) + + self.glb = glb + + self.report_vars = ReportVars() + + self.setWindowTitle(title) + self.setMinimumWidth(600) + + self.data_items = [x(glb, self) for x in items] + + self.partial = partial + + self.grid = QGridLayout() + + for row in range(len(self.data_items)): + self.grid.addWidget(QLabel(self.data_items[row].label), row, 0) + self.grid.addWidget(self.data_items[row].widget, row, 1) + + self.status = QLabel() + + self.ok_button = QPushButton("Ok", self) + self.ok_button.setDefault(True) + self.ok_button.released.connect(self.Ok) + self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self.cancel_button = QPushButton("Cancel", self) + self.cancel_button.released.connect(self.reject) + self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self.hbox = QHBoxLayout() + #self.hbox.addStretch() + self.hbox.addWidget(self.status) + self.hbox.addWidget(self.ok_button) + self.hbox.addWidget(self.cancel_button) + + self.vbox = QVBoxLayout() + self.vbox.addLayout(self.grid) + self.vbox.addLayout(self.hbox) + + self.setLayout(self.vbox) + + def Ok(self): + rep_vars = self.report_vars + for d in self.data_items: + if d.id == "REPORTNAME": + rep_vars.name = d.value + if not rep_vars.name: + self.ShowMessage("Report name is required") + return + for d in self.data_items: + if not d.IsValid(): + return + for d in self.data_items[1:]: + if d.id == "LIMIT": + rep_vars.limit = d.value + elif len(d.value): + if len(rep_vars.where_clause): + rep_vars.where_clause += " AND " + rep_vars.where_clause += d.value + if len(rep_vars.where_clause): + if self.partial: + rep_vars.where_clause = " AND ( " + rep_vars.where_clause + " ) " + else: + rep_vars.where_clause = " WHERE " + rep_vars.where_clause + " " + self.accept() + + def ShowMessage(self, msg): + self.status.setText("" + msg) + + def ClearMessage(self): + self.status.setText("") + +# Selected branch report creation dialog + +class SelectedBranchDialog(ReportDialogBase): + + def __init__(self, glb, parent=None): + title = "Selected Branches" + items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), + lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p), + lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p), + lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p), + lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p), + lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p), + lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p), + lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p), + lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p)) + super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent) + +# Event list + +def GetEventList(db): + events = [] + query = QSqlQuery(db) + QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id") + while query.next(): + events.append(query.value(0)) + return events + +# Is a table selectable + +def IsSelectable(db, table, sql = "", columns = "*"): + query = QSqlQuery(db) + try: + QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1") + except RuntimeError: + return False + return True + +# SQL table data model item + +class SQLTableItem(): + + def __init__(self, row, data): + self.row = row + self.data = data + + def getData(self, column): + return self.data[column] + +# SQL table data model + +class SQLTableModel(TableModel): + + progress = Signal(object) + + def __init__(self, glb, sql, column_headers, parent=None): + super(SQLTableModel, self).__init__(parent) + self.glb = glb + self.more = True + self.populated = 0 + self.column_headers = column_headers + self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample) + self.fetcher.done.connect(self.Update) + self.fetcher.Fetch(glb_chunk_sz) + + def DisplayData(self, item, index): + self.FetchIfNeeded(item.row) + return item.getData(index.column()) + + def AddSample(self, data): + child = SQLTableItem(self.populated, data) + self.child_items.append(child) + self.populated += 1 + + def Update(self, fetched): + if not fetched: + self.more = False + self.progress.emit(0) + child_count = self.child_count + count = self.populated - child_count + if count > 0: + parent = QModelIndex() + self.beginInsertRows(parent, child_count, child_count + count - 1) + self.insertRows(child_count, count, parent) + self.child_count += count + self.endInsertRows() + self.progress.emit(self.child_count) + + def FetchMoreRecords(self, count): + current = self.child_count + if self.more: + self.fetcher.Fetch(count) + else: + self.progress.emit(0) + return current + + def HasMoreRecords(self): + return self.more + + def columnCount(self, _parent=None): + return len(self.column_headers) + + def columnHeader(self, column): + return self.column_headers[column] + + def SQLTableDataPrep(self, query, count): + data = [] + for i in range(count): + data.append(query.value(i)) + return data + +# SQL automatic table data model + +class SQLAutoTableModel(SQLTableModel): + + def __init__(self, glb, table_name, parent=None): + sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz) + if table_name == "comm_threads_view": + # For now, comm_threads_view has no id column + sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz) + column_headers = [] + query = QSqlQuery(glb.db) + if glb.dbref.is_sqlite3: + QueryExec(query, "PRAGMA table_info(" + table_name + ")") + while query.next(): + column_headers.append(query.value(1)) + if table_name == "sqlite_master": + sql = "SELECT * FROM " + table_name + else: + if table_name[:19] == "information_schema.": + sql = "SELECT * FROM " + table_name + select_table_name = table_name[19:] + schema = "information_schema" + else: + select_table_name = table_name + schema = "public" + QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'") + while query.next(): + column_headers.append(query.value(0)) + if pyside_version_1 and sys.version_info[0] == 3: + if table_name == "samples_view": + self.SQLTableDataPrep = self.samples_view_DataPrep + if table_name == "samples": + self.SQLTableDataPrep = self.samples_DataPrep + super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent) + + def samples_view_DataPrep(self, query, count): + data = [] + data.append(query.value(0)) + # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string + data.append("{:>19}".format(query.value(1))) + for i in range(2, count): + data.append(query.value(i)) + return data + + def samples_DataPrep(self, query, count): + data = [] + for i in range(9): + data.append(query.value(i)) + # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string + data.append("{:>19}".format(query.value(9))) + for i in range(10, count): + data.append(query.value(i)) + return data + +# Base class for custom ResizeColumnsToContents + +class ResizeColumnsToContentsBase(QObject): + + def __init__(self, parent=None): + super(ResizeColumnsToContentsBase, self).__init__(parent) + + def ResizeColumnToContents(self, column, n): + # Using the view's resizeColumnToContents() here is extrememly slow + # so implement a crude alternative + font = self.view.font() + metrics = QFontMetrics(font) + max_w = 0 + for row in range(n): + val = self.data_model.child_items[row].data[column] + width = metrics.width(str(val) + "MM") + max_w = width if width > max_w else max_w + val = self.data_model.columnHeader(column) + width = metrics.width(str(val) + "MM") + max_w = width if width > max_w else max_w + self.view.setColumnWidth(column, max_w) + + def ResizeColumnsToContents(self): + n = min(self.data_model.child_count, 100) + if n < 1: + # No data yet, so connect a signal to notify when there is + self.data_model.rowsInserted.connect(self.UpdateColumnWidths) + return + columns = self.data_model.columnCount() + for i in range(columns): + self.ResizeColumnToContents(i, n) + + def UpdateColumnWidths(self, *_x): + # This only needs to be done once, so disconnect the signal now + self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths) + self.ResizeColumnsToContents() + +# Convert value to CSV + +def ToCSValue(val): + if '"' in val: + val = val.replace('"', '""') + if "," in val or '"' in val: + val = '"' + val + '"' + return val + +# Key to sort table model indexes by row / column, assuming fewer than 1000 columns + +glb_max_cols = 1000 + +def RowColumnKey(a): + return a.row() * glb_max_cols + a.column() + +# Copy selected table cells to clipboard + +def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False): + indexes = sorted(view.selectedIndexes(), key=RowColumnKey) + idx_cnt = len(indexes) + if not idx_cnt: + return + if idx_cnt == 1: + with_hdr=False + min_row = indexes[0].row() + max_row = indexes[0].row() + min_col = indexes[0].column() + max_col = indexes[0].column() + for i in indexes: + min_row = min(min_row, i.row()) + max_row = max(max_row, i.row()) + min_col = min(min_col, i.column()) + max_col = max(max_col, i.column()) + if max_col > glb_max_cols: + raise RuntimeError("glb_max_cols is too low") + max_width = [0] * (1 + max_col - min_col) + for i in indexes: + c = i.column() - min_col + max_width[c] = max(max_width[c], len(str(i.data()))) + text = "" + pad = "" + sep = "" + if with_hdr: + model = indexes[0].model() + for col in range(min_col, max_col + 1): + val = model.headerData(col, Qt.Horizontal, Qt.DisplayRole) + if as_csv: + text += sep + ToCSValue(val) + sep = "," + else: + c = col - min_col + max_width[c] = max(max_width[c], len(val)) + width = max_width[c] + align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole) + if align & Qt.AlignRight: + val = val.rjust(width) + text += pad + sep + val + pad = " " * (width - len(val)) + sep = " " + text += "\n" + pad = "" + sep = "" + last_row = min_row + for i in indexes: + if i.row() > last_row: + last_row = i.row() + text += "\n" + pad = "" + sep = "" + if as_csv: + text += sep + ToCSValue(str(i.data())) + sep = "," + else: + width = max_width[i.column() - min_col] + if i.data(Qt.TextAlignmentRole) & Qt.AlignRight: + val = str(i.data()).rjust(width) + else: + val = str(i.data()) + text += pad + sep + val + pad = " " * (width - len(val)) + sep = " " + QApplication.clipboard().setText(text) + +def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False): + indexes = view.selectedIndexes() + if not len(indexes): + return + + selection = view.selectionModel() + + first = None + for i in indexes: + above = view.indexAbove(i) + if not selection.isSelected(above): + first = i + break + + if first is None: + raise RuntimeError("CopyTreeCellsToClipboard internal error") + + model = first.model() + row_cnt = 0 + col_cnt = model.columnCount(first) + max_width = [0] * col_cnt + + indent_sz = 2 + indent_str = " " * indent_sz + + expanded_mark_sz = 2 + if sys.version_info[0] == 3: + expanded_mark = "\u25BC " + not_expanded_mark = "\u25B6 " + else: + expanded_mark = chr(0x25BC) + " " + not_expanded_mark = chr(0x25B6) + " " + leaf_mark = " " + + if not as_csv: + pos = first + while True: + row_cnt += 1 + row = pos.row() + for c in range(col_cnt): + i = pos.sibling(row, c) + if c: + n = len(str(i.data())) + else: + n = len(str(i.data()).strip()) + n += (i.internalPointer().level - 1) * indent_sz + n += expanded_mark_sz + max_width[c] = max(max_width[c], n) + pos = view.indexBelow(pos) + if not selection.isSelected(pos): + break + + text = "" + pad = "" + sep = "" + if with_hdr: + for c in range(col_cnt): + val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip() + if as_csv: + text += sep + ToCSValue(val) + sep = "," + else: + max_width[c] = max(max_width[c], len(val)) + width = max_width[c] + align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole) + if align & Qt.AlignRight: + val = val.rjust(width) + text += pad + sep + val + pad = " " * (width - len(val)) + sep = " " + text += "\n" + pad = "" + sep = "" + + pos = first + while True: + row = pos.row() + for c in range(col_cnt): + i = pos.sibling(row, c) + val = str(i.data()) + if not c: + if model.hasChildren(i): + if view.isExpanded(i): + mark = expanded_mark + else: + mark = not_expanded_mark + else: + mark = leaf_mark + val = indent_str * (i.internalPointer().level - 1) + mark + val.strip() + if as_csv: + text += sep + ToCSValue(val) + sep = "," + else: + width = max_width[c] + if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight: + val = val.rjust(width) + text += pad + sep + val + pad = " " * (width - len(val)) + sep = " " + pos = view.indexBelow(pos) + if not selection.isSelected(pos): + break + text = text.rstrip() + "\n" + pad = "" + sep = "" + + QApplication.clipboard().setText(text) + +def CopyCellsToClipboard(view, as_csv=False, with_hdr=False): + view.CopyCellsToClipboard(view, as_csv, with_hdr) + +def CopyCellsToClipboardHdr(view): + CopyCellsToClipboard(view, False, True) + +def CopyCellsToClipboardCSV(view): + CopyCellsToClipboard(view, True, True) + +# Context menu + +class ContextMenu(object): + + def __init__(self, view): + self.view = view + self.view.setContextMenuPolicy(Qt.CustomContextMenu) + self.view.customContextMenuRequested.connect(self.ShowContextMenu) + + def ShowContextMenu(self, pos): + menu = QMenu(self.view) + self.AddActions(menu) + menu.exec_(self.view.mapToGlobal(pos)) + + def AddCopy(self, menu): + menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view)) + menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view)) + + def AddActions(self, menu): + self.AddCopy(menu) + +class TreeContextMenu(ContextMenu): + + def AddActions(self, menu): + i = self.view.currentIndex() + text = str(i.data()).strip() + if len(text): + menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view)) + self.AddCopy(menu) + +# Table window + +class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase): + + def __init__(self, glb, table_name, parent=None): + super(TableWindow, self).__init__(parent) + + self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name)) + + self.model = QSortFilterProxyModel() + self.model.setSourceModel(self.data_model) + + self.view = QTableView() + self.view.setModel(self.model) + self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.view.verticalHeader().setVisible(False) + self.view.sortByColumn(-1, Qt.AscendingOrder) + self.view.setSortingEnabled(True) + self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) + self.view.CopyCellsToClipboard = CopyTableCellsToClipboard + + self.ResizeColumnsToContents() + + self.context_menu = ContextMenu(self.view) + + self.find_bar = FindBar(self, self, True) + + self.finder = ChildDataItemFinder(self.data_model) + + self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) + + self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) + + self.setWidget(self.vbox.Widget()) + + AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table") + + def Find(self, value, direction, pattern, context): + self.view.setFocus() + self.find_bar.Busy() + self.finder.Find(value, direction, pattern, context, self.FindDone) + + def FindDone(self, row): + self.find_bar.Idle() + if row >= 0: + self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex()))) + else: + self.find_bar.NotFound() + +# Table list + +def GetTableList(glb): + tables = [] + query = QSqlQuery(glb.db) + if glb.dbref.is_sqlite3: + QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name") + else: + QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name") + while query.next(): + tables.append(query.value(0)) + if glb.dbref.is_sqlite3: + tables.append("sqlite_master") + else: + tables.append("information_schema.tables") + tables.append("information_schema.views") + tables.append("information_schema.columns") + return tables + +# Top Calls data model + +class TopCallsModel(SQLTableModel): + + def __init__(self, glb, report_vars, parent=None): + text = "" + if not glb.dbref.is_sqlite3: + text = "::text" + limit = "" + if len(report_vars.limit): + limit = " LIMIT " + report_vars.limit + sql = ("SELECT comm, pid, tid, name," + " CASE" + " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text + + " ELSE short_name" + " END AS dso," + " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, " + " CASE" + " WHEN (calls.flags = 1) THEN 'no call'" + text + + " WHEN (calls.flags = 2) THEN 'no return'" + text + + " WHEN (calls.flags = 3) THEN 'no call/return'" + text + + " ELSE ''" + text + + " END AS flags" + " FROM calls" + " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" + " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" + " INNER JOIN dsos ON symbols.dso_id = dsos.id" + " INNER JOIN comms ON calls.comm_id = comms.id" + " INNER JOIN threads ON calls.thread_id = threads.id" + + report_vars.where_clause + + " ORDER BY elapsed_time DESC" + + limit + ) + column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags") + self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft) + super(TopCallsModel, self).__init__(glb, sql, column_headers, parent) + + def columnAlignment(self, column): + return self.alignment[column] + +# Top Calls report creation dialog + +class TopCallsDialog(ReportDialogBase): + + def __init__(self, glb, parent=None): + title = "Top Calls by Elapsed Time" + items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), + lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p), + lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p), + lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p), + lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p), + lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p), + lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p), + lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100")) + super(TopCallsDialog, self).__init__(glb, title, items, False, parent) + +# Top Calls window + +class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase): + + def __init__(self, glb, report_vars, parent=None): + super(TopCallsWindow, self).__init__(parent) + + self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars)) + self.model = self.data_model + + self.view = QTableView() + self.view.setModel(self.model) + self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.view.verticalHeader().setVisible(False) + self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) + self.view.CopyCellsToClipboard = CopyTableCellsToClipboard + + self.context_menu = ContextMenu(self.view) + + self.ResizeColumnsToContents() + + self.find_bar = FindBar(self, self, True) + + self.finder = ChildDataItemFinder(self.model) + + self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) + + self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) + + self.setWidget(self.vbox.Widget()) + + AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name) + + def Find(self, value, direction, pattern, context): + self.view.setFocus() + self.find_bar.Busy() + self.finder.Find(value, direction, pattern, context, self.FindDone) + + def FindDone(self, row): + self.find_bar.Idle() + if row >= 0: + self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) + else: + self.find_bar.NotFound() + +# Action Definition + +def CreateAction(label, tip, callback, parent=None, shortcut=None): + action = QAction(label, parent) + if shortcut != None: + action.setShortcuts(shortcut) + action.setStatusTip(tip) + action.triggered.connect(callback) + return action + +# Typical application actions + +def CreateExitAction(app, parent=None): + return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) + +# Typical MDI actions + +def CreateCloseActiveWindowAction(mdi_area): + return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) + +def CreateCloseAllWindowsAction(mdi_area): + return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) + +def CreateTileWindowsAction(mdi_area): + return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) + +def CreateCascadeWindowsAction(mdi_area): + return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) + +def CreateNextWindowAction(mdi_area): + return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) + +def CreatePreviousWindowAction(mdi_area): + return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) + +# Typical MDI window menu + +class WindowMenu(): + + def __init__(self, mdi_area, menu): + self.mdi_area = mdi_area + self.window_menu = menu.addMenu("&Windows") + self.close_active_window = CreateCloseActiveWindowAction(mdi_area) + self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) + self.tile_windows = CreateTileWindowsAction(mdi_area) + self.cascade_windows = CreateCascadeWindowsAction(mdi_area) + self.next_window = CreateNextWindowAction(mdi_area) + self.previous_window = CreatePreviousWindowAction(mdi_area) + self.window_menu.aboutToShow.connect(self.Update) + + def Update(self): + self.window_menu.clear() + sub_window_count = len(self.mdi_area.subWindowList()) + have_sub_windows = sub_window_count != 0 + self.close_active_window.setEnabled(have_sub_windows) + self.close_all_windows.setEnabled(have_sub_windows) + self.tile_windows.setEnabled(have_sub_windows) + self.cascade_windows.setEnabled(have_sub_windows) + self.next_window.setEnabled(have_sub_windows) + self.previous_window.setEnabled(have_sub_windows) + self.window_menu.addAction(self.close_active_window) + self.window_menu.addAction(self.close_all_windows) + self.window_menu.addSeparator() + self.window_menu.addAction(self.tile_windows) + self.window_menu.addAction(self.cascade_windows) + self.window_menu.addSeparator() + self.window_menu.addAction(self.next_window) + self.window_menu.addAction(self.previous_window) + if sub_window_count == 0: + return + self.window_menu.addSeparator() + nr = 1 + for sub_window in self.mdi_area.subWindowList(): + label = str(nr) + " " + sub_window.name + if nr < 10: + label = "&" + label + action = self.window_menu.addAction(label) + action.setCheckable(True) + action.setChecked(sub_window == self.mdi_area.activeSubWindow()) + action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x)) + self.window_menu.addAction(action) + nr += 1 + + def setActiveSubWindow(self, nr): + self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) + +# Help text + +glb_help_text = """ +

Contents

+ +

1. Reports

+

1.1 Context-Sensitive Call Graph

+

1.2 Call Tree

+

1.3 All branches

+

1.4 Selected branches

+

1.5 Top calls by elapsed time

+

2. Charts

+

2.1 Time chart by CPU

+

3. Tables

+

1. Reports

+

1.1 Context-Sensitive Call Graph

+The result is a GUI window with a tree representing a context-sensitive +call-graph. Expanding a couple of levels of the tree and adjusting column +widths to suit will display something like: +
+                                         Call Graph: pt_example
+Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
+v- ls
+    v- 2638:2638
+        v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
+          |- unknown               unknown       1        13198     0.1              1              0.0
+          >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
+          >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
+          v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
+             >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
+             >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
+             >- __libc_csu_init    ls            1        10354     0.1             10              0.0
+             |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
+             v- main               ls            1      8182043    99.6         180254             99.9
+
+

Points to note:

+
    +
  • The top level is a command name (comm)
  • +
  • The next level is a thread (pid:tid)
  • +
  • Subsequent levels are functions
  • +
  • 'Count' is the number of calls
  • +
  • 'Time' is the elapsed time until the function returns
  • +
  • Percentages are relative to the level above
  • +
  • 'Branch Count' is the total number of branches for that function and all functions that it calls +
+

Find

+Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match. +The pattern matching symbols are ? for any character and * for zero or more characters. +

1.2 Call Tree

+The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated. +Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'. +

1.3 All branches

+The All branches report displays all branches in chronological order. +Not all data is fetched immediately. More records can be fetched using the Fetch bar provided. +

Disassembly

+Open a branch to display disassembly. This only works if: +
    +
  1. The disassembler is available. Currently, only Intel XED is supported - see Intel XED Setup
  2. +
  3. The object code is available. Currently, only the perf build ID cache is searched for object code. +The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR. +One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu), +or alternatively, set environment variable PERF_KCORE to the kcore file name.
  4. +
+

Intel XED Setup

+To use Intel XED, libxed.so must be present. To build and install libxed.so: +
+git clone https://github.com/intelxed/mbuild.git mbuild
+git clone https://github.com/intelxed/xed
+cd xed
+./mfile.py --share
+sudo ./mfile.py --prefix=/usr/local install
+sudo ldconfig
+
+

Instructions per Cycle (IPC)

+If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'. +

Intel PT note: The information applies to the blocks of code ending with, and including, that branch. +Due to the granularity of timing information, the number of cycles for some code blocks will not be known. +In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period +since the previous displayed 'IPC'. +

Find

+Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. +Refer to Python documentation for the regular expression syntax. +All columns are searched, but only currently fetched rows are searched. +

1.4 Selected branches

+This is the same as the All branches report but with the data reduced +by various selection criteria. A dialog box displays available criteria which are AND'ed together. +

1.4.1 Time ranges

+The time ranges hint text shows the total time range. Relative time ranges can also be entered in +ms, us or ns. Also, negative values are relative to the end of trace. Examples: +
+    81073085947329-81073085958238	From 81073085947329 to 81073085958238
+    100us-200us		From 100us to 200us
+    10ms-			From 10ms to the end
+    -100ns			The first 100ns
+    -10ms-			The last 10ms
+
+N.B. Due to the granularity of timestamps, there could be no branches in any given time range. +

1.5 Top calls by elapsed time

+The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned. +The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together. +If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar. +

2. Charts

+

2.1 Time chart by CPU

+This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu. +

Features

+
    +
  1. Mouse over to highight the task and show the time
  2. +
  3. Drag the mouse to select a region and zoom by pushing the Zoom button
  4. +
  5. Go back and forward by pressing the arrow buttons
  6. +
  7. If call information is available, right-click to show a call tree opened to that task and time. +Note, the call tree may take some time to appear, and there may not be call information for the task or time selected. +
  8. +
+

Important

+The graph can be misleading in the following respects: +
    +
  1. The graph shows the first task on each CPU as running from the beginning of the time range. +Because tracing might start on different CPUs at different times, that is not necessarily the case. +Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.
  2. +
  3. Similarly, the last task on each CPU can be showing running longer than it really was. +Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.
  4. +
  5. When the mouse is over a task, the highlighted task might not be visible on the legend without scrolling if the legend does not fit fully in the window
  6. +
+

3. Tables

+The Tables menu shows all tables and views in the database. Most tables have an associated view +which displays the information in a more friendly way. Not all data for large tables is fetched +immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted, +but that can be slow for large tables. +

There are also tables of database meta-information. +For SQLite3 databases, the sqlite_master table is included. +For PostgreSQL databases, information_schema.tables/views/columns are included. +

Find

+Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. +Refer to Python documentation for the regular expression syntax. +All columns are searched, but only currently fetched rows are searched. +

N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous +will go to the next/previous result in id order, instead of display order. +""" + +# Help window + +class HelpWindow(QMdiSubWindow): + + def __init__(self, glb, parent=None): + super(HelpWindow, self).__init__(parent) + + self.text = QTextBrowser() + self.text.setHtml(glb_help_text) + self.text.setReadOnly(True) + self.text.setOpenExternalLinks(True) + + self.setWidget(self.text) + + AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help") + +# Main window that only displays the help text + +class HelpOnlyWindow(QMainWindow): + + def __init__(self, parent=None): + super(HelpOnlyWindow, self).__init__(parent) + + self.setMinimumSize(200, 100) + self.resize(800, 600) + self.setWindowTitle("Exported SQL Viewer Help") + self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation)) + + self.text = QTextBrowser() + self.text.setHtml(glb_help_text) + self.text.setReadOnly(True) + self.text.setOpenExternalLinks(True) + + self.setCentralWidget(self.text) + +# PostqreSQL server version + +def PostqreSQLServerVersion(db): + query = QSqlQuery(db) + QueryExec(query, "SELECT VERSION()") + if query.next(): + v_str = query.value(0) + v_list = v_str.strip().split(" ") + if v_list[0] == "PostgreSQL" and v_list[2] == "on": + return v_list[1] + return v_str + return "Unknown" + +# SQLite version + +def SQLiteVersion(db): + query = QSqlQuery(db) + QueryExec(query, "SELECT sqlite_version()") + if query.next(): + return query.value(0) + return "Unknown" + +# About dialog + +class AboutDialog(QDialog): + + def __init__(self, glb, parent=None): + super(AboutDialog, self).__init__(parent) + + self.setWindowTitle("About Exported SQL Viewer") + self.setMinimumWidth(300) + + pyside_version = "1" if pyside_version_1 else "2" + + text = "

"
+        text += "Python version:     " + sys.version.split(" ")[0] + "\n"
+        text += "PySide version:     " + pyside_version + "\n"
+        text += "Qt version:         " + qVersion() + "\n"
+        if glb.dbref.is_sqlite3:
+            text += "SQLite version:     " + SQLiteVersion(glb.db) + "\n"
+        else:
+            text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
+        text += "
" + + self.text = QTextBrowser() + self.text.setHtml(text) + self.text.setReadOnly(True) + self.text.setOpenExternalLinks(True) + + self.vbox = QVBoxLayout() + self.vbox.addWidget(self.text) + + self.setLayout(self.vbox) + +# Font resize + +def ResizeFont(widget, diff): + font = widget.font() + sz = font.pointSize() + font.setPointSize(sz + diff) + widget.setFont(font) + +def ShrinkFont(widget): + ResizeFont(widget, -1) + +def EnlargeFont(widget): + ResizeFont(widget, 1) + +# Unique name for sub-windows + +def NumberedWindowName(name, nr): + if nr > 1: + name += " <" + str(nr) + ">" + return name + +def UniqueSubWindowName(mdi_area, name): + nr = 1 + while True: + unique_name = NumberedWindowName(name, nr) + ok = True + for sub_window in mdi_area.subWindowList(): + if sub_window.name == unique_name: + ok = False + break + if ok: + return unique_name + nr += 1 + +# Add a sub-window + +def AddSubWindow(mdi_area, sub_window, name): + unique_name = UniqueSubWindowName(mdi_area, name) + sub_window.setMinimumSize(200, 100) + sub_window.resize(800, 600) + sub_window.setWindowTitle(unique_name) + sub_window.setAttribute(Qt.WA_DeleteOnClose) + sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) + sub_window.name = unique_name + mdi_area.addSubWindow(sub_window) + sub_window.show() + +# Main window + +class MainWindow(QMainWindow): + + def __init__(self, glb, parent=None): + super(MainWindow, self).__init__(parent) + + self.glb = glb + + self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) + self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) + self.setMinimumSize(200, 100) + + self.mdi_area = QMdiArea() + self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + self.setCentralWidget(self.mdi_area) + + menu = self.menuBar() + + file_menu = menu.addMenu("&File") + file_menu.addAction(CreateExitAction(glb.app, self)) + + edit_menu = menu.addMenu("&Edit") + edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy)) + edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self)) + edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find)) + edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)])) + edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")])) + edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")])) + + reports_menu = menu.addMenu("&Reports") + if IsSelectable(glb.db, "calls"): + reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) + + if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"): + reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self)) + + self.EventMenu(GetEventList(glb.db), reports_menu) + + if IsSelectable(glb.db, "calls"): + reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self)) + + if IsSelectable(glb.db, "context_switches"): + charts_menu = menu.addMenu("&Charts") + charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self)) + + self.TableMenu(GetTableList(glb), menu) + + self.window_menu = WindowMenu(self.mdi_area, menu) + + help_menu = menu.addMenu("&Help") + help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents)) + help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self)) + + def Try(self, fn): + win = self.mdi_area.activeSubWindow() + if win: + try: + fn(win.view) + except AttributeError: + pass + + def CopyToClipboard(self): + self.Try(CopyCellsToClipboardHdr) + + def CopyToClipboardCSV(self): + self.Try(CopyCellsToClipboardCSV) + + def Find(self): + win = self.mdi_area.activeSubWindow() + if win: + try: + win.find_bar.Activate() + except AttributeError: + pass + + def FetchMoreRecords(self): + win = self.mdi_area.activeSubWindow() + if win: + try: + win.fetch_bar.Activate() + except AttributeError: + pass + + def ShrinkFont(self): + self.Try(ShrinkFont) + + def EnlargeFont(self): + self.Try(EnlargeFont) + + def EventMenu(self, events, reports_menu): + branches_events = 0 + for event in events: + event = event.split(":")[0] + if event == "branches": + branches_events += 1 + dbid = 0 + for event in events: + dbid += 1 + event = event.split(":")[0] + if event == "branches": + label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")" + reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self)) + label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")" + reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self)) + + def TimeChartByCPU(self): + TimeChartByCPUWindow(self.glb, self) + + def TableMenu(self, tables, menu): + table_menu = menu.addMenu("&Tables") + for table in tables: + table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self)) + + def NewCallGraph(self): + CallGraphWindow(self.glb, self) + + def NewCallTree(self): + CallTreeWindow(self.glb, self) + + def NewTopCalls(self): + dialog = TopCallsDialog(self.glb, self) + ret = dialog.exec_() + if ret: + TopCallsWindow(self.glb, dialog.report_vars, self) + + def NewBranchView(self, event_id): + BranchWindow(self.glb, event_id, ReportVars(), self) + + def NewSelectedBranchView(self, event_id): + dialog = SelectedBranchDialog(self.glb, self) + ret = dialog.exec_() + if ret: + BranchWindow(self.glb, event_id, dialog.report_vars, self) + + def NewTableView(self, table_name): + TableWindow(self.glb, table_name, self) + + def Help(self): + HelpWindow(self.glb, self) + + def About(self): + dialog = AboutDialog(self.glb, self) + dialog.exec_() + +def TryOpen(file_name): + try: + return open(file_name, "rb") + except OSError: + return None + +def Is64Bit(f): + result = sizeof(c_void_p) + # ELF support only + pos = f.tell() + f.seek(0) + header = f.read(7) + f.seek(pos) + magic = header[0:4] + if sys.version_info[0] == 2: + eclass = ord(header[4]) + encoding = ord(header[5]) + version = ord(header[6]) + else: + eclass = header[4] + encoding = header[5] + version = header[6] + if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1: + result = True if eclass == 2 else False + return result + +# Global data + +class Glb(): + + def __init__(self, dbref, db, dbname): + self.dbref = dbref + self.db = db + self.dbname = dbname + self.home_dir = os.path.expanduser("~") + self.buildid_dir = os.getenv("PERF_BUILDID_DIR") + if self.buildid_dir: + self.buildid_dir += "/.build-id/" + else: + self.buildid_dir = self.home_dir + "/.debug/.build-id/" + self.app = None + self.mainwindow = None + self.instances_to_shutdown_on_exit = weakref.WeakSet() + try: + self.disassembler = LibXED() + self.have_disassembler = True + except (OSError, RuntimeError, AttributeError): + self.have_disassembler = False + self.host_machine_id = 0 + self.host_start_time = 0 + self.host_finish_time = 0 + + def FileFromBuildId(self, build_id): + file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf" + return TryOpen(file_name) + + def FileFromNamesAndBuildId(self, short_name, long_name, build_id): + # Assume current machine i.e. no support for virtualization + if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore": + file_name = os.getenv("PERF_KCORE") + f = TryOpen(file_name) if file_name else None + if f: + return f + # For now, no special handling if long_name is /proc/kcore + f = TryOpen(long_name) + if f: + return f + f = self.FileFromBuildId(build_id) + if f: + return f + return None + + def AddInstanceToShutdownOnExit(self, instance): + self.instances_to_shutdown_on_exit.add(instance) + + # Shutdown any background processes or threads + def ShutdownInstances(self): + for x in self.instances_to_shutdown_on_exit: + try: + x.Shutdown() + except AttributeError: + pass + + def GetHostMachineId(self): + query = QSqlQuery(self.db) + QueryExec(query, "SELECT id FROM machines WHERE pid = -1") + if query.next(): + self.host_machine_id = query.value(0) + else: + self.host_machine_id = 0 + return self.host_machine_id + + def HostMachineId(self): + if self.host_machine_id: + return self.host_machine_id + return self.GetHostMachineId() + + def SelectValue(self, sql): + query = QSqlQuery(self.db) + try: + QueryExec(query, sql) + except RuntimeError: + return None + if query.next(): + return Decimal(query.value(0)) + return None + + def SwitchesMinTime(self, machine_id): + return self.SelectValue("SELECT time" + " FROM context_switches" + " WHERE time != 0 AND machine_id = " + str(machine_id) + + " ORDER BY id LIMIT 1") + + def SwitchesMaxTime(self, machine_id): + return self.SelectValue("SELECT time" + " FROM context_switches" + " WHERE time != 0 AND machine_id = " + str(machine_id) + + " ORDER BY id DESC LIMIT 1") + + def SamplesMinTime(self, machine_id): + return self.SelectValue("SELECT time" + " FROM samples" + " WHERE time != 0 AND machine_id = " + str(machine_id) + + " ORDER BY id LIMIT 1") + + def SamplesMaxTime(self, machine_id): + return self.SelectValue("SELECT time" + " FROM samples" + " WHERE time != 0 AND machine_id = " + str(machine_id) + + " ORDER BY id DESC LIMIT 1") + + def CallsMinTime(self, machine_id): + return self.SelectValue("SELECT calls.call_time" + " FROM calls" + " INNER JOIN threads ON threads.id = calls.thread_id" + " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) + + " ORDER BY calls.id LIMIT 1") + + def CallsMaxTime(self, machine_id): + return self.SelectValue("SELECT calls.return_time" + " FROM calls" + " INNER JOIN threads ON threads.id = calls.thread_id" + " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) + + " ORDER BY calls.return_time DESC LIMIT 1") + + def GetStartTime(self, machine_id): + t0 = self.SwitchesMinTime(machine_id) + t1 = self.SamplesMinTime(machine_id) + t2 = self.CallsMinTime(machine_id) + if t0 is None or (not(t1 is None) and t1 < t0): + t0 = t1 + if t0 is None or (not(t2 is None) and t2 < t0): + t0 = t2 + return t0 + + def GetFinishTime(self, machine_id): + t0 = self.SwitchesMaxTime(machine_id) + t1 = self.SamplesMaxTime(machine_id) + t2 = self.CallsMaxTime(machine_id) + if t0 is None or (not(t1 is None) and t1 > t0): + t0 = t1 + if t0 is None or (not(t2 is None) and t2 > t0): + t0 = t2 + return t0 + + def HostStartTime(self): + if self.host_start_time: + return self.host_start_time + self.host_start_time = self.GetStartTime(self.HostMachineId()) + return self.host_start_time + + def HostFinishTime(self): + if self.host_finish_time: + return self.host_finish_time + self.host_finish_time = self.GetFinishTime(self.HostMachineId()) + return self.host_finish_time + + def StartTime(self, machine_id): + if machine_id == self.HostMachineId(): + return self.HostStartTime() + return self.GetStartTime(machine_id) + + def FinishTime(self, machine_id): + if machine_id == self.HostMachineId(): + return self.HostFinishTime() + return self.GetFinishTime(machine_id) + +# Database reference + +class DBRef(): + + def __init__(self, is_sqlite3, dbname): + self.is_sqlite3 = is_sqlite3 + self.dbname = dbname + self.TRUE = "TRUE" + self.FALSE = "FALSE" + # SQLite prior to version 3.23 does not support TRUE and FALSE + if self.is_sqlite3: + self.TRUE = "1" + self.FALSE = "0" + + def Open(self, connection_name): + dbname = self.dbname + if self.is_sqlite3: + db = QSqlDatabase.addDatabase("QSQLITE", connection_name) + else: + db = QSqlDatabase.addDatabase("QPSQL", connection_name) + opts = dbname.split() + for opt in opts: + if "=" in opt: + opt = opt.split("=") + if opt[0] == "hostname": + db.setHostName(opt[1]) + elif opt[0] == "port": + db.setPort(int(opt[1])) + elif opt[0] == "username": + db.setUserName(opt[1]) + elif opt[0] == "password": + db.setPassword(opt[1]) + elif opt[0] == "dbname": + dbname = opt[1] + else: + dbname = opt + + db.setDatabaseName(dbname) + if not db.open(): + raise RuntimeError("Failed to open database " + dbname + " error: " + db.lastError().text()) + return db, dbname + +# Main + +def Main(): + usage_str = "exported-sql-viewer.py [--pyside-version-1] \n" \ + " or: exported-sql-viewer.py --help-only" + ap = argparse.ArgumentParser(usage = usage_str, add_help = False) + ap.add_argument("--pyside-version-1", action='store_true') + ap.add_argument("dbname", nargs="?") + ap.add_argument("--help-only", action='store_true') + args = ap.parse_args() + + if args.help_only: + app = QApplication(sys.argv) + mainwindow = HelpOnlyWindow() + mainwindow.show() + err = app.exec_() + sys.exit(err) + + dbname = args.dbname + if dbname is None: + ap.print_usage() + print("Too few arguments") + sys.exit(1) + + is_sqlite3 = False + try: + f = open(dbname, "rb") + if f.read(15) == b'SQLite format 3': + is_sqlite3 = True + f.close() + except OSError: + pass + + dbref = DBRef(is_sqlite3, dbname) + db, dbname = dbref.Open("main") + glb = Glb(dbref, db, dbname) + app = QApplication(sys.argv) + glb.app = app + mainwindow = MainWindow(glb) + glb.mainwindow = mainwindow + mainwindow.show() + err = app.exec_() + glb.ShutdownInstances() + db.close() + sys.exit(err) + +if __name__ == "__main__": + Main() diff --git a/tools/perf/scripts/python/exported-sql-viewer.py b/tools/perf/scripts/python/exported-sql-viewer.py deleted file mode 100755 index e0b2e7268ef6..000000000000 --- a/tools/perf/scripts/python/exported-sql-viewer.py +++ /dev/null @@ -1,5030 +0,0 @@ -#!/usr/bin/env python -# SPDX-License-Identifier: GPL-2.0 -# exported-sql-viewer.py: view data from sql database -# Copyright (c) 2014-2018, Intel Corporation. - -# To use this script you will need to have exported data using either the -# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those -# scripts for details. -# -# Following on from the example in the export scripts, a -# call-graph can be displayed for the pt_example database like this: -# -# python tools/perf/scripts/python/exported-sql-viewer.py pt_example -# -# Note that for PostgreSQL, this script supports connecting to remote databases -# by setting hostname, port, username, password, and dbname e.g. -# -# python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example" -# -# The result is a GUI window with a tree representing a context-sensitive -# call-graph. Expanding a couple of levels of the tree and adjusting column -# widths to suit will display something like: -# -# Call Graph: pt_example -# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) -# v- ls -# v- 2638:2638 -# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 -# |- unknown unknown 1 13198 0.1 1 0.0 -# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 -# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 -# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 -# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 -# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 -# >- __libc_csu_init ls 1 10354 0.1 10 0.0 -# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 -# v- main ls 1 8182043 99.6 180254 99.9 -# -# Points to note: -# The top level is a command name (comm) -# The next level is a thread (pid:tid) -# Subsequent levels are functions -# 'Count' is the number of calls -# 'Time' is the elapsed time until the function returns -# Percentages are relative to the level above -# 'Branch Count' is the total number of branches for that function and all -# functions that it calls - -# There is also a "All branches" report, which displays branches and -# possibly disassembly. However, presently, the only supported disassembler is -# Intel XED, and additionally the object code must be present in perf build ID -# cache. To use Intel XED, libxed.so must be present. To build and install -# libxed.so: -# git clone https://github.com/intelxed/mbuild.git mbuild -# git clone https://github.com/intelxed/xed -# cd xed -# ./mfile.py --share -# sudo ./mfile.py --prefix=/usr/local install -# sudo ldconfig -# -# Example report: -# -# Time CPU Command PID TID Branch Type In Tx Branch -# 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) -# 7fab593ea260 48 89 e7 mov %rsp, %rdi -# 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) -# 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) -# 7fab593ea260 48 89 e7 mov %rsp, %rdi -# 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930 -# 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so) -# 7fab593ea930 55 pushq %rbp -# 7fab593ea931 48 89 e5 mov %rsp, %rbp -# 7fab593ea934 41 57 pushq %r15 -# 7fab593ea936 41 56 pushq %r14 -# 7fab593ea938 41 55 pushq %r13 -# 7fab593ea93a 41 54 pushq %r12 -# 7fab593ea93c 53 pushq %rbx -# 7fab593ea93d 48 89 fb mov %rdi, %rbx -# 7fab593ea940 48 83 ec 68 sub $0x68, %rsp -# 7fab593ea944 0f 31 rdtsc -# 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx -# 7fab593ea94a 89 c0 mov %eax, %eax -# 7fab593ea94c 48 09 c2 or %rax, %rdx -# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax -# 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) -# 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax -# 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip) -# 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) - -from __future__ import print_function - -import sys -# Only change warnings if the python -W option was not used -if not sys.warnoptions: - import warnings - # PySide2 causes deprecation warnings, ignore them. - warnings.filterwarnings("ignore", category=DeprecationWarning) -import argparse -import weakref -import threading -import string -try: - # Python2 - import cPickle as pickle - # size of pickled integer big enough for record size - glb_nsz = 8 -except ImportError: - import pickle - glb_nsz = 16 -import re -import os -import random -import copy -import math -from libxed import LibXED - -pyside_version_1 = True -if not "--pyside-version-1" in sys.argv: - try: - from PySide2.QtCore import * - from PySide2.QtGui import * - from PySide2.QtSql import * - from PySide2.QtWidgets import * - pyside_version_1 = False - except: - pass - -if pyside_version_1: - from PySide.QtCore import * - from PySide.QtGui import * - from PySide.QtSql import * - -from decimal import Decimal, ROUND_HALF_UP -from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \ - c_void_p, c_bool, c_byte, c_char, c_int, c_uint, c_longlong, c_ulonglong -from multiprocessing import Process, Array, Value, Event - -# xrange is range in Python3 -try: - xrange -except NameError: - xrange = range - -def printerr(*args, **keyword_args): - print(*args, file=sys.stderr, **keyword_args) - -# Data formatting helpers - -def tohex(ip): - if ip < 0: - ip += 1 << 64 - return "%x" % ip - -def offstr(offset): - if offset: - return "+0x%x" % offset - return "" - -def dsoname(name): - if name == "[kernel.kallsyms]": - return "[kernel]" - return name - -def findnth(s, sub, n, offs=0): - pos = s.find(sub) - if pos < 0: - return pos - if n <= 1: - return offs + pos - return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1) - -# Percent to one decimal place - -def PercentToOneDP(n, d): - if not d: - return "0.0" - x = (n * Decimal(100)) / d - return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) - -# Helper for queries that must not fail - -def QueryExec(query, stmt): - ret = query.exec_(stmt) - if not ret: - raise Exception("Query failed: " + query.lastError().text()) - -# Background thread - -class Thread(QThread): - - done = Signal(object) - - def __init__(self, task, param=None, parent=None): - super(Thread, self).__init__(parent) - self.task = task - self.param = param - - def run(self): - while True: - if self.param is None: - done, result = self.task() - else: - done, result = self.task(self.param) - self.done.emit(result) - if done: - break - -# Tree data model - -class TreeModel(QAbstractItemModel): - - def __init__(self, glb, params, parent=None): - super(TreeModel, self).__init__(parent) - self.glb = glb - self.params = params - self.root = self.GetRoot() - self.last_row_read = 0 - - def Item(self, parent): - if parent.isValid(): - return parent.internalPointer() - else: - return self.root - - def rowCount(self, parent): - result = self.Item(parent).childCount() - if result < 0: - result = 0 - self.dataChanged.emit(parent, parent) - return result - - def hasChildren(self, parent): - return self.Item(parent).hasChildren() - - def headerData(self, section, orientation, role): - if role == Qt.TextAlignmentRole: - return self.columnAlignment(section) - if role != Qt.DisplayRole: - return None - if orientation != Qt.Horizontal: - return None - return self.columnHeader(section) - - def parent(self, child): - child_item = child.internalPointer() - if child_item is self.root: - return QModelIndex() - parent_item = child_item.getParentItem() - return self.createIndex(parent_item.getRow(), 0, parent_item) - - def index(self, row, column, parent): - child_item = self.Item(parent).getChildItem(row) - return self.createIndex(row, column, child_item) - - def DisplayData(self, item, index): - return item.getData(index.column()) - - def FetchIfNeeded(self, row): - if row > self.last_row_read: - self.last_row_read = row - if row + 10 >= self.root.child_count: - self.fetcher.Fetch(glb_chunk_sz) - - def columnAlignment(self, column): - return Qt.AlignLeft - - def columnFont(self, column): - return None - - def data(self, index, role): - if role == Qt.TextAlignmentRole: - return self.columnAlignment(index.column()) - if role == Qt.FontRole: - return self.columnFont(index.column()) - if role != Qt.DisplayRole: - return None - item = index.internalPointer() - return self.DisplayData(item, index) - -# Table data model - -class TableModel(QAbstractTableModel): - - def __init__(self, parent=None): - super(TableModel, self).__init__(parent) - self.child_count = 0 - self.child_items = [] - self.last_row_read = 0 - - def Item(self, parent): - if parent.isValid(): - return parent.internalPointer() - else: - return self - - def rowCount(self, parent): - return self.child_count - - def headerData(self, section, orientation, role): - if role == Qt.TextAlignmentRole: - return self.columnAlignment(section) - if role != Qt.DisplayRole: - return None - if orientation != Qt.Horizontal: - return None - return self.columnHeader(section) - - def index(self, row, column, parent): - return self.createIndex(row, column, self.child_items[row]) - - def DisplayData(self, item, index): - return item.getData(index.column()) - - def FetchIfNeeded(self, row): - if row > self.last_row_read: - self.last_row_read = row - if row + 10 >= self.child_count: - self.fetcher.Fetch(glb_chunk_sz) - - def columnAlignment(self, column): - return Qt.AlignLeft - - def columnFont(self, column): - return None - - def data(self, index, role): - if role == Qt.TextAlignmentRole: - return self.columnAlignment(index.column()) - if role == Qt.FontRole: - return self.columnFont(index.column()) - if role != Qt.DisplayRole: - return None - item = index.internalPointer() - return self.DisplayData(item, index) - -# Model cache - -model_cache = weakref.WeakValueDictionary() -model_cache_lock = threading.Lock() - -def LookupCreateModel(model_name, create_fn): - model_cache_lock.acquire() - try: - model = model_cache[model_name] - except: - model = None - if model is None: - model = create_fn() - model_cache[model_name] = model - model_cache_lock.release() - return model - -def LookupModel(model_name): - model_cache_lock.acquire() - try: - model = model_cache[model_name] - except: - model = None - model_cache_lock.release() - return model - -# Find bar - -class FindBar(): - - def __init__(self, parent, finder, is_reg_expr=False): - self.finder = finder - self.context = [] - self.last_value = None - self.last_pattern = None - - label = QLabel("Find:") - label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.textbox = QComboBox() - self.textbox.setEditable(True) - self.textbox.currentIndexChanged.connect(self.ValueChanged) - - self.progress = QProgressBar() - self.progress.setRange(0, 0) - self.progress.hide() - - if is_reg_expr: - self.pattern = QCheckBox("Regular Expression") - else: - self.pattern = QCheckBox("Pattern") - self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.next_button = QToolButton() - self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown)) - self.next_button.released.connect(lambda: self.NextPrev(1)) - - self.prev_button = QToolButton() - self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp)) - self.prev_button.released.connect(lambda: self.NextPrev(-1)) - - self.close_button = QToolButton() - self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) - self.close_button.released.connect(self.Deactivate) - - self.hbox = QHBoxLayout() - self.hbox.setContentsMargins(0, 0, 0, 0) - - self.hbox.addWidget(label) - self.hbox.addWidget(self.textbox) - self.hbox.addWidget(self.progress) - self.hbox.addWidget(self.pattern) - self.hbox.addWidget(self.next_button) - self.hbox.addWidget(self.prev_button) - self.hbox.addWidget(self.close_button) - - self.bar = QWidget() - self.bar.setLayout(self.hbox) - self.bar.hide() - - def Widget(self): - return self.bar - - def Activate(self): - self.bar.show() - self.textbox.lineEdit().selectAll() - self.textbox.setFocus() - - def Deactivate(self): - self.bar.hide() - - def Busy(self): - self.textbox.setEnabled(False) - self.pattern.hide() - self.next_button.hide() - self.prev_button.hide() - self.progress.show() - - def Idle(self): - self.textbox.setEnabled(True) - self.progress.hide() - self.pattern.show() - self.next_button.show() - self.prev_button.show() - - def Find(self, direction): - value = self.textbox.currentText() - pattern = self.pattern.isChecked() - self.last_value = value - self.last_pattern = pattern - self.finder.Find(value, direction, pattern, self.context) - - def ValueChanged(self): - value = self.textbox.currentText() - pattern = self.pattern.isChecked() - index = self.textbox.currentIndex() - data = self.textbox.itemData(index) - # Store the pattern in the combo box to keep it with the text value - if data == None: - self.textbox.setItemData(index, pattern) - else: - self.pattern.setChecked(data) - self.Find(0) - - def NextPrev(self, direction): - value = self.textbox.currentText() - pattern = self.pattern.isChecked() - if value != self.last_value: - index = self.textbox.findText(value) - # Allow for a button press before the value has been added to the combo box - if index < 0: - index = self.textbox.count() - self.textbox.addItem(value, pattern) - self.textbox.setCurrentIndex(index) - return - else: - self.textbox.setItemData(index, pattern) - elif pattern != self.last_pattern: - # Keep the pattern recorded in the combo box up to date - index = self.textbox.currentIndex() - self.textbox.setItemData(index, pattern) - self.Find(direction) - - def NotFound(self): - QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found") - -# Context-sensitive call graph data model item base - -class CallGraphLevelItemBase(object): - - def __init__(self, glb, params, row, parent_item): - self.glb = glb - self.params = params - self.row = row - self.parent_item = parent_item - self.query_done = False - self.child_count = 0 - self.child_items = [] - if parent_item: - self.level = parent_item.level + 1 - else: - self.level = 0 - - def getChildItem(self, row): - return self.child_items[row] - - def getParentItem(self): - return self.parent_item - - def getRow(self): - return self.row - - def childCount(self): - if not self.query_done: - self.Select() - if not self.child_count: - return -1 - return self.child_count - - def hasChildren(self): - if not self.query_done: - return True - return self.child_count > 0 - - def getData(self, column): - return self.data[column] - -# Context-sensitive call graph data model level 2+ item base - -class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item): - super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) - self.comm_id = comm_id - self.thread_id = thread_id - self.call_path_id = call_path_id - self.insn_cnt = insn_cnt - self.cyc_cnt = cyc_cnt - self.branch_count = branch_count - self.time = time - - def Select(self): - self.query_done = True - query = QSqlQuery(self.glb.db) - if self.params.have_ipc: - ipc_str = ", SUM(insn_count), SUM(cyc_count)" - else: - ipc_str = "" - QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " INNER JOIN dsos ON symbols.dso_id = dsos.id" - " WHERE parent_call_path_id = " + str(self.call_path_id) + - " AND comm_id = " + str(self.comm_id) + - " AND thread_id = " + str(self.thread_id) + - " GROUP BY call_path_id, name, short_name" - " ORDER BY call_path_id") - while query.next(): - if self.params.have_ipc: - insn_cnt = int(query.value(5)) - cyc_cnt = int(query.value(6)) - branch_count = int(query.value(7)) - else: - insn_cnt = 0 - cyc_cnt = 0 - branch_count = int(query.value(5)) - child_item = CallGraphLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self) - self.child_items.append(child_item) - self.child_count += 1 - -# Context-sensitive call graph data model level three item - -class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item): - super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item) - dso = dsoname(dso) - if self.params.have_ipc: - insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) - cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) - br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) - ipc = CalcIPC(cyc_cnt, insn_cnt) - self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ] - else: - self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] - self.dbid = call_path_id - -# Context-sensitive call graph data model level two item - -class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): - super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item) - if self.params.have_ipc: - self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] - else: - self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] - self.dbid = thread_id - - def Select(self): - super(CallGraphLevelTwoItem, self).Select() - for child_item in self.child_items: - self.time += child_item.time - self.insn_cnt += child_item.insn_cnt - self.cyc_cnt += child_item.cyc_cnt - self.branch_count += child_item.branch_count - for child_item in self.child_items: - child_item.data[4] = PercentToOneDP(child_item.time, self.time) - if self.params.have_ipc: - child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) - child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) - child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) - else: - child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) - -# Context-sensitive call graph data model level one item - -class CallGraphLevelOneItem(CallGraphLevelItemBase): - - def __init__(self, glb, params, row, comm_id, comm, parent_item): - super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item) - if self.params.have_ipc: - self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] - else: - self.data = [comm, "", "", "", "", "", ""] - self.dbid = comm_id - - def Select(self): - self.query_done = True - query = QSqlQuery(self.glb.db) - QueryExec(query, "SELECT thread_id, pid, tid" - " FROM comm_threads" - " INNER JOIN threads ON thread_id = threads.id" - " WHERE comm_id = " + str(self.dbid)) - while query.next(): - child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) - self.child_items.append(child_item) - self.child_count += 1 - -# Context-sensitive call graph data model root item - -class CallGraphRootItem(CallGraphLevelItemBase): - - def __init__(self, glb, params): - super(CallGraphRootItem, self).__init__(glb, params, 0, None) - self.dbid = 0 - self.query_done = True - if_has_calls = "" - if IsSelectable(glb.db, "comms", columns = "has_calls"): - if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE - query = QSqlQuery(glb.db) - QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) - while query.next(): - if not query.value(0): - continue - child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) - self.child_items.append(child_item) - self.child_count += 1 - -# Call graph model parameters - -class CallGraphModelParams(): - - def __init__(self, glb, parent=None): - self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count") - -# Context-sensitive call graph data model base - -class CallGraphModelBase(TreeModel): - - def __init__(self, glb, parent=None): - super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent) - - def FindSelect(self, value, pattern, query): - if pattern: - # postgresql and sqlite pattern patching differences: - # postgresql LIKE is case sensitive but sqlite LIKE is not - # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not - # postgresql supports ILIKE which is case insensitive - # sqlite supports GLOB (text only) which uses * and ? and is case sensitive - if not self.glb.dbref.is_sqlite3: - # Escape % and _ - s = value.replace("%", "\\%") - s = s.replace("_", "\\_") - # Translate * and ? into SQL LIKE pattern characters % and _ - if sys.version_info[0] == 3: - trans = str.maketrans("*?", "%_") - else: - trans = string.maketrans("*?", "%_") - match = " LIKE '" + str(s).translate(trans) + "'" - else: - match = " GLOB '" + str(value) + "'" - else: - match = " = '" + str(value) + "'" - self.DoFindSelect(query, match) - - def Found(self, query, found): - if found: - return self.FindPath(query) - return [] - - def FindValue(self, value, pattern, query, last_value, last_pattern): - if last_value == value and pattern == last_pattern: - found = query.first() - else: - self.FindSelect(value, pattern, query) - found = query.next() - return self.Found(query, found) - - def FindNext(self, query): - found = query.next() - if not found: - found = query.first() - return self.Found(query, found) - - def FindPrev(self, query): - found = query.previous() - if not found: - found = query.last() - return self.Found(query, found) - - def FindThread(self, c): - if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern: - ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern) - elif c.direction > 0: - ids = self.FindNext(c.query) - else: - ids = self.FindPrev(c.query) - return (True, ids) - - def Find(self, value, direction, pattern, context, callback): - class Context(): - def __init__(self, *x): - self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x - def Update(self, *x): - self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern) - if len(context): - context[0].Update(value, direction, pattern) - else: - context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None)) - # Use a thread so the UI is not blocked during the SELECT - thread = Thread(self.FindThread, context[0]) - thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection) - thread.start() - - def FindDone(self, thread, callback, ids): - callback(ids) - -# Context-sensitive call graph data model - -class CallGraphModel(CallGraphModelBase): - - def __init__(self, glb, parent=None): - super(CallGraphModel, self).__init__(glb, parent) - - def GetRoot(self): - return CallGraphRootItem(self.glb, self.params) - - def columnCount(self, parent=None): - if self.params.have_ipc: - return 12 - else: - return 7 - - def columnHeader(self, column): - if self.params.have_ipc: - headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] - else: - headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] - return headers[column] - - def columnAlignment(self, column): - if self.params.have_ipc: - alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] - else: - alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] - return alignment[column] - - def DoFindSelect(self, query, match): - QueryExec(query, "SELECT call_path_id, comm_id, thread_id" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " WHERE calls.id <> 0" - " AND symbols.name" + match + - " GROUP BY comm_id, thread_id, call_path_id" - " ORDER BY comm_id, thread_id, call_path_id") - - def FindPath(self, query): - # Turn the query result into a list of ids that the tree view can walk - # to open the tree at the right place. - ids = [] - parent_id = query.value(0) - while parent_id: - ids.insert(0, parent_id) - q2 = QSqlQuery(self.glb.db) - QueryExec(q2, "SELECT parent_id" - " FROM call_paths" - " WHERE id = " + str(parent_id)) - if not q2.next(): - break - parent_id = q2.value(0) - # The call path root is not used - if ids[0] == 1: - del ids[0] - ids.insert(0, query.value(2)) - ids.insert(0, query.value(1)) - return ids - -# Call tree data model level 2+ item base - -class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item): - super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) - self.comm_id = comm_id - self.thread_id = thread_id - self.calls_id = calls_id - self.call_time = call_time - self.time = time - self.insn_cnt = insn_cnt - self.cyc_cnt = cyc_cnt - self.branch_count = branch_count - - def Select(self): - self.query_done = True - if self.calls_id == 0: - comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id) - else: - comm_thread = "" - if self.params.have_ipc: - ipc_str = ", insn_count, cyc_count" - else: - ipc_str = "" - query = QSqlQuery(self.glb.db) - QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " INNER JOIN dsos ON symbols.dso_id = dsos.id" - " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread + - " ORDER BY call_time, calls.id") - while query.next(): - if self.params.have_ipc: - insn_cnt = int(query.value(5)) - cyc_cnt = int(query.value(6)) - branch_count = int(query.value(7)) - else: - insn_cnt = 0 - cyc_cnt = 0 - branch_count = int(query.value(5)) - child_item = CallTreeLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self) - self.child_items.append(child_item) - self.child_count += 1 - -# Call tree data model level three item - -class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item): - super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item) - dso = dsoname(dso) - if self.params.have_ipc: - insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) - cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) - br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) - ipc = CalcIPC(cyc_cnt, insn_cnt) - self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ] - else: - self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] - self.dbid = calls_id - -# Call tree data model level two item - -class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): - super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item) - if self.params.have_ipc: - self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] - else: - self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] - self.dbid = thread_id - - def Select(self): - super(CallTreeLevelTwoItem, self).Select() - for child_item in self.child_items: - self.time += child_item.time - self.insn_cnt += child_item.insn_cnt - self.cyc_cnt += child_item.cyc_cnt - self.branch_count += child_item.branch_count - for child_item in self.child_items: - child_item.data[4] = PercentToOneDP(child_item.time, self.time) - if self.params.have_ipc: - child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) - child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) - child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) - else: - child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) - -# Call tree data model level one item - -class CallTreeLevelOneItem(CallGraphLevelItemBase): - - def __init__(self, glb, params, row, comm_id, comm, parent_item): - super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item) - if self.params.have_ipc: - self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] - else: - self.data = [comm, "", "", "", "", "", ""] - self.dbid = comm_id - - def Select(self): - self.query_done = True - query = QSqlQuery(self.glb.db) - QueryExec(query, "SELECT thread_id, pid, tid" - " FROM comm_threads" - " INNER JOIN threads ON thread_id = threads.id" - " WHERE comm_id = " + str(self.dbid)) - while query.next(): - child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) - self.child_items.append(child_item) - self.child_count += 1 - -# Call tree data model root item - -class CallTreeRootItem(CallGraphLevelItemBase): - - def __init__(self, glb, params): - super(CallTreeRootItem, self).__init__(glb, params, 0, None) - self.dbid = 0 - self.query_done = True - if_has_calls = "" - if IsSelectable(glb.db, "comms", columns = "has_calls"): - if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE - query = QSqlQuery(glb.db) - QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) - while query.next(): - if not query.value(0): - continue - child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) - self.child_items.append(child_item) - self.child_count += 1 - -# Call Tree data model - -class CallTreeModel(CallGraphModelBase): - - def __init__(self, glb, parent=None): - super(CallTreeModel, self).__init__(glb, parent) - - def GetRoot(self): - return CallTreeRootItem(self.glb, self.params) - - def columnCount(self, parent=None): - if self.params.have_ipc: - return 12 - else: - return 7 - - def columnHeader(self, column): - if self.params.have_ipc: - headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] - else: - headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] - return headers[column] - - def columnAlignment(self, column): - if self.params.have_ipc: - alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] - else: - alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] - return alignment[column] - - def DoFindSelect(self, query, match): - QueryExec(query, "SELECT calls.id, comm_id, thread_id" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " WHERE calls.id <> 0" - " AND symbols.name" + match + - " ORDER BY comm_id, thread_id, call_time, calls.id") - - def FindPath(self, query): - # Turn the query result into a list of ids that the tree view can walk - # to open the tree at the right place. - ids = [] - parent_id = query.value(0) - while parent_id: - ids.insert(0, parent_id) - q2 = QSqlQuery(self.glb.db) - QueryExec(q2, "SELECT parent_id" - " FROM calls" - " WHERE id = " + str(parent_id)) - if not q2.next(): - break - parent_id = q2.value(0) - ids.insert(0, query.value(2)) - ids.insert(0, query.value(1)) - return ids - -# Vertical layout - -class HBoxLayout(QHBoxLayout): - - def __init__(self, *children): - super(HBoxLayout, self).__init__() - - self.layout().setContentsMargins(0, 0, 0, 0) - for child in children: - if child.isWidgetType(): - self.layout().addWidget(child) - else: - self.layout().addLayout(child) - -# Horizontal layout - -class VBoxLayout(QVBoxLayout): - - def __init__(self, *children): - super(VBoxLayout, self).__init__() - - self.layout().setContentsMargins(0, 0, 0, 0) - for child in children: - if child.isWidgetType(): - self.layout().addWidget(child) - else: - self.layout().addLayout(child) - -# Vertical layout widget - -class VBox(): - - def __init__(self, *children): - self.vbox = QWidget() - self.vbox.setLayout(VBoxLayout(*children)) - - def Widget(self): - return self.vbox - -# Tree window base - -class TreeWindowBase(QMdiSubWindow): - - def __init__(self, parent=None): - super(TreeWindowBase, self).__init__(parent) - - self.model = None - self.find_bar = None - - self.view = QTreeView() - self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) - self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard - - self.context_menu = TreeContextMenu(self.view) - - def DisplayFound(self, ids): - if not len(ids): - return False - parent = QModelIndex() - for dbid in ids: - found = False - n = self.model.rowCount(parent) - for row in xrange(n): - child = self.model.index(row, 0, parent) - if child.internalPointer().dbid == dbid: - found = True - self.view.setExpanded(parent, True) - self.view.setCurrentIndex(child) - parent = child - break - if not found: - break - return found - - def Find(self, value, direction, pattern, context): - self.view.setFocus() - self.find_bar.Busy() - self.model.Find(value, direction, pattern, context, self.FindDone) - - def FindDone(self, ids): - found = True - if not self.DisplayFound(ids): - found = False - self.find_bar.Idle() - if not found: - self.find_bar.NotFound() - - -# Context-sensitive call graph window - -class CallGraphWindow(TreeWindowBase): - - def __init__(self, glb, parent=None): - super(CallGraphWindow, self).__init__(parent) - - self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) - - self.view.setModel(self.model) - - for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): - self.view.setColumnWidth(c, w) - - self.find_bar = FindBar(self, self) - - self.vbox = VBox(self.view, self.find_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") - -# Call tree window - -class CallTreeWindow(TreeWindowBase): - - def __init__(self, glb, parent=None, thread_at_time=None): - super(CallTreeWindow, self).__init__(parent) - - self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x)) - - self.view.setModel(self.model) - - for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)): - self.view.setColumnWidth(c, w) - - self.find_bar = FindBar(self, self) - - self.vbox = VBox(self.view, self.find_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree") - - if thread_at_time: - self.DisplayThreadAtTime(*thread_at_time) - - def DisplayThreadAtTime(self, comm_id, thread_id, time): - parent = QModelIndex() - for dbid in (comm_id, thread_id): - found = False - n = self.model.rowCount(parent) - for row in xrange(n): - child = self.model.index(row, 0, parent) - if child.internalPointer().dbid == dbid: - found = True - self.view.setExpanded(parent, True) - self.view.setCurrentIndex(child) - parent = child - break - if not found: - return - found = False - while True: - n = self.model.rowCount(parent) - if not n: - return - last_child = None - for row in xrange(n): - self.view.setExpanded(parent, True) - child = self.model.index(row, 0, parent) - child_call_time = child.internalPointer().call_time - if child_call_time < time: - last_child = child - elif child_call_time == time: - self.view.setCurrentIndex(child) - return - elif child_call_time > time: - break - if not last_child: - if not found: - child = self.model.index(0, 0, parent) - self.view.setExpanded(parent, True) - self.view.setCurrentIndex(child) - return - found = True - self.view.setExpanded(parent, True) - self.view.setCurrentIndex(last_child) - parent = last_child - -# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name - -def ExecComm(db, thread_id, time): - query = QSqlQuery(db) - QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag" - " FROM comm_threads" - " INNER JOIN comms ON comms.id = comm_threads.comm_id" - " WHERE comm_threads.thread_id = " + str(thread_id) + - " ORDER BY comms.c_time, comms.id") - first = None - last = None - while query.next(): - if first is None: - first = query.value(0) - if query.value(2) and Decimal(query.value(1)) <= Decimal(time): - last = query.value(0) - if not(last is None): - return last - return first - -# Container for (x, y) data - -class XY(): - def __init__(self, x=0, y=0): - self.x = x - self.y = y - - def __str__(self): - return "XY({}, {})".format(str(self.x), str(self.y)) - -# Container for sub-range data - -class Subrange(): - def __init__(self, lo=0, hi=0): - self.lo = lo - self.hi = hi - - def __str__(self): - return "Subrange({}, {})".format(str(self.lo), str(self.hi)) - -# Graph data region base class - -class GraphDataRegion(object): - - def __init__(self, key, title = "", ordinal = ""): - self.key = key - self.title = title - self.ordinal = ordinal - -# Function to sort GraphDataRegion - -def GraphDataRegionOrdinal(data_region): - return data_region.ordinal - -# Attributes for a graph region - -class GraphRegionAttribute(): - - def __init__(self, colour): - self.colour = colour - -# Switch graph data region represents a task - -class SwitchGraphDataRegion(GraphDataRegion): - - def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id): - super(SwitchGraphDataRegion, self).__init__(key) - - self.title = str(pid) + " / " + str(tid) + " " + comm - # Order graph legend within exec comm by pid / tid / time - self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16) - self.exec_comm_id = exec_comm_id - self.pid = pid - self.tid = tid - self.comm = comm - self.thread_id = thread_id - self.comm_id = comm_id - -# Graph data point - -class GraphDataPoint(): - - def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None): - self.data = data - self.index = index - self.x = x - self.y = y - self.altx = altx - self.alty = alty - self.hregion = hregion - self.vregion = vregion - -# Graph data (single graph) base class - -class GraphData(object): - - def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)): - self.collection = collection - self.points = [] - self.xbase = xbase - self.ybase = ybase - self.title = "" - - def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None): - index = len(self.points) - - x = float(Decimal(x) - self.xbase) - y = float(Decimal(y) - self.ybase) - - self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion)) - - def XToData(self, x): - return Decimal(x) + self.xbase - - def YToData(self, y): - return Decimal(y) + self.ybase - -# Switch graph data (for one CPU) - -class SwitchGraphData(GraphData): - - def __init__(self, db, collection, cpu, xbase): - super(SwitchGraphData, self).__init__(collection, xbase) - - self.cpu = cpu - self.title = "CPU " + str(cpu) - self.SelectSwitches(db) - - def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time): - query = QSqlQuery(db) - QueryExec(query, "SELECT id, c_time" - " FROM comms" - " WHERE c_thread_id = " + str(thread_id) + - " AND exec_flag = " + self.collection.glb.dbref.TRUE + - " AND c_time >= " + str(start_time) + - " AND c_time <= " + str(end_time) + - " ORDER BY c_time, id") - while query.next(): - comm_id = query.value(0) - if comm_id == last_comm_id: - continue - time = query.value(1) - hregion = self.HRegion(db, thread_id, comm_id, time) - self.AddPoint(time, 1000, None, None, hregion) - - def SelectSwitches(self, db): - last_time = None - last_comm_id = None - last_thread_id = None - query = QSqlQuery(db) - QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags" - " FROM context_switches" - " WHERE machine_id = " + str(self.collection.machine_id) + - " AND cpu = " + str(self.cpu) + - " ORDER BY time, id") - while query.next(): - flags = int(query.value(5)) - if flags & 1: - # Schedule-out: detect and add exec's - if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3): - self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0)) - continue - # Schedule-in: add data point - if len(self.points) == 0: - start_time = self.collection.glb.StartTime(self.collection.machine_id) - hregion = self.HRegion(db, query.value(1), query.value(3), start_time) - self.AddPoint(start_time, 1000, None, None, hregion) - time = query.value(0) - comm_id = query.value(4) - thread_id = query.value(2) - hregion = self.HRegion(db, thread_id, comm_id, time) - self.AddPoint(time, 1000, None, None, hregion) - last_time = time - last_comm_id = comm_id - last_thread_id = thread_id - - def NewHRegion(self, db, key, thread_id, comm_id, time): - exec_comm_id = ExecComm(db, thread_id, time) - query = QSqlQuery(db) - QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id)) - if query.next(): - pid = query.value(0) - tid = query.value(1) - else: - pid = -1 - tid = -1 - query = QSqlQuery(db) - QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id)) - if query.next(): - comm = query.value(0) - else: - comm = "" - return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id) - - def HRegion(self, db, thread_id, comm_id, time): - key = str(thread_id) + ":" + str(comm_id) - hregion = self.collection.LookupHRegion(key) - if hregion is None: - hregion = self.NewHRegion(db, key, thread_id, comm_id, time) - self.collection.AddHRegion(key, hregion) - return hregion - -# Graph data collection (multiple related graphs) base class - -class GraphDataCollection(object): - - def __init__(self, glb): - self.glb = glb - self.data = [] - self.hregions = {} - self.xrangelo = None - self.xrangehi = None - self.yrangelo = None - self.yrangehi = None - self.dp = XY(0, 0) - - def AddGraphData(self, data): - self.data.append(data) - - def LookupHRegion(self, key): - if key in self.hregions: - return self.hregions[key] - return None - - def AddHRegion(self, key, hregion): - self.hregions[key] = hregion - -# Switch graph data collection (SwitchGraphData for each CPU) - -class SwitchGraphDataCollection(GraphDataCollection): - - def __init__(self, glb, db, machine_id): - super(SwitchGraphDataCollection, self).__init__(glb) - - self.machine_id = machine_id - self.cpus = self.SelectCPUs(db) - - self.xrangelo = glb.StartTime(machine_id) - self.xrangehi = glb.FinishTime(machine_id) - - self.yrangelo = Decimal(0) - self.yrangehi = Decimal(1000) - - for cpu in self.cpus: - self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo)) - - def SelectCPUs(self, db): - cpus = [] - query = QSqlQuery(db) - QueryExec(query, "SELECT DISTINCT cpu" - " FROM context_switches" - " WHERE machine_id = " + str(self.machine_id)) - while query.next(): - cpus.append(int(query.value(0))) - return sorted(cpus) - -# Switch graph data graphics item displays the graphed data - -class SwitchGraphDataGraphicsItem(QGraphicsItem): - - def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None): - super(SwitchGraphDataGraphicsItem, self).__init__(parent) - - self.data = data - self.graph_width = graph_width - self.graph_height = graph_height - self.attrs = attrs - self.event_handler = event_handler - self.setAcceptHoverEvents(True) - - def boundingRect(self): - return QRectF(0, 0, self.graph_width, self.graph_height) - - def PaintPoint(self, painter, last, x): - if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo): - if last.x < self.attrs.subrange.x.lo: - x0 = self.attrs.subrange.x.lo - else: - x0 = last.x - if x > self.attrs.subrange.x.hi: - x1 = self.attrs.subrange.x.hi - else: - x1 = x - 1 - x0 = self.attrs.XToPixel(x0) - x1 = self.attrs.XToPixel(x1) - - y0 = self.attrs.YToPixel(last.y) - - colour = self.attrs.region_attributes[last.hregion.key].colour - - width = x1 - x0 + 1 - if width < 2: - painter.setPen(colour) - painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height) - else: - painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour) - - def paint(self, painter, option, widget): - last = None - for point in self.data.points: - self.PaintPoint(painter, last, point.x) - if point.x > self.attrs.subrange.x.hi: - break; - last = point - self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1) - - def BinarySearchPoint(self, target): - lower_pos = 0 - higher_pos = len(self.data.points) - while True: - pos = int((lower_pos + higher_pos) / 2) - val = self.data.points[pos].x - if target >= val: - lower_pos = pos - else: - higher_pos = pos - if higher_pos <= lower_pos + 1: - return lower_pos - - def XPixelToData(self, x): - x = self.attrs.PixelToX(x) - if x < self.data.points[0].x: - x = 0 - pos = 0 - low = True - else: - pos = self.BinarySearchPoint(x) - low = False - return (low, pos, self.data.XToData(x)) - - def EventToData(self, event): - no_data = (None,) * 4 - if len(self.data.points) < 1: - return no_data - x = event.pos().x() - if x < 0: - return no_data - low0, pos0, time_from = self.XPixelToData(x) - low1, pos1, time_to = self.XPixelToData(x + 1) - hregions = set() - hregion_times = [] - if not low1: - for i in xrange(pos0, pos1 + 1): - hregion = self.data.points[i].hregion - hregions.add(hregion) - if i == pos0: - time = time_from - else: - time = self.data.XToData(self.data.points[i].x) - hregion_times.append((hregion, time)) - return (time_from, time_to, hregions, hregion_times) - - def hoverMoveEvent(self, event): - time_from, time_to, hregions, hregion_times = self.EventToData(event) - if time_from is not None: - self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions) - - def hoverLeaveEvent(self, event): - self.event_handler.NoPointEvent() - - def mousePressEvent(self, event): - if event.button() != Qt.RightButton: - super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event) - return - time_from, time_to, hregions, hregion_times = self.EventToData(event) - if hregion_times: - self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos()) - -# X-axis graphics item - -class XAxisGraphicsItem(QGraphicsItem): - - def __init__(self, width, parent=None): - super(XAxisGraphicsItem, self).__init__(parent) - - self.width = width - self.max_mark_sz = 4 - self.height = self.max_mark_sz + 1 - - def boundingRect(self): - return QRectF(0, 0, self.width, self.height) - - def Step(self): - attrs = self.parentItem().attrs - subrange = attrs.subrange.x - t = subrange.hi - subrange.lo - s = (3.0 * t) / self.width - n = 1.0 - while s > n: - n = n * 10.0 - return n - - def PaintMarks(self, painter, at_y, lo, hi, step, i): - attrs = self.parentItem().attrs - x = lo - while x <= hi: - xp = attrs.XToPixel(x) - if i % 10: - if i % 5: - sz = 1 - else: - sz = 2 - else: - sz = self.max_mark_sz - i = 0 - painter.drawLine(xp, at_y, xp, at_y + sz) - x += step - i += 1 - - def paint(self, painter, option, widget): - # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1 - painter.drawLine(0, 0, self.width - 1, 0) - n = self.Step() - attrs = self.parentItem().attrs - subrange = attrs.subrange.x - if subrange.lo: - x_offset = n - (subrange.lo % n) - else: - x_offset = 0.0 - x = subrange.lo + x_offset - i = (x / n) % 10 - self.PaintMarks(painter, 0, x, subrange.hi, n, i) - - def ScaleDimensions(self): - n = self.Step() - attrs = self.parentItem().attrs - lo = attrs.subrange.x.lo - hi = (n * 10.0) + lo - width = attrs.XToPixel(hi) - if width > 500: - width = 0 - return (n, lo, hi, width) - - def PaintScale(self, painter, at_x, at_y): - n, lo, hi, width = self.ScaleDimensions() - if not width: - return - painter.drawLine(at_x, at_y, at_x + width, at_y) - self.PaintMarks(painter, at_y, lo, hi, n, 0) - - def ScaleWidth(self): - n, lo, hi, width = self.ScaleDimensions() - return width - - def ScaleHeight(self): - return self.height - - def ScaleUnit(self): - return self.Step() * 10 - -# Scale graphics item base class - -class ScaleGraphicsItem(QGraphicsItem): - - def __init__(self, axis, parent=None): - super(ScaleGraphicsItem, self).__init__(parent) - self.axis = axis - - def boundingRect(self): - scale_width = self.axis.ScaleWidth() - if not scale_width: - return QRectF() - return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight()) - - def paint(self, painter, option, widget): - scale_width = self.axis.ScaleWidth() - if not scale_width: - return - self.axis.PaintScale(painter, 0, 5) - x = scale_width + 4 - painter.drawText(QPointF(x, 10), self.Text()) - - def Unit(self): - return self.axis.ScaleUnit() - - def Text(self): - return "" - -# Switch graph scale graphics item - -class SwitchScaleGraphicsItem(ScaleGraphicsItem): - - def __init__(self, axis, parent=None): - super(SwitchScaleGraphicsItem, self).__init__(axis, parent) - - def Text(self): - unit = self.Unit() - if unit >= 1000000000: - unit = int(unit / 1000000000) - us = "s" - elif unit >= 1000000: - unit = int(unit / 1000000) - us = "ms" - elif unit >= 1000: - unit = int(unit / 1000) - us = "us" - else: - unit = int(unit) - us = "ns" - return " = " + str(unit) + " " + us - -# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data - -class SwitchGraphGraphicsItem(QGraphicsItem): - - def __init__(self, collection, data, attrs, event_handler, first, parent=None): - super(SwitchGraphGraphicsItem, self).__init__(parent) - self.collection = collection - self.data = data - self.attrs = attrs - self.event_handler = event_handler - - margin = 20 - title_width = 50 - - self.title_graphics = QGraphicsSimpleTextItem(data.title, self) - - self.title_graphics.setPos(margin, margin) - graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1 - graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1 - - self.graph_origin_x = margin + title_width + margin - self.graph_origin_y = graph_height + margin - - x_axis_size = 1 - y_axis_size = 1 - self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self) - - self.x_axis = XAxisGraphicsItem(graph_width, self) - self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1) - - if first: - self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self) - self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10) - - self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height) - - self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self) - self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1) - - self.width = self.graph_origin_x + graph_width + margin - self.height = self.graph_origin_y + margin - - self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self) - self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height) - - if parent and 'EnableRubberBand' in dir(parent): - parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self) - - def boundingRect(self): - return QRectF(0, 0, self.width, self.height) - - def paint(self, painter, option, widget): - pass - - def RBXToPixel(self, x): - return self.attrs.PixelToX(x - self.graph_origin_x) - - def RBXRangeToPixel(self, x0, x1): - return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1)) - - def RBPixelToTime(self, x): - if x < self.data.points[0].x: - return self.data.XToData(0) - return self.data.XToData(x) - - def RBEventTimes(self, x0, x1): - x0, x1 = self.RBXRangeToPixel(x0, x1) - time_from = self.RBPixelToTime(x0) - time_to = self.RBPixelToTime(x1) - return (time_from, time_to) - - def RBEvent(self, x0, x1): - time_from, time_to = self.RBEventTimes(x0, x1) - self.event_handler.RangeEvent(time_from, time_to) - - def RBMoveEvent(self, x0, x1): - if x1 < x0: - x0, x1 = x1, x0 - self.RBEvent(x0, x1) - - def RBReleaseEvent(self, x0, x1, selection_state): - if x1 < x0: - x0, x1 = x1, x0 - x0, x1 = self.RBXRangeToPixel(x0, x1) - self.event_handler.SelectEvent(x0, x1, selection_state) - -# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range) - -class VerticalBracketGraphicsItem(QGraphicsItem): - - def __init__(self, parent=None): - super(VerticalBracketGraphicsItem, self).__init__(parent) - - self.width = 0 - self.height = 0 - self.hide() - - def SetSize(self, width, height): - self.width = width + 1 - self.height = height + 1 - - def boundingRect(self): - return QRectF(0, 0, self.width, self.height) - - def paint(self, painter, option, widget): - colour = QColor(255, 255, 0, 32) - painter.fillRect(0, 0, self.width, self.height, colour) - x1 = self.width - 1 - y1 = self.height - 1 - painter.drawLine(0, 0, x1, 0) - painter.drawLine(0, 0, 0, 3) - painter.drawLine(x1, 0, x1, 3) - painter.drawLine(0, y1, x1, y1) - painter.drawLine(0, y1, 0, y1 - 3) - painter.drawLine(x1, y1, x1, y1 - 3) - -# Graphics item to contain graphs arranged vertically - -class VertcalGraphSetGraphicsItem(QGraphicsItem): - - def __init__(self, collection, attrs, event_handler, child_class, parent=None): - super(VertcalGraphSetGraphicsItem, self).__init__(parent) - - self.collection = collection - - self.top = 10 - - self.width = 0 - self.height = self.top - - self.rubber_band = None - self.rb_enabled = False - - first = True - for data in collection.data: - child = child_class(collection, data, attrs, event_handler, first, self) - child.setPos(0, self.height + 1) - rect = child.boundingRect() - if rect.right() > self.width: - self.width = rect.right() - self.height = self.height + rect.bottom() + 1 - first = False - - self.bracket = VerticalBracketGraphicsItem(self) - - def EnableRubberBand(self, xlo, xhi, rb_event_handler): - if self.rb_enabled: - return - self.rb_enabled = True - self.rb_in_view = False - self.setAcceptedMouseButtons(Qt.LeftButton) - self.rb_xlo = xlo - self.rb_xhi = xhi - self.rb_event_handler = rb_event_handler - self.mousePressEvent = self.MousePressEvent - self.mouseMoveEvent = self.MouseMoveEvent - self.mouseReleaseEvent = self.MouseReleaseEvent - - def boundingRect(self): - return QRectF(0, 0, self.width, self.height) - - def paint(self, painter, option, widget): - pass - - def RubberBandParent(self): - scene = self.scene() - view = scene.views()[0] - viewport = view.viewport() - return viewport - - def RubberBandSetGeometry(self, rect): - scene_rectf = self.mapRectToScene(QRectF(rect)) - scene = self.scene() - view = scene.views()[0] - poly = view.mapFromScene(scene_rectf) - self.rubber_band.setGeometry(poly.boundingRect()) - - def SetSelection(self, selection_state): - if self.rubber_band: - if selection_state: - self.RubberBandSetGeometry(selection_state) - self.rubber_band.show() - else: - self.rubber_band.hide() - - def SetBracket(self, rect): - if rect: - x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height() - self.bracket.setPos(x, y) - self.bracket.SetSize(width, height) - self.bracket.show() - else: - self.bracket.hide() - - def RubberBandX(self, event): - x = event.pos().toPoint().x() - if x < self.rb_xlo: - x = self.rb_xlo - elif x > self.rb_xhi: - x = self.rb_xhi - else: - self.rb_in_view = True - return x - - def RubberBandRect(self, x): - if self.rb_origin.x() <= x: - width = x - self.rb_origin.x() - rect = QRect(self.rb_origin, QSize(width, self.height)) - else: - width = self.rb_origin.x() - x - top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y()) - rect = QRect(top_left, QSize(width, self.height)) - return rect - - def MousePressEvent(self, event): - self.rb_in_view = False - x = self.RubberBandX(event) - self.rb_origin = QPoint(x, self.top) - if self.rubber_band is None: - self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent()) - self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height))) - if self.rb_in_view: - self.rubber_band.show() - self.rb_event_handler.RBMoveEvent(x, x) - else: - self.rubber_band.hide() - - def MouseMoveEvent(self, event): - x = self.RubberBandX(event) - rect = self.RubberBandRect(x) - self.RubberBandSetGeometry(rect) - if self.rb_in_view: - self.rubber_band.show() - self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x) - - def MouseReleaseEvent(self, event): - x = self.RubberBandX(event) - if self.rb_in_view: - selection_state = self.RubberBandRect(x) - else: - selection_state = None - self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state) - -# Switch graph legend data model - -class SwitchGraphLegendModel(QAbstractTableModel): - - def __init__(self, collection, region_attributes, parent=None): - super(SwitchGraphLegendModel, self).__init__(parent) - - self.region_attributes = region_attributes - - self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal) - self.child_count = len(self.child_items) - - self.highlight_set = set() - - self.column_headers = ("pid", "tid", "comm") - - def rowCount(self, parent): - return self.child_count - - def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: - return None - if orientation != Qt.Horizontal: - return None - return self.columnHeader(section) - - def index(self, row, column, parent): - return self.createIndex(row, column, self.child_items[row]) - - def columnCount(self, parent=None): - return len(self.column_headers) - - def columnHeader(self, column): - return self.column_headers[column] - - def data(self, index, role): - if role == Qt.BackgroundRole: - child = self.child_items[index.row()] - if child in self.highlight_set: - return self.region_attributes[child.key].colour - return None - if role == Qt.ForegroundRole: - child = self.child_items[index.row()] - if child in self.highlight_set: - return QColor(255, 255, 255) - return self.region_attributes[child.key].colour - if role != Qt.DisplayRole: - return None - hregion = self.child_items[index.row()] - col = index.column() - if col == 0: - return hregion.pid - if col == 1: - return hregion.tid - if col == 2: - return hregion.comm - return None - - def SetHighlight(self, row, set_highlight): - child = self.child_items[row] - top_left = self.createIndex(row, 0, child) - bottom_right = self.createIndex(row, len(self.column_headers) - 1, child) - self.dataChanged.emit(top_left, bottom_right) - - def Highlight(self, highlight_set): - for row in xrange(self.child_count): - child = self.child_items[row] - if child in self.highlight_set: - if child not in highlight_set: - self.SetHighlight(row, False) - elif child in highlight_set: - self.SetHighlight(row, True) - self.highlight_set = highlight_set - -# Switch graph legend is a table - -class SwitchGraphLegend(QWidget): - - def __init__(self, collection, region_attributes, parent=None): - super(SwitchGraphLegend, self).__init__(parent) - - self.data_model = SwitchGraphLegendModel(collection, region_attributes) - - self.model = QSortFilterProxyModel() - self.model.setSourceModel(self.data_model) - - self.view = QTableView() - self.view.setModel(self.model) - self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.view.verticalHeader().setVisible(False) - self.view.sortByColumn(-1, Qt.AscendingOrder) - self.view.setSortingEnabled(True) - self.view.resizeColumnsToContents() - self.view.resizeRowsToContents() - - self.vbox = VBoxLayout(self.view) - self.setLayout(self.vbox) - - sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2 - sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width() - self.saved_size = sz1 - - def resizeEvent(self, event): - self.saved_size = self.size().width() - super(SwitchGraphLegend, self).resizeEvent(event) - - def Highlight(self, highlight_set): - self.data_model.Highlight(highlight_set) - self.update() - - def changeEvent(self, event): - if event.type() == QEvent.FontChange: - self.view.resizeRowsToContents() - self.view.resizeColumnsToContents() - # Need to resize rows again after column resize - self.view.resizeRowsToContents() - super(SwitchGraphLegend, self).changeEvent(event) - -# Random colour generation - -def RGBColourTooLight(r, g, b): - if g > 230: - return True - if g <= 160: - return False - if r <= 180 and g <= 180: - return False - if r < 60: - return False - return True - -def GenerateColours(x): - cs = [0] - for i in xrange(1, x): - cs.append(int((255.0 / i) + 0.5)) - colours = [] - for r in cs: - for g in cs: - for b in cs: - # Exclude black and colours that look too light against a white background - if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b): - continue - colours.append(QColor(r, g, b)) - return colours - -def GenerateNColours(n): - for x in xrange(2, n + 2): - colours = GenerateColours(x) - if len(colours) >= n: - return colours - return [] - -def GenerateNRandomColours(n, seed): - colours = GenerateNColours(n) - random.seed(seed) - random.shuffle(colours) - return colours - -# Graph attributes, in particular the scale and subrange that change when zooming - -class GraphAttributes(): - - def __init__(self, scale, subrange, region_attributes, dp): - self.scale = scale - self.subrange = subrange - self.region_attributes = region_attributes - # Rounding avoids errors due to finite floating point precision - self.dp = dp # data decimal places - self.Update() - - def XToPixel(self, x): - return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x)) - - def YToPixel(self, y): - return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y)) - - def PixelToXRounded(self, px): - return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo - - def PixelToYRounded(self, py): - return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo - - def PixelToX(self, px): - x = self.PixelToXRounded(px) - if self.pdp.x == 0: - rt = self.XToPixel(x) - if rt > px: - return x - 1 - return x - - def PixelToY(self, py): - y = self.PixelToYRounded(py) - if self.pdp.y == 0: - rt = self.YToPixel(y) - if rt > py: - return y - 1 - return y - - def ToPDP(self, dp, scale): - # Calculate pixel decimal places: - # (10 ** dp) is the minimum delta in the data - # scale it to get the minimum delta in pixels - # log10 gives the number of decimals places negatively - # subtrace 1 to divide by 10 - # round to the lower negative number - # change the sign to get the number of decimals positively - x = math.log10((10 ** dp) * scale) - if x < 0: - x -= 1 - x = -int(math.floor(x) - 0.1) - else: - x = 0 - return x - - def Update(self): - x = self.ToPDP(self.dp.x, self.scale.x) - y = self.ToPDP(self.dp.y, self.scale.y) - self.pdp = XY(x, y) # pixel decimal places - -# Switch graph splitter which divides the CPU graphs from the legend - -class SwitchGraphSplitter(QSplitter): - - def __init__(self, parent=None): - super(SwitchGraphSplitter, self).__init__(parent) - - self.first_time = False - - def resizeEvent(self, ev): - if self.first_time: - self.first_time = False - sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2 - sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width() - sz0 = self.size().width() - self.handleWidth() - sz1 - self.setSizes([sz0, sz1]) - elif not(self.widget(1).saved_size is None): - sz1 = self.widget(1).saved_size - sz0 = self.size().width() - self.handleWidth() - sz1 - self.setSizes([sz0, sz1]) - super(SwitchGraphSplitter, self).resizeEvent(ev) - -# Graph widget base class - -class GraphWidget(QWidget): - - graph_title_changed = Signal(object) - - def __init__(self, parent=None): - super(GraphWidget, self).__init__(parent) - - def GraphTitleChanged(self, title): - self.graph_title_changed.emit(title) - - def Title(self): - return "" - -# Display time in s, ms, us or ns - -def ToTimeStr(val): - val = Decimal(val) - if val >= 1000000000: - return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001"))) - if val >= 1000000: - return "{} ms".format((val / 1000000).quantize(Decimal("0.000001"))) - if val >= 1000: - return "{} us".format((val / 1000).quantize(Decimal("0.001"))) - return "{} ns".format(val.quantize(Decimal("1"))) - -# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons - -class SwitchGraphWidget(GraphWidget): - - def __init__(self, glb, collection, parent=None): - super(SwitchGraphWidget, self).__init__(parent) - - self.glb = glb - self.collection = collection - - self.back_state = [] - self.forward_state = [] - self.selection_state = (None, None) - self.fwd_rect = None - self.start_time = self.glb.StartTime(collection.machine_id) - - i = 0 - hregions = collection.hregions.values() - colours = GenerateNRandomColours(len(hregions), 1013) - region_attributes = {} - for hregion in hregions: - if hregion.pid == 0 and hregion.tid == 0: - region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0)) - else: - region_attributes[hregion.key] = GraphRegionAttribute(colours[i]) - i = i + 1 - - # Default to entire range - xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0) - ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0) - subrange = XY(xsubrange, ysubrange) - - scale = self.GetScaleForRange(subrange) - - self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp) - - self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem) - - self.scene = QGraphicsScene() - self.scene.addItem(self.item) - - self.view = QGraphicsView(self.scene) - self.view.centerOn(0, 0) - self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop) - - self.legend = SwitchGraphLegend(collection, region_attributes) - - self.splitter = SwitchGraphSplitter() - self.splitter.addWidget(self.view) - self.splitter.addWidget(self.legend) - - self.point_label = QLabel("") - self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) - - self.back_button = QToolButton() - self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft)) - self.back_button.setDisabled(True) - self.back_button.released.connect(lambda: self.Back()) - - self.forward_button = QToolButton() - self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight)) - self.forward_button.setDisabled(True) - self.forward_button.released.connect(lambda: self.Forward()) - - self.zoom_button = QToolButton() - self.zoom_button.setText("Zoom") - self.zoom_button.setDisabled(True) - self.zoom_button.released.connect(lambda: self.Zoom()) - - self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label) - - self.vbox = VBoxLayout(self.splitter, self.hbox) - - self.setLayout(self.vbox) - - def GetScaleForRangeX(self, xsubrange): - # Default graph 1000 pixels wide - dflt = 1000.0 - r = xsubrange.hi - xsubrange.lo - return dflt / r - - def GetScaleForRangeY(self, ysubrange): - # Default graph 50 pixels high - dflt = 50.0 - r = ysubrange.hi - ysubrange.lo - return dflt / r - - def GetScaleForRange(self, subrange): - # Default graph 1000 pixels wide, 50 pixels high - xscale = self.GetScaleForRangeX(subrange.x) - yscale = self.GetScaleForRangeY(subrange.y) - return XY(xscale, yscale) - - def PointEvent(self, cpu, time_from, time_to, hregions): - text = "CPU: " + str(cpu) - time_from = time_from.quantize(Decimal(1)) - rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id) - text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")" - self.point_label.setText(text) - self.legend.Highlight(hregions) - - def RightClickEvent(self, cpu, hregion_times, pos): - if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"): - return - menu = QMenu(self.view) - for hregion, time in hregion_times: - thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time) - menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time) - menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view)) - menu.exec_(pos) - - def RightClickSelect(self, args): - CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args) - - def NoPointEvent(self): - self.point_label.setText("") - self.legend.Highlight({}) - - def RangeEvent(self, time_from, time_to): - time_from = time_from.quantize(Decimal(1)) - time_to = time_to.quantize(Decimal(1)) - if time_to <= time_from: - self.point_label.setText("") - return - rel_time_from = time_from - self.start_time - rel_time_to = time_to - self.start_time - text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")" - text = text + " duration: " + ToTimeStr(time_to - time_from) - self.point_label.setText(text) - - def BackState(self): - return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect) - - def PushBackState(self): - state = copy.deepcopy(self.BackState()) - self.back_state.append(state) - self.back_button.setEnabled(True) - - def PopBackState(self): - self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop() - self.attrs.Update() - if not self.back_state: - self.back_button.setDisabled(True) - - def PushForwardState(self): - state = copy.deepcopy(self.BackState()) - self.forward_state.append(state) - self.forward_button.setEnabled(True) - - def PopForwardState(self): - self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop() - self.attrs.Update() - if not self.forward_state: - self.forward_button.setDisabled(True) - - def Title(self): - time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo) - time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi) - rel_time_from = time_from - self.start_time - rel_time_to = time_to - self.start_time - title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to) - title = title + " (" + ToTimeStr(time_to - time_from) + ")" - return title - - def Update(self): - selected_subrange, selection_state = self.selection_state - self.item.SetSelection(selection_state) - self.item.SetBracket(self.fwd_rect) - self.zoom_button.setDisabled(selected_subrange is None) - self.GraphTitleChanged(self.Title()) - self.item.update(self.item.boundingRect()) - - def Back(self): - if not self.back_state: - return - self.PushForwardState() - self.PopBackState() - self.Update() - - def Forward(self): - if not self.forward_state: - return - self.PushBackState() - self.PopForwardState() - self.Update() - - def SelectEvent(self, x0, x1, selection_state): - if selection_state is None: - selected_subrange = None - else: - if x1 - x0 < 1.0: - x1 += 1.0 - selected_subrange = Subrange(x0, x1) - self.selection_state = (selected_subrange, selection_state) - self.zoom_button.setDisabled(selected_subrange is None) - - def Zoom(self): - selected_subrange, selection_state = self.selection_state - if selected_subrange is None: - return - self.fwd_rect = selection_state - self.item.SetSelection(None) - self.PushBackState() - self.attrs.subrange.x = selected_subrange - self.forward_state = [] - self.forward_button.setDisabled(True) - self.selection_state = (None, None) - self.fwd_rect = None - self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x) - self.attrs.Update() - self.Update() - -# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting - -class SlowInitClass(): - - def __init__(self, glb, title, init_fn): - self.init_fn = init_fn - self.done = False - self.result = None - - self.msg_box = QMessageBox(glb.mainwindow) - self.msg_box.setText("Initializing " + title + ". Please wait.") - self.msg_box.setWindowTitle("Initializing " + title) - self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation)) - - self.init_thread = Thread(self.ThreadFn, glb) - self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection) - - self.init_thread.start() - - def Done(self): - self.msg_box.done(0) - - def ThreadFn(self, glb): - conn_name = "SlowInitClass" + str(os.getpid()) - db, dbname = glb.dbref.Open(conn_name) - self.result = self.init_fn(db) - self.done = True - return (True, 0) - - def Result(self): - while not self.done: - self.msg_box.exec_() - self.init_thread.wait() - return self.result - -def SlowInit(glb, title, init_fn): - init = SlowInitClass(glb, title, init_fn) - return init.Result() - -# Time chart by CPU window - -class TimeChartByCPUWindow(QMdiSubWindow): - - def __init__(self, glb, parent=None): - super(TimeChartByCPUWindow, self).__init__(parent) - - self.glb = glb - self.machine_id = glb.HostMachineId() - self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id) - - collection = LookupModel(self.collection_name) - if collection is None: - collection = SlowInit(glb, "Time Chart", self.Init) - - self.widget = SwitchGraphWidget(glb, collection, self) - self.view = self.widget - - self.base_title = "Time Chart by CPU" - self.setWindowTitle(self.base_title + self.widget.Title()) - self.widget.graph_title_changed.connect(self.GraphTitleChanged) - - self.setWidget(self.widget) - - AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle()) - - def Init(self, db): - return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id)) - - def GraphTitleChanged(self, title): - self.setWindowTitle(self.base_title + " : " + title) - -# Child data item finder - -class ChildDataItemFinder(): - - def __init__(self, root): - self.root = root - self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5 - self.rows = [] - self.pos = 0 - - def FindSelect(self): - self.rows = [] - if self.pattern: - pattern = re.compile(self.value) - for child in self.root.child_items: - for column_data in child.data: - if re.search(pattern, str(column_data)) is not None: - self.rows.append(child.row) - break - else: - for child in self.root.child_items: - for column_data in child.data: - if self.value in str(column_data): - self.rows.append(child.row) - break - - def FindValue(self): - self.pos = 0 - if self.last_value != self.value or self.pattern != self.last_pattern: - self.FindSelect() - if not len(self.rows): - return -1 - return self.rows[self.pos] - - def FindThread(self): - if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern: - row = self.FindValue() - elif len(self.rows): - if self.direction > 0: - self.pos += 1 - if self.pos >= len(self.rows): - self.pos = 0 - else: - self.pos -= 1 - if self.pos < 0: - self.pos = len(self.rows) - 1 - row = self.rows[self.pos] - else: - row = -1 - return (True, row) - - def Find(self, value, direction, pattern, context, callback): - self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern) - # Use a thread so the UI is not blocked - thread = Thread(self.FindThread) - thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection) - thread.start() - - def FindDone(self, thread, callback, row): - callback(row) - -# Number of database records to fetch in one go - -glb_chunk_sz = 10000 - -# Background process for SQL data fetcher - -class SQLFetcherProcess(): - - def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep): - # Need a unique connection name - conn_name = "SQLFetcher" + str(os.getpid()) - self.db, dbname = dbref.Open(conn_name) - self.sql = sql - self.buffer = buffer - self.head = head - self.tail = tail - self.fetch_count = fetch_count - self.fetching_done = fetching_done - self.process_target = process_target - self.wait_event = wait_event - self.fetched_event = fetched_event - self.prep = prep - self.query = QSqlQuery(self.db) - self.query_limit = 0 if "$$last_id$$" in sql else 2 - self.last_id = -1 - self.fetched = 0 - self.more = True - self.local_head = self.head.value - self.local_tail = self.tail.value - - def Select(self): - if self.query_limit: - if self.query_limit == 1: - return - self.query_limit -= 1 - stmt = self.sql.replace("$$last_id$$", str(self.last_id)) - QueryExec(self.query, stmt) - - def Next(self): - if not self.query.next(): - self.Select() - if not self.query.next(): - return None - self.last_id = self.query.value(0) - return self.prep(self.query) - - def WaitForTarget(self): - while True: - self.wait_event.clear() - target = self.process_target.value - if target > self.fetched or target < 0: - break - self.wait_event.wait() - return target - - def HasSpace(self, sz): - if self.local_tail <= self.local_head: - space = len(self.buffer) - self.local_head - if space > sz: - return True - if space >= glb_nsz: - # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer - nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL) - self.buffer[self.local_head : self.local_head + len(nd)] = nd - self.local_head = 0 - if self.local_tail - self.local_head > sz: - return True - return False - - def WaitForSpace(self, sz): - if self.HasSpace(sz): - return - while True: - self.wait_event.clear() - self.local_tail = self.tail.value - if self.HasSpace(sz): - return - self.wait_event.wait() - - def AddToBuffer(self, obj): - d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) - n = len(d) - nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL) - sz = n + glb_nsz - self.WaitForSpace(sz) - pos = self.local_head - self.buffer[pos : pos + len(nd)] = nd - self.buffer[pos + glb_nsz : pos + sz] = d - self.local_head += sz - - def FetchBatch(self, batch_size): - fetched = 0 - while batch_size > fetched: - obj = self.Next() - if obj is None: - self.more = False - break - self.AddToBuffer(obj) - fetched += 1 - if fetched: - self.fetched += fetched - with self.fetch_count.get_lock(): - self.fetch_count.value += fetched - self.head.value = self.local_head - self.fetched_event.set() - - def Run(self): - while self.more: - target = self.WaitForTarget() - if target < 0: - break - batch_size = min(glb_chunk_sz, target - self.fetched) - self.FetchBatch(batch_size) - self.fetching_done.value = True - self.fetched_event.set() - -def SQLFetcherFn(*x): - process = SQLFetcherProcess(*x) - process.Run() - -# SQL data fetcher - -class SQLFetcher(QObject): - - done = Signal(object) - - def __init__(self, glb, sql, prep, process_data, parent=None): - super(SQLFetcher, self).__init__(parent) - self.process_data = process_data - self.more = True - self.target = 0 - self.last_target = 0 - self.fetched = 0 - self.buffer_size = 16 * 1024 * 1024 - self.buffer = Array(c_char, self.buffer_size, lock=False) - self.head = Value(c_longlong) - self.tail = Value(c_longlong) - self.local_tail = 0 - self.fetch_count = Value(c_longlong) - self.fetching_done = Value(c_bool) - self.last_count = 0 - self.process_target = Value(c_longlong) - self.wait_event = Event() - self.fetched_event = Event() - glb.AddInstanceToShutdownOnExit(self) - self.process = Process(target=SQLFetcherFn, args=(glb.dbref, sql, self.buffer, self.head, self.tail, self.fetch_count, self.fetching_done, self.process_target, self.wait_event, self.fetched_event, prep)) - self.process.start() - self.thread = Thread(self.Thread) - self.thread.done.connect(self.ProcessData, Qt.QueuedConnection) - self.thread.start() - - def Shutdown(self): - # Tell the thread and process to exit - self.process_target.value = -1 - self.wait_event.set() - self.more = False - self.fetching_done.value = True - self.fetched_event.set() - - def Thread(self): - if not self.more: - return True, 0 - while True: - self.fetched_event.clear() - fetch_count = self.fetch_count.value - if fetch_count != self.last_count: - break - if self.fetching_done.value: - self.more = False - return True, 0 - self.fetched_event.wait() - count = fetch_count - self.last_count - self.last_count = fetch_count - self.fetched += count - return False, count - - def Fetch(self, nr): - if not self.more: - # -1 inidcates there are no more - return -1 - result = self.fetched - extra = result + nr - self.target - if extra > 0: - self.target += extra - # process_target < 0 indicates shutting down - if self.process_target.value >= 0: - self.process_target.value = self.target - self.wait_event.set() - return result - - def RemoveFromBuffer(self): - pos = self.local_tail - if len(self.buffer) - pos < glb_nsz: - pos = 0 - n = pickle.loads(self.buffer[pos : pos + glb_nsz]) - if n == 0: - pos = 0 - n = pickle.loads(self.buffer[0 : glb_nsz]) - pos += glb_nsz - obj = pickle.loads(self.buffer[pos : pos + n]) - self.local_tail = pos + n - return obj - - def ProcessData(self, count): - for i in xrange(count): - obj = self.RemoveFromBuffer() - self.process_data(obj) - self.tail.value = self.local_tail - self.wait_event.set() - self.done.emit(count) - -# Fetch more records bar - -class FetchMoreRecordsBar(): - - def __init__(self, model, parent): - self.model = model - - self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:") - self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.fetch_count = QSpinBox() - self.fetch_count.setRange(1, 1000000) - self.fetch_count.setValue(10) - self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.fetch = QPushButton("Go!") - self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - self.fetch.released.connect(self.FetchMoreRecords) - - self.progress = QProgressBar() - self.progress.setRange(0, 100) - self.progress.hide() - - self.done_label = QLabel("All records fetched") - self.done_label.hide() - - self.spacer = QLabel("") - - self.close_button = QToolButton() - self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) - self.close_button.released.connect(self.Deactivate) - - self.hbox = QHBoxLayout() - self.hbox.setContentsMargins(0, 0, 0, 0) - - self.hbox.addWidget(self.label) - self.hbox.addWidget(self.fetch_count) - self.hbox.addWidget(self.fetch) - self.hbox.addWidget(self.spacer) - self.hbox.addWidget(self.progress) - self.hbox.addWidget(self.done_label) - self.hbox.addWidget(self.close_button) - - self.bar = QWidget() - self.bar.setLayout(self.hbox) - self.bar.show() - - self.in_progress = False - self.model.progress.connect(self.Progress) - - self.done = False - - if not model.HasMoreRecords(): - self.Done() - - def Widget(self): - return self.bar - - def Activate(self): - self.bar.show() - self.fetch.setFocus() - - def Deactivate(self): - self.bar.hide() - - def Enable(self, enable): - self.fetch.setEnabled(enable) - self.fetch_count.setEnabled(enable) - - def Busy(self): - self.Enable(False) - self.fetch.hide() - self.spacer.hide() - self.progress.show() - - def Idle(self): - self.in_progress = False - self.Enable(True) - self.progress.hide() - self.fetch.show() - self.spacer.show() - - def Target(self): - return self.fetch_count.value() * glb_chunk_sz - - def Done(self): - self.done = True - self.Idle() - self.label.hide() - self.fetch_count.hide() - self.fetch.hide() - self.spacer.hide() - self.done_label.show() - - def Progress(self, count): - if self.in_progress: - if count: - percent = ((count - self.start) * 100) / self.Target() - if percent >= 100: - self.Idle() - else: - self.progress.setValue(percent) - if not count: - # Count value of zero means no more records - self.Done() - - def FetchMoreRecords(self): - if self.done: - return - self.progress.setValue(0) - self.Busy() - self.in_progress = True - self.start = self.model.FetchMoreRecords(self.Target()) - -# Brance data model level two item - -class BranchLevelTwoItem(): - - def __init__(self, row, col, text, parent_item): - self.row = row - self.parent_item = parent_item - self.data = [""] * (col + 1) - self.data[col] = text - self.level = 2 - - def getParentItem(self): - return self.parent_item - - def getRow(self): - return self.row - - def childCount(self): - return 0 - - def hasChildren(self): - return False - - def getData(self, column): - return self.data[column] - -# Brance data model level one item - -class BranchLevelOneItem(): - - def __init__(self, glb, row, data, parent_item): - self.glb = glb - self.row = row - self.parent_item = parent_item - self.child_count = 0 - self.child_items = [] - self.data = data[1:] - self.dbid = data[0] - self.level = 1 - self.query_done = False - self.br_col = len(self.data) - 1 - - def getChildItem(self, row): - return self.child_items[row] - - def getParentItem(self): - return self.parent_item - - def getRow(self): - return self.row - - def Select(self): - self.query_done = True - - if not self.glb.have_disassembler: - return - - query = QSqlQuery(self.glb.db) - - QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip" - " FROM samples" - " INNER JOIN dsos ON samples.to_dso_id = dsos.id" - " INNER JOIN symbols ON samples.to_symbol_id = symbols.id" - " WHERE samples.id = " + str(self.dbid)) - if not query.next(): - return - cpu = query.value(0) - dso = query.value(1) - sym = query.value(2) - if dso == 0 or sym == 0: - return - off = query.value(3) - short_name = query.value(4) - long_name = query.value(5) - build_id = query.value(6) - sym_start = query.value(7) - ip = query.value(8) - - QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start" - " FROM samples" - " INNER JOIN symbols ON samples.symbol_id = symbols.id" - " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) + - " ORDER BY samples.id" - " LIMIT 1") - if not query.next(): - return - if query.value(0) != dso: - # Cannot disassemble from one dso to another - return - bsym = query.value(1) - boff = query.value(2) - bsym_start = query.value(3) - if bsym == 0: - return - tot = bsym_start + boff + 1 - sym_start - off - if tot <= 0 or tot > 16384: - return - - inst = self.glb.disassembler.Instruction() - f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id) - if not f: - return - mode = 0 if Is64Bit(f) else 1 - self.glb.disassembler.SetMode(inst, mode) - - buf_sz = tot + 16 - buf = create_string_buffer(tot + 16) - f.seek(sym_start + off) - buf.value = f.read(buf_sz) - buf_ptr = addressof(buf) - i = 0 - while tot > 0: - cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip) - if cnt: - byte_str = tohex(ip).rjust(16) - for k in xrange(cnt): - byte_str += " %02x" % ord(buf[i]) - i += 1 - while k < 15: - byte_str += " " - k += 1 - self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self)) - self.child_count += 1 - else: - return - buf_ptr += cnt - tot -= cnt - buf_sz -= cnt - ip += cnt - - def childCount(self): - if not self.query_done: - self.Select() - if not self.child_count: - return -1 - return self.child_count - - def hasChildren(self): - if not self.query_done: - return True - return self.child_count > 0 - - def getData(self, column): - return self.data[column] - -# Brance data model root item - -class BranchRootItem(): - - def __init__(self): - self.child_count = 0 - self.child_items = [] - self.level = 0 - - def getChildItem(self, row): - return self.child_items[row] - - def getParentItem(self): - return None - - def getRow(self): - return 0 - - def childCount(self): - return self.child_count - - def hasChildren(self): - return self.child_count > 0 - - def getData(self, column): - return "" - -# Calculate instructions per cycle - -def CalcIPC(cyc_cnt, insn_cnt): - if cyc_cnt and insn_cnt: - ipc = Decimal(float(insn_cnt) / cyc_cnt) - ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP)) - else: - ipc = "0" - return ipc - -# Branch data preparation - -def BranchDataPrepBr(query, data): - data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) + - " (" + dsoname(query.value(11)) + ")" + " -> " + - tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) + - " (" + dsoname(query.value(15)) + ")") - -def BranchDataPrepIPC(query, data): - insn_cnt = query.value(16) - cyc_cnt = query.value(17) - ipc = CalcIPC(cyc_cnt, insn_cnt) - data.append(insn_cnt) - data.append(cyc_cnt) - data.append(ipc) - -def BranchDataPrep(query): - data = [] - for i in xrange(0, 8): - data.append(query.value(i)) - BranchDataPrepBr(query, data) - return data - -def BranchDataPrepWA(query): - data = [] - data.append(query.value(0)) - # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string - data.append("{:>19}".format(query.value(1))) - for i in xrange(2, 8): - data.append(query.value(i)) - BranchDataPrepBr(query, data) - return data - -def BranchDataWithIPCPrep(query): - data = [] - for i in xrange(0, 8): - data.append(query.value(i)) - BranchDataPrepIPC(query, data) - BranchDataPrepBr(query, data) - return data - -def BranchDataWithIPCPrepWA(query): - data = [] - data.append(query.value(0)) - # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string - data.append("{:>19}".format(query.value(1))) - for i in xrange(2, 8): - data.append(query.value(i)) - BranchDataPrepIPC(query, data) - BranchDataPrepBr(query, data) - return data - -# Branch data model - -class BranchModel(TreeModel): - - progress = Signal(object) - - def __init__(self, glb, event_id, where_clause, parent=None): - super(BranchModel, self).__init__(glb, None, parent) - self.event_id = event_id - self.more = True - self.populated = 0 - self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count") - if self.have_ipc: - select_ipc = ", insn_count, cyc_count" - prep_fn = BranchDataWithIPCPrep - prep_wa_fn = BranchDataWithIPCPrepWA - else: - select_ipc = "" - prep_fn = BranchDataPrep - prep_wa_fn = BranchDataPrepWA - sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name," - " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END," - " ip, symbols.name, sym_offset, dsos.short_name," - " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name" - + select_ipc + - " FROM samples" - " INNER JOIN comms ON comm_id = comms.id" - " INNER JOIN threads ON thread_id = threads.id" - " INNER JOIN branch_types ON branch_type = branch_types.id" - " INNER JOIN symbols ON symbol_id = symbols.id" - " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id" - " INNER JOIN dsos ON samples.dso_id = dsos.id" - " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id" - " WHERE samples.id > $$last_id$$" + where_clause + - " AND evsel_id = " + str(self.event_id) + - " ORDER BY samples.id" - " LIMIT " + str(glb_chunk_sz)) - if pyside_version_1 and sys.version_info[0] == 3: - prep = prep_fn - else: - prep = prep_wa_fn - self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample) - self.fetcher.done.connect(self.Update) - self.fetcher.Fetch(glb_chunk_sz) - - def GetRoot(self): - return BranchRootItem() - - def columnCount(self, parent=None): - if self.have_ipc: - return 11 - else: - return 8 - - def columnHeader(self, column): - if self.have_ipc: - return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column] - else: - return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column] - - def columnFont(self, column): - if self.have_ipc: - br_col = 10 - else: - br_col = 7 - if column != br_col: - return None - return QFont("Monospace") - - def DisplayData(self, item, index): - if item.level == 1: - self.FetchIfNeeded(item.row) - return item.getData(index.column()) - - def AddSample(self, data): - child = BranchLevelOneItem(self.glb, self.populated, data, self.root) - self.root.child_items.append(child) - self.populated += 1 - - def Update(self, fetched): - if not fetched: - self.more = False - self.progress.emit(0) - child_count = self.root.child_count - count = self.populated - child_count - if count > 0: - parent = QModelIndex() - self.beginInsertRows(parent, child_count, child_count + count - 1) - self.insertRows(child_count, count, parent) - self.root.child_count += count - self.endInsertRows() - self.progress.emit(self.root.child_count) - - def FetchMoreRecords(self, count): - current = self.root.child_count - if self.more: - self.fetcher.Fetch(count) - else: - self.progress.emit(0) - return current - - def HasMoreRecords(self): - return self.more - -# Report Variables - -class ReportVars(): - - def __init__(self, name = "", where_clause = "", limit = ""): - self.name = name - self.where_clause = where_clause - self.limit = limit - - def UniqueId(self): - return str(self.where_clause + ";" + self.limit) - -# Branch window - -class BranchWindow(QMdiSubWindow): - - def __init__(self, glb, event_id, report_vars, parent=None): - super(BranchWindow, self).__init__(parent) - - model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId() - - self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause)) - - self.view = QTreeView() - self.view.setUniformRowHeights(True) - self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) - self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard - self.view.setModel(self.model) - - self.ResizeColumnsToContents() - - self.context_menu = TreeContextMenu(self.view) - - self.find_bar = FindBar(self, self, True) - - self.finder = ChildDataItemFinder(self.model.root) - - self.fetch_bar = FetchMoreRecordsBar(self.model, self) - - self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events") - - def ResizeColumnToContents(self, column, n): - # Using the view's resizeColumnToContents() here is extrememly slow - # so implement a crude alternative - mm = "MM" if column else "MMMM" - font = self.view.font() - metrics = QFontMetrics(font) - max = 0 - for row in xrange(n): - val = self.model.root.child_items[row].data[column] - len = metrics.width(str(val) + mm) - max = len if len > max else max - val = self.model.columnHeader(column) - len = metrics.width(str(val) + mm) - max = len if len > max else max - self.view.setColumnWidth(column, max) - - def ResizeColumnsToContents(self): - n = min(self.model.root.child_count, 100) - if n < 1: - # No data yet, so connect a signal to notify when there is - self.model.rowsInserted.connect(self.UpdateColumnWidths) - return - columns = self.model.columnCount() - for i in xrange(columns): - self.ResizeColumnToContents(i, n) - - def UpdateColumnWidths(self, *x): - # This only needs to be done once, so disconnect the signal now - self.model.rowsInserted.disconnect(self.UpdateColumnWidths) - self.ResizeColumnsToContents() - - def Find(self, value, direction, pattern, context): - self.view.setFocus() - self.find_bar.Busy() - self.finder.Find(value, direction, pattern, context, self.FindDone) - - def FindDone(self, row): - self.find_bar.Idle() - if row >= 0: - self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) - else: - self.find_bar.NotFound() - -# Line edit data item - -class LineEditDataItem(object): - - def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): - self.glb = glb - self.label = label - self.placeholder_text = placeholder_text - self.parent = parent - self.id = id - - self.value = default - - self.widget = QLineEdit(default) - self.widget.editingFinished.connect(self.Validate) - self.widget.textChanged.connect(self.Invalidate) - self.red = False - self.error = "" - self.validated = True - - if placeholder_text: - self.widget.setPlaceholderText(placeholder_text) - - def TurnTextRed(self): - if not self.red: - palette = QPalette() - palette.setColor(QPalette.Text,Qt.red) - self.widget.setPalette(palette) - self.red = True - - def TurnTextNormal(self): - if self.red: - palette = QPalette() - self.widget.setPalette(palette) - self.red = False - - def InvalidValue(self, value): - self.value = "" - self.TurnTextRed() - self.error = self.label + " invalid value '" + value + "'" - self.parent.ShowMessage(self.error) - - def Invalidate(self): - self.validated = False - - def DoValidate(self, input_string): - self.value = input_string.strip() - - def Validate(self): - self.validated = True - self.error = "" - self.TurnTextNormal() - self.parent.ClearMessage() - input_string = self.widget.text() - if not len(input_string.strip()): - self.value = "" - return - self.DoValidate(input_string) - - def IsValid(self): - if not self.validated: - self.Validate() - if len(self.error): - self.parent.ShowMessage(self.error) - return False - return True - - def IsNumber(self, value): - try: - x = int(value) - except: - x = 0 - return str(x) == value - -# Non-negative integer ranges dialog data item - -class NonNegativeIntegerRangesDataItem(LineEditDataItem): - - def __init__(self, glb, label, placeholder_text, column_name, parent): - super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent) - - self.column_name = column_name - - def DoValidate(self, input_string): - singles = [] - ranges = [] - for value in [x.strip() for x in input_string.split(",")]: - if "-" in value: - vrange = value.split("-") - if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): - return self.InvalidValue(value) - ranges.append(vrange) - else: - if not self.IsNumber(value): - return self.InvalidValue(value) - singles.append(value) - ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] - if len(singles): - ranges.append(self.column_name + " IN (" + ",".join(singles) + ")") - self.value = " OR ".join(ranges) - -# Positive integer dialog data item - -class PositiveIntegerDataItem(LineEditDataItem): - - def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): - super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default) - - def DoValidate(self, input_string): - if not self.IsNumber(input_string.strip()): - return self.InvalidValue(input_string) - value = int(input_string.strip()) - if value <= 0: - return self.InvalidValue(input_string) - self.value = str(value) - -# Dialog data item converted and validated using a SQL table - -class SQLTableDataItem(LineEditDataItem): - - def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent): - super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent) - - self.table_name = table_name - self.match_column = match_column - self.column_name1 = column_name1 - self.column_name2 = column_name2 - - def ValueToIds(self, value): - ids = [] - query = QSqlQuery(self.glb.db) - stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'" - ret = query.exec_(stmt) - if ret: - while query.next(): - ids.append(str(query.value(0))) - return ids - - def DoValidate(self, input_string): - all_ids = [] - for value in [x.strip() for x in input_string.split(",")]: - ids = self.ValueToIds(value) - if len(ids): - all_ids.extend(ids) - else: - return self.InvalidValue(value) - self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")" - if self.column_name2: - self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )" - -# Sample time ranges dialog data item converted and validated using 'samples' SQL table - -class SampleTimeRangesDataItem(LineEditDataItem): - - def __init__(self, glb, label, placeholder_text, column_name, parent): - self.column_name = column_name - - self.last_id = 0 - self.first_time = 0 - self.last_time = 2 ** 64 - - query = QSqlQuery(glb.db) - QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1") - if query.next(): - self.last_id = int(query.value(0)) - self.first_time = int(glb.HostStartTime()) - self.last_time = int(glb.HostFinishTime()) - if placeholder_text: - placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time) - - super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent) - - def IdBetween(self, query, lower_id, higher_id, order): - QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1") - if query.next(): - return True, int(query.value(0)) - else: - return False, 0 - - def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor): - query = QSqlQuery(self.glb.db) - while True: - next_id = int((lower_id + higher_id) / 2) - QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) - if not query.next(): - ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC") - if not ok: - ok, dbid = self.IdBetween(query, next_id, higher_id, "") - if not ok: - return str(higher_id) - next_id = dbid - QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) - next_time = int(query.value(0)) - if get_floor: - if target_time > next_time: - lower_id = next_id - else: - higher_id = next_id - if higher_id <= lower_id + 1: - return str(higher_id) - else: - if target_time >= next_time: - lower_id = next_id - else: - higher_id = next_id - if higher_id <= lower_id + 1: - return str(lower_id) - - def ConvertRelativeTime(self, val): - mult = 1 - suffix = val[-2:] - if suffix == "ms": - mult = 1000000 - elif suffix == "us": - mult = 1000 - elif suffix == "ns": - mult = 1 - else: - return val - val = val[:-2].strip() - if not self.IsNumber(val): - return val - val = int(val) * mult - if val >= 0: - val += self.first_time - else: - val += self.last_time - return str(val) - - def ConvertTimeRange(self, vrange): - if vrange[0] == "": - vrange[0] = str(self.first_time) - if vrange[1] == "": - vrange[1] = str(self.last_time) - vrange[0] = self.ConvertRelativeTime(vrange[0]) - vrange[1] = self.ConvertRelativeTime(vrange[1]) - if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): - return False - beg_range = max(int(vrange[0]), self.first_time) - end_range = min(int(vrange[1]), self.last_time) - if beg_range > self.last_time or end_range < self.first_time: - return False - vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True) - vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False) - return True - - def AddTimeRange(self, value, ranges): - n = value.count("-") - if n == 1: - pass - elif n == 2: - if value.split("-")[1].strip() == "": - n = 1 - elif n == 3: - n = 2 - else: - return False - pos = findnth(value, "-", n) - vrange = [value[:pos].strip() ,value[pos+1:].strip()] - if self.ConvertTimeRange(vrange): - ranges.append(vrange) - return True - return False - - def DoValidate(self, input_string): - ranges = [] - for value in [x.strip() for x in input_string.split(",")]: - if not self.AddTimeRange(value, ranges): - return self.InvalidValue(value) - ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] - self.value = " OR ".join(ranges) - -# Report Dialog Base - -class ReportDialogBase(QDialog): - - def __init__(self, glb, title, items, partial, parent=None): - super(ReportDialogBase, self).__init__(parent) - - self.glb = glb - - self.report_vars = ReportVars() - - self.setWindowTitle(title) - self.setMinimumWidth(600) - - self.data_items = [x(glb, self) for x in items] - - self.partial = partial - - self.grid = QGridLayout() - - for row in xrange(len(self.data_items)): - self.grid.addWidget(QLabel(self.data_items[row].label), row, 0) - self.grid.addWidget(self.data_items[row].widget, row, 1) - - self.status = QLabel() - - self.ok_button = QPushButton("Ok", self) - self.ok_button.setDefault(True) - self.ok_button.released.connect(self.Ok) - self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.cancel_button = QPushButton("Cancel", self) - self.cancel_button.released.connect(self.reject) - self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.hbox = QHBoxLayout() - #self.hbox.addStretch() - self.hbox.addWidget(self.status) - self.hbox.addWidget(self.ok_button) - self.hbox.addWidget(self.cancel_button) - - self.vbox = QVBoxLayout() - self.vbox.addLayout(self.grid) - self.vbox.addLayout(self.hbox) - - self.setLayout(self.vbox) - - def Ok(self): - vars = self.report_vars - for d in self.data_items: - if d.id == "REPORTNAME": - vars.name = d.value - if not vars.name: - self.ShowMessage("Report name is required") - return - for d in self.data_items: - if not d.IsValid(): - return - for d in self.data_items[1:]: - if d.id == "LIMIT": - vars.limit = d.value - elif len(d.value): - if len(vars.where_clause): - vars.where_clause += " AND " - vars.where_clause += d.value - if len(vars.where_clause): - if self.partial: - vars.where_clause = " AND ( " + vars.where_clause + " ) " - else: - vars.where_clause = " WHERE " + vars.where_clause + " " - self.accept() - - def ShowMessage(self, msg): - self.status.setText("" + msg) - - def ClearMessage(self): - self.status.setText("") - -# Selected branch report creation dialog - -class SelectedBranchDialog(ReportDialogBase): - - def __init__(self, glb, parent=None): - title = "Selected Branches" - items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), - lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p), - lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p), - lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p), - lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p), - lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p), - lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p), - lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p), - lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p)) - super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent) - -# Event list - -def GetEventList(db): - events = [] - query = QSqlQuery(db) - QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id") - while query.next(): - events.append(query.value(0)) - return events - -# Is a table selectable - -def IsSelectable(db, table, sql = "", columns = "*"): - query = QSqlQuery(db) - try: - QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1") - except: - return False - return True - -# SQL table data model item - -class SQLTableItem(): - - def __init__(self, row, data): - self.row = row - self.data = data - - def getData(self, column): - return self.data[column] - -# SQL table data model - -class SQLTableModel(TableModel): - - progress = Signal(object) - - def __init__(self, glb, sql, column_headers, parent=None): - super(SQLTableModel, self).__init__(parent) - self.glb = glb - self.more = True - self.populated = 0 - self.column_headers = column_headers - self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample) - self.fetcher.done.connect(self.Update) - self.fetcher.Fetch(glb_chunk_sz) - - def DisplayData(self, item, index): - self.FetchIfNeeded(item.row) - return item.getData(index.column()) - - def AddSample(self, data): - child = SQLTableItem(self.populated, data) - self.child_items.append(child) - self.populated += 1 - - def Update(self, fetched): - if not fetched: - self.more = False - self.progress.emit(0) - child_count = self.child_count - count = self.populated - child_count - if count > 0: - parent = QModelIndex() - self.beginInsertRows(parent, child_count, child_count + count - 1) - self.insertRows(child_count, count, parent) - self.child_count += count - self.endInsertRows() - self.progress.emit(self.child_count) - - def FetchMoreRecords(self, count): - current = self.child_count - if self.more: - self.fetcher.Fetch(count) - else: - self.progress.emit(0) - return current - - def HasMoreRecords(self): - return self.more - - def columnCount(self, parent=None): - return len(self.column_headers) - - def columnHeader(self, column): - return self.column_headers[column] - - def SQLTableDataPrep(self, query, count): - data = [] - for i in xrange(count): - data.append(query.value(i)) - return data - -# SQL automatic table data model - -class SQLAutoTableModel(SQLTableModel): - - def __init__(self, glb, table_name, parent=None): - sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz) - if table_name == "comm_threads_view": - # For now, comm_threads_view has no id column - sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz) - column_headers = [] - query = QSqlQuery(glb.db) - if glb.dbref.is_sqlite3: - QueryExec(query, "PRAGMA table_info(" + table_name + ")") - while query.next(): - column_headers.append(query.value(1)) - if table_name == "sqlite_master": - sql = "SELECT * FROM " + table_name - else: - if table_name[:19] == "information_schema.": - sql = "SELECT * FROM " + table_name - select_table_name = table_name[19:] - schema = "information_schema" - else: - select_table_name = table_name - schema = "public" - QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'") - while query.next(): - column_headers.append(query.value(0)) - if pyside_version_1 and sys.version_info[0] == 3: - if table_name == "samples_view": - self.SQLTableDataPrep = self.samples_view_DataPrep - if table_name == "samples": - self.SQLTableDataPrep = self.samples_DataPrep - super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent) - - def samples_view_DataPrep(self, query, count): - data = [] - data.append(query.value(0)) - # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string - data.append("{:>19}".format(query.value(1))) - for i in xrange(2, count): - data.append(query.value(i)) - return data - - def samples_DataPrep(self, query, count): - data = [] - for i in xrange(9): - data.append(query.value(i)) - # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string - data.append("{:>19}".format(query.value(9))) - for i in xrange(10, count): - data.append(query.value(i)) - return data - -# Base class for custom ResizeColumnsToContents - -class ResizeColumnsToContentsBase(QObject): - - def __init__(self, parent=None): - super(ResizeColumnsToContentsBase, self).__init__(parent) - - def ResizeColumnToContents(self, column, n): - # Using the view's resizeColumnToContents() here is extrememly slow - # so implement a crude alternative - font = self.view.font() - metrics = QFontMetrics(font) - max = 0 - for row in xrange(n): - val = self.data_model.child_items[row].data[column] - len = metrics.width(str(val) + "MM") - max = len if len > max else max - val = self.data_model.columnHeader(column) - len = metrics.width(str(val) + "MM") - max = len if len > max else max - self.view.setColumnWidth(column, max) - - def ResizeColumnsToContents(self): - n = min(self.data_model.child_count, 100) - if n < 1: - # No data yet, so connect a signal to notify when there is - self.data_model.rowsInserted.connect(self.UpdateColumnWidths) - return - columns = self.data_model.columnCount() - for i in xrange(columns): - self.ResizeColumnToContents(i, n) - - def UpdateColumnWidths(self, *x): - # This only needs to be done once, so disconnect the signal now - self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths) - self.ResizeColumnsToContents() - -# Convert value to CSV - -def ToCSValue(val): - if '"' in val: - val = val.replace('"', '""') - if "," in val or '"' in val: - val = '"' + val + '"' - return val - -# Key to sort table model indexes by row / column, assuming fewer than 1000 columns - -glb_max_cols = 1000 - -def RowColumnKey(a): - return a.row() * glb_max_cols + a.column() - -# Copy selected table cells to clipboard - -def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False): - indexes = sorted(view.selectedIndexes(), key=RowColumnKey) - idx_cnt = len(indexes) - if not idx_cnt: - return - if idx_cnt == 1: - with_hdr=False - min_row = indexes[0].row() - max_row = indexes[0].row() - min_col = indexes[0].column() - max_col = indexes[0].column() - for i in indexes: - min_row = min(min_row, i.row()) - max_row = max(max_row, i.row()) - min_col = min(min_col, i.column()) - max_col = max(max_col, i.column()) - if max_col > glb_max_cols: - raise RuntimeError("glb_max_cols is too low") - max_width = [0] * (1 + max_col - min_col) - for i in indexes: - c = i.column() - min_col - max_width[c] = max(max_width[c], len(str(i.data()))) - text = "" - pad = "" - sep = "" - if with_hdr: - model = indexes[0].model() - for col in range(min_col, max_col + 1): - val = model.headerData(col, Qt.Horizontal, Qt.DisplayRole) - if as_csv: - text += sep + ToCSValue(val) - sep = "," - else: - c = col - min_col - max_width[c] = max(max_width[c], len(val)) - width = max_width[c] - align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole) - if align & Qt.AlignRight: - val = val.rjust(width) - text += pad + sep + val - pad = " " * (width - len(val)) - sep = " " - text += "\n" - pad = "" - sep = "" - last_row = min_row - for i in indexes: - if i.row() > last_row: - last_row = i.row() - text += "\n" - pad = "" - sep = "" - if as_csv: - text += sep + ToCSValue(str(i.data())) - sep = "," - else: - width = max_width[i.column() - min_col] - if i.data(Qt.TextAlignmentRole) & Qt.AlignRight: - val = str(i.data()).rjust(width) - else: - val = str(i.data()) - text += pad + sep + val - pad = " " * (width - len(val)) - sep = " " - QApplication.clipboard().setText(text) - -def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False): - indexes = view.selectedIndexes() - if not len(indexes): - return - - selection = view.selectionModel() - - first = None - for i in indexes: - above = view.indexAbove(i) - if not selection.isSelected(above): - first = i - break - - if first is None: - raise RuntimeError("CopyTreeCellsToClipboard internal error") - - model = first.model() - row_cnt = 0 - col_cnt = model.columnCount(first) - max_width = [0] * col_cnt - - indent_sz = 2 - indent_str = " " * indent_sz - - expanded_mark_sz = 2 - if sys.version_info[0] == 3: - expanded_mark = "\u25BC " - not_expanded_mark = "\u25B6 " - else: - expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8") - not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8") - leaf_mark = " " - - if not as_csv: - pos = first - while True: - row_cnt += 1 - row = pos.row() - for c in range(col_cnt): - i = pos.sibling(row, c) - if c: - n = len(str(i.data())) - else: - n = len(str(i.data()).strip()) - n += (i.internalPointer().level - 1) * indent_sz - n += expanded_mark_sz - max_width[c] = max(max_width[c], n) - pos = view.indexBelow(pos) - if not selection.isSelected(pos): - break - - text = "" - pad = "" - sep = "" - if with_hdr: - for c in range(col_cnt): - val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip() - if as_csv: - text += sep + ToCSValue(val) - sep = "," - else: - max_width[c] = max(max_width[c], len(val)) - width = max_width[c] - align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole) - if align & Qt.AlignRight: - val = val.rjust(width) - text += pad + sep + val - pad = " " * (width - len(val)) - sep = " " - text += "\n" - pad = "" - sep = "" - - pos = first - while True: - row = pos.row() - for c in range(col_cnt): - i = pos.sibling(row, c) - val = str(i.data()) - if not c: - if model.hasChildren(i): - if view.isExpanded(i): - mark = expanded_mark - else: - mark = not_expanded_mark - else: - mark = leaf_mark - val = indent_str * (i.internalPointer().level - 1) + mark + val.strip() - if as_csv: - text += sep + ToCSValue(val) - sep = "," - else: - width = max_width[c] - if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight: - val = val.rjust(width) - text += pad + sep + val - pad = " " * (width - len(val)) - sep = " " - pos = view.indexBelow(pos) - if not selection.isSelected(pos): - break - text = text.rstrip() + "\n" - pad = "" - sep = "" - - QApplication.clipboard().setText(text) - -def CopyCellsToClipboard(view, as_csv=False, with_hdr=False): - view.CopyCellsToClipboard(view, as_csv, with_hdr) - -def CopyCellsToClipboardHdr(view): - CopyCellsToClipboard(view, False, True) - -def CopyCellsToClipboardCSV(view): - CopyCellsToClipboard(view, True, True) - -# Context menu - -class ContextMenu(object): - - def __init__(self, view): - self.view = view - self.view.setContextMenuPolicy(Qt.CustomContextMenu) - self.view.customContextMenuRequested.connect(self.ShowContextMenu) - - def ShowContextMenu(self, pos): - menu = QMenu(self.view) - self.AddActions(menu) - menu.exec_(self.view.mapToGlobal(pos)) - - def AddCopy(self, menu): - menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view)) - menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view)) - - def AddActions(self, menu): - self.AddCopy(menu) - -class TreeContextMenu(ContextMenu): - - def __init__(self, view): - super(TreeContextMenu, self).__init__(view) - - def AddActions(self, menu): - i = self.view.currentIndex() - text = str(i.data()).strip() - if len(text): - menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view)) - self.AddCopy(menu) - -# Table window - -class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase): - - def __init__(self, glb, table_name, parent=None): - super(TableWindow, self).__init__(parent) - - self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name)) - - self.model = QSortFilterProxyModel() - self.model.setSourceModel(self.data_model) - - self.view = QTableView() - self.view.setModel(self.model) - self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.view.verticalHeader().setVisible(False) - self.view.sortByColumn(-1, Qt.AscendingOrder) - self.view.setSortingEnabled(True) - self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) - self.view.CopyCellsToClipboard = CopyTableCellsToClipboard - - self.ResizeColumnsToContents() - - self.context_menu = ContextMenu(self.view) - - self.find_bar = FindBar(self, self, True) - - self.finder = ChildDataItemFinder(self.data_model) - - self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) - - self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table") - - def Find(self, value, direction, pattern, context): - self.view.setFocus() - self.find_bar.Busy() - self.finder.Find(value, direction, pattern, context, self.FindDone) - - def FindDone(self, row): - self.find_bar.Idle() - if row >= 0: - self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex()))) - else: - self.find_bar.NotFound() - -# Table list - -def GetTableList(glb): - tables = [] - query = QSqlQuery(glb.db) - if glb.dbref.is_sqlite3: - QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name") - else: - QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name") - while query.next(): - tables.append(query.value(0)) - if glb.dbref.is_sqlite3: - tables.append("sqlite_master") - else: - tables.append("information_schema.tables") - tables.append("information_schema.views") - tables.append("information_schema.columns") - return tables - -# Top Calls data model - -class TopCallsModel(SQLTableModel): - - def __init__(self, glb, report_vars, parent=None): - text = "" - if not glb.dbref.is_sqlite3: - text = "::text" - limit = "" - if len(report_vars.limit): - limit = " LIMIT " + report_vars.limit - sql = ("SELECT comm, pid, tid, name," - " CASE" - " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text + - " ELSE short_name" - " END AS dso," - " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, " - " CASE" - " WHEN (calls.flags = 1) THEN 'no call'" + text + - " WHEN (calls.flags = 2) THEN 'no return'" + text + - " WHEN (calls.flags = 3) THEN 'no call/return'" + text + - " ELSE ''" + text + - " END AS flags" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " INNER JOIN dsos ON symbols.dso_id = dsos.id" - " INNER JOIN comms ON calls.comm_id = comms.id" - " INNER JOIN threads ON calls.thread_id = threads.id" + - report_vars.where_clause + - " ORDER BY elapsed_time DESC" + - limit - ) - column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags") - self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft) - super(TopCallsModel, self).__init__(glb, sql, column_headers, parent) - - def columnAlignment(self, column): - return self.alignment[column] - -# Top Calls report creation dialog - -class TopCallsDialog(ReportDialogBase): - - def __init__(self, glb, parent=None): - title = "Top Calls by Elapsed Time" - items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), - lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p), - lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p), - lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p), - lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p), - lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p), - lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p), - lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100")) - super(TopCallsDialog, self).__init__(glb, title, items, False, parent) - -# Top Calls window - -class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase): - - def __init__(self, glb, report_vars, parent=None): - super(TopCallsWindow, self).__init__(parent) - - self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars)) - self.model = self.data_model - - self.view = QTableView() - self.view.setModel(self.model) - self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.view.verticalHeader().setVisible(False) - self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) - self.view.CopyCellsToClipboard = CopyTableCellsToClipboard - - self.context_menu = ContextMenu(self.view) - - self.ResizeColumnsToContents() - - self.find_bar = FindBar(self, self, True) - - self.finder = ChildDataItemFinder(self.model) - - self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) - - self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name) - - def Find(self, value, direction, pattern, context): - self.view.setFocus() - self.find_bar.Busy() - self.finder.Find(value, direction, pattern, context, self.FindDone) - - def FindDone(self, row): - self.find_bar.Idle() - if row >= 0: - self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) - else: - self.find_bar.NotFound() - -# Action Definition - -def CreateAction(label, tip, callback, parent=None, shortcut=None): - action = QAction(label, parent) - if shortcut != None: - action.setShortcuts(shortcut) - action.setStatusTip(tip) - action.triggered.connect(callback) - return action - -# Typical application actions - -def CreateExitAction(app, parent=None): - return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) - -# Typical MDI actions - -def CreateCloseActiveWindowAction(mdi_area): - return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) - -def CreateCloseAllWindowsAction(mdi_area): - return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) - -def CreateTileWindowsAction(mdi_area): - return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) - -def CreateCascadeWindowsAction(mdi_area): - return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) - -def CreateNextWindowAction(mdi_area): - return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) - -def CreatePreviousWindowAction(mdi_area): - return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) - -# Typical MDI window menu - -class WindowMenu(): - - def __init__(self, mdi_area, menu): - self.mdi_area = mdi_area - self.window_menu = menu.addMenu("&Windows") - self.close_active_window = CreateCloseActiveWindowAction(mdi_area) - self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) - self.tile_windows = CreateTileWindowsAction(mdi_area) - self.cascade_windows = CreateCascadeWindowsAction(mdi_area) - self.next_window = CreateNextWindowAction(mdi_area) - self.previous_window = CreatePreviousWindowAction(mdi_area) - self.window_menu.aboutToShow.connect(self.Update) - - def Update(self): - self.window_menu.clear() - sub_window_count = len(self.mdi_area.subWindowList()) - have_sub_windows = sub_window_count != 0 - self.close_active_window.setEnabled(have_sub_windows) - self.close_all_windows.setEnabled(have_sub_windows) - self.tile_windows.setEnabled(have_sub_windows) - self.cascade_windows.setEnabled(have_sub_windows) - self.next_window.setEnabled(have_sub_windows) - self.previous_window.setEnabled(have_sub_windows) - self.window_menu.addAction(self.close_active_window) - self.window_menu.addAction(self.close_all_windows) - self.window_menu.addSeparator() - self.window_menu.addAction(self.tile_windows) - self.window_menu.addAction(self.cascade_windows) - self.window_menu.addSeparator() - self.window_menu.addAction(self.next_window) - self.window_menu.addAction(self.previous_window) - if sub_window_count == 0: - return - self.window_menu.addSeparator() - nr = 1 - for sub_window in self.mdi_area.subWindowList(): - label = str(nr) + " " + sub_window.name - if nr < 10: - label = "&" + label - action = self.window_menu.addAction(label) - action.setCheckable(True) - action.setChecked(sub_window == self.mdi_area.activeSubWindow()) - action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x)) - self.window_menu.addAction(action) - nr += 1 - - def setActiveSubWindow(self, nr): - self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) - -# Help text - -glb_help_text = """ -

Contents

- -

1. Reports

-

1.1 Context-Sensitive Call Graph

-

1.2 Call Tree

-

1.3 All branches

-

1.4 Selected branches

-

1.5 Top calls by elapsed time

-

2. Charts

-

2.1 Time chart by CPU

-

3. Tables

-

1. Reports

-

1.1 Context-Sensitive Call Graph

-The result is a GUI window with a tree representing a context-sensitive -call-graph. Expanding a couple of levels of the tree and adjusting column -widths to suit will display something like: -
-                                         Call Graph: pt_example
-Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
-v- ls
-    v- 2638:2638
-        v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
-          |- unknown               unknown       1        13198     0.1              1              0.0
-          >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
-          >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
-          v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
-             >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
-             >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
-             >- __libc_csu_init    ls            1        10354     0.1             10              0.0
-             |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
-             v- main               ls            1      8182043    99.6         180254             99.9
-
-

Points to note:

-
    -
  • The top level is a command name (comm)
  • -
  • The next level is a thread (pid:tid)
  • -
  • Subsequent levels are functions
  • -
  • 'Count' is the number of calls
  • -
  • 'Time' is the elapsed time until the function returns
  • -
  • Percentages are relative to the level above
  • -
  • 'Branch Count' is the total number of branches for that function and all functions that it calls -
-

Find

-Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match. -The pattern matching symbols are ? for any character and * for zero or more characters. -

1.2 Call Tree

-The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated. -Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'. -

1.3 All branches

-The All branches report displays all branches in chronological order. -Not all data is fetched immediately. More records can be fetched using the Fetch bar provided. -

Disassembly

-Open a branch to display disassembly. This only works if: -
    -
  1. The disassembler is available. Currently, only Intel XED is supported - see Intel XED Setup
  2. -
  3. The object code is available. Currently, only the perf build ID cache is searched for object code. -The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR. -One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu), -or alternatively, set environment variable PERF_KCORE to the kcore file name.
  4. -
-

Intel XED Setup

-To use Intel XED, libxed.so must be present. To build and install libxed.so: -
-git clone https://github.com/intelxed/mbuild.git mbuild
-git clone https://github.com/intelxed/xed
-cd xed
-./mfile.py --share
-sudo ./mfile.py --prefix=/usr/local install
-sudo ldconfig
-
-

Instructions per Cycle (IPC)

-If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'. -

Intel PT note: The information applies to the blocks of code ending with, and including, that branch. -Due to the granularity of timing information, the number of cycles for some code blocks will not be known. -In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period -since the previous displayed 'IPC'. -

Find

-Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. -Refer to Python documentation for the regular expression syntax. -All columns are searched, but only currently fetched rows are searched. -

1.4 Selected branches

-This is the same as the All branches report but with the data reduced -by various selection criteria. A dialog box displays available criteria which are AND'ed together. -

1.4.1 Time ranges

-The time ranges hint text shows the total time range. Relative time ranges can also be entered in -ms, us or ns. Also, negative values are relative to the end of trace. Examples: -
-	81073085947329-81073085958238	From 81073085947329 to 81073085958238
-	100us-200us		From 100us to 200us
-	10ms-			From 10ms to the end
-	-100ns			The first 100ns
-	-10ms-			The last 10ms
-
-N.B. Due to the granularity of timestamps, there could be no branches in any given time range. -

1.5 Top calls by elapsed time

-The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned. -The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together. -If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar. -

2. Charts

-

2.1 Time chart by CPU

-This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu. -

Features

-
    -
  1. Mouse over to highight the task and show the time
  2. -
  3. Drag the mouse to select a region and zoom by pushing the Zoom button
  4. -
  5. Go back and forward by pressing the arrow buttons
  6. -
  7. If call information is available, right-click to show a call tree opened to that task and time. -Note, the call tree may take some time to appear, and there may not be call information for the task or time selected. -
  8. -
-

Important

-The graph can be misleading in the following respects: -
    -
  1. The graph shows the first task on each CPU as running from the beginning of the time range. -Because tracing might start on different CPUs at different times, that is not necessarily the case. -Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.
  2. -
  3. Similarly, the last task on each CPU can be showing running longer than it really was. -Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.
  4. -
  5. When the mouse is over a task, the highlighted task might not be visible on the legend without scrolling if the legend does not fit fully in the window
  6. -
-

3. Tables

-The Tables menu shows all tables and views in the database. Most tables have an associated view -which displays the information in a more friendly way. Not all data for large tables is fetched -immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted, -but that can be slow for large tables. -

There are also tables of database meta-information. -For SQLite3 databases, the sqlite_master table is included. -For PostgreSQL databases, information_schema.tables/views/columns are included. -

Find

-Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. -Refer to Python documentation for the regular expression syntax. -All columns are searched, but only currently fetched rows are searched. -

N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous -will go to the next/previous result in id order, instead of display order. -""" - -# Help window - -class HelpWindow(QMdiSubWindow): - - def __init__(self, glb, parent=None): - super(HelpWindow, self).__init__(parent) - - self.text = QTextBrowser() - self.text.setHtml(glb_help_text) - self.text.setReadOnly(True) - self.text.setOpenExternalLinks(True) - - self.setWidget(self.text) - - AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help") - -# Main window that only displays the help text - -class HelpOnlyWindow(QMainWindow): - - def __init__(self, parent=None): - super(HelpOnlyWindow, self).__init__(parent) - - self.setMinimumSize(200, 100) - self.resize(800, 600) - self.setWindowTitle("Exported SQL Viewer Help") - self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation)) - - self.text = QTextBrowser() - self.text.setHtml(glb_help_text) - self.text.setReadOnly(True) - self.text.setOpenExternalLinks(True) - - self.setCentralWidget(self.text) - -# PostqreSQL server version - -def PostqreSQLServerVersion(db): - query = QSqlQuery(db) - QueryExec(query, "SELECT VERSION()") - if query.next(): - v_str = query.value(0) - v_list = v_str.strip().split(" ") - if v_list[0] == "PostgreSQL" and v_list[2] == "on": - return v_list[1] - return v_str - return "Unknown" - -# SQLite version - -def SQLiteVersion(db): - query = QSqlQuery(db) - QueryExec(query, "SELECT sqlite_version()") - if query.next(): - return query.value(0) - return "Unknown" - -# About dialog - -class AboutDialog(QDialog): - - def __init__(self, glb, parent=None): - super(AboutDialog, self).__init__(parent) - - self.setWindowTitle("About Exported SQL Viewer") - self.setMinimumWidth(300) - - pyside_version = "1" if pyside_version_1 else "2" - - text = "

"
-		text += "Python version:     " + sys.version.split(" ")[0] + "\n"
-		text += "PySide version:     " + pyside_version + "\n"
-		text += "Qt version:         " + qVersion() + "\n"
-		if glb.dbref.is_sqlite3:
-			text += "SQLite version:     " + SQLiteVersion(glb.db) + "\n"
-		else:
-			text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
-		text += "
" - - self.text = QTextBrowser() - self.text.setHtml(text) - self.text.setReadOnly(True) - self.text.setOpenExternalLinks(True) - - self.vbox = QVBoxLayout() - self.vbox.addWidget(self.text) - - self.setLayout(self.vbox) - -# Font resize - -def ResizeFont(widget, diff): - font = widget.font() - sz = font.pointSize() - font.setPointSize(sz + diff) - widget.setFont(font) - -def ShrinkFont(widget): - ResizeFont(widget, -1) - -def EnlargeFont(widget): - ResizeFont(widget, 1) - -# Unique name for sub-windows - -def NumberedWindowName(name, nr): - if nr > 1: - name += " <" + str(nr) + ">" - return name - -def UniqueSubWindowName(mdi_area, name): - nr = 1 - while True: - unique_name = NumberedWindowName(name, nr) - ok = True - for sub_window in mdi_area.subWindowList(): - if sub_window.name == unique_name: - ok = False - break - if ok: - return unique_name - nr += 1 - -# Add a sub-window - -def AddSubWindow(mdi_area, sub_window, name): - unique_name = UniqueSubWindowName(mdi_area, name) - sub_window.setMinimumSize(200, 100) - sub_window.resize(800, 600) - sub_window.setWindowTitle(unique_name) - sub_window.setAttribute(Qt.WA_DeleteOnClose) - sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) - sub_window.name = unique_name - mdi_area.addSubWindow(sub_window) - sub_window.show() - -# Main window - -class MainWindow(QMainWindow): - - def __init__(self, glb, parent=None): - super(MainWindow, self).__init__(parent) - - self.glb = glb - - self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) - self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) - self.setMinimumSize(200, 100) - - self.mdi_area = QMdiArea() - self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) - - self.setCentralWidget(self.mdi_area) - - menu = self.menuBar() - - file_menu = menu.addMenu("&File") - file_menu.addAction(CreateExitAction(glb.app, self)) - - edit_menu = menu.addMenu("&Edit") - edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy)) - edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self)) - edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find)) - edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)])) - edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")])) - edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")])) - - reports_menu = menu.addMenu("&Reports") - if IsSelectable(glb.db, "calls"): - reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) - - if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"): - reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self)) - - self.EventMenu(GetEventList(glb.db), reports_menu) - - if IsSelectable(glb.db, "calls"): - reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self)) - - if IsSelectable(glb.db, "context_switches"): - charts_menu = menu.addMenu("&Charts") - charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self)) - - self.TableMenu(GetTableList(glb), menu) - - self.window_menu = WindowMenu(self.mdi_area, menu) - - help_menu = menu.addMenu("&Help") - help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents)) - help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self)) - - def Try(self, fn): - win = self.mdi_area.activeSubWindow() - if win: - try: - fn(win.view) - except: - pass - - def CopyToClipboard(self): - self.Try(CopyCellsToClipboardHdr) - - def CopyToClipboardCSV(self): - self.Try(CopyCellsToClipboardCSV) - - def Find(self): - win = self.mdi_area.activeSubWindow() - if win: - try: - win.find_bar.Activate() - except: - pass - - def FetchMoreRecords(self): - win = self.mdi_area.activeSubWindow() - if win: - try: - win.fetch_bar.Activate() - except: - pass - - def ShrinkFont(self): - self.Try(ShrinkFont) - - def EnlargeFont(self): - self.Try(EnlargeFont) - - def EventMenu(self, events, reports_menu): - branches_events = 0 - for event in events: - event = event.split(":")[0] - if event == "branches": - branches_events += 1 - dbid = 0 - for event in events: - dbid += 1 - event = event.split(":")[0] - if event == "branches": - label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")" - reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self)) - label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")" - reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self)) - - def TimeChartByCPU(self): - TimeChartByCPUWindow(self.glb, self) - - def TableMenu(self, tables, menu): - table_menu = menu.addMenu("&Tables") - for table in tables: - table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self)) - - def NewCallGraph(self): - CallGraphWindow(self.glb, self) - - def NewCallTree(self): - CallTreeWindow(self.glb, self) - - def NewTopCalls(self): - dialog = TopCallsDialog(self.glb, self) - ret = dialog.exec_() - if ret: - TopCallsWindow(self.glb, dialog.report_vars, self) - - def NewBranchView(self, event_id): - BranchWindow(self.glb, event_id, ReportVars(), self) - - def NewSelectedBranchView(self, event_id): - dialog = SelectedBranchDialog(self.glb, self) - ret = dialog.exec_() - if ret: - BranchWindow(self.glb, event_id, dialog.report_vars, self) - - def NewTableView(self, table_name): - TableWindow(self.glb, table_name, self) - - def Help(self): - HelpWindow(self.glb, self) - - def About(self): - dialog = AboutDialog(self.glb, self) - dialog.exec_() - -def TryOpen(file_name): - try: - return open(file_name, "rb") - except: - return None - -def Is64Bit(f): - result = sizeof(c_void_p) - # ELF support only - pos = f.tell() - f.seek(0) - header = f.read(7) - f.seek(pos) - magic = header[0:4] - if sys.version_info[0] == 2: - eclass = ord(header[4]) - encoding = ord(header[5]) - version = ord(header[6]) - else: - eclass = header[4] - encoding = header[5] - version = header[6] - if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1: - result = True if eclass == 2 else False - return result - -# Global data - -class Glb(): - - def __init__(self, dbref, db, dbname): - self.dbref = dbref - self.db = db - self.dbname = dbname - self.home_dir = os.path.expanduser("~") - self.buildid_dir = os.getenv("PERF_BUILDID_DIR") - if self.buildid_dir: - self.buildid_dir += "/.build-id/" - else: - self.buildid_dir = self.home_dir + "/.debug/.build-id/" - self.app = None - self.mainwindow = None - self.instances_to_shutdown_on_exit = weakref.WeakSet() - try: - self.disassembler = LibXED() - self.have_disassembler = True - except: - self.have_disassembler = False - self.host_machine_id = 0 - self.host_start_time = 0 - self.host_finish_time = 0 - - def FileFromBuildId(self, build_id): - file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf" - return TryOpen(file_name) - - def FileFromNamesAndBuildId(self, short_name, long_name, build_id): - # Assume current machine i.e. no support for virtualization - if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore": - file_name = os.getenv("PERF_KCORE") - f = TryOpen(file_name) if file_name else None - if f: - return f - # For now, no special handling if long_name is /proc/kcore - f = TryOpen(long_name) - if f: - return f - f = self.FileFromBuildId(build_id) - if f: - return f - return None - - def AddInstanceToShutdownOnExit(self, instance): - self.instances_to_shutdown_on_exit.add(instance) - - # Shutdown any background processes or threads - def ShutdownInstances(self): - for x in self.instances_to_shutdown_on_exit: - try: - x.Shutdown() - except: - pass - - def GetHostMachineId(self): - query = QSqlQuery(self.db) - QueryExec(query, "SELECT id FROM machines WHERE pid = -1") - if query.next(): - self.host_machine_id = query.value(0) - else: - self.host_machine_id = 0 - return self.host_machine_id - - def HostMachineId(self): - if self.host_machine_id: - return self.host_machine_id - return self.GetHostMachineId() - - def SelectValue(self, sql): - query = QSqlQuery(self.db) - try: - QueryExec(query, sql) - except: - return None - if query.next(): - return Decimal(query.value(0)) - return None - - def SwitchesMinTime(self, machine_id): - return self.SelectValue("SELECT time" - " FROM context_switches" - " WHERE time != 0 AND machine_id = " + str(machine_id) + - " ORDER BY id LIMIT 1") - - def SwitchesMaxTime(self, machine_id): - return self.SelectValue("SELECT time" - " FROM context_switches" - " WHERE time != 0 AND machine_id = " + str(machine_id) + - " ORDER BY id DESC LIMIT 1") - - def SamplesMinTime(self, machine_id): - return self.SelectValue("SELECT time" - " FROM samples" - " WHERE time != 0 AND machine_id = " + str(machine_id) + - " ORDER BY id LIMIT 1") - - def SamplesMaxTime(self, machine_id): - return self.SelectValue("SELECT time" - " FROM samples" - " WHERE time != 0 AND machine_id = " + str(machine_id) + - " ORDER BY id DESC LIMIT 1") - - def CallsMinTime(self, machine_id): - return self.SelectValue("SELECT calls.call_time" - " FROM calls" - " INNER JOIN threads ON threads.thread_id = calls.thread_id" - " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) + - " ORDER BY calls.id LIMIT 1") - - def CallsMaxTime(self, machine_id): - return self.SelectValue("SELECT calls.return_time" - " FROM calls" - " INNER JOIN threads ON threads.thread_id = calls.thread_id" - " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) + - " ORDER BY calls.return_time DESC LIMIT 1") - - def GetStartTime(self, machine_id): - t0 = self.SwitchesMinTime(machine_id) - t1 = self.SamplesMinTime(machine_id) - t2 = self.CallsMinTime(machine_id) - if t0 is None or (not(t1 is None) and t1 < t0): - t0 = t1 - if t0 is None or (not(t2 is None) and t2 < t0): - t0 = t2 - return t0 - - def GetFinishTime(self, machine_id): - t0 = self.SwitchesMaxTime(machine_id) - t1 = self.SamplesMaxTime(machine_id) - t2 = self.CallsMaxTime(machine_id) - if t0 is None or (not(t1 is None) and t1 > t0): - t0 = t1 - if t0 is None or (not(t2 is None) and t2 > t0): - t0 = t2 - return t0 - - def HostStartTime(self): - if self.host_start_time: - return self.host_start_time - self.host_start_time = self.GetStartTime(self.HostMachineId()) - return self.host_start_time - - def HostFinishTime(self): - if self.host_finish_time: - return self.host_finish_time - self.host_finish_time = self.GetFinishTime(self.HostMachineId()) - return self.host_finish_time - - def StartTime(self, machine_id): - if machine_id == self.HostMachineId(): - return self.HostStartTime() - return self.GetStartTime(machine_id) - - def FinishTime(self, machine_id): - if machine_id == self.HostMachineId(): - return self.HostFinishTime() - return self.GetFinishTime(machine_id) - -# Database reference - -class DBRef(): - - def __init__(self, is_sqlite3, dbname): - self.is_sqlite3 = is_sqlite3 - self.dbname = dbname - self.TRUE = "TRUE" - self.FALSE = "FALSE" - # SQLite prior to version 3.23 does not support TRUE and FALSE - if self.is_sqlite3: - self.TRUE = "1" - self.FALSE = "0" - - def Open(self, connection_name): - dbname = self.dbname - if self.is_sqlite3: - db = QSqlDatabase.addDatabase("QSQLITE", connection_name) - else: - db = QSqlDatabase.addDatabase("QPSQL", connection_name) - opts = dbname.split() - for opt in opts: - if "=" in opt: - opt = opt.split("=") - if opt[0] == "hostname": - db.setHostName(opt[1]) - elif opt[0] == "port": - db.setPort(int(opt[1])) - elif opt[0] == "username": - db.setUserName(opt[1]) - elif opt[0] == "password": - db.setPassword(opt[1]) - elif opt[0] == "dbname": - dbname = opt[1] - else: - dbname = opt - - db.setDatabaseName(dbname) - if not db.open(): - raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) - return db, dbname - -# Main - -def Main(): - usage_str = "exported-sql-viewer.py [--pyside-version-1] \n" \ - " or: exported-sql-viewer.py --help-only" - ap = argparse.ArgumentParser(usage = usage_str, add_help = False) - ap.add_argument("--pyside-version-1", action='store_true') - ap.add_argument("dbname", nargs="?") - ap.add_argument("--help-only", action='store_true') - args = ap.parse_args() - - if args.help_only: - app = QApplication(sys.argv) - mainwindow = HelpOnlyWindow() - mainwindow.show() - err = app.exec_() - sys.exit(err) - - dbname = args.dbname - if dbname is None: - ap.print_usage() - print("Too few arguments") - sys.exit(1) - - is_sqlite3 = False - try: - f = open(dbname, "rb") - if f.read(15) == b'SQLite format 3': - is_sqlite3 = True - f.close() - except: - pass - - dbref = DBRef(is_sqlite3, dbname) - db, dbname = dbref.Open("main") - glb = Glb(dbref, db, dbname) - app = QApplication(sys.argv) - glb.app = app - mainwindow = MainWindow(glb) - glb.mainwindow = mainwindow - mainwindow.show() - err = app.exec_() - glb.ShutdownInstances() - db.close() - sys.exit(err) - -if __name__ == "__main__": - Main() From 5084b5f91a70e0aa2811c7c894ac19424bfc4194 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:12 -0700 Subject: [PATCH 220/425] perf python: Move and clean up parallel-perf.py Move parallel-perf.py from tools/perf/scripts/python/ to tools/perf/python/ as it is a standalone Python utility that invokes 'perf script' in parallel across time slices and CPUs. Update tools/perf/tests/shell/script.sh accordingly. Also fix bugs and clean up the script to pass mypy and pylint without suppression comments: - Fix Work.command() to return the shlex.quote()-escaped command string (previously sh_cmd was computed with shlex.quote() and discarded in favor of unquoted self.cmd). - Close self.popen.stdout in the parent process after spawning the consumer subprocess in Work.start() when --pipe-to is used, ensuring the producer receives SIGPIPE if the consumer exits early and avoiding leaking the pipe file descriptor. - Convert method and function names to snake_case, specify utf-8 file encodings, and add type annotations and docstrings. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/python/parallel-perf.py | 1250 ++++++++++++++++++++ tools/perf/scripts/python/parallel-perf.py | 989 ---------------- tools/perf/tests/shell/script.sh | 4 +- 3 files changed, 1252 insertions(+), 991 deletions(-) create mode 100755 tools/perf/python/parallel-perf.py delete mode 100755 tools/perf/scripts/python/parallel-perf.py diff --git a/tools/perf/python/parallel-perf.py b/tools/perf/python/parallel-perf.py new file mode 100755 index 000000000000..a43b003634fd --- /dev/null +++ b/tools/perf/python/parallel-perf.py @@ -0,0 +1,1250 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +"""Parallel perf script.""" +# +# run a perf script command multiple times in parallel, using perf script +# options --cpu and --time so that each job processes a different chunk +# of the data. +# +# Copyright (c) 2024, Intel Corporation. + +import subprocess +import argparse +import pathlib +import shlex +import time +from typing import Any +import copy +import sys +import os +import re + +glb_prog_name = "parallel-perf.py" +glb_min_interval = 10.0 +glb_min_samples = 64 + + +class Verbosity(): + + def __init__(self, quiet: bool = False, verbose: bool = False, + debug: bool = False) -> None: + """__init__.""" + + self.normal = True + self.verbose = verbose + self.debug = debug + self.self_test = True + if self.debug: + self.verbose = True + if self.verbose: + quiet = False + if quiet: + self.normal = False + +# Manage work (start/wait/kill), as represented by a subprocess.Popen command + + +class Work(): + + def __init__(self, cmd: list[str], pipe_to: str, + output_dir: str = ".") -> None: + """__init__.""" + + self.popen: Any = None + self.consumer: Any = None + self.cmd = cmd + self.pipe_to = pipe_to + self.output_dir = output_dir + self.cmdout_name = f"{output_dir}/cmd.txt" + self.stdout_name = f"{output_dir}/out.txt" + self.stderr_name = f"{output_dir}/err.txt" + + def command(self): + """command.""" + + return " ".join(shlex.quote(x) for x in self.cmd) + + def stdout(self): + """stdout.""" + + return open(self.stdout_name, "w", encoding="utf-8") + + def stderr(self): + """stderr.""" + + return open(self.stderr_name, "w", encoding="utf-8") + + def create_output_dir(self): + """create_output_dir.""" + + pathlib.Path(self.output_dir).mkdir(parents=True, exist_ok=True) + + def start(self): + """start.""" + + if self.popen: + return + self.create_output_dir() + with open(self.cmdout_name, "w", encoding="utf-8") as f: + f.write(self.command()) + f.write("\n") + stdout = self.stdout() + stderr = self.stderr() + if self.pipe_to: + self.popen = subprocess.Popen( + self.cmd, stdout=subprocess.PIPE, stderr=stderr) + args = shlex.split(self.pipe_to) + self.consumer = subprocess.Popen( + args, stdin=self.popen.stdout, stdout=stdout, stderr=stderr) + # The consumer now owns the read end of the pipe. Close this + # process's copy so that the producer receives SIGPIPE if the + # consumer exits early, and so that the descriptor isn't leaked + # for the lifetime of this Work. + self.popen.stdout.close() + self.popen.stdout = None + else: + self.popen = subprocess.Popen( + self.cmd, stdout=stdout, stderr=stderr) + + def remove_empty_err_file(self): + """remove_empty_err_file.""" + + if os.path.exists(self.stderr_name): + if os.path.getsize(self.stderr_name) == 0: + os.unlink(self.stderr_name) + + def errors(self): + """errors.""" + + if os.path.exists(self.stderr_name): + if os.path.getsize(self.stderr_name) != 0: + return [f"Non-empty error file {self.stderr_name}"] + return [] + + def tidy_up(self): + """tidy_up.""" + + self.remove_empty_err_file() + + def raw_poll_wait(self, p, wait): + """raw_poll_wait.""" + + if wait: + return p.wait() + return p.poll() + + def poll(self, wait=False): + """poll.""" + + if not self.popen: + return None + result = self.raw_poll_wait(self.popen, wait) + if self.consumer: + res = result + result = self.raw_poll_wait(self.consumer, wait) + if result is not None and res is None: + self.popen.kill() + result = None + elif result == 0 and res is not None and res != 0: + result = res + if result is not None: + self.tidy_up() + return result + + def wait(self): + """wait.""" + + return self.poll(wait=True) + + def kill(self): + """kill.""" + + if not self.popen: + return + self.popen.kill() + if self.consumer: + self.consumer.kill() + + +def kill_work(worklist, _verbosity): + """kill_work.""" + + for w in worklist: + w.kill() + for w in worklist: + w.wait() + + +def number_of_cp_us(): + """number_of_cp_us.""" + + return os.sysconf("SC_NPROCESSORS_ONLN") + + +def nano_secs_to_secs_str(x): + """nano_secs_to_secs_str.""" + + if x is None: + return "" + x = str(x) + if len(x) < 10: + x = "0" * (10 - len(x)) + x + return x[:len(x) - 9] + "." + x[-9:] + + +def insert_option_after(cmd, option, after): + """insert_option_after.""" + + try: + pos = cmd.index(after) + cmd.insert(pos + 1, option) + except (OSError, ValueError, RuntimeError): + cmd.append(option) + + +def create_work_list(cmd, pipe_to, output_dir, cpus, time_ranges_by_cpu): + """create_work_list.""" + + max_len = len(str(cpus[-1])) + cpu_dir_fmt = f"cpu-%.{max_len}u" + worklist = [] + pos = 0 + for cpu in cpus: + if cpu >= 0: + cpu_dir = os.path.join(output_dir, cpu_dir_fmt % cpu) + cpu_option = f"--cpu={cpu}" + else: + cpu_dir = output_dir + cpu_option = None + + tr_dir_fmt = "time-range" + + if len(time_ranges_by_cpu) > 1: + time_ranges = time_ranges_by_cpu[pos] + tr_dir_fmt += f"-{pos}" + pos += 1 + else: + time_ranges = time_ranges_by_cpu[0] + + max_len = len(str(len(time_ranges))) + tr_dir_fmt += f"-%.{max_len}u" + + i = 0 + for r in time_ranges: + if r == [None, None]: + time_option = None + work_output_dir = cpu_dir + else: + time_option = "--time=" + \ + nano_secs_to_secs_str(r[0]) + "," + \ + nano_secs_to_secs_str(r[1]) + work_output_dir = os.path.join(cpu_dir, tr_dir_fmt % i) + i += 1 + work_cmd = list(cmd) + if time_option is not None: + insert_option_after(work_cmd, time_option, "script") + if cpu_option is not None: + insert_option_after(work_cmd, cpu_option, "script") + w = Work(work_cmd, pipe_to, work_output_dir) + worklist.append(w) + return worklist + + +def do_run_work(worklist: list[Work], nr_jobs: int, + _verbosity: Verbosity) -> bool: + """do_run_work.""" + + nr_to_do = len(worklist) + not_started = list(worklist) + running: list[Work] = [] + done: list[Work] = [] + chg = False + while True: + nr_done = len(done) + if chg and _verbosity.normal: + nr_run = len(running) + print( + f"\rThere are {nr_to_do} jobs: {nr_done} completed, {nr_run} running", + flush=True, end=" ") + if _verbosity.verbose: + print() + chg = False + if nr_done == nr_to_do: + break + while len(running) < nr_jobs and len(not_started): + w = not_started.pop(0) + running.append(w) + if _verbosity.verbose: + print("Starting:", w.command()) + w.start() + chg = True + if len(running): + time.sleep(0.1) + finished: list[Work] = [] + not_finished: list[Work] = [] + while len(running): + w = running.pop(0) + r = w.poll() + if r is None: + not_finished.append(w) + continue + if r == 0: + if _verbosity.verbose: + print("Finished:", w.command()) + finished.append(w) + chg = True + continue + if _verbosity.normal and not _verbosity.verbose: + print() + print("Job failed!\n return code:", r, + "\n command: ", w.command()) + if w.pipe_to: + print(" piped to: ", w.pipe_to) + print("Killing outstanding jobs") + kill_work(not_finished, _verbosity) + kill_work(running, _verbosity) + return False + running = not_finished + done += finished + errorlist: list[str] = [] + for w in worklist: + errorlist += w.errors() + if len(errorlist): + print("errors:") + for e in errorlist: + print(e) + elif _verbosity.normal: + print("\r", " "*50, "\rAll jobs finished successfully", flush=True) + return True + + +def run_work(worklist: list[Work], nr_jobs: int = number_of_cp_us(), + _verbosity: Verbosity = Verbosity()) -> bool: + """run_work.""" + try: + return do_run_work(worklist, nr_jobs, _verbosity) + except BaseException: + for w in worklist: + w.kill() + raise + + +def read_header(perf, file_name): + """read_header.""" + + cmd = [perf, "script", "--header-only", "--input", file_name] + with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc: + out = proc.stdout.read() if proc.stdout else b"" + return out.decode("utf-8") + + +def parse_header(hdr): + """parse_header.""" + + result = {} + lines = hdr.split("\n") + for line in lines: + if ":" in line and line[0] == "#": + pos = line.index(":") + name = line[1:pos-1].strip() + value = line[pos+1:].strip() + if name in result: + orig_name = name + nr = 2 + while True: + name = f"{orig_name} {nr}" + if name not in result: + break + nr += 1 + result[name] = value + return result + + +def header_field(hdr_dict, hdr_fld): + """header_field.""" + + if hdr_fld not in hdr_dict: + raise RuntimeError(f"'{hdr_fld}' missing from header information") + return hdr_dict[hdr_fld] + +# Represent the position of an option within a command string +# and provide the option value and/or remove the option + + +class OptPos(): + + def init(self, opt_element=-1, value_element=-1, opt_pos=-1, value_pos=-1, error=None): + """init.""" + + self.opt_element = opt_element # list element that contains option + self.value_element = value_element # list element that contains option value + self.opt_pos = opt_pos # string position of option + self.value_pos = value_pos # string position of value + self.error = error # error message string + + def __init__(self, args, short_name, long_name, default=None): + """__init__.""" + + self.args = list(args) + self.default = default + n = 2 + len(long_name) + m = len(short_name) + pos = -1 + for opt in args: + pos += 1 + if m and opt[:2] == f"-{short_name}": + if len(opt) == 2: + if pos + 1 < len(args): + self.init(pos, pos + 1, 0, 0) + else: + self.init(error=f"-{short_name} option missing value") + else: + self.init(pos, pos, 0, 2) + return + if opt[:n] == f"--{long_name}": + if len(opt) == n: + if pos + 1 < len(args): + self.init(pos, pos + 1, 0, 0) + else: + self.init(error=f"--{long_name} option missing value") + elif opt[n] == "=": + self.init(pos, pos, 0, n + 1) + else: + self.init(error=f"--{long_name} option expected '='") + return + if m and opt[:1] == "-" and opt[:2] != "--" and short_name in opt: + ipos = opt.index(short_name) + if "-" in opt[1:]: + hpos = opt[1:].index("-") + if hpos < ipos: + continue + if ipos + 1 == len(opt): + if pos + 1 < len(args): + self.init(pos, pos + 1, ipos, 0) + else: + self.init(error=f"-{short_name} option missing value") + else: + self.init(pos, pos, ipos, ipos + 1) + return + self.init() + + def value(self): + """value.""" + + if self.opt_element >= 0: + if self.opt_element != self.value_element: + return self.args[self.value_element] + else: + return self.args[self.value_element][self.value_pos:] + return self.default + + def remove(self, args): + """remove.""" + + if self.opt_element == -1: + return + if self.opt_element != self.value_element: + del args[self.value_element] + if self.opt_pos: + args[self.opt_element] = args[self.opt_element][:self.opt_pos] + else: + del args[self.opt_element] + + +def determine_input_file_name(cmd): + """determine_input_file_name.""" + + p = OptPos(cmd, "i", "input", "perf.data") + if p.error: + raise RuntimeError(f"perf command {p.error}") + file_name = p.value() + if not os.path.exists(file_name): + raise RuntimeError(f"perf command input file '{file_name}' not found") + return file_name + + +def read_option(args, short_name, long_name, err_prefix, remove=False): + """read_option.""" + + p = OptPos(args, short_name, long_name) + if p.error: + raise RuntimeError(f"{err_prefix}{p.error}") + value = p.value() + if remove: + p.remove(args) + return value + + +def extract_option(args, short_name, long_name, err_prefix): + """extract_option.""" + + return read_option(args, short_name, long_name, err_prefix, True) + + +def read_perf_option(args, short_name, long_name): + """read_perf_option.""" + + return read_option(args, short_name, long_name, "perf command ") + + +def extract_perf_option(args, short_name, long_name): + """extract_perf_option.""" + + return extract_option(args, short_name, long_name, "perf command ") + + +def perf_double_quick_commands(cmd, file_name): + """perf_double_quick_commands.""" + + cpu_str = read_perf_option(cmd, "C", "cpu") + time_str = read_perf_option(cmd, "", "time") + # Use double-quick sampling to determine trace data density + times_cmd = ["perf", "script", "--ns", + "--input", file_name, "--itrace=qqi"] + if cpu_str is not None and cpu_str != "": + times_cmd.append(f"--cpu={cpu_str}") + if time_str is not None and time_str != "": + times_cmd.append(f"--time={time_str}") + cnts_cmd = list(times_cmd) + cnts_cmd.append("-Fcpu") + times_cmd.append("-Fcpu,time") + return cnts_cmd, times_cmd + + +class CPUTimeRange(): + def __init__(self, cpu): + """__init__.""" + + self.cpu = cpu + self.sample_cnt = 0 + self.time_ranges = None + self.interval = 0 + self.interval_remaining = 0 + self.remaining = 0 + self.tr_pos = 0 + + +def calc_time_ranges_by_cpu(line, cpu, cpu_time_ranges, max_time): + """calc_time_ranges_by_cpu.""" + + cpu_time_range = cpu_time_ranges[cpu] + cpu_time_range.remaining -= 1 + cpu_time_range.interval_remaining -= 1 + if cpu_time_range.remaining == 0: + cpu_time_range.time_ranges[cpu_time_range.tr_pos][1] = max_time + return + if cpu_time_range.interval_remaining == 0: + ts = time_val(line[1][:-1], 0) + time_ranges = cpu_time_range.time_ranges + time_ranges[cpu_time_range.tr_pos][1] = ts - 1 + time_ranges.append([ts, max_time]) + cpu_time_range.tr_pos += 1 + cpu_time_range.interval_remaining = cpu_time_range.interval + + +def count_samples_by_cpu(_line: list[str], cpu: int, + cpu_time_ranges: list[Any]) -> None: + """count_samples_by_cpu.""" + + try: + cpu_time_ranges[cpu].sample_cnt += 1 + except (OSError, ValueError, RuntimeError, IndexError): + print("exception") + print("cpu", cpu) + print("len(cpu_time_ranges)", len(cpu_time_ranges)) + raise + + +def process_command_output_lines(cmd, per_cpu, fn, *x): + """process_command_output_lines.""" + + # Assume CPU number is at beginning of line and enclosed by [] + pat = re.compile(r"\s*\[[0-9]+\]") + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) + while True: + line = p.stdout.readline() + if line: + line = line.decode("utf-8") + if pat.match(line): + line = line.split() + if per_cpu: + # Assumes CPU number is enclosed by [] + cpu = int(line[0][1:-1]) + else: + cpu = 0 + fn(line, cpu, *x) + else: + break + p.wait() + + +def intersect_time_ranges(new_time_ranges, time_ranges): + """intersect_time_ranges.""" + + pos = 0 + new_pos = 0 + # Can assume len(time_ranges) != 0 and len(new_time_ranges) != 0 + # Note also, there *must* be at least one intersection. + while pos < len(time_ranges) and new_pos < len(new_time_ranges): + # new end < old start => no intersection, remove new + if new_time_ranges[new_pos][1] < time_ranges[pos][0]: + del new_time_ranges[new_pos] + continue + # new start > old end => no intersection, check next + if new_time_ranges[new_pos][0] > time_ranges[pos][1]: + pos += 1 + if pos < len(time_ranges): + continue + # no next, so remove remaining + while new_pos < len(new_time_ranges): + del new_time_ranges[new_pos] + return + # Found an intersection + # new start < old start => adjust new start = old start + if new_time_ranges[new_pos][0] < time_ranges[pos][0]: + new_time_ranges[new_pos][0] = time_ranges[pos][0] + # new end > old end => keep the overlap, insert the remainder + if new_time_ranges[new_pos][1] > time_ranges[pos][1]: + r = [time_ranges[pos][1] + 1, new_time_ranges[new_pos][1]] + new_time_ranges[new_pos][1] = time_ranges[pos][1] + new_pos += 1 + new_time_ranges.insert(new_pos, r) + continue + # new [start, end] is within old [start, end] + new_pos += 1 + + +def split_time_ranges_by_trace_data_density( + time_ranges, cpus, nr, cmd, file_name, per_cpu, min_size, min_interval, _verbosity +): + """split_time_ranges_by_trace_data_density.""" + + if _verbosity.normal: + print("\rAnalyzing...", flush=True, end=" ") + if _verbosity.verbose: + print() + cnts_cmd, times_cmd = perf_double_quick_commands(cmd, file_name) + + nr_cpus = cpus[-1] + 1 if per_cpu else 1 + if per_cpu: + nr_cpus = cpus[-1] + 1 + cpu_time_ranges = [CPUTimeRange(cpu) for cpu in range(nr_cpus)] + else: + nr_cpus = 1 + cpu_time_ranges = [CPUTimeRange(-1)] + + if _verbosity.debug: + print("nr_cpus", nr_cpus) + print("cnts_cmd", cnts_cmd) + print("times_cmd", times_cmd) + + # Count the number of "double quick" samples per CPU + process_command_output_lines( + cnts_cmd, per_cpu, count_samples_by_cpu, cpu_time_ranges) + + tot = 0 + mx = 0 + for cpu_time_range in cpu_time_ranges: + cnt = cpu_time_range.sample_cnt + tot += cnt + if cnt > mx: + mx = cnt + if _verbosity.debug: + print("cpu:", cpu_time_range.cpu, "sample_cnt", cnt) + + if min_size < 1: + min_size = 1 + + if mx < min_size: + # Too little data to be worth splitting + if _verbosity.debug: + print("Too little data to split by time") + if nr == 0: + nr = 1 + return [split_time_ranges_into_n(time_ranges, nr, min_interval)] + + if nr: + divisor = nr + min_size = 1 + else: + divisor = number_of_cp_us() + + interval = int(round(tot / divisor, 0)) + if interval < min_size: + interval = min_size + + if _verbosity.debug: + print("divisor", divisor) + print("min_size", min_size) + print("interval", interval) + + min_time = time_ranges[0][0] + max_time = time_ranges[-1][1] + + for cpu_time_range in cpu_time_ranges: + cnt = cpu_time_range.sample_cnt + if cnt == 0: + cpu_time_range.time_ranges = copy.deepcopy(time_ranges) + continue + # Adjust target interval for CPU to give approximately equal interval sizes + # Determine number of intervals, rounding to nearest integer + n = int(round(cnt / interval, 0)) + if n < 1: + n = 1 + # Determine interval size, rounding up + d, m = divmod(cnt, n) + if m: + d += 1 + cpu_time_range.interval = d + cpu_time_range.interval_remaining = d + cpu_time_range.remaining = cnt + # init. time ranges for each CPU with the start time + cpu_time_range.time_ranges = [[min_time, max_time]] + + # Set time ranges so that the same number of "double quick" samples + # will fall into each time range. + process_command_output_lines( + times_cmd, per_cpu, calc_time_ranges_by_cpu, cpu_time_ranges, max_time) + + for cpu_time_range in cpu_time_ranges: + if cpu_time_range.sample_cnt: + intersect_time_ranges(cpu_time_range.time_ranges, time_ranges) + + return [cpu_time_ranges[cpu].time_ranges for cpu in cpus] + + +def split_single_time_range_into_n(time_range, n): + """split_single_time_range_into_n.""" + + if n <= 1: + return [time_range] + start = time_range[0] + end = time_range[1] + duration = int((end - start + 1) / n) + if duration < 1: + return [time_range] + time_ranges = [] + for _i in range(n): + time_ranges.append([start, start + duration - 1]) + start += duration + time_ranges[-1][1] = end + return time_ranges + + +def time_range_duration(r): + """time_range_duration.""" + + return r[1] - r[0] + 1 + + +def total_duration(time_ranges): + """total_duration.""" + + duration = 0 + for r in time_ranges: + duration += time_range_duration(r) + return duration + + +def split_time_ranges_by_interval(time_ranges, interval): + """split_time_ranges_by_interval.""" + + new_ranges = [] + for r in time_ranges: + duration = time_range_duration(r) + n = duration / interval + n = int(round(n, 0)) + new_ranges += split_single_time_range_into_n(r, n) + return new_ranges + + +def split_time_ranges_into_n(time_ranges, n, min_interval): + """split_time_ranges_into_n.""" + + if n <= len(time_ranges): + return time_ranges + duration = total_duration(time_ranges) + interval = duration / n + if interval < min_interval: + interval = min_interval + return split_time_ranges_by_interval(time_ranges, interval) + + +def recombine_time_ranges(tr): + """recombine_time_ranges.""" + + new_tr = copy.deepcopy(tr) + i = 1 + while i < len(new_tr): + # if prev end + 1 == cur start, combine them + if new_tr[i - 1][1] + 1 == new_tr[i][0]: + new_tr[i][0] = new_tr[i - 1][0] + del new_tr[i - 1] + else: + i += 1 + return new_tr + + +def open_time_range_ends(time_ranges, min_time, max_time): + """open_time_range_ends.""" + + if time_ranges[0][0] <= min_time: + time_ranges[0][0] = None + if time_ranges[-1][1] >= max_time: + time_ranges[-1][1] = None + + +def bad_time_str(time_str): + """bad_time_str.""" + + raise RuntimeError( + f"perf command bad time option: '{time_str}'\n" + "Check also 'time of first sample' and 'time of last sample' " + "in perf script --header-only" + ) + + +def validate_time_ranges(time_ranges, time_str): + """validate_time_ranges.""" + + n = len(time_ranges) + for i in range(n): + start = time_ranges[i][0] + end = time_ranges[i][1] + if i != 0 and start <= time_ranges[i - 1][1]: + bad_time_str(time_str) + if start > end: + bad_time_str(time_str) + + +def time_val(s, dflt): + """time_val.""" + + s = s.strip() + if s == "": + return dflt + a = s.split(".") + if len(a) > 2: + raise RuntimeError(f"Bad time value'{s}'") + x = int(a[0]) + if x < 0: + raise RuntimeError("Negative time not allowed") + x *= 1000000000 + if len(a) > 1: + x += int((a[1] + "000000000")[:9]) + return x + + +def bad_cpu_str(cpu_str): + """bad_cpu_str.""" + + raise RuntimeError( + f"perf command bad cpu option: '{cpu_str}'\n" + "Check also 'nrcpus avail' in perf script --header-only" + ) + + +def parse_time_str(time_str, min_time, max_time): + """parse_time_str.""" + + if time_str is None or time_str == "": + return [[min_time, max_time]] + time_ranges = [] + for r in time_str.split(): + a = r.split(",") + if len(a) != 2: + bad_time_str(time_str) + try: + start = time_val(a[0], min_time) + end = time_val(a[1], max_time) + except (OSError, ValueError, RuntimeError): + bad_time_str(time_str) + time_ranges.append([start, end]) + validate_time_ranges(time_ranges, time_str) + return time_ranges + + +def parse_cpu_str(cpu_str, nr_cpus): + """parse_cpu_str.""" + + if cpu_str is None or cpu_str == "": + return [-1] + cpus = [] + for r in cpu_str.split(","): + a = r.split("-") + if len(a) < 1 or len(a) > 2: + bad_cpu_str(cpu_str) + try: + start = int(a[0].strip()) + if len(a) > 1: + end = int(a[1].strip()) + else: + end = start + except (OSError, ValueError, RuntimeError): + bad_cpu_str(cpu_str) + if start < 0 or end < 0 or end < start or end >= nr_cpus: + bad_cpu_str(cpu_str) + cpus.extend(range(start, end + 1)) + cpus = list(set(cpus)) # remove duplicates + cpus.sort() + return cpus + + +class ParallelPerf(): + + def __init__(self, a): + """init.""" + self.nr = 0 + self.jobs = 0 + self.file_name = None + self.hdr = None + self.hdr_dict = None + self.cmd_line = None + self.min_time = None + self.max_time = None + self.time_str = None + self.time_ranges = None + self.cpu_str = None + self.cpus = None + self.split_time_ranges_for_each_cpu = None + self.worklist = None + self.per_cpu = None + self.interval = None + self.min_size = None + self.min_interval = None + self.pipe_to = None + self.output_dir = None + self.cmd = getattr(a, 'cmd', None) + self.no_per_cpu = getattr(a, 'no_per_cpu', False) + self.dry_run = getattr(a, 'dry_run', False) + self._verbosity = getattr(a, "_verbosity", Verbosity()) + + for arg_name in vars(a): + setattr(self, arg_name, getattr(a, arg_name)) + self.orig_nr = self.nr + self.orig_cmd = list(self.cmd) + self.perf = self.cmd[0] + if os.path.exists(self.output_dir): + raise RuntimeError(f"Output '{self.output_dir}' already exists") + if self.jobs < 0 or self.nr < 0 or self.interval < 0: + raise RuntimeError( + "Bad options (negative values): try -h option for help") + if self.nr != 0 and self.interval != 0: + raise RuntimeError( + "Cannot specify number of time subdivisions and time interval") + if self.jobs == 0: + self.jobs = number_of_cp_us() + if self.nr == 0 and self.interval == 0: + if self.per_cpu: + self.nr = 1 + else: + self.nr = self.jobs + + def init(self): + """init.""" + + if self._verbosity.debug: + print("cmd", self.cmd) + self.file_name = determine_input_file_name(self.cmd) + self.hdr = read_header(self.perf, self.file_name) + self.hdr_dict = parse_header(self.hdr) + self.cmd_line = header_field(self.hdr_dict, "cmdline") + + def extract_time_info(self): + """extract_time_info.""" + + self.min_time = time_val(header_field( + self.hdr_dict, "time of first sample"), 0) + self.max_time = time_val(header_field( + self.hdr_dict, "time of last sample"), 0) + self.time_str = extract_perf_option(self.cmd, "", "time") + self.time_ranges = parse_time_str( + self.time_str, self.min_time, self.max_time) + if self._verbosity.debug: + print("time_ranges", self.time_ranges) + + def extract_cpu_info(self): + """extract_cpu_info.""" + + if self.per_cpu: + nr_cpus = int(header_field(self.hdr_dict, "nrcpus avail")) + self.cpu_str = extract_perf_option(self.cmd, "C", "cpu") + if self.cpu_str is None or self.cpu_str == "": + self.cpus = [x for x in range(nr_cpus)] + else: + self.cpus = parse_cpu_str(self.cpu_str, nr_cpus) + else: + self.cpu_str = None + self.cpus = [-1] + if self._verbosity.debug: + print("cpus", self.cpus) + + def is_intel_pt(self): + """is_intel_pt.""" + + return self.cmd_line.find("intel_pt") >= 0 + + def split_time_ranges(self): + """split_time_ranges.""" + + if self.is_intel_pt() and self.interval == 0: + self.split_time_ranges_for_each_cpu = ( + split_time_ranges_by_trace_data_density( + self.time_ranges, self.cpus, self.orig_nr, + self.orig_cmd, self.file_name, self.per_cpu, + self.min_size, self.min_interval, self._verbosity + ) + ) + elif self.nr: + self.split_time_ranges_for_each_cpu = [split_time_ranges_into_n( + self.time_ranges, self.nr, self.min_interval)] + else: + self.split_time_ranges_for_each_cpu = [ + split_time_ranges_by_interval(self.time_ranges, self.interval)] + + def check_time_ranges(self): + """check_time_ranges.""" + + for tr in self.split_time_ranges_for_each_cpu: + # Re-combined time ranges should be the same + new_tr = recombine_time_ranges(tr) + if new_tr != self.time_ranges: + if self._verbosity.debug: + print("tr", tr) + print("new_tr", new_tr) + raise RuntimeError("Self test failed!") + + def open_time_range_ends(self): + """open_time_range_ends.""" + + for time_ranges in self.split_time_ranges_for_each_cpu: + open_time_range_ends(time_ranges, self.min_time, self.max_time) + + def create_work_list(self): + """create_work_list.""" + + self.worklist = create_work_list( + self.cmd, self.pipe_to, self.output_dir, self.cpus, self.split_time_ranges_for_each_cpu) + + def perf_data_recorded_per_cpu(self): + """perf_data_recorded_per_cpu.""" + + if "--per-thread" in self.cmd_line.split(): + return False + return True + + def default_to_per_cpu(self): + """default_to_per_cpu.""" + + # --no-per-cpu option takes precedence + if self.no_per_cpu: + return False + if not self.perf_data_recorded_per_cpu(): + return False + # Default to per-cpu for Intel PT data that was recorded per-cpu, + # because decoding can be done for each CPU separately. + if self.is_intel_pt(): + return True + return False + + def config(self): + """config.""" + + self.init() + self.extract_time_info() + if not self.per_cpu: + self.per_cpu = self.default_to_per_cpu() + if self._verbosity.debug: + print("per_cpu", self.per_cpu) + self.extract_cpu_info() + self.split_time_ranges() + if self._verbosity.self_test: + self.check_time_ranges() + # Prefer open-ended time range to starting / ending with min_time / max_time resp. + self.open_time_range_ends() + self.create_work_list() + + def run(self): + """run.""" + + if self.dry_run: + print(len(self.worklist), "jobs:") + for w in self.worklist: + print(w.command()) + return True + result = run_work(self.worklist, self.jobs, _verbosity=self._verbosity) + if self._verbosity.verbose: + print(glb_prog_name, "done") + return result + + +def run_parallel_perf(a): + """run_parallel_perf.""" + + pp = ParallelPerf(a) + pp.config() + return pp.run() + + +def main(args): + """main.""" + + ap = argparse.ArgumentParser( + prog=glb_prog_name, formatter_class=argparse.RawDescriptionHelpFormatter, + description=""" +run a perf script command multiple times in parallel, using perf script options +--cpu and --time so that each job processes a different chunk of the data. +""", + epilog=""" +Follow the options by '--' and then the perf script command e.g. + + $ perf record -a -- sleep 10 + $ parallel-perf.py --nr=4 -- perf script --ns + All jobs finished successfully + $ tree parallel-perf-output/ + parallel-perf-output/ + ├── time-range-0 + │   ├── cmd.txt + │   └── out.txt + ├── time-range-1 + │   ├── cmd.txt + │   └── out.txt + ├── time-range-2 + │   ├── cmd.txt + │   └── out.txt + └── time-range-3 + ├── cmd.txt + └── out.txt + $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H . + parallel-perf-output/time-range-0/cmd.txt:perf script --time=,9466.504461499 --ns + parallel-perf-output/time-range-1/cmd.txt:perf script --time=9466.504461500,9469.005396999 --ns + parallel-perf-output/time-range-2/cmd.txt:perf script --time=9469.005397000,9471.506332499 --ns + parallel-perf-output/time-range-3/cmd.txt:perf script --time=9471.506332500, --ns + +Any perf script command can be used, including the use of perf script options +--dlfilter and --script, so that the benefit of running parallel jobs +naturally extends to them also. + +If option --pipe-to is used, standard output is first piped through that +command. Beware, if the command fails (e.g. grep with no matches), it will be +considered a fatal error. + +Final standard output is redirected to files named out.txt in separate +subdirectories under the output directory. Similarly, standard error is +written to files named err.txt. In addition, files named cmd.txt contain the +corresponding perf script command. After processing, err.txt files are removed +if they are empty. + +If any job exits with a non-zero exit code, then all jobs are killed and no +more are started. A message is printed if any job results in a non-empty +err.txt file. + +There is a separate output subdirectory for each time range. If the --per-cpu +option is used, these are further grouped under cpu-n subdirectories, e.g. + + $ parallel-perf.py --per-cpu --nr=2 -- perf script --ns --cpu=0,1 + All jobs finished successfully + $ tree parallel-perf-output + parallel-perf-output/ + ├── cpu-0 + │   ├── time-range-0 + │   │   ├── cmd.txt + │   │   └── out.txt + │   └── time-range-1 + │   ├── cmd.txt + │   └── out.txt + └── cpu-1 + ├── time-range-0 + │   ├── cmd.txt + │   └── out.txt + └── time-range-1 + ├── cmd.txt + └── out.txt + $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H . + parallel-perf-output/cpu-0/time-range-0/cmd.txt:perf script --cpu=0 --time=,9469.005396999 --ns + parallel-perf-output/cpu-0/time-range-1/cmd.txt:perf script --cpu=0 --time=9469.005397000, --ns + parallel-perf-output/cpu-1/time-range-0/cmd.txt:perf script --cpu=1 --time=,9469.005396999 --ns + parallel-perf-output/cpu-1/time-range-1/cmd.txt:perf script --cpu=1 --time=9469.005397000, --ns + +Subdivisions of time range, and cpus if the --per-cpu option is used, are +expressed by the --time and --cpu perf script options respectively. If the +supplied perf script command has a --time option, then that time range is +subdivided, otherwise the time range given by 'time of first sample' to +'time of last sample' is used (refer perf script --header-only). Similarly, the +supplied perf script command may provide a --cpu option, and only those CPUs +will be processed. + +To prevent time intervals becoming too small, the --min-interval option can +be used. + +Note there is special handling for processing Intel PT traces. If an interval is +not specified and the perf record command contained the intel_pt event, then the +time range will be subdivided in order to produce subdivisions that contain +approximately the same amount of trace data. That is accomplished by counting +double-quick (--itrace=qqi) samples, and choosing time ranges that encompass +approximately the same number of samples. In that case, time ranges may not be +the same for each CPU processed. For Intel PT, --per-cpu is the default, but +that can be overridden by --no-per-cpu. Note, for Intel PT, double-quick +decoding produces 1 sample for each PSB synchronization packet, which in turn +come after a certain number of bytes output, determined by psb_period (refer +perf Intel PT documentation). The minimum number of double-quick samples that +will define a time range can be set by the --min_size option, which defaults to +64. +""") + ap.add_argument("-o", "--output-dir", default="parallel-perf-output", + help="output directory (default 'parallel-perf-output')") + ap.add_argument("-j", "--jobs", type=int, default=0, + help="maximum number of jobs to run in parallel at one time " + "(default is the number of CPUs)") + ap.add_argument("-n", "--nr", type=int, default=0, + help="number of time subdivisions (default is the number of jobs)") + ap.add_argument("-i", "--interval", type=float, default=0, + help="subdivide the time range using this time interval " + "(in seconds e.g. 0.1 for a tenth of a second)") + ap.add_argument("-c", "--per-cpu", action="store_true", + help="process data for each CPU in parallel") + ap.add_argument("-m", "--min-interval", type=float, default=glb_min_interval, + help=f"minimum interval (default {glb_min_interval} seconds)") + ap.add_argument("-p", "--pipe-to", + help="command to pipe output to (optional)") + ap.add_argument("-N", "--no-per-cpu", action="store_true", + help="do not process data for each CPU in parallel") + ap.add_argument("-b", "--min_size", type=int, default=glb_min_samples, + help="minimum data size (for Intel PT in PSBs)") + ap.add_argument("-D", "--dry-run", action="store_true", + help="do not run any jobs, just show the perf script commands") + ap.add_argument("-q", "--quiet", action="store_true", + help="do not print any messages except errors") + ap.add_argument("-v", "--verbose", action="store_true", + help="print more messages") + ap.add_argument("-d", "--debug", action="store_true", + help="print debugging messages") + cmd_line = list(args) + try: + split_pos = cmd_line.index("--") + cmd = cmd_line[split_pos + 1:] + args = cmd_line[:split_pos] + except (OSError, ValueError, RuntimeError): + cmd = None + args = cmd_line + a = ap.parse_args(args=args[1:]) + a.cmd = cmd + setattr(a, "_verbosity", Verbosity(a.quiet, a.verbose, a.debug)) + try: + if not a.cmd: + if a.cmd is None and len(args) <= 1: + ap.print_help() + return True + raise RuntimeError( + "command line must contain '--' before perf command") + return run_parallel_perf(a) + except (OSError, ValueError, RuntimeError, KeyboardInterrupt) as e: + print("Fatal error: ", str(e)) + if a.debug: + raise + return False + + +if __name__ == "__main__": + if not main(sys.argv): + sys.exit(1) diff --git a/tools/perf/scripts/python/parallel-perf.py b/tools/perf/scripts/python/parallel-perf.py deleted file mode 100755 index be85fd7f6632..000000000000 --- a/tools/perf/scripts/python/parallel-perf.py +++ /dev/null @@ -1,989 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0 -# -# Run a perf script command multiple times in parallel, using perf script -# options --cpu and --time so that each job processes a different chunk -# of the data. -# -# Copyright (c) 2024, Intel Corporation. - -import subprocess -import argparse -import pathlib -import shlex -import time -import copy -import sys -import os -import re - -glb_prog_name = "parallel-perf.py" -glb_min_interval = 10.0 -glb_min_samples = 64 - -class Verbosity(): - - def __init__(self, quiet=False, verbose=False, debug=False): - self.normal = True - self.verbose = verbose - self.debug = debug - self.self_test = True - if self.debug: - self.verbose = True - if self.verbose: - quiet = False - if quiet: - self.normal = False - -# Manage work (Start/Wait/Kill), as represented by a subprocess.Popen command -class Work(): - - def __init__(self, cmd, pipe_to, output_dir="."): - self.popen = None - self.consumer = None - self.cmd = cmd - self.pipe_to = pipe_to - self.output_dir = output_dir - self.cmdout_name = f"{output_dir}/cmd.txt" - self.stdout_name = f"{output_dir}/out.txt" - self.stderr_name = f"{output_dir}/err.txt" - - def Command(self): - sh_cmd = [ shlex.quote(x) for x in self.cmd ] - return " ".join(self.cmd) - - def Stdout(self): - return open(self.stdout_name, "w") - - def Stderr(self): - return open(self.stderr_name, "w") - - def CreateOutputDir(self): - pathlib.Path(self.output_dir).mkdir(parents=True, exist_ok=True) - - def Start(self): - if self.popen: - return - self.CreateOutputDir() - with open(self.cmdout_name, "w") as f: - f.write(self.Command()) - f.write("\n") - stdout = self.Stdout() - stderr = self.Stderr() - if self.pipe_to: - self.popen = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=stderr) - args = shlex.split(self.pipe_to) - self.consumer = subprocess.Popen(args, stdin=self.popen.stdout, stdout=stdout, stderr=stderr) - else: - self.popen = subprocess.Popen(self.cmd, stdout=stdout, stderr=stderr) - - def RemoveEmptyErrFile(self): - if os.path.exists(self.stderr_name): - if os.path.getsize(self.stderr_name) == 0: - os.unlink(self.stderr_name) - - def Errors(self): - if os.path.exists(self.stderr_name): - if os.path.getsize(self.stderr_name) != 0: - return [ f"Non-empty error file {self.stderr_name}" ] - return [] - - def TidyUp(self): - self.RemoveEmptyErrFile() - - def RawPollWait(self, p, wait): - if wait: - return p.wait() - return p.poll() - - def Poll(self, wait=False): - if not self.popen: - return None - result = self.RawPollWait(self.popen, wait) - if self.consumer: - res = result - result = self.RawPollWait(self.consumer, wait) - if result != None and res == None: - self.popen.kill() - result = None - elif result == 0 and res != None and res != 0: - result = res - if result != None: - self.TidyUp() - return result - - def Wait(self): - return self.Poll(wait=True) - - def Kill(self): - if not self.popen: - return - self.popen.kill() - if self.consumer: - self.consumer.kill() - -def KillWork(worklist, verbosity): - for w in worklist: - w.Kill() - for w in worklist: - w.Wait() - -def NumberOfCPUs(): - return os.sysconf("SC_NPROCESSORS_ONLN") - -def NanoSecsToSecsStr(x): - if x == None: - return "" - x = str(x) - if len(x) < 10: - x = "0" * (10 - len(x)) + x - return x[:len(x) - 9] + "." + x[-9:] - -def InsertOptionAfter(cmd, option, after): - try: - pos = cmd.index(after) - cmd.insert(pos + 1, option) - except: - cmd.append(option) - -def CreateWorkList(cmd, pipe_to, output_dir, cpus, time_ranges_by_cpu): - max_len = len(str(cpus[-1])) - cpu_dir_fmt = f"cpu-%.{max_len}u" - worklist = [] - pos = 0 - for cpu in cpus: - if cpu >= 0: - cpu_dir = os.path.join(output_dir, cpu_dir_fmt % cpu) - cpu_option = f"--cpu={cpu}" - else: - cpu_dir = output_dir - cpu_option = None - - tr_dir_fmt = "time-range" - - if len(time_ranges_by_cpu) > 1: - time_ranges = time_ranges_by_cpu[pos] - tr_dir_fmt += f"-{pos}" - pos += 1 - else: - time_ranges = time_ranges_by_cpu[0] - - max_len = len(str(len(time_ranges))) - tr_dir_fmt += f"-%.{max_len}u" - - i = 0 - for r in time_ranges: - if r == [None, None]: - time_option = None - work_output_dir = cpu_dir - else: - time_option = "--time=" + NanoSecsToSecsStr(r[0]) + "," + NanoSecsToSecsStr(r[1]) - work_output_dir = os.path.join(cpu_dir, tr_dir_fmt % i) - i += 1 - work_cmd = list(cmd) - if time_option != None: - InsertOptionAfter(work_cmd, time_option, "script") - if cpu_option != None: - InsertOptionAfter(work_cmd, cpu_option, "script") - w = Work(work_cmd, pipe_to, work_output_dir) - worklist.append(w) - return worklist - -def DoRunWork(worklist, nr_jobs, verbosity): - nr_to_do = len(worklist) - not_started = list(worklist) - running = [] - done = [] - chg = False - while True: - nr_done = len(done) - if chg and verbosity.normal: - nr_run = len(running) - print(f"\rThere are {nr_to_do} jobs: {nr_done} completed, {nr_run} running", flush=True, end=" ") - if verbosity.verbose: - print() - chg = False - if nr_done == nr_to_do: - break - while len(running) < nr_jobs and len(not_started): - w = not_started.pop(0) - running.append(w) - if verbosity.verbose: - print("Starting:", w.Command()) - w.Start() - chg = True - if len(running): - time.sleep(0.1) - finished = [] - not_finished = [] - while len(running): - w = running.pop(0) - r = w.Poll() - if r == None: - not_finished.append(w) - continue - if r == 0: - if verbosity.verbose: - print("Finished:", w.Command()) - finished.append(w) - chg = True - continue - if verbosity.normal and not verbosity.verbose: - print() - print("Job failed!\n return code:", r, "\n command: ", w.Command()) - if w.pipe_to: - print(" piped to: ", w.pipe_to) - print("Killing outstanding jobs") - KillWork(not_finished, verbosity) - KillWork(running, verbosity) - return False - running = not_finished - done += finished - errorlist = [] - for w in worklist: - errorlist += w.Errors() - if len(errorlist): - print("Errors:") - for e in errorlist: - print(e) - elif verbosity.normal: - print("\r"," "*50, "\rAll jobs finished successfully", flush=True) - return True - -def RunWork(worklist, nr_jobs=NumberOfCPUs(), verbosity=Verbosity()): - try: - return DoRunWork(worklist, nr_jobs, verbosity) - except: - for w in worklist: - w.Kill() - raise - return True - -def ReadHeader(perf, file_name): - return subprocess.Popen([perf, "script", "--header-only", "--input", file_name], stdout=subprocess.PIPE).stdout.read().decode("utf-8") - -def ParseHeader(hdr): - result = {} - lines = hdr.split("\n") - for line in lines: - if ":" in line and line[0] == "#": - pos = line.index(":") - name = line[1:pos-1].strip() - value = line[pos+1:].strip() - if name in result: - orig_name = name - nr = 2 - while True: - name = f"{orig_name} {nr}" - if name not in result: - break - nr += 1 - result[name] = value - return result - -def HeaderField(hdr_dict, hdr_fld): - if hdr_fld not in hdr_dict: - raise Exception(f"'{hdr_fld}' missing from header information") - return hdr_dict[hdr_fld] - -# Represent the position of an option within a command string -# and provide the option value and/or remove the option -class OptPos(): - - def Init(self, opt_element=-1, value_element=-1, opt_pos=-1, value_pos=-1, error=None): - self.opt_element = opt_element # list element that contains option - self.value_element = value_element # list element that contains option value - self.opt_pos = opt_pos # string position of option - self.value_pos = value_pos # string position of value - self.error = error # error message string - - def __init__(self, args, short_name, long_name, default=None): - self.args = list(args) - self.default = default - n = 2 + len(long_name) - m = len(short_name) - pos = -1 - for opt in args: - pos += 1 - if m and opt[:2] == f"-{short_name}": - if len(opt) == 2: - if pos + 1 < len(args): - self.Init(pos, pos + 1, 0, 0) - else: - self.Init(error = f"-{short_name} option missing value") - else: - self.Init(pos, pos, 0, 2) - return - if opt[:n] == f"--{long_name}": - if len(opt) == n: - if pos + 1 < len(args): - self.Init(pos, pos + 1, 0, 0) - else: - self.Init(error = f"--{long_name} option missing value") - elif opt[n] == "=": - self.Init(pos, pos, 0, n + 1) - else: - self.Init(error = f"--{long_name} option expected '='") - return - if m and opt[:1] == "-" and opt[:2] != "--" and short_name in opt: - ipos = opt.index(short_name) - if "-" in opt[1:]: - hpos = opt[1:].index("-") - if hpos < ipos: - continue - if ipos + 1 == len(opt): - if pos + 1 < len(args): - self.Init(pos, pos + 1, ipos, 0) - else: - self.Init(error = f"-{short_name} option missing value") - else: - self.Init(pos, pos, ipos, ipos + 1) - return - self.Init() - - def Value(self): - if self.opt_element >= 0: - if self.opt_element != self.value_element: - return self.args[self.value_element] - else: - return self.args[self.value_element][self.value_pos:] - return self.default - - def Remove(self, args): - if self.opt_element == -1: - return - if self.opt_element != self.value_element: - del args[self.value_element] - if self.opt_pos: - args[self.opt_element] = args[self.opt_element][:self.opt_pos] - else: - del args[self.opt_element] - -def DetermineInputFileName(cmd): - p = OptPos(cmd, "i", "input", "perf.data") - if p.error: - raise Exception(f"perf command {p.error}") - file_name = p.Value() - if not os.path.exists(file_name): - raise Exception(f"perf command input file '{file_name}' not found") - return file_name - -def ReadOption(args, short_name, long_name, err_prefix, remove=False): - p = OptPos(args, short_name, long_name) - if p.error: - raise Exception(f"{err_prefix}{p.error}") - value = p.Value() - if remove: - p.Remove(args) - return value - -def ExtractOption(args, short_name, long_name, err_prefix): - return ReadOption(args, short_name, long_name, err_prefix, True) - -def ReadPerfOption(args, short_name, long_name): - return ReadOption(args, short_name, long_name, "perf command ") - -def ExtractPerfOption(args, short_name, long_name): - return ExtractOption(args, short_name, long_name, "perf command ") - -def PerfDoubleQuickCommands(cmd, file_name): - cpu_str = ReadPerfOption(cmd, "C", "cpu") - time_str = ReadPerfOption(cmd, "", "time") - # Use double-quick sampling to determine trace data density - times_cmd = ["perf", "script", "--ns", "--input", file_name, "--itrace=qqi"] - if cpu_str != None and cpu_str != "": - times_cmd.append(f"--cpu={cpu_str}") - if time_str != None and time_str != "": - times_cmd.append(f"--time={time_str}") - cnts_cmd = list(times_cmd) - cnts_cmd.append("-Fcpu") - times_cmd.append("-Fcpu,time") - return cnts_cmd, times_cmd - -class CPUTimeRange(): - def __init__(self, cpu): - self.cpu = cpu - self.sample_cnt = 0 - self.time_ranges = None - self.interval = 0 - self.interval_remaining = 0 - self.remaining = 0 - self.tr_pos = 0 - -def CalcTimeRangesByCPU(line, cpu, cpu_time_ranges, max_time): - cpu_time_range = cpu_time_ranges[cpu] - cpu_time_range.remaining -= 1 - cpu_time_range.interval_remaining -= 1 - if cpu_time_range.remaining == 0: - cpu_time_range.time_ranges[cpu_time_range.tr_pos][1] = max_time - return - if cpu_time_range.interval_remaining == 0: - time = TimeVal(line[1][:-1], 0) - time_ranges = cpu_time_range.time_ranges - time_ranges[cpu_time_range.tr_pos][1] = time - 1 - time_ranges.append([time, max_time]) - cpu_time_range.tr_pos += 1 - cpu_time_range.interval_remaining = cpu_time_range.interval - -def CountSamplesByCPU(line, cpu, cpu_time_ranges): - try: - cpu_time_ranges[cpu].sample_cnt += 1 - except: - print("exception") - print("cpu", cpu) - print("len(cpu_time_ranges)", len(cpu_time_ranges)) - raise - -def ProcessCommandOutputLines(cmd, per_cpu, fn, *x): - # Assume CPU number is at beginning of line and enclosed by [] - pat = re.compile(r"\s*\[[0-9]+\]") - p = subprocess.Popen(cmd, stdout=subprocess.PIPE) - while True: - line = p.stdout.readline() - if line: - line = line.decode("utf-8") - if pat.match(line): - line = line.split() - if per_cpu: - # Assumes CPU number is enclosed by [] - cpu = int(line[0][1:-1]) - else: - cpu = 0 - fn(line, cpu, *x) - else: - break - p.wait() - -def IntersectTimeRanges(new_time_ranges, time_ranges): - pos = 0 - new_pos = 0 - # Can assume len(time_ranges) != 0 and len(new_time_ranges) != 0 - # Note also, there *must* be at least one intersection. - while pos < len(time_ranges) and new_pos < len(new_time_ranges): - # new end < old start => no intersection, remove new - if new_time_ranges[new_pos][1] < time_ranges[pos][0]: - del new_time_ranges[new_pos] - continue - # new start > old end => no intersection, check next - if new_time_ranges[new_pos][0] > time_ranges[pos][1]: - pos += 1 - if pos < len(time_ranges): - continue - # no next, so remove remaining - while new_pos < len(new_time_ranges): - del new_time_ranges[new_pos] - return - # Found an intersection - # new start < old start => adjust new start = old start - if new_time_ranges[new_pos][0] < time_ranges[pos][0]: - new_time_ranges[new_pos][0] = time_ranges[pos][0] - # new end > old end => keep the overlap, insert the remainder - if new_time_ranges[new_pos][1] > time_ranges[pos][1]: - r = [ time_ranges[pos][1] + 1, new_time_ranges[new_pos][1] ] - new_time_ranges[new_pos][1] = time_ranges[pos][1] - new_pos += 1 - new_time_ranges.insert(new_pos, r) - continue - # new [start, end] is within old [start, end] - new_pos += 1 - -def SplitTimeRangesByTraceDataDensity(time_ranges, cpus, nr, cmd, file_name, per_cpu, min_size, min_interval, verbosity): - if verbosity.normal: - print("\rAnalyzing...", flush=True, end=" ") - if verbosity.verbose: - print() - cnts_cmd, times_cmd = PerfDoubleQuickCommands(cmd, file_name) - - nr_cpus = cpus[-1] + 1 if per_cpu else 1 - if per_cpu: - nr_cpus = cpus[-1] + 1 - cpu_time_ranges = [ CPUTimeRange(cpu) for cpu in range(nr_cpus) ] - else: - nr_cpus = 1 - cpu_time_ranges = [ CPUTimeRange(-1) ] - - if verbosity.debug: - print("nr_cpus", nr_cpus) - print("cnts_cmd", cnts_cmd) - print("times_cmd", times_cmd) - - # Count the number of "double quick" samples per CPU - ProcessCommandOutputLines(cnts_cmd, per_cpu, CountSamplesByCPU, cpu_time_ranges) - - tot = 0 - mx = 0 - for cpu_time_range in cpu_time_ranges: - cnt = cpu_time_range.sample_cnt - tot += cnt - if cnt > mx: - mx = cnt - if verbosity.debug: - print("cpu:", cpu_time_range.cpu, "sample_cnt", cnt) - - if min_size < 1: - min_size = 1 - - if mx < min_size: - # Too little data to be worth splitting - if verbosity.debug: - print("Too little data to split by time") - if nr == 0: - nr = 1 - return [ SplitTimeRangesIntoN(time_ranges, nr, min_interval) ] - - if nr: - divisor = nr - min_size = 1 - else: - divisor = NumberOfCPUs() - - interval = int(round(tot / divisor, 0)) - if interval < min_size: - interval = min_size - - if verbosity.debug: - print("divisor", divisor) - print("min_size", min_size) - print("interval", interval) - - min_time = time_ranges[0][0] - max_time = time_ranges[-1][1] - - for cpu_time_range in cpu_time_ranges: - cnt = cpu_time_range.sample_cnt - if cnt == 0: - cpu_time_range.time_ranges = copy.deepcopy(time_ranges) - continue - # Adjust target interval for CPU to give approximately equal interval sizes - # Determine number of intervals, rounding to nearest integer - n = int(round(cnt / interval, 0)) - if n < 1: - n = 1 - # Determine interval size, rounding up - d, m = divmod(cnt, n) - if m: - d += 1 - cpu_time_range.interval = d - cpu_time_range.interval_remaining = d - cpu_time_range.remaining = cnt - # Init. time ranges for each CPU with the start time - cpu_time_range.time_ranges = [ [min_time, max_time] ] - - # Set time ranges so that the same number of "double quick" samples - # will fall into each time range. - ProcessCommandOutputLines(times_cmd, per_cpu, CalcTimeRangesByCPU, cpu_time_ranges, max_time) - - for cpu_time_range in cpu_time_ranges: - if cpu_time_range.sample_cnt: - IntersectTimeRanges(cpu_time_range.time_ranges, time_ranges) - - return [cpu_time_ranges[cpu].time_ranges for cpu in cpus] - -def SplitSingleTimeRangeIntoN(time_range, n): - if n <= 1: - return [time_range] - start = time_range[0] - end = time_range[1] - duration = int((end - start + 1) / n) - if duration < 1: - return [time_range] - time_ranges = [] - for i in range(n): - time_ranges.append([start, start + duration - 1]) - start += duration - time_ranges[-1][1] = end - return time_ranges - -def TimeRangeDuration(r): - return r[1] - r[0] + 1 - -def TotalDuration(time_ranges): - duration = 0 - for r in time_ranges: - duration += TimeRangeDuration(r) - return duration - -def SplitTimeRangesByInterval(time_ranges, interval): - new_ranges = [] - for r in time_ranges: - duration = TimeRangeDuration(r) - n = duration / interval - n = int(round(n, 0)) - new_ranges += SplitSingleTimeRangeIntoN(r, n) - return new_ranges - -def SplitTimeRangesIntoN(time_ranges, n, min_interval): - if n <= len(time_ranges): - return time_ranges - duration = TotalDuration(time_ranges) - interval = duration / n - if interval < min_interval: - interval = min_interval - return SplitTimeRangesByInterval(time_ranges, interval) - -def RecombineTimeRanges(tr): - new_tr = copy.deepcopy(tr) - n = len(new_tr) - i = 1 - while i < len(new_tr): - # if prev end + 1 == cur start, combine them - if new_tr[i - 1][1] + 1 == new_tr[i][0]: - new_tr[i][0] = new_tr[i - 1][0] - del new_tr[i - 1] - else: - i += 1 - return new_tr - -def OpenTimeRangeEnds(time_ranges, min_time, max_time): - if time_ranges[0][0] <= min_time: - time_ranges[0][0] = None - if time_ranges[-1][1] >= max_time: - time_ranges[-1][1] = None - -def BadTimeStr(time_str): - raise Exception(f"perf command bad time option: '{time_str}'\nCheck also 'time of first sample' and 'time of last sample' in perf script --header-only") - -def ValidateTimeRanges(time_ranges, time_str): - n = len(time_ranges) - for i in range(n): - start = time_ranges[i][0] - end = time_ranges[i][1] - if i != 0 and start <= time_ranges[i - 1][1]: - BadTimeStr(time_str) - if start > end: - BadTimeStr(time_str) - -def TimeVal(s, dflt): - s = s.strip() - if s == "": - return dflt - a = s.split(".") - if len(a) > 2: - raise Exception(f"Bad time value'{s}'") - x = int(a[0]) - if x < 0: - raise Exception("Negative time not allowed") - x *= 1000000000 - if len(a) > 1: - x += int((a[1] + "000000000")[:9]) - return x - -def BadCPUStr(cpu_str): - raise Exception(f"perf command bad cpu option: '{cpu_str}'\nCheck also 'nrcpus avail' in perf script --header-only") - -def ParseTimeStr(time_str, min_time, max_time): - if time_str == None or time_str == "": - return [[min_time, max_time]] - time_ranges = [] - for r in time_str.split(): - a = r.split(",") - if len(a) != 2: - BadTimeStr(time_str) - try: - start = TimeVal(a[0], min_time) - end = TimeVal(a[1], max_time) - except: - BadTimeStr(time_str) - time_ranges.append([start, end]) - ValidateTimeRanges(time_ranges, time_str) - return time_ranges - -def ParseCPUStr(cpu_str, nr_cpus): - if cpu_str == None or cpu_str == "": - return [-1] - cpus = [] - for r in cpu_str.split(","): - a = r.split("-") - if len(a) < 1 or len(a) > 2: - BadCPUStr(cpu_str) - try: - start = int(a[0].strip()) - if len(a) > 1: - end = int(a[1].strip()) - else: - end = start - except: - BadCPUStr(cpu_str) - if start < 0 or end < 0 or end < start or end >= nr_cpus: - BadCPUStr(cpu_str) - cpus.extend(range(start, end + 1)) - cpus = list(set(cpus)) # Remove duplicates - cpus.sort() - return cpus - -class ParallelPerf(): - - def __init__(self, a): - for arg_name in vars(a): - setattr(self, arg_name, getattr(a, arg_name)) - self.orig_nr = self.nr - self.orig_cmd = list(self.cmd) - self.perf = self.cmd[0] - if os.path.exists(self.output_dir): - raise Exception(f"Output '{self.output_dir}' already exists") - if self.jobs < 0 or self.nr < 0 or self.interval < 0: - raise Exception("Bad options (negative values): try -h option for help") - if self.nr != 0 and self.interval != 0: - raise Exception("Cannot specify number of time subdivisions and time interval") - if self.jobs == 0: - self.jobs = NumberOfCPUs() - if self.nr == 0 and self.interval == 0: - if self.per_cpu: - self.nr = 1 - else: - self.nr = self.jobs - - def Init(self): - if self.verbosity.debug: - print("cmd", self.cmd) - self.file_name = DetermineInputFileName(self.cmd) - self.hdr = ReadHeader(self.perf, self.file_name) - self.hdr_dict = ParseHeader(self.hdr) - self.cmd_line = HeaderField(self.hdr_dict, "cmdline") - - def ExtractTimeInfo(self): - self.min_time = TimeVal(HeaderField(self.hdr_dict, "time of first sample"), 0) - self.max_time = TimeVal(HeaderField(self.hdr_dict, "time of last sample"), 0) - self.time_str = ExtractPerfOption(self.cmd, "", "time") - self.time_ranges = ParseTimeStr(self.time_str, self.min_time, self.max_time) - if self.verbosity.debug: - print("time_ranges", self.time_ranges) - - def ExtractCPUInfo(self): - if self.per_cpu: - nr_cpus = int(HeaderField(self.hdr_dict, "nrcpus avail")) - self.cpu_str = ExtractPerfOption(self.cmd, "C", "cpu") - if self.cpu_str == None or self.cpu_str == "": - self.cpus = [ x for x in range(nr_cpus) ] - else: - self.cpus = ParseCPUStr(self.cpu_str, nr_cpus) - else: - self.cpu_str = None - self.cpus = [-1] - if self.verbosity.debug: - print("cpus", self.cpus) - - def IsIntelPT(self): - return self.cmd_line.find("intel_pt") >= 0 - - def SplitTimeRanges(self): - if self.IsIntelPT() and self.interval == 0: - self.split_time_ranges_for_each_cpu = \ - SplitTimeRangesByTraceDataDensity(self.time_ranges, self.cpus, self.orig_nr, - self.orig_cmd, self.file_name, self.per_cpu, - self.min_size, self.min_interval, self.verbosity) - elif self.nr: - self.split_time_ranges_for_each_cpu = [ SplitTimeRangesIntoN(self.time_ranges, self.nr, self.min_interval) ] - else: - self.split_time_ranges_for_each_cpu = [ SplitTimeRangesByInterval(self.time_ranges, self.interval) ] - - def CheckTimeRanges(self): - for tr in self.split_time_ranges_for_each_cpu: - # Re-combined time ranges should be the same - new_tr = RecombineTimeRanges(tr) - if new_tr != self.time_ranges: - if self.verbosity.debug: - print("tr", tr) - print("new_tr", new_tr) - raise Exception("Self test failed!") - - def OpenTimeRangeEnds(self): - for time_ranges in self.split_time_ranges_for_each_cpu: - OpenTimeRangeEnds(time_ranges, self.min_time, self.max_time) - - def CreateWorkList(self): - self.worklist = CreateWorkList(self.cmd, self.pipe_to, self.output_dir, self.cpus, self.split_time_ranges_for_each_cpu) - - def PerfDataRecordedPerCPU(self): - if "--per-thread" in self.cmd_line.split(): - return False - return True - - def DefaultToPerCPU(self): - # --no-per-cpu option takes precedence - if self.no_per_cpu: - return False - if not self.PerfDataRecordedPerCPU(): - return False - # Default to per-cpu for Intel PT data that was recorded per-cpu, - # because decoding can be done for each CPU separately. - if self.IsIntelPT(): - return True - return False - - def Config(self): - self.Init() - self.ExtractTimeInfo() - if not self.per_cpu: - self.per_cpu = self.DefaultToPerCPU() - if self.verbosity.debug: - print("per_cpu", self.per_cpu) - self.ExtractCPUInfo() - self.SplitTimeRanges() - if self.verbosity.self_test: - self.CheckTimeRanges() - # Prefer open-ended time range to starting / ending with min_time / max_time resp. - self.OpenTimeRangeEnds() - self.CreateWorkList() - - def Run(self): - if self.dry_run: - print(len(self.worklist),"jobs:") - for w in self.worklist: - print(w.Command()) - return True - result = RunWork(self.worklist, self.jobs, verbosity=self.verbosity) - if self.verbosity.verbose: - print(glb_prog_name, "done") - return result - -def RunParallelPerf(a): - pp = ParallelPerf(a) - pp.Config() - return pp.Run() - -def Main(args): - ap = argparse.ArgumentParser( - prog=glb_prog_name, formatter_class = argparse.RawDescriptionHelpFormatter, - description = -""" -Run a perf script command multiple times in parallel, using perf script options ---cpu and --time so that each job processes a different chunk of the data. -""", - epilog = -""" -Follow the options by '--' and then the perf script command e.g. - - $ perf record -a -- sleep 10 - $ parallel-perf.py --nr=4 -- perf script --ns - All jobs finished successfully - $ tree parallel-perf-output/ - parallel-perf-output/ - ├── time-range-0 - │   ├── cmd.txt - │   └── out.txt - ├── time-range-1 - │   ├── cmd.txt - │   └── out.txt - ├── time-range-2 - │   ├── cmd.txt - │   └── out.txt - └── time-range-3 - ├── cmd.txt - └── out.txt - $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H . - parallel-perf-output/time-range-0/cmd.txt:perf script --time=,9466.504461499 --ns - parallel-perf-output/time-range-1/cmd.txt:perf script --time=9466.504461500,9469.005396999 --ns - parallel-perf-output/time-range-2/cmd.txt:perf script --time=9469.005397000,9471.506332499 --ns - parallel-perf-output/time-range-3/cmd.txt:perf script --time=9471.506332500, --ns - -Any perf script command can be used, including the use of perf script options ---dlfilter and --script, so that the benefit of running parallel jobs -naturally extends to them also. - -If option --pipe-to is used, standard output is first piped through that -command. Beware, if the command fails (e.g. grep with no matches), it will be -considered a fatal error. - -Final standard output is redirected to files named out.txt in separate -subdirectories under the output directory. Similarly, standard error is -written to files named err.txt. In addition, files named cmd.txt contain the -corresponding perf script command. After processing, err.txt files are removed -if they are empty. - -If any job exits with a non-zero exit code, then all jobs are killed and no -more are started. A message is printed if any job results in a non-empty -err.txt file. - -There is a separate output subdirectory for each time range. If the --per-cpu -option is used, these are further grouped under cpu-n subdirectories, e.g. - - $ parallel-perf.py --per-cpu --nr=2 -- perf script --ns --cpu=0,1 - All jobs finished successfully - $ tree parallel-perf-output - parallel-perf-output/ - ├── cpu-0 - │   ├── time-range-0 - │   │   ├── cmd.txt - │   │   └── out.txt - │   └── time-range-1 - │   ├── cmd.txt - │   └── out.txt - └── cpu-1 - ├── time-range-0 - │   ├── cmd.txt - │   └── out.txt - └── time-range-1 - ├── cmd.txt - └── out.txt - $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H . - parallel-perf-output/cpu-0/time-range-0/cmd.txt:perf script --cpu=0 --time=,9469.005396999 --ns - parallel-perf-output/cpu-0/time-range-1/cmd.txt:perf script --cpu=0 --time=9469.005397000, --ns - parallel-perf-output/cpu-1/time-range-0/cmd.txt:perf script --cpu=1 --time=,9469.005396999 --ns - parallel-perf-output/cpu-1/time-range-1/cmd.txt:perf script --cpu=1 --time=9469.005397000, --ns - -Subdivisions of time range, and cpus if the --per-cpu option is used, are -expressed by the --time and --cpu perf script options respectively. If the -supplied perf script command has a --time option, then that time range is -subdivided, otherwise the time range given by 'time of first sample' to -'time of last sample' is used (refer perf script --header-only). Similarly, the -supplied perf script command may provide a --cpu option, and only those CPUs -will be processed. - -To prevent time intervals becoming too small, the --min-interval option can -be used. - -Note there is special handling for processing Intel PT traces. If an interval is -not specified and the perf record command contained the intel_pt event, then the -time range will be subdivided in order to produce subdivisions that contain -approximately the same amount of trace data. That is accomplished by counting -double-quick (--itrace=qqi) samples, and choosing time ranges that encompass -approximately the same number of samples. In that case, time ranges may not be -the same for each CPU processed. For Intel PT, --per-cpu is the default, but -that can be overridden by --no-per-cpu. Note, for Intel PT, double-quick -decoding produces 1 sample for each PSB synchronization packet, which in turn -come after a certain number of bytes output, determined by psb_period (refer -perf Intel PT documentation). The minimum number of double-quick samples that -will define a time range can be set by the --min_size option, which defaults to -64. -""") - ap.add_argument("-o", "--output-dir", default="parallel-perf-output", help="output directory (default 'parallel-perf-output')") - ap.add_argument("-j", "--jobs", type=int, default=0, help="maximum number of jobs to run in parallel at one time (default is the number of CPUs)") - ap.add_argument("-n", "--nr", type=int, default=0, help="number of time subdivisions (default is the number of jobs)") - ap.add_argument("-i", "--interval", type=float, default=0, help="subdivide the time range using this time interval (in seconds e.g. 0.1 for a tenth of a second)") - ap.add_argument("-c", "--per-cpu", action="store_true", help="process data for each CPU in parallel") - ap.add_argument("-m", "--min-interval", type=float, default=glb_min_interval, help=f"minimum interval (default {glb_min_interval} seconds)") - ap.add_argument("-p", "--pipe-to", help="command to pipe output to (optional)") - ap.add_argument("-N", "--no-per-cpu", action="store_true", help="do not process data for each CPU in parallel") - ap.add_argument("-b", "--min_size", type=int, default=glb_min_samples, help="minimum data size (for Intel PT in PSBs)") - ap.add_argument("-D", "--dry-run", action="store_true", help="do not run any jobs, just show the perf script commands") - ap.add_argument("-q", "--quiet", action="store_true", help="do not print any messages except errors") - ap.add_argument("-v", "--verbose", action="store_true", help="print more messages") - ap.add_argument("-d", "--debug", action="store_true", help="print debugging messages") - cmd_line = list(args) - try: - split_pos = cmd_line.index("--") - cmd = cmd_line[split_pos + 1:] - args = cmd_line[:split_pos] - except: - cmd = None - args = cmd_line - a = ap.parse_args(args=args[1:]) - a.cmd = cmd - a.verbosity = Verbosity(a.quiet, a.verbose, a.debug) - try: - if a.cmd == None: - if len(args) <= 1: - ap.print_help() - return True - raise Exception("Command line must contain '--' before perf command") - return RunParallelPerf(a) - except Exception as e: - print("Fatal error: ", str(e)) - if a.debug: - raise - return False - -if __name__ == "__main__": - if not Main(sys.argv): - sys.exit(1) diff --git a/tools/perf/tests/shell/script.sh b/tools/perf/tests/shell/script.sh index 7007f1cdf761..254fc3ae94e7 100755 --- a/tools/perf/tests/shell/script.sh +++ b/tools/perf/tests/shell/script.sh @@ -1,6 +1,6 @@ #!/bin/bash -# perf script tests # SPDX-License-Identifier: GPL-2.0 +# perf script tests set -e @@ -76,7 +76,7 @@ test_parallel_perf() err=2 return fi - pp=$(dirname "$0")/../../scripts/python/parallel-perf.py + pp=$(dirname "$0")/../../python/parallel-perf.py if [ ! -f "${pp}" ] ; then echo "SKIP: parallel-perf.py script not found " err=2 From 0d0f9e540d940a48b91cbea1657794d44b25aedf Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:13 -0700 Subject: [PATCH 221/425] perf: Remove libpython support and legacy Python scripts Remove embedded Python interpreter support (libpython) from perf, as all Python scripts have been migrated to standalone scripts using the perf Python extension module. Changes include: - Remove libpython detection and build flags from Makefile.config. - Remove legacy Python script installation rules from Makefile.perf. - Delete tools/perf/util/scripting-engines/trace-event-python.c and tools/perf/scripts/python/Perf-Trace-Util/Context.c. - Remove Python scripting engine registration from trace-event-scripting.c. - Remove libpython from the supported features list in builtin-check.c and Documentation/perf-check.txt. - Delete the legacy Python scripts and bin wrappers in tools/perf/scripts/python/ and update shell tests. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/perf-check.txt | 1 - tools/perf/Makefile.config | 26 +- tools/perf/builtin-check.c | 2 +- tools/perf/scripts/Build | 27 - .../perf/scripts/python/Perf-Trace-Util/Build | 4 - .../scripts/python/Perf-Trace-Util/Context.c | 225 -- .../Perf-Trace-Util/lib/Perf/Trace/Core.py | 116 - .../lib/Perf/Trace/EventClass.py | 97 - .../lib/Perf/Trace/SchedGui.py | 184 -- .../Perf-Trace-Util/lib/Perf/Trace/Util.py | 92 - .../scripts/python/arm-cs-trace-disasm.py | 356 --- .../python/bin/compaction-times-record | 2 - .../python/bin/compaction-times-report | 4 - .../python/bin/event_analyzing_sample-record | 8 - .../python/bin/event_analyzing_sample-report | 3 - .../python/bin/export-to-postgresql-record | 8 - .../python/bin/export-to-postgresql-report | 29 - .../python/bin/export-to-sqlite-record | 8 - .../python/bin/export-to-sqlite-report | 29 - .../python/bin/failed-syscalls-by-pid-record | 3 - .../python/bin/failed-syscalls-by-pid-report | 10 - .../perf/scripts/python/bin/flamegraph-record | 2 - .../perf/scripts/python/bin/flamegraph-report | 3 - .../python/bin/futex-contention-record | 2 - .../python/bin/futex-contention-report | 4 - tools/perf/scripts/python/bin/gecko-record | 2 - tools/perf/scripts/python/bin/gecko-report | 7 - .../scripts/python/bin/intel-pt-events-record | 13 - .../scripts/python/bin/intel-pt-events-report | 3 - .../scripts/python/bin/mem-phys-addr-record | 19 - .../scripts/python/bin/mem-phys-addr-report | 3 - .../scripts/python/bin/net_dropmonitor-record | 2 - .../scripts/python/bin/net_dropmonitor-report | 4 - .../scripts/python/bin/netdev-times-record | 8 - .../scripts/python/bin/netdev-times-report | 5 - .../scripts/python/bin/powerpc-hcalls-record | 2 - .../scripts/python/bin/powerpc-hcalls-report | 2 - .../scripts/python/bin/sched-migration-record | 2 - .../scripts/python/bin/sched-migration-report | 3 - tools/perf/scripts/python/bin/sctop-record | 3 - tools/perf/scripts/python/bin/sctop-report | 24 - .../scripts/python/bin/stackcollapse-record | 8 - .../scripts/python/bin/stackcollapse-report | 3 - .../python/bin/syscall-counts-by-pid-record | 3 - .../python/bin/syscall-counts-by-pid-report | 10 - .../scripts/python/bin/syscall-counts-record | 3 - .../scripts/python/bin/syscall-counts-report | 10 - .../scripts/python/bin/task-analyzer-record | 2 - .../scripts/python/bin/task-analyzer-report | 3 - tools/perf/scripts/python/check-perf-trace.py | 84 - tools/perf/scripts/python/compaction-times.py | 311 --- .../scripts/python/event_analyzing_sample.py | 192 -- .../scripts/python/export-to-postgresql.py | 1114 -------- tools/perf/scripts/python/export-to-sqlite.py | 799 ------ .../scripts/python/failed-syscalls-by-pid.py | 79 - tools/perf/scripts/python/flamegraph.py | 267 -- tools/perf/scripts/python/futex-contention.py | 57 - tools/perf/scripts/python/gecko.py | 395 --- tools/perf/scripts/python/intel-pt-events.py | 494 ---- tools/perf/scripts/python/libxed.py | 107 - tools/perf/scripts/python/mem-phys-addr.py | 127 - tools/perf/scripts/python/net_dropmonitor.py | 78 - tools/perf/scripts/python/netdev-times.py | 473 ---- tools/perf/scripts/python/powerpc-hcalls.py | 335 --- tools/perf/scripts/python/sched-migration.py | 462 ---- tools/perf/scripts/python/sctop.py | 89 - tools/perf/scripts/python/stackcollapse.py | 127 - tools/perf/scripts/python/stat-cpi.py | 79 - .../scripts/python/syscall-counts-by-pid.py | 75 - tools/perf/scripts/python/syscall-counts.py | 65 - tools/perf/scripts/python/task-analyzer.py | 934 ------- tools/perf/tests/shell/script.sh | 42 - tools/perf/tests/shell/script_python.sh | 113 - tools/perf/util/scripting-engines/Build | 4 - .../scripting-engines/trace-event-python.c | 2335 ----------------- tools/perf/util/trace-event-scripting.c | 9 - 76 files changed, 2 insertions(+), 10633 deletions(-) delete mode 100644 tools/perf/scripts/python/Perf-Trace-Util/Build delete mode 100644 tools/perf/scripts/python/Perf-Trace-Util/Context.c delete mode 100644 tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/Core.py delete mode 100755 tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/EventClass.py delete mode 100644 tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/SchedGui.py delete mode 100644 tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/Util.py delete mode 100755 tools/perf/scripts/python/arm-cs-trace-disasm.py delete mode 100644 tools/perf/scripts/python/bin/compaction-times-record delete mode 100644 tools/perf/scripts/python/bin/compaction-times-report delete mode 100644 tools/perf/scripts/python/bin/event_analyzing_sample-record delete mode 100644 tools/perf/scripts/python/bin/event_analyzing_sample-report delete mode 100644 tools/perf/scripts/python/bin/export-to-postgresql-record delete mode 100644 tools/perf/scripts/python/bin/export-to-postgresql-report delete mode 100644 tools/perf/scripts/python/bin/export-to-sqlite-record delete mode 100644 tools/perf/scripts/python/bin/export-to-sqlite-report delete mode 100644 tools/perf/scripts/python/bin/failed-syscalls-by-pid-record delete mode 100644 tools/perf/scripts/python/bin/failed-syscalls-by-pid-report delete mode 100755 tools/perf/scripts/python/bin/flamegraph-record delete mode 100755 tools/perf/scripts/python/bin/flamegraph-report delete mode 100644 tools/perf/scripts/python/bin/futex-contention-record delete mode 100644 tools/perf/scripts/python/bin/futex-contention-report delete mode 100644 tools/perf/scripts/python/bin/gecko-record delete mode 100755 tools/perf/scripts/python/bin/gecko-report delete mode 100644 tools/perf/scripts/python/bin/intel-pt-events-record delete mode 100644 tools/perf/scripts/python/bin/intel-pt-events-report delete mode 100644 tools/perf/scripts/python/bin/mem-phys-addr-record delete mode 100644 tools/perf/scripts/python/bin/mem-phys-addr-report delete mode 100755 tools/perf/scripts/python/bin/net_dropmonitor-record delete mode 100755 tools/perf/scripts/python/bin/net_dropmonitor-report delete mode 100644 tools/perf/scripts/python/bin/netdev-times-record delete mode 100644 tools/perf/scripts/python/bin/netdev-times-report delete mode 100644 tools/perf/scripts/python/bin/powerpc-hcalls-record delete mode 100644 tools/perf/scripts/python/bin/powerpc-hcalls-report delete mode 100644 tools/perf/scripts/python/bin/sched-migration-record delete mode 100644 tools/perf/scripts/python/bin/sched-migration-report delete mode 100644 tools/perf/scripts/python/bin/sctop-record delete mode 100644 tools/perf/scripts/python/bin/sctop-report delete mode 100755 tools/perf/scripts/python/bin/stackcollapse-record delete mode 100755 tools/perf/scripts/python/bin/stackcollapse-report delete mode 100644 tools/perf/scripts/python/bin/syscall-counts-by-pid-record delete mode 100644 tools/perf/scripts/python/bin/syscall-counts-by-pid-report delete mode 100644 tools/perf/scripts/python/bin/syscall-counts-record delete mode 100644 tools/perf/scripts/python/bin/syscall-counts-report delete mode 100755 tools/perf/scripts/python/bin/task-analyzer-record delete mode 100755 tools/perf/scripts/python/bin/task-analyzer-report delete mode 100644 tools/perf/scripts/python/check-perf-trace.py delete mode 100644 tools/perf/scripts/python/compaction-times.py delete mode 100644 tools/perf/scripts/python/event_analyzing_sample.py delete mode 100644 tools/perf/scripts/python/export-to-postgresql.py delete mode 100644 tools/perf/scripts/python/export-to-sqlite.py delete mode 100644 tools/perf/scripts/python/failed-syscalls-by-pid.py delete mode 100755 tools/perf/scripts/python/flamegraph.py delete mode 100644 tools/perf/scripts/python/futex-contention.py delete mode 100644 tools/perf/scripts/python/gecko.py delete mode 100644 tools/perf/scripts/python/intel-pt-events.py delete mode 100644 tools/perf/scripts/python/libxed.py delete mode 100644 tools/perf/scripts/python/mem-phys-addr.py delete mode 100755 tools/perf/scripts/python/net_dropmonitor.py delete mode 100644 tools/perf/scripts/python/netdev-times.py delete mode 100644 tools/perf/scripts/python/powerpc-hcalls.py delete mode 100644 tools/perf/scripts/python/sched-migration.py delete mode 100644 tools/perf/scripts/python/sctop.py delete mode 100755 tools/perf/scripts/python/stackcollapse.py delete mode 100644 tools/perf/scripts/python/stat-cpi.py delete mode 100644 tools/perf/scripts/python/syscall-counts-by-pid.py delete mode 100644 tools/perf/scripts/python/syscall-counts.py delete mode 100755 tools/perf/scripts/python/task-analyzer.py delete mode 100755 tools/perf/tests/shell/script_python.sh delete mode 100644 tools/perf/util/scripting-engines/trace-event-python.c diff --git a/tools/perf/Documentation/perf-check.txt b/tools/perf/Documentation/perf-check.txt index 09e1d35677f5..3d169e5bb372 100644 --- a/tools/perf/Documentation/perf-check.txt +++ b/tools/perf/Documentation/perf-check.txt @@ -60,7 +60,6 @@ feature:: libopencsd / HAVE_CSTRACE_SUPPORT libperl / HAVE_LIBPERL_SUPPORT libpfm4 / HAVE_LIBPFM - libpython / HAVE_LIBPYTHON_SUPPORT libslang / HAVE_SLANG_SUPPORT libtraceevent / HAVE_LIBTRACEEVENT libunwind / HAVE_LIBUNWIND_SUPPORT diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config index c4ae1cf04e00..e04125ad4f28 100644 --- a/tools/perf/Makefile.config +++ b/tools/perf/Makefile.config @@ -800,27 +800,6 @@ ifdef GTK4 endif endif -ifdef LIBPERL - PERL_EMBED_LDOPTS = $(shell perl -MExtUtils::Embed -e ldopts 2>/dev/null) - PERL_EMBED_LDFLAGS = $(call strip-libs,$(PERL_EMBED_LDOPTS)) - PERL_EMBED_LIBADD = $(call grep-libs,$(PERL_EMBED_LDOPTS)) - PERL_EMBED_CCOPTS = $(shell perl -MExtUtils::Embed -e ccopts 2>/dev/null) - PERL_EMBED_CCOPTS := $(filter-out -specs=%,$(PERL_EMBED_CCOPTS)) - PERL_EMBED_CCOPTS := $(filter-out -flto% -ffat-lto-objects, $(PERL_EMBED_CCOPTS)) - PERL_EMBED_LDOPTS := $(filter-out -specs=%,$(PERL_EMBED_LDOPTS)) - FLAGS_PERL_EMBED=$(PERL_EMBED_CCOPTS) $(PERL_EMBED_LDOPTS) - - $(call feature_check,libperl) - ifneq ($(feature-libperl), 1) - $(error Missing perl devel files. Please install perl-ExtUtils-Embed/libperl-dev) - else - LDFLAGS += $(PERL_EMBED_LDFLAGS) - EXTLIBS += $(PERL_EMBED_LIBADD) - CFLAGS += -DHAVE_LIBPERL_SUPPORT - $(call detected,CONFIG_LIBPERL) - endif -endif - ifeq ($(feature-timerfd), 1) CFLAGS += -DHAVE_TIMERFD_SUPPORT else @@ -869,8 +848,7 @@ else ifneq ($(PYTHON_39_GOOD), 1) $(error ERROR: Python interpreter needed for the python binding too old (older than 3.9). Install a newer python or build with NO_LIBPYTHON=1.) endif - LDFLAGS += $(PYTHON_EMBED_LDFLAGS) - EXTLIBS += $(PYTHON_EMBED_LIBADD) + CFLAGS += -DHAVE_LIBPYTHON_SUPPORT PYTHON_SETUPTOOLS_INSTALLED := $(shell $(PYTHON) -c 'import setuptools;' 2> /dev/null && echo "yes" || echo "no") ifeq ($(PYTHON_SETUPTOOLS_INSTALLED), yes) PYTHON_EXTENSION_SUFFIX := $(shell $(PYTHON) -c 'from importlib import machinery; print(machinery.EXTENSION_SUFFIXES[0])') @@ -881,8 +859,6 @@ else else $(warning Missing python setuptools, the python binding won't be built, please install python3-setuptools or equivalent) endif - CFLAGS += -DHAVE_LIBPYTHON_SUPPORT - $(call detected,CONFIG_LIBPYTHON) ifeq ($(filter -fPIC,$(CFLAGS)),) # Building a shared library requires position independent code. CFLAGS += -fPIC diff --git a/tools/perf/builtin-check.c b/tools/perf/builtin-check.c index 60437650c50f..35272aaeb613 100644 --- a/tools/perf/builtin-check.c +++ b/tools/perf/builtin-check.c @@ -52,8 +52,8 @@ struct feature_status supported_features[] = { FEATURE_STATUS("libnuma", HAVE_LIBNUMA_SUPPORT), FEATURE_STATUS("libopencsd", HAVE_CSTRACE_SUPPORT), FEATURE_STATUS_TIP("libperl", HAVE_LIBPERL_SUPPORT, "Deprecated, use LIBPERL=1 and install perl-ExtUtils-Embed/libperl-dev to build with it"), + FEATURE_STATUS("python-module", HAVE_LIBPYTHON_SUPPORT), FEATURE_STATUS("libpfm4", HAVE_LIBPFM), - FEATURE_STATUS("libpython", HAVE_LIBPYTHON_SUPPORT), FEATURE_STATUS("libslang", HAVE_SLANG_SUPPORT), FEATURE_STATUS("libtraceevent", HAVE_LIBTRACEEVENT), FEATURE_STATUS_TIP("libunwind", HAVE_LIBUNWIND_SUPPORT, "Deprecated, use LIBUNWIND=1 and install libunwind-dev[el] to build with it"), diff --git a/tools/perf/scripts/Build b/tools/perf/scripts/Build index 91229a1fe3ff..fbeab8fff88b 100644 --- a/tools/perf/scripts/Build +++ b/tools/perf/scripts/Build @@ -1,30 +1,3 @@ ifeq ($(CONFIG_LIBTRACEEVENT),y) perf-util-$(CONFIG_LIBPERL) += perl/Perf-Trace-Util/ endif -perf-util-$(CONFIG_LIBPYTHON) += python/Perf-Trace-Util/ - -ifdef MYPY - PY_TESTS := $(shell find python -type f -name '*.py') - MYPY_TEST_LOGS := $(PY_TESTS:python/%=python/%.mypy_log) -else - MYPY_TEST_LOGS := -endif - -$(OUTPUT)%.mypy_log: % - $(call rule_mkdir) - $(Q)$(call echo-cmd,test)mypy "$<" > $@ || (cat $@ && rm $@ && false) - -perf-y += $(MYPY_TEST_LOGS) - -ifdef PYLINT - PY_TESTS := $(shell find python -type f -name '*.py') - PYLINT_TEST_LOGS := $(PY_TESTS:python/%=python/%.pylint_log) -else - PYLINT_TEST_LOGS := -endif - -$(OUTPUT)%.pylint_log: % - $(call rule_mkdir) - $(Q)$(call echo-cmd,test)pylint "$<" > $@ || (cat $@ && rm $@ && false) - -perf-y += $(PYLINT_TEST_LOGS) diff --git a/tools/perf/scripts/python/Perf-Trace-Util/Build b/tools/perf/scripts/python/Perf-Trace-Util/Build deleted file mode 100644 index be3710c61320..000000000000 --- a/tools/perf/scripts/python/Perf-Trace-Util/Build +++ /dev/null @@ -1,4 +0,0 @@ -perf-util-y += Context.o - -# -Wno-declaration-after-statement: The python headers have mixed code with declarations (decls after asserts, for instance) -CFLAGS_Context.o += $(PYTHON_EMBED_CCOPTS) -Wno-redundant-decls -Wno-strict-prototypes -Wno-unused-parameter -Wno-nested-externs -Wno-declaration-after-statement diff --git a/tools/perf/scripts/python/Perf-Trace-Util/Context.c b/tools/perf/scripts/python/Perf-Trace-Util/Context.c deleted file mode 100644 index c19f44610983..000000000000 --- a/tools/perf/scripts/python/Perf-Trace-Util/Context.c +++ /dev/null @@ -1,225 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Context.c. Python interfaces for perf script. - * - * Copyright (C) 2010 Tom Zanussi - */ - -/* - * Use Py_ssize_t for '#' formats to avoid DeprecationWarning: PY_SSIZE_T_CLEAN - * will be required for '#' formats. - */ -#define PY_SSIZE_T_CLEAN - -#include -#include "../../../util/config.h" -#include "../../../util/trace-event.h" -#include "../../../util/event.h" -#include "../../../util/symbol.h" -#include "../../../util/thread.h" -#include "../../../util/map.h" -#include "../../../util/maps.h" -#include "../../../util/auxtrace.h" -#include "../../../util/session.h" -#include "../../../util/srcline.h" -#include "../../../util/srccode.h" - -#define _PyCapsule_GetPointer(arg1, arg2) \ - PyCapsule_GetPointer((arg1), (arg2)) -#define _PyBytes_FromStringAndSize(arg1, arg2) \ - PyBytes_FromStringAndSize((arg1), (arg2)) -#define _PyUnicode_AsUTF8(arg) \ - PyUnicode_AsUTF8(arg) - -PyMODINIT_FUNC PyInit_perf_trace_context(void); - -static struct scripting_context *get_args(PyObject *args, const char *name, PyObject **arg2) -{ - int cnt = 1 + !!arg2; - PyObject *context; - - if (!PyArg_UnpackTuple(args, name, 1, cnt, &context, arg2)) - return NULL; - - return _PyCapsule_GetPointer(context, NULL); -} - -static struct scripting_context *get_scripting_context(PyObject *args) -{ - return get_args(args, "context", NULL); -} - -#ifdef HAVE_LIBTRACEEVENT -static PyObject *perf_trace_context_common_pc(PyObject *obj, PyObject *args) -{ - struct scripting_context *c = get_scripting_context(args); - - if (!c) - return NULL; - - return Py_BuildValue("i", common_pc(c)); -} - -static PyObject *perf_trace_context_common_flags(PyObject *obj, - PyObject *args) -{ - struct scripting_context *c = get_scripting_context(args); - - if (!c) - return NULL; - - return Py_BuildValue("i", common_flags(c)); -} - -static PyObject *perf_trace_context_common_lock_depth(PyObject *obj, - PyObject *args) -{ - struct scripting_context *c = get_scripting_context(args); - - if (!c) - return NULL; - - return Py_BuildValue("i", common_lock_depth(c)); -} -#endif - -static PyObject *perf_sample_insn(PyObject *obj, PyObject *args) -{ - struct scripting_context *c = get_scripting_context(args); - - if (!c) - return NULL; - - if (c->sample->ip && !c->sample->insn_len && thread__maps(c->al->thread)) { - struct machine *machine = maps__machine(thread__maps(c->al->thread)); - - perf_sample__fetch_insn(c->sample, c->al->thread, machine); - } - if (!c->sample->insn_len) - Py_RETURN_NONE; /* N.B. This is a return statement */ - - return _PyBytes_FromStringAndSize(c->sample->insn, c->sample->insn_len); -} - -static PyObject *perf_set_itrace_options(PyObject *obj, PyObject *args) -{ - struct scripting_context *c; - const char *itrace_options; - int retval = -1; - PyObject *str; - - c = get_args(args, "itrace_options", &str); - if (!c) - return NULL; - - if (!c->session || !c->session->itrace_synth_opts) - goto out; - - if (c->session->itrace_synth_opts->set) { - retval = 1; - goto out; - } - - itrace_options = _PyUnicode_AsUTF8(str); - - retval = itrace_do_parse_synth_opts(c->session->itrace_synth_opts, itrace_options, 0); -out: - return Py_BuildValue("i", retval); -} - -static PyObject *perf_sample_src(PyObject *obj, PyObject *args, bool get_srccode) -{ - struct scripting_context *c = get_scripting_context(args); - unsigned int line = 0; - char *srcfile = NULL; - char *srccode = NULL; - PyObject *result; - struct map *map; - struct dso *dso; - int len = 0; - u64 addr; - - if (!c) - return NULL; - - map = c->al->map; - addr = c->al->addr; - dso = map ? map__dso(map) : NULL; - - if (dso) - srcfile = get_srcline_split(dso, map__rip_2objdump(map, addr), &line); - - if (get_srccode) { - if (srcfile) - srccode = find_sourceline(srcfile, line, &len); - result = Py_BuildValue("(sIs#)", srcfile, line, srccode, (Py_ssize_t)len); - } else { - result = Py_BuildValue("(sI)", srcfile, line); - } - - free(srcfile); - - return result; -} - -static PyObject *perf_sample_srcline(PyObject *obj, PyObject *args) -{ - return perf_sample_src(obj, args, false); -} - -static PyObject *perf_sample_srccode(PyObject *obj, PyObject *args) -{ - return perf_sample_src(obj, args, true); -} - -static PyObject *__perf_config_get(PyObject *obj, PyObject *args) -{ - const char *config_name; - - if (!PyArg_ParseTuple(args, "s", &config_name)) - return NULL; - return Py_BuildValue("s", perf_config_get(config_name)); -} - -static PyMethodDef ContextMethods[] = { -#ifdef HAVE_LIBTRACEEVENT - { "common_pc", perf_trace_context_common_pc, METH_VARARGS, - "Get the common preempt count event field value."}, - { "common_flags", perf_trace_context_common_flags, METH_VARARGS, - "Get the common flags event field value."}, - { "common_lock_depth", perf_trace_context_common_lock_depth, - METH_VARARGS, "Get the common lock depth event field value."}, -#endif - { "perf_sample_insn", perf_sample_insn, - METH_VARARGS, "Get the machine code instruction."}, - { "perf_set_itrace_options", perf_set_itrace_options, - METH_VARARGS, "Set --itrace options."}, - { "perf_sample_srcline", perf_sample_srcline, - METH_VARARGS, "Get source file name and line number."}, - { "perf_sample_srccode", perf_sample_srccode, - METH_VARARGS, "Get source file name, line number and line."}, - { "perf_config_get", __perf_config_get, METH_VARARGS, "Get perf config entry"}, - { NULL, NULL, 0, NULL} -}; - -PyMODINIT_FUNC PyInit_perf_trace_context(void) -{ - static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "perf_trace_context", /* m_name */ - "", /* m_doc */ - -1, /* m_size */ - ContextMethods, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ - }; - PyObject *mod; - - mod = PyModule_Create(&moduledef); - /* Add perf_script_context to the module so it can be imported */ - PyObject_SetAttrString(mod, "perf_script_context", Py_None); - - return mod; -} diff --git a/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/Core.py b/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/Core.py deleted file mode 100644 index 54ace2f6bc36..000000000000 --- a/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/Core.py +++ /dev/null @@ -1,116 +0,0 @@ -# Core.py - Python extension for perf script, core functions -# -# Copyright (C) 2010 by Tom Zanussi -# -# This software may be distributed under the terms of the GNU General -# Public License ("GPL") version 2 as published by the Free Software -# Foundation. - -from collections import defaultdict - -def autodict(): - return defaultdict(autodict) - -flag_fields = autodict() -symbolic_fields = autodict() - -def define_flag_field(event_name, field_name, delim): - flag_fields[event_name][field_name]['delim'] = delim - -def define_flag_value(event_name, field_name, value, field_str): - flag_fields[event_name][field_name]['values'][value] = field_str - -def define_symbolic_field(event_name, field_name): - # nothing to do, really - pass - -def define_symbolic_value(event_name, field_name, value, field_str): - symbolic_fields[event_name][field_name]['values'][value] = field_str - -def flag_str(event_name, field_name, value): - string = "" - - if flag_fields[event_name][field_name]: - print_delim = 0 - for idx in sorted(flag_fields[event_name][field_name]['values']): - if not value and not idx: - string += flag_fields[event_name][field_name]['values'][idx] - break - if idx and (value & idx) == idx: - if print_delim and flag_fields[event_name][field_name]['delim']: - string += " " + flag_fields[event_name][field_name]['delim'] + " " - string += flag_fields[event_name][field_name]['values'][idx] - print_delim = 1 - value &= ~idx - - return string - -def symbol_str(event_name, field_name, value): - string = "" - - if symbolic_fields[event_name][field_name]: - for idx in sorted(symbolic_fields[event_name][field_name]['values']): - if not value and not idx: - string = symbolic_fields[event_name][field_name]['values'][idx] - break - if (value == idx): - string = symbolic_fields[event_name][field_name]['values'][idx] - break - - return string - -trace_flags = { 0x00: "NONE", \ - 0x01: "IRQS_OFF", \ - 0x02: "IRQS_NOSUPPORT", \ - 0x04: "NEED_RESCHED", \ - 0x08: "HARDIRQ", \ - 0x10: "SOFTIRQ" } - -def trace_flag_str(value): - string = "" - print_delim = 0 - - for idx in trace_flags: - if not value and not idx: - string += "NONE" - break - - if idx and (value & idx) == idx: - if print_delim: - string += " | "; - string += trace_flags[idx] - print_delim = 1 - value &= ~idx - - return string - - -def taskState(state): - states = { - 0 : "R", - 1 : "S", - 2 : "D", - 64: "DEAD" - } - - if state not in states: - return "Unknown" - - return states[state] - - -class EventHeaders: - def __init__(self, common_cpu, common_secs, common_nsecs, - common_pid, common_comm, common_callchain): - self.cpu = common_cpu - self.secs = common_secs - self.nsecs = common_nsecs - self.pid = common_pid - self.comm = common_comm - self.callchain = common_callchain - - def ts(self): - return (self.secs * (10 ** 9)) + self.nsecs - - def ts_format(self): - return "%d.%d" % (self.secs, int(self.nsecs / 1000)) diff --git a/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/EventClass.py b/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/EventClass.py deleted file mode 100755 index 21a7a1298094..000000000000 --- a/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/EventClass.py +++ /dev/null @@ -1,97 +0,0 @@ -# EventClass.py -# SPDX-License-Identifier: GPL-2.0 -# -# This is a library defining some events types classes, which could -# be used by other scripts to analyzing the perf samples. -# -# Currently there are just a few classes defined for examples, -# PerfEvent is the base class for all perf event sample, PebsEvent -# is a HW base Intel x86 PEBS event, and user could add more SW/HW -# event classes based on requirements. -from __future__ import print_function - -import struct - -# Event types, user could add more here -EVTYPE_GENERIC = 0 -EVTYPE_PEBS = 1 # Basic PEBS event -EVTYPE_PEBS_LL = 2 # PEBS event with load latency info -EVTYPE_IBS = 3 - -# -# Currently we don't have good way to tell the event type, but by -# the size of raw buffer, raw PEBS event with load latency data's -# size is 176 bytes, while the pure PEBS event's size is 144 bytes. -# -def create_event(name, comm, dso, symbol, raw_buf): - if (len(raw_buf) == 144): - event = PebsEvent(name, comm, dso, symbol, raw_buf) - elif (len(raw_buf) == 176): - event = PebsNHM(name, comm, dso, symbol, raw_buf) - else: - event = PerfEvent(name, comm, dso, symbol, raw_buf) - - return event - -class PerfEvent(object): - event_num = 0 - def __init__(self, name, comm, dso, symbol, raw_buf, ev_type=EVTYPE_GENERIC): - self.name = name - self.comm = comm - self.dso = dso - self.symbol = symbol - self.raw_buf = raw_buf - self.ev_type = ev_type - PerfEvent.event_num += 1 - - def show(self): - print("PMU event: name=%12s, symbol=%24s, comm=%8s, dso=%12s" % - (self.name, self.symbol, self.comm, self.dso)) - -# -# Basic Intel PEBS (Precise Event-based Sampling) event, whose raw buffer -# contains the context info when that event happened: the EFLAGS and -# linear IP info, as well as all the registers. -# -class PebsEvent(PerfEvent): - pebs_num = 0 - def __init__(self, name, comm, dso, symbol, raw_buf, ev_type=EVTYPE_PEBS): - tmp_buf=raw_buf[0:80] - flags, ip, ax, bx, cx, dx, si, di, bp, sp = struct.unpack('QQQQQQQQQQ', tmp_buf) - self.flags = flags - self.ip = ip - self.ax = ax - self.bx = bx - self.cx = cx - self.dx = dx - self.si = si - self.di = di - self.bp = bp - self.sp = sp - - PerfEvent.__init__(self, name, comm, dso, symbol, raw_buf, ev_type) - PebsEvent.pebs_num += 1 - del tmp_buf - -# -# Intel Nehalem and Westmere support PEBS plus Load Latency info which lie -# in the four 64 bit words write after the PEBS data: -# Status: records the IA32_PERF_GLOBAL_STATUS register value -# DLA: Data Linear Address (EIP) -# DSE: Data Source Encoding, where the latency happens, hit or miss -# in L1/L2/L3 or IO operations -# LAT: the actual latency in cycles -# -class PebsNHM(PebsEvent): - pebs_nhm_num = 0 - def __init__(self, name, comm, dso, symbol, raw_buf, ev_type=EVTYPE_PEBS_LL): - tmp_buf=raw_buf[144:176] - status, dla, dse, lat = struct.unpack('QQQQ', tmp_buf) - self.status = status - self.dla = dla - self.dse = dse - self.lat = lat - - PebsEvent.__init__(self, name, comm, dso, symbol, raw_buf, ev_type) - PebsNHM.pebs_nhm_num += 1 - del tmp_buf diff --git a/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/SchedGui.py b/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/SchedGui.py deleted file mode 100644 index cac7b2542ee8..000000000000 --- a/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/SchedGui.py +++ /dev/null @@ -1,184 +0,0 @@ -# SchedGui.py - Python extension for perf script, basic GUI code for -# traces drawing and overview. -# -# Copyright (C) 2010 by Frederic Weisbecker -# -# This software is distributed under the terms of the GNU General -# Public License ("GPL") version 2 as published by the Free Software -# Foundation. - - -try: - import wx -except ImportError: - raise ImportError("You need to install the wxpython lib for this script") - - -class RootFrame(wx.Frame): - Y_OFFSET = 100 - RECT_HEIGHT = 100 - RECT_SPACE = 50 - EVENT_MARKING_WIDTH = 5 - - def __init__(self, sched_tracer, title, parent = None, id = -1): - wx.Frame.__init__(self, parent, id, title) - - (self.screen_width, self.screen_height) = wx.GetDisplaySize() - self.screen_width -= 10 - self.screen_height -= 10 - self.zoom = 0.5 - self.scroll_scale = 20 - self.sched_tracer = sched_tracer - self.sched_tracer.set_root_win(self) - (self.ts_start, self.ts_end) = sched_tracer.interval() - self.update_width_virtual() - self.nr_rects = sched_tracer.nr_rectangles() + 1 - self.height_virtual = RootFrame.Y_OFFSET + (self.nr_rects * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE)) - - # whole window panel - self.panel = wx.Panel(self, size=(self.screen_width, self.screen_height)) - - # scrollable container - self.scroll = wx.ScrolledWindow(self.panel) - self.scroll.SetScrollbars(self.scroll_scale, self.scroll_scale, self.width_virtual / self.scroll_scale, self.height_virtual / self.scroll_scale) - self.scroll.EnableScrolling(True, True) - self.scroll.SetFocus() - - # scrollable drawing area - self.scroll_panel = wx.Panel(self.scroll, size=(self.screen_width - 15, self.screen_height / 2)) - self.scroll_panel.Bind(wx.EVT_PAINT, self.on_paint) - self.scroll_panel.Bind(wx.EVT_KEY_DOWN, self.on_key_press) - self.scroll_panel.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down) - self.scroll.Bind(wx.EVT_PAINT, self.on_paint) - self.scroll.Bind(wx.EVT_KEY_DOWN, self.on_key_press) - self.scroll.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down) - - self.scroll.Fit() - self.Fit() - - self.scroll_panel.SetDimensions(-1, -1, self.width_virtual, self.height_virtual, wx.SIZE_USE_EXISTING) - - self.txt = None - - self.Show(True) - - def us_to_px(self, val): - return val / (10 ** 3) * self.zoom - - def px_to_us(self, val): - return (val / self.zoom) * (10 ** 3) - - def scroll_start(self): - (x, y) = self.scroll.GetViewStart() - return (x * self.scroll_scale, y * self.scroll_scale) - - def scroll_start_us(self): - (x, y) = self.scroll_start() - return self.px_to_us(x) - - def paint_rectangle_zone(self, nr, color, top_color, start, end): - offset_px = self.us_to_px(start - self.ts_start) - width_px = self.us_to_px(end - self.ts_start) - - offset_py = RootFrame.Y_OFFSET + (nr * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE)) - width_py = RootFrame.RECT_HEIGHT - - dc = self.dc - - if top_color is not None: - (r, g, b) = top_color - top_color = wx.Colour(r, g, b) - brush = wx.Brush(top_color, wx.SOLID) - dc.SetBrush(brush) - dc.DrawRectangle(offset_px, offset_py, width_px, RootFrame.EVENT_MARKING_WIDTH) - width_py -= RootFrame.EVENT_MARKING_WIDTH - offset_py += RootFrame.EVENT_MARKING_WIDTH - - (r ,g, b) = color - color = wx.Colour(r, g, b) - brush = wx.Brush(color, wx.SOLID) - dc.SetBrush(brush) - dc.DrawRectangle(offset_px, offset_py, width_px, width_py) - - def update_rectangles(self, dc, start, end): - start += self.ts_start - end += self.ts_start - self.sched_tracer.fill_zone(start, end) - - def on_paint(self, event): - dc = wx.PaintDC(self.scroll_panel) - self.dc = dc - - width = min(self.width_virtual, self.screen_width) - (x, y) = self.scroll_start() - start = self.px_to_us(x) - end = self.px_to_us(x + width) - self.update_rectangles(dc, start, end) - - def rect_from_ypixel(self, y): - y -= RootFrame.Y_OFFSET - rect = y / (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE) - height = y % (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE) - - if rect < 0 or rect > self.nr_rects - 1 or height > RootFrame.RECT_HEIGHT: - return -1 - - return rect - - def update_summary(self, txt): - if self.txt: - self.txt.Destroy() - self.txt = wx.StaticText(self.panel, -1, txt, (0, (self.screen_height / 2) + 50)) - - - def on_mouse_down(self, event): - (x, y) = event.GetPositionTuple() - rect = self.rect_from_ypixel(y) - if rect == -1: - return - - t = self.px_to_us(x) + self.ts_start - - self.sched_tracer.mouse_down(rect, t) - - - def update_width_virtual(self): - self.width_virtual = self.us_to_px(self.ts_end - self.ts_start) - - def __zoom(self, x): - self.update_width_virtual() - (xpos, ypos) = self.scroll.GetViewStart() - xpos = self.us_to_px(x) / self.scroll_scale - self.scroll.SetScrollbars(self.scroll_scale, self.scroll_scale, self.width_virtual / self.scroll_scale, self.height_virtual / self.scroll_scale, xpos, ypos) - self.Refresh() - - def zoom_in(self): - x = self.scroll_start_us() - self.zoom *= 2 - self.__zoom(x) - - def zoom_out(self): - x = self.scroll_start_us() - self.zoom /= 2 - self.__zoom(x) - - - def on_key_press(self, event): - key = event.GetRawKeyCode() - if key == ord("+"): - self.zoom_in() - return - if key == ord("-"): - self.zoom_out() - return - - key = event.GetKeyCode() - (x, y) = self.scroll.GetViewStart() - if key == wx.WXK_RIGHT: - self.scroll.Scroll(x + 1, y) - elif key == wx.WXK_LEFT: - self.scroll.Scroll(x - 1, y) - elif key == wx.WXK_DOWN: - self.scroll.Scroll(x, y + 1) - elif key == wx.WXK_UP: - self.scroll.Scroll(x, y - 1) diff --git a/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/Util.py b/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/Util.py deleted file mode 100644 index b75d31858e54..000000000000 --- a/tools/perf/scripts/python/Perf-Trace-Util/lib/Perf/Trace/Util.py +++ /dev/null @@ -1,92 +0,0 @@ -# Util.py - Python extension for perf script, miscellaneous utility code -# -# Copyright (C) 2010 by Tom Zanussi -# -# This software may be distributed under the terms of the GNU General -# Public License ("GPL") version 2 as published by the Free Software -# Foundation. -from __future__ import print_function - -import errno, os - -FUTEX_WAIT = 0 -FUTEX_WAKE = 1 -FUTEX_PRIVATE_FLAG = 128 -FUTEX_CLOCK_REALTIME = 256 -FUTEX_CMD_MASK = ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME) - -NSECS_PER_SEC = 1000000000 - -def avg(total, n): - return total / n - -def nsecs(secs, nsecs): - return secs * NSECS_PER_SEC + nsecs - -def nsecs_secs(nsecs): - return nsecs / NSECS_PER_SEC - -def nsecs_nsecs(nsecs): - return nsecs % NSECS_PER_SEC - -def nsecs_str(nsecs): - str = "%5u.%09u" % (nsecs_secs(nsecs), nsecs_nsecs(nsecs)), - return str - -def add_stats(dict, key, value): - if key not in dict: - dict[key] = (value, value, value, 1) - else: - min, max, avg, count = dict[key] - if value < min: - min = value - if value > max: - max = value - avg = (avg + value) / 2 - dict[key] = (min, max, avg, count + 1) - -def clear_term(): - print("\x1b[H\x1b[2J") - -audit_package_warned = False - -try: - import audit - machine_to_id = { - 'x86_64': audit.MACH_86_64, - 'aarch64': audit.MACH_AARCH64, - 'alpha' : audit.MACH_ALPHA, - 'ia64' : audit.MACH_IA64, - 'ppc' : audit.MACH_PPC, - 'ppc64' : audit.MACH_PPC64, - 'ppc64le' : audit.MACH_PPC64LE, - 's390' : audit.MACH_S390, - 's390x' : audit.MACH_S390X, - 'i386' : audit.MACH_X86, - 'i586' : audit.MACH_X86, - 'i686' : audit.MACH_X86, - } - try: - machine_to_id['armeb'] = audit.MACH_ARMEB - except: - pass - machine_id = machine_to_id[os.uname()[4]] -except: - if not audit_package_warned: - audit_package_warned = True - print("Install the python-audit package to get syscall names.\n" - "For example:\n # apt-get install python3-audit (Ubuntu)" - "\n # yum install python3-audit (Fedora)" - "\n etc.\n") - -def syscall_name(id): - try: - return audit.audit_syscall_to_name(id, machine_id) - except: - return str(id) - -def strerror(nr): - try: - return errno.errorcode[abs(nr)] - except: - return "Unknown %d errno" % nr diff --git a/tools/perf/scripts/python/arm-cs-trace-disasm.py b/tools/perf/scripts/python/arm-cs-trace-disasm.py deleted file mode 100755 index 42579f858684..000000000000 --- a/tools/perf/scripts/python/arm-cs-trace-disasm.py +++ /dev/null @@ -1,356 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -# arm-cs-trace-disasm.py: ARM CoreSight Trace Dump With Disassember -# -# Author: Tor Jeremiassen -# Mathieu Poirier -# Leo Yan -# Al Grant - -from __future__ import print_function -import os -from os import path -import re -from subprocess import * -import argparse -import platform - -from perf_trace_context import perf_sample_srccode, perf_config_get - -# Below are some example commands for using this script. -# Note a --kcore recording is required for accurate decode -# due to the alternatives patching mechanism. In addition to this, -# source line info comes from Perf, and when using kcore there is -# no debug info. The following lists the supported features in each mode: -# -# +-----------+-----------------+------------------+------------------+ -# | Recording | Accurate decode | Source line dump | Disassembly dump | -# +-----------+-----------------+------------------+------------------+ -# | --kcore | yes | no | yes | -# | normal | no | yes (inaccurate) | yes (inaccurate) | -# +-----------+-----------------+------------------+------------------+ -# -# Output disassembly with objdump and auto detect vmlinux -# (when running on same machine.): -# perf script --itrace=b -s scripts/python/arm-cs-trace-disasm.py \ -# -- -d -# -# Output disassembly with llvm-objdump: -# perf script --itrace=b -s scripts/python/arm-cs-trace-disasm.py \ -# -- -d llvm-objdump-11 -k path/to/vmlinux -# -# Output accurate disassembly by passing kcore to script: -# perf script --itrace=b -s scripts/python/arm-cs-trace-disasm.py \ -# -- -d -k perf.data/kcore_dir/kcore -# -# Output only source line and symbols: -# perf script --itrace=b -s scripts/python/arm-cs-trace-disasm.py - -def default_objdump(): - config = perf_config_get("annotate.objdump") - return config if config else "objdump" - -# Command line parsing. -def int_arg(v): - v = int(v) - if v < 0: - raise argparse.ArgumentTypeError("Argument must be a positive integer") - return v - -args = argparse.ArgumentParser() -args.add_argument("-k", "--vmlinux", - help="Set path to vmlinux or kcore file. Omit to autodetect if running on same machine") -args.add_argument("-d", "--objdump", nargs="?", const=default_objdump(), - help="Show disassembly. Can also be used to change the objdump path"), -args.add_argument("-v", "--verbose", action="store_true", help="Enable debugging log") -args.add_argument("--start-time", type=int_arg, help="Monotonic clock time of sample to start from. " - "See 'time' field on samples in -v mode.") -args.add_argument("--stop-time", type=int_arg, help="Monotonic clock time of sample to stop at. " - "See 'time' field on samples in -v mode.") -args.add_argument("--start-sample", type=int_arg, help="Index of sample to start from. " - "See 'index' field on samples in -v mode.") -args.add_argument("--stop-sample", type=int_arg, help="Index of sample to stop at. " - "See 'index' field on samples in -v mode.") - -options = args.parse_args() -if (options.start_time and options.stop_time and - options.start_time >= options.stop_time): - print("--start-time must less than --stop-time") - exit(2) -if (options.start_sample and options.stop_sample and - options.start_sample >= options.stop_sample): - print("--start-sample must less than --stop-sample") - exit(2) - -# Initialize global dicts and regular expression -disasm_cache = dict() -cpu_data = dict() -disasm_re = re.compile(r"^\s*([0-9a-fA-F]+):") -disasm_func_re = re.compile(r"^\s*([0-9a-fA-F]+)\s.*:") -cache_size = 64*1024 -sample_idx = -1 - -glb_source_file_name = None -glb_line_number = None -glb_dso = None - -kver = platform.release() -vmlinux_paths = [ - f"/usr/lib/debug/boot/vmlinux-{kver}.debug", - f"/usr/lib/debug/lib/modules/{kver}/vmlinux", - f"/lib/modules/{kver}/build/vmlinux", - f"/usr/lib/debug/boot/vmlinux-{kver}", - f"/boot/vmlinux-{kver}", - f"/boot/vmlinux", - f"vmlinux" -] - -def get_optional(perf_dict, field): - if field in perf_dict: - return perf_dict[field] - return "[unknown]" - -def get_offset(perf_dict, field): - if field in perf_dict: - return "+%#x" % perf_dict[field] - return "" - -def find_vmlinux(): - if hasattr(find_vmlinux, "path"): - return find_vmlinux.path - - for v in vmlinux_paths: - if os.access(v, os.R_OK): - find_vmlinux.path = v - break - else: - find_vmlinux.path = None - - return find_vmlinux.path - -def get_dso_file_path(dso_name, dso_build_id): - if (dso_name == "[kernel.kallsyms]" or dso_name == "vmlinux"): - if (options.vmlinux): - return options.vmlinux; - else: - return find_vmlinux() if find_vmlinux() else dso_name - - if (dso_name == "[vdso]") : - append = "/vdso" - else: - append = "/elf" - - dso_path = os.environ['PERF_BUILDID_DIR'] + "/" + dso_name + "/" + dso_build_id + append; - # Replace duplicate slash chars to single slash char - dso_path = dso_path.replace('//', '/', 1) - return dso_path - -def read_disam(dso_fname, dso_start, start_addr, stop_addr): - addr_range = str(start_addr) + ":" + str(stop_addr) + ":" + dso_fname - - # Don't let the cache get too big, clear it when it hits max size - if (len(disasm_cache) > cache_size): - disasm_cache.clear(); - - if addr_range in disasm_cache: - disasm_output = disasm_cache[addr_range]; - else: - start_addr = start_addr - dso_start; - stop_addr = stop_addr - dso_start; - disasm = [ options.objdump, "-d", "-z", - "--start-address="+format(start_addr,"#x"), - "--stop-address="+format(stop_addr,"#x") ] - disasm += [ dso_fname ] - disasm_output = check_output(disasm).decode('utf-8').split('\n') - disasm_cache[addr_range] = disasm_output - - return disasm_output - -def print_disam(dso_fname, dso_start, start_addr, stop_addr): - for line in read_disam(dso_fname, dso_start, start_addr, stop_addr): - m = disasm_func_re.search(line) - if m is None: - m = disasm_re.search(line) - if m is None: - continue - print("\t" + line) - -def print_sample(sample): - print("Sample = { cpu: %04d addr: 0x%016x phys_addr: 0x%016x ip: 0x%016x " \ - "pid: %d tid: %d period: %d time: %d index: %d}" % \ - (sample['cpu'], sample['addr'], sample['phys_addr'], \ - sample['ip'], sample['pid'], sample['tid'], \ - sample['period'], sample['time'], sample_idx)) - -def trace_begin(): - print('ARM CoreSight Trace Data Assembler Dump') - -def trace_end(): - print('End') - -def trace_unhandled(event_name, context, event_fields_dict): - print(' '.join(['%s=%s'%(k,str(v))for k,v in sorted(event_fields_dict.items())])) - -def common_start_str(comm, sample): - sec = int(sample["time"] / 1000000000) - ns = sample["time"] % 1000000000 - cpu = sample["cpu"] - pid = sample["pid"] - tid = sample["tid"] - return "%16s %5u/%-5u [%04u] %9u.%09u " % (comm, pid, tid, cpu, sec, ns) - -# This code is copied from intel-pt-events.py for printing source code -# line and symbols. -def print_srccode(comm, param_dict, sample, symbol, dso): - ip = sample["ip"] - if symbol == "[unknown]": - start_str = common_start_str(comm, sample) + ("%x" % ip).rjust(16).ljust(40) - else: - offs = get_offset(param_dict, "symoff") - start_str = common_start_str(comm, sample) + (symbol + offs).ljust(40) - - global glb_source_file_name - global glb_line_number - global glb_dso - - source_file_name, line_number, source_line = perf_sample_srccode(perf_script_context) - if source_file_name: - if glb_line_number == line_number and glb_source_file_name == source_file_name: - src_str = "" - else: - if len(source_file_name) > 40: - src_file = ("..." + source_file_name[-37:]) + " " - else: - src_file = source_file_name.ljust(41) - - if source_line is None: - src_str = src_file + str(line_number).rjust(4) + " " - else: - src_str = src_file + str(line_number).rjust(4) + " " + source_line - glb_dso = None - elif dso == glb_dso: - src_str = "" - else: - src_str = dso - glb_dso = dso - - glb_line_number = line_number - glb_source_file_name = source_file_name - - print(start_str, src_str) - -def process_event(param_dict): - global cache_size - global options - global sample_idx - - sample = param_dict["sample"] - comm = param_dict["comm"] - - name = param_dict["ev_name"] - dso = get_optional(param_dict, "dso") - dso_bid = get_optional(param_dict, "dso_bid") - dso_start = get_optional(param_dict, "dso_map_start") - dso_end = get_optional(param_dict, "dso_map_end") - symbol = get_optional(param_dict, "symbol") - map_pgoff = get_optional(param_dict, "map_pgoff") - # check for valid map offset - if (str(map_pgoff) == '[unknown]'): - map_pgoff = 0 - - cpu = sample["cpu"] - ip = sample["ip"] - addr = sample["addr"] - - sample_idx += 1 - - if (options.start_time and sample["time"] < options.start_time): - return - if (options.stop_time and sample["time"] > options.stop_time): - exit(0) - if (options.start_sample and sample_idx < options.start_sample): - return - if (options.stop_sample and sample_idx > options.stop_sample): - exit(0) - - if (options.verbose == True): - print("Event type: %s" % name) - print_sample(sample) - - # Initialize CPU data if it's empty, and directly return back - # if this is the first tracing event for this CPU. - if (cpu_data.get(str(cpu) + 'addr') == None): - cpu_data[str(cpu) + 'addr'] = addr - return - - # If cannot find dso so cannot dump assembler, bail out - if (dso == '[unknown]'): - return - - # Validate dso start and end addresses - if ((dso_start == '[unknown]') or (dso_end == '[unknown]')): - print("Failed to find valid dso map for dso %s" % dso) - return - - if (name[0:12] == "instructions"): - print_srccode(comm, param_dict, sample, symbol, dso) - return - - # Don't proceed if this event is not a branch sample, . - if (name[0:8] != "branches"): - return - - # The format for packet is: - # - # +------------+------------+------------+ - # sample_prev: | addr | ip | cpu | - # +------------+------------+------------+ - # sample_next: | addr | ip | cpu | - # +------------+------------+------------+ - # - # We need to combine the two continuous packets to get the instruction - # range for sample_prev::cpu: - # - # [ sample_prev::addr .. sample_next::ip ] - # - # For this purose, sample_prev::addr is stored into cpu_data structure - # and read back for 'start_addr' when the new packet comes, and we need - # to use sample_next::ip to calculate 'stop_addr', plusing extra 4 for - # 'stop_addr' is for the sake of objdump so the final assembler dump can - # include last instruction for sample_next::ip. - start_addr = cpu_data[str(cpu) + 'addr'] - stop_addr = ip + 4 - - # Record for previous sample packet - cpu_data[str(cpu) + 'addr'] = addr - - # Filter out zero start_address. Optionally identify CS_ETM_TRACE_ON packet - if (start_addr == 0): - if ((stop_addr == 4) and (options.verbose == True)): - print("CPU%d: CS_ETM_TRACE_ON packet is inserted" % cpu) - return - - if (start_addr < int(dso_start) or start_addr > int(dso_end)): - print("Start address 0x%x is out of range [ 0x%x .. 0x%x ] for dso %s" % (start_addr, int(dso_start), int(dso_end), dso)) - return - - if (stop_addr < int(dso_start) or stop_addr > int(dso_end)): - print("Stop address 0x%x is out of range [ 0x%x .. 0x%x ] for dso %s" % (stop_addr, int(dso_start), int(dso_end), dso)) - return - - if (options.objdump != None): - # It doesn't need to decrease virtual memory offset for disassembly - # for kernel dso and executable file dso, so in this case we set - # vm_start to zero. - if (dso == "[kernel.kallsyms]" or dso_start == 0x400000): - dso_vm_start = 0 - map_pgoff = 0 - else: - dso_vm_start = int(dso_start) - - dso_fname = get_dso_file_path(dso, dso_bid) - if path.exists(dso_fname): - print_disam(dso_fname, dso_vm_start, start_addr + map_pgoff, stop_addr + map_pgoff) - else: - print("Failed to find dso %s for address range [ 0x%x .. 0x%x ]" % (dso, start_addr + map_pgoff, stop_addr + map_pgoff)) - - print_srccode(comm, param_dict, sample, symbol, dso) diff --git a/tools/perf/scripts/python/bin/compaction-times-record b/tools/perf/scripts/python/bin/compaction-times-record deleted file mode 100644 index 6edcd40e14e8..000000000000 --- a/tools/perf/scripts/python/bin/compaction-times-record +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -perf record -e compaction:mm_compaction_begin -e compaction:mm_compaction_end -e compaction:mm_compaction_migratepages -e compaction:mm_compaction_isolate_migratepages -e compaction:mm_compaction_isolate_freepages $@ diff --git a/tools/perf/scripts/python/bin/compaction-times-report b/tools/perf/scripts/python/bin/compaction-times-report deleted file mode 100644 index 3dc13897cfde..000000000000 --- a/tools/perf/scripts/python/bin/compaction-times-report +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -#description: display time taken by mm compaction -#args: [-h] [-u] [-p|-pv] [-t | [-m] [-fs] [-ms]] [pid|pid-range|comm-regex] -perf script -s "$PERF_EXEC_PATH"/scripts/python/compaction-times.py $@ diff --git a/tools/perf/scripts/python/bin/event_analyzing_sample-record b/tools/perf/scripts/python/bin/event_analyzing_sample-record deleted file mode 100644 index 5ce652dabd02..000000000000 --- a/tools/perf/scripts/python/bin/event_analyzing_sample-record +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# -# event_analyzing_sample.py can cover all type of perf samples including -# the tracepoints, so no special record requirements, just record what -# you want to analyze. -# -perf record $@ diff --git a/tools/perf/scripts/python/bin/event_analyzing_sample-report b/tools/perf/scripts/python/bin/event_analyzing_sample-report deleted file mode 100644 index 0941fc94e158..000000000000 --- a/tools/perf/scripts/python/bin/event_analyzing_sample-report +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# description: analyze all perf samples -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/event_analyzing_sample.py diff --git a/tools/perf/scripts/python/bin/export-to-postgresql-record b/tools/perf/scripts/python/bin/export-to-postgresql-record deleted file mode 100644 index 221d66e05713..000000000000 --- a/tools/perf/scripts/python/bin/export-to-postgresql-record +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# -# export perf data to a postgresql database. Can cover -# perf ip samples (excluding the tracepoints). No special -# record requirements, just record what you want to export. -# -perf record $@ diff --git a/tools/perf/scripts/python/bin/export-to-postgresql-report b/tools/perf/scripts/python/bin/export-to-postgresql-report deleted file mode 100644 index cd335b6e2a01..000000000000 --- a/tools/perf/scripts/python/bin/export-to-postgresql-report +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# description: export perf data to a postgresql database -# args: [database name] [columns] [calls] -n_args=0 -for i in "$@" -do - if expr match "$i" "-" > /dev/null ; then - break - fi - n_args=$(( $n_args + 1 )) -done -if [ "$n_args" -gt 3 ] ; then - echo "usage: export-to-postgresql-report [database name] [columns] [calls]" - exit -fi -if [ "$n_args" -gt 2 ] ; then - dbname=$1 - columns=$2 - calls=$3 - shift 3 -elif [ "$n_args" -gt 1 ] ; then - dbname=$1 - columns=$2 - shift 2 -elif [ "$n_args" -gt 0 ] ; then - dbname=$1 - shift -fi -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/export-to-postgresql.py $dbname $columns $calls diff --git a/tools/perf/scripts/python/bin/export-to-sqlite-record b/tools/perf/scripts/python/bin/export-to-sqlite-record deleted file mode 100644 index 070204fd6d00..000000000000 --- a/tools/perf/scripts/python/bin/export-to-sqlite-record +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# -# export perf data to a sqlite3 database. Can cover -# perf ip samples (excluding the tracepoints). No special -# record requirements, just record what you want to export. -# -perf record $@ diff --git a/tools/perf/scripts/python/bin/export-to-sqlite-report b/tools/perf/scripts/python/bin/export-to-sqlite-report deleted file mode 100644 index 5ff6033e70ba..000000000000 --- a/tools/perf/scripts/python/bin/export-to-sqlite-report +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# description: export perf data to a sqlite3 database -# args: [database name] [columns] [calls] -n_args=0 -for i in "$@" -do - if expr match "$i" "-" > /dev/null ; then - break - fi - n_args=$(( $n_args + 1 )) -done -if [ "$n_args" -gt 3 ] ; then - echo "usage: export-to-sqlite-report [database name] [columns] [calls]" - exit -fi -if [ "$n_args" -gt 2 ] ; then - dbname=$1 - columns=$2 - calls=$3 - shift 3 -elif [ "$n_args" -gt 1 ] ; then - dbname=$1 - columns=$2 - shift 2 -elif [ "$n_args" -gt 0 ] ; then - dbname=$1 - shift -fi -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/export-to-sqlite.py $dbname $columns $calls diff --git a/tools/perf/scripts/python/bin/failed-syscalls-by-pid-record b/tools/perf/scripts/python/bin/failed-syscalls-by-pid-record deleted file mode 100644 index 74685f318379..000000000000 --- a/tools/perf/scripts/python/bin/failed-syscalls-by-pid-record +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -(perf record -e raw_syscalls:sys_exit $@ || \ - perf record -e syscalls:sys_exit $@) 2> /dev/null diff --git a/tools/perf/scripts/python/bin/failed-syscalls-by-pid-report b/tools/perf/scripts/python/bin/failed-syscalls-by-pid-report deleted file mode 100644 index fda5096d0cbf..000000000000 --- a/tools/perf/scripts/python/bin/failed-syscalls-by-pid-report +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# description: system-wide failed syscalls, by pid -# args: [comm] -if [ $# -gt 0 ] ; then - if ! expr match "$1" "-" > /dev/null ; then - comm=$1 - shift - fi -fi -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/failed-syscalls-by-pid.py $comm diff --git a/tools/perf/scripts/python/bin/flamegraph-record b/tools/perf/scripts/python/bin/flamegraph-record deleted file mode 100755 index 7df5a19c0163..000000000000 --- a/tools/perf/scripts/python/bin/flamegraph-record +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -perf record -g "$@" diff --git a/tools/perf/scripts/python/bin/flamegraph-report b/tools/perf/scripts/python/bin/flamegraph-report deleted file mode 100755 index 453a6918afbe..000000000000 --- a/tools/perf/scripts/python/bin/flamegraph-report +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# description: create flame graphs -perf script -s "$PERF_EXEC_PATH"/scripts/python/flamegraph.py "$@" diff --git a/tools/perf/scripts/python/bin/futex-contention-record b/tools/perf/scripts/python/bin/futex-contention-record deleted file mode 100644 index b1495c9a9b20..000000000000 --- a/tools/perf/scripts/python/bin/futex-contention-record +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -perf record -e syscalls:sys_enter_futex -e syscalls:sys_exit_futex $@ diff --git a/tools/perf/scripts/python/bin/futex-contention-report b/tools/perf/scripts/python/bin/futex-contention-report deleted file mode 100644 index 6c44271091ab..000000000000 --- a/tools/perf/scripts/python/bin/futex-contention-report +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# description: futext contention measurement - -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/futex-contention.py diff --git a/tools/perf/scripts/python/bin/gecko-record b/tools/perf/scripts/python/bin/gecko-record deleted file mode 100644 index f0d1aa55f171..000000000000 --- a/tools/perf/scripts/python/bin/gecko-record +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -perf record -F 99 -g "$@" diff --git a/tools/perf/scripts/python/bin/gecko-report b/tools/perf/scripts/python/bin/gecko-report deleted file mode 100755 index 1867ec8d9757..000000000000 --- a/tools/perf/scripts/python/bin/gecko-report +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# description: create firefox gecko profile json format from perf.data -if [ "$*" = "-i -" ]; then -perf script -s "$PERF_EXEC_PATH"/scripts/python/gecko.py -else -perf script -s "$PERF_EXEC_PATH"/scripts/python/gecko.py -- "$@" -fi diff --git a/tools/perf/scripts/python/bin/intel-pt-events-record b/tools/perf/scripts/python/bin/intel-pt-events-record deleted file mode 100644 index 6b9877cfe23e..000000000000 --- a/tools/perf/scripts/python/bin/intel-pt-events-record +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# -# print Intel PT Events including Power Events and PTWRITE. The intel_pt PMU -# event needs to be specified with appropriate config terms. -# -if ! echo "$@" | grep -q intel_pt ; then - echo "Options must include the Intel PT event e.g. -e intel_pt/pwr_evt,ptw/" - echo "and for power events it probably needs to be system wide i.e. -a option" - echo "For example: -a -e intel_pt/pwr_evt,branch=0/ sleep 1" - exit 1 -fi -perf record $@ diff --git a/tools/perf/scripts/python/bin/intel-pt-events-report b/tools/perf/scripts/python/bin/intel-pt-events-report deleted file mode 100644 index beeac3fde9db..000000000000 --- a/tools/perf/scripts/python/bin/intel-pt-events-report +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# description: print Intel PT Events including Power Events and PTWRITE -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/intel-pt-events.py diff --git a/tools/perf/scripts/python/bin/mem-phys-addr-record b/tools/perf/scripts/python/bin/mem-phys-addr-record deleted file mode 100644 index 5a875122a904..000000000000 --- a/tools/perf/scripts/python/bin/mem-phys-addr-record +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# -# Profiling physical memory by all retired load instructions/uops event -# MEM_INST_RETIRED.ALL_LOADS or MEM_UOPS_RETIRED.ALL_LOADS -# - -load=`perf list | grep mem_inst_retired.all_loads` -if [ -z "$load" ]; then - load=`perf list | grep mem_uops_retired.all_loads` -fi -if [ -z "$load" ]; then - echo "There is no event to count all retired load instructions/uops." - exit 1 -fi - -arg=$(echo $load | tr -d ' ') -arg="$arg:P" -perf record --phys-data -e $arg $@ diff --git a/tools/perf/scripts/python/bin/mem-phys-addr-report b/tools/perf/scripts/python/bin/mem-phys-addr-report deleted file mode 100644 index 3f2b847e2eab..000000000000 --- a/tools/perf/scripts/python/bin/mem-phys-addr-report +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# description: resolve physical address samples -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/mem-phys-addr.py diff --git a/tools/perf/scripts/python/bin/net_dropmonitor-record b/tools/perf/scripts/python/bin/net_dropmonitor-record deleted file mode 100755 index 423fb81dadae..000000000000 --- a/tools/perf/scripts/python/bin/net_dropmonitor-record +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -perf record -e skb:kfree_skb $@ diff --git a/tools/perf/scripts/python/bin/net_dropmonitor-report b/tools/perf/scripts/python/bin/net_dropmonitor-report deleted file mode 100755 index 8d698f5a06aa..000000000000 --- a/tools/perf/scripts/python/bin/net_dropmonitor-report +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# description: display a table of dropped frames - -perf script -s "$PERF_EXEC_PATH"/scripts/python/net_dropmonitor.py $@ diff --git a/tools/perf/scripts/python/bin/netdev-times-record b/tools/perf/scripts/python/bin/netdev-times-record deleted file mode 100644 index 558754b840a9..000000000000 --- a/tools/perf/scripts/python/bin/netdev-times-record +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -perf record -e net:net_dev_xmit -e net:net_dev_queue \ - -e net:netif_receive_skb -e net:netif_rx \ - -e skb:consume_skb -e skb:kfree_skb \ - -e skb:skb_copy_datagram_iovec -e napi:napi_poll \ - -e irq:irq_handler_entry -e irq:irq_handler_exit \ - -e irq:softirq_entry -e irq:softirq_exit \ - -e irq:softirq_raise $@ diff --git a/tools/perf/scripts/python/bin/netdev-times-report b/tools/perf/scripts/python/bin/netdev-times-report deleted file mode 100644 index 8f759291da86..000000000000 --- a/tools/perf/scripts/python/bin/netdev-times-report +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# description: display a process of packet and processing time -# args: [tx] [rx] [dev=] [debug] - -perf script -s "$PERF_EXEC_PATH"/scripts/python/netdev-times.py $@ diff --git a/tools/perf/scripts/python/bin/powerpc-hcalls-record b/tools/perf/scripts/python/bin/powerpc-hcalls-record deleted file mode 100644 index b7402aa9147d..000000000000 --- a/tools/perf/scripts/python/bin/powerpc-hcalls-record +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -perf record -e "{powerpc:hcall_entry,powerpc:hcall_exit}" $@ diff --git a/tools/perf/scripts/python/bin/powerpc-hcalls-report b/tools/perf/scripts/python/bin/powerpc-hcalls-report deleted file mode 100644 index dd32ad7465f6..000000000000 --- a/tools/perf/scripts/python/bin/powerpc-hcalls-report +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/powerpc-hcalls.py diff --git a/tools/perf/scripts/python/bin/sched-migration-record b/tools/perf/scripts/python/bin/sched-migration-record deleted file mode 100644 index 7493fddbe995..000000000000 --- a/tools/perf/scripts/python/bin/sched-migration-record +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -perf record -m 16384 -e sched:sched_wakeup -e sched:sched_wakeup_new -e sched:sched_switch -e sched:sched_migrate_task $@ diff --git a/tools/perf/scripts/python/bin/sched-migration-report b/tools/perf/scripts/python/bin/sched-migration-report deleted file mode 100644 index 68b037a1849b..000000000000 --- a/tools/perf/scripts/python/bin/sched-migration-report +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# description: sched migration overview -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/sched-migration.py diff --git a/tools/perf/scripts/python/bin/sctop-record b/tools/perf/scripts/python/bin/sctop-record deleted file mode 100644 index d6940841e54f..000000000000 --- a/tools/perf/scripts/python/bin/sctop-record +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -(perf record -e raw_syscalls:sys_enter $@ || \ - perf record -e syscalls:sys_enter $@) 2> /dev/null diff --git a/tools/perf/scripts/python/bin/sctop-report b/tools/perf/scripts/python/bin/sctop-report deleted file mode 100644 index c32db294124d..000000000000 --- a/tools/perf/scripts/python/bin/sctop-report +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# description: syscall top -# args: [comm] [interval] -n_args=0 -for i in "$@" -do - if expr match "$i" "-" > /dev/null ; then - break - fi - n_args=$(( $n_args + 1 )) -done -if [ "$n_args" -gt 2 ] ; then - echo "usage: sctop-report [comm] [interval]" - exit -fi -if [ "$n_args" -gt 1 ] ; then - comm=$1 - interval=$2 - shift 2 -elif [ "$n_args" -gt 0 ] ; then - interval=$1 - shift -fi -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/sctop.py $comm $interval diff --git a/tools/perf/scripts/python/bin/stackcollapse-record b/tools/perf/scripts/python/bin/stackcollapse-record deleted file mode 100755 index 9d8f9f0f3a17..000000000000 --- a/tools/perf/scripts/python/bin/stackcollapse-record +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -# -# stackcollapse.py can cover all type of perf samples including -# the tracepoints, so no special record requirements, just record what -# you want to analyze. -# -perf record "$@" diff --git a/tools/perf/scripts/python/bin/stackcollapse-report b/tools/perf/scripts/python/bin/stackcollapse-report deleted file mode 100755 index 21a356bd27f6..000000000000 --- a/tools/perf/scripts/python/bin/stackcollapse-report +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -# description: produce callgraphs in short form for scripting use -perf script -s "$PERF_EXEC_PATH"/scripts/python/stackcollapse.py "$@" diff --git a/tools/perf/scripts/python/bin/syscall-counts-by-pid-record b/tools/perf/scripts/python/bin/syscall-counts-by-pid-record deleted file mode 100644 index d6940841e54f..000000000000 --- a/tools/perf/scripts/python/bin/syscall-counts-by-pid-record +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -(perf record -e raw_syscalls:sys_enter $@ || \ - perf record -e syscalls:sys_enter $@) 2> /dev/null diff --git a/tools/perf/scripts/python/bin/syscall-counts-by-pid-report b/tools/perf/scripts/python/bin/syscall-counts-by-pid-report deleted file mode 100644 index 16eb8d65c543..000000000000 --- a/tools/perf/scripts/python/bin/syscall-counts-by-pid-report +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# description: system-wide syscall counts, by pid -# args: [comm] -if [ $# -gt 0 ] ; then - if ! expr match "$1" "-" > /dev/null ; then - comm=$1 - shift - fi -fi -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/syscall-counts-by-pid.py $comm diff --git a/tools/perf/scripts/python/bin/syscall-counts-record b/tools/perf/scripts/python/bin/syscall-counts-record deleted file mode 100644 index d6940841e54f..000000000000 --- a/tools/perf/scripts/python/bin/syscall-counts-record +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -(perf record -e raw_syscalls:sys_enter $@ || \ - perf record -e syscalls:sys_enter $@) 2> /dev/null diff --git a/tools/perf/scripts/python/bin/syscall-counts-report b/tools/perf/scripts/python/bin/syscall-counts-report deleted file mode 100644 index 0f0e9d453bb4..000000000000 --- a/tools/perf/scripts/python/bin/syscall-counts-report +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# description: system-wide syscall counts -# args: [comm] -if [ $# -gt 0 ] ; then - if ! expr match "$1" "-" > /dev/null ; then - comm=$1 - shift - fi -fi -perf script $@ -s "$PERF_EXEC_PATH"/scripts/python/syscall-counts.py $comm diff --git a/tools/perf/scripts/python/bin/task-analyzer-record b/tools/perf/scripts/python/bin/task-analyzer-record deleted file mode 100755 index 0f6b51bb2767..000000000000 --- a/tools/perf/scripts/python/bin/task-analyzer-record +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -perf record -e sched:sched_switch -e sched:sched_migrate_task "$@" diff --git a/tools/perf/scripts/python/bin/task-analyzer-report b/tools/perf/scripts/python/bin/task-analyzer-report deleted file mode 100755 index 4b16a8cc40a0..000000000000 --- a/tools/perf/scripts/python/bin/task-analyzer-report +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -# description: analyze timings of tasks -perf script -s "$PERF_EXEC_PATH"/scripts/python/task-analyzer.py -- "$@" diff --git a/tools/perf/scripts/python/check-perf-trace.py b/tools/perf/scripts/python/check-perf-trace.py deleted file mode 100644 index d2c22954800d..000000000000 --- a/tools/perf/scripts/python/check-perf-trace.py +++ /dev/null @@ -1,84 +0,0 @@ -# perf script event handlers, generated by perf script -g python -# (c) 2010, Tom Zanussi -# Licensed under the terms of the GNU GPL License version 2 -# -# This script tests basic functionality such as flag and symbol -# strings, common_xxx() calls back into perf, begin, end, unhandled -# events, etc. Basically, if this script runs successfully and -# displays expected results, Python scripting support should be ok. - -from __future__ import print_function - -import os -import sys - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from Core import * -from perf_trace_context import * - -unhandled = autodict() - -def trace_begin(): - print("trace_begin") - pass - -def trace_end(): - print_unhandled() - -def irq__softirq_entry(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, vec): - print_header(event_name, common_cpu, common_secs, common_nsecs, - common_pid, common_comm) - - print_uncommon(context) - - print("vec=%s" % (symbol_str("irq__softirq_entry", "vec", vec))) - -def kmem__kmalloc(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, call_site, ptr, bytes_req, bytes_alloc, - gfp_flags): - print_header(event_name, common_cpu, common_secs, common_nsecs, - common_pid, common_comm) - - print_uncommon(context) - - print("call_site=%u, ptr=%u, bytes_req=%u, " - "bytes_alloc=%u, gfp_flags=%s" % - (call_site, ptr, bytes_req, bytes_alloc, - flag_str("kmem__kmalloc", "gfp_flags", gfp_flags))) - -def trace_unhandled(event_name, context, event_fields_dict): - try: - unhandled[event_name] += 1 - except TypeError: - unhandled[event_name] = 1 - -def print_header(event_name, cpu, secs, nsecs, pid, comm): - print("%-20s %5u %05u.%09u %8u %-20s " % - (event_name, cpu, secs, nsecs, pid, comm), - end=' ') - -# print trace fields not included in handler args -def print_uncommon(context): - print("common_preempt_count=%d, common_flags=%s, " - "common_lock_depth=%d, " % - (common_pc(context), trace_flag_str(common_flags(context)), - common_lock_depth(context))) - -def print_unhandled(): - keys = unhandled.keys() - if not keys: - return - - print("\nunhandled events:\n") - - print("%-40s %10s" % ("event", "count")) - print("%-40s %10s" % ("----------------------------------------", - "-----------")) - - for event_name in keys: - print("%-40s %10d\n" % (event_name, unhandled[event_name])) diff --git a/tools/perf/scripts/python/compaction-times.py b/tools/perf/scripts/python/compaction-times.py deleted file mode 100644 index 9401f7c14747..000000000000 --- a/tools/perf/scripts/python/compaction-times.py +++ /dev/null @@ -1,311 +0,0 @@ -# report time spent in compaction -# Licensed under the terms of the GNU GPL License version 2 - -# testing: -# 'echo 1 > /proc/sys/vm/compact_memory' to force compaction of all zones - -import os -import sys -import re - -import signal -signal.signal(signal.SIGPIPE, signal.SIG_DFL) - -usage = "usage: perf script report compaction-times.py -- [-h] [-u] [-p|-pv] [-t | [-m] [-fs] [-ms]] [pid|pid-range|comm-regex]\n" - -class popt: - DISP_DFL = 0 - DISP_PROC = 1 - DISP_PROC_VERBOSE=2 - -class topt: - DISP_TIME = 0 - DISP_MIG = 1 - DISP_ISOLFREE = 2 - DISP_ISOLMIG = 4 - DISP_ALL = 7 - -class comm_filter: - def __init__(self, re): - self.re = re - - def filter(self, pid, comm): - m = self.re.search(comm) - return m == None or m.group() == "" - -class pid_filter: - def __init__(self, low, high): - self.low = (0 if low == "" else int(low)) - self.high = (0 if high == "" else int(high)) - - def filter(self, pid, comm): - return not (pid >= self.low and (self.high == 0 or pid <= self.high)) - -def set_type(t): - global opt_disp - opt_disp = (t if opt_disp == topt.DISP_ALL else opt_disp|t) - -def ns(sec, nsec): - return (sec * 1000000000) + nsec - -def time(ns): - return "%dns" % ns if opt_ns else "%dus" % (round(ns, -3) / 1000) - -class pair: - def __init__(self, aval, bval, alabel = None, blabel = None): - self.alabel = alabel - self.blabel = blabel - self.aval = aval - self.bval = bval - - def __add__(self, rhs): - self.aval += rhs.aval - self.bval += rhs.bval - return self - - def __str__(self): - return "%s=%d %s=%d" % (self.alabel, self.aval, self.blabel, self.bval) - -class cnode: - def __init__(self, ns): - self.ns = ns - self.migrated = pair(0, 0, "moved", "failed") - self.fscan = pair(0,0, "scanned", "isolated") - self.mscan = pair(0,0, "scanned", "isolated") - - def __add__(self, rhs): - self.ns += rhs.ns - self.migrated += rhs.migrated - self.fscan += rhs.fscan - self.mscan += rhs.mscan - return self - - def __str__(self): - prev = 0 - s = "%s " % time(self.ns) - if (opt_disp & topt.DISP_MIG): - s += "migration: %s" % self.migrated - prev = 1 - if (opt_disp & topt.DISP_ISOLFREE): - s += "%sfree_scanner: %s" % (" " if prev else "", self.fscan) - prev = 1 - if (opt_disp & topt.DISP_ISOLMIG): - s += "%smigration_scanner: %s" % (" " if prev else "", self.mscan) - return s - - def complete(self, secs, nsecs): - self.ns = ns(secs, nsecs) - self.ns - - def increment(self, migrated, fscan, mscan): - if (migrated != None): - self.migrated += migrated - if (fscan != None): - self.fscan += fscan - if (mscan != None): - self.mscan += mscan - - -class chead: - heads = {} - val = cnode(0); - fobj = None - - @classmethod - def add_filter(cls, filter): - cls.fobj = filter - - @classmethod - def create_pending(cls, pid, comm, start_secs, start_nsecs): - filtered = 0 - try: - head = cls.heads[pid] - filtered = head.is_filtered() - except KeyError: - if cls.fobj != None: - filtered = cls.fobj.filter(pid, comm) - head = cls.heads[pid] = chead(comm, pid, filtered) - - if not filtered: - head.mark_pending(start_secs, start_nsecs) - - @classmethod - def increment_pending(cls, pid, migrated, fscan, mscan): - head = cls.heads[pid] - if not head.is_filtered(): - if head.is_pending(): - head.do_increment(migrated, fscan, mscan) - else: - sys.stderr.write("missing start compaction event for pid %d\n" % pid) - - @classmethod - def complete_pending(cls, pid, secs, nsecs): - head = cls.heads[pid] - if not head.is_filtered(): - if head.is_pending(): - head.make_complete(secs, nsecs) - else: - sys.stderr.write("missing start compaction event for pid %d\n" % pid) - - @classmethod - def gen(cls): - if opt_proc != popt.DISP_DFL: - for i in cls.heads: - yield cls.heads[i] - - @classmethod - def str(cls): - return cls.val - - def __init__(self, comm, pid, filtered): - self.comm = comm - self.pid = pid - self.val = cnode(0) - self.pending = None - self.filtered = filtered - self.list = [] - - def __add__(self, rhs): - self.ns += rhs.ns - self.val += rhs.val - return self - - def mark_pending(self, secs, nsecs): - self.pending = cnode(ns(secs, nsecs)) - - def do_increment(self, migrated, fscan, mscan): - self.pending.increment(migrated, fscan, mscan) - - def make_complete(self, secs, nsecs): - self.pending.complete(secs, nsecs) - chead.val += self.pending - - if opt_proc != popt.DISP_DFL: - self.val += self.pending - - if opt_proc == popt.DISP_PROC_VERBOSE: - self.list.append(self.pending) - self.pending = None - - def enumerate(self): - if opt_proc == popt.DISP_PROC_VERBOSE and not self.is_filtered(): - for i, pelem in enumerate(self.list): - sys.stdout.write("%d[%s].%d: %s\n" % (self.pid, self.comm, i+1, pelem)) - - def is_pending(self): - return self.pending != None - - def is_filtered(self): - return self.filtered - - def display(self): - if not self.is_filtered(): - sys.stdout.write("%d[%s]: %s\n" % (self.pid, self.comm, self.val)) - - -def trace_end(): - sys.stdout.write("total: %s\n" % chead.str()) - for i in chead.gen(): - i.display(), - i.enumerate() - -def compaction__mm_compaction_migratepages(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, nr_migrated, nr_failed): - - chead.increment_pending(common_pid, - pair(nr_migrated, nr_failed), None, None) - -def compaction__mm_compaction_isolate_freepages(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, start_pfn, end_pfn, nr_scanned, nr_taken): - - chead.increment_pending(common_pid, - None, pair(nr_scanned, nr_taken), None) - -def compaction__mm_compaction_isolate_migratepages(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, start_pfn, end_pfn, nr_scanned, nr_taken): - - chead.increment_pending(common_pid, - None, None, pair(nr_scanned, nr_taken)) - -def compaction__mm_compaction_end(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, zone_start, migrate_start, free_start, zone_end, - sync, status): - - chead.complete_pending(common_pid, common_secs, common_nsecs) - -def compaction__mm_compaction_begin(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, zone_start, migrate_start, free_start, zone_end, - sync): - - chead.create_pending(common_pid, common_comm, common_secs, common_nsecs) - -def pr_help(): - global usage - - sys.stdout.write(usage) - sys.stdout.write("\n") - sys.stdout.write("-h display this help\n") - sys.stdout.write("-p display by process\n") - sys.stdout.write("-pv display by process (verbose)\n") - sys.stdout.write("-t display stall times only\n") - sys.stdout.write("-m display stats for migration\n") - sys.stdout.write("-fs display stats for free scanner\n") - sys.stdout.write("-ms display stats for migration scanner\n") - sys.stdout.write("-u display results in microseconds (default nanoseconds)\n") - - -comm_re = None -pid_re = None -pid_regex = r"^(\d*)-(\d*)$|^(\d*)$" - -opt_proc = popt.DISP_DFL -opt_disp = topt.DISP_ALL - -opt_ns = True - -argc = len(sys.argv) - 1 -if argc >= 1: - pid_re = re.compile(pid_regex) - - for i, opt in enumerate(sys.argv[1:]): - if opt[0] == "-": - if opt == "-h": - pr_help() - exit(0); - elif opt == "-p": - opt_proc = popt.DISP_PROC - elif opt == "-pv": - opt_proc = popt.DISP_PROC_VERBOSE - elif opt == '-u': - opt_ns = False - elif opt == "-t": - set_type(topt.DISP_TIME) - elif opt == "-m": - set_type(topt.DISP_MIG) - elif opt == "-fs": - set_type(topt.DISP_ISOLFREE) - elif opt == "-ms": - set_type(topt.DISP_ISOLMIG) - else: - sys.exit(usage) - - elif i == argc - 1: - m = pid_re.search(opt) - if m != None and m.group() != "": - if m.group(3) != None: - f = pid_filter(m.group(3), m.group(3)) - else: - f = pid_filter(m.group(1), m.group(2)) - else: - try: - comm_re=re.compile(opt) - except: - sys.stderr.write("invalid regex '%s'" % opt) - sys.exit(usage) - f = comm_filter(comm_re) - - chead.add_filter(f) diff --git a/tools/perf/scripts/python/event_analyzing_sample.py b/tools/perf/scripts/python/event_analyzing_sample.py deleted file mode 100644 index aa1e2cfa26a6..000000000000 --- a/tools/perf/scripts/python/event_analyzing_sample.py +++ /dev/null @@ -1,192 +0,0 @@ -# event_analyzing_sample.py: general event handler in python -# SPDX-License-Identifier: GPL-2.0 -# -# Current perf report is already very powerful with the annotation integrated, -# and this script is not trying to be as powerful as perf report, but -# providing end user/developer a flexible way to analyze the events other -# than trace points. -# -# The 2 database related functions in this script just show how to gather -# the basic information, and users can modify and write their own functions -# according to their specific requirement. -# -# The first function "show_general_events" just does a basic grouping for all -# generic events with the help of sqlite, and the 2nd one "show_pebs_ll" is -# for a x86 HW PMU event: PEBS with load latency data. -# - -from __future__ import print_function - -import os -import sys -import math -import struct -import sqlite3 - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from EventClass import * - -# -# If the perf.data has a big number of samples, then the insert operation -# will be very time consuming (about 10+ minutes for 10000 samples) if the -# .db database is on disk. Move the .db file to RAM based FS to speedup -# the handling, which will cut the time down to several seconds. -# -con = sqlite3.connect("/dev/shm/perf.db") -con.isolation_level = None - -def trace_begin(): - print("In trace_begin:\n") - - # - # Will create several tables at the start, pebs_ll is for PEBS data with - # load latency info, while gen_events is for general event. - # - con.execute(""" - create table if not exists gen_events ( - name text, - symbol text, - comm text, - dso text - );""") - con.execute(""" - create table if not exists pebs_ll ( - name text, - symbol text, - comm text, - dso text, - flags integer, - ip integer, - status integer, - dse integer, - dla integer, - lat integer - );""") - -# -# Create and insert event object to a database so that user could -# do more analysis with simple database commands. -# -def process_event(param_dict): - event_attr = param_dict["attr"] - sample = param_dict["sample"] - raw_buf = param_dict["raw_buf"] - comm = param_dict["comm"] - name = param_dict["ev_name"] - - # Symbol and dso info are not always resolved - if ("dso" in param_dict): - dso = param_dict["dso"] - else: - dso = "Unknown_dso" - - if ("symbol" in param_dict): - symbol = param_dict["symbol"] - else: - symbol = "Unknown_symbol" - - # Create the event object and insert it to the right table in database - event = create_event(name, comm, dso, symbol, raw_buf) - insert_db(event) - -def insert_db(event): - if event.ev_type == EVTYPE_GENERIC: - con.execute("insert into gen_events values(?, ?, ?, ?)", - (event.name, event.symbol, event.comm, event.dso)) - elif event.ev_type == EVTYPE_PEBS_LL: - event.ip &= 0x7fffffffffffffff - event.dla &= 0x7fffffffffffffff - con.execute("insert into pebs_ll values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - (event.name, event.symbol, event.comm, event.dso, event.flags, - event.ip, event.status, event.dse, event.dla, event.lat)) - -def trace_end(): - print("In trace_end:\n") - # We show the basic info for the 2 type of event classes - show_general_events() - show_pebs_ll() - con.close() - -# -# As the event number may be very big, so we can't use linear way -# to show the histogram in real number, but use a log2 algorithm. -# - -def num2sym(num): - # Each number will have at least one '#' - snum = '#' * (int)(math.log(num, 2) + 1) - return snum - -def show_general_events(): - - # Check the total record number in the table - count = con.execute("select count(*) from gen_events") - for t in count: - print("There is %d records in gen_events table" % t[0]) - if t[0] == 0: - return - - print("Statistics about the general events grouped by thread/symbol/dso: \n") - - # Group by thread - commq = con.execute("select comm, count(comm) from gen_events group by comm order by -count(comm)") - print("\n%16s %8s %16s\n%s" % ("comm", "number", "histogram", "="*42)) - for row in commq: - print("%16s %8d %s" % (row[0], row[1], num2sym(row[1]))) - - # Group by symbol - print("\n%32s %8s %16s\n%s" % ("symbol", "number", "histogram", "="*58)) - symbolq = con.execute("select symbol, count(symbol) from gen_events group by symbol order by -count(symbol)") - for row in symbolq: - print("%32s %8d %s" % (row[0], row[1], num2sym(row[1]))) - - # Group by dso - print("\n%40s %8s %16s\n%s" % ("dso", "number", "histogram", "="*74)) - dsoq = con.execute("select dso, count(dso) from gen_events group by dso order by -count(dso)") - for row in dsoq: - print("%40s %8d %s" % (row[0], row[1], num2sym(row[1]))) - -# -# This function just shows the basic info, and we could do more with the -# data in the tables, like checking the function parameters when some -# big latency events happen. -# -def show_pebs_ll(): - - count = con.execute("select count(*) from pebs_ll") - for t in count: - print("There is %d records in pebs_ll table" % t[0]) - if t[0] == 0: - return - - print("Statistics about the PEBS Load Latency events grouped by thread/symbol/dse/latency: \n") - - # Group by thread - commq = con.execute("select comm, count(comm) from pebs_ll group by comm order by -count(comm)") - print("\n%16s %8s %16s\n%s" % ("comm", "number", "histogram", "="*42)) - for row in commq: - print("%16s %8d %s" % (row[0], row[1], num2sym(row[1]))) - - # Group by symbol - print("\n%32s %8s %16s\n%s" % ("symbol", "number", "histogram", "="*58)) - symbolq = con.execute("select symbol, count(symbol) from pebs_ll group by symbol order by -count(symbol)") - for row in symbolq: - print("%32s %8d %s" % (row[0], row[1], num2sym(row[1]))) - - # Group by dse - dseq = con.execute("select dse, count(dse) from pebs_ll group by dse order by -count(dse)") - print("\n%32s %8s %16s\n%s" % ("dse", "number", "histogram", "="*58)) - for row in dseq: - print("%32s %8d %s" % (row[0], row[1], num2sym(row[1]))) - - # Group by latency - latq = con.execute("select lat, count(lat) from pebs_ll group by lat order by lat") - print("\n%32s %8s %16s\n%s" % ("latency", "number", "histogram", "="*58)) - for row in latq: - print("%32s %8d %s" % (row[0], row[1], num2sym(row[1]))) - -def trace_unhandled(event_name, context, event_fields_dict): - print (' '.join(['%s=%s'%(k,str(v))for k,v in sorted(event_fields_dict.items())])) diff --git a/tools/perf/scripts/python/export-to-postgresql.py b/tools/perf/scripts/python/export-to-postgresql.py deleted file mode 100644 index 3a6bdcd74e60..000000000000 --- a/tools/perf/scripts/python/export-to-postgresql.py +++ /dev/null @@ -1,1114 +0,0 @@ -# export-to-postgresql.py: export perf data to a postgresql database -# Copyright (c) 2014, Intel Corporation. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms and conditions of the GNU General Public License, -# version 2, as published by the Free Software Foundation. -# -# This program is distributed in the hope it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. - -from __future__ import print_function - -import os -import sys -import struct -import datetime - -# To use this script you will need to have installed package python-pyside which -# provides LGPL-licensed Python bindings for Qt. You will also need the package -# libqt4-sql-psql for Qt postgresql support. -# -# The script assumes postgresql is running on the local machine and that the -# user has postgresql permissions to create databases. Examples of installing -# postgresql and adding such a user are: -# -# fedora: -# -# $ sudo yum install postgresql postgresql-server qt-postgresql -# $ sudo su - postgres -c initdb -# $ sudo service postgresql start -# $ sudo su - postgres -# $ createuser -s # Older versions may not support -s, in which case answer the prompt below: -# Shall the new role be a superuser? (y/n) y -# $ sudo yum install python-pyside -# -# Alternately, to use Python3 and/or pyside 2, one of the following: -# $ sudo yum install python3-pyside -# $ pip install --user PySide2 -# $ pip3 install --user PySide2 -# -# ubuntu: -# -# $ sudo apt-get install postgresql -# $ sudo su - postgres -# $ createuser -s -# $ sudo apt-get install python-pyside.qtsql libqt4-sql-psql -# -# Alternately, to use Python3 and/or pyside 2, one of the following: -# -# $ sudo apt-get install python3-pyside.qtsql libqt4-sql-psql -# $ sudo apt-get install python-pyside2.qtsql libqt5sql5-psql -# $ sudo apt-get install python3-pyside2.qtsql libqt5sql5-psql -# -# An example of using this script with Intel PT: -# -# $ perf record -e intel_pt//u ls -# $ perf script -s ~/libexec/perf-core/scripts/python/export-to-postgresql.py pt_example branches calls -# 2015-05-29 12:49:23.464364 Creating database... -# 2015-05-29 12:49:26.281717 Writing to intermediate files... -# 2015-05-29 12:49:27.190383 Copying to database... -# 2015-05-29 12:49:28.140451 Removing intermediate files... -# 2015-05-29 12:49:28.147451 Adding primary keys -# 2015-05-29 12:49:28.655683 Adding foreign keys -# 2015-05-29 12:49:29.365350 Done -# -# To browse the database, psql can be used e.g. -# -# $ psql pt_example -# pt_example=# select * from samples_view where id < 100; -# pt_example=# \d+ -# pt_example=# \d+ samples_view -# pt_example=# \q -# -# An example of using the database is provided by the script -# exported-sql-viewer.py. Refer to that script for details. -# -# Tables: -# -# The tables largely correspond to perf tools' data structures. They are largely self-explanatory. -# -# samples -# -# 'samples' is the main table. It represents what instruction was executing at a point in time -# when something (a selected event) happened. The memory address is the instruction pointer or 'ip'. -# -# calls -# -# 'calls' represents function calls and is related to 'samples' by 'call_id' and 'return_id'. -# 'calls' is only created when the 'calls' option to this script is specified. -# -# call_paths -# -# 'call_paths' represents all the call stacks. Each 'call' has an associated record in 'call_paths'. -# 'calls_paths' is only created when the 'calls' option to this script is specified. -# -# branch_types -# -# 'branch_types' provides descriptions for each type of branch. -# -# comm_threads -# -# 'comm_threads' shows how 'comms' relates to 'threads'. -# -# comms -# -# 'comms' contains a record for each 'comm' - the name given to the executable that is running. -# -# dsos -# -# 'dsos' contains a record for each executable file or library. -# -# machines -# -# 'machines' can be used to distinguish virtual machines if virtualization is supported. -# -# selected_events -# -# 'selected_events' contains a record for each kind of event that has been sampled. -# -# symbols -# -# 'symbols' contains a record for each symbol. Only symbols that have samples are present. -# -# threads -# -# 'threads' contains a record for each thread. -# -# Views: -# -# Most of the tables have views for more friendly display. The views are: -# -# calls_view -# call_paths_view -# comm_threads_view -# dsos_view -# machines_view -# samples_view -# symbols_view -# threads_view -# -# More examples of browsing the database with psql: -# Note that some of the examples are not the most optimal SQL query. -# Note that call information is only available if the script's 'calls' option has been used. -# -# Top 10 function calls (not aggregated by symbol): -# -# SELECT * FROM calls_view ORDER BY elapsed_time DESC LIMIT 10; -# -# Top 10 function calls (aggregated by symbol): -# -# SELECT symbol_id,(SELECT name FROM symbols WHERE id = symbol_id) AS symbol, -# SUM(elapsed_time) AS tot_elapsed_time,SUM(branch_count) AS tot_branch_count -# FROM calls_view GROUP BY symbol_id ORDER BY tot_elapsed_time DESC LIMIT 10; -# -# Note that the branch count gives a rough estimation of cpu usage, so functions -# that took a long time but have a relatively low branch count must have spent time -# waiting. -# -# Find symbols by pattern matching on part of the name (e.g. names containing 'alloc'): -# -# SELECT * FROM symbols_view WHERE name LIKE '%alloc%'; -# -# Top 10 function calls for a specific symbol (e.g. whose symbol_id is 187): -# -# SELECT * FROM calls_view WHERE symbol_id = 187 ORDER BY elapsed_time DESC LIMIT 10; -# -# Show function calls made by function in the same context (i.e. same call path) (e.g. one with call_path_id 254): -# -# SELECT * FROM calls_view WHERE parent_call_path_id = 254; -# -# Show branches made during a function call (e.g. where call_id is 29357 and return_id is 29370 and tid is 29670) -# -# SELECT * FROM samples_view WHERE id >= 29357 AND id <= 29370 AND tid = 29670 AND event LIKE 'branches%'; -# -# Show transactions: -# -# SELECT * FROM samples_view WHERE event = 'transactions'; -# -# Note transaction start has 'in_tx' true whereas, transaction end has 'in_tx' false. -# Transaction aborts have branch_type_name 'transaction abort' -# -# Show transaction aborts: -# -# SELECT * FROM samples_view WHERE event = 'transactions' AND branch_type_name = 'transaction abort'; -# -# To print a call stack requires walking the call_paths table. For example this python script: -# #!/usr/bin/python2 -# -# import sys -# from PySide.QtSql import * -# -# if __name__ == '__main__': -# if (len(sys.argv) < 3): -# print >> sys.stderr, "Usage is: printcallstack.py " -# raise Exception("Too few arguments") -# dbname = sys.argv[1] -# call_path_id = sys.argv[2] -# db = QSqlDatabase.addDatabase('QPSQL') -# db.setDatabaseName(dbname) -# if not db.open(): -# raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) -# query = QSqlQuery(db) -# print " id ip symbol_id symbol dso_id dso_short_name" -# while call_path_id != 0 and call_path_id != 1: -# ret = query.exec_('SELECT * FROM call_paths_view WHERE id = ' + str(call_path_id)) -# if not ret: -# raise Exception("Query failed: " + query.lastError().text()) -# if not query.next(): -# raise Exception("Query failed") -# print "{0:>6} {1:>10} {2:>9} {3:<30} {4:>6} {5:<30}".format(query.value(0), query.value(1), query.value(2), query.value(3), query.value(4), query.value(5)) -# call_path_id = query.value(6) - -pyside_version_1 = True -if not "pyside-version-1" in sys.argv: - try: - from PySide2.QtSql import * - pyside_version_1 = False - except: - pass - -if pyside_version_1: - from PySide.QtSql import * - -if sys.version_info < (3, 0): - def toserverstr(str): - return str - def toclientstr(str): - return str -else: - # Assume UTF-8 server_encoding and client_encoding - def toserverstr(str): - return bytes(str, "UTF_8") - def toclientstr(str): - return bytes(str, "UTF_8") - -# Need to access PostgreSQL C library directly to use COPY FROM STDIN -from ctypes import * -libpq = CDLL("libpq.so.5") -PQconnectdb = libpq.PQconnectdb -PQconnectdb.restype = c_void_p -PQconnectdb.argtypes = [ c_char_p ] -PQfinish = libpq.PQfinish -PQfinish.argtypes = [ c_void_p ] -PQstatus = libpq.PQstatus -PQstatus.restype = c_int -PQstatus.argtypes = [ c_void_p ] -PQexec = libpq.PQexec -PQexec.restype = c_void_p -PQexec.argtypes = [ c_void_p, c_char_p ] -PQresultStatus = libpq.PQresultStatus -PQresultStatus.restype = c_int -PQresultStatus.argtypes = [ c_void_p ] -PQputCopyData = libpq.PQputCopyData -PQputCopyData.restype = c_int -PQputCopyData.argtypes = [ c_void_p, c_void_p, c_int ] -PQputCopyEnd = libpq.PQputCopyEnd -PQputCopyEnd.restype = c_int -PQputCopyEnd.argtypes = [ c_void_p, c_void_p ] - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -# These perf imports are not used at present -#from perf_trace_context import * -#from Core import * - -perf_db_export_mode = True -perf_db_export_calls = False -perf_db_export_callchains = False - -def printerr(*args, **kw_args): - print(*args, file=sys.stderr, **kw_args) - -def printdate(*args, **kw_args): - print(datetime.datetime.today(), *args, sep=' ', **kw_args) - -def usage(): - printerr("Usage is: export-to-postgresql.py [] [] [] []"); - printerr("where: columns 'all' or 'branches'"); - printerr(" calls 'calls' => create calls and call_paths table"); - printerr(" callchains 'callchains' => create call_paths table"); - printerr(" pyside-version-1 'pyside-version-1' => use pyside version 1"); - raise Exception("Too few or bad arguments") - -if (len(sys.argv) < 2): - usage() - -dbname = sys.argv[1] - -if (len(sys.argv) >= 3): - columns = sys.argv[2] -else: - columns = "all" - -if columns not in ("all", "branches"): - usage() - -branches = (columns == "branches") - -for i in range(3,len(sys.argv)): - if (sys.argv[i] == "calls"): - perf_db_export_calls = True - elif (sys.argv[i] == "callchains"): - perf_db_export_callchains = True - elif (sys.argv[i] == "pyside-version-1"): - pass - else: - usage() - -output_dir_name = os.getcwd() + "/" + dbname + "-perf-data" -os.mkdir(output_dir_name) - -def do_query(q, s): - if (q.exec_(s)): - return - raise Exception("Query failed: " + q.lastError().text()) - -printdate("Creating database...") - -db = QSqlDatabase.addDatabase('QPSQL') -query = QSqlQuery(db) -db.setDatabaseName('postgres') -db.open() -try: - do_query(query, 'CREATE DATABASE ' + dbname) -except: - os.rmdir(output_dir_name) - raise -query.finish() -query.clear() -db.close() - -db.setDatabaseName(dbname) -db.open() - -query = QSqlQuery(db) -do_query(query, 'SET client_min_messages TO WARNING') - -do_query(query, 'CREATE TABLE selected_events (' - 'id bigint NOT NULL,' - 'name varchar(80))') -do_query(query, 'CREATE TABLE machines (' - 'id bigint NOT NULL,' - 'pid integer,' - 'root_dir varchar(4096))') -do_query(query, 'CREATE TABLE threads (' - 'id bigint NOT NULL,' - 'machine_id bigint,' - 'process_id bigint,' - 'pid integer,' - 'tid integer)') -do_query(query, 'CREATE TABLE comms (' - 'id bigint NOT NULL,' - 'comm varchar(16),' - 'c_thread_id bigint,' - 'c_time bigint,' - 'exec_flag boolean)') -do_query(query, 'CREATE TABLE comm_threads (' - 'id bigint NOT NULL,' - 'comm_id bigint,' - 'thread_id bigint)') -do_query(query, 'CREATE TABLE dsos (' - 'id bigint NOT NULL,' - 'machine_id bigint,' - 'short_name varchar(256),' - 'long_name varchar(4096),' - 'build_id varchar(64))') -do_query(query, 'CREATE TABLE symbols (' - 'id bigint NOT NULL,' - 'dso_id bigint,' - 'sym_start bigint,' - 'sym_end bigint,' - 'binding integer,' - 'name varchar(2048))') -do_query(query, 'CREATE TABLE branch_types (' - 'id integer NOT NULL,' - 'name varchar(80))') - -if branches: - do_query(query, 'CREATE TABLE samples (' - 'id bigint NOT NULL,' - 'evsel_id bigint,' - 'machine_id bigint,' - 'thread_id bigint,' - 'comm_id bigint,' - 'dso_id bigint,' - 'symbol_id bigint,' - 'sym_offset bigint,' - 'ip bigint,' - 'time bigint,' - 'cpu integer,' - 'to_dso_id bigint,' - 'to_symbol_id bigint,' - 'to_sym_offset bigint,' - 'to_ip bigint,' - 'branch_type integer,' - 'in_tx boolean,' - 'call_path_id bigint,' - 'insn_count bigint,' - 'cyc_count bigint,' - 'flags integer)') -else: - do_query(query, 'CREATE TABLE samples (' - 'id bigint NOT NULL,' - 'evsel_id bigint,' - 'machine_id bigint,' - 'thread_id bigint,' - 'comm_id bigint,' - 'dso_id bigint,' - 'symbol_id bigint,' - 'sym_offset bigint,' - 'ip bigint,' - 'time bigint,' - 'cpu integer,' - 'to_dso_id bigint,' - 'to_symbol_id bigint,' - 'to_sym_offset bigint,' - 'to_ip bigint,' - 'period bigint,' - 'weight bigint,' - 'transaction bigint,' - 'data_src bigint,' - 'branch_type integer,' - 'in_tx boolean,' - 'call_path_id bigint,' - 'insn_count bigint,' - 'cyc_count bigint,' - 'flags integer)') - -if perf_db_export_calls or perf_db_export_callchains: - do_query(query, 'CREATE TABLE call_paths (' - 'id bigint NOT NULL,' - 'parent_id bigint,' - 'symbol_id bigint,' - 'ip bigint)') -if perf_db_export_calls: - do_query(query, 'CREATE TABLE calls (' - 'id bigint NOT NULL,' - 'thread_id bigint,' - 'comm_id bigint,' - 'call_path_id bigint,' - 'call_time bigint,' - 'return_time bigint,' - 'branch_count bigint,' - 'call_id bigint,' - 'return_id bigint,' - 'parent_call_path_id bigint,' - 'flags integer,' - 'parent_id bigint,' - 'insn_count bigint,' - 'cyc_count bigint)') - -do_query(query, 'CREATE TABLE ptwrite (' - 'id bigint NOT NULL,' - 'payload bigint,' - 'exact_ip boolean)') - -do_query(query, 'CREATE TABLE cbr (' - 'id bigint NOT NULL,' - 'cbr integer,' - 'mhz integer,' - 'percent integer)') - -do_query(query, 'CREATE TABLE mwait (' - 'id bigint NOT NULL,' - 'hints integer,' - 'extensions integer)') - -do_query(query, 'CREATE TABLE pwre (' - 'id bigint NOT NULL,' - 'cstate integer,' - 'subcstate integer,' - 'hw boolean)') - -do_query(query, 'CREATE TABLE exstop (' - 'id bigint NOT NULL,' - 'exact_ip boolean)') - -do_query(query, 'CREATE TABLE pwrx (' - 'id bigint NOT NULL,' - 'deepest_cstate integer,' - 'last_cstate integer,' - 'wake_reason integer)') - -do_query(query, 'CREATE TABLE context_switches (' - 'id bigint NOT NULL,' - 'machine_id bigint,' - 'time bigint,' - 'cpu integer,' - 'thread_out_id bigint,' - 'comm_out_id bigint,' - 'thread_in_id bigint,' - 'comm_in_id bigint,' - 'flags integer)') - -do_query(query, 'CREATE VIEW machines_view AS ' - 'SELECT ' - 'id,' - 'pid,' - 'root_dir,' - 'CASE WHEN id=0 THEN \'unknown\' WHEN pid=-1 THEN \'host\' ELSE \'guest\' END AS host_or_guest' - ' FROM machines') - -do_query(query, 'CREATE VIEW dsos_view AS ' - 'SELECT ' - 'id,' - 'machine_id,' - '(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest,' - 'short_name,' - 'long_name,' - 'build_id' - ' FROM dsos') - -do_query(query, 'CREATE VIEW symbols_view AS ' - 'SELECT ' - 'id,' - 'name,' - '(SELECT short_name FROM dsos WHERE id=dso_id) AS dso,' - 'dso_id,' - 'sym_start,' - 'sym_end,' - 'CASE WHEN binding=0 THEN \'local\' WHEN binding=1 THEN \'global\' ELSE \'weak\' END AS binding' - ' FROM symbols') - -do_query(query, 'CREATE VIEW threads_view AS ' - 'SELECT ' - 'id,' - 'machine_id,' - '(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest,' - 'process_id,' - 'pid,' - 'tid' - ' FROM threads') - -do_query(query, 'CREATE VIEW comm_threads_view AS ' - 'SELECT ' - 'comm_id,' - '(SELECT comm FROM comms WHERE id = comm_id) AS command,' - 'thread_id,' - '(SELECT pid FROM threads WHERE id = thread_id) AS pid,' - '(SELECT tid FROM threads WHERE id = thread_id) AS tid' - ' FROM comm_threads') - -if perf_db_export_calls or perf_db_export_callchains: - do_query(query, 'CREATE VIEW call_paths_view AS ' - 'SELECT ' - 'c.id,' - 'to_hex(c.ip) AS ip,' - 'c.symbol_id,' - '(SELECT name FROM symbols WHERE id = c.symbol_id) AS symbol,' - '(SELECT dso_id FROM symbols WHERE id = c.symbol_id) AS dso_id,' - '(SELECT dso FROM symbols_view WHERE id = c.symbol_id) AS dso_short_name,' - 'c.parent_id,' - 'to_hex(p.ip) AS parent_ip,' - 'p.symbol_id AS parent_symbol_id,' - '(SELECT name FROM symbols WHERE id = p.symbol_id) AS parent_symbol,' - '(SELECT dso_id FROM symbols WHERE id = p.symbol_id) AS parent_dso_id,' - '(SELECT dso FROM symbols_view WHERE id = p.symbol_id) AS parent_dso_short_name' - ' FROM call_paths c INNER JOIN call_paths p ON p.id = c.parent_id') -if perf_db_export_calls: - do_query(query, 'CREATE VIEW calls_view AS ' - 'SELECT ' - 'calls.id,' - 'thread_id,' - '(SELECT pid FROM threads WHERE id = thread_id) AS pid,' - '(SELECT tid FROM threads WHERE id = thread_id) AS tid,' - '(SELECT comm FROM comms WHERE id = comm_id) AS command,' - 'call_path_id,' - 'to_hex(ip) AS ip,' - 'symbol_id,' - '(SELECT name FROM symbols WHERE id = symbol_id) AS symbol,' - 'call_time,' - 'return_time,' - 'return_time - call_time AS elapsed_time,' - 'branch_count,' - 'insn_count,' - 'cyc_count,' - 'CASE WHEN cyc_count=0 THEN CAST(0 AS NUMERIC(20, 2)) ELSE CAST((CAST(insn_count AS FLOAT) / cyc_count) AS NUMERIC(20, 2)) END AS IPC,' - 'call_id,' - 'return_id,' - 'CASE WHEN flags=0 THEN \'\' WHEN flags=1 THEN \'no call\' WHEN flags=2 THEN \'no return\' WHEN flags=3 THEN \'no call/return\' WHEN flags=6 THEN \'jump\' ELSE CAST ( flags AS VARCHAR(6) ) END AS flags,' - 'parent_call_path_id,' - 'calls.parent_id' - ' FROM calls INNER JOIN call_paths ON call_paths.id = call_path_id') - -do_query(query, 'CREATE VIEW samples_view AS ' - 'SELECT ' - 'id,' - 'time,' - 'cpu,' - '(SELECT pid FROM threads WHERE id = thread_id) AS pid,' - '(SELECT tid FROM threads WHERE id = thread_id) AS tid,' - '(SELECT comm FROM comms WHERE id = comm_id) AS command,' - '(SELECT name FROM selected_events WHERE id = evsel_id) AS event,' - 'to_hex(ip) AS ip_hex,' - '(SELECT name FROM symbols WHERE id = symbol_id) AS symbol,' - 'sym_offset,' - '(SELECT short_name FROM dsos WHERE id = dso_id) AS dso_short_name,' - 'to_hex(to_ip) AS to_ip_hex,' - '(SELECT name FROM symbols WHERE id = to_symbol_id) AS to_symbol,' - 'to_sym_offset,' - '(SELECT short_name FROM dsos WHERE id = to_dso_id) AS to_dso_short_name,' - '(SELECT name FROM branch_types WHERE id = branch_type) AS branch_type_name,' - 'in_tx,' - 'insn_count,' - 'cyc_count,' - 'CASE WHEN cyc_count=0 THEN CAST(0 AS NUMERIC(20, 2)) ELSE CAST((CAST(insn_count AS FLOAT) / cyc_count) AS NUMERIC(20, 2)) END AS IPC,' - 'flags' - ' FROM samples') - -do_query(query, 'CREATE VIEW ptwrite_view AS ' - 'SELECT ' - 'ptwrite.id,' - 'time,' - 'cpu,' - 'to_hex(payload) AS payload_hex,' - 'CASE WHEN exact_ip=FALSE THEN \'False\' ELSE \'True\' END AS exact_ip' - ' FROM ptwrite' - ' INNER JOIN samples ON samples.id = ptwrite.id') - -do_query(query, 'CREATE VIEW cbr_view AS ' - 'SELECT ' - 'cbr.id,' - 'time,' - 'cpu,' - 'cbr,' - 'mhz,' - 'percent' - ' FROM cbr' - ' INNER JOIN samples ON samples.id = cbr.id') - -do_query(query, 'CREATE VIEW mwait_view AS ' - 'SELECT ' - 'mwait.id,' - 'time,' - 'cpu,' - 'to_hex(hints) AS hints_hex,' - 'to_hex(extensions) AS extensions_hex' - ' FROM mwait' - ' INNER JOIN samples ON samples.id = mwait.id') - -do_query(query, 'CREATE VIEW pwre_view AS ' - 'SELECT ' - 'pwre.id,' - 'time,' - 'cpu,' - 'cstate,' - 'subcstate,' - 'CASE WHEN hw=FALSE THEN \'False\' ELSE \'True\' END AS hw' - ' FROM pwre' - ' INNER JOIN samples ON samples.id = pwre.id') - -do_query(query, 'CREATE VIEW exstop_view AS ' - 'SELECT ' - 'exstop.id,' - 'time,' - 'cpu,' - 'CASE WHEN exact_ip=FALSE THEN \'False\' ELSE \'True\' END AS exact_ip' - ' FROM exstop' - ' INNER JOIN samples ON samples.id = exstop.id') - -do_query(query, 'CREATE VIEW pwrx_view AS ' - 'SELECT ' - 'pwrx.id,' - 'time,' - 'cpu,' - 'deepest_cstate,' - 'last_cstate,' - 'CASE WHEN wake_reason=1 THEN \'Interrupt\'' - ' WHEN wake_reason=2 THEN \'Timer Deadline\'' - ' WHEN wake_reason=4 THEN \'Monitored Address\'' - ' WHEN wake_reason=8 THEN \'HW\'' - ' ELSE CAST ( wake_reason AS VARCHAR(2) )' - 'END AS wake_reason' - ' FROM pwrx' - ' INNER JOIN samples ON samples.id = pwrx.id') - -do_query(query, 'CREATE VIEW power_events_view AS ' - 'SELECT ' - 'samples.id,' - 'samples.time,' - 'samples.cpu,' - 'selected_events.name AS event,' - 'FORMAT(\'%6s\', cbr.cbr) AS cbr,' - 'FORMAT(\'%6s\', cbr.mhz) AS MHz,' - 'FORMAT(\'%5s\', cbr.percent) AS percent,' - 'to_hex(mwait.hints) AS hints_hex,' - 'to_hex(mwait.extensions) AS extensions_hex,' - 'FORMAT(\'%3s\', pwre.cstate) AS cstate,' - 'FORMAT(\'%3s\', pwre.subcstate) AS subcstate,' - 'CASE WHEN pwre.hw=FALSE THEN \'False\' WHEN pwre.hw=TRUE THEN \'True\' ELSE NULL END AS hw,' - 'CASE WHEN exstop.exact_ip=FALSE THEN \'False\' WHEN exstop.exact_ip=TRUE THEN \'True\' ELSE NULL END AS exact_ip,' - 'FORMAT(\'%3s\', pwrx.deepest_cstate) AS deepest_cstate,' - 'FORMAT(\'%3s\', pwrx.last_cstate) AS last_cstate,' - 'CASE WHEN pwrx.wake_reason=1 THEN \'Interrupt\'' - ' WHEN pwrx.wake_reason=2 THEN \'Timer Deadline\'' - ' WHEN pwrx.wake_reason=4 THEN \'Monitored Address\'' - ' WHEN pwrx.wake_reason=8 THEN \'HW\'' - ' ELSE FORMAT(\'%2s\', pwrx.wake_reason)' - 'END AS wake_reason' - ' FROM cbr' - ' FULL JOIN mwait ON mwait.id = cbr.id' - ' FULL JOIN pwre ON pwre.id = cbr.id' - ' FULL JOIN exstop ON exstop.id = cbr.id' - ' FULL JOIN pwrx ON pwrx.id = cbr.id' - ' INNER JOIN samples ON samples.id = coalesce(cbr.id, mwait.id, pwre.id, exstop.id, pwrx.id)' - ' INNER JOIN selected_events ON selected_events.id = samples.evsel_id' - ' ORDER BY samples.id') - -do_query(query, 'CREATE VIEW context_switches_view AS ' - 'SELECT ' - 'context_switches.id,' - 'context_switches.machine_id,' - 'context_switches.time,' - 'context_switches.cpu,' - 'th_out.pid AS pid_out,' - 'th_out.tid AS tid_out,' - 'comm_out.comm AS comm_out,' - 'th_in.pid AS pid_in,' - 'th_in.tid AS tid_in,' - 'comm_in.comm AS comm_in,' - 'CASE WHEN context_switches.flags = 0 THEN \'in\'' - ' WHEN context_switches.flags = 1 THEN \'out\'' - ' WHEN context_switches.flags = 3 THEN \'out preempt\'' - ' ELSE CAST ( context_switches.flags AS VARCHAR(11) )' - 'END AS flags' - ' FROM context_switches' - ' INNER JOIN threads AS th_out ON th_out.id = context_switches.thread_out_id' - ' INNER JOIN threads AS th_in ON th_in.id = context_switches.thread_in_id' - ' INNER JOIN comms AS comm_out ON comm_out.id = context_switches.comm_out_id' - ' INNER JOIN comms AS comm_in ON comm_in.id = context_switches.comm_in_id') - -file_header = struct.pack("!11sii", b"PGCOPY\n\377\r\n\0", 0, 0) -file_trailer = b"\377\377" - -def open_output_file(file_name): - path_name = output_dir_name + "/" + file_name - file = open(path_name, "wb+") - file.write(file_header) - return file - -def close_output_file(file): - file.write(file_trailer) - file.close() - -def copy_output_file_direct(file, table_name): - close_output_file(file) - sql = "COPY " + table_name + " FROM '" + file.name + "' (FORMAT 'binary')" - do_query(query, sql) - -# Use COPY FROM STDIN because security may prevent postgres from accessing the files directly -def copy_output_file(file, table_name): - conn = PQconnectdb(toclientstr("dbname = " + dbname)) - if (PQstatus(conn)): - raise Exception("COPY FROM STDIN PQconnectdb failed") - file.write(file_trailer) - file.seek(0) - sql = "COPY " + table_name + " FROM STDIN (FORMAT 'binary')" - res = PQexec(conn, toclientstr(sql)) - if (PQresultStatus(res) != 4): - raise Exception("COPY FROM STDIN PQexec failed") - data = file.read(65536) - while (len(data)): - ret = PQputCopyData(conn, data, len(data)) - if (ret != 1): - raise Exception("COPY FROM STDIN PQputCopyData failed, error " + str(ret)) - data = file.read(65536) - ret = PQputCopyEnd(conn, None) - if (ret != 1): - raise Exception("COPY FROM STDIN PQputCopyEnd failed, error " + str(ret)) - PQfinish(conn) - -def remove_output_file(file): - name = file.name - file.close() - os.unlink(name) - -evsel_file = open_output_file("evsel_table.bin") -machine_file = open_output_file("machine_table.bin") -thread_file = open_output_file("thread_table.bin") -comm_file = open_output_file("comm_table.bin") -comm_thread_file = open_output_file("comm_thread_table.bin") -dso_file = open_output_file("dso_table.bin") -symbol_file = open_output_file("symbol_table.bin") -branch_type_file = open_output_file("branch_type_table.bin") -sample_file = open_output_file("sample_table.bin") -if perf_db_export_calls or perf_db_export_callchains: - call_path_file = open_output_file("call_path_table.bin") -if perf_db_export_calls: - call_file = open_output_file("call_table.bin") -ptwrite_file = open_output_file("ptwrite_table.bin") -cbr_file = open_output_file("cbr_table.bin") -mwait_file = open_output_file("mwait_table.bin") -pwre_file = open_output_file("pwre_table.bin") -exstop_file = open_output_file("exstop_table.bin") -pwrx_file = open_output_file("pwrx_table.bin") -context_switches_file = open_output_file("context_switches_table.bin") - -def trace_begin(): - printdate("Writing to intermediate files...") - # id == 0 means unknown. It is easier to create records for them than replace the zeroes with NULLs - evsel_table(0, "unknown") - machine_table(0, 0, "unknown") - thread_table(0, 0, 0, -1, -1) - comm_table(0, "unknown", 0, 0, 0) - dso_table(0, 0, "unknown", "unknown", "") - symbol_table(0, 0, 0, 0, 0, "unknown") - sample_table(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - if perf_db_export_calls or perf_db_export_callchains: - call_path_table(0, 0, 0, 0) - call_return_table(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - -unhandled_count = 0 - -def is_table_empty(table_name): - do_query(query, 'SELECT * FROM ' + table_name + ' LIMIT 1'); - if query.next(): - return False - return True - -def drop(table_name): - do_query(query, 'DROP VIEW ' + table_name + '_view'); - do_query(query, 'DROP TABLE ' + table_name); - -def trace_end(): - printdate("Copying to database...") - copy_output_file(evsel_file, "selected_events") - copy_output_file(machine_file, "machines") - copy_output_file(thread_file, "threads") - copy_output_file(comm_file, "comms") - copy_output_file(comm_thread_file, "comm_threads") - copy_output_file(dso_file, "dsos") - copy_output_file(symbol_file, "symbols") - copy_output_file(branch_type_file, "branch_types") - copy_output_file(sample_file, "samples") - if perf_db_export_calls or perf_db_export_callchains: - copy_output_file(call_path_file, "call_paths") - if perf_db_export_calls: - copy_output_file(call_file, "calls") - copy_output_file(ptwrite_file, "ptwrite") - copy_output_file(cbr_file, "cbr") - copy_output_file(mwait_file, "mwait") - copy_output_file(pwre_file, "pwre") - copy_output_file(exstop_file, "exstop") - copy_output_file(pwrx_file, "pwrx") - copy_output_file(context_switches_file, "context_switches") - - printdate("Removing intermediate files...") - remove_output_file(evsel_file) - remove_output_file(machine_file) - remove_output_file(thread_file) - remove_output_file(comm_file) - remove_output_file(comm_thread_file) - remove_output_file(dso_file) - remove_output_file(symbol_file) - remove_output_file(branch_type_file) - remove_output_file(sample_file) - if perf_db_export_calls or perf_db_export_callchains: - remove_output_file(call_path_file) - if perf_db_export_calls: - remove_output_file(call_file) - remove_output_file(ptwrite_file) - remove_output_file(cbr_file) - remove_output_file(mwait_file) - remove_output_file(pwre_file) - remove_output_file(exstop_file) - remove_output_file(pwrx_file) - remove_output_file(context_switches_file) - os.rmdir(output_dir_name) - printdate("Adding primary keys") - do_query(query, 'ALTER TABLE selected_events ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE machines ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE threads ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE comms ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE comm_threads ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE dsos ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE symbols ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE branch_types ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE samples ADD PRIMARY KEY (id)') - if perf_db_export_calls or perf_db_export_callchains: - do_query(query, 'ALTER TABLE call_paths ADD PRIMARY KEY (id)') - if perf_db_export_calls: - do_query(query, 'ALTER TABLE calls ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE ptwrite ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE cbr ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE mwait ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE pwre ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE exstop ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE pwrx ADD PRIMARY KEY (id)') - do_query(query, 'ALTER TABLE context_switches ADD PRIMARY KEY (id)') - - printdate("Adding foreign keys") - do_query(query, 'ALTER TABLE threads ' - 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id),' - 'ADD CONSTRAINT processfk FOREIGN KEY (process_id) REFERENCES threads (id)') - do_query(query, 'ALTER TABLE comms ' - 'ADD CONSTRAINT threadfk FOREIGN KEY (c_thread_id) REFERENCES threads (id)') - do_query(query, 'ALTER TABLE comm_threads ' - 'ADD CONSTRAINT commfk FOREIGN KEY (comm_id) REFERENCES comms (id),' - 'ADD CONSTRAINT threadfk FOREIGN KEY (thread_id) REFERENCES threads (id)') - do_query(query, 'ALTER TABLE dsos ' - 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id)') - do_query(query, 'ALTER TABLE symbols ' - 'ADD CONSTRAINT dsofk FOREIGN KEY (dso_id) REFERENCES dsos (id)') - do_query(query, 'ALTER TABLE samples ' - 'ADD CONSTRAINT evselfk FOREIGN KEY (evsel_id) REFERENCES selected_events (id),' - 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id),' - 'ADD CONSTRAINT threadfk FOREIGN KEY (thread_id) REFERENCES threads (id),' - 'ADD CONSTRAINT commfk FOREIGN KEY (comm_id) REFERENCES comms (id),' - 'ADD CONSTRAINT dsofk FOREIGN KEY (dso_id) REFERENCES dsos (id),' - 'ADD CONSTRAINT symbolfk FOREIGN KEY (symbol_id) REFERENCES symbols (id),' - 'ADD CONSTRAINT todsofk FOREIGN KEY (to_dso_id) REFERENCES dsos (id),' - 'ADD CONSTRAINT tosymbolfk FOREIGN KEY (to_symbol_id) REFERENCES symbols (id)') - if perf_db_export_calls or perf_db_export_callchains: - do_query(query, 'ALTER TABLE call_paths ' - 'ADD CONSTRAINT parentfk FOREIGN KEY (parent_id) REFERENCES call_paths (id),' - 'ADD CONSTRAINT symbolfk FOREIGN KEY (symbol_id) REFERENCES symbols (id)') - if perf_db_export_calls: - do_query(query, 'ALTER TABLE calls ' - 'ADD CONSTRAINT threadfk FOREIGN KEY (thread_id) REFERENCES threads (id),' - 'ADD CONSTRAINT commfk FOREIGN KEY (comm_id) REFERENCES comms (id),' - 'ADD CONSTRAINT call_pathfk FOREIGN KEY (call_path_id) REFERENCES call_paths (id),' - 'ADD CONSTRAINT callfk FOREIGN KEY (call_id) REFERENCES samples (id),' - 'ADD CONSTRAINT returnfk FOREIGN KEY (return_id) REFERENCES samples (id),' - 'ADD CONSTRAINT parent_call_pathfk FOREIGN KEY (parent_call_path_id) REFERENCES call_paths (id)') - do_query(query, 'CREATE INDEX pcpid_idx ON calls (parent_call_path_id)') - do_query(query, 'CREATE INDEX pid_idx ON calls (parent_id)') - do_query(query, 'ALTER TABLE comms ADD has_calls boolean') - do_query(query, 'UPDATE comms SET has_calls = TRUE WHERE comms.id IN (SELECT DISTINCT comm_id FROM calls)') - do_query(query, 'ALTER TABLE ptwrite ' - 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') - do_query(query, 'ALTER TABLE cbr ' - 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') - do_query(query, 'ALTER TABLE mwait ' - 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') - do_query(query, 'ALTER TABLE pwre ' - 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') - do_query(query, 'ALTER TABLE exstop ' - 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') - do_query(query, 'ALTER TABLE pwrx ' - 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)') - do_query(query, 'ALTER TABLE context_switches ' - 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id),' - 'ADD CONSTRAINT toutfk FOREIGN KEY (thread_out_id) REFERENCES threads (id),' - 'ADD CONSTRAINT tinfk FOREIGN KEY (thread_in_id) REFERENCES threads (id),' - 'ADD CONSTRAINT coutfk FOREIGN KEY (comm_out_id) REFERENCES comms (id),' - 'ADD CONSTRAINT cinfk FOREIGN KEY (comm_in_id) REFERENCES comms (id)') - - printdate("Dropping unused tables") - if is_table_empty("ptwrite"): - drop("ptwrite") - if is_table_empty("mwait") and is_table_empty("pwre") and is_table_empty("exstop") and is_table_empty("pwrx"): - do_query(query, 'DROP VIEW power_events_view'); - drop("mwait") - drop("pwre") - drop("exstop") - drop("pwrx") - if is_table_empty("cbr"): - drop("cbr") - if is_table_empty("context_switches"): - drop("context_switches") - - if (unhandled_count): - printdate("Warning: ", unhandled_count, " unhandled events") - printdate("Done") - -def trace_unhandled(event_name, context, event_fields_dict): - global unhandled_count - unhandled_count += 1 - -def sched__sched_switch(*x): - pass - -def evsel_table(evsel_id, evsel_name, *x): - evsel_name = toserverstr(evsel_name) - n = len(evsel_name) - fmt = "!hiqi" + str(n) + "s" - value = struct.pack(fmt, 2, 8, evsel_id, n, evsel_name) - evsel_file.write(value) - -def machine_table(machine_id, pid, root_dir, *x): - root_dir = toserverstr(root_dir) - n = len(root_dir) - fmt = "!hiqiii" + str(n) + "s" - value = struct.pack(fmt, 3, 8, machine_id, 4, pid, n, root_dir) - machine_file.write(value) - -def thread_table(thread_id, machine_id, process_id, pid, tid, *x): - value = struct.pack("!hiqiqiqiiii", 5, 8, thread_id, 8, machine_id, 8, process_id, 4, pid, 4, tid) - thread_file.write(value) - -def comm_table(comm_id, comm_str, thread_id, time, exec_flag, *x): - comm_str = toserverstr(comm_str) - n = len(comm_str) - fmt = "!hiqi" + str(n) + "s" + "iqiqiB" - value = struct.pack(fmt, 5, 8, comm_id, n, comm_str, 8, thread_id, 8, time, 1, exec_flag) - comm_file.write(value) - -def comm_thread_table(comm_thread_id, comm_id, thread_id, *x): - fmt = "!hiqiqiq" - value = struct.pack(fmt, 3, 8, comm_thread_id, 8, comm_id, 8, thread_id) - comm_thread_file.write(value) - -def dso_table(dso_id, machine_id, short_name, long_name, build_id, *x): - short_name = toserverstr(short_name) - long_name = toserverstr(long_name) - build_id = toserverstr(build_id) - n1 = len(short_name) - n2 = len(long_name) - n3 = len(build_id) - fmt = "!hiqiqi" + str(n1) + "si" + str(n2) + "si" + str(n3) + "s" - value = struct.pack(fmt, 5, 8, dso_id, 8, machine_id, n1, short_name, n2, long_name, n3, build_id) - dso_file.write(value) - -def symbol_table(symbol_id, dso_id, sym_start, sym_end, binding, symbol_name, *x): - symbol_name = toserverstr(symbol_name) - n = len(symbol_name) - fmt = "!hiqiqiqiqiii" + str(n) + "s" - value = struct.pack(fmt, 6, 8, symbol_id, 8, dso_id, 8, sym_start, 8, sym_end, 4, binding, n, symbol_name) - symbol_file.write(value) - -def branch_type_table(branch_type, name, *x): - name = toserverstr(name) - n = len(name) - fmt = "!hiii" + str(n) + "s" - value = struct.pack(fmt, 2, 4, branch_type, n, name) - branch_type_file.write(value) - -def sample_table(sample_id, evsel_id, machine_id, thread_id, comm_id, dso_id, symbol_id, sym_offset, ip, time, cpu, to_dso_id, to_symbol_id, to_sym_offset, to_ip, period, weight, transaction, data_src, branch_type, in_tx, call_path_id, insn_cnt, cyc_cnt, flags, *x): - if branches: - value = struct.pack("!hiqiqiqiqiqiqiqiqiqiqiiiqiqiqiqiiiBiqiqiqii", 21, 8, sample_id, 8, evsel_id, 8, machine_id, 8, thread_id, 8, comm_id, 8, dso_id, 8, symbol_id, 8, sym_offset, 8, ip, 8, time, 4, cpu, 8, to_dso_id, 8, to_symbol_id, 8, to_sym_offset, 8, to_ip, 4, branch_type, 1, in_tx, 8, call_path_id, 8, insn_cnt, 8, cyc_cnt, 4, flags) - else: - value = struct.pack("!hiqiqiqiqiqiqiqiqiqiqiiiqiqiqiqiqiqiqiqiiiBiqiqiqii", 25, 8, sample_id, 8, evsel_id, 8, machine_id, 8, thread_id, 8, comm_id, 8, dso_id, 8, symbol_id, 8, sym_offset, 8, ip, 8, time, 4, cpu, 8, to_dso_id, 8, to_symbol_id, 8, to_sym_offset, 8, to_ip, 8, period, 8, weight, 8, transaction, 8, data_src, 4, branch_type, 1, in_tx, 8, call_path_id, 8, insn_cnt, 8, cyc_cnt, 4, flags) - sample_file.write(value) - -def call_path_table(cp_id, parent_id, symbol_id, ip, *x): - fmt = "!hiqiqiqiq" - value = struct.pack(fmt, 4, 8, cp_id, 8, parent_id, 8, symbol_id, 8, ip) - call_path_file.write(value) - -def call_return_table(cr_id, thread_id, comm_id, call_path_id, call_time, return_time, branch_count, call_id, return_id, parent_call_path_id, flags, parent_id, insn_cnt, cyc_cnt, *x): - fmt = "!hiqiqiqiqiqiqiqiqiqiqiiiqiqiq" - value = struct.pack(fmt, 14, 8, cr_id, 8, thread_id, 8, comm_id, 8, call_path_id, 8, call_time, 8, return_time, 8, branch_count, 8, call_id, 8, return_id, 8, parent_call_path_id, 4, flags, 8, parent_id, 8, insn_cnt, 8, cyc_cnt) - call_file.write(value) - -def ptwrite(id, raw_buf): - data = struct.unpack_from("> 32) & 0x3 - value = struct.pack("!hiqiiii", 3, 8, id, 4, hints, 4, extensions) - mwait_file.write(value) - -def pwre(id, raw_buf): - data = struct.unpack_from("> 7) & 1 - cstate = (payload >> 12) & 0xf - subcstate = (payload >> 8) & 0xf - value = struct.pack("!hiqiiiiiB", 4, 8, id, 4, cstate, 4, subcstate, 1, hw) - pwre_file.write(value) - -def exstop(id, raw_buf): - data = struct.unpack_from("> 4) & 0xf - wake_reason = (payload >> 8) & 0xf - value = struct.pack("!hiqiiiiii", 4, 8, id, 4, deepest_cstate, 4, last_cstate, 4, wake_reason) - pwrx_file.write(value) - -def synth_data(id, config, raw_buf, *x): - if config == 0: - ptwrite(id, raw_buf) - elif config == 1: - mwait(id, raw_buf) - elif config == 2: - pwre(id, raw_buf) - elif config == 3: - exstop(id, raw_buf) - elif config == 4: - pwrx(id, raw_buf) - elif config == 5: - cbr(id, raw_buf) - -def context_switch_table(id, machine_id, time, cpu, thread_out_id, comm_out_id, thread_in_id, comm_in_id, flags, *x): - fmt = "!hiqiqiqiiiqiqiqiqii" - value = struct.pack(fmt, 9, 8, id, 8, machine_id, 8, time, 4, cpu, 8, thread_out_id, 8, comm_out_id, 8, thread_in_id, 8, comm_in_id, 4, flags) - context_switches_file.write(value) diff --git a/tools/perf/scripts/python/export-to-sqlite.py b/tools/perf/scripts/python/export-to-sqlite.py deleted file mode 100644 index 73c992feb1b9..000000000000 --- a/tools/perf/scripts/python/export-to-sqlite.py +++ /dev/null @@ -1,799 +0,0 @@ -# export-to-sqlite.py: export perf data to a sqlite3 database -# Copyright (c) 2017, Intel Corporation. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms and conditions of the GNU General Public License, -# version 2, as published by the Free Software Foundation. -# -# This program is distributed in the hope it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. - -from __future__ import print_function - -import os -import sys -import struct -import datetime - -# To use this script you will need to have installed package python-pyside which -# provides LGPL-licensed Python bindings for Qt. You will also need the package -# libqt4-sql-sqlite for Qt sqlite3 support. -# -# Examples of installing pyside: -# -# ubuntu: -# -# $ sudo apt-get install python-pyside.qtsql libqt4-sql-psql -# -# Alternately, to use Python3 and/or pyside 2, one of the following: -# -# $ sudo apt-get install python3-pyside.qtsql libqt4-sql-psql -# $ sudo apt-get install python-pyside2.qtsql libqt5sql5-psql -# $ sudo apt-get install python3-pyside2.qtsql libqt5sql5-psql -# fedora: -# -# $ sudo yum install python-pyside -# -# Alternately, to use Python3 and/or pyside 2, one of the following: -# $ sudo yum install python3-pyside -# $ pip install --user PySide2 -# $ pip3 install --user PySide2 -# -# An example of using this script with Intel PT: -# -# $ perf record -e intel_pt//u ls -# $ perf script -s ~/libexec/perf-core/scripts/python/export-to-sqlite.py pt_example branches calls -# 2017-07-31 14:26:07.326913 Creating database... -# 2017-07-31 14:26:07.538097 Writing records... -# 2017-07-31 14:26:09.889292 Adding indexes -# 2017-07-31 14:26:09.958746 Done -# -# To browse the database, sqlite3 can be used e.g. -# -# $ sqlite3 pt_example -# sqlite> .header on -# sqlite> select * from samples_view where id < 10; -# sqlite> .mode column -# sqlite> select * from samples_view where id < 10; -# sqlite> .tables -# sqlite> .schema samples_view -# sqlite> .quit -# -# An example of using the database is provided by the script -# exported-sql-viewer.py. Refer to that script for details. -# -# The database structure is practically the same as created by the script -# export-to-postgresql.py. Refer to that script for details. A notable -# difference is the 'transaction' column of the 'samples' table which is -# renamed 'transaction_' in sqlite because 'transaction' is a reserved word. - -pyside_version_1 = True -if not "pyside-version-1" in sys.argv: - try: - from PySide2.QtSql import * - pyside_version_1 = False - except: - pass - -if pyside_version_1: - from PySide.QtSql import * - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -# These perf imports are not used at present -#from perf_trace_context import * -#from Core import * - -perf_db_export_mode = True -perf_db_export_calls = False -perf_db_export_callchains = False - -def printerr(*args, **keyword_args): - print(*args, file=sys.stderr, **keyword_args) - -def printdate(*args, **kw_args): - print(datetime.datetime.today(), *args, sep=' ', **kw_args) - -def usage(): - printerr("Usage is: export-to-sqlite.py [] [] [] []"); - printerr("where: columns 'all' or 'branches'"); - printerr(" calls 'calls' => create calls and call_paths table"); - printerr(" callchains 'callchains' => create call_paths table"); - printerr(" pyside-version-1 'pyside-version-1' => use pyside version 1"); - raise Exception("Too few or bad arguments") - -if (len(sys.argv) < 2): - usage() - -dbname = sys.argv[1] - -if (len(sys.argv) >= 3): - columns = sys.argv[2] -else: - columns = "all" - -if columns not in ("all", "branches"): - usage() - -branches = (columns == "branches") - -for i in range(3,len(sys.argv)): - if (sys.argv[i] == "calls"): - perf_db_export_calls = True - elif (sys.argv[i] == "callchains"): - perf_db_export_callchains = True - elif (sys.argv[i] == "pyside-version-1"): - pass - else: - usage() - -def do_query(q, s): - if (q.exec_(s)): - return - raise Exception("Query failed: " + q.lastError().text()) - -def do_query_(q): - if (q.exec_()): - return - raise Exception("Query failed: " + q.lastError().text()) - -printdate("Creating database ...") - -db_exists = False -try: - f = open(dbname) - f.close() - db_exists = True -except: - pass - -if db_exists: - raise Exception(dbname + " already exists") - -db = QSqlDatabase.addDatabase('QSQLITE') -db.setDatabaseName(dbname) -db.open() - -query = QSqlQuery(db) - -do_query(query, 'PRAGMA journal_mode = OFF') -do_query(query, 'BEGIN TRANSACTION') - -do_query(query, 'CREATE TABLE selected_events (' - 'id integer NOT NULL PRIMARY KEY,' - 'name varchar(80))') -do_query(query, 'CREATE TABLE machines (' - 'id integer NOT NULL PRIMARY KEY,' - 'pid integer,' - 'root_dir varchar(4096))') -do_query(query, 'CREATE TABLE threads (' - 'id integer NOT NULL PRIMARY KEY,' - 'machine_id bigint,' - 'process_id bigint,' - 'pid integer,' - 'tid integer)') -do_query(query, 'CREATE TABLE comms (' - 'id integer NOT NULL PRIMARY KEY,' - 'comm varchar(16),' - 'c_thread_id bigint,' - 'c_time bigint,' - 'exec_flag boolean)') -do_query(query, 'CREATE TABLE comm_threads (' - 'id integer NOT NULL PRIMARY KEY,' - 'comm_id bigint,' - 'thread_id bigint)') -do_query(query, 'CREATE TABLE dsos (' - 'id integer NOT NULL PRIMARY KEY,' - 'machine_id bigint,' - 'short_name varchar(256),' - 'long_name varchar(4096),' - 'build_id varchar(64))') -do_query(query, 'CREATE TABLE symbols (' - 'id integer NOT NULL PRIMARY KEY,' - 'dso_id bigint,' - 'sym_start bigint,' - 'sym_end bigint,' - 'binding integer,' - 'name varchar(2048))') -do_query(query, 'CREATE TABLE branch_types (' - 'id integer NOT NULL PRIMARY KEY,' - 'name varchar(80))') - -if branches: - do_query(query, 'CREATE TABLE samples (' - 'id integer NOT NULL PRIMARY KEY,' - 'evsel_id bigint,' - 'machine_id bigint,' - 'thread_id bigint,' - 'comm_id bigint,' - 'dso_id bigint,' - 'symbol_id bigint,' - 'sym_offset bigint,' - 'ip bigint,' - 'time bigint,' - 'cpu integer,' - 'to_dso_id bigint,' - 'to_symbol_id bigint,' - 'to_sym_offset bigint,' - 'to_ip bigint,' - 'branch_type integer,' - 'in_tx boolean,' - 'call_path_id bigint,' - 'insn_count bigint,' - 'cyc_count bigint,' - 'flags integer)') -else: - do_query(query, 'CREATE TABLE samples (' - 'id integer NOT NULL PRIMARY KEY,' - 'evsel_id bigint,' - 'machine_id bigint,' - 'thread_id bigint,' - 'comm_id bigint,' - 'dso_id bigint,' - 'symbol_id bigint,' - 'sym_offset bigint,' - 'ip bigint,' - 'time bigint,' - 'cpu integer,' - 'to_dso_id bigint,' - 'to_symbol_id bigint,' - 'to_sym_offset bigint,' - 'to_ip bigint,' - 'period bigint,' - 'weight bigint,' - 'transaction_ bigint,' - 'data_src bigint,' - 'branch_type integer,' - 'in_tx boolean,' - 'call_path_id bigint,' - 'insn_count bigint,' - 'cyc_count bigint,' - 'flags integer)') - -if perf_db_export_calls or perf_db_export_callchains: - do_query(query, 'CREATE TABLE call_paths (' - 'id integer NOT NULL PRIMARY KEY,' - 'parent_id bigint,' - 'symbol_id bigint,' - 'ip bigint)') -if perf_db_export_calls: - do_query(query, 'CREATE TABLE calls (' - 'id integer NOT NULL PRIMARY KEY,' - 'thread_id bigint,' - 'comm_id bigint,' - 'call_path_id bigint,' - 'call_time bigint,' - 'return_time bigint,' - 'branch_count bigint,' - 'call_id bigint,' - 'return_id bigint,' - 'parent_call_path_id bigint,' - 'flags integer,' - 'parent_id bigint,' - 'insn_count bigint,' - 'cyc_count bigint)') - -do_query(query, 'CREATE TABLE ptwrite (' - 'id integer NOT NULL PRIMARY KEY,' - 'payload bigint,' - 'exact_ip integer)') - -do_query(query, 'CREATE TABLE cbr (' - 'id integer NOT NULL PRIMARY KEY,' - 'cbr integer,' - 'mhz integer,' - 'percent integer)') - -do_query(query, 'CREATE TABLE mwait (' - 'id integer NOT NULL PRIMARY KEY,' - 'hints integer,' - 'extensions integer)') - -do_query(query, 'CREATE TABLE pwre (' - 'id integer NOT NULL PRIMARY KEY,' - 'cstate integer,' - 'subcstate integer,' - 'hw integer)') - -do_query(query, 'CREATE TABLE exstop (' - 'id integer NOT NULL PRIMARY KEY,' - 'exact_ip integer)') - -do_query(query, 'CREATE TABLE pwrx (' - 'id integer NOT NULL PRIMARY KEY,' - 'deepest_cstate integer,' - 'last_cstate integer,' - 'wake_reason integer)') - -do_query(query, 'CREATE TABLE context_switches (' - 'id integer NOT NULL PRIMARY KEY,' - 'machine_id bigint,' - 'time bigint,' - 'cpu integer,' - 'thread_out_id bigint,' - 'comm_out_id bigint,' - 'thread_in_id bigint,' - 'comm_in_id bigint,' - 'flags integer)') - -# printf was added to sqlite in version 3.8.3 -sqlite_has_printf = False -try: - do_query(query, 'SELECT printf("") FROM machines') - sqlite_has_printf = True -except: - pass - -def emit_to_hex(x): - if sqlite_has_printf: - return 'printf("%x", ' + x + ')' - else: - return x - -do_query(query, 'CREATE VIEW machines_view AS ' - 'SELECT ' - 'id,' - 'pid,' - 'root_dir,' - 'CASE WHEN id=0 THEN \'unknown\' WHEN pid=-1 THEN \'host\' ELSE \'guest\' END AS host_or_guest' - ' FROM machines') - -do_query(query, 'CREATE VIEW dsos_view AS ' - 'SELECT ' - 'id,' - 'machine_id,' - '(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest,' - 'short_name,' - 'long_name,' - 'build_id' - ' FROM dsos') - -do_query(query, 'CREATE VIEW symbols_view AS ' - 'SELECT ' - 'id,' - 'name,' - '(SELECT short_name FROM dsos WHERE id=dso_id) AS dso,' - 'dso_id,' - 'sym_start,' - 'sym_end,' - 'CASE WHEN binding=0 THEN \'local\' WHEN binding=1 THEN \'global\' ELSE \'weak\' END AS binding' - ' FROM symbols') - -do_query(query, 'CREATE VIEW threads_view AS ' - 'SELECT ' - 'id,' - 'machine_id,' - '(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest,' - 'process_id,' - 'pid,' - 'tid' - ' FROM threads') - -do_query(query, 'CREATE VIEW comm_threads_view AS ' - 'SELECT ' - 'comm_id,' - '(SELECT comm FROM comms WHERE id = comm_id) AS command,' - 'thread_id,' - '(SELECT pid FROM threads WHERE id = thread_id) AS pid,' - '(SELECT tid FROM threads WHERE id = thread_id) AS tid' - ' FROM comm_threads') - -if perf_db_export_calls or perf_db_export_callchains: - do_query(query, 'CREATE VIEW call_paths_view AS ' - 'SELECT ' - 'c.id,' - + emit_to_hex('c.ip') + ' AS ip,' - 'c.symbol_id,' - '(SELECT name FROM symbols WHERE id = c.symbol_id) AS symbol,' - '(SELECT dso_id FROM symbols WHERE id = c.symbol_id) AS dso_id,' - '(SELECT dso FROM symbols_view WHERE id = c.symbol_id) AS dso_short_name,' - 'c.parent_id,' - + emit_to_hex('p.ip') + ' AS parent_ip,' - 'p.symbol_id AS parent_symbol_id,' - '(SELECT name FROM symbols WHERE id = p.symbol_id) AS parent_symbol,' - '(SELECT dso_id FROM symbols WHERE id = p.symbol_id) AS parent_dso_id,' - '(SELECT dso FROM symbols_view WHERE id = p.symbol_id) AS parent_dso_short_name' - ' FROM call_paths c INNER JOIN call_paths p ON p.id = c.parent_id') -if perf_db_export_calls: - do_query(query, 'CREATE VIEW calls_view AS ' - 'SELECT ' - 'calls.id,' - 'thread_id,' - '(SELECT pid FROM threads WHERE id = thread_id) AS pid,' - '(SELECT tid FROM threads WHERE id = thread_id) AS tid,' - '(SELECT comm FROM comms WHERE id = comm_id) AS command,' - 'call_path_id,' - + emit_to_hex('ip') + ' AS ip,' - 'symbol_id,' - '(SELECT name FROM symbols WHERE id = symbol_id) AS symbol,' - 'call_time,' - 'return_time,' - 'return_time - call_time AS elapsed_time,' - 'branch_count,' - 'insn_count,' - 'cyc_count,' - 'CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) ELSE ROUND(CAST(insn_count AS FLOAT) / cyc_count, 2) END AS IPC,' - 'call_id,' - 'return_id,' - 'CASE WHEN flags=0 THEN \'\' WHEN flags=1 THEN \'no call\' WHEN flags=2 THEN \'no return\' WHEN flags=3 THEN \'no call/return\' WHEN flags=6 THEN \'jump\' ELSE flags END AS flags,' - 'parent_call_path_id,' - 'calls.parent_id' - ' FROM calls INNER JOIN call_paths ON call_paths.id = call_path_id') - -do_query(query, 'CREATE VIEW samples_view AS ' - 'SELECT ' - 'id,' - 'time,' - 'cpu,' - '(SELECT pid FROM threads WHERE id = thread_id) AS pid,' - '(SELECT tid FROM threads WHERE id = thread_id) AS tid,' - '(SELECT comm FROM comms WHERE id = comm_id) AS command,' - '(SELECT name FROM selected_events WHERE id = evsel_id) AS event,' - + emit_to_hex('ip') + ' AS ip_hex,' - '(SELECT name FROM symbols WHERE id = symbol_id) AS symbol,' - 'sym_offset,' - '(SELECT short_name FROM dsos WHERE id = dso_id) AS dso_short_name,' - + emit_to_hex('to_ip') + ' AS to_ip_hex,' - '(SELECT name FROM symbols WHERE id = to_symbol_id) AS to_symbol,' - 'to_sym_offset,' - '(SELECT short_name FROM dsos WHERE id = to_dso_id) AS to_dso_short_name,' - '(SELECT name FROM branch_types WHERE id = branch_type) AS branch_type_name,' - 'in_tx,' - 'insn_count,' - 'cyc_count,' - 'CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) ELSE ROUND(CAST(insn_count AS FLOAT) / cyc_count, 2) END AS IPC,' - 'flags' - ' FROM samples') - -do_query(query, 'CREATE VIEW ptwrite_view AS ' - 'SELECT ' - 'ptwrite.id,' - 'time,' - 'cpu,' - + emit_to_hex('payload') + ' AS payload_hex,' - 'CASE WHEN exact_ip=0 THEN \'False\' ELSE \'True\' END AS exact_ip' - ' FROM ptwrite' - ' INNER JOIN samples ON samples.id = ptwrite.id') - -do_query(query, 'CREATE VIEW cbr_view AS ' - 'SELECT ' - 'cbr.id,' - 'time,' - 'cpu,' - 'cbr,' - 'mhz,' - 'percent' - ' FROM cbr' - ' INNER JOIN samples ON samples.id = cbr.id') - -do_query(query, 'CREATE VIEW mwait_view AS ' - 'SELECT ' - 'mwait.id,' - 'time,' - 'cpu,' - + emit_to_hex('hints') + ' AS hints_hex,' - + emit_to_hex('extensions') + ' AS extensions_hex' - ' FROM mwait' - ' INNER JOIN samples ON samples.id = mwait.id') - -do_query(query, 'CREATE VIEW pwre_view AS ' - 'SELECT ' - 'pwre.id,' - 'time,' - 'cpu,' - 'cstate,' - 'subcstate,' - 'CASE WHEN hw=0 THEN \'False\' ELSE \'True\' END AS hw' - ' FROM pwre' - ' INNER JOIN samples ON samples.id = pwre.id') - -do_query(query, 'CREATE VIEW exstop_view AS ' - 'SELECT ' - 'exstop.id,' - 'time,' - 'cpu,' - 'CASE WHEN exact_ip=0 THEN \'False\' ELSE \'True\' END AS exact_ip' - ' FROM exstop' - ' INNER JOIN samples ON samples.id = exstop.id') - -do_query(query, 'CREATE VIEW pwrx_view AS ' - 'SELECT ' - 'pwrx.id,' - 'time,' - 'cpu,' - 'deepest_cstate,' - 'last_cstate,' - 'CASE WHEN wake_reason=1 THEN \'Interrupt\'' - ' WHEN wake_reason=2 THEN \'Timer Deadline\'' - ' WHEN wake_reason=4 THEN \'Monitored Address\'' - ' WHEN wake_reason=8 THEN \'HW\'' - ' ELSE wake_reason ' - 'END AS wake_reason' - ' FROM pwrx' - ' INNER JOIN samples ON samples.id = pwrx.id') - -do_query(query, 'CREATE VIEW power_events_view AS ' - 'SELECT ' - 'samples.id,' - 'time,' - 'cpu,' - 'selected_events.name AS event,' - 'CASE WHEN selected_events.name=\'cbr\' THEN (SELECT cbr FROM cbr WHERE cbr.id = samples.id) ELSE "" END AS cbr,' - 'CASE WHEN selected_events.name=\'cbr\' THEN (SELECT mhz FROM cbr WHERE cbr.id = samples.id) ELSE "" END AS mhz,' - 'CASE WHEN selected_events.name=\'cbr\' THEN (SELECT percent FROM cbr WHERE cbr.id = samples.id) ELSE "" END AS percent,' - 'CASE WHEN selected_events.name=\'mwait\' THEN (SELECT ' + emit_to_hex('hints') + ' FROM mwait WHERE mwait.id = samples.id) ELSE "" END AS hints_hex,' - 'CASE WHEN selected_events.name=\'mwait\' THEN (SELECT ' + emit_to_hex('extensions') + ' FROM mwait WHERE mwait.id = samples.id) ELSE "" END AS extensions_hex,' - 'CASE WHEN selected_events.name=\'pwre\' THEN (SELECT cstate FROM pwre WHERE pwre.id = samples.id) ELSE "" END AS cstate,' - 'CASE WHEN selected_events.name=\'pwre\' THEN (SELECT subcstate FROM pwre WHERE pwre.id = samples.id) ELSE "" END AS subcstate,' - 'CASE WHEN selected_events.name=\'pwre\' THEN (SELECT hw FROM pwre WHERE pwre.id = samples.id) ELSE "" END AS hw,' - 'CASE WHEN selected_events.name=\'exstop\' THEN (SELECT exact_ip FROM exstop WHERE exstop.id = samples.id) ELSE "" END AS exact_ip,' - 'CASE WHEN selected_events.name=\'pwrx\' THEN (SELECT deepest_cstate FROM pwrx WHERE pwrx.id = samples.id) ELSE "" END AS deepest_cstate,' - 'CASE WHEN selected_events.name=\'pwrx\' THEN (SELECT last_cstate FROM pwrx WHERE pwrx.id = samples.id) ELSE "" END AS last_cstate,' - 'CASE WHEN selected_events.name=\'pwrx\' THEN (SELECT ' - 'CASE WHEN wake_reason=1 THEN \'Interrupt\'' - ' WHEN wake_reason=2 THEN \'Timer Deadline\'' - ' WHEN wake_reason=4 THEN \'Monitored Address\'' - ' WHEN wake_reason=8 THEN \'HW\'' - ' ELSE wake_reason ' - 'END' - ' FROM pwrx WHERE pwrx.id = samples.id) ELSE "" END AS wake_reason' - ' FROM samples' - ' INNER JOIN selected_events ON selected_events.id = evsel_id' - ' WHERE selected_events.name IN (\'cbr\',\'mwait\',\'exstop\',\'pwre\',\'pwrx\')') - -do_query(query, 'CREATE VIEW context_switches_view AS ' - 'SELECT ' - 'context_switches.id,' - 'context_switches.machine_id,' - 'context_switches.time,' - 'context_switches.cpu,' - 'th_out.pid AS pid_out,' - 'th_out.tid AS tid_out,' - 'comm_out.comm AS comm_out,' - 'th_in.pid AS pid_in,' - 'th_in.tid AS tid_in,' - 'comm_in.comm AS comm_in,' - 'CASE WHEN context_switches.flags = 0 THEN \'in\'' - ' WHEN context_switches.flags = 1 THEN \'out\'' - ' WHEN context_switches.flags = 3 THEN \'out preempt\'' - ' ELSE context_switches.flags ' - 'END AS flags' - ' FROM context_switches' - ' INNER JOIN threads AS th_out ON th_out.id = context_switches.thread_out_id' - ' INNER JOIN threads AS th_in ON th_in.id = context_switches.thread_in_id' - ' INNER JOIN comms AS comm_out ON comm_out.id = context_switches.comm_out_id' - ' INNER JOIN comms AS comm_in ON comm_in.id = context_switches.comm_in_id') - -do_query(query, 'END TRANSACTION') - -evsel_query = QSqlQuery(db) -evsel_query.prepare("INSERT INTO selected_events VALUES (?, ?)") -machine_query = QSqlQuery(db) -machine_query.prepare("INSERT INTO machines VALUES (?, ?, ?)") -thread_query = QSqlQuery(db) -thread_query.prepare("INSERT INTO threads VALUES (?, ?, ?, ?, ?)") -comm_query = QSqlQuery(db) -comm_query.prepare("INSERT INTO comms VALUES (?, ?, ?, ?, ?)") -comm_thread_query = QSqlQuery(db) -comm_thread_query.prepare("INSERT INTO comm_threads VALUES (?, ?, ?)") -dso_query = QSqlQuery(db) -dso_query.prepare("INSERT INTO dsos VALUES (?, ?, ?, ?, ?)") -symbol_query = QSqlQuery(db) -symbol_query.prepare("INSERT INTO symbols VALUES (?, ?, ?, ?, ?, ?)") -branch_type_query = QSqlQuery(db) -branch_type_query.prepare("INSERT INTO branch_types VALUES (?, ?)") -sample_query = QSqlQuery(db) -if branches: - sample_query.prepare("INSERT INTO samples VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") -else: - sample_query.prepare("INSERT INTO samples VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") -if perf_db_export_calls or perf_db_export_callchains: - call_path_query = QSqlQuery(db) - call_path_query.prepare("INSERT INTO call_paths VALUES (?, ?, ?, ?)") -if perf_db_export_calls: - call_query = QSqlQuery(db) - call_query.prepare("INSERT INTO calls VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") -ptwrite_query = QSqlQuery(db) -ptwrite_query.prepare("INSERT INTO ptwrite VALUES (?, ?, ?)") -cbr_query = QSqlQuery(db) -cbr_query.prepare("INSERT INTO cbr VALUES (?, ?, ?, ?)") -mwait_query = QSqlQuery(db) -mwait_query.prepare("INSERT INTO mwait VALUES (?, ?, ?)") -pwre_query = QSqlQuery(db) -pwre_query.prepare("INSERT INTO pwre VALUES (?, ?, ?, ?)") -exstop_query = QSqlQuery(db) -exstop_query.prepare("INSERT INTO exstop VALUES (?, ?)") -pwrx_query = QSqlQuery(db) -pwrx_query.prepare("INSERT INTO pwrx VALUES (?, ?, ?, ?)") -context_switch_query = QSqlQuery(db) -context_switch_query.prepare("INSERT INTO context_switches VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)") - -def trace_begin(): - printdate("Writing records...") - do_query(query, 'BEGIN TRANSACTION') - # id == 0 means unknown. It is easier to create records for them than replace the zeroes with NULLs - evsel_table(0, "unknown") - machine_table(0, 0, "unknown") - thread_table(0, 0, 0, -1, -1) - comm_table(0, "unknown", 0, 0, 0) - dso_table(0, 0, "unknown", "unknown", "") - symbol_table(0, 0, 0, 0, 0, "unknown") - sample_table(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - if perf_db_export_calls or perf_db_export_callchains: - call_path_table(0, 0, 0, 0) - call_return_table(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - -unhandled_count = 0 - -def is_table_empty(table_name): - do_query(query, 'SELECT * FROM ' + table_name + ' LIMIT 1'); - if query.next(): - return False - return True - -def drop(table_name): - do_query(query, 'DROP VIEW ' + table_name + '_view'); - do_query(query, 'DROP TABLE ' + table_name); - -def trace_end(): - do_query(query, 'END TRANSACTION') - - printdate("Adding indexes") - if perf_db_export_calls: - do_query(query, 'CREATE INDEX pcpid_idx ON calls (parent_call_path_id)') - do_query(query, 'CREATE INDEX pid_idx ON calls (parent_id)') - do_query(query, 'ALTER TABLE comms ADD has_calls boolean') - do_query(query, 'UPDATE comms SET has_calls = 1 WHERE comms.id IN (SELECT DISTINCT comm_id FROM calls)') - - printdate("Dropping unused tables") - if is_table_empty("ptwrite"): - drop("ptwrite") - if is_table_empty("mwait") and is_table_empty("pwre") and is_table_empty("exstop") and is_table_empty("pwrx"): - do_query(query, 'DROP VIEW power_events_view'); - drop("mwait") - drop("pwre") - drop("exstop") - drop("pwrx") - if is_table_empty("cbr"): - drop("cbr") - if is_table_empty("context_switches"): - drop("context_switches") - - if (unhandled_count): - printdate("Warning: ", unhandled_count, " unhandled events") - printdate("Done") - -def trace_unhandled(event_name, context, event_fields_dict): - global unhandled_count - unhandled_count += 1 - -def sched__sched_switch(*x): - pass - -def bind_exec(q, n, x): - for xx in x[0:n]: - q.addBindValue(str(xx)) - do_query_(q) - -def evsel_table(*x): - bind_exec(evsel_query, 2, x) - -def machine_table(*x): - bind_exec(machine_query, 3, x) - -def thread_table(*x): - bind_exec(thread_query, 5, x) - -def comm_table(*x): - bind_exec(comm_query, 5, x) - -def comm_thread_table(*x): - bind_exec(comm_thread_query, 3, x) - -def dso_table(*x): - bind_exec(dso_query, 5, x) - -def symbol_table(*x): - bind_exec(symbol_query, 6, x) - -def branch_type_table(*x): - bind_exec(branch_type_query, 2, x) - -def sample_table(*x): - if branches: - for xx in x[0:15]: - sample_query.addBindValue(str(xx)) - for xx in x[19:25]: - sample_query.addBindValue(str(xx)) - do_query_(sample_query) - else: - bind_exec(sample_query, 25, x) - -def call_path_table(*x): - bind_exec(call_path_query, 4, x) - -def call_return_table(*x): - bind_exec(call_query, 14, x) - -def ptwrite(id, raw_buf): - data = struct.unpack_from("> 32) & 0x3 - mwait_query.addBindValue(str(id)) - mwait_query.addBindValue(str(hints)) - mwait_query.addBindValue(str(extensions)) - do_query_(mwait_query) - -def pwre(id, raw_buf): - data = struct.unpack_from("> 7) & 1 - cstate = (payload >> 12) & 0xf - subcstate = (payload >> 8) & 0xf - pwre_query.addBindValue(str(id)) - pwre_query.addBindValue(str(cstate)) - pwre_query.addBindValue(str(subcstate)) - pwre_query.addBindValue(str(hw)) - do_query_(pwre_query) - -def exstop(id, raw_buf): - data = struct.unpack_from("> 4) & 0xf - wake_reason = (payload >> 8) & 0xf - pwrx_query.addBindValue(str(id)) - pwrx_query.addBindValue(str(deepest_cstate)) - pwrx_query.addBindValue(str(last_cstate)) - pwrx_query.addBindValue(str(wake_reason)) - do_query_(pwrx_query) - -def synth_data(id, config, raw_buf, *x): - if config == 0: - ptwrite(id, raw_buf) - elif config == 1: - mwait(id, raw_buf) - elif config == 2: - pwre(id, raw_buf) - elif config == 3: - exstop(id, raw_buf) - elif config == 4: - pwrx(id, raw_buf) - elif config == 5: - cbr(id, raw_buf) - -def context_switch_table(*x): - bind_exec(context_switch_query, 9, x) diff --git a/tools/perf/scripts/python/failed-syscalls-by-pid.py b/tools/perf/scripts/python/failed-syscalls-by-pid.py deleted file mode 100644 index 310efe5e7e23..000000000000 --- a/tools/perf/scripts/python/failed-syscalls-by-pid.py +++ /dev/null @@ -1,79 +0,0 @@ -# failed system call counts, by pid -# (c) 2010, Tom Zanussi -# Licensed under the terms of the GNU GPL License version 2 -# -# Displays system-wide failed system call totals, broken down by pid. -# If a [comm] arg is specified, only syscalls called by [comm] are displayed. - -from __future__ import print_function - -import os -import sys - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * -from Util import * - -usage = "perf script -s syscall-counts-by-pid.py [comm|pid]\n"; - -for_comm = None -for_pid = None - -if len(sys.argv) > 2: - sys.exit(usage) - -if len(sys.argv) > 1: - try: - for_pid = int(sys.argv[1]) - except: - for_comm = sys.argv[1] - -syscalls = autodict() - -def trace_begin(): - print("Press control+C to stop and show the summary") - -def trace_end(): - print_error_totals() - -def raw_syscalls__sys_exit(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, id, ret): - if (for_comm and common_comm != for_comm) or \ - (for_pid and common_pid != for_pid ): - return - - if ret < 0: - try: - syscalls[common_comm][common_pid][id][ret] += 1 - except TypeError: - syscalls[common_comm][common_pid][id][ret] = 1 - -def syscalls__sys_exit(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - id, ret): - raw_syscalls__sys_exit(**locals()) - -def print_error_totals(): - if for_comm is not None: - print("\nsyscall errors for %s:\n" % (for_comm)) - else: - print("\nsyscall errors:\n") - - print("%-30s %10s" % ("comm [pid]", "count")) - print("%-30s %10s" % ("------------------------------", "----------")) - - comm_keys = syscalls.keys() - for comm in comm_keys: - pid_keys = syscalls[comm].keys() - for pid in pid_keys: - print("\n%s [%d]" % (comm, pid)) - id_keys = syscalls[comm][pid].keys() - for id in id_keys: - print(" syscall: %-16s" % syscall_name(id)) - ret_keys = syscalls[comm][pid][id].keys() - for ret, val in sorted(syscalls[comm][pid][id].items(), key = lambda kv: (kv[1], kv[0]), reverse = True): - print(" err = %-20s %10d" % (strerror(ret), val)) diff --git a/tools/perf/scripts/python/flamegraph.py b/tools/perf/scripts/python/flamegraph.py deleted file mode 100755 index ad735990c5be..000000000000 --- a/tools/perf/scripts/python/flamegraph.py +++ /dev/null @@ -1,267 +0,0 @@ -# flamegraph.py - create flame graphs from perf samples -# SPDX-License-Identifier: GPL-2.0 -# -# Usage: -# -# perf record -a -g -F 99 sleep 60 -# perf script report flamegraph -# -# Combined: -# -# perf script flamegraph -a -F 99 sleep 60 -# -# Written by Andreas Gerstmayr -# Flame Graphs invented by Brendan Gregg -# Works in tandem with d3-flame-graph by Martin Spier -# -# pylint: disable=missing-module-docstring -# pylint: disable=missing-class-docstring -# pylint: disable=missing-function-docstring - -import argparse -import hashlib -import io -import json -import os -import subprocess -import sys -from typing import Dict, Optional, Union -import urllib.request - -MINIMAL_HTML = """ - - - -
- - - - -""" - -# pylint: disable=too-few-public-methods -class Node: - def __init__(self, name: str, libtype: str): - self.name = name - # "root" | "kernel" | "" - # "" indicates user space - self.libtype = libtype - self.value: int = 0 - self.children: list[Node] = [] - - def to_json(self) -> Dict[str, Union[str, int, list[Dict]]]: - return { - "n": self.name, - "l": self.libtype, - "v": self.value, - "c": [x.to_json() for x in self.children] - } - - -class FlameGraphCLI: - def __init__(self, args): - self.args = args - self.stack = Node("all", "root") - - @staticmethod - def get_libtype_from_dso(dso: Optional[str]) -> str: - """ - when kernel-debuginfo is installed, - dso points to /usr/lib/debug/lib/modules/*/vmlinux - """ - if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux")): - return "kernel" - - return "" - - @staticmethod - def find_or_create_node(node: Node, name: str, libtype: str) -> Node: - for child in node.children: - if child.name == name: - return child - - child = Node(name, libtype) - node.children.append(child) - return child - - def process_event(self, event) -> None: - # ignore events where the event name does not match - # the one specified by the user - if self.args.event_name and event.get("ev_name") != self.args.event_name: - return - - pid = event.get("sample", {}).get("pid", 0) - # event["dso"] sometimes contains /usr/lib/debug/lib/modules/*/vmlinux - # for user-space processes; let's use pid for kernel or user-space distinction - if pid == 0: - comm = event["comm"] - libtype = "kernel" - else: - comm = f"{event['comm']} ({pid})" - libtype = "" - node = self.find_or_create_node(self.stack, comm, libtype) - - if "callchain" in event: - for entry in reversed(event["callchain"]): - name = entry.get("sym", {}).get("name", "[unknown]") - libtype = self.get_libtype_from_dso(entry.get("dso")) - node = self.find_or_create_node(node, name, libtype) - else: - name = event.get("symbol", "[unknown]") - libtype = self.get_libtype_from_dso(event.get("dso")) - node = self.find_or_create_node(node, name, libtype) - node.value += 1 - - def get_report_header(self) -> str: - if self.args.input == "-": - # when this script is invoked with "perf script flamegraph", - # no perf.data is created and we cannot read the header of it - return "" - - try: - # if the file name other than perf.data is given, - # we read the header of that file - if self.args.input: - output = subprocess.check_output(["perf", "report", "--header-only", - "-i", self.args.input]) - else: - output = subprocess.check_output(["perf", "report", "--header-only"]) - - result = output.decode("utf-8") - if self.args.event_name: - result += "\nFocused event: " + self.args.event_name - return result - except Exception as err: # pylint: disable=broad-except - print(f"Error reading report header: {err}", file=sys.stderr) - return "" - - def trace_end(self) -> None: - stacks_json = json.dumps(self.stack, default=lambda x: x.to_json()) - - if self.args.format == "html": - report_header = self.get_report_header() - options = { - "colorscheme": self.args.colorscheme, - "context": report_header - } - options_json = json.dumps(options) - - template_md5sum = None - if self.args.format == "html": - if os.path.isfile(self.args.template): - template = f"file://{self.args.template}" - else: - if not self.args.allow_download: - print(f"""Warning: Flame Graph template '{self.args.template}' -does not exist. To avoid this please install a package such as the -js-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame -graph template (--template PATH) or use another output format (--format -FORMAT).""", - file=sys.stderr) - if self.args.input == "-": - print( -"""Not attempting to download Flame Graph template as script command line -input is disabled due to using live mode. If you want to download the -template retry without live mode. For example, use 'perf record -a -g --F 99 sleep 60' and 'perf script report flamegraph'. Alternatively, -download the template from: -https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html -and place it at: -/usr/share/d3-flame-graph/d3-flamegraph-base.html""", - file=sys.stderr) - sys.exit(1) - s = None - while s not in ["y", "n"]: - s = input("Do you wish to download a template from cdn.jsdelivr.net?" + - "(this warning can be suppressed with --allow-download) [yn] " - ).lower() - if s == "n": - sys.exit(1) - template = ("https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/" - "d3-flamegraph-base.html") - template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36" - - try: - with urllib.request.urlopen(template) as url_template: - output_str = "".join([ - l.decode("utf-8") for l in url_template.readlines() - ]) - except Exception as err: - print(f"Error reading template {template}: {err}\n" - "a minimal flame graph will be generated", file=sys.stderr) - output_str = MINIMAL_HTML - template_md5sum = None - - if template_md5sum: - download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest() - if download_md5sum != template_md5sum: - s = None - while s not in ["y", "n"]: - s = input(f"""Unexpected template md5sum. -{download_md5sum} != {template_md5sum}, for: -{output_str} -continue?[yn] """).lower() - if s == "n": - sys.exit(1) - - output_str = output_str.replace("/** @options_json **/", options_json) - output_str = output_str.replace("/** @flamegraph_json **/", stacks_json) - - output_fn = self.args.output or "flamegraph.html" - else: - output_str = stacks_json - output_fn = self.args.output or "stacks.json" - - if output_fn == "-": - with io.open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out: - out.write(output_str) - else: - print(f"dumping data to {output_fn}") - try: - with io.open(output_fn, "w", encoding="utf-8") as out: - out.write(output_str) - except IOError as err: - print(f"Error writing output file: {err}", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Create flame graphs.") - parser.add_argument("-f", "--format", - default="html", choices=["json", "html"], - help="output file format") - parser.add_argument("-o", "--output", - help="output file name") - parser.add_argument("--template", - default="/usr/share/d3-flame-graph/d3-flamegraph-base.html", - help="path to flame graph HTML template") - parser.add_argument("--colorscheme", - default="blue-green", - help="flame graph color scheme", - choices=["blue-green", "orange"]) - parser.add_argument("-i", "--input", - help=argparse.SUPPRESS) - parser.add_argument("--allow-download", - default=False, - action="store_true", - help="allow unprompted downloading of HTML template") - parser.add_argument("-e", "--event", - default="", - dest="event_name", - type=str, - help="specify the event to generate flamegraph for") - - cli_args = parser.parse_args() - cli = FlameGraphCLI(cli_args) - - process_event = cli.process_event - trace_end = cli.trace_end diff --git a/tools/perf/scripts/python/futex-contention.py b/tools/perf/scripts/python/futex-contention.py deleted file mode 100644 index 7e884d46f920..000000000000 --- a/tools/perf/scripts/python/futex-contention.py +++ /dev/null @@ -1,57 +0,0 @@ -# futex contention -# (c) 2010, Arnaldo Carvalho de Melo -# Licensed under the terms of the GNU GPL License version 2 -# -# Translation of: -# -# http://sourceware.org/systemtap/wiki/WSFutexContention -# -# to perf python scripting. -# -# Measures futex contention - -from __future__ import print_function - -import os -import sys -sys.path.append(os.environ['PERF_EXEC_PATH'] + - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') -from Util import * - -process_names = {} -thread_thislock = {} -thread_blocktime = {} - -lock_waits = {} # long-lived stats on (tid,lock) blockage elapsed time -process_names = {} # long-lived pid-to-execname mapping - - -def syscalls__sys_enter_futex(event, ctxt, cpu, s, ns, tid, comm, callchain, - nr, uaddr, op, val, utime, uaddr2, val3): - cmd = op & FUTEX_CMD_MASK - if cmd != FUTEX_WAIT: - return # we don't care about originators of WAKE events - - process_names[tid] = comm - thread_thislock[tid] = uaddr - thread_blocktime[tid] = nsecs(s, ns) - - -def syscalls__sys_exit_futex(event, ctxt, cpu, s, ns, tid, comm, callchain, - nr, ret): - if tid in thread_blocktime: - elapsed = nsecs(s, ns) - thread_blocktime[tid] - add_stats(lock_waits, (tid, thread_thislock[tid]), elapsed) - del thread_blocktime[tid] - del thread_thislock[tid] - - -def trace_begin(): - print("Press control+C to stop and show the summary") - - -def trace_end(): - for (tid, lock) in lock_waits: - min, max, avg, count = lock_waits[tid, lock] - print("%s[%d] lock %x contended %d times, %d avg ns [max: %d ns, min %d ns]" % - (process_names[tid], tid, lock, count, avg, max, min)) diff --git a/tools/perf/scripts/python/gecko.py b/tools/perf/scripts/python/gecko.py deleted file mode 100644 index bc5a72f94bfa..000000000000 --- a/tools/perf/scripts/python/gecko.py +++ /dev/null @@ -1,395 +0,0 @@ -# gecko.py - Convert perf record output to Firefox's gecko profile format -# SPDX-License-Identifier: GPL-2.0 -# -# The script converts perf.data to Gecko Profile Format, -# which can be read by https://profiler.firefox.com/. -# -# Usage: -# -# perf record -a -g -F 99 sleep 60 -# perf script report gecko -# -# Combined: -# -# perf script gecko -F 99 -a sleep 60 - -import os -import sys -import time -import json -import string -import random -import argparse -import threading -import webbrowser -import urllib.parse -from os import system -from functools import reduce -from dataclasses import dataclass, field -from http.server import HTTPServer, SimpleHTTPRequestHandler, test -from typing import List, Dict, Optional, NamedTuple, Set, Tuple, Any - -# Add the Perf-Trace-Util library to the Python path -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * - -StringID = int -StackID = int -FrameID = int -CategoryID = int -Milliseconds = float - -# start_time is intialiazed only once for the all event traces. -start_time = None - -# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/profile.js#L425 -# Follow Brendan Gregg's Flamegraph convention: orange for kernel and yellow for user space by default. -CATEGORIES = None - -# The product name is used by the profiler UI to show the Operating system and Processor. -PRODUCT = os.popen('uname -op').read().strip() - -# store the output file -output_file = None - -# Here key = tid, value = Thread -tid_to_thread = dict() - -# The HTTP server is used to serve the profile to the profiler UI. -http_server_thread = None - -# The category index is used by the profiler UI to show the color of the flame graph. -USER_CATEGORY_INDEX = 0 -KERNEL_CATEGORY_INDEX = 1 - -# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 -class Frame(NamedTuple): - string_id: StringID - relevantForJS: bool - innerWindowID: int - implementation: None - optimizations: None - line: None - column: None - category: CategoryID - subcategory: int - -# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 -class Stack(NamedTuple): - prefix_id: Optional[StackID] - frame_id: FrameID - -# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 -class Sample(NamedTuple): - stack_id: Optional[StackID] - time_ms: Milliseconds - responsiveness: int - -@dataclass -class Thread: - """A builder for a profile of the thread. - - Attributes: - comm: Thread command-line (name). - pid: process ID of containing process. - tid: thread ID. - samples: Timeline of profile samples. - frameTable: interned stack frame ID -> stack frame. - stringTable: interned string ID -> string. - stringMap: interned string -> string ID. - stackTable: interned stack ID -> stack. - stackMap: (stack prefix ID, leaf stack frame ID) -> interned Stack ID. - frameMap: Stack Frame string -> interned Frame ID. - comm: str - pid: int - tid: int - samples: List[Sample] = field(default_factory=list) - frameTable: List[Frame] = field(default_factory=list) - stringTable: List[str] = field(default_factory=list) - stringMap: Dict[str, int] = field(default_factory=dict) - stackTable: List[Stack] = field(default_factory=list) - stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) - frameMap: Dict[str, int] = field(default_factory=dict) - """ - comm: str - pid: int - tid: int - samples: List[Sample] = field(default_factory=list) - frameTable: List[Frame] = field(default_factory=list) - stringTable: List[str] = field(default_factory=list) - stringMap: Dict[str, int] = field(default_factory=dict) - stackTable: List[Stack] = field(default_factory=list) - stackMap: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict) - frameMap: Dict[str, int] = field(default_factory=dict) - - def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int: - """Gets a matching stack, or saves the new stack. Returns a Stack ID.""" - key = f"{frame_id}" if prefix_id is None else f"{frame_id},{prefix_id}" - # key = (prefix_id, frame_id) - stack_id = self.stackMap.get(key) - if stack_id is None: - # return stack_id - stack_id = len(self.stackTable) - self.stackTable.append(Stack(prefix_id=prefix_id, frame_id=frame_id)) - self.stackMap[key] = stack_id - return stack_id - - def _intern_string(self, string: str) -> int: - """Gets a matching string, or saves the new string. Returns a String ID.""" - string_id = self.stringMap.get(string) - if string_id is not None: - return string_id - string_id = len(self.stringTable) - self.stringTable.append(string) - self.stringMap[string] = string_id - return string_id - - def _intern_frame(self, frame_str: str) -> int: - """Gets a matching stack frame, or saves the new frame. Returns a Frame ID.""" - frame_id = self.frameMap.get(frame_str) - if frame_id is not None: - return frame_id - frame_id = len(self.frameTable) - self.frameMap[frame_str] = frame_id - string_id = self._intern_string(frame_str) - - symbol_name_to_category = KERNEL_CATEGORY_INDEX if frame_str.find('kallsyms') != -1 \ - or frame_str.find('/vmlinux') != -1 \ - or frame_str.endswith('.ko)') \ - else USER_CATEGORY_INDEX - - self.frameTable.append(Frame( - string_id=string_id, - relevantForJS=False, - innerWindowID=0, - implementation=None, - optimizations=None, - line=None, - column=None, - category=symbol_name_to_category, - subcategory=None, - )) - return frame_id - - def _add_sample(self, comm: str, stack: List[str], time_ms: Milliseconds) -> None: - """Add a timestamped stack trace sample to the thread builder. - Args: - comm: command-line (name) of the thread at this sample - stack: sampled stack frames. Root first, leaf last. - time_ms: timestamp of sample in milliseconds. - """ - # Ihreads may not set their names right after they are created. - # Instead, they might do it later. In such situations, to use the latest name they have set. - if self.comm != comm: - self.comm = comm - - prefix_stack_id = reduce(lambda prefix_id, frame: self._intern_stack - (self._intern_frame(frame), prefix_id), stack, None) - if prefix_stack_id is not None: - self.samples.append(Sample(stack_id=prefix_stack_id, - time_ms=time_ms, - responsiveness=0)) - - def _to_json_dict(self) -> Dict: - """Converts current Thread to GeckoThread JSON format.""" - # Gecko profile format is row-oriented data as List[List], - # And a schema for interpreting each index. - # Schema: - # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/gecko-profile-format.md - # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L230 - return { - "tid": self.tid, - "pid": self.pid, - "name": self.comm, - # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L51 - "markers": { - "schema": { - "name": 0, - "startTime": 1, - "endTime": 2, - "phase": 3, - "category": 4, - "data": 5, - }, - "data": [], - }, - - # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90 - "samples": { - "schema": { - "stack": 0, - "time": 1, - "responsiveness": 2, - }, - "data": self.samples - }, - - # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156 - "frameTable": { - "schema": { - "location": 0, - "relevantForJS": 1, - "innerWindowID": 2, - "implementation": 3, - "optimizations": 4, - "line": 5, - "column": 6, - "category": 7, - "subcategory": 8, - }, - "data": self.frameTable, - }, - - # https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216 - "stackTable": { - "schema": { - "prefix": 0, - "frame": 1, - }, - "data": self.stackTable, - }, - "stringTable": self.stringTable, - "registerTime": 0, - "unregisterTime": None, - "processType": "default", - } - -# Uses perf script python interface to parse each -# event and store the data in the thread builder. -def process_event(param_dict: Dict) -> None: - global start_time - global tid_to_thread - time_stamp = (param_dict['sample']['time'] // 1000) / 1000 - pid = param_dict['sample']['pid'] - tid = param_dict['sample']['tid'] - comm = param_dict['comm'] - - # Start time is the time of the first sample - if not start_time: - start_time = time_stamp - - # Parse and append the callchain of the current sample into a stack. - stack = [] - if param_dict['callchain']: - for call in param_dict['callchain']: - if 'sym' not in call: - continue - stack.append(f'{call["sym"]["name"]} (in {call["dso"]})') - if len(stack) != 0: - # Reverse the stack, as root come first and the leaf at the end. - stack = stack[::-1] - - # During perf record if -g is not used, the callchain is not available. - # In that case, the symbol and dso are available in the event parameters. - else: - func = param_dict['symbol'] if 'symbol' in param_dict else '[unknown]' - dso = param_dict['dso'] if 'dso' in param_dict else '[unknown]' - stack.append(f'{func} (in {dso})') - - # Add sample to the specific thread. - thread = tid_to_thread.get(tid) - if thread is None: - thread = Thread(comm=comm, pid=pid, tid=tid) - tid_to_thread[tid] = thread - thread._add_sample(comm=comm, stack=stack, time_ms=time_stamp) - -def trace_begin() -> None: - global output_file - if (output_file is None): - print("Staring Firefox Profiler on your default browser...") - global http_server_thread - http_server_thread = threading.Thread(target=test, args=(CORSRequestHandler, HTTPServer,)) - http_server_thread.daemon = True - http_server_thread.start() - -# Trace_end runs at the end and will be used to aggregate -# the data into the final json object and print it out to stdout. -def trace_end() -> None: - global output_file - threads = [thread._to_json_dict() for thread in tid_to_thread.values()] - - # Schema: https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L305 - gecko_profile_with_meta = { - "meta": { - "interval": 1, - "processType": 0, - "product": PRODUCT, - "stackwalk": 1, - "debug": 0, - "gcpoison": 0, - "asyncstack": 1, - "startTime": start_time, - "shutdownTime": None, - "version": 24, - "presymbolicated": True, - "categories": CATEGORIES, - "markerSchema": [], - }, - "libs": [], - "threads": threads, - "processes": [], - "pausedRanges": [], - } - # launch the profiler on local host if not specified --save-only args, otherwise print to file - if (output_file is None): - output_file = 'gecko_profile.json' - with open(output_file, 'w') as f: - json.dump(gecko_profile_with_meta, f, indent=2) - launchFirefox(output_file) - time.sleep(1) - print(f'[ perf gecko: Captured and wrote into {output_file} ]') - else: - print(f'[ perf gecko: Captured and wrote into {output_file} ]') - with open(output_file, 'w') as f: - json.dump(gecko_profile_with_meta, f, indent=2) - -# Used to enable Cross-Origin Resource Sharing (CORS) for requests coming from 'https://profiler.firefox.com', allowing it to access resources from this server. -class CORSRequestHandler(SimpleHTTPRequestHandler): - def end_headers (self): - self.send_header('Access-Control-Allow-Origin', 'https://profiler.firefox.com') - SimpleHTTPRequestHandler.end_headers(self) - -# start a local server to serve the gecko_profile.json file to the profiler.firefox.com -def launchFirefox(file): - safe_string = urllib.parse.quote_plus(f'http://localhost:8000/{file}') - url = 'https://profiler.firefox.com/from-url/' + safe_string - webbrowser.open(f'{url}') - -def main() -> None: - global output_file - global CATEGORIES - parser = argparse.ArgumentParser(description="Convert perf.data to Firefox\'s Gecko Profile format which can be uploaded to profiler.firefox.com for visualization") - - # Add the command-line options - # Colors must be defined according to this: - # https://github.com/firefox-devtools/profiler/blob/50124adbfa488adba6e2674a8f2618cf34b59cd2/res/css/categories.css - parser.add_argument('--user-color', default='yellow', help='Color for the User category', choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red', 'grey', 'magenta']) - parser.add_argument('--kernel-color', default='orange', help='Color for the Kernel category', choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red', 'grey', 'magenta']) - # If --save-only is specified, the output will be saved to a file instead of opening Firefox's profiler directly. - parser.add_argument('--save-only', help='Save the output to a file instead of opening Firefox\'s profiler') - - # Parse the command-line arguments - args = parser.parse_args() - # Access the values provided by the user - user_color = args.user_color - kernel_color = args.kernel_color - output_file = args.save_only - - CATEGORIES = [ - { - "name": 'User', - "color": user_color, - "subcategories": ['Other'] - }, - { - "name": 'Kernel', - "color": kernel_color, - "subcategories": ['Other'] - }, - ] - -if __name__ == '__main__': - main() diff --git a/tools/perf/scripts/python/intel-pt-events.py b/tools/perf/scripts/python/intel-pt-events.py deleted file mode 100644 index 346c89bd16d6..000000000000 --- a/tools/perf/scripts/python/intel-pt-events.py +++ /dev/null @@ -1,494 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -# intel-pt-events.py: Print Intel PT Events including Power Events and PTWRITE -# Copyright (c) 2017-2021, Intel Corporation. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms and conditions of the GNU General Public License, -# version 2, as published by the Free Software Foundation. -# -# This program is distributed in the hope it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. - -from __future__ import division, print_function - -import io -import os -import sys -import struct -import argparse -import contextlib - -from libxed import LibXED -from ctypes import create_string_buffer, addressof - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import perf_set_itrace_options, \ - perf_sample_insn, perf_sample_srccode - -try: - broken_pipe_exception = BrokenPipeError -except: - broken_pipe_exception = IOError - -glb_switch_str = {} -glb_insn = False -glb_disassembler = None -glb_src = False -glb_source_file_name = None -glb_line_number = None -glb_dso = None -glb_stash_dict = {} -glb_output = None -glb_output_pos = 0 -glb_cpu = -1 -glb_time = 0 - -def get_optional_null(perf_dict, field): - if field in perf_dict: - return perf_dict[field] - return "" - -def get_optional_zero(perf_dict, field): - if field in perf_dict: - return perf_dict[field] - return 0 - -def get_optional_bytes(perf_dict, field): - if field in perf_dict: - return perf_dict[field] - return bytes() - -def get_optional(perf_dict, field): - if field in perf_dict: - return perf_dict[field] - return "[unknown]" - -def get_offset(perf_dict, field): - if field in perf_dict: - return "+%#x" % perf_dict[field] - return "" - -def trace_begin(): - ap = argparse.ArgumentParser(usage = "", add_help = False) - ap.add_argument("--insn-trace", action='store_true') - ap.add_argument("--src-trace", action='store_true') - ap.add_argument("--all-switch-events", action='store_true') - ap.add_argument("--interleave", type=int, nargs='?', const=4, default=0) - global glb_args - global glb_insn - global glb_src - glb_args = ap.parse_args() - if glb_args.insn_trace: - print("Intel PT Instruction Trace") - itrace = "i0nsepwxI" - glb_insn = True - elif glb_args.src_trace: - print("Intel PT Source Trace") - itrace = "i0nsepwxI" - glb_insn = True - glb_src = True - else: - print("Intel PT Branch Trace, Power Events, Event Trace and PTWRITE") - itrace = "bepwxI" - global glb_disassembler - try: - glb_disassembler = LibXED() - except: - glb_disassembler = None - perf_set_itrace_options(perf_script_context, itrace) - -def trace_end(): - if glb_args.interleave: - flush_stashed_output() - print("End") - -def trace_unhandled(event_name, context, event_fields_dict): - print(' '.join(['%s=%s'%(k,str(v))for k,v in sorted(event_fields_dict.items())])) - -def stash_output(): - global glb_stash_dict - global glb_output_pos - output_str = glb_output.getvalue()[glb_output_pos:] - n = len(output_str) - if n: - glb_output_pos += n - if glb_cpu not in glb_stash_dict: - glb_stash_dict[glb_cpu] = [] - glb_stash_dict[glb_cpu].append(output_str) - -def flush_stashed_output(): - global glb_stash_dict - while glb_stash_dict: - cpus = list(glb_stash_dict.keys()) - # Output at most glb_args.interleave output strings per cpu - for cpu in cpus: - items = glb_stash_dict[cpu] - countdown = glb_args.interleave - while len(items) and countdown: - sys.stdout.write(items[0]) - del items[0] - countdown -= 1 - if not items: - del glb_stash_dict[cpu] - -def print_ptwrite(raw_buf): - data = struct.unpack_from("> 32) & 0x3 - print("hints: %#x extensions: %#x" % (hints, extensions), end=' ') - -def print_pwre(raw_buf): - data = struct.unpack_from("> 7) & 1 - cstate = (payload >> 12) & 0xf - subcstate = (payload >> 8) & 0xf - print("hw: %u cstate: %u sub-cstate: %u" % (hw, cstate, subcstate), - end=' ') - -def print_exstop(raw_buf): - data = struct.unpack_from("> 4) & 0xf - wake_reason = (payload >> 8) & 0xf - print("deepest cstate: %u last cstate: %u wake reason: %#x" % - (deepest_cstate, last_cstate, wake_reason), end=' ') - -def print_psb(raw_buf): - data = struct.unpack_from("> 7 - vector = data[1] - evd_cnt = data[2] - s = glb_cfe[typ] - if s: - print(" cfe: %s IP: %u vector: %u" % (s, ip_flag, vector), end=' ') - else: - print(" cfe: %u IP: %u vector: %u" % (typ, ip_flag, vector), end=' ') - pos = 4 - for i in range(evd_cnt): - data = struct.unpack_from("%u %s branch" % (old_iflag, iflag, s), end=' ') - -def common_start_str(comm, sample): - ts = sample["time"] - cpu = sample["cpu"] - pid = sample["pid"] - tid = sample["tid"] - if "machine_pid" in sample: - machine_pid = sample["machine_pid"] - vcpu = sample["vcpu"] - return "VM:%5d VCPU:%03d %16s %5u/%-5u [%03u] %9u.%09u " % (machine_pid, vcpu, comm, pid, tid, cpu, ts / 1000000000, ts %1000000000) - else: - return "%16s %5u/%-5u [%03u] %9u.%09u " % (comm, pid, tid, cpu, ts / 1000000000, ts %1000000000) - -def print_common_start(comm, sample, name): - flags_disp = get_optional_null(sample, "flags_disp") - # Unused fields: - # period = sample["period"] - # phys_addr = sample["phys_addr"] - # weight = sample["weight"] - # transaction = sample["transaction"] - # cpumode = get_optional_zero(sample, "cpumode") - print(common_start_str(comm, sample) + "%8s %21s" % (name, flags_disp), end=' ') - -def print_instructions_start(comm, sample): - if "x" in get_optional_null(sample, "flags"): - print(common_start_str(comm, sample) + "x", end=' ') - else: - print(common_start_str(comm, sample), end=' ') - -def disassem(insn, ip): - inst = glb_disassembler.Instruction() - glb_disassembler.SetMode(inst, 0) # Assume 64-bit - buf = create_string_buffer(64) - buf.value = insn - return glb_disassembler.DisassembleOne(inst, addressof(buf), len(insn), ip) - -def print_common_ip(param_dict, sample, symbol, dso): - ip = sample["ip"] - offs = get_offset(param_dict, "symoff") - if "cyc_cnt" in sample: - cyc_cnt = sample["cyc_cnt"] - insn_cnt = get_optional_zero(sample, "insn_cnt") - ipc_str = " IPC: %#.2f (%u/%u)" % (insn_cnt / cyc_cnt, insn_cnt, cyc_cnt) - else: - ipc_str = "" - if glb_insn and glb_disassembler is not None: - insn = perf_sample_insn(perf_script_context) - if insn and len(insn): - cnt, text = disassem(insn, ip) - byte_str = ("%x" % ip).rjust(16) - if sys.version_info.major >= 3: - for k in range(cnt): - byte_str += " %02x" % insn[k] - else: - for k in xrange(cnt): - byte_str += " %02x" % ord(insn[k]) - print("%-40s %-30s" % (byte_str, text), end=' ') - print("%s%s (%s)" % (symbol, offs, dso), end=' ') - else: - print("%16x %s%s (%s)" % (ip, symbol, offs, dso), end=' ') - if "addr_correlates_sym" in sample: - addr = sample["addr"] - dso = get_optional(sample, "addr_dso") - symbol = get_optional(sample, "addr_symbol") - offs = get_offset(sample, "addr_symoff") - print("=> %x %s%s (%s)%s" % (addr, symbol, offs, dso, ipc_str)) - else: - print(ipc_str) - -def print_srccode(comm, param_dict, sample, symbol, dso, with_insn): - ip = sample["ip"] - if symbol == "[unknown]": - start_str = common_start_str(comm, sample) + ("%x" % ip).rjust(16).ljust(40) - else: - offs = get_offset(param_dict, "symoff") - start_str = common_start_str(comm, sample) + (symbol + offs).ljust(40) - - if with_insn and glb_insn and glb_disassembler is not None: - insn = perf_sample_insn(perf_script_context) - if insn and len(insn): - cnt, text = disassem(insn, ip) - start_str += text.ljust(30) - - global glb_source_file_name - global glb_line_number - global glb_dso - - source_file_name, line_number, source_line = perf_sample_srccode(perf_script_context) - if source_file_name: - if glb_line_number == line_number and glb_source_file_name == source_file_name: - src_str = "" - else: - if len(source_file_name) > 40: - src_file = ("..." + source_file_name[-37:]) + " " - else: - src_file = source_file_name.ljust(41) - if source_line is None: - src_str = src_file + str(line_number).rjust(4) + " " - else: - src_str = src_file + str(line_number).rjust(4) + " " + source_line - glb_dso = None - elif dso == glb_dso: - src_str = "" - else: - src_str = dso - glb_dso = dso - - glb_line_number = line_number - glb_source_file_name = source_file_name - - print(start_str, src_str) - -def do_process_event(param_dict): - sample = param_dict["sample"] - raw_buf = param_dict["raw_buf"] - comm = param_dict["comm"] - name = param_dict["ev_name"] - # Unused fields: - # callchain = param_dict["callchain"] - # brstack = param_dict["brstack"] - # brstacksym = param_dict["brstacksym"] - # event_attr = param_dict["attr"] - - # Symbol and dso info are not always resolved - dso = get_optional(param_dict, "dso") - symbol = get_optional(param_dict, "symbol") - - cpu = sample["cpu"] - if cpu in glb_switch_str: - print(glb_switch_str[cpu]) - del glb_switch_str[cpu] - - if name.startswith("instructions"): - if glb_src: - print_srccode(comm, param_dict, sample, symbol, dso, True) - else: - print_instructions_start(comm, sample) - print_common_ip(param_dict, sample, symbol, dso) - elif name.startswith("branches"): - if glb_src: - print_srccode(comm, param_dict, sample, symbol, dso, False) - else: - print_common_start(comm, sample, name) - print_common_ip(param_dict, sample, symbol, dso) - elif name == "ptwrite": - print_common_start(comm, sample, name) - print_ptwrite(raw_buf) - print_common_ip(param_dict, sample, symbol, dso) - elif name == "cbr": - print_common_start(comm, sample, name) - print_cbr(raw_buf) - print_common_ip(param_dict, sample, symbol, dso) - elif name == "mwait": - print_common_start(comm, sample, name) - print_mwait(raw_buf) - print_common_ip(param_dict, sample, symbol, dso) - elif name == "pwre": - print_common_start(comm, sample, name) - print_pwre(raw_buf) - print_common_ip(param_dict, sample, symbol, dso) - elif name == "exstop": - print_common_start(comm, sample, name) - print_exstop(raw_buf) - print_common_ip(param_dict, sample, symbol, dso) - elif name == "pwrx": - print_common_start(comm, sample, name) - print_pwrx(raw_buf) - print_common_ip(param_dict, sample, symbol, dso) - elif name == "psb": - print_common_start(comm, sample, name) - print_psb(raw_buf) - print_common_ip(param_dict, sample, symbol, dso) - elif name == "evt": - print_common_start(comm, sample, name) - print_evt(raw_buf) - print_common_ip(param_dict, sample, symbol, dso) - elif name == "iflag": - print_common_start(comm, sample, name) - print_iflag(raw_buf) - print_common_ip(param_dict, sample, symbol, dso) - else: - print_common_start(comm, sample, name) - print_common_ip(param_dict, sample, symbol, dso) - -def interleave_events(param_dict): - global glb_cpu - global glb_time - global glb_output - global glb_output_pos - - sample = param_dict["sample"] - glb_cpu = sample["cpu"] - ts = sample["time"] - - if glb_time != ts: - glb_time = ts - flush_stashed_output() - - glb_output_pos = 0 - with contextlib.redirect_stdout(io.StringIO()) as glb_output: - do_process_event(param_dict) - - stash_output() - -def process_event(param_dict): - try: - if glb_args.interleave: - interleave_events(param_dict) - else: - do_process_event(param_dict) - except broken_pipe_exception: - # Stop python printing broken pipe errors and traceback - sys.stdout = open(os.devnull, 'w') - sys.exit(1) - -def auxtrace_error(typ, code, cpu, pid, tid, ip, ts, msg, cpumode, *x): - if glb_args.interleave: - flush_stashed_output() - if len(x) >= 2 and x[0]: - machine_pid = x[0] - vcpu = x[1] - else: - machine_pid = 0 - vcpu = -1 - try: - if machine_pid: - print("VM:%5d VCPU:%03d %16s %5u/%-5u [%03u] %9u.%09u error type %u code %u: %s ip 0x%16x" % - (machine_pid, vcpu, "Trace error", pid, tid, cpu, ts / 1000000000, ts %1000000000, typ, code, msg, ip)) - else: - print("%16s %5u/%-5u [%03u] %9u.%09u error type %u code %u: %s ip 0x%16x" % - ("Trace error", pid, tid, cpu, ts / 1000000000, ts %1000000000, typ, code, msg, ip)) - except broken_pipe_exception: - # Stop python printing broken pipe errors and traceback - sys.stdout = open(os.devnull, 'w') - sys.exit(1) - -def context_switch(ts, cpu, pid, tid, np_pid, np_tid, machine_pid, out, out_preempt, *x): - if glb_args.interleave: - flush_stashed_output() - if out: - out_str = "Switch out " - else: - out_str = "Switch In " - if out_preempt: - preempt_str = "preempt" - else: - preempt_str = "" - if len(x) >= 2 and x[0]: - machine_pid = x[0] - vcpu = x[1] - else: - vcpu = None; - if machine_pid == -1: - machine_str = "" - elif vcpu is None: - machine_str = "machine PID %d" % machine_pid - else: - machine_str = "machine PID %d VCPU %d" % (machine_pid, vcpu) - switch_str = "%16s %5d/%-5d [%03u] %9u.%09u %5d/%-5d %s %s" % \ - (out_str, pid, tid, cpu, ts / 1000000000, ts %1000000000, np_pid, np_tid, machine_str, preempt_str) - if glb_args.all_switch_events: - print(switch_str) - else: - global glb_switch_str - glb_switch_str[cpu] = switch_str diff --git a/tools/perf/scripts/python/libxed.py b/tools/perf/scripts/python/libxed.py deleted file mode 100644 index 2c70a5a7eb9c..000000000000 --- a/tools/perf/scripts/python/libxed.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python -# SPDX-License-Identifier: GPL-2.0 -# libxed.py: Python wrapper for libxed.so -# Copyright (c) 2014-2021, Intel Corporation. - -# To use Intel XED, libxed.so must be present. To build and install -# libxed.so: -# git clone https://github.com/intelxed/mbuild.git mbuild -# git clone https://github.com/intelxed/xed -# cd xed -# ./mfile.py --share -# sudo ./mfile.py --prefix=/usr/local install -# sudo ldconfig -# - -import sys - -from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \ - c_void_p, c_bool, c_byte, c_char, c_int, c_uint, c_longlong, c_ulonglong - -# XED Disassembler - -class xed_state_t(Structure): - - _fields_ = [ - ("mode", c_int), - ("width", c_int) - ] - -class XEDInstruction(): - - def __init__(self, libxed): - # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion - xedd_t = c_byte * 512 - self.xedd = xedd_t() - self.xedp = addressof(self.xedd) - libxed.xed_decoded_inst_zero(self.xedp) - self.state = xed_state_t() - self.statep = addressof(self.state) - # Buffer for disassembled instruction text - self.buffer = create_string_buffer(256) - self.bufferp = addressof(self.buffer) - -class LibXED(): - - def __init__(self): - try: - self.libxed = CDLL("libxed.so") - except: - self.libxed = None - if not self.libxed: - self.libxed = CDLL("/usr/local/lib/libxed.so") - - self.xed_tables_init = self.libxed.xed_tables_init - self.xed_tables_init.restype = None - self.xed_tables_init.argtypes = [] - - self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero - self.xed_decoded_inst_zero.restype = None - self.xed_decoded_inst_zero.argtypes = [ c_void_p ] - - self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode - self.xed_operand_values_set_mode.restype = None - self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ] - - self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode - self.xed_decoded_inst_zero_keep_mode.restype = None - self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ] - - self.xed_decode = self.libxed.xed_decode - self.xed_decode.restype = c_int - self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ] - - self.xed_format_context = self.libxed.xed_format_context - self.xed_format_context.restype = c_uint - self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ] - - self.xed_tables_init() - - def Instruction(self): - return XEDInstruction(self) - - def SetMode(self, inst, mode): - if mode: - inst.state.mode = 4 # 32-bit - inst.state.width = 4 # 4 bytes - else: - inst.state.mode = 1 # 64-bit - inst.state.width = 8 # 8 bytes - self.xed_operand_values_set_mode(inst.xedp, inst.statep) - - def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip): - self.xed_decoded_inst_zero_keep_mode(inst.xedp) - err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt) - if err: - return 0, "" - # Use AT&T mode (2), alternative is Intel (3) - ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0) - if not ok: - return 0, "" - if sys.version_info[0] == 2: - result = inst.buffer.value - else: - result = inst.buffer.value.decode() - # Return instruction length and the disassembled instruction text - # For now, assume the length is in byte 166 - return inst.xedd[166], result diff --git a/tools/perf/scripts/python/mem-phys-addr.py b/tools/perf/scripts/python/mem-phys-addr.py deleted file mode 100644 index 5e237a5a5f1b..000000000000 --- a/tools/perf/scripts/python/mem-phys-addr.py +++ /dev/null @@ -1,127 +0,0 @@ -# mem-phys-addr.py: Resolve physical address samples -# SPDX-License-Identifier: GPL-2.0 -# -# Copyright (c) 2018, Intel Corporation. - -import os -import sys -import re -import bisect -import collections -from dataclasses import dataclass -from typing import (Dict, Optional) - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -@dataclass(frozen=True) -class IomemEntry: - """Read from a line in /proc/iomem""" - begin: int - end: int - indent: int - label: str - -# Physical memory layout from /proc/iomem. Key is the indent and then -# a list of ranges. -iomem: Dict[int, list[IomemEntry]] = collections.defaultdict(list) -# Child nodes from the iomem parent. -children: Dict[IomemEntry, set[IomemEntry]] = collections.defaultdict(set) -# Maximum indent seen before an entry in the iomem file. -max_indent: int = 0 -# Count for each range of memory. -load_mem_type_cnt: Dict[IomemEntry, int] = collections.Counter() -# Perf event name set from the first sample in the data. -event_name: Optional[str] = None - -def parse_iomem(): - """Populate iomem from /proc/iomem file""" - global iomem - global max_indent - global children - with open('/proc/iomem', 'r', encoding='ascii') as f: - for line in f: - indent = 0 - while line[indent] == ' ': - indent += 1 - if indent > max_indent: - max_indent = indent - m = re.split('-|:', line, 2) - begin = int(m[0], 16) - end = int(m[1], 16) - label = m[2].strip() - entry = IomemEntry(begin, end, indent, label) - # Before adding entry, search for a parent node using its begin. - if indent > 0: - parent = find_memory_type(begin) - assert parent, f"Given indent expected a parent for {label}" - children[parent].add(entry) - iomem[indent].append(entry) - -def find_memory_type(phys_addr) -> Optional[IomemEntry]: - """Search iomem for the range containing phys_addr with the maximum indent""" - for i in range(max_indent, -1, -1): - if i not in iomem: - continue - position = bisect.bisect_right(iomem[i], phys_addr, - key=lambda entry: entry.begin) - if position is None: - continue - iomem_entry = iomem[i][position-1] - if iomem_entry.begin <= phys_addr <= iomem_entry.end: - return iomem_entry - print(f"Didn't find {phys_addr}") - return None - -def print_memory_type(): - print(f"Event: {event_name}") - print(f"{'Memory type':<40} {'count':>10} {'percentage':>10}") - print(f"{'-' * 40:<40} {'-' * 10:>10} {'-' * 10:>10}") - total = sum(load_mem_type_cnt.values()) - # Add count from children into the parent. - for i in range(max_indent, -1, -1): - if i not in iomem: - continue - for entry in iomem[i]: - global children - for child in children[entry]: - if load_mem_type_cnt[child] > 0: - load_mem_type_cnt[entry] += load_mem_type_cnt[child] - - def print_entries(entries): - """Print counts from parents down to their children""" - global children - for entry in sorted(entries, - key = lambda entry: load_mem_type_cnt[entry], - reverse = True): - count = load_mem_type_cnt[entry] - if count > 0: - mem_type = ' ' * entry.indent + f"{entry.begin:x}-{entry.end:x} : {entry.label}" - percent = 100 * count / total - print(f"{mem_type:<40} {count:>10} {percent:>10.1f}") - print_entries(children[entry]) - - print_entries(iomem[0]) - -def trace_begin(): - parse_iomem() - -def trace_end(): - print_memory_type() - -def process_event(param_dict): - if "sample" not in param_dict: - return - - sample = param_dict["sample"] - if "phys_addr" not in sample: - return - - phys_addr = sample["phys_addr"] - entry = find_memory_type(phys_addr) - if entry: - load_mem_type_cnt[entry] += 1 - - global event_name - if event_name is None: - event_name = param_dict["ev_name"] diff --git a/tools/perf/scripts/python/net_dropmonitor.py b/tools/perf/scripts/python/net_dropmonitor.py deleted file mode 100755 index a97e7a6e0940..000000000000 --- a/tools/perf/scripts/python/net_dropmonitor.py +++ /dev/null @@ -1,78 +0,0 @@ -# Monitor the system for dropped packets and proudce a report of drop locations and counts -# SPDX-License-Identifier: GPL-2.0 - -from __future__ import print_function - -import os -import sys - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * -from Util import * - -drop_log = {} -kallsyms = [] - -def get_kallsyms_table(): - global kallsyms - - try: - f = open("/proc/kallsyms", "r") - except: - return - - for line in f: - loc = int(line.split()[0], 16) - name = line.split()[2] - kallsyms.append((loc, name)) - kallsyms.sort() - -def get_sym(sloc): - loc = int(sloc) - - # Invariant: kallsyms[i][0] <= loc for all 0 <= i <= start - # kallsyms[i][0] > loc for all end <= i < len(kallsyms) - start, end = -1, len(kallsyms) - while end != start + 1: - pivot = (start + end) // 2 - if loc < kallsyms[pivot][0]: - end = pivot - else: - start = pivot - - # Now (start == -1 or kallsyms[start][0] <= loc) - # and (start == len(kallsyms) - 1 or loc < kallsyms[start + 1][0]) - if start >= 0: - symloc, name = kallsyms[start] - return (name, loc - symloc) - else: - return (None, 0) - -def print_drop_table(): - print("%25s %25s %25s" % ("LOCATION", "OFFSET", "COUNT")) - for i in drop_log.keys(): - (sym, off) = get_sym(i) - if sym == None: - sym = i - print("%25s %25s %25s" % (sym, off, drop_log[i])) - - -def trace_begin(): - print("Starting trace (Ctrl-C to dump results)") - -def trace_end(): - print("Gathering kallsyms data") - get_kallsyms_table() - print_drop_table() - -# called from perf, when it finds a corresponding event -def skb__kfree_skb(name, context, cpu, sec, nsec, pid, comm, callchain, - skbaddr, location, protocol, reason): - slocation = str(location) - try: - drop_log[slocation] = drop_log[slocation] + 1 - except: - drop_log[slocation] = 1 diff --git a/tools/perf/scripts/python/netdev-times.py b/tools/perf/scripts/python/netdev-times.py deleted file mode 100644 index 30c4bccee5b2..000000000000 --- a/tools/perf/scripts/python/netdev-times.py +++ /dev/null @@ -1,473 +0,0 @@ -# Display a process of packets and processed time. -# SPDX-License-Identifier: GPL-2.0 -# It helps us to investigate networking or network device. -# -# options -# tx: show only tx chart -# rx: show only rx chart -# dev=: show only thing related to specified device -# debug: work with debug mode. It shows buffer status. - -from __future__ import print_function - -import os -import sys - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * -from Util import * -from functools import cmp_to_key - -all_event_list = []; # insert all tracepoint event related with this script -irq_dic = {}; # key is cpu and value is a list which stacks irqs - # which raise NET_RX softirq -net_rx_dic = {}; # key is cpu and value include time of NET_RX softirq-entry - # and a list which stacks receive -receive_hunk_list = []; # a list which include a sequence of receive events -rx_skb_list = []; # received packet list for matching - # skb_copy_datagram_iovec - -buffer_budget = 65536; # the budget of rx_skb_list, tx_queue_list and - # tx_xmit_list -of_count_rx_skb_list = 0; # overflow count - -tx_queue_list = []; # list of packets which pass through dev_queue_xmit -of_count_tx_queue_list = 0; # overflow count - -tx_xmit_list = []; # list of packets which pass through dev_hard_start_xmit -of_count_tx_xmit_list = 0; # overflow count - -tx_free_list = []; # list of packets which is freed - -# options -show_tx = 0; -show_rx = 0; -dev = 0; # store a name of device specified by option "dev=" -debug = 0; - -# indices of event_info tuple -EINFO_IDX_NAME= 0 -EINFO_IDX_CONTEXT=1 -EINFO_IDX_CPU= 2 -EINFO_IDX_TIME= 3 -EINFO_IDX_PID= 4 -EINFO_IDX_COMM= 5 - -# Calculate a time interval(msec) from src(nsec) to dst(nsec) -def diff_msec(src, dst): - return (dst - src) / 1000000.0 - -# Display a process of transmitting a packet -def print_transmit(hunk): - if dev != 0 and hunk['dev'].find(dev) < 0: - return - print("%7s %5d %6d.%06dsec %12.3fmsec %12.3fmsec" % - (hunk['dev'], hunk['len'], - nsecs_secs(hunk['queue_t']), - nsecs_nsecs(hunk['queue_t'])/1000, - diff_msec(hunk['queue_t'], hunk['xmit_t']), - diff_msec(hunk['xmit_t'], hunk['free_t']))) - -# Format for displaying rx packet processing -PF_IRQ_ENTRY= " irq_entry(+%.3fmsec irq=%d:%s)" -PF_SOFT_ENTRY=" softirq_entry(+%.3fmsec)" -PF_NAPI_POLL= " napi_poll_exit(+%.3fmsec %s)" -PF_JOINT= " |" -PF_WJOINT= " | |" -PF_NET_RECV= " |---netif_receive_skb(+%.3fmsec skb=%x len=%d)" -PF_NET_RX= " |---netif_rx(+%.3fmsec skb=%x)" -PF_CPY_DGRAM= " | skb_copy_datagram_iovec(+%.3fmsec %d:%s)" -PF_KFREE_SKB= " | kfree_skb(+%.3fmsec location=%x)" -PF_CONS_SKB= " | consume_skb(+%.3fmsec)" - -# Display a process of received packets and interrputs associated with -# a NET_RX softirq -def print_receive(hunk): - show_hunk = 0 - irq_list = hunk['irq_list'] - cpu = irq_list[0]['cpu'] - base_t = irq_list[0]['irq_ent_t'] - # check if this hunk should be showed - if dev != 0: - for i in range(len(irq_list)): - if irq_list[i]['name'].find(dev) >= 0: - show_hunk = 1 - break - else: - show_hunk = 1 - if show_hunk == 0: - return - - print("%d.%06dsec cpu=%d" % - (nsecs_secs(base_t), nsecs_nsecs(base_t)/1000, cpu)) - for i in range(len(irq_list)): - print(PF_IRQ_ENTRY % - (diff_msec(base_t, irq_list[i]['irq_ent_t']), - irq_list[i]['irq'], irq_list[i]['name'])) - print(PF_JOINT) - irq_event_list = irq_list[i]['event_list'] - for j in range(len(irq_event_list)): - irq_event = irq_event_list[j] - if irq_event['event'] == 'netif_rx': - print(PF_NET_RX % - (diff_msec(base_t, irq_event['time']), - irq_event['skbaddr'])) - print(PF_JOINT) - print(PF_SOFT_ENTRY % - diff_msec(base_t, hunk['sirq_ent_t'])) - print(PF_JOINT) - event_list = hunk['event_list'] - for i in range(len(event_list)): - event = event_list[i] - if event['event_name'] == 'napi_poll': - print(PF_NAPI_POLL % - (diff_msec(base_t, event['event_t']), - event['dev'])) - if i == len(event_list) - 1: - print("") - else: - print(PF_JOINT) - else: - print(PF_NET_RECV % - (diff_msec(base_t, event['event_t']), - event['skbaddr'], - event['len'])) - if 'comm' in event.keys(): - print(PF_WJOINT) - print(PF_CPY_DGRAM % - (diff_msec(base_t, event['comm_t']), - event['pid'], event['comm'])) - elif 'handle' in event.keys(): - print(PF_WJOINT) - if event['handle'] == "kfree_skb": - print(PF_KFREE_SKB % - (diff_msec(base_t, - event['comm_t']), - event['location'])) - elif event['handle'] == "consume_skb": - print(PF_CONS_SKB % - diff_msec(base_t, - event['comm_t'])) - print(PF_JOINT) - -def trace_begin(): - global show_tx - global show_rx - global dev - global debug - - for i in range(len(sys.argv)): - if i == 0: - continue - arg = sys.argv[i] - if arg == 'tx': - show_tx = 1 - elif arg =='rx': - show_rx = 1 - elif arg.find('dev=',0, 4) >= 0: - dev = arg[4:] - elif arg == 'debug': - debug = 1 - if show_tx == 0 and show_rx == 0: - show_tx = 1 - show_rx = 1 - -def trace_end(): - # order all events in time - all_event_list.sort(key=cmp_to_key(lambda a,b :a[EINFO_IDX_TIME] < b[EINFO_IDX_TIME])) - # process all events - for i in range(len(all_event_list)): - event_info = all_event_list[i] - name = event_info[EINFO_IDX_NAME] - if name == 'irq__softirq_exit': - handle_irq_softirq_exit(event_info) - elif name == 'irq__softirq_entry': - handle_irq_softirq_entry(event_info) - elif name == 'irq__softirq_raise': - handle_irq_softirq_raise(event_info) - elif name == 'irq__irq_handler_entry': - handle_irq_handler_entry(event_info) - elif name == 'irq__irq_handler_exit': - handle_irq_handler_exit(event_info) - elif name == 'napi__napi_poll': - handle_napi_poll(event_info) - elif name == 'net__netif_receive_skb': - handle_netif_receive_skb(event_info) - elif name == 'net__netif_rx': - handle_netif_rx(event_info) - elif name == 'skb__skb_copy_datagram_iovec': - handle_skb_copy_datagram_iovec(event_info) - elif name == 'net__net_dev_queue': - handle_net_dev_queue(event_info) - elif name == 'net__net_dev_xmit': - handle_net_dev_xmit(event_info) - elif name == 'skb__kfree_skb': - handle_kfree_skb(event_info) - elif name == 'skb__consume_skb': - handle_consume_skb(event_info) - # display receive hunks - if show_rx: - for i in range(len(receive_hunk_list)): - print_receive(receive_hunk_list[i]) - # display transmit hunks - if show_tx: - print(" dev len Qdisc " - " netdevice free") - for i in range(len(tx_free_list)): - print_transmit(tx_free_list[i]) - if debug: - print("debug buffer status") - print("----------------------------") - print("xmit Qdisc:remain:%d overflow:%d" % - (len(tx_queue_list), of_count_tx_queue_list)) - print("xmit netdevice:remain:%d overflow:%d" % - (len(tx_xmit_list), of_count_tx_xmit_list)) - print("receive:remain:%d overflow:%d" % - (len(rx_skb_list), of_count_rx_skb_list)) - -# called from perf, when it finds a correspoinding event -def irq__softirq_entry(name, context, cpu, sec, nsec, pid, comm, callchain, vec): - if symbol_str("irq__softirq_entry", "vec", vec) != "NET_RX": - return - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, vec) - all_event_list.append(event_info) - -def irq__softirq_exit(name, context, cpu, sec, nsec, pid, comm, callchain, vec): - if symbol_str("irq__softirq_entry", "vec", vec) != "NET_RX": - return - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, vec) - all_event_list.append(event_info) - -def irq__softirq_raise(name, context, cpu, sec, nsec, pid, comm, callchain, vec): - if symbol_str("irq__softirq_entry", "vec", vec) != "NET_RX": - return - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, vec) - all_event_list.append(event_info) - -def irq__irq_handler_entry(name, context, cpu, sec, nsec, pid, comm, - callchain, irq, irq_name): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, - irq, irq_name) - all_event_list.append(event_info) - -def irq__irq_handler_exit(name, context, cpu, sec, nsec, pid, comm, callchain, irq, ret): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, irq, ret) - all_event_list.append(event_info) - -def napi__napi_poll(name, context, cpu, sec, nsec, pid, comm, callchain, napi, - dev_name, work=None, budget=None): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, - napi, dev_name, work, budget) - all_event_list.append(event_info) - -def net__netif_receive_skb(name, context, cpu, sec, nsec, pid, comm, callchain, skbaddr, - skblen, dev_name): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, - skbaddr, skblen, dev_name) - all_event_list.append(event_info) - -def net__netif_rx(name, context, cpu, sec, nsec, pid, comm, callchain, skbaddr, - skblen, dev_name): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, - skbaddr, skblen, dev_name) - all_event_list.append(event_info) - -def net__net_dev_queue(name, context, cpu, sec, nsec, pid, comm, callchain, - skbaddr, skblen, dev_name): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, - skbaddr, skblen, dev_name) - all_event_list.append(event_info) - -def net__net_dev_xmit(name, context, cpu, sec, nsec, pid, comm, callchain, - skbaddr, skblen, rc, dev_name): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, - skbaddr, skblen, rc ,dev_name) - all_event_list.append(event_info) - -def skb__kfree_skb(name, context, cpu, sec, nsec, pid, comm, callchain, - skbaddr, location, protocol, reason): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, - skbaddr, location, protocol, reason) - all_event_list.append(event_info) - -def skb__consume_skb(name, context, cpu, sec, nsec, pid, comm, callchain, - skbaddr, location): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, - skbaddr) - all_event_list.append(event_info) - -def skb__skb_copy_datagram_iovec(name, context, cpu, sec, nsec, pid, comm, callchain, - skbaddr, skblen): - event_info = (name, context, cpu, nsecs(sec, nsec), pid, comm, - skbaddr, skblen) - all_event_list.append(event_info) - -def handle_irq_handler_entry(event_info): - (name, context, cpu, time, pid, comm, irq, irq_name) = event_info - if cpu not in irq_dic.keys(): - irq_dic[cpu] = [] - irq_record = {'irq':irq, 'name':irq_name, 'cpu':cpu, 'irq_ent_t':time} - irq_dic[cpu].append(irq_record) - -def handle_irq_handler_exit(event_info): - (name, context, cpu, time, pid, comm, irq, ret) = event_info - if cpu not in irq_dic.keys(): - return - irq_record = irq_dic[cpu].pop() - if irq != irq_record['irq']: - return - irq_record.update({'irq_ext_t':time}) - # if an irq doesn't include NET_RX softirq, drop. - if 'event_list' in irq_record.keys(): - irq_dic[cpu].append(irq_record) - -def handle_irq_softirq_raise(event_info): - (name, context, cpu, time, pid, comm, vec) = event_info - if cpu not in irq_dic.keys() \ - or len(irq_dic[cpu]) == 0: - return - irq_record = irq_dic[cpu].pop() - if 'event_list' in irq_record.keys(): - irq_event_list = irq_record['event_list'] - else: - irq_event_list = [] - irq_event_list.append({'time':time, 'event':'sirq_raise'}) - irq_record.update({'event_list':irq_event_list}) - irq_dic[cpu].append(irq_record) - -def handle_irq_softirq_entry(event_info): - (name, context, cpu, time, pid, comm, vec) = event_info - net_rx_dic[cpu] = {'sirq_ent_t':time, 'event_list':[]} - -def handle_irq_softirq_exit(event_info): - (name, context, cpu, time, pid, comm, vec) = event_info - irq_list = [] - event_list = 0 - if cpu in irq_dic.keys(): - irq_list = irq_dic[cpu] - del irq_dic[cpu] - if cpu in net_rx_dic.keys(): - sirq_ent_t = net_rx_dic[cpu]['sirq_ent_t'] - event_list = net_rx_dic[cpu]['event_list'] - del net_rx_dic[cpu] - if irq_list == [] or event_list == 0: - return - rec_data = {'sirq_ent_t':sirq_ent_t, 'sirq_ext_t':time, - 'irq_list':irq_list, 'event_list':event_list} - # merge information related to a NET_RX softirq - receive_hunk_list.append(rec_data) - -def handle_napi_poll(event_info): - (name, context, cpu, time, pid, comm, napi, dev_name, - work, budget) = event_info - if cpu in net_rx_dic.keys(): - event_list = net_rx_dic[cpu]['event_list'] - rec_data = {'event_name':'napi_poll', - 'dev':dev_name, 'event_t':time, - 'work':work, 'budget':budget} - event_list.append(rec_data) - -def handle_netif_rx(event_info): - (name, context, cpu, time, pid, comm, - skbaddr, skblen, dev_name) = event_info - if cpu not in irq_dic.keys() \ - or len(irq_dic[cpu]) == 0: - return - irq_record = irq_dic[cpu].pop() - if 'event_list' in irq_record.keys(): - irq_event_list = irq_record['event_list'] - else: - irq_event_list = [] - irq_event_list.append({'time':time, 'event':'netif_rx', - 'skbaddr':skbaddr, 'skblen':skblen, 'dev_name':dev_name}) - irq_record.update({'event_list':irq_event_list}) - irq_dic[cpu].append(irq_record) - -def handle_netif_receive_skb(event_info): - global of_count_rx_skb_list - - (name, context, cpu, time, pid, comm, - skbaddr, skblen, dev_name) = event_info - if cpu in net_rx_dic.keys(): - rec_data = {'event_name':'netif_receive_skb', - 'event_t':time, 'skbaddr':skbaddr, 'len':skblen} - event_list = net_rx_dic[cpu]['event_list'] - event_list.append(rec_data) - rx_skb_list.insert(0, rec_data) - if len(rx_skb_list) > buffer_budget: - rx_skb_list.pop() - of_count_rx_skb_list += 1 - -def handle_net_dev_queue(event_info): - global of_count_tx_queue_list - - (name, context, cpu, time, pid, comm, - skbaddr, skblen, dev_name) = event_info - skb = {'dev':dev_name, 'skbaddr':skbaddr, 'len':skblen, 'queue_t':time} - tx_queue_list.insert(0, skb) - if len(tx_queue_list) > buffer_budget: - tx_queue_list.pop() - of_count_tx_queue_list += 1 - -def handle_net_dev_xmit(event_info): - global of_count_tx_xmit_list - - (name, context, cpu, time, pid, comm, - skbaddr, skblen, rc, dev_name) = event_info - if rc == 0: # NETDEV_TX_OK - for i in range(len(tx_queue_list)): - skb = tx_queue_list[i] - if skb['skbaddr'] == skbaddr: - skb['xmit_t'] = time - tx_xmit_list.insert(0, skb) - del tx_queue_list[i] - if len(tx_xmit_list) > buffer_budget: - tx_xmit_list.pop() - of_count_tx_xmit_list += 1 - return - -def handle_kfree_skb(event_info): - (name, context, cpu, time, pid, comm, - skbaddr, location, protocol, reason) = event_info - for i in range(len(tx_queue_list)): - skb = tx_queue_list[i] - if skb['skbaddr'] == skbaddr: - del tx_queue_list[i] - return - for i in range(len(tx_xmit_list)): - skb = tx_xmit_list[i] - if skb['skbaddr'] == skbaddr: - skb['free_t'] = time - tx_free_list.append(skb) - del tx_xmit_list[i] - return - for i in range(len(rx_skb_list)): - rec_data = rx_skb_list[i] - if rec_data['skbaddr'] == skbaddr: - rec_data.update({'handle':"kfree_skb", - 'comm':comm, 'pid':pid, 'comm_t':time}) - del rx_skb_list[i] - return - -def handle_consume_skb(event_info): - (name, context, cpu, time, pid, comm, skbaddr) = event_info - for i in range(len(tx_xmit_list)): - skb = tx_xmit_list[i] - if skb['skbaddr'] == skbaddr: - skb['free_t'] = time - tx_free_list.append(skb) - del tx_xmit_list[i] - return - -def handle_skb_copy_datagram_iovec(event_info): - (name, context, cpu, time, pid, comm, skbaddr, skblen) = event_info - for i in range(len(rx_skb_list)): - rec_data = rx_skb_list[i] - if skbaddr == rec_data['skbaddr']: - rec_data.update({'handle':"skb_copy_datagram_iovec", - 'comm':comm, 'pid':pid, 'comm_t':time}) - del rx_skb_list[i] - return diff --git a/tools/perf/scripts/python/powerpc-hcalls.py b/tools/perf/scripts/python/powerpc-hcalls.py deleted file mode 100644 index fedce1b68cad..000000000000 --- a/tools/perf/scripts/python/powerpc-hcalls.py +++ /dev/null @@ -1,335 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0+ -# -# Copyright (C) 2018 Ravi Bangoria, IBM Corporation -# -# Hypervisor call statisics - -from __future__ import print_function - -import os -import sys - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * -from Util import * - -# output: { -# opcode: { -# 'min': minimum time nsec -# 'max': maximum time nsec -# 'time': average time nsec -# 'cnt': counter -# } ... -# } -output = {} -sort_key = 'count' - -# d_enter: { -# cpu: { -# opcode: nsec -# } ... -# } -d_enter = {} - -hcall_table = { - 4: 'H_REMOVE', - 8: 'H_ENTER', - 12: 'H_READ', - 16: 'H_CLEAR_MOD', - 20: 'H_CLEAR_REF', - 24: 'H_PROTECT', - 28: 'H_GET_TCE', - 32: 'H_PUT_TCE', - 36: 'H_SET_SPRG0', - 40: 'H_SET_DABR', - 44: 'H_PAGE_INIT', - 48: 'H_SET_ASR', - 52: 'H_ASR_ON', - 56: 'H_ASR_OFF', - 60: 'H_LOGICAL_CI_LOAD', - 64: 'H_LOGICAL_CI_STORE', - 68: 'H_LOGICAL_CACHE_LOAD', - 72: 'H_LOGICAL_CACHE_STORE', - 76: 'H_LOGICAL_ICBI', - 80: 'H_LOGICAL_DCBF', - 84: 'H_GET_TERM_CHAR', - 88: 'H_PUT_TERM_CHAR', - 92: 'H_REAL_TO_LOGICAL', - 96: 'H_HYPERVISOR_DATA', - 100: 'H_EOI', - 104: 'H_CPPR', - 108: 'H_IPI', - 112: 'H_IPOLL', - 116: 'H_XIRR', - 120: 'H_MIGRATE_DMA', - 124: 'H_PERFMON', - 220: 'H_REGISTER_VPA', - 224: 'H_CEDE', - 228: 'H_CONFER', - 232: 'H_PROD', - 236: 'H_GET_PPP', - 240: 'H_SET_PPP', - 244: 'H_PURR', - 248: 'H_PIC', - 252: 'H_REG_CRQ', - 256: 'H_FREE_CRQ', - 260: 'H_VIO_SIGNAL', - 264: 'H_SEND_CRQ', - 272: 'H_COPY_RDMA', - 276: 'H_REGISTER_LOGICAL_LAN', - 280: 'H_FREE_LOGICAL_LAN', - 284: 'H_ADD_LOGICAL_LAN_BUFFER', - 288: 'H_SEND_LOGICAL_LAN', - 292: 'H_BULK_REMOVE', - 304: 'H_MULTICAST_CTRL', - 308: 'H_SET_XDABR', - 312: 'H_STUFF_TCE', - 316: 'H_PUT_TCE_INDIRECT', - 332: 'H_CHANGE_LOGICAL_LAN_MAC', - 336: 'H_VTERM_PARTNER_INFO', - 340: 'H_REGISTER_VTERM', - 344: 'H_FREE_VTERM', - 348: 'H_RESET_EVENTS', - 352: 'H_ALLOC_RESOURCE', - 356: 'H_FREE_RESOURCE', - 360: 'H_MODIFY_QP', - 364: 'H_QUERY_QP', - 368: 'H_REREGISTER_PMR', - 372: 'H_REGISTER_SMR', - 376: 'H_QUERY_MR', - 380: 'H_QUERY_MW', - 384: 'H_QUERY_HCA', - 388: 'H_QUERY_PORT', - 392: 'H_MODIFY_PORT', - 396: 'H_DEFINE_AQP1', - 400: 'H_GET_TRACE_BUFFER', - 404: 'H_DEFINE_AQP0', - 408: 'H_RESIZE_MR', - 412: 'H_ATTACH_MCQP', - 416: 'H_DETACH_MCQP', - 420: 'H_CREATE_RPT', - 424: 'H_REMOVE_RPT', - 428: 'H_REGISTER_RPAGES', - 432: 'H_DISABLE_AND_GET', - 436: 'H_ERROR_DATA', - 440: 'H_GET_HCA_INFO', - 444: 'H_GET_PERF_COUNT', - 448: 'H_MANAGE_TRACE', - 456: 'H_GET_CPU_CHARACTERISTICS', - 468: 'H_FREE_LOGICAL_LAN_BUFFER', - 472: 'H_POLL_PENDING', - 484: 'H_QUERY_INT_STATE', - 580: 'H_ILLAN_ATTRIBUTES', - 584: 'H_ADD_LOGICAL_LAN_BUFFERS', - 592: 'H_MODIFY_HEA_QP', - 596: 'H_QUERY_HEA_QP', - 600: 'H_QUERY_HEA', - 604: 'H_QUERY_HEA_PORT', - 608: 'H_MODIFY_HEA_PORT', - 612: 'H_REG_BCMC', - 616: 'H_DEREG_BCMC', - 620: 'H_REGISTER_HEA_RPAGES', - 624: 'H_DISABLE_AND_GET_HEA', - 628: 'H_GET_HEA_INFO', - 632: 'H_ALLOC_HEA_RESOURCE', - 644: 'H_ADD_CONN', - 648: 'H_DEL_CONN', - 664: 'H_JOIN', - 672: 'H_VASI_SIGNAL', - 676: 'H_VASI_STATE', - 680: 'H_VIOCTL', - 688: 'H_ENABLE_CRQ', - 696: 'H_GET_EM_PARMS', - 720: 'H_SET_MPP', - 724: 'H_GET_MPP', - 732: 'H_REG_SUB_CRQ', - 736: 'H_FREE_SUB_CRQ', - 740: 'H_SEND_SUB_CRQ', - 744: 'H_SEND_SUB_CRQ_INDIRECT', - 748: 'H_HOME_NODE_ASSOCIATIVITY', - 756: 'H_BEST_ENERGY', - 764: 'H_XIRR_X', - 768: 'H_RANDOM', - 772: 'H_COP', - 788: 'H_GET_MPP_X', - 796: 'H_SET_MODE', - 808: 'H_BLOCK_REMOVE', - 856: 'H_CLEAR_HPT', - 864: 'H_REQUEST_VMC', - 876: 'H_RESIZE_HPT_PREPARE', - 880: 'H_RESIZE_HPT_COMMIT', - 892: 'H_REGISTER_PROC_TBL', - 896: 'H_SIGNAL_SYS_RESET', - 904: 'H_ALLOCATE_VAS_WINDOW', - 908: 'H_MODIFY_VAS_WINDOW', - 912: 'H_DEALLOCATE_VAS_WINDOW', - 916: 'H_QUERY_VAS_WINDOW', - 920: 'H_QUERY_VAS_CAPABILITIES', - 924: 'H_QUERY_NX_CAPABILITIES', - 928: 'H_GET_NX_FAULT', - 936: 'H_INT_GET_SOURCE_INFO', - 940: 'H_INT_SET_SOURCE_CONFIG', - 944: 'H_INT_GET_SOURCE_CONFIG', - 948: 'H_INT_GET_QUEUE_INFO', - 952: 'H_INT_SET_QUEUE_CONFIG', - 956: 'H_INT_GET_QUEUE_CONFIG', - 960: 'H_INT_SET_OS_REPORTING_LINE', - 964: 'H_INT_GET_OS_REPORTING_LINE', - 968: 'H_INT_ESB', - 972: 'H_INT_SYNC', - 976: 'H_INT_RESET', - 996: 'H_SCM_READ_METADATA', - 1000: 'H_SCM_WRITE_METADATA', - 1004: 'H_SCM_BIND_MEM', - 1008: 'H_SCM_UNBIND_MEM', - 1012: 'H_SCM_QUERY_BLOCK_MEM_BINDING', - 1016: 'H_SCM_QUERY_LOGICAL_MEM_BINDING', - 1020: 'H_SCM_UNBIND_ALL', - 1024: 'H_SCM_HEALTH', - 1048: 'H_SCM_PERFORMANCE_STATS', - 1052: 'H_PKS_GET_CONFIG', - 1056: 'H_PKS_SET_PASSWORD', - 1060: 'H_PKS_GEN_PASSWORD', - 1068: 'H_PKS_WRITE_OBJECT', - 1072: 'H_PKS_GEN_KEY', - 1076: 'H_PKS_READ_OBJECT', - 1080: 'H_PKS_REMOVE_OBJECT', - 1084: 'H_PKS_CONFIRM_OBJECT_FLUSHED', - 1096: 'H_RPT_INVALIDATE', - 1100: 'H_SCM_FLUSH', - 1104: 'H_GET_ENERGY_SCALE_INFO', - 1108: 'H_PKS_SIGNED_UPDATE', - 1112: 'H_HTM', - 1116: 'H_WATCHDOG', - # Platform specific hcalls used by KVM on PowerVM - 1120: 'H_GUEST_GET_CAPABILITIES', - 1124: 'H_GUEST_SET_CAPABILITIES', - 1136: 'H_GUEST_CREATE', - 1140: 'H_GUEST_CREATE_VCPU', - 1144: 'H_GUEST_GET_STATE', - 1148: 'H_GUEST_SET_STATE', - 1152: 'H_GUEST_RUN_VCPU', - 1156: 'H_GUEST_COPY_MEMORY', - 1160: 'H_GUEST_DELETE', - # Key wrapping hcalls - 1168: 'H_PKS_WRAP_OBJECT', - 1172: 'H_PKS_UNWRAP_OBJECT', - # Platform-specific hcalls used by the Ultravisor - 61184: 'H_SVM_PAGE_IN', - 61188: 'H_SVM_PAGE_OUT', - 61192: 'H_SVM_INIT_START', - 61196: 'H_SVM_INIT_DONE', - 61204: 'H_SVM_INIT_ABORT', - # Platform specific hcalls used by KVM - 61440: 'H_RTAS', - # Platform specific hcalls used by QEMU/SLOF - 61441: 'H_LOGICAL_MEMOP', - 61442: 'H_CAS', - 61443: 'H_UPDATE_DT', - # Platform specific hcalls provided by PHYP - 61560: 'H_GET_24X7_CATALOG_PAGE', - 61564: 'H_GET_24X7_DATA', - 61568: 'H_GET_PERF_COUNTER_INFO', - # Platform-specific hcalls used for nested HV KVM - 63488: 'H_SET_PARTITION_TABLE', - 63492: 'H_ENTER_NESTED', - 63496: 'H_TLB_INVALIDATE', - 63500: 'H_COPY_TOFROM_GUEST', -} - -def hcall_table_lookup(opcode): - if (opcode in hcall_table): - return hcall_table[opcode] - else: - return opcode - -print_ptrn = '%-28s%10s%10s%10s%10s' - -def sort_output(opcode): - stats = output[opcode] - - if sort_key == 'min': - return stats['min'] - if sort_key == 'max': - return stats['max'] - if sort_key == 'avg': - return stats['time'] // stats['cnt'] - - return stats['cnt'] - -def trace_begin(): - global sort_key - - valid_sort_keys = ['count', 'min', 'max', 'avg'] - - i = 1 - while i < len(sys.argv): - arg = sys.argv[i] - - if arg == '-s' or arg == '--sort': - if i + 1 >= len(sys.argv): - print("Error: -s/--sort requires a sort key argument") - sys.exit(1) - sort_key = sys.argv[i + 1] - i += 2 - continue - - if arg.startswith('--sort='): - sort_key = arg.split('=', 1)[1] - i += 1 - continue - - i += 1 - - if sort_key not in valid_sort_keys: - print(f"Error: Invalid sort key '{sort_key}'. Valid options are: {', '.join(valid_sort_keys)}") - sys.exit(1) - - print("SORT KEY =", sort_key) - -def trace_end(): - print(print_ptrn % ('hcall', 'count', 'min(ns)', 'max(ns)', 'avg(ns)')) - print('-' * 68) - for opcode in sorted(output, key = sort_output, - reverse=True): - h_name = hcall_table_lookup(opcode) - time = output[opcode]['time'] - cnt = output[opcode]['cnt'] - min_t = output[opcode]['min'] - max_t = output[opcode]['max'] - - print(print_ptrn % (h_name, cnt, min_t, max_t, time//cnt)) - -def powerpc__hcall_exit(name, context, cpu, sec, nsec, pid, comm, callchain, - opcode, retval): - if (cpu in d_enter and opcode in d_enter[cpu]): - diff = nsecs(sec, nsec) - d_enter[cpu][opcode] - - if (opcode in output): - output[opcode]['time'] += diff - output[opcode]['cnt'] += 1 - if (output[opcode]['min'] > diff): - output[opcode]['min'] = diff - if (output[opcode]['max'] < diff): - output[opcode]['max'] = diff - else: - output[opcode] = { - 'time': diff, - 'cnt': 1, - 'min': diff, - 'max': diff, - } - - del d_enter[cpu][opcode] -# else: -# print("Can't find matching hcall_enter event. Ignoring sample") - -def powerpc__hcall_entry(event_name, context, cpu, sec, nsec, pid, comm, - callchain, opcode): - if (cpu in d_enter): - d_enter[cpu][opcode] = nsecs(sec, nsec) - else: - d_enter[cpu] = {opcode: nsecs(sec, nsec)} diff --git a/tools/perf/scripts/python/sched-migration.py b/tools/perf/scripts/python/sched-migration.py deleted file mode 100644 index 8196e3087c9e..000000000000 --- a/tools/perf/scripts/python/sched-migration.py +++ /dev/null @@ -1,462 +0,0 @@ -# Cpu task migration overview toy -# -# Copyright (C) 2010 Frederic Weisbecker -# -# perf script event handlers have been generated by perf script -g python -# -# This software is distributed under the terms of the GNU General -# Public License ("GPL") version 2 as published by the Free Software -# Foundation. -from __future__ import print_function - -import os -import sys - -from collections import defaultdict -try: - from UserList import UserList -except ImportError: - # Python 3: UserList moved to the collections package - from collections import UserList - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') -sys.path.append('scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * -from SchedGui import * - - -threads = { 0 : "idle"} - -def thread_name(pid): - return "%s:%d" % (threads[pid], pid) - -class RunqueueEventUnknown: - @staticmethod - def color(): - return None - - def __repr__(self): - return "unknown" - -class RunqueueEventSleep: - @staticmethod - def color(): - return (0, 0, 0xff) - - def __init__(self, sleeper): - self.sleeper = sleeper - - def __repr__(self): - return "%s gone to sleep" % thread_name(self.sleeper) - -class RunqueueEventWakeup: - @staticmethod - def color(): - return (0xff, 0xff, 0) - - def __init__(self, wakee): - self.wakee = wakee - - def __repr__(self): - return "%s woke up" % thread_name(self.wakee) - -class RunqueueEventFork: - @staticmethod - def color(): - return (0, 0xff, 0) - - def __init__(self, child): - self.child = child - - def __repr__(self): - return "new forked task %s" % thread_name(self.child) - -class RunqueueMigrateIn: - @staticmethod - def color(): - return (0, 0xf0, 0xff) - - def __init__(self, new): - self.new = new - - def __repr__(self): - return "task migrated in %s" % thread_name(self.new) - -class RunqueueMigrateOut: - @staticmethod - def color(): - return (0xff, 0, 0xff) - - def __init__(self, old): - self.old = old - - def __repr__(self): - return "task migrated out %s" % thread_name(self.old) - -class RunqueueSnapshot: - def __init__(self, tasks = [0], event = RunqueueEventUnknown()): - self.tasks = tuple(tasks) - self.event = event - - def sched_switch(self, prev, prev_state, next): - event = RunqueueEventUnknown() - - if taskState(prev_state) == "R" and next in self.tasks \ - and prev in self.tasks: - return self - - if taskState(prev_state) != "R": - event = RunqueueEventSleep(prev) - - next_tasks = list(self.tasks[:]) - if prev in self.tasks: - if taskState(prev_state) != "R": - next_tasks.remove(prev) - elif taskState(prev_state) == "R": - next_tasks.append(prev) - - if next not in next_tasks: - next_tasks.append(next) - - return RunqueueSnapshot(next_tasks, event) - - def migrate_out(self, old): - if old not in self.tasks: - return self - next_tasks = [task for task in self.tasks if task != old] - - return RunqueueSnapshot(next_tasks, RunqueueMigrateOut(old)) - - def __migrate_in(self, new, event): - if new in self.tasks: - self.event = event - return self - next_tasks = self.tasks[:] + tuple([new]) - - return RunqueueSnapshot(next_tasks, event) - - def migrate_in(self, new): - return self.__migrate_in(new, RunqueueMigrateIn(new)) - - def wake_up(self, new): - return self.__migrate_in(new, RunqueueEventWakeup(new)) - - def wake_up_new(self, new): - return self.__migrate_in(new, RunqueueEventFork(new)) - - def load(self): - """ Provide the number of tasks on the runqueue. - Don't count idle""" - return len(self.tasks) - 1 - - def __repr__(self): - ret = self.tasks.__repr__() - ret += self.origin_tostring() - - return ret - -class TimeSlice: - def __init__(self, start, prev): - self.start = start - self.prev = prev - self.end = start - # cpus that triggered the event - self.event_cpus = [] - if prev is not None: - self.total_load = prev.total_load - self.rqs = prev.rqs.copy() - else: - self.rqs = defaultdict(RunqueueSnapshot) - self.total_load = 0 - - def __update_total_load(self, old_rq, new_rq): - diff = new_rq.load() - old_rq.load() - self.total_load += diff - - def sched_switch(self, ts_list, prev, prev_state, next, cpu): - old_rq = self.prev.rqs[cpu] - new_rq = old_rq.sched_switch(prev, prev_state, next) - - if old_rq is new_rq: - return - - self.rqs[cpu] = new_rq - self.__update_total_load(old_rq, new_rq) - ts_list.append(self) - self.event_cpus = [cpu] - - def migrate(self, ts_list, new, old_cpu, new_cpu): - if old_cpu == new_cpu: - return - old_rq = self.prev.rqs[old_cpu] - out_rq = old_rq.migrate_out(new) - self.rqs[old_cpu] = out_rq - self.__update_total_load(old_rq, out_rq) - - new_rq = self.prev.rqs[new_cpu] - in_rq = new_rq.migrate_in(new) - self.rqs[new_cpu] = in_rq - self.__update_total_load(new_rq, in_rq) - - ts_list.append(self) - - if old_rq is not out_rq: - self.event_cpus.append(old_cpu) - self.event_cpus.append(new_cpu) - - def wake_up(self, ts_list, pid, cpu, fork): - old_rq = self.prev.rqs[cpu] - if fork: - new_rq = old_rq.wake_up_new(pid) - else: - new_rq = old_rq.wake_up(pid) - - if new_rq is old_rq: - return - self.rqs[cpu] = new_rq - self.__update_total_load(old_rq, new_rq) - ts_list.append(self) - self.event_cpus = [cpu] - - def next(self, t): - self.end = t - return TimeSlice(t, self) - -class TimeSliceList(UserList): - def __init__(self, arg = []): - self.data = arg - - def get_time_slice(self, ts): - if len(self.data) == 0: - slice = TimeSlice(ts, TimeSlice(-1, None)) - else: - slice = self.data[-1].next(ts) - return slice - - def find_time_slice(self, ts): - start = 0 - end = len(self.data) - found = -1 - searching = True - while searching: - if start == end or start == end - 1: - searching = False - - i = (end + start) / 2 - if self.data[i].start <= ts and self.data[i].end >= ts: - found = i - end = i - continue - - if self.data[i].end < ts: - start = i - - elif self.data[i].start > ts: - end = i - - return found - - def set_root_win(self, win): - self.root_win = win - - def mouse_down(self, cpu, t): - idx = self.find_time_slice(t) - if idx == -1: - return - - ts = self[idx] - rq = ts.rqs[cpu] - raw = "CPU: %d\n" % cpu - raw += "Last event : %s\n" % rq.event.__repr__() - raw += "Timestamp : %d.%06d\n" % (ts.start / (10 ** 9), (ts.start % (10 ** 9)) / 1000) - raw += "Duration : %6d us\n" % ((ts.end - ts.start) / (10 ** 6)) - raw += "Load = %d\n" % rq.load() - for t in rq.tasks: - raw += "%s \n" % thread_name(t) - - self.root_win.update_summary(raw) - - def update_rectangle_cpu(self, slice, cpu): - rq = slice.rqs[cpu] - - if slice.total_load != 0: - load_rate = rq.load() / float(slice.total_load) - else: - load_rate = 0 - - red_power = int(0xff - (0xff * load_rate)) - color = (0xff, red_power, red_power) - - top_color = None - - if cpu in slice.event_cpus: - top_color = rq.event.color() - - self.root_win.paint_rectangle_zone(cpu, color, top_color, slice.start, slice.end) - - def fill_zone(self, start, end): - i = self.find_time_slice(start) - if i == -1: - return - - for i in range(i, len(self.data)): - timeslice = self.data[i] - if timeslice.start > end: - return - - for cpu in timeslice.rqs: - self.update_rectangle_cpu(timeslice, cpu) - - def interval(self): - if len(self.data) == 0: - return (0, 0) - - return (self.data[0].start, self.data[-1].end) - - def nr_rectangles(self): - last_ts = self.data[-1] - max_cpu = 0 - for cpu in last_ts.rqs: - if cpu > max_cpu: - max_cpu = cpu - return max_cpu - - -class SchedEventProxy: - def __init__(self): - self.current_tsk = defaultdict(lambda : -1) - self.timeslices = TimeSliceList() - - def sched_switch(self, headers, prev_comm, prev_pid, prev_prio, prev_state, - next_comm, next_pid, next_prio): - """ Ensure the task we sched out this cpu is really the one - we logged. Otherwise we may have missed traces """ - - on_cpu_task = self.current_tsk[headers.cpu] - - if on_cpu_task != -1 and on_cpu_task != prev_pid: - print("Sched switch event rejected ts: %s cpu: %d prev: %s(%d) next: %s(%d)" % \ - headers.ts_format(), headers.cpu, prev_comm, prev_pid, next_comm, next_pid) - - threads[prev_pid] = prev_comm - threads[next_pid] = next_comm - self.current_tsk[headers.cpu] = next_pid - - ts = self.timeslices.get_time_slice(headers.ts()) - ts.sched_switch(self.timeslices, prev_pid, prev_state, next_pid, headers.cpu) - - def migrate(self, headers, pid, prio, orig_cpu, dest_cpu): - ts = self.timeslices.get_time_slice(headers.ts()) - ts.migrate(self.timeslices, pid, orig_cpu, dest_cpu) - - def wake_up(self, headers, comm, pid, success, target_cpu, fork): - if success == 0: - return - ts = self.timeslices.get_time_slice(headers.ts()) - ts.wake_up(self.timeslices, pid, target_cpu, fork) - - -def trace_begin(): - global parser - parser = SchedEventProxy() - -def trace_end(): - app = wx.App(False) - timeslices = parser.timeslices - frame = RootFrame(timeslices, "Migration") - app.MainLoop() - -def sched__sched_stat_runtime(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, runtime, vruntime): - pass - -def sched__sched_stat_iowait(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, delay): - pass - -def sched__sched_stat_sleep(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, delay): - pass - -def sched__sched_stat_wait(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, delay): - pass - -def sched__sched_process_fork(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, parent_comm, parent_pid, child_comm, child_pid): - pass - -def sched__sched_process_wait(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, prio): - pass - -def sched__sched_process_exit(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, prio): - pass - -def sched__sched_process_free(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, prio): - pass - -def sched__sched_migrate_task(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, prio, orig_cpu, - dest_cpu): - headers = EventHeaders(common_cpu, common_secs, common_nsecs, - common_pid, common_comm, common_callchain) - parser.migrate(headers, pid, prio, orig_cpu, dest_cpu) - -def sched__sched_switch(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, common_callchain, - prev_comm, prev_pid, prev_prio, prev_state, - next_comm, next_pid, next_prio): - - headers = EventHeaders(common_cpu, common_secs, common_nsecs, - common_pid, common_comm, common_callchain) - parser.sched_switch(headers, prev_comm, prev_pid, prev_prio, prev_state, - next_comm, next_pid, next_prio) - -def sched__sched_wakeup_new(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, prio, success, - target_cpu): - headers = EventHeaders(common_cpu, common_secs, common_nsecs, - common_pid, common_comm, common_callchain) - parser.wake_up(headers, comm, pid, success, target_cpu, 1) - -def sched__sched_wakeup(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, prio, success, - target_cpu): - headers = EventHeaders(common_cpu, common_secs, common_nsecs, - common_pid, common_comm, common_callchain) - parser.wake_up(headers, comm, pid, success, target_cpu, 0) - -def sched__sched_wait_task(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid, prio): - pass - -def sched__sched_kthread_stop_ret(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, ret): - pass - -def sched__sched_kthread_stop(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, comm, pid): - pass - -def trace_unhandled(event_name, context, event_fields_dict): - pass diff --git a/tools/perf/scripts/python/sctop.py b/tools/perf/scripts/python/sctop.py deleted file mode 100644 index 6e0278dcb092..000000000000 --- a/tools/perf/scripts/python/sctop.py +++ /dev/null @@ -1,89 +0,0 @@ -# system call top -# (c) 2010, Tom Zanussi -# Licensed under the terms of the GNU GPL License version 2 -# -# Periodically displays system-wide system call totals, broken down by -# syscall. If a [comm] arg is specified, only syscalls called by -# [comm] are displayed. If an [interval] arg is specified, the display -# will be refreshed every [interval] seconds. The default interval is -# 3 seconds. - -from __future__ import print_function - -import os, sys, time - -try: - import thread -except ImportError: - import _thread as thread - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * -from Util import * - -usage = "perf script -s sctop.py [comm] [interval]\n"; - -for_comm = None -default_interval = 3 -interval = default_interval - -if len(sys.argv) > 3: - sys.exit(usage) - -if len(sys.argv) > 2: - for_comm = sys.argv[1] - interval = int(sys.argv[2]) -elif len(sys.argv) > 1: - try: - interval = int(sys.argv[1]) - except ValueError: - for_comm = sys.argv[1] - interval = default_interval - -syscalls = autodict() - -def trace_begin(): - thread.start_new_thread(print_syscall_totals, (interval,)) - pass - -def raw_syscalls__sys_enter(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, id, args): - if for_comm is not None: - if common_comm != for_comm: - return - try: - syscalls[id] += 1 - except TypeError: - syscalls[id] = 1 - -def syscalls__sys_enter(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - id, args): - raw_syscalls__sys_enter(**locals()) - -def print_syscall_totals(interval): - while 1: - clear_term() - if for_comm is not None: - print("\nsyscall events for %s:\n" % (for_comm)) - else: - print("\nsyscall events:\n") - - print("%-40s %10s" % ("event", "count")) - print("%-40s %10s" % - ("----------------------------------------", - "----------")) - - for id, val in sorted(syscalls.items(), - key = lambda kv: (kv[1], kv[0]), - reverse = True): - try: - print("%-40s %10d" % (syscall_name(id), val)) - except TypeError: - pass - syscalls.clear() - time.sleep(interval) diff --git a/tools/perf/scripts/python/stackcollapse.py b/tools/perf/scripts/python/stackcollapse.py deleted file mode 100755 index b1c4def1410a..000000000000 --- a/tools/perf/scripts/python/stackcollapse.py +++ /dev/null @@ -1,127 +0,0 @@ -# stackcollapse.py - format perf samples with one line per distinct call stack -# SPDX-License-Identifier: GPL-2.0 -# -# This script's output has two space-separated fields. The first is a semicolon -# separated stack including the program name (from the "comm" field) and the -# function names from the call stack. The second is a count: -# -# swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 2 -# -# The file is sorted according to the first field. -# -# Input may be created and processed using: -# -# perf record -a -g -F 99 sleep 60 -# perf script report stackcollapse > out.stacks-folded -# -# (perf script record stackcollapse works too). -# -# Written by Paolo Bonzini -# Based on Brendan Gregg's stackcollapse-perf.pl script. - -from __future__ import print_function - -import os -import sys -from collections import defaultdict -from optparse import OptionParser, make_option - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * -from EventClass import * - -# command line parsing - -option_list = [ - # formatting options for the bottom entry of the stack - make_option("--include-tid", dest="include_tid", - action="store_true", default=False, - help="include thread id in stack"), - make_option("--include-pid", dest="include_pid", - action="store_true", default=False, - help="include process id in stack"), - make_option("--no-comm", dest="include_comm", - action="store_false", default=True, - help="do not separate stacks according to comm"), - make_option("--tidy-java", dest="tidy_java", - action="store_true", default=False, - help="beautify Java signatures"), - make_option("--kernel", dest="annotate_kernel", - action="store_true", default=False, - help="annotate kernel functions with _[k]") -] - -parser = OptionParser(option_list=option_list) -(opts, args) = parser.parse_args() - -if len(args) != 0: - parser.error("unexpected command line argument") -if opts.include_tid and not opts.include_comm: - parser.error("requesting tid but not comm is invalid") -if opts.include_pid and not opts.include_comm: - parser.error("requesting pid but not comm is invalid") - -# event handlers - -lines = defaultdict(lambda: 0) - -def process_event(param_dict): - def tidy_function_name(sym, dso): - if sym is None: - sym = '[unknown]' - - sym = sym.replace(';', ':') - if opts.tidy_java: - # the original stackcollapse-perf.pl script gives the - # example of converting this: - # Lorg/mozilla/javascript/MemberBox;.(Ljava/lang/reflect/Method;)V - # to this: - # org/mozilla/javascript/MemberBox:.init - sym = sym.replace('<', '') - sym = sym.replace('>', '') - if sym[0] == 'L' and sym.find('/'): - sym = sym[1:] - try: - sym = sym[:sym.index('(')] - except ValueError: - pass - - if opts.annotate_kernel and dso == '[kernel.kallsyms]': - return sym + '_[k]' - else: - return sym - - stack = list() - if 'callchain' in param_dict: - for entry in param_dict['callchain']: - entry.setdefault('sym', dict()) - entry['sym'].setdefault('name', None) - entry.setdefault('dso', None) - stack.append(tidy_function_name(entry['sym']['name'], - entry['dso'])) - else: - param_dict.setdefault('symbol', None) - param_dict.setdefault('dso', None) - stack.append(tidy_function_name(param_dict['symbol'], - param_dict['dso'])) - - if opts.include_comm: - comm = param_dict["comm"].replace(' ', '_') - sep = "-" - if opts.include_pid: - comm = comm + sep + str(param_dict['sample']['pid']) - sep = "/" - if opts.include_tid: - comm = comm + sep + str(param_dict['sample']['tid']) - stack.append(comm) - - stack_string = ';'.join(reversed(stack)) - lines[stack_string] = lines[stack_string] + 1 - -def trace_end(): - list = sorted(lines) - for stack in list: - print("%s %d" % (stack, lines[stack])) diff --git a/tools/perf/scripts/python/stat-cpi.py b/tools/perf/scripts/python/stat-cpi.py deleted file mode 100644 index 01fa933ff3cf..000000000000 --- a/tools/perf/scripts/python/stat-cpi.py +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 - -from __future__ import print_function - -data = {} -times = [] -threads = [] -cpus = [] - -def get_key(time, event, cpu, thread): - return "%d-%s-%d-%d" % (time, event, cpu, thread) - -def store_key(time, cpu, thread): - if (time not in times): - times.append(time) - - if (cpu not in cpus): - cpus.append(cpu) - - if (thread not in threads): - threads.append(thread) - -def store(time, event, cpu, thread, val, ena, run): - #print("event %s cpu %d, thread %d, time %d, val %d, ena %d, run %d" % - # (event, cpu, thread, time, val, ena, run)) - - store_key(time, cpu, thread) - key = get_key(time, event, cpu, thread) - data[key] = [ val, ena, run] - -def get(time, event, cpu, thread): - key = get_key(time, event, cpu, thread) - return data[key][0] - -def stat__cycles_k(cpu, thread, time, val, ena, run): - store(time, "cycles", cpu, thread, val, ena, run); - -def stat__instructions_k(cpu, thread, time, val, ena, run): - store(time, "instructions", cpu, thread, val, ena, run); - -def stat__cycles_u(cpu, thread, time, val, ena, run): - store(time, "cycles", cpu, thread, val, ena, run); - -def stat__instructions_u(cpu, thread, time, val, ena, run): - store(time, "instructions", cpu, thread, val, ena, run); - -def stat__cycles(cpu, thread, time, val, ena, run): - store(time, "cycles", cpu, thread, val, ena, run); - -def stat__instructions(cpu, thread, time, val, ena, run): - store(time, "instructions", cpu, thread, val, ena, run); - -def stat__interval(time): - for cpu in cpus: - for thread in threads: - cyc = get(time, "cycles", cpu, thread) - ins = get(time, "instructions", cpu, thread) - cpi = 0 - - if ins != 0: - cpi = cyc/float(ins) - - print("%15f: cpu %d, thread %d -> cpi %f (%d/%d)" % (time/(float(1000000000)), cpu, thread, cpi, cyc, ins)) - -def trace_end(): - pass -# XXX trace_end callback could be used as an alternative place -# to compute same values as in the script above: -# -# for time in times: -# for cpu in cpus: -# for thread in threads: -# cyc = get(time, "cycles", cpu, thread) -# ins = get(time, "instructions", cpu, thread) -# -# if ins != 0: -# cpi = cyc/float(ins) -# -# print("time %.9f, cpu %d, thread %d -> cpi %f" % (time/(float(1000000000)), cpu, thread, cpi)) diff --git a/tools/perf/scripts/python/syscall-counts-by-pid.py b/tools/perf/scripts/python/syscall-counts-by-pid.py deleted file mode 100644 index f254e40c6f0f..000000000000 --- a/tools/perf/scripts/python/syscall-counts-by-pid.py +++ /dev/null @@ -1,75 +0,0 @@ -# system call counts, by pid -# (c) 2010, Tom Zanussi -# Licensed under the terms of the GNU GPL License version 2 -# -# Displays system-wide system call totals, broken down by syscall. -# If a [comm] arg is specified, only syscalls called by [comm] are displayed. - -from __future__ import print_function - -import os, sys - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * -from Util import syscall_name - -usage = "perf script -s syscall-counts-by-pid.py [comm]\n"; - -for_comm = None -for_pid = None - -if len(sys.argv) > 2: - sys.exit(usage) - -if len(sys.argv) > 1: - try: - for_pid = int(sys.argv[1]) - except: - for_comm = sys.argv[1] - -syscalls = autodict() - -def trace_begin(): - print("Press control+C to stop and show the summary") - -def trace_end(): - print_syscall_totals() - -def raw_syscalls__sys_enter(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, id, args): - if (for_comm and common_comm != for_comm) or \ - (for_pid and common_pid != for_pid ): - return - try: - syscalls[common_comm][common_pid][id] += 1 - except TypeError: - syscalls[common_comm][common_pid][id] = 1 - -def syscalls__sys_enter(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - id, args): - raw_syscalls__sys_enter(**locals()) - -def print_syscall_totals(): - if for_comm is not None: - print("\nsyscall events for %s:\n" % (for_comm)) - else: - print("\nsyscall events by comm/pid:\n") - - print("%-40s %10s" % ("comm [pid]/syscalls", "count")) - print("%-40s %10s" % ("----------------------------------------", - "----------")) - - comm_keys = syscalls.keys() - for comm in comm_keys: - pid_keys = syscalls[comm].keys() - for pid in pid_keys: - print("\n%s [%d]" % (comm, pid)) - id_keys = syscalls[comm][pid].keys() - for id, val in sorted(syscalls[comm][pid].items(), - key = lambda kv: (kv[1], kv[0]), reverse = True): - print(" %-38s %10d" % (syscall_name(id), val)) diff --git a/tools/perf/scripts/python/syscall-counts.py b/tools/perf/scripts/python/syscall-counts.py deleted file mode 100644 index 8adb95ff1664..000000000000 --- a/tools/perf/scripts/python/syscall-counts.py +++ /dev/null @@ -1,65 +0,0 @@ -# system call counts -# (c) 2010, Tom Zanussi -# Licensed under the terms of the GNU GPL License version 2 -# -# Displays system-wide system call totals, broken down by syscall. -# If a [comm] arg is specified, only syscalls called by [comm] are displayed. - -from __future__ import print_function - -import os -import sys - -sys.path.append(os.environ['PERF_EXEC_PATH'] + \ - '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') - -from perf_trace_context import * -from Core import * -from Util import syscall_name - -usage = "perf script -s syscall-counts.py [comm]\n"; - -for_comm = None - -if len(sys.argv) > 2: - sys.exit(usage) - -if len(sys.argv) > 1: - for_comm = sys.argv[1] - -syscalls = autodict() - -def trace_begin(): - print("Press control+C to stop and show the summary") - -def trace_end(): - print_syscall_totals() - -def raw_syscalls__sys_enter(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, - common_callchain, id, args): - if for_comm is not None: - if common_comm != for_comm: - return - try: - syscalls[id] += 1 - except TypeError: - syscalls[id] = 1 - -def syscalls__sys_enter(event_name, context, common_cpu, - common_secs, common_nsecs, common_pid, common_comm, id, args): - raw_syscalls__sys_enter(**locals()) - -def print_syscall_totals(): - if for_comm is not None: - print("\nsyscall events for %s:\n" % (for_comm)) - else: - print("\nsyscall events:\n") - - print("%-40s %10s" % ("event", "count")) - print("%-40s %10s" % ("----------------------------------------", - "-----------")) - - for id, val in sorted(syscalls.items(), - key = lambda kv: (kv[1], kv[0]), reverse = True): - print("%-40s %10d" % (syscall_name(id), val)) diff --git a/tools/perf/scripts/python/task-analyzer.py b/tools/perf/scripts/python/task-analyzer.py deleted file mode 100755 index 3f1df9894246..000000000000 --- a/tools/perf/scripts/python/task-analyzer.py +++ /dev/null @@ -1,934 +0,0 @@ -# task-analyzer.py - comprehensive perf tasks analysis -# SPDX-License-Identifier: GPL-2.0 -# Copyright (c) 2022, Hagen Paul Pfeifer -# Licensed under the terms of the GNU GPL License version 2 -# -# Usage: -# -# perf record -e sched:sched_switch -a -- sleep 10 -# perf script report task-analyzer -# - -from __future__ import print_function -import sys -import os -import string -import argparse -import decimal - - -sys.path.append( - os.environ["PERF_EXEC_PATH"] + "/scripts/python/Perf-Trace-Util/lib/Perf/Trace" -) -from perf_trace_context import * -from Core import * - -# Definition of possible ASCII color codes -_COLORS = { - "grey": "\033[90m", - "red": "\033[91m", - "green": "\033[92m", - "yellow": "\033[93m", - "blue": "\033[94m", - "violet": "\033[95m", - "reset": "\033[0m", -} - -# Columns will have a static size to align everything properly -# Support of 116 days of active update with nano precision -LEN_SWITCHED_IN = len("9999999.999999999") # 17 -LEN_SWITCHED_OUT = len("9999999.999999999") # 17 -LEN_CPU = len("000") -LEN_PID = len("maxvalue") # 8 -LEN_TID = len("maxvalue") # 8 -LEN_COMM = len("max-comms-length") # 16 -LEN_RUNTIME = len("999999.999") # 10 -# Support of 3.45 hours of timespans -LEN_OUT_IN = len("99999999999.999") # 15 -LEN_OUT_OUT = len("99999999999.999") # 15 -LEN_IN_IN = len("99999999999.999") # 15 -LEN_IN_OUT = len("99999999999.999") # 15 - - -# py2/py3 compatibility layer, see PEP469 -try: - dict.iteritems -except AttributeError: - # py3 - def itervalues(d): - return iter(d.values()) - - def iteritems(d): - return iter(d.items()) - -else: - # py2 - def itervalues(d): - return d.itervalues() - - def iteritems(d): - return d.iteritems() - - -def _check_color(): - global _COLORS - """user enforced no-color or if stdout is no tty we disable colors""" - if sys.stdout.isatty() and args.stdio_color != "never": - return - _COLORS = { - "grey": "", - "red": "", - "green": "", - "yellow": "", - "blue": "", - "violet": "", - "reset": "", - } - - -def _parse_args(): - global args - parser = argparse.ArgumentParser(description="Analyze tasks behavior") - parser.add_argument( - "--time-limit", - default=[], - help= - "print tasks only in time[s] window e.g" - " --time-limit 123.111:789.222(print all between 123.111 and 789.222)" - " --time-limit 123: (print all from 123)" - " --time-limit :456 (print all until incl. 456)", - ) - parser.add_argument( - "--summary", action="store_true", help="print addtional runtime information" - ) - parser.add_argument( - "--summary-only", action="store_true", help="print only summary without traces" - ) - parser.add_argument( - "--summary-extended", - action="store_true", - help="print the summary with additional information of max inter task times" - " relative to the prev task", - ) - parser.add_argument( - "--ns", action="store_true", help="show timestamps in nanoseconds" - ) - parser.add_argument( - "--ms", action="store_true", help="show timestamps in milliseconds" - ) - parser.add_argument( - "--extended-times", - action="store_true", - help="Show the elapsed times between schedule in/schedule out" - " of this task and the schedule in/schedule out of previous occurrence" - " of the same task", - ) - parser.add_argument( - "--filter-tasks", - default=[], - help="filter out unneeded tasks by tid, pid or processname." - " E.g --filter-task 1337,/sbin/init ", - ) - parser.add_argument( - "--limit-to-tasks", - default=[], - help="limit output to selected task by tid, pid, processname." - " E.g --limit-to-tasks 1337,/sbin/init", - ) - parser.add_argument( - "--highlight-tasks", - default="", - help="colorize special tasks by their pid/tid/comm." - " E.g. --highlight-tasks 1:red,mutt:yellow" - " Colors available: red,grey,yellow,blue,violet,green", - ) - parser.add_argument( - "--rename-comms-by-tids", - default="", - help="rename task names by using tid (:,:)" - " This option is handy for inexpressive processnames like python interpreted" - " process. E.g --rename 1337:my-python-app", - ) - parser.add_argument( - "--stdio-color", - default="auto", - choices=["always", "never", "auto"], - help="always, never or auto, allowing configuring color output" - " via the command line", - ) - parser.add_argument( - "--csv", - default="", - help="Write trace to file selected by user. Options, like --ns or --extended" - "-times are used.", - ) - parser.add_argument( - "--csv-summary", - default="", - help="Write summary to file selected by user. Options, like --ns or" - " --summary-extended are used.", - ) - args = parser.parse_args() - args.tid_renames = dict() - - _argument_filter_sanity_check() - _argument_prepare_check() - - -def time_uniter(unit): - picker = { - "s": 1, - "ms": 1e3, - "us": 1e6, - "ns": 1e9, - } - return picker[unit] - - -def _init_db(): - global db - db = dict() - db["running"] = dict() - db["cpu"] = dict() - db["tid"] = dict() - db["global"] = [] - if args.summary or args.summary_extended or args.summary_only: - db["task_info"] = dict() - db["runtime_info"] = dict() - # min values for summary depending on the header - db["task_info"]["pid"] = len("PID") - db["task_info"]["tid"] = len("TID") - db["task_info"]["comm"] = len("Comm") - db["runtime_info"]["runs"] = len("Runs") - db["runtime_info"]["acc"] = len("Accumulated") - db["runtime_info"]["max"] = len("Max") - db["runtime_info"]["max_at"] = len("Max At") - db["runtime_info"]["min"] = len("Min") - db["runtime_info"]["mean"] = len("Mean") - db["runtime_info"]["median"] = len("Median") - if args.summary_extended: - db["inter_times"] = dict() - db["inter_times"]["out_in"] = len("Out-In") - db["inter_times"]["inter_at"] = len("At") - db["inter_times"]["out_out"] = len("Out-Out") - db["inter_times"]["in_in"] = len("In-In") - db["inter_times"]["in_out"] = len("In-Out") - - -def _median(numbers): - """phython3 hat statistics module - we have nothing""" - n = len(numbers) - index = n // 2 - if n % 2: - return sorted(numbers)[index] - return sum(sorted(numbers)[index - 1 : index + 1]) / 2 - - -def _mean(numbers): - return sum(numbers) / len(numbers) - - -class Timespans(object): - """ - The elapsed time between two occurrences of the same task is being tracked with the - help of this class. There are 4 of those Timespans Out-Out, In-Out, Out-In and - In-In. - The first half of the name signals the first time point of the - first task. The second half of the name represents the second - timepoint of the second task. - """ - - def __init__(self): - self._last_start = None - self._last_finish = None - self.out_out = -1 - self.in_out = -1 - self.out_in = -1 - self.in_in = -1 - if args.summary_extended: - self._time_in = -1 - self.max_out_in = -1 - self.max_at = -1 - self.max_in_out = -1 - self.max_in_in = -1 - self.max_out_out = -1 - - def feed(self, task): - """ - Called for every recorded trace event to find process pair and calculate the - task timespans. Chronological ordering, feed does not do reordering - """ - if not self._last_finish: - self._last_start = task.time_in(time_unit) - self._last_finish = task.time_out(time_unit) - return - self._time_in = task.time_in() - time_in = task.time_in(time_unit) - time_out = task.time_out(time_unit) - self.in_in = time_in - self._last_start - self.out_in = time_in - self._last_finish - self.in_out = time_out - self._last_start - self.out_out = time_out - self._last_finish - if args.summary_extended: - self._update_max_entries() - self._last_finish = task.time_out(time_unit) - self._last_start = task.time_in(time_unit) - - def _update_max_entries(self): - if self.in_in > self.max_in_in: - self.max_in_in = self.in_in - if self.out_out > self.max_out_out: - self.max_out_out = self.out_out - if self.in_out > self.max_in_out: - self.max_in_out = self.in_out - if self.out_in > self.max_out_in: - self.max_out_in = self.out_in - self.max_at = self._time_in - - - -class Summary(object): - """ - Primary instance for calculating the summary output. Processes the whole trace to - find and memorize relevant data such as mean, max et cetera. This instance handles - dynamic alignment aspects for summary output. - """ - - def __init__(self): - self._body = [] - - class AlignmentHelper: - """ - Used to calculated the alignment for the output of the summary. - """ - def __init__(self, pid, tid, comm, runs, acc, mean, - median, min, max, max_at): - self.pid = pid - self.tid = tid - self.comm = comm - self.runs = runs - self.acc = acc - self.mean = mean - self.median = median - self.min = min - self.max = max - self.max_at = max_at - if args.summary_extended: - self.out_in = None - self.inter_at = None - self.out_out = None - self.in_in = None - self.in_out = None - - def _print_header(self): - ''' - Output is trimmed in _format_stats thus additional adjustment in the header - is needed, depending on the choice of timeunit. The adjustment corresponds - to the amount of column titles being adjusted in _column_titles. - ''' - decimal_precision = 6 if not args.ns else 9 - fmt = " {{:^{}}}".format(sum(db["task_info"].values())) - fmt += " {{:^{}}}".format( - sum(db["runtime_info"].values()) - 2 * decimal_precision - ) - _header = ("Task Information", "Runtime Information") - - if args.summary_extended: - fmt += " {{:^{}}}".format( - sum(db["inter_times"].values()) - 4 * decimal_precision - ) - _header += ("Max Inter Task Times",) - fd_sum.write(fmt.format(*_header) + "\n") - - def _column_titles(self): - """ - Cells are being processed and displayed in different way so an alignment adjust - is implemented depeding on the choice of the timeunit. The positions of the max - values are being displayed in grey. Thus in their format two additional {}, - are placed for color set and reset. - """ - separator, fix_csv_align = _prepare_fmt_sep() - decimal_precision, time_precision = _prepare_fmt_precision() - fmt = "{{:>{}}}".format(db["task_info"]["pid"] * fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, db["task_info"]["tid"] * fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, db["task_info"]["comm"] * fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, db["runtime_info"]["runs"] * fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, db["runtime_info"]["acc"] * fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, db["runtime_info"]["mean"] * fix_csv_align) - fmt += "{}{{:>{}}}".format( - separator, db["runtime_info"]["median"] * fix_csv_align - ) - fmt += "{}{{:>{}}}".format( - separator, (db["runtime_info"]["min"] - decimal_precision) * fix_csv_align - ) - fmt += "{}{{:>{}}}".format( - separator, (db["runtime_info"]["max"] - decimal_precision) * fix_csv_align - ) - fmt += "{}{{}}{{:>{}}}{{}}".format( - separator, (db["runtime_info"]["max_at"] - time_precision) * fix_csv_align - ) - - column_titles = ("PID", "TID", "Comm") - column_titles += ("Runs", "Accumulated", "Mean", "Median", "Min", "Max") - column_titles += (_COLORS["grey"], "Max At", _COLORS["reset"]) - - if args.summary_extended: - fmt += "{}{{:>{}}}".format( - separator, - (db["inter_times"]["out_in"] - decimal_precision) * fix_csv_align - ) - fmt += "{}{{}}{{:>{}}}{{}}".format( - separator, - (db["inter_times"]["inter_at"] - time_precision) * fix_csv_align - ) - fmt += "{}{{:>{}}}".format( - separator, - (db["inter_times"]["out_out"] - decimal_precision) * fix_csv_align - ) - fmt += "{}{{:>{}}}".format( - separator, - (db["inter_times"]["in_in"] - decimal_precision) * fix_csv_align - ) - fmt += "{}{{:>{}}}".format( - separator, - (db["inter_times"]["in_out"] - decimal_precision) * fix_csv_align - ) - - column_titles += ("Out-In", _COLORS["grey"], "Max At", _COLORS["reset"], - "Out-Out", "In-In", "In-Out") - - fd_sum.write(fmt.format(*column_titles) + "\n") - - - def _task_stats(self): - """calculates the stats of every task and constructs the printable summary""" - for tid in sorted(db["tid"]): - color_one_sample = _COLORS["grey"] - color_reset = _COLORS["reset"] - no_executed = 0 - runtimes = [] - time_in = [] - timespans = Timespans() - for task in db["tid"][tid]: - pid = task.pid - comm = task.comm - no_executed += 1 - runtimes.append(task.runtime(time_unit)) - time_in.append(task.time_in()) - timespans.feed(task) - if len(runtimes) > 1: - color_one_sample = "" - color_reset = "" - time_max = max(runtimes) - time_min = min(runtimes) - max_at = time_in[runtimes.index(max(runtimes))] - - # The size of the decimal after sum,mean and median varies, thus we cut - # the decimal number, by rounding it. It has no impact on the output, - # because we have a precision of the decimal points at the output. - time_sum = round(sum(runtimes), 3) - time_mean = round(_mean(runtimes), 3) - time_median = round(_median(runtimes), 3) - - align_helper = self.AlignmentHelper(pid, tid, comm, no_executed, time_sum, - time_mean, time_median, time_min, time_max, max_at) - self._body.append([pid, tid, comm, no_executed, time_sum, color_one_sample, - time_mean, time_median, time_min, time_max, - _COLORS["grey"], max_at, _COLORS["reset"], color_reset]) - if args.summary_extended: - self._body[-1].extend([timespans.max_out_in, - _COLORS["grey"], timespans.max_at, - _COLORS["reset"], timespans.max_out_out, - timespans.max_in_in, - timespans.max_in_out]) - align_helper.out_in = timespans.max_out_in - align_helper.inter_at = timespans.max_at - align_helper.out_out = timespans.max_out_out - align_helper.in_in = timespans.max_in_in - align_helper.in_out = timespans.max_in_out - self._calc_alignments_summary(align_helper) - - def _format_stats(self): - separator, fix_csv_align = _prepare_fmt_sep() - decimal_precision, time_precision = _prepare_fmt_precision() - len_pid = db["task_info"]["pid"] * fix_csv_align - len_tid = db["task_info"]["tid"] * fix_csv_align - len_comm = db["task_info"]["comm"] * fix_csv_align - len_runs = db["runtime_info"]["runs"] * fix_csv_align - len_acc = db["runtime_info"]["acc"] * fix_csv_align - len_mean = db["runtime_info"]["mean"] * fix_csv_align - len_median = db["runtime_info"]["median"] * fix_csv_align - len_min = (db["runtime_info"]["min"] - decimal_precision) * fix_csv_align - len_max = (db["runtime_info"]["max"] - decimal_precision) * fix_csv_align - len_max_at = (db["runtime_info"]["max_at"] - time_precision) * fix_csv_align - if args.summary_extended: - len_out_in = ( - db["inter_times"]["out_in"] - decimal_precision - ) * fix_csv_align - len_inter_at = ( - db["inter_times"]["inter_at"] - time_precision - ) * fix_csv_align - len_out_out = ( - db["inter_times"]["out_out"] - decimal_precision - ) * fix_csv_align - len_in_in = (db["inter_times"]["in_in"] - decimal_precision) * fix_csv_align - len_in_out = ( - db["inter_times"]["in_out"] - decimal_precision - ) * fix_csv_align - - fmt = "{{:{}d}}".format(len_pid) - fmt += "{}{{:{}d}}".format(separator, len_tid) - fmt += "{}{{:>{}}}".format(separator, len_comm) - fmt += "{}{{:{}d}}".format(separator, len_runs) - fmt += "{}{{:{}.{}f}}".format(separator, len_acc, time_precision) - fmt += "{}{{}}{{:{}.{}f}}".format(separator, len_mean, time_precision) - fmt += "{}{{:{}.{}f}}".format(separator, len_median, time_precision) - fmt += "{}{{:{}.{}f}}".format(separator, len_min, time_precision) - fmt += "{}{{:{}.{}f}}".format(separator, len_max, time_precision) - fmt += "{}{{}}{{:{}.{}f}}{{}}{{}}".format( - separator, len_max_at, decimal_precision - ) - if args.summary_extended: - fmt += "{}{{:{}.{}f}}".format(separator, len_out_in, time_precision) - fmt += "{}{{}}{{:{}.{}f}}{{}}".format( - separator, len_inter_at, decimal_precision - ) - fmt += "{}{{:{}.{}f}}".format(separator, len_out_out, time_precision) - fmt += "{}{{:{}.{}f}}".format(separator, len_in_in, time_precision) - fmt += "{}{{:{}.{}f}}".format(separator, len_in_out, time_precision) - return fmt - - - def _calc_alignments_summary(self, align_helper): - # Length is being cut in 3 groups so that further addition is easier to handle. - # The length of every argument from the alignment helper is being checked if it - # is longer than the longest until now. In that case the length is being saved. - for key in db["task_info"]: - if len(str(getattr(align_helper, key))) > db["task_info"][key]: - db["task_info"][key] = len(str(getattr(align_helper, key))) - for key in db["runtime_info"]: - if len(str(getattr(align_helper, key))) > db["runtime_info"][key]: - db["runtime_info"][key] = len(str(getattr(align_helper, key))) - if args.summary_extended: - for key in db["inter_times"]: - if len(str(getattr(align_helper, key))) > db["inter_times"][key]: - db["inter_times"][key] = len(str(getattr(align_helper, key))) - - - def print(self): - self._task_stats() - fmt = self._format_stats() - - if not args.csv_summary: - print("\nSummary") - self._print_header() - self._column_titles() - for i in range(len(self._body)): - fd_sum.write(fmt.format(*tuple(self._body[i])) + "\n") - - - -class Task(object): - """ The class is used to handle the information of a given task.""" - - def __init__(self, id, tid, cpu, comm): - self.id = id - self.tid = tid - self.cpu = cpu - self.comm = comm - self.pid = None - self._time_in = None - self._time_out = None - - def schedule_in_at(self, time): - """set the time where the task was scheduled in""" - self._time_in = time - - def schedule_out_at(self, time): - """set the time where the task was scheduled out""" - self._time_out = time - - def time_out(self, unit="s"): - """return time where a given task was scheduled out""" - factor = time_uniter(unit) - return self._time_out * decimal.Decimal(factor) - - def time_in(self, unit="s"): - """return time where a given task was scheduled in""" - factor = time_uniter(unit) - return self._time_in * decimal.Decimal(factor) - - def runtime(self, unit="us"): - factor = time_uniter(unit) - return (self._time_out - self._time_in) * decimal.Decimal(factor) - - def update_pid(self, pid): - self.pid = pid - - -def _task_id(pid, cpu): - """returns a "unique-enough" identifier, please do not change""" - return "{}-{}".format(pid, cpu) - - -def _filter_non_printable(unfiltered): - """comm names may contain loony chars like '\x00000'""" - filtered = "" - for char in unfiltered: - if char not in string.printable: - continue - filtered += char - return filtered - - -def _fmt_header(): - separator, fix_csv_align = _prepare_fmt_sep() - fmt = "{{:>{}}}".format(LEN_SWITCHED_IN*fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, LEN_SWITCHED_OUT*fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, LEN_CPU*fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, LEN_PID*fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, LEN_TID*fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, LEN_COMM*fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, LEN_RUNTIME*fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, LEN_OUT_IN*fix_csv_align) - if args.extended_times: - fmt += "{}{{:>{}}}".format(separator, LEN_OUT_OUT*fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, LEN_IN_IN*fix_csv_align) - fmt += "{}{{:>{}}}".format(separator, LEN_IN_OUT*fix_csv_align) - return fmt - - -def _fmt_body(): - separator, fix_csv_align = _prepare_fmt_sep() - decimal_precision, time_precision = _prepare_fmt_precision() - fmt = "{{}}{{:{}.{}f}}".format(LEN_SWITCHED_IN*fix_csv_align, decimal_precision) - fmt += "{}{{:{}.{}f}}".format( - separator, LEN_SWITCHED_OUT*fix_csv_align, decimal_precision - ) - fmt += "{}{{:{}d}}".format(separator, LEN_CPU*fix_csv_align) - fmt += "{}{{:{}d}}".format(separator, LEN_PID*fix_csv_align) - fmt += "{}{{}}{{:{}d}}{{}}".format(separator, LEN_TID*fix_csv_align) - fmt += "{}{{}}{{:>{}}}".format(separator, LEN_COMM*fix_csv_align) - fmt += "{}{{:{}.{}f}}".format(separator, LEN_RUNTIME*fix_csv_align, time_precision) - if args.extended_times: - fmt += "{}{{:{}.{}f}}".format(separator, LEN_OUT_IN*fix_csv_align, time_precision) - fmt += "{}{{:{}.{}f}}".format(separator, LEN_OUT_OUT*fix_csv_align, time_precision) - fmt += "{}{{:{}.{}f}}".format(separator, LEN_IN_IN*fix_csv_align, time_precision) - fmt += "{}{{:{}.{}f}}{{}}".format( - separator, LEN_IN_OUT*fix_csv_align, time_precision - ) - else: - fmt += "{}{{:{}.{}f}}{{}}".format( - separator, LEN_OUT_IN*fix_csv_align, time_precision - ) - return fmt - - -def _print_header(): - fmt = _fmt_header() - header = ("Switched-In", "Switched-Out", "CPU", "PID", "TID", "Comm", "Runtime", - "Time Out-In") - if args.extended_times: - header += ("Time Out-Out", "Time In-In", "Time In-Out") - fd_task.write(fmt.format(*header) + "\n") - - - -def _print_task_finish(task): - """calculating every entry of a row and printing it immediately""" - c_row_set = "" - c_row_reset = "" - out_in = -1 - out_out = -1 - in_in = -1 - in_out = -1 - fmt = _fmt_body() - # depending on user provided highlight option we change the color - # for particular tasks - if str(task.tid) in args.highlight_tasks_map: - c_row_set = _COLORS[args.highlight_tasks_map[str(task.tid)]] - c_row_reset = _COLORS["reset"] - if task.comm in args.highlight_tasks_map: - c_row_set = _COLORS[args.highlight_tasks_map[task.comm]] - c_row_reset = _COLORS["reset"] - # grey-out entries if PID == TID, they - # are identical, no threaded model so the - # thread id (tid) do not matter - c_tid_set = "" - c_tid_reset = "" - if task.pid == task.tid: - c_tid_set = _COLORS["grey"] - c_tid_reset = _COLORS["reset"] - if task.tid in db["tid"]: - # get last task of tid - last_tid_task = db["tid"][task.tid][-1] - # feed the timespan calculate, last in tid db - # and second the current one - timespan_gap_tid = Timespans() - timespan_gap_tid.feed(last_tid_task) - timespan_gap_tid.feed(task) - out_in = timespan_gap_tid.out_in - out_out = timespan_gap_tid.out_out - in_in = timespan_gap_tid.in_in - in_out = timespan_gap_tid.in_out - - - if args.extended_times: - line_out = fmt.format(c_row_set, task.time_in(), task.time_out(), task.cpu, - task.pid, c_tid_set, task.tid, c_tid_reset, c_row_set, task.comm, - task.runtime(time_unit), out_in, out_out, in_in, in_out, - c_row_reset) + "\n" - else: - line_out = fmt.format(c_row_set, task.time_in(), task.time_out(), task.cpu, - task.pid, c_tid_set, task.tid, c_tid_reset, c_row_set, task.comm, - task.runtime(time_unit), out_in, c_row_reset) + "\n" - try: - fd_task.write(line_out) - except(IOError): - # don't mangle the output if user SIGINT this script - sys.exit() - -def _record_cleanup(_list): - """ - no need to store more then one element if --summarize - is not enabled - """ - if not args.summary and len(_list) > 1: - _list = _list[len(_list) - 1 :] - - -def _record_by_tid(task): - tid = task.tid - if tid not in db["tid"]: - db["tid"][tid] = [] - db["tid"][tid].append(task) - _record_cleanup(db["tid"][tid]) - - -def _record_by_cpu(task): - cpu = task.cpu - if cpu not in db["cpu"]: - db["cpu"][cpu] = [] - db["cpu"][cpu].append(task) - _record_cleanup(db["cpu"][cpu]) - - -def _record_global(task): - """record all executed task, ordered by finish chronological""" - db["global"].append(task) - _record_cleanup(db["global"]) - - -def _handle_task_finish(tid, cpu, time, perf_sample_dict): - if tid == 0: - return - _id = _task_id(tid, cpu) - if _id not in db["running"]: - # may happen, if we missed the switch to - # event. Seen in combination with --exclude-perf - # where the start is filtered out, but not the - # switched in. Probably a bug in exclude-perf - # option. - return - task = db["running"][_id] - task.schedule_out_at(time) - - # record tid, during schedule in the tid - # is not available, update now - pid = int(perf_sample_dict["sample"]["pid"]) - - task.update_pid(pid) - del db["running"][_id] - - # print only tasks which are not being filtered and no print of trace - # for summary only, but record every task. - if not _limit_filtered(tid, pid, task.comm) and not args.summary_only: - _print_task_finish(task) - _record_by_tid(task) - _record_by_cpu(task) - _record_global(task) - - -def _handle_task_start(tid, cpu, comm, time): - if tid == 0: - return - if tid in args.tid_renames: - comm = args.tid_renames[tid] - _id = _task_id(tid, cpu) - if _id in db["running"]: - # handle corner cases where already running tasks - # are switched-to again - saw this via --exclude-perf - # recorded traces. We simple ignore this "second start" - # event. - return - assert _id not in db["running"] - task = Task(_id, tid, cpu, comm) - task.schedule_in_at(time) - db["running"][_id] = task - - -def _time_to_internal(time_ns): - """ - To prevent float rounding errors we use Decimal internally - """ - return decimal.Decimal(time_ns) / decimal.Decimal(1e9) - - -def _limit_filtered(tid, pid, comm): - if args.filter_tasks: - if str(tid) in args.filter_tasks or comm in args.filter_tasks: - return True - else: - return False - if args.limit_to_tasks: - if str(tid) in args.limit_to_tasks or comm in args.limit_to_tasks: - return False - else: - return True - - -def _argument_filter_sanity_check(): - if args.limit_to_tasks and args.filter_tasks: - sys.exit("Error: Filter and Limit at the same time active.") - if args.extended_times and args.summary_only: - sys.exit("Error: Summary only and extended times active.") - if args.time_limit and ":" not in args.time_limit: - sys.exit( - "Error: No bound set for time limit. Please set bound by ':' e.g :123." - ) - if args.time_limit and (args.summary or args.summary_only or args.summary_extended): - sys.exit("Error: Cannot set time limit and print summary") - if args.csv_summary: - args.summary = True - if args.csv == args.csv_summary: - sys.exit("Error: Chosen files for csv and csv summary are the same") - if args.csv and (args.summary_extended or args.summary) and not args.csv_summary: - sys.exit("Error: No file chosen to write summary to. Choose with --csv-summary " - "") - if args.csv and args.summary_only: - sys.exit("Error: --csv chosen and --summary-only. Standard task would not be" - "written to csv file.") - -def _argument_prepare_check(): - global time_unit, fd_task, fd_sum - if args.filter_tasks: - args.filter_tasks = args.filter_tasks.split(",") - if args.limit_to_tasks: - args.limit_to_tasks = args.limit_to_tasks.split(",") - if args.time_limit: - args.time_limit = args.time_limit.split(":") - for rename_tuple in args.rename_comms_by_tids.split(","): - tid_name = rename_tuple.split(":") - if len(tid_name) != 2: - continue - args.tid_renames[int(tid_name[0])] = tid_name[1] - args.highlight_tasks_map = dict() - for highlight_tasks_tuple in args.highlight_tasks.split(","): - tasks_color_map = highlight_tasks_tuple.split(":") - # default highlight color to red if no color set by user - if len(tasks_color_map) == 1: - tasks_color_map.append("red") - if args.highlight_tasks and tasks_color_map[1].lower() not in _COLORS: - sys.exit( - "Error: Color not defined, please choose from grey,red,green,yellow,blue," - "violet" - ) - if len(tasks_color_map) != 2: - continue - args.highlight_tasks_map[tasks_color_map[0]] = tasks_color_map[1] - time_unit = "us" - if args.ns: - time_unit = "ns" - elif args.ms: - time_unit = "ms" - - - fd_task = sys.stdout - if args.csv: - args.stdio_color = "never" - fd_task = open(args.csv, "w") - print("generating csv at",args.csv,) - - fd_sum = sys.stdout - if args.csv_summary: - args.stdio_color = "never" - fd_sum = open(args.csv_summary, "w") - print("generating csv summary at",args.csv_summary) - if not args.csv: - args.summary_only = True - - -def _is_within_timelimit(time): - """ - Check if a time limit was given by parameter, if so ignore the rest. If not, - process the recorded trace in its entirety. - """ - if not args.time_limit: - return True - lower_time_limit = args.time_limit[0] - upper_time_limit = args.time_limit[1] - # check for upper limit - if upper_time_limit == "": - if time >= decimal.Decimal(lower_time_limit): - return True - # check for lower limit - if lower_time_limit == "": - if time <= decimal.Decimal(upper_time_limit): - return True - # quit if time exceeds upper limit. Good for big datasets - else: - quit() - if lower_time_limit != "" and upper_time_limit != "": - if (time >= decimal.Decimal(lower_time_limit) and - time <= decimal.Decimal(upper_time_limit)): - return True - # quit if time exceeds upper limit. Good for big datasets - elif time > decimal.Decimal(upper_time_limit): - quit() - -def _prepare_fmt_precision(): - decimal_precision = 6 - time_precision = 3 - if args.ns: - decimal_precision = 9 - time_precision = 0 - return decimal_precision, time_precision - -def _prepare_fmt_sep(): - separator = " " - fix_csv_align = 1 - if args.csv or args.csv_summary: - separator = ";" - fix_csv_align = 0 - return separator, fix_csv_align - -def trace_unhandled(event_name, context, event_fields_dict, perf_sample_dict): - pass - - -def trace_begin(): - _parse_args() - _check_color() - _init_db() - if not args.summary_only: - _print_header() - -def trace_end(): - if args.summary or args.summary_extended or args.summary_only: - Summary().print() - -def sched__sched_switch(event_name, context, common_cpu, common_secs, common_nsecs, - common_pid, common_comm, common_callchain, prev_comm, - prev_pid, prev_prio, prev_state, next_comm, next_pid, - next_prio, perf_sample_dict): - # ignore common_secs & common_nsecs cause we need - # high res timestamp anyway, using the raw value is - # faster - time = _time_to_internal(perf_sample_dict["sample"]["time"]) - if not _is_within_timelimit(time): - # user specific --time-limit a:b set - return - - next_comm = _filter_non_printable(next_comm) - _handle_task_finish(prev_pid, common_cpu, time, perf_sample_dict) - _handle_task_start(next_pid, common_cpu, next_comm, time) diff --git a/tools/perf/tests/shell/script.sh b/tools/perf/tests/shell/script.sh index 254fc3ae94e7..af687c7cfe44 100755 --- a/tools/perf/tests/shell/script.sh +++ b/tools/perf/tests/shell/script.sh @@ -6,9 +6,6 @@ set -e temp_dir=$(mktemp -d /tmp/perf-test-script.XXXXXXXXXX) -perfdatafile="${temp_dir}/perf.data" -db_test="${temp_dir}/db_test.py" - err=0 cleanup() @@ -30,44 +27,6 @@ trap_cleanup() trap trap_cleanup EXIT TERM INT - -test_db() -{ - echo "DB test" - - # Check if python script is supported - if perf version --build-options | grep python | grep -q OFF ; then - echo "SKIP: python scripting is not supported" - err=2 - return - fi - - cat << "_end_of_file_" > "${db_test}" -perf_db_export_mode = True -perf_db_export_calls = False -perf_db_export_callchains = True - -def sample_table(*args): - print(f'sample_table({args})') - -def call_path_table(*args): - print(f'call_path_table({args}') -_end_of_file_ - case $(uname -m) - in s390x) - cmd_flags="--call-graph dwarf -e cpu-clock";; - *) - cmd_flags="-g";; - esac - - perf record $cmd_flags -o "${perfdatafile}" true - # Disable lsan to avoid warnings about python memory leaks. - export ASAN_OPTIONS=detect_leaks=0 - perf script -i "${perfdatafile}" -s "${db_test}" - export ASAN_OPTIONS= - echo "DB test [Success]" -} - test_parallel_perf() { echo "parallel-perf test" @@ -91,7 +50,6 @@ test_parallel_perf() echo "parallel-perf test [Success]" } -test_db test_parallel_perf cleanup diff --git a/tools/perf/tests/shell/script_python.sh b/tools/perf/tests/shell/script_python.sh deleted file mode 100755 index 6bc66074a31f..000000000000 --- a/tools/perf/tests/shell/script_python.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/bash -# perf script python tests -# SPDX-License-Identifier: GPL-2.0 - -set -e - -# set PERF_EXEC_PATH to find scripts in the source directory -perfdir=$(dirname "$0")/../.. -if [ -e "$perfdir/scripts/python/Perf-Trace-Util" ]; then - export PERF_EXEC_PATH=$perfdir -fi - - -perfdata=$(mktemp /tmp/__perf_test_script_python.perf.data.XXXXX) -generated_script=$(mktemp /tmp/__perf_test_script.XXXXX.py) - -cleanup() { - rm -f "${perfdata}" - rm -f "${generated_script}" - trap - EXIT TERM INT -} - -trap_cleanup() { - echo "Unexpected signal in ${FUNCNAME[1]}" - cleanup - exit 1 -} -trap trap_cleanup TERM INT -trap cleanup EXIT - -check_python_support() { - if perf check feature -q libpython; then - return 0 - fi - echo "perf script python test [Skipped: no libpython support]" - return 2 -} - -test_script() { - local event_name=$1 - local expected_output=$2 - local record_opts=$3 - - echo "Testing event: $event_name" - - # Try to record. If this fails, it might be permissions or lack of - # support. Return 2 to indicate "skip this event" rather than "fail - # test". - if ! perf record -o "${perfdata}" -e "$event_name" $record_opts -- perf test -w thloop > /dev/null 2>&1; then - echo "perf script python test [Skipped: failed to record $event_name]" - return 2 - fi - - echo "Generating python script..." - if ! perf script -i "${perfdata}" -g "${generated_script}"; then - echo "perf script python test [Failed: script generation for $event_name]" - return 1 - fi - - if [ ! -f "${generated_script}" ]; then - echo "perf script python test [Failed: script not generated for $event_name]" - return 1 - fi - - # Perf script -g python doesn't generate process_event for generic - # events so append it manually to test that the callback works. - if ! grep -q "def process_event" "${generated_script}"; then - cat <> "${generated_script}" - -def process_event(param_dict): - print("param_dict: %s" % param_dict) -EOF - fi - - echo "Executing python script..." - output=$(perf script -i "${perfdata}" -s "${generated_script}" 2>&1) - - if echo "$output" | grep -q "$expected_output"; then - echo "perf script python test [Success: $event_name triggered $expected_output]" - return 0 - else - echo "perf script python test [Failed: $event_name did not trigger $expected_output]" - echo "Output was:" - echo "$output" | head -n 20 - return 1 - fi -} - -check_python_support || exit 2 - -# Try tracepoint first -test_script "sched:sched_switch" "sched__sched_switch" "-c 1" && res=0 || res=$? - -if [ $res -eq 0 ]; then - exit 0 -elif [ $res -eq 1 ]; then - exit 1 -fi - -# If tracepoint skipped (res=2), try task-clock -# For generic events like task-clock, the generated script uses process_event() -# which prints the param_dict. -test_script "task-clock" "param_dict" "-c 100" && res=0 || res=$? - -if [ $res -eq 0 ]; then - exit 0 -elif [ $res -eq 1 ]; then - exit 1 -fi - -# If both skipped -echo "perf script python test [Skipped: Could not record tracepoint or task-clock]" -exit 2 diff --git a/tools/perf/util/scripting-engines/Build b/tools/perf/util/scripting-engines/Build index 24f087b0cd11..3f1dc10526f8 100644 --- a/tools/perf/util/scripting-engines/Build +++ b/tools/perf/util/scripting-engines/Build @@ -1,9 +1,5 @@ ifeq ($(CONFIG_LIBTRACEEVENT),y) perf-util-$(CONFIG_LIBPERL) += trace-event-perl.o endif -perf-util-$(CONFIG_LIBPYTHON) += trace-event-python.o CFLAGS_trace-event-perl.o += $(PERL_EMBED_CCOPTS) -Wno-redundant-decls -Wno-strict-prototypes -Wno-unused-parameter -Wno-shadow -Wno-nested-externs -Wno-undef -Wno-switch-default -Wno-bad-function-cast -Wno-declaration-after-statement -Wno-switch-enum -Wno-thread-safety-analysis - -# -Wno-declaration-after-statement: The python headers have mixed code with declarations (decls after asserts, for instance) -CFLAGS_trace-event-python.o += $(PYTHON_EMBED_CCOPTS) -Wno-redundant-decls -Wno-strict-prototypes -Wno-unused-parameter -Wno-shadow -Wno-deprecated-declarations -Wno-switch-enum -Wno-declaration-after-statement diff --git a/tools/perf/util/scripting-engines/trace-event-python.c b/tools/perf/util/scripting-engines/trace-event-python.c deleted file mode 100644 index 038c4268a429..000000000000 --- a/tools/perf/util/scripting-engines/trace-event-python.c +++ /dev/null @@ -1,2335 +0,0 @@ -/* - * trace-event-python. Feed trace events to an embedded Python interpreter. - * - * Copyright (C) 2010 Tom Zanussi - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - */ - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef HAVE_LIBTRACEEVENT -#include -#endif - -#include "../build-id.h" -#include "../counts.h" -#include "../debug.h" -#include "../dso.h" -#include "../callchain.h" -#include "../env.h" -#include "../evsel.h" -#include "../event.h" -#include "../thread.h" -#include "../comm.h" -#include "../machine.h" -#include "../mem-info.h" -#include "../db-export.h" -#include "../thread-stack.h" -#include "../trace-event.h" -#include "../call-path.h" -#include "dwarf-regs.h" -#include "map.h" -#include "symbol.h" -#include "thread_map.h" -#include "print_binary.h" -#include "stat.h" -#include "mem-events.h" -#include "util/perf_regs.h" -#include "util/session.h" - -#define _PyUnicode_FromString(arg) \ - PyUnicode_FromString(arg) -#define _PyUnicode_FromStringAndSize(arg1, arg2) \ - PyUnicode_FromStringAndSize((arg1), (arg2)) -#define _PyBytes_FromStringAndSize(arg1, arg2) \ - PyBytes_FromStringAndSize((arg1), (arg2)) -#define _PyLong_FromLong(arg) \ - PyLong_FromLong(arg) -#define _PyLong_AsLong(arg) \ - PyLong_AsLong(arg) -#define _PyCapsule_New(arg1, arg2, arg3) \ - PyCapsule_New((arg1), (arg2), (arg3)) - -PyMODINIT_FUNC PyInit_perf_trace_context(void); - -#ifdef HAVE_LIBTRACEEVENT -#define TRACE_EVENT_TYPE_MAX \ - ((1 << (sizeof(unsigned short) * 8)) - 1) - -#define N_COMMON_FIELDS 7 - -static char *cur_field_name; -static int zero_flag_atom; -#endif - -#define MAX_FIELDS 64 - -extern struct scripting_context *scripting_context; - -static PyObject *main_module, *main_dict; - -struct tables { - struct db_export dbe; - PyObject *evsel_handler; - PyObject *machine_handler; - PyObject *thread_handler; - PyObject *comm_handler; - PyObject *comm_thread_handler; - PyObject *dso_handler; - PyObject *symbol_handler; - PyObject *branch_type_handler; - PyObject *sample_handler; - PyObject *call_path_handler; - PyObject *call_return_handler; - PyObject *synth_handler; - PyObject *context_switch_handler; - bool db_export_mode; -}; - -static struct tables tables_global; - -static void handler_call_die(const char *handler_name) __noreturn; -static void handler_call_die(const char *handler_name) -{ - PyErr_Print(); - Py_FatalError("problem in Python trace event handler"); - // Py_FatalError does not return - // but we have to make the compiler happy - abort(); -} - -/* - * Insert val into the dictionary and decrement the reference counter. - * This is necessary for dictionaries since PyDict_SetItemString() does not - * steal a reference, as opposed to PyTuple_SetItem(). - */ -static void pydict_set_item_string_decref(PyObject *dict, const char *key, PyObject *val) -{ - PyDict_SetItemString(dict, key, val); - Py_DECREF(val); -} - -static PyObject *get_handler(const char *handler_name) -{ - PyObject *handler; - - handler = PyDict_GetItemString(main_dict, handler_name); - if (handler && !PyCallable_Check(handler)) - return NULL; - return handler; -} - -static void call_object(PyObject *handler, PyObject *args, const char *die_msg) -{ - PyObject *retval; - - retval = PyObject_CallObject(handler, args); - if (retval == NULL) - handler_call_die(die_msg); - Py_DECREF(retval); -} - -static void try_call_object(const char *handler_name, PyObject *args) -{ - PyObject *handler; - - handler = get_handler(handler_name); - if (handler) - call_object(handler, args, handler_name); -} - -#ifdef HAVE_LIBTRACEEVENT -static int get_argument_count(PyObject *handler) -{ - int arg_count = 0; - - PyObject *code_obj = PyObject_GetAttrString(handler, "__code__"); - PyErr_Clear(); - if (code_obj) { - PyObject *arg_count_obj = PyObject_GetAttrString(code_obj, - "co_argcount"); - if (arg_count_obj) { - arg_count = (int) _PyLong_AsLong(arg_count_obj); - Py_DECREF(arg_count_obj); - } - Py_DECREF(code_obj); - } - return arg_count; -} - -static void define_value(enum tep_print_arg_type field_type, - const char *ev_name, - const char *field_name, - const char *field_value, - const char *field_str) -{ - const char *handler_name = "define_flag_value"; - PyObject *t; - unsigned long long value; - unsigned n = 0; - - if (field_type == TEP_PRINT_SYMBOL) - handler_name = "define_symbolic_value"; - - t = PyTuple_New(4); - if (!t) - Py_FatalError("couldn't create Python tuple"); - - value = eval_flag(field_value); - - PyTuple_SetItem(t, n++, _PyUnicode_FromString(ev_name)); - PyTuple_SetItem(t, n++, _PyUnicode_FromString(field_name)); - PyTuple_SetItem(t, n++, _PyLong_FromLong(value)); - PyTuple_SetItem(t, n++, _PyUnicode_FromString(field_str)); - - try_call_object(handler_name, t); - - Py_DECREF(t); -} - -static void define_values(enum tep_print_arg_type field_type, - struct tep_print_flag_sym *field, - const char *ev_name, - const char *field_name) -{ - define_value(field_type, ev_name, field_name, field->value, - field->str); - - if (field->next) - define_values(field_type, field->next, ev_name, field_name); -} - -static void define_field(enum tep_print_arg_type field_type, - const char *ev_name, - const char *field_name, - const char *delim) -{ - const char *handler_name = "define_flag_field"; - PyObject *t; - unsigned n = 0; - - if (field_type == TEP_PRINT_SYMBOL) - handler_name = "define_symbolic_field"; - - if (field_type == TEP_PRINT_FLAGS) - t = PyTuple_New(3); - else - t = PyTuple_New(2); - if (!t) - Py_FatalError("couldn't create Python tuple"); - - PyTuple_SetItem(t, n++, _PyUnicode_FromString(ev_name)); - PyTuple_SetItem(t, n++, _PyUnicode_FromString(field_name)); - if (field_type == TEP_PRINT_FLAGS) - PyTuple_SetItem(t, n++, _PyUnicode_FromString(delim)); - - try_call_object(handler_name, t); - - Py_DECREF(t); -} - -static void define_event_symbols(struct tep_event *event, - const char *ev_name, - struct tep_print_arg *args) -{ - if (args == NULL) - return; - - switch (args->type) { - case TEP_PRINT_NULL: - break; - case TEP_PRINT_ATOM: - define_value(TEP_PRINT_FLAGS, ev_name, cur_field_name, "0", - args->atom.atom); - zero_flag_atom = 0; - break; - case TEP_PRINT_FIELD: - free(cur_field_name); - cur_field_name = strdup(args->field.name); - break; - case TEP_PRINT_FLAGS: - define_event_symbols(event, ev_name, args->flags.field); - define_field(TEP_PRINT_FLAGS, ev_name, cur_field_name, - args->flags.delim); - define_values(TEP_PRINT_FLAGS, args->flags.flags, ev_name, - cur_field_name); - break; - case TEP_PRINT_SYMBOL: - define_event_symbols(event, ev_name, args->symbol.field); - define_field(TEP_PRINT_SYMBOL, ev_name, cur_field_name, NULL); - define_values(TEP_PRINT_SYMBOL, args->symbol.symbols, ev_name, - cur_field_name); - break; - case TEP_PRINT_HEX: - case TEP_PRINT_HEX_STR: - define_event_symbols(event, ev_name, args->hex.field); - define_event_symbols(event, ev_name, args->hex.size); - break; - case TEP_PRINT_INT_ARRAY: - define_event_symbols(event, ev_name, args->int_array.field); - define_event_symbols(event, ev_name, args->int_array.count); - define_event_symbols(event, ev_name, args->int_array.el_size); - break; - case TEP_PRINT_STRING: - break; - case TEP_PRINT_TYPE: - define_event_symbols(event, ev_name, args->typecast.item); - break; - case TEP_PRINT_OP: - if (strcmp(args->op.op, ":") == 0) - zero_flag_atom = 1; - define_event_symbols(event, ev_name, args->op.left); - define_event_symbols(event, ev_name, args->op.right); - break; - default: - /* gcc warns for these? */ - case TEP_PRINT_BSTRING: - case TEP_PRINT_DYNAMIC_ARRAY: - case TEP_PRINT_DYNAMIC_ARRAY_LEN: - case TEP_PRINT_FUNC: - case TEP_PRINT_BITMASK: - /* we should warn... */ - return; - } - - if (args->next) - define_event_symbols(event, ev_name, args->next); -} - -static PyObject *get_field_numeric_entry(struct tep_event *event, - struct tep_format_field *field, void *data) -{ - bool is_array = field->flags & TEP_FIELD_IS_ARRAY; - PyObject *obj = NULL, *list = NULL; - unsigned long long val; - unsigned int item_size, n_items, i; - - if (is_array) { - list = PyList_New(field->arraylen); - if (!list) - Py_FatalError("couldn't create Python list"); - item_size = field->size / field->arraylen; - n_items = field->arraylen; - } else { - item_size = field->size; - n_items = 1; - } - - for (i = 0; i < n_items; i++) { - - val = read_size(event, data + field->offset + i * item_size, - item_size); - if (field->flags & TEP_FIELD_IS_SIGNED) { - if ((long long)val >= LONG_MIN && - (long long)val <= LONG_MAX) - obj = _PyLong_FromLong(val); - else - obj = PyLong_FromLongLong(val); - } else { - if (val <= LONG_MAX) - obj = _PyLong_FromLong(val); - else - obj = PyLong_FromUnsignedLongLong(val); - } - if (is_array) - PyList_SET_ITEM(list, i, obj); - } - if (is_array) - obj = list; - return obj; -} -#endif - -static const char *get_dsoname(struct map *map) -{ - const char *dsoname = "[unknown]"; - struct dso *dso = map ? map__dso(map) : NULL; - - if (dso) { - if (symbol_conf.show_kernel_path && dso__long_name(dso)) - dsoname = dso__long_name(dso); - else - dsoname = dso__name(dso); - } - - return dsoname; -} - -static unsigned long get_offset(struct symbol *sym, struct addr_location *al) -{ - unsigned long offset; - - if (al->addr < sym->end) - offset = al->addr - sym->start; - else - offset = al->addr - map__start(al->map) - sym->start; - - return offset; -} - -static PyObject *python_process_callchain(struct perf_sample *sample, - struct addr_location *al) -{ - PyObject *pylist; - struct callchain_cursor *cursor; - - pylist = PyList_New(0); - if (!pylist) - Py_FatalError("couldn't create Python list"); - - if (!symbol_conf.use_callchain || !sample->callchain) - goto exit; - - cursor = get_tls_callchain_cursor(); - if (thread__resolve_callchain(al->thread, cursor, - sample, NULL, NULL, - scripting_max_stack) != 0) { - pr_err("Failed to resolve callchain. Skipping\n"); - goto exit; - } - callchain_cursor_commit(cursor); - - - while (1) { - PyObject *pyelem; - struct callchain_cursor_node *node; - node = callchain_cursor_current(cursor); - if (!node) - break; - - pyelem = PyDict_New(); - if (!pyelem) - Py_FatalError("couldn't create Python dictionary"); - - - pydict_set_item_string_decref(pyelem, "ip", - PyLong_FromUnsignedLongLong(node->ip)); - - if (node->ms.sym) { - PyObject *pysym = PyDict_New(); - if (!pysym) - Py_FatalError("couldn't create Python dictionary"); - pydict_set_item_string_decref(pysym, "start", - PyLong_FromUnsignedLongLong(node->ms.sym->start)); - pydict_set_item_string_decref(pysym, "end", - PyLong_FromUnsignedLongLong(node->ms.sym->end)); - pydict_set_item_string_decref(pysym, "binding", - _PyLong_FromLong(symbol__binding(node->ms.sym))); - pydict_set_item_string_decref(pysym, "name", - _PyUnicode_FromStringAndSize(node->ms.sym->name, - node->ms.sym->namelen)); - pydict_set_item_string_decref(pyelem, "sym", pysym); - - if (node->ms.map) { - struct map *map = node->ms.map; - struct addr_location node_al; - unsigned long offset; - - addr_location__init(&node_al); - node_al.addr = map__map_ip(map, node->ip); - node_al.map = map__get(map); - offset = get_offset(node->ms.sym, &node_al); - addr_location__exit(&node_al); - - pydict_set_item_string_decref( - pyelem, "sym_off", - PyLong_FromUnsignedLongLong(offset)); - } - if (node->srcline && strcmp(":0", node->srcline)) { - pydict_set_item_string_decref( - pyelem, "sym_srcline", - _PyUnicode_FromString(node->srcline)); - } - } - - if (node->ms.map) { - const char *dsoname = get_dsoname(node->ms.map); - - pydict_set_item_string_decref(pyelem, "dso", - _PyUnicode_FromString(dsoname)); - } - - callchain_cursor_advance(cursor); - PyList_Append(pylist, pyelem); - Py_DECREF(pyelem); - } - -exit: - return pylist; -} - -static PyObject *python_process_brstack(struct perf_sample *sample, - struct thread *thread) -{ - struct branch_stack *br = sample->branch_stack; - struct branch_entry *entries = perf_sample__branch_entries(sample); - PyObject *pylist; - u64 i; - - pylist = PyList_New(0); - if (!pylist) - Py_FatalError("couldn't create Python list"); - - if (!(br && br->nr)) - goto exit; - - for (i = 0; i < br->nr; i++) { - PyObject *pyelem; - struct addr_location al; - const char *dsoname; - - pyelem = PyDict_New(); - if (!pyelem) - Py_FatalError("couldn't create Python dictionary"); - - pydict_set_item_string_decref(pyelem, "from", - PyLong_FromUnsignedLongLong(entries[i].from)); - pydict_set_item_string_decref(pyelem, "to", - PyLong_FromUnsignedLongLong(entries[i].to)); - pydict_set_item_string_decref(pyelem, "mispred", - PyBool_FromLong(entries[i].flags.mispred)); - pydict_set_item_string_decref(pyelem, "predicted", - PyBool_FromLong(entries[i].flags.predicted)); - pydict_set_item_string_decref(pyelem, "in_tx", - PyBool_FromLong(entries[i].flags.in_tx)); - pydict_set_item_string_decref(pyelem, "abort", - PyBool_FromLong(entries[i].flags.abort)); - pydict_set_item_string_decref(pyelem, "cycles", - PyLong_FromUnsignedLongLong(entries[i].flags.cycles)); - - addr_location__init(&al); - thread__find_map_fb(thread, sample->cpumode, - entries[i].from, &al); - dsoname = get_dsoname(al.map); - pydict_set_item_string_decref(pyelem, "from_dsoname", - _PyUnicode_FromString(dsoname)); - - thread__find_map_fb(thread, sample->cpumode, - entries[i].to, &al); - dsoname = get_dsoname(al.map); - pydict_set_item_string_decref(pyelem, "to_dsoname", - _PyUnicode_FromString(dsoname)); - - addr_location__exit(&al); - PyList_Append(pylist, pyelem); - Py_DECREF(pyelem); - } - -exit: - return pylist; -} - -static int get_symoff(struct symbol *sym, struct addr_location *al, - bool print_off, char *bf, int size) -{ - unsigned long offset; - - if (!sym || !sym->name[0]) - return scnprintf(bf, size, "%s", "[unknown]"); - - if (!print_off) - return scnprintf(bf, size, "%s", sym->name); - - offset = get_offset(sym, al); - - return scnprintf(bf, size, "%s+0x%x", sym->name, offset); -} - -static int get_br_mspred(struct branch_flags *flags, char *bf, int size) -{ - if (!flags->mispred && !flags->predicted) - return scnprintf(bf, size, "%s", "-"); - - if (flags->mispred) - return scnprintf(bf, size, "%s", "M"); - - return scnprintf(bf, size, "%s", "P"); -} - -static PyObject *python_process_brstacksym(struct perf_sample *sample, - struct thread *thread) -{ - struct branch_stack *br = sample->branch_stack; - struct branch_entry *entries = perf_sample__branch_entries(sample); - PyObject *pylist; - u64 i; - char bf[512]; - - pylist = PyList_New(0); - if (!pylist) - Py_FatalError("couldn't create Python list"); - - if (!(br && br->nr)) - goto exit; - - for (i = 0; i < br->nr; i++) { - PyObject *pyelem; - struct addr_location al; - - addr_location__init(&al); - pyelem = PyDict_New(); - if (!pyelem) - Py_FatalError("couldn't create Python dictionary"); - - thread__find_symbol_fb(thread, sample->cpumode, - entries[i].from, &al); - get_symoff(al.sym, &al, true, bf, sizeof(bf)); - pydict_set_item_string_decref(pyelem, "from", - _PyUnicode_FromString(bf)); - - thread__find_symbol_fb(thread, sample->cpumode, - entries[i].to, &al); - get_symoff(al.sym, &al, true, bf, sizeof(bf)); - pydict_set_item_string_decref(pyelem, "to", - _PyUnicode_FromString(bf)); - - get_br_mspred(&entries[i].flags, bf, sizeof(bf)); - pydict_set_item_string_decref(pyelem, "pred", - _PyUnicode_FromString(bf)); - - if (entries[i].flags.in_tx) { - pydict_set_item_string_decref(pyelem, "in_tx", - _PyUnicode_FromString("X")); - } else { - pydict_set_item_string_decref(pyelem, "in_tx", - _PyUnicode_FromString("-")); - } - - if (entries[i].flags.abort) { - pydict_set_item_string_decref(pyelem, "abort", - _PyUnicode_FromString("A")); - } else { - pydict_set_item_string_decref(pyelem, "abort", - _PyUnicode_FromString("-")); - } - - PyList_Append(pylist, pyelem); - Py_DECREF(pyelem); - addr_location__exit(&al); - } - -exit: - return pylist; -} - -static PyObject *get_sample_value_as_tuple(struct sample_read_value *value, - u64 read_format) -{ - PyObject *t; - - t = PyTuple_New(3); - if (!t) - Py_FatalError("couldn't create Python tuple"); - PyTuple_SetItem(t, 0, PyLong_FromUnsignedLongLong(value->id)); - PyTuple_SetItem(t, 1, PyLong_FromUnsignedLongLong(value->value)); - if (read_format & PERF_FORMAT_LOST) - PyTuple_SetItem(t, 2, PyLong_FromUnsignedLongLong(value->lost)); - - return t; -} - -static void set_sample_read_in_dict(PyObject *dict_sample, struct perf_sample *sample) -{ - u64 read_format = sample->evsel->core.attr.read_format; - PyObject *values; - unsigned int i; - - if (read_format & PERF_FORMAT_TOTAL_TIME_ENABLED) { - pydict_set_item_string_decref(dict_sample, "time_enabled", - PyLong_FromUnsignedLongLong(sample->read.time_enabled)); - } - - if (read_format & PERF_FORMAT_TOTAL_TIME_RUNNING) { - pydict_set_item_string_decref(dict_sample, "time_running", - PyLong_FromUnsignedLongLong(sample->read.time_running)); - } - - if (read_format & PERF_FORMAT_GROUP) - values = PyList_New(sample->read.group.nr); - else - values = PyList_New(1); - - if (!values) - Py_FatalError("couldn't create Python list"); - - if (read_format & PERF_FORMAT_GROUP) { - struct sample_read_value *v = sample->read.group.values; - - i = 0; - sample_read_group__for_each(v, sample->read.group.nr, read_format) { - PyObject *t = get_sample_value_as_tuple(v, read_format); - PyList_SET_ITEM(values, i, t); - i++; - } - } else { - PyObject *t = get_sample_value_as_tuple(&sample->read.one, - read_format); - PyList_SET_ITEM(values, 0, t); - } - pydict_set_item_string_decref(dict_sample, "values", values); -} - -static void set_sample_datasrc_in_dict(PyObject *dict, - struct perf_sample *sample) -{ - struct perf_session *session = evsel__session(sample->evsel); - struct mem_info *mi = mem_info__new(); - char decode[200]; - - if (!mi) - Py_FatalError("couldn't create mem-info"); - - pydict_set_item_string_decref(dict, "datasrc", - PyLong_FromUnsignedLongLong(sample->data_src)); - - mem_info__data_src(mi)->val = sample->data_src; - perf_script__meminfo_scnprintf(decode, 200, mi, session); - mem_info__put(mi); - - pydict_set_item_string_decref(dict, "datasrc_decode", - _PyUnicode_FromString(decode)); -} - -static int regs_map(struct regs_dump *regs, uint64_t mask, uint16_t e_machine, - uint32_t e_flags, char *bf, int size) -{ - unsigned int i = 0, r; - int printed = 0; - - bf[0] = 0; - - if (size <= 0) - return 0; - - if (!regs || !regs->regs) - return 0; - - for_each_set_bit(r, (unsigned long *) &mask, sizeof(mask) * 8) { - u64 val = regs->regs[i++]; - - printed += scnprintf(bf + printed, size - printed, - "%5s:0x%" PRIx64 " ", - perf_reg_name(r, e_machine, e_flags, regs->abi), val); - } - - return printed; -} - -static void simd_regs_map(struct regs_dump *regs, struct perf_event_attr *attr, - uint16_t e_machine, char *bf, int size, int printed, bool intr) -{ - const char *name = "unknown"; - int reg_c, idx, pred_base; - unsigned int i = 0, j; - uint16_t qwords; - - if (size <= 0 || printed >= size) - return; - - if (!regs || !regs->simd_data) - return; - - if (!regs->nr_vectors && !regs->nr_pred) - return; - - for (reg_c = 0; reg_c < 64; reg_c++) { - if (!regs->nr_vectors) - break; - if (intr) { - perf_intr_simd_reg_class_bitmap_qwords(e_machine, reg_c, - &qwords, /*pred=*/false); - } else { - perf_user_simd_reg_class_bitmap_qwords(e_machine, reg_c, - &qwords, /*pred=*/false); - } - if (regs->vector_qwords == qwords) { - name = perf_simd_reg_class_name(e_machine, reg_c, /*pred=*/false); - break; - } - } - - for (i = 0; i < regs->nr_vectors; i++) { - for (j = 0; j < regs->vector_qwords; j++) { - idx = i * regs->vector_qwords + j; - if (regs->vector_qwords > 1) { - printed += scnprintf(bf + printed, size - printed, - "%5s[%d][%d]:0x%" PRIx64 " ", - name, i, j, regs->simd_data[idx]); - } else { - printed += scnprintf(bf + printed, size - printed, - "%5s[%d]:0x%" PRIx64 " ", - name, i, regs->simd_data[idx]); - } - } - } - - name = "unknown"; - for (reg_c = 0; reg_c < 64; reg_c++) { - if (!regs->nr_pred) - break; - if (intr) { - perf_intr_simd_reg_class_bitmap_qwords(e_machine, reg_c, - &qwords, /*pred=*/true); - } else { - perf_user_simd_reg_class_bitmap_qwords(e_machine, reg_c, - &qwords, /*pred=*/true); - } - if (regs->pred_qwords == qwords) { - name = perf_simd_reg_class_name(e_machine, reg_c, /*pred=*/true); - break; - } - } - - pred_base = regs->nr_vectors * regs->vector_qwords; - for (i = 0; i < regs->nr_pred; i++) { - for (j = 0; j < regs->pred_qwords; j++) { - idx = pred_base + i * regs->pred_qwords + j; - if (regs->pred_qwords > 1) { - printed += scnprintf(bf + printed, size - printed, - "%5s[%d][%d]:0x%" PRIx64 " ", - name, i, j, regs->simd_data[idx]); - } else { - printed += scnprintf(bf + printed, size - printed, - "%5s[%d]:0x%" PRIx64 " ", - name, i, regs->simd_data[idx]); - } - } - } -} - -#define MAX_REG_SIZE 128 - -static int set_regs_in_dict(PyObject *dict, - struct perf_sample *sample, - uint16_t e_machine, - uint32_t e_flags) -{ - struct perf_event_attr *attr = &sample->evsel->core.attr; - int intr_size, user_size, size; - struct regs_dump *regs; - char *bf = NULL; - int printed; - - intr_size = (__sw_hweight64(attr->sample_regs_intr) * MAX_REG_SIZE) + 1; - user_size = (__sw_hweight64(attr->sample_regs_user) * MAX_REG_SIZE) + 1; - if (sample->intr_regs && attr->sample_simd_regs_enabled) { - regs = sample->intr_regs; - intr_size += (regs->nr_vectors * regs->vector_qwords + - regs->nr_pred * regs->pred_qwords) * MAX_REG_SIZE; - } - if (sample->user_regs && attr->sample_simd_regs_enabled) { - regs = sample->user_regs; - user_size += (regs->nr_vectors * regs->vector_qwords + - regs->nr_pred * regs->pred_qwords) * MAX_REG_SIZE; - } - size = intr_size > user_size ? intr_size : user_size; - - if (sample->intr_regs) { - bf = malloc(size); - if (!bf) - return -1; - - printed = regs_map(sample->intr_regs, attr->sample_regs_intr, - e_machine, e_flags, bf, size); - if (attr->sample_simd_regs_enabled) { - simd_regs_map(sample->intr_regs, attr, e_machine, bf, - size, printed, true); - } - - pydict_set_item_string_decref(dict, "iregs", - _PyUnicode_FromString(bf)); - } - - if (sample->user_regs) { - if (!bf) { - bf = malloc(size); - if (!bf) - return -1; - } - printed = regs_map(sample->user_regs, attr->sample_regs_user, - e_machine, e_flags, bf, size); - if (attr->sample_simd_regs_enabled) { - simd_regs_map(sample->user_regs, attr, e_machine, bf, - size, printed, false); - } - - pydict_set_item_string_decref(dict, "uregs", - _PyUnicode_FromString(bf)); - } - free(bf); - - return 0; -} - -static void set_sym_in_dict(PyObject *dict, struct addr_location *al, - const char *dso_field, const char *dso_bid_field, - const char *dso_map_start, const char *dso_map_end, - const char *sym_field, const char *symoff_field, - const char *map_pgoff) -{ - if (al->map) { - char sbuild_id[SBUILD_ID_SIZE]; - struct dso *dso = map__dso(al->map); - - pydict_set_item_string_decref(dict, dso_field, - _PyUnicode_FromString(dso__name(dso))); - build_id__snprintf(dso__bid(dso), sbuild_id, sizeof(sbuild_id)); - pydict_set_item_string_decref(dict, dso_bid_field, - _PyUnicode_FromString(sbuild_id)); - pydict_set_item_string_decref(dict, dso_map_start, - PyLong_FromUnsignedLong(map__start(al->map))); - pydict_set_item_string_decref(dict, dso_map_end, - PyLong_FromUnsignedLong(map__end(al->map))); - pydict_set_item_string_decref(dict, map_pgoff, - PyLong_FromUnsignedLongLong(map__pgoff(al->map))); - } - if (al->sym) { - pydict_set_item_string_decref(dict, sym_field, - _PyUnicode_FromString(al->sym->name)); - pydict_set_item_string_decref(dict, symoff_field, - PyLong_FromUnsignedLong(get_offset(al->sym, al))); - } -} - -static void set_sample_flags(PyObject *dict, u32 flags) -{ - const char *ch = PERF_IP_FLAG_CHARS; - char *p, str[33]; - - for (p = str; *ch; ch++, flags >>= 1) { - if (flags & 1) - *p++ = *ch; - } - *p = 0; - pydict_set_item_string_decref(dict, "flags", _PyUnicode_FromString(str)); -} - -static void python_process_sample_flags(struct perf_sample *sample, PyObject *dict_sample) -{ - char flags_disp[SAMPLE_FLAGS_BUF_SIZE]; - - set_sample_flags(dict_sample, sample->flags); - perf_sample__sprintf_flags(sample->flags, flags_disp, sizeof(flags_disp)); - pydict_set_item_string_decref(dict_sample, "flags_disp", - _PyUnicode_FromString(flags_disp)); -} - -static PyObject *get_perf_sample_dict(struct perf_sample *sample, - struct addr_location *al, - struct addr_location *addr_al, - PyObject *callchain) -{ - PyObject *dict, *dict_sample, *brstack, *brstacksym; - uint16_t e_machine = EM_HOST; - uint32_t e_flags = EF_HOST; - struct evsel *evsel = sample->evsel; - - dict = PyDict_New(); - if (!dict) - Py_FatalError("couldn't create Python dictionary"); - - dict_sample = PyDict_New(); - if (!dict_sample) - Py_FatalError("couldn't create Python dictionary"); - - pydict_set_item_string_decref(dict, "ev_name", _PyUnicode_FromString(evsel__name(evsel))); - pydict_set_item_string_decref(dict, "attr", _PyBytes_FromStringAndSize((const char *)&evsel->core.attr, sizeof(evsel->core.attr))); - - pydict_set_item_string_decref(dict_sample, "id", - PyLong_FromUnsignedLongLong(sample->id)); - pydict_set_item_string_decref(dict_sample, "stream_id", - PyLong_FromUnsignedLongLong(sample->stream_id)); - pydict_set_item_string_decref(dict_sample, "pid", - _PyLong_FromLong(sample->pid)); - pydict_set_item_string_decref(dict_sample, "tid", - _PyLong_FromLong(sample->tid)); - pydict_set_item_string_decref(dict_sample, "cpu", - _PyLong_FromLong(sample->cpu)); - pydict_set_item_string_decref(dict_sample, "ip", - PyLong_FromUnsignedLongLong(sample->ip)); - pydict_set_item_string_decref(dict_sample, "time", - PyLong_FromUnsignedLongLong(sample->time)); - pydict_set_item_string_decref(dict_sample, "period", - PyLong_FromUnsignedLongLong(sample->period)); - pydict_set_item_string_decref(dict_sample, "phys_addr", - PyLong_FromUnsignedLongLong(sample->phys_addr)); - pydict_set_item_string_decref(dict_sample, "addr", - PyLong_FromUnsignedLongLong(sample->addr)); - set_sample_read_in_dict(dict_sample, sample); - pydict_set_item_string_decref(dict_sample, "weight", - PyLong_FromUnsignedLongLong(sample->weight)); - pydict_set_item_string_decref(dict_sample, "ins_lat", - PyLong_FromUnsignedLong(sample->ins_lat)); - pydict_set_item_string_decref(dict_sample, "transaction", - PyLong_FromUnsignedLongLong(sample->transaction)); - set_sample_datasrc_in_dict(dict_sample, sample); - pydict_set_item_string_decref(dict, "sample", dict_sample); - - pydict_set_item_string_decref(dict, "raw_buf", _PyBytes_FromStringAndSize( - (const char *)sample->raw_data, sample->raw_size)); - pydict_set_item_string_decref(dict, "comm", - _PyUnicode_FromString(thread__comm_str(al->thread))); - set_sym_in_dict(dict, al, "dso", "dso_bid", "dso_map_start", "dso_map_end", - "symbol", "symoff", "map_pgoff"); - - pydict_set_item_string_decref(dict, "callchain", callchain); - - brstack = python_process_brstack(sample, al->thread); - pydict_set_item_string_decref(dict, "brstack", brstack); - - brstacksym = python_process_brstacksym(sample, al->thread); - pydict_set_item_string_decref(dict, "brstacksym", brstacksym); - - if (sample->machine_pid) { - pydict_set_item_string_decref(dict_sample, "machine_pid", - _PyLong_FromLong(sample->machine_pid)); - pydict_set_item_string_decref(dict_sample, "vcpu", - _PyLong_FromLong(sample->vcpu)); - } - - pydict_set_item_string_decref(dict_sample, "cpumode", - _PyLong_FromLong((unsigned long)sample->cpumode)); - - if (addr_al) { - pydict_set_item_string_decref(dict_sample, "addr_correlates_sym", - PyBool_FromLong(1)); - set_sym_in_dict(dict_sample, addr_al, "addr_dso", "addr_dso_bid", - "addr_dso_map_start", "addr_dso_map_end", - "addr_symbol", "addr_symoff", "addr_map_pgoff"); - } - - if (sample->flags) - python_process_sample_flags(sample, dict_sample); - - /* Instructions per cycle (IPC) */ - if (sample->insn_cnt && sample->cyc_cnt) { - pydict_set_item_string_decref(dict_sample, "insn_cnt", - PyLong_FromUnsignedLongLong(sample->insn_cnt)); - pydict_set_item_string_decref(dict_sample, "cyc_cnt", - PyLong_FromUnsignedLongLong(sample->cyc_cnt)); - } - - if (al->thread) - e_machine = thread__e_machine(al->thread, /*machine=*/NULL, &e_flags); - - if (set_regs_in_dict(dict, sample, e_machine, e_flags)) - Py_FatalError("Failed to setting regs in dict"); - - return dict; -} - -#ifdef HAVE_LIBTRACEEVENT -static void python_process_tracepoint(struct perf_sample *sample, - struct addr_location *al, - struct addr_location *addr_al) -{ - struct tep_event *event; - PyObject *handler, *context, *t, *obj = NULL, *callchain; - PyObject *dict = NULL, *all_entries_dict = NULL; - static char handler_name[256]; - struct tep_format_field *field; - unsigned long s, ns; - unsigned n = 0; - int pid; - int cpu = sample->cpu; - void *data = sample->raw_data; - unsigned long long nsecs = sample->time; - const char *comm = thread__comm_str(al->thread); - const char *default_handler_name = "trace_unhandled"; - DECLARE_BITMAP(events_defined, TRACE_EVENT_TYPE_MAX); - struct evsel *evsel = sample->evsel; - - bitmap_zero(events_defined, TRACE_EVENT_TYPE_MAX); - - event = evsel__tp_format(evsel); - if (!event) { - snprintf(handler_name, sizeof(handler_name), - "ug! no event found for type %" PRIu64, (u64)evsel->core.attr.config); - Py_FatalError(handler_name); - } - - pid = raw_field_value(event, "common_pid", data); - - sprintf(handler_name, "%s__%s", event->system, event->name); - - if (!__test_and_set_bit(event->id, events_defined)) - define_event_symbols(event, handler_name, event->print_fmt.args); - - handler = get_handler(handler_name); - if (!handler) { - handler = get_handler(default_handler_name); - if (!handler) - return; - dict = PyDict_New(); - if (!dict) - Py_FatalError("couldn't create Python dict"); - } - - t = PyTuple_New(MAX_FIELDS); - if (!t) - Py_FatalError("couldn't create Python tuple"); - - - s = nsecs / NSEC_PER_SEC; - ns = nsecs - s * NSEC_PER_SEC; - - context = _PyCapsule_New(scripting_context, NULL, NULL); - - PyTuple_SetItem(t, n++, _PyUnicode_FromString(handler_name)); - PyTuple_SetItem(t, n++, context); - - /* ip unwinding */ - callchain = python_process_callchain(sample, al); - /* Need an additional reference for the perf_sample dict */ - Py_INCREF(callchain); - - if (!dict) { - PyTuple_SetItem(t, n++, _PyLong_FromLong(cpu)); - PyTuple_SetItem(t, n++, _PyLong_FromLong(s)); - PyTuple_SetItem(t, n++, _PyLong_FromLong(ns)); - PyTuple_SetItem(t, n++, _PyLong_FromLong(pid)); - PyTuple_SetItem(t, n++, _PyUnicode_FromString(comm)); - PyTuple_SetItem(t, n++, callchain); - } else { - pydict_set_item_string_decref(dict, "common_cpu", _PyLong_FromLong(cpu)); - pydict_set_item_string_decref(dict, "common_s", _PyLong_FromLong(s)); - pydict_set_item_string_decref(dict, "common_ns", _PyLong_FromLong(ns)); - pydict_set_item_string_decref(dict, "common_pid", _PyLong_FromLong(pid)); - pydict_set_item_string_decref(dict, "common_comm", _PyUnicode_FromString(comm)); - pydict_set_item_string_decref(dict, "common_callchain", callchain); - } - for (field = event->format.fields; field; field = field->next) { - unsigned int offset, len; - unsigned long long val; - - if (field->flags & TEP_FIELD_IS_ARRAY) { - offset = field->offset; - len = field->size; - if (field->flags & TEP_FIELD_IS_DYNAMIC) { - val = tep_read_number(scripting_context->pevent, - data + offset, len); - offset = val; - len = offset >> 16; - offset &= 0xffff; - if (tep_field_is_relative(field->flags)) - offset += field->offset + field->size; - } - if (field->flags & TEP_FIELD_IS_STRING && - is_printable_array(data + offset, len)) { - obj = _PyUnicode_FromString((char *) data + offset); - } else { - obj = PyByteArray_FromStringAndSize((const char *) data + offset, len); - field->flags &= ~TEP_FIELD_IS_STRING; - } - } else { /* FIELD_IS_NUMERIC */ - obj = get_field_numeric_entry(event, field, data); - } - if (!dict) - PyTuple_SetItem(t, n++, obj); - else - pydict_set_item_string_decref(dict, field->name, obj); - - } - - if (dict) - PyTuple_SetItem(t, n++, dict); - - if (get_argument_count(handler) == (int) n + 1) { - all_entries_dict = get_perf_sample_dict(sample, al, addr_al, - callchain); - PyTuple_SetItem(t, n++, all_entries_dict); - } else { - Py_DECREF(callchain); - } - - if (_PyTuple_Resize(&t, n) == -1) - Py_FatalError("error resizing Python tuple"); - - if (!dict) - call_object(handler, t, handler_name); - else - call_object(handler, t, default_handler_name); - - Py_DECREF(t); -} -#else -static void python_process_tracepoint(struct perf_sample *sample __maybe_unused, - struct addr_location *al __maybe_unused, - struct addr_location *addr_al __maybe_unused) -{ - fprintf(stderr, "Tracepoint events are not supported because " - "perf is not linked with libtraceevent.\n"); -} -#endif - -static PyObject *tuple_new(unsigned int sz) -{ - PyObject *t; - - t = PyTuple_New(sz); - if (!t) - Py_FatalError("couldn't create Python tuple"); - return t; -} - -static int tuple_set_s64(PyObject *t, unsigned int pos, s64 val) -{ -#if BITS_PER_LONG == 64 - return PyTuple_SetItem(t, pos, _PyLong_FromLong(val)); -#endif -#if BITS_PER_LONG == 32 - return PyTuple_SetItem(t, pos, PyLong_FromLongLong(val)); -#endif -} - -/* - * Databases support only signed 64-bit numbers, so even though we are - * exporting a u64, it must be as s64. - */ -#define tuple_set_d64 tuple_set_s64 - -static int tuple_set_u64(PyObject *t, unsigned int pos, u64 val) -{ -#if BITS_PER_LONG == 64 - return PyTuple_SetItem(t, pos, PyLong_FromUnsignedLong(val)); -#endif -#if BITS_PER_LONG == 32 - return PyTuple_SetItem(t, pos, PyLong_FromUnsignedLongLong(val)); -#endif -} - -static int tuple_set_u32(PyObject *t, unsigned int pos, u32 val) -{ - return PyTuple_SetItem(t, pos, PyLong_FromUnsignedLong(val)); -} - -static int tuple_set_s32(PyObject *t, unsigned int pos, s32 val) -{ - return PyTuple_SetItem(t, pos, _PyLong_FromLong(val)); -} - -static int tuple_set_bool(PyObject *t, unsigned int pos, bool val) -{ - return PyTuple_SetItem(t, pos, PyBool_FromLong(val)); -} - -static int tuple_set_string(PyObject *t, unsigned int pos, const char *s) -{ - return PyTuple_SetItem(t, pos, _PyUnicode_FromString(s)); -} - -static int tuple_set_bytes(PyObject *t, unsigned int pos, void *bytes, - unsigned int sz) -{ - return PyTuple_SetItem(t, pos, _PyBytes_FromStringAndSize(bytes, sz)); -} - -static int python_export_evsel(struct db_export *dbe, struct evsel *evsel) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - - t = tuple_new(2); - - tuple_set_d64(t, 0, evsel->db_id); - tuple_set_string(t, 1, evsel__name(evsel)); - - call_object(tables->evsel_handler, t, "evsel_table"); - - Py_DECREF(t); - - return 0; -} - -static int python_export_machine(struct db_export *dbe, - struct machine *machine) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - - t = tuple_new(3); - - tuple_set_d64(t, 0, machine->db_id); - tuple_set_s32(t, 1, machine->pid); - tuple_set_string(t, 2, machine->root_dir ? machine->root_dir : ""); - - call_object(tables->machine_handler, t, "machine_table"); - - Py_DECREF(t); - - return 0; -} - -static int python_export_thread(struct db_export *dbe, struct thread *thread, - u64 main_thread_db_id, struct machine *machine) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - - t = tuple_new(5); - - tuple_set_d64(t, 0, thread__db_id(thread)); - tuple_set_d64(t, 1, machine->db_id); - tuple_set_d64(t, 2, main_thread_db_id); - tuple_set_s32(t, 3, thread__pid(thread)); - tuple_set_s32(t, 4, thread__tid(thread)); - - call_object(tables->thread_handler, t, "thread_table"); - - Py_DECREF(t); - - return 0; -} - -static int python_export_comm(struct db_export *dbe, struct comm *comm, - struct thread *thread) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - - t = tuple_new(5); - - tuple_set_d64(t, 0, comm->db_id); - tuple_set_string(t, 1, comm__str(comm)); - tuple_set_d64(t, 2, thread__db_id(thread)); - tuple_set_d64(t, 3, comm->start); - tuple_set_s32(t, 4, comm->exec); - - call_object(tables->comm_handler, t, "comm_table"); - - Py_DECREF(t); - - return 0; -} - -static int python_export_comm_thread(struct db_export *dbe, u64 db_id, - struct comm *comm, struct thread *thread) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - - t = tuple_new(3); - - tuple_set_d64(t, 0, db_id); - tuple_set_d64(t, 1, comm->db_id); - tuple_set_d64(t, 2, thread__db_id(thread)); - - call_object(tables->comm_thread_handler, t, "comm_thread_table"); - - Py_DECREF(t); - - return 0; -} - -static int python_export_dso(struct db_export *dbe, struct dso *dso, - struct machine *machine) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - char sbuild_id[SBUILD_ID_SIZE]; - PyObject *t; - - build_id__snprintf(dso__bid(dso), sbuild_id, sizeof(sbuild_id)); - - t = tuple_new(5); - - tuple_set_d64(t, 0, dso__db_id(dso)); - tuple_set_d64(t, 1, machine->db_id); - tuple_set_string(t, 2, dso__short_name(dso)); - tuple_set_string(t, 3, dso__long_name(dso)); - tuple_set_string(t, 4, sbuild_id); - - call_object(tables->dso_handler, t, "dso_table"); - - Py_DECREF(t); - - return 0; -} - -static int python_export_symbol(struct db_export *dbe, struct symbol *sym, - struct dso *dso) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - u64 *sym_db_id = symbol__priv(sym); - PyObject *t; - - t = tuple_new(6); - - tuple_set_d64(t, 0, *sym_db_id); - tuple_set_d64(t, 1, dso__db_id(dso)); - tuple_set_d64(t, 2, sym->start); - tuple_set_d64(t, 3, sym->end); - tuple_set_s32(t, 4, symbol__binding(sym)); - tuple_set_string(t, 5, sym->name); - - call_object(tables->symbol_handler, t, "symbol_table"); - - Py_DECREF(t); - - return 0; -} - -static int python_export_branch_type(struct db_export *dbe, u32 branch_type, - const char *name) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - - t = tuple_new(2); - - tuple_set_s32(t, 0, branch_type); - tuple_set_string(t, 1, name); - - call_object(tables->branch_type_handler, t, "branch_type_table"); - - Py_DECREF(t); - - return 0; -} - -static void python_export_sample_table(struct db_export *dbe, - struct export_sample *es) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - - t = tuple_new(28); - - tuple_set_d64(t, 0, es->db_id); - tuple_set_d64(t, 1, es->sample->evsel->db_id); - tuple_set_d64(t, 2, maps__machine(thread__maps(es->al->thread))->db_id); - tuple_set_d64(t, 3, thread__db_id(es->al->thread)); - tuple_set_d64(t, 4, es->comm_db_id); - tuple_set_d64(t, 5, es->dso_db_id); - tuple_set_d64(t, 6, es->sym_db_id); - tuple_set_d64(t, 7, es->offset); - tuple_set_d64(t, 8, es->sample->ip); - tuple_set_d64(t, 9, es->sample->time); - tuple_set_s32(t, 10, es->sample->cpu); - tuple_set_d64(t, 11, es->addr_dso_db_id); - tuple_set_d64(t, 12, es->addr_sym_db_id); - tuple_set_d64(t, 13, es->addr_offset); - tuple_set_d64(t, 14, es->sample->addr); - tuple_set_d64(t, 15, es->sample->period); - tuple_set_d64(t, 16, es->sample->weight); - tuple_set_d64(t, 17, es->sample->transaction); - tuple_set_d64(t, 18, es->sample->data_src); - tuple_set_s32(t, 19, es->sample->flags & PERF_BRANCH_MASK); - tuple_set_s32(t, 20, !!(es->sample->flags & PERF_IP_FLAG_IN_TX)); - tuple_set_d64(t, 21, es->call_path_id); - tuple_set_d64(t, 22, es->sample->insn_cnt); - tuple_set_d64(t, 23, es->sample->cyc_cnt); - tuple_set_s32(t, 24, es->sample->flags); - tuple_set_d64(t, 25, es->sample->id); - tuple_set_d64(t, 26, es->sample->stream_id); - tuple_set_u32(t, 27, es->sample->ins_lat); - - call_object(tables->sample_handler, t, "sample_table"); - - Py_DECREF(t); -} - -static void python_export_synth(struct db_export *dbe, struct export_sample *es) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - - t = tuple_new(3); - - tuple_set_d64(t, 0, es->db_id); - tuple_set_d64(t, 1, es->sample->evsel->core.attr.config); - tuple_set_bytes(t, 2, es->sample->raw_data, es->sample->raw_size); - - call_object(tables->synth_handler, t, "synth_data"); - - Py_DECREF(t); -} - -static int python_export_sample(struct db_export *dbe, - struct export_sample *es) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - - python_export_sample_table(dbe, es); - - if (es->sample->evsel->core.attr.type == PERF_TYPE_SYNTH && tables->synth_handler) - python_export_synth(dbe, es); - - return 0; -} - -static int python_export_call_path(struct db_export *dbe, struct call_path *cp) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - u64 parent_db_id, sym_db_id; - - parent_db_id = cp->parent ? cp->parent->db_id : 0; - sym_db_id = cp->sym ? *(u64 *)symbol__priv(cp->sym) : 0; - - t = tuple_new(4); - - tuple_set_d64(t, 0, cp->db_id); - tuple_set_d64(t, 1, parent_db_id); - tuple_set_d64(t, 2, sym_db_id); - tuple_set_d64(t, 3, cp->ip); - - call_object(tables->call_path_handler, t, "call_path_table"); - - Py_DECREF(t); - - return 0; -} - -static int python_export_call_return(struct db_export *dbe, - struct call_return *cr) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - u64 comm_db_id = cr->comm ? cr->comm->db_id : 0; - PyObject *t; - - t = tuple_new(14); - - tuple_set_d64(t, 0, cr->db_id); - tuple_set_d64(t, 1, thread__db_id(cr->thread)); - tuple_set_d64(t, 2, comm_db_id); - tuple_set_d64(t, 3, cr->cp->db_id); - tuple_set_d64(t, 4, cr->call_time); - tuple_set_d64(t, 5, cr->return_time); - tuple_set_d64(t, 6, cr->branch_count); - tuple_set_d64(t, 7, cr->call_ref); - tuple_set_d64(t, 8, cr->return_ref); - tuple_set_d64(t, 9, cr->cp->parent->db_id); - tuple_set_s32(t, 10, cr->flags); - tuple_set_d64(t, 11, cr->parent_db_id); - tuple_set_d64(t, 12, cr->insn_count); - tuple_set_d64(t, 13, cr->cyc_count); - - call_object(tables->call_return_handler, t, "call_return_table"); - - Py_DECREF(t); - - return 0; -} - -static int python_export_context_switch(struct db_export *dbe, u64 db_id, - struct machine *machine, - struct perf_sample *sample, - u64 th_out_id, u64 comm_out_id, - u64 th_in_id, u64 comm_in_id, int flags) -{ - struct tables *tables = container_of(dbe, struct tables, dbe); - PyObject *t; - - t = tuple_new(9); - - tuple_set_d64(t, 0, db_id); - tuple_set_d64(t, 1, machine->db_id); - tuple_set_d64(t, 2, sample->time); - tuple_set_s32(t, 3, sample->cpu); - tuple_set_d64(t, 4, th_out_id); - tuple_set_d64(t, 5, comm_out_id); - tuple_set_d64(t, 6, th_in_id); - tuple_set_d64(t, 7, comm_in_id); - tuple_set_s32(t, 8, flags); - - call_object(tables->context_switch_handler, t, "context_switch"); - - Py_DECREF(t); - - return 0; -} - -static int python_process_call_return(struct call_return *cr, u64 *parent_db_id, - void *data) -{ - struct db_export *dbe = data; - - return db_export__call_return(dbe, cr, parent_db_id); -} - -static void python_process_general_event(struct perf_sample *sample, - struct addr_location *al, - struct addr_location *addr_al) -{ - PyObject *handler, *t, *dict, *callchain; - static char handler_name[64]; - unsigned n = 0; - - snprintf(handler_name, sizeof(handler_name), "%s", "process_event"); - - handler = get_handler(handler_name); - if (!handler) - return; - - /* - * Use the MAX_FIELDS to make the function expandable, though - * currently there is only one item for the tuple. - */ - t = PyTuple_New(MAX_FIELDS); - if (!t) - Py_FatalError("couldn't create Python tuple"); - - /* ip unwinding */ - callchain = python_process_callchain(sample, al); - dict = get_perf_sample_dict(sample, al, addr_al, callchain); - - PyTuple_SetItem(t, n++, dict); - if (_PyTuple_Resize(&t, n) == -1) - Py_FatalError("error resizing Python tuple"); - - call_object(handler, t, handler_name); - - Py_DECREF(t); -} - -static void python_process_event(union perf_event *event, - struct perf_sample *sample, - struct addr_location *al, - struct addr_location *addr_al) -{ - struct tables *tables = &tables_global; - - scripting_context__update(scripting_context, event, sample, al, addr_al); - - switch (sample->evsel->core.attr.type) { - case PERF_TYPE_TRACEPOINT: - python_process_tracepoint(sample, al, addr_al); - break; - /* Reserve for future process_hw/sw/raw APIs */ - default: - if (tables->db_export_mode) - db_export__sample(&tables->dbe, event, sample, al, addr_al); - else - python_process_general_event(sample, al, addr_al); - } -} - -static void python_process_throttle(union perf_event *event, - struct perf_sample *sample, - struct machine *machine) -{ - const char *handler_name; - PyObject *handler, *t; - - if (event->header.type == PERF_RECORD_THROTTLE) - handler_name = "throttle"; - else - handler_name = "unthrottle"; - handler = get_handler(handler_name); - if (!handler) - return; - - t = tuple_new(6); - if (!t) - return; - - tuple_set_u64(t, 0, event->throttle.time); - tuple_set_u64(t, 1, event->throttle.id); - tuple_set_u64(t, 2, event->throttle.stream_id); - tuple_set_s32(t, 3, sample->cpu); - tuple_set_s32(t, 4, sample->pid); - tuple_set_s32(t, 5, sample->tid); - - call_object(handler, t, handler_name); - - Py_DECREF(t); -} - -static void python_do_process_switch(union perf_event *event, - struct perf_sample *sample, - struct machine *machine) -{ - const char *handler_name = "context_switch"; - bool out = event->header.misc & PERF_RECORD_MISC_SWITCH_OUT; - bool out_preempt = out && (event->header.misc & PERF_RECORD_MISC_SWITCH_OUT_PREEMPT); - pid_t np_pid = -1, np_tid = -1; - PyObject *handler, *t; - - handler = get_handler(handler_name); - if (!handler) - return; - - if (event->header.type == PERF_RECORD_SWITCH_CPU_WIDE) { - np_pid = event->context_switch.next_prev_pid; - np_tid = event->context_switch.next_prev_tid; - } - - t = tuple_new(11); - if (!t) - return; - - tuple_set_u64(t, 0, sample->time); - tuple_set_s32(t, 1, sample->cpu); - tuple_set_s32(t, 2, sample->pid); - tuple_set_s32(t, 3, sample->tid); - tuple_set_s32(t, 4, np_pid); - tuple_set_s32(t, 5, np_tid); - tuple_set_s32(t, 6, machine->pid); - tuple_set_bool(t, 7, out); - tuple_set_bool(t, 8, out_preempt); - tuple_set_s32(t, 9, sample->machine_pid); - tuple_set_s32(t, 10, sample->vcpu); - - call_object(handler, t, handler_name); - - Py_DECREF(t); -} - -static void python_process_switch(union perf_event *event, - struct perf_sample *sample, - struct machine *machine) -{ - struct tables *tables = &tables_global; - - if (tables->db_export_mode) - db_export__switch(&tables->dbe, event, sample, machine); - else - python_do_process_switch(event, sample, machine); -} - -static void python_process_auxtrace_error(struct perf_session *session __maybe_unused, - union perf_event *event) -{ - struct perf_record_auxtrace_error *e = &event->auxtrace_error; - u8 cpumode = e->header.misc & PERF_RECORD_MISC_CPUMODE_MASK; - const char *handler_name = "auxtrace_error"; - unsigned long long tm = e->time; - const char *msg = e->msg; - s32 machine_pid = 0, vcpu = 0; - char msg_buf[MAX_AUXTRACE_ERROR_MSG + 1]; - int msg_max; - PyObject *handler, *t; - - handler = get_handler(handler_name); - if (!handler) - return; - - if (!e->fmt) { - tm = 0; - msg = (const char *)&e->time; - } - - /* Bound msg to the bytes within the event, ensure NUL-termination */ - msg_max = (int)((void *)event + event->header.size - (void *)msg); - if (msg_max <= 0) { - msg_buf[0] = '\0'; - } else { - if (msg_max > (int)sizeof(msg_buf) - 1) - msg_max = sizeof(msg_buf) - 1; - memcpy(msg_buf, msg, msg_max); - msg_buf[msg_max] = '\0'; - } - - /* Only access fmt >= 2 fields if the event is large enough */ - if (e->fmt >= 2 && - event->header.size >= offsetof(typeof(event->auxtrace_error), vcpu) + - sizeof(event->auxtrace_error.vcpu)) { - machine_pid = e->machine_pid; - vcpu = e->vcpu; - } - - t = tuple_new(11); - - tuple_set_u32(t, 0, e->type); - tuple_set_u32(t, 1, e->code); - tuple_set_s32(t, 2, e->cpu); - tuple_set_s32(t, 3, e->pid); - tuple_set_s32(t, 4, e->tid); - tuple_set_u64(t, 5, e->ip); - tuple_set_u64(t, 6, tm); - tuple_set_string(t, 7, msg_buf); - tuple_set_u32(t, 8, cpumode); - tuple_set_s32(t, 9, machine_pid); - tuple_set_s32(t, 10, vcpu); - - call_object(handler, t, handler_name); - - Py_DECREF(t); -} - -static void get_handler_name(char *str, size_t size, - struct evsel *evsel) -{ - char *p = str; - - scnprintf(str, size, "stat__%s", evsel__name(evsel)); - - while ((p = strchr(p, ':'))) { - *p = '_'; - p++; - } -} - -static void -process_stat(struct evsel *counter, struct perf_cpu cpu, int thread, u64 tstamp, - struct perf_counts_values *count) -{ - PyObject *handler, *t; - static char handler_name[256]; - int n = 0; - - t = PyTuple_New(MAX_FIELDS); - if (!t) - Py_FatalError("couldn't create Python tuple"); - - get_handler_name(handler_name, sizeof(handler_name), - counter); - - handler = get_handler(handler_name); - if (!handler) { - pr_debug("can't find python handler %s\n", handler_name); - return; - } - - PyTuple_SetItem(t, n++, _PyLong_FromLong(cpu.cpu)); - PyTuple_SetItem(t, n++, _PyLong_FromLong(thread)); - - tuple_set_u64(t, n++, tstamp); - tuple_set_u64(t, n++, count->val); - tuple_set_u64(t, n++, count->ena); - tuple_set_u64(t, n++, count->run); - - if (_PyTuple_Resize(&t, n) == -1) - Py_FatalError("error resizing Python tuple"); - - call_object(handler, t, handler_name); - - Py_DECREF(t); -} - -static void python_process_stat(struct perf_stat_config *config, - struct evsel *counter, u64 tstamp) -{ - struct perf_thread_map *threads = counter->core.threads; - struct perf_cpu_map *cpus = counter->core.cpus; - - for (int thread = 0; thread < perf_thread_map__nr(threads); thread++) { - unsigned int idx; - struct perf_cpu cpu; - - perf_cpu_map__for_each_cpu(cpu, idx, cpus) { - process_stat(counter, cpu, - perf_thread_map__pid(threads, thread), tstamp, - perf_counts(counter->counts, idx, thread)); - } - } -} - -static void python_process_stat_interval(u64 tstamp) -{ - PyObject *handler, *t; - static const char handler_name[] = "stat__interval"; - int n = 0; - - t = PyTuple_New(MAX_FIELDS); - if (!t) - Py_FatalError("couldn't create Python tuple"); - - handler = get_handler(handler_name); - if (!handler) { - pr_debug("can't find python handler %s\n", handler_name); - return; - } - - tuple_set_u64(t, n++, tstamp); - - if (_PyTuple_Resize(&t, n) == -1) - Py_FatalError("error resizing Python tuple"); - - call_object(handler, t, handler_name); - - Py_DECREF(t); -} - -static int perf_script_context_init(void) -{ - PyObject *perf_script_context; - PyObject *perf_trace_context; - PyObject *dict; - int ret; - - perf_trace_context = PyImport_AddModule("perf_trace_context"); - if (!perf_trace_context) - return -1; - dict = PyModule_GetDict(perf_trace_context); - if (!dict) - return -1; - - perf_script_context = _PyCapsule_New(scripting_context, NULL, NULL); - if (!perf_script_context) - return -1; - - ret = PyDict_SetItemString(dict, "perf_script_context", perf_script_context); - if (!ret) - ret = PyDict_SetItemString(main_dict, "perf_script_context", perf_script_context); - Py_DECREF(perf_script_context); - return ret; -} - -static int run_start_sub(void) -{ - main_module = PyImport_AddModule("__main__"); - if (main_module == NULL) - return -1; - Py_INCREF(main_module); - - main_dict = PyModule_GetDict(main_module); - if (main_dict == NULL) - goto error; - Py_INCREF(main_dict); - - if (perf_script_context_init()) - goto error; - - try_call_object("trace_begin", NULL); - - return 0; - -error: - Py_XDECREF(main_dict); - Py_XDECREF(main_module); - return -1; -} - -#define SET_TABLE_HANDLER_(name, handler_name, table_name) do { \ - tables->handler_name = get_handler(#table_name); \ - if (tables->handler_name) \ - tables->dbe.export_ ## name = python_export_ ## name; \ -} while (0) - -#define SET_TABLE_HANDLER(name) \ - SET_TABLE_HANDLER_(name, name ## _handler, name ## _table) - -static void set_table_handlers(struct tables *tables) -{ - const char *perf_db_export_mode = "perf_db_export_mode"; - const char *perf_db_export_calls = "perf_db_export_calls"; - const char *perf_db_export_callchains = "perf_db_export_callchains"; - PyObject *db_export_mode, *db_export_calls, *db_export_callchains; - bool export_calls = false; - bool export_callchains = false; - int ret; - - memset(tables, 0, sizeof(struct tables)); - if (db_export__init(&tables->dbe)) - Py_FatalError("failed to initialize export"); - - db_export_mode = PyDict_GetItemString(main_dict, perf_db_export_mode); - if (!db_export_mode) - return; - - ret = PyObject_IsTrue(db_export_mode); - if (ret == -1) - handler_call_die(perf_db_export_mode); - if (!ret) - return; - - /* handle export calls */ - tables->dbe.crp = NULL; - db_export_calls = PyDict_GetItemString(main_dict, perf_db_export_calls); - if (db_export_calls) { - ret = PyObject_IsTrue(db_export_calls); - if (ret == -1) - handler_call_die(perf_db_export_calls); - export_calls = !!ret; - } - - if (export_calls) { - tables->dbe.crp = - call_return_processor__new(python_process_call_return, - &tables->dbe); - if (!tables->dbe.crp) - Py_FatalError("failed to create calls processor"); - } - - /* handle export callchains */ - tables->dbe.cpr = NULL; - db_export_callchains = PyDict_GetItemString(main_dict, - perf_db_export_callchains); - if (db_export_callchains) { - ret = PyObject_IsTrue(db_export_callchains); - if (ret == -1) - handler_call_die(perf_db_export_callchains); - export_callchains = !!ret; - } - - if (export_callchains) { - /* - * Attempt to use the call path root from the call return - * processor, if the call return processor is in use. Otherwise, - * we allocate a new call path root. This prevents exporting - * duplicate call path ids when both are in use simultaneously. - */ - if (tables->dbe.crp) - tables->dbe.cpr = tables->dbe.crp->cpr; - else - tables->dbe.cpr = call_path_root__new(); - - if (!tables->dbe.cpr) - Py_FatalError("failed to create call path root"); - } - - tables->db_export_mode = true; - /* - * Reserve per symbol space for symbol->db_id via symbol__priv() - */ - symbol_conf.priv_size = sizeof(u64); - - SET_TABLE_HANDLER(evsel); - SET_TABLE_HANDLER(machine); - SET_TABLE_HANDLER(thread); - SET_TABLE_HANDLER(comm); - SET_TABLE_HANDLER(comm_thread); - SET_TABLE_HANDLER(dso); - SET_TABLE_HANDLER(symbol); - SET_TABLE_HANDLER(branch_type); - SET_TABLE_HANDLER(sample); - SET_TABLE_HANDLER(call_path); - SET_TABLE_HANDLER(call_return); - SET_TABLE_HANDLER(context_switch); - - /* - * Synthesized events are samples but with architecture-specific data - * stored in sample->raw_data. They are exported via - * python_export_sample() and consequently do not need a separate export - * callback. - */ - tables->synth_handler = get_handler("synth_data"); -} - -static void _free_command_line(wchar_t **command_line, int num) -{ - int i; - for (i = 0; i < num; i++) - PyMem_RawFree(command_line[i]); - free(command_line); -} - - -/* - * Start trace script - */ -static int python_start_script(const char *script, int argc, const char **argv, - struct perf_session *session) -{ - struct tables *tables = &tables_global; - wchar_t **command_line; - char buf[PATH_MAX]; - int i, err = 0; - FILE *fp; - - scripting_context->session = session; - command_line = malloc((argc + 1) * sizeof(wchar_t *)); - if (!command_line) - return -1; - - command_line[0] = Py_DecodeLocale(script, NULL); - for (i = 1; i < argc + 1; i++) - command_line[i] = Py_DecodeLocale(argv[i - 1], NULL); - PyImport_AppendInittab("perf_trace_context", PyInit_perf_trace_context); - Py_Initialize(); - - PySys_SetArgv(argc + 1, command_line); - - fp = fopen(script, "r"); - if (!fp) { - sprintf(buf, "Can't open python script \"%s\"", script); - perror(buf); - err = -1; - goto error; - } - - err = PyRun_SimpleFile(fp, script); - if (err) { - fprintf(stderr, "Error running python script %s\n", script); - goto error; - } - - err = run_start_sub(); - if (err) { - fprintf(stderr, "Error starting python script %s\n", script); - goto error; - } - - set_table_handlers(tables); - - if (tables->db_export_mode) { - err = db_export__branch_types(&tables->dbe); - if (err) - goto error; - } - - _free_command_line(command_line, argc + 1); - - return err; -error: - Py_Finalize(); - _free_command_line(command_line, argc + 1); - - return err; -} - -static int python_flush_script(void) -{ - return 0; -} - -/* - * Stop trace script - */ -static int python_stop_script(void) -{ - struct tables *tables = &tables_global; - - try_call_object("trace_end", NULL); - - db_export__exit(&tables->dbe); - - Py_XDECREF(main_dict); - Py_XDECREF(main_module); - Py_Finalize(); - - return 0; -} - -#ifdef HAVE_LIBTRACEEVENT -static int python_generate_script(struct tep_handle *pevent, const char *outfile) -{ - int i, not_first, count, nr_events; - struct tep_event **all_events; - struct tep_event *event = NULL; - struct tep_format_field *f; - char fname[PATH_MAX]; - FILE *ofp; - - sprintf(fname, "%s.py", outfile); - ofp = fopen(fname, "w"); - if (ofp == NULL) { - fprintf(stderr, "couldn't open %s\n", fname); - return -1; - } - fprintf(ofp, "# perf script event handlers, " - "generated by perf script -g python\n"); - - fprintf(ofp, "# Licensed under the terms of the GNU GPL" - " License version 2\n\n"); - - fprintf(ofp, "# The common_* event handler fields are the most useful " - "fields common to\n"); - - fprintf(ofp, "# all events. They don't necessarily correspond to " - "the 'common_*' fields\n"); - - fprintf(ofp, "# in the format files. Those fields not available as " - "handler params can\n"); - - fprintf(ofp, "# be retrieved using Python functions of the form " - "common_*(context).\n"); - - fprintf(ofp, "# See the perf-script-python Documentation for the list " - "of available functions.\n\n"); - - fprintf(ofp, "from __future__ import print_function\n\n"); - fprintf(ofp, "import os\n"); - fprintf(ofp, "import sys\n\n"); - - fprintf(ofp, "sys.path.append(os.environ['PERF_EXEC_PATH'] + \\\n"); - fprintf(ofp, "\t'/scripts/python/Perf-Trace-Util/lib/Perf/Trace')\n"); - fprintf(ofp, "\nfrom perf_trace_context import *\n"); - fprintf(ofp, "from Core import *\n\n\n"); - - fprintf(ofp, "def trace_begin():\n"); - fprintf(ofp, "\tprint(\"in trace_begin\")\n\n"); - - fprintf(ofp, "def trace_end():\n"); - fprintf(ofp, "\tprint(\"in trace_end\")\n\n"); - - nr_events = tep_get_events_count(pevent); - all_events = tep_list_events(pevent, TEP_EVENT_SORT_ID); - - for (i = 0; all_events && i < nr_events; i++) { - event = all_events[i]; - fprintf(ofp, "def %s__%s(", event->system, event->name); - fprintf(ofp, "event_name, "); - fprintf(ofp, "context, "); - fprintf(ofp, "common_cpu,\n"); - fprintf(ofp, "\tcommon_secs, "); - fprintf(ofp, "common_nsecs, "); - fprintf(ofp, "common_pid, "); - fprintf(ofp, "common_comm,\n\t"); - fprintf(ofp, "common_callchain, "); - - not_first = 0; - count = 0; - - for (f = event->format.fields; f; f = f->next) { - if (not_first++) - fprintf(ofp, ", "); - if (++count % 5 == 0) - fprintf(ofp, "\n\t"); - - fprintf(ofp, "%s", f->name); - } - if (not_first++) - fprintf(ofp, ", "); - if (++count % 5 == 0) - fprintf(ofp, "\n\t\t"); - fprintf(ofp, "perf_sample_dict"); - - fprintf(ofp, "):\n"); - - fprintf(ofp, "\t\tprint_header(event_name, common_cpu, " - "common_secs, common_nsecs,\n\t\t\t" - "common_pid, common_comm)\n\n"); - - fprintf(ofp, "\t\tprint(\""); - - not_first = 0; - count = 0; - - for (f = event->format.fields; f; f = f->next) { - if (not_first++) - fprintf(ofp, ", "); - if (count && count % 3 == 0) { - fprintf(ofp, "\" \\\n\t\t\""); - } - count++; - - fprintf(ofp, "%s=", f->name); - if (f->flags & TEP_FIELD_IS_STRING || - f->flags & TEP_FIELD_IS_FLAG || - f->flags & TEP_FIELD_IS_ARRAY || - f->flags & TEP_FIELD_IS_SYMBOLIC) - fprintf(ofp, "%%s"); - else if (f->flags & TEP_FIELD_IS_SIGNED) - fprintf(ofp, "%%d"); - else - fprintf(ofp, "%%u"); - } - - fprintf(ofp, "\" %% \\\n\t\t("); - - not_first = 0; - count = 0; - - for (f = event->format.fields; f; f = f->next) { - if (not_first++) - fprintf(ofp, ", "); - - if (++count % 5 == 0) - fprintf(ofp, "\n\t\t"); - - if (f->flags & TEP_FIELD_IS_FLAG) { - if ((count - 1) % 5 != 0) { - fprintf(ofp, "\n\t\t"); - count = 4; - } - fprintf(ofp, "flag_str(\""); - fprintf(ofp, "%s__%s\", ", event->system, - event->name); - fprintf(ofp, "\"%s\", %s)", f->name, - f->name); - } else if (f->flags & TEP_FIELD_IS_SYMBOLIC) { - if ((count - 1) % 5 != 0) { - fprintf(ofp, "\n\t\t"); - count = 4; - } - fprintf(ofp, "symbol_str(\""); - fprintf(ofp, "%s__%s\", ", event->system, - event->name); - fprintf(ofp, "\"%s\", %s)", f->name, - f->name); - } else - fprintf(ofp, "%s", f->name); - } - - fprintf(ofp, "))\n\n"); - - fprintf(ofp, "\t\tprint('Sample: {'+" - "get_dict_as_string(perf_sample_dict['sample'], ', ')+'}')\n\n"); - - fprintf(ofp, "\t\tfor node in common_callchain:"); - fprintf(ofp, "\n\t\t\tif 'sym' in node:"); - fprintf(ofp, "\n\t\t\t\tprint(\"\t[%%x] %%s%%s%%s%%s\" %% ("); - fprintf(ofp, "\n\t\t\t\t\tnode['ip'], node['sym']['name'],"); - fprintf(ofp, "\n\t\t\t\t\t\"+0x{:x}\".format(node['sym_off']) if 'sym_off' in node else \"\","); - fprintf(ofp, "\n\t\t\t\t\t\" ({})\".format(node['dso']) if 'dso' in node else \"\","); - fprintf(ofp, "\n\t\t\t\t\t\" \" + node['sym_srcline'] if 'sym_srcline' in node else \"\"))"); - fprintf(ofp, "\n\t\t\telse:"); - fprintf(ofp, "\n\t\t\t\tprint(\"\t[%%x]\" %% (node['ip']))\n\n"); - fprintf(ofp, "\t\tprint()\n\n"); - - } - - fprintf(ofp, "def trace_unhandled(event_name, context, " - "event_fields_dict, perf_sample_dict):\n"); - - fprintf(ofp, "\t\tprint(get_dict_as_string(event_fields_dict))\n"); - fprintf(ofp, "\t\tprint('Sample: {'+" - "get_dict_as_string(perf_sample_dict['sample'], ', ')+'}')\n\n"); - - fprintf(ofp, "def print_header(" - "event_name, cpu, secs, nsecs, pid, comm):\n" - "\tprint(\"%%-20s %%5u %%05u.%%09u %%8u %%-20s \" %% \\\n\t" - "(event_name, cpu, secs, nsecs, pid, comm), end=\"\")\n\n"); - - fprintf(ofp, "def get_dict_as_string(a_dict, delimiter=' '):\n" - "\treturn delimiter.join" - "(['%%s=%%s'%%(k,str(v))for k,v in sorted(a_dict.items())])\n"); - - fclose(ofp); - - fprintf(stderr, "generated Python script: %s\n", fname); - - return 0; -} -#else -static int python_generate_script(struct tep_handle *pevent __maybe_unused, - const char *outfile __maybe_unused) -{ - fprintf(stderr, "Generating Python perf-script is not supported." - " Install libtraceevent and rebuild perf to enable it.\n" - "For example:\n # apt install libtraceevent-dev (ubuntu)" - "\n # yum install libtraceevent-devel (Fedora)" - "\n etc.\n"); - return -1; -} -#endif - -struct scripting_ops python_scripting_ops = { - .name = "Python", - .dirname = "python", - .start_script = python_start_script, - .flush_script = python_flush_script, - .stop_script = python_stop_script, - .process_event = python_process_event, - .process_switch = python_process_switch, - .process_auxtrace_error = python_process_auxtrace_error, - .process_stat = python_process_stat, - .process_stat_interval = python_process_stat_interval, - .process_throttle = python_process_throttle, - .generate_script = python_generate_script, -}; diff --git a/tools/perf/util/trace-event-scripting.c b/tools/perf/util/trace-event-scripting.c index dc584ac316a3..c78b317978dd 100644 --- a/tools/perf/util/trace-event-scripting.c +++ b/tools/perf/util/trace-event-scripting.c @@ -189,19 +189,10 @@ static void register_python_scripting(struct scripting_ops *scripting_ops) } } -#ifndef HAVE_LIBPYTHON_SUPPORT void setup_python_scripting(void) { register_python_scripting(&python_scripting_unsupported_ops); } -#else -extern struct scripting_ops python_scripting_ops; - -void setup_python_scripting(void) -{ - register_python_scripting(&python_scripting_ops); -} -#endif #ifdef HAVE_LIBTRACEEVENT static void print_perl_unsupported_msg(void) From 1b7e0280e247b93db9e7b4473adb7fb633e06154 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:14 -0700 Subject: [PATCH 222/425] perf Makefile: Update Python script installation path Replace the libpython feature test with a python-module feature test checking for Python C extension build capability, and update feature test references accordingly. Remove references to the legacy scripts/python directory and install standalone Python scripts (python/*.py), type stubs (python/perf.pyi), and the compiled perf extension module (python/perf*.so, when built) directly under the python directory in libexec. Update the TUI script browser (ui/browsers/scripts.c) to discover standalone scripts from the updated installation path. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/build/Makefile.feature | 4 +- tools/build/feature/Makefile | 4 +- tools/build/feature/test-all.c | 6 +- tools/build/feature/test-libpython.c | 10 -- tools/build/feature/test-python-module.c | 13 ++ tools/perf/Documentation/perf-check.txt | 1 + tools/perf/Makefile.config | 17 +-- tools/perf/Makefile.perf | 10 +- tools/perf/builtin-check.c | 2 +- tools/perf/scripts/install-build-deps.sh | 4 +- tools/perf/tests/make | 8 +- tools/perf/ui/browsers/scripts.c | 186 +++++++++++++++-------- 12 files changed, 157 insertions(+), 108 deletions(-) delete mode 100644 tools/build/feature/test-libpython.c create mode 100644 tools/build/feature/test-python-module.c diff --git a/tools/build/Makefile.feature b/tools/build/Makefile.feature index 331f5cdfc34b..3ca7a4f5c7fd 100644 --- a/tools/build/Makefile.feature +++ b/tools/build/Makefile.feature @@ -79,7 +79,7 @@ FEATURE_TESTS_BASIC := \ libelf-zstd \ libnuma \ numa_num_possible_cpus \ - libpython \ + python-module \ libslang \ libtraceevent \ libcpupower \ @@ -145,7 +145,7 @@ FEATURE_DISPLAY ?= \ libelf \ libnuma \ numa_num_possible_cpus \ - libpython \ + python-module \ libcapstone \ llvm-perf \ zlib \ diff --git a/tools/build/feature/Makefile b/tools/build/feature/Makefile index cdf89f132074..269af8e8f5cf 100644 --- a/tools/build/feature/Makefile +++ b/tools/build/feature/Makefile @@ -32,7 +32,7 @@ FILES= \ test-libnuma.bin \ test-numa_num_possible_cpus.bin \ test-libperl.bin \ - test-libpython.bin \ + test-python-module.bin \ test-libslang.bin \ test-libtraceevent.bin \ test-libcpupower.bin \ @@ -261,7 +261,7 @@ endif $(OUTPUT)test-libperl.bin: $(BUILD) $(FLAGS_PERL_EMBED) -$(OUTPUT)test-libpython.bin: +$(OUTPUT)test-python-module.bin: $(BUILD) $(FLAGS_PYTHON_EMBED) $(OUTPUT)test-libbfd.bin: diff --git a/tools/build/feature/test-all.c b/tools/build/feature/test-all.c index 544563d62950..0ee16eccd9e0 100644 --- a/tools/build/feature/test-all.c +++ b/tools/build/feature/test-all.c @@ -10,8 +10,8 @@ * Quirk: Python headers cannot be in arbitrary places, so keep this testcase at * the top: */ -#define main main_test_libpython -# include "test-libpython.c" +#define main main_test_python_module +# include "test-python-module.c" #undef main #define main main_test_hello @@ -148,7 +148,7 @@ int main(int argc, char *argv[]) { - main_test_libpython(); + main_test_python_module(); main_test_hello(); main_test_libelf(); main_test_gettid(); diff --git a/tools/build/feature/test-libpython.c b/tools/build/feature/test-libpython.c deleted file mode 100644 index 371c9113e49d..000000000000 --- a/tools/build/feature/test-libpython.c +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -#include - -int main(void) -{ - Py_Initialize(); - - return 0; -} -#undef _GNU_SOURCE diff --git a/tools/build/feature/test-python-module.c b/tools/build/feature/test-python-module.c new file mode 100644 index 000000000000..50e9e5062feb --- /dev/null +++ b/tools/build/feature/test-python-module.c @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0 +#include + +int main(void) +{ + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + .m_name = "test", + }; + PyObject *module = PyModule_Create(&moduledef); + + return module ? 0 : -1; +} diff --git a/tools/perf/Documentation/perf-check.txt b/tools/perf/Documentation/perf-check.txt index 3d169e5bb372..a80913794eb6 100644 --- a/tools/perf/Documentation/perf-check.txt +++ b/tools/perf/Documentation/perf-check.txt @@ -65,6 +65,7 @@ feature:: libunwind / HAVE_LIBUNWIND_SUPPORT lzma / HAVE_LZMA_SUPPORT numa_num_possible_cpus / HAVE_LIBNUMA_SUPPORT + python-module / HAVE_PYTHON_MODULE_SUPPORT zlib / HAVE_ZLIB_SUPPORT zstd / HAVE_ZSTD_SUPPORT diff --git a/tools/perf/Makefile.config b/tools/perf/Makefile.config index e04125ad4f28..6cb6b02fb0d2 100644 --- a/tools/perf/Makefile.config +++ b/tools/perf/Makefile.config @@ -312,7 +312,7 @@ PYTHON_CONFIG_SQ := $(call shell-sq,$(PYTHON_CONFIG)) # Python 3.8 changed the output of `python-config --ldflags` to not include the # '-lpythonX.Y' flag unless '--embed' is also passed. The feature check for -# libpython fails if that flag is not included in LDFLAGS +# python-module fails if that flag is not included in LDFLAGS ifeq ($(shell $(PYTHON_CONFIG_SQ) --ldflags --embed 2>&1 1>/dev/null; echo $$?), 0) PYTHON_CONFIG_LDFLAGS := --ldflags --embed else @@ -335,8 +335,8 @@ ifdef PYTHON_CONFIG endif endif -FEATURE_CHECK_CFLAGS-libpython := $(PYTHON_EMBED_CCOPTS) -FEATURE_CHECK_LDFLAGS-libpython := $(PYTHON_EMBED_LDOPTS) +FEATURE_CHECK_CFLAGS-python-module := $(PYTHON_EMBED_CCOPTS) +FEATURE_CHECK_LDFLAGS-python-module := $(PYTHON_EMBED_LDOPTS) FEATURE_CHECK_LDFLAGS-libaio = -lrt @@ -808,9 +808,8 @@ endif disable-python = $(eval $(disable-python_code)) define disable-python_code - CFLAGS += -DNO_LIBPYTHON $(warning $1) - NO_LIBPYTHON := 1 + NO_PYTHON_MODULE := 1 endef # Minimum Python version for the interpreters perf runs while building, @@ -824,7 +823,7 @@ ifdef PYTHON endif PYTHON_EXTENSION_SUFFIX := '.so' -ifdef NO_LIBPYTHON +ifdef NO_PYTHON_MODULE $(call disable-python,Python support disabled by user) else @@ -837,7 +836,7 @@ else $(call disable-python,No 'python-config' tool was found: disables Python support - please install python-devel/python-dev) else - ifneq ($(feature-libpython), 1) + ifneq ($(feature-python-module), 1) $(call disable-python,No 'Python.h' was found: disables Python support - please install python-devel/python-dev) else # The binding is built even when jevents generation is turned off @@ -846,9 +845,9 @@ else # older interpreters with "TypeError: 'type' object is not # subscriptable" instead of a Makefile error. ifneq ($(PYTHON_39_GOOD), 1) - $(error ERROR: Python interpreter needed for the python binding too old (older than 3.9). Install a newer python or build with NO_LIBPYTHON=1.) + $(error ERROR: Python interpreter needed for the python binding too old (older than 3.9). Install a newer python or build with NO_PYTHON_MODULE=1.) endif - CFLAGS += -DHAVE_LIBPYTHON_SUPPORT + CFLAGS += -DHAVE_PYTHON_MODULE_SUPPORT PYTHON_SETUPTOOLS_INSTALLED := $(shell $(PYTHON) -c 'import setuptools;' 2> /dev/null && echo "yes" || echo "no") ifeq ($(PYTHON_SETUPTOOLS_INSTALLED), yes) PYTHON_EXTENSION_SUFFIX := $(shell $(PYTHON) -c 'from importlib import machinery; print(machinery.EXTENSION_SUFFIXES[0])') diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf index 89edf2720fc1..106f8e21a9d4 100644 --- a/tools/perf/Makefile.perf +++ b/tools/perf/Makefile.perf @@ -19,7 +19,7 @@ include ../scripts/utilities.mak # # Define LIBPERL to enable perl script extension. # -# Define NO_LIBPYTHON to disable python script extension. +# Define NO_PYTHON_MODULE to disable python script extension. # # Define PYTHON to point to the python binary if the default # `python' is not correct; for example: PYTHON=python2 @@ -900,14 +900,8 @@ ifdef LIBPERL $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/perl/bin'; \ $(INSTALL) scripts/perl/bin/* -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/perl/bin' endif -ifndef NO_LIBPYTHON +ifndef NO_PYTHON_MODULE $(call QUIET_INSTALL, python-scripts) \ - $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/python/Perf-Trace-Util/lib/Perf/Trace'; \ - $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/python/bin'; \ - $(INSTALL) scripts/python/Perf-Trace-Util/lib/Perf/Trace/* -m 644 -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/python/Perf-Trace-Util/lib/Perf/Trace'; \ - $(INSTALL) scripts/python/*.py -m 644 -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/python'; \ - $(INSTALL) scripts/python/bin/* -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/scripts/python/bin' - $(call QUIET_INSTALL, python-scripts-standalone) \ $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/python' && \ $(INSTALL) python/*.py -m 755 -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/python' && \ $(INSTALL) python/perf.pyi -m 644 -t '$(DESTDIR_SQ)$(perfexec_instdir_SQ)/python' && \ diff --git a/tools/perf/builtin-check.c b/tools/perf/builtin-check.c index 35272aaeb613..06711362d37f 100644 --- a/tools/perf/builtin-check.c +++ b/tools/perf/builtin-check.c @@ -52,7 +52,7 @@ struct feature_status supported_features[] = { FEATURE_STATUS("libnuma", HAVE_LIBNUMA_SUPPORT), FEATURE_STATUS("libopencsd", HAVE_CSTRACE_SUPPORT), FEATURE_STATUS_TIP("libperl", HAVE_LIBPERL_SUPPORT, "Deprecated, use LIBPERL=1 and install perl-ExtUtils-Embed/libperl-dev to build with it"), - FEATURE_STATUS("python-module", HAVE_LIBPYTHON_SUPPORT), + FEATURE_STATUS("python-module", HAVE_PYTHON_MODULE_SUPPORT), FEATURE_STATUS("libpfm4", HAVE_LIBPFM), FEATURE_STATUS("libslang", HAVE_SLANG_SUPPORT), FEATURE_STATUS("libtraceevent", HAVE_LIBTRACEEVENT), diff --git a/tools/perf/scripts/install-build-deps.sh b/tools/perf/scripts/install-build-deps.sh index a601a5260c17..3e53c7bf0f85 100755 --- a/tools/perf/scripts/install-build-deps.sh +++ b/tools/perf/scripts/install-build-deps.sh @@ -155,7 +155,7 @@ fedora_pkg_for() { libcapstone) echo "capstone-devel" ;; - libpython) + python-module) echo "python3-devel" ;; libtraceevent) @@ -314,7 +314,7 @@ debian_pkg_for() { libcapstone) echo "libcapstone-dev" ;; - libpython) + python-module) echo "python3-dev" ;; libtraceevent) diff --git a/tools/perf/tests/make b/tools/perf/tests/make index 8e55ff1aecbd..cf9d4b5d3153 100644 --- a/tools/perf/tests/make +++ b/tools/perf/tests/make @@ -80,8 +80,8 @@ make_jevents_all := JEVENTS_ARCH=all make_no_bpf_skel := BUILD_BPF_SKEL=0 make_gen_vmlinux_h := GEN_VMLINUX_H=1 make_libperl := LIBPERL=1 -make_no_libpython := NO_LIBPYTHON=1 -make_no_scripts := NO_LIBPYTHON=1 +make_no_python_module := NO_PYTHON_MODULE=1 +make_no_scripts := NO_PYTHON_MODULE=1 make_no_slang := NO_SLANG=1 make_no_demangle := NO_DEMANGLE=1 make_no_libelf := NO_LIBELF=1 @@ -121,7 +121,7 @@ make_install_prefix_slash := install prefix=/tmp/krava/ make_static := LDFLAGS=-static NO_PERF_READ_VDSO32=1 NO_PERF_READ_VDSOX32=1 NO_JVMTI=1 NO_LIBTRACEEVENT=1 NO_LIBELF=1 # all the NO_* variable combined -make_minimal := NO_LIBPYTHON=1 +make_minimal := NO_PYTHON_MODULE=1 make_minimal += NO_DEMANGLE=1 NO_LIBELF=1 NO_BACKTRACE=1 make_minimal += NO_LIBNUMA=1 NO_LIBBIONIC=1 NO_LIBDW=1 make_minimal += NO_LIBBPF=1 @@ -176,7 +176,7 @@ run += make_jevents_all run += make_no_bpf_skel run += make_gen_vmlinux_h run += make_libperl -run += make_no_libpython +run += make_no_python_module run += make_no_scripts run += make_no_slang run += make_no_demangle diff --git a/tools/perf/ui/browsers/scripts.c b/tools/perf/ui/browsers/scripts.c index 94cc1f427c96..b8a3383ec02a 100644 --- a/tools/perf/ui/browsers/scripts.c +++ b/tools/perf/ui/browsers/scripts.c @@ -1,4 +1,11 @@ // SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include +#include +#include +#include #include "../../util/util.h" // perf_exe() #include "../util.h" #include "../../util/evlist.h" @@ -14,7 +21,6 @@ #include #include #include -#include #define SCRIPT_NAMELEN 128 #define SCRIPT_MAX_NO 64 @@ -128,7 +134,7 @@ static int check_ev_match(int dir_fd, const char *scriptname, struct perf_sessio if (!len) break; - snprintf(evname, len + 1, "%s", p); + snprintf(evname, sizeof(evname), "%.*s", (int)len, p); match = 0; evlist__for_each_entry(session->evlist, pos) { @@ -159,6 +165,7 @@ static int check_ev_match(int dir_fd, const char *scriptname, struct perf_sessio static int find_scripts(char **scripts_array, char **scripts_path_array, int num, int pathlen) { + int namelen; struct dirent *script_dirent, *lang_dirent; int scripts_dir_fd, lang_dir_fd; DIR *scripts_dir, *lang_dir; @@ -180,73 +187,118 @@ static int find_scripts(char **scripts_array, char **scripts_path_array, int num snprintf(scripts_path, sizeof(scripts_path), "%s/scripts", exec_path); scripts_dir_fd = open(scripts_path, O_DIRECTORY); - pr_err("Failed to open directory '%s'", scripts_path); - if (scripts_dir_fd == -1) { - perf_session__delete(session); - return -1; + } + if (scripts_dir_fd != -1) { + scripts_dir = fdopendir(scripts_dir_fd); + if (scripts_dir) { + while ((lang_dirent = readdir(scripts_dir)) != NULL) { + if (lang_dirent->d_type != DT_DIR && + (lang_dirent->d_type == DT_UNKNOWN && + !is_directory_at(scripts_dir_fd, lang_dirent->d_name))) + continue; + if (!strcmp(lang_dirent->d_name, ".") || + !strcmp(lang_dirent->d_name, "..")) + continue; + + if (strstr(lang_dirent->d_name, "python")) + continue; + + lang_dir_fd = openat(scripts_dir_fd, lang_dirent->d_name, + O_DIRECTORY); + if (lang_dir_fd == -1) + continue; + lang_dir = fdopendir(lang_dir_fd); + if (!lang_dir) { + close(lang_dir_fd); + continue; + } + while ((script_dirent = readdir(lang_dir)) != NULL) { + if (script_dirent->d_type == DT_DIR) + continue; + if (script_dirent->d_type == DT_UNKNOWN && + is_directory_at(lang_dir_fd, script_dirent->d_name)) + continue; + /* Skip those real time scripts: xxxtop.p[yl] */ + if (strstr(script_dirent->d_name, "top.")) + continue; + if (i >= num) + break; + scnprintf(scripts_path_array[i], pathlen, + "%s/scripts/%s/%s", exec_path, + lang_dirent->d_name, + script_dirent->d_name); + temp = strrchr(script_dirent->d_name, '.'); + namelen = temp ? (int)(temp - script_dirent->d_name) + : (int)strlen(script_dirent->d_name); + + if (namelen >= SCRIPT_NAMELEN) + namelen = SCRIPT_NAMELEN - 1; + snprintf(scripts_array[i], namelen + 1, "%s", + script_dirent->d_name); + + if (check_ev_match(lang_dir_fd, scripts_array[i], session)) + continue; + + i++; + } + closedir(lang_dir); + } + closedir(scripts_dir); + } else { + close(scripts_dir_fd); } } - scripts_dir = fdopendir(scripts_dir_fd); - if (!scripts_dir) { - close(scripts_dir_fd); - perf_session__delete(session); - return -1; + +#ifdef HAVE_PYTHON_MODULE_SUPPORT + { + char py_scripts_path[PATH_MAX]; + int py_scripts_dir_fd; + DIR *py_scripts_dir; + int len; + + snprintf(py_scripts_path, sizeof(py_scripts_path), "%s/python", exec_path); + py_scripts_dir_fd = open(py_scripts_path, O_DIRECTORY); + if (py_scripts_dir_fd != -1) { + py_scripts_dir = fdopendir(py_scripts_dir_fd); + if (py_scripts_dir) { + while ((script_dirent = readdir(py_scripts_dir)) != NULL) { + if (script_dirent->d_type == DT_DIR) + continue; + if (script_dirent->d_type == DT_UNKNOWN && + is_directory_at(py_scripts_dir_fd, + script_dirent->d_name)) + continue; + /* Skip those real time scripts: xxxtop.p[yl] */ + if (strstr(script_dirent->d_name, "top.")) + continue; + if (i >= num) + break; + len = strlen(script_dirent->d_name); + if (len <= 3 || + strcmp(script_dirent->d_name + len - 3, ".py")) + continue; + + scnprintf(scripts_path_array[i], pathlen, "%s/python/%s", + exec_path, + script_dirent->d_name); + temp = strrchr(script_dirent->d_name, '.'); + namelen = temp ? (int)(temp - script_dirent->d_name) + : (int)strlen(script_dirent->d_name); + + if (namelen >= SCRIPT_NAMELEN) + namelen = SCRIPT_NAMELEN - 1; + snprintf(scripts_array[i], namelen + 1, "%s", + script_dirent->d_name); + + i++; + } + closedir(py_scripts_dir); + } else { + close(py_scripts_dir_fd); + } + } } - - while ((lang_dirent = readdir(scripts_dir)) != NULL) { - if (lang_dirent->d_type != DT_DIR && - (lang_dirent->d_type == DT_UNKNOWN && - !is_directory_at(scripts_dir_fd, lang_dirent->d_name))) - continue; - if (!strcmp(lang_dirent->d_name, ".") || !strcmp(lang_dirent->d_name, "..")) - continue; - -#ifndef HAVE_LIBPERL_SUPPORT - if (strstr(lang_dirent->d_name, "perl")) - continue; #endif -#ifndef HAVE_LIBPYTHON_SUPPORT - if (strstr(lang_dirent->d_name, "python")) - continue; -#endif - - lang_dir_fd = openat(scripts_dir_fd, lang_dirent->d_name, O_DIRECTORY); - if (lang_dir_fd == -1) - continue; - lang_dir = fdopendir(lang_dir_fd); - if (!lang_dir) { - close(lang_dir_fd); - continue; - } - while ((script_dirent = readdir(lang_dir)) != NULL) { - if (script_dirent->d_type == DT_DIR) - continue; - if (script_dirent->d_type == DT_UNKNOWN && - is_directory_at(lang_dir_fd, script_dirent->d_name)) - continue; - /* Skip those real time scripts: xxxtop.p[yl] */ - if (strstr(script_dirent->d_name, "top.")) - continue; - if (i >= num) - break; - scnprintf(scripts_path_array[i], pathlen, "%s/scripts/%s/%s", - exec_path, - lang_dirent->d_name, - script_dirent->d_name); - temp = strchr(script_dirent->d_name, '.'); - snprintf(scripts_array[i], - (temp - script_dirent->d_name) + 1, - "%s", script_dirent->d_name); - - if (check_ev_match(lang_dir_fd, scripts_array[i], session)) - continue; - - i++; - } - closedir(lang_dir); - } - - closedir(scripts_dir); perf_session__delete(session); return i; } @@ -264,7 +316,7 @@ static int list_scripts(char *script_name, bool *custom, int ret = 0; int max_std, custom_perf; char pbuf[256]; - const char *perf = perf_exe(pbuf, sizeof pbuf); + const char *perf = perf_exe(pbuf, sizeof(pbuf)); struct script_config scriptc = { .names = (const char **)names, .paths = paths, @@ -354,7 +406,7 @@ int script_browse(const char *script_opt, struct evsel *evsel) return -1; if (asprintf(&cmd, "%s%s %s %s%s 2>&1 | less", - custom ? "perf script -s " : "", + custom ? "perf script " : "", script_name, script_opt ? script_opt : "", input_name ? "-i " : "", From b02aa1d1343f580e5a43789b392e0c813bc1cba9 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:15 -0700 Subject: [PATCH 223/425] perf script: Support standalone scripts and remove embedded scripting Refactor 'perf script' to launch standalone scripts directly via fork() and execvp() and remove the legacy embedded Perl and Python scripting engines: - Remove the embedded Perl scripting engine (util/scripting-engines/trace-event-perl.c), Perl scripts, bin wrappers, and Trace-Util library (scripts/perl/Perf-Trace-Util/), and script_perl.sh test. - Remove libperl feature checks from Makefile.config, Makefile.perf, builtin-check.c, and Documentation/perf-check.txt. - Remove -g / --gen-script option and scripting_ops dispatch table from builtin-script.c and trace-event-scripting.c. - Hide the legacy -s / --script option and update script discovery in find_script() and list_available_scripts() to prioritize the system 'python' directory over bare filenames in the current directory. - Update the Python script shell tests (test_arm_coresight_disasm.sh, test_task_analyzer.sh, and test_*_python.sh) to invoke scripts via 'perf script EOF - if ! "$PYTHON" "$script_path" -i "${temp_data}" -f html --template "${temp_tpl}" \ + if ! perf script flamegraph -i "${temp_data}" -f html --template "${temp_tpl}" \ --colorscheme blue-green -o "${temp_html}" >/dev/null 2>&1; then echo "Custom template HTML test failed." err=1 diff --git a/tools/perf/tests/shell/test_futex_contention_python.sh b/tools/perf/tests/shell/test_futex_contention_python.sh index 26b13d263d20..a8846ed68ac2 100755 --- a/tools/perf/tests/shell/test_futex_contention_python.sh +++ b/tools/perf/tests/shell/test_futex_contention_python.sh @@ -101,7 +101,7 @@ test_file_mode() { return fi - if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + if ! perf script futex-contention -i "${temp_data}" > "${temp_out}"; then echo "File mode test failed." err=1 else diff --git a/tools/perf/tests/shell/test_gecko_python.sh b/tools/perf/tests/shell/test_gecko_python.sh index eac9b2f8e3c7..de1d7fcf88b7 100755 --- a/tools/perf/tests/shell/test_gecko_python.sh +++ b/tools/perf/tests/shell/test_gecko_python.sh @@ -45,7 +45,7 @@ test_file_mode() { fi # Run the script in save-only mode with custom product and category colors - if ! "$PYTHON" "$script_path" -i "${temp_data}" \ + if ! perf script gecko -i "${temp_data}" \ --product "perf-test-product" --user-color blue --kernel-color red \ --save-only "${temp_json}" >/dev/null; then echo "File mode test failed." diff --git a/tools/perf/tests/shell/test_intel_pt_events_python.sh b/tools/perf/tests/shell/test_intel_pt_events_python.sh index 0d397792ced8..b5c3173fa2db 100755 --- a/tools/perf/tests/shell/test_intel_pt_events_python.sh +++ b/tools/perf/tests/shell/test_intel_pt_events_python.sh @@ -49,7 +49,7 @@ test_intel_pt() { fi # Run the script and check output - if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + if ! perf script intel-pt-events -i "${temp_data}" > "${temp_out}"; then echo "intel-pt-events.py test failed." err=1 else diff --git a/tools/perf/tests/shell/test_mem_phys_addr_python.sh b/tools/perf/tests/shell/test_mem_phys_addr_python.sh index d094a0611a24..451692d5e089 100755 --- a/tools/perf/tests/shell/test_mem_phys_addr_python.sh +++ b/tools/perf/tests/shell/test_mem_phys_addr_python.sh @@ -87,7 +87,8 @@ test_file_mode() { fi # Run the script with custom --iomem - if ! "$PYTHON" "$script_path" -i "${temp_data}" --iomem "${temp_iomem}" > "${temp_out}" || \ + if ! perf script mem-phys-addr -i "${temp_data}" \ + --iomem "${temp_iomem}" > "${temp_out}" || \ [ ! -s "${temp_out}" ]; then echo "File mode test failed." err=1 diff --git a/tools/perf/tests/shell/test_net_dropmonitor_python.sh b/tools/perf/tests/shell/test_net_dropmonitor_python.sh index d00be255d259..05056a897bc9 100755 --- a/tools/perf/tests/shell/test_net_dropmonitor_python.sh +++ b/tools/perf/tests/shell/test_net_dropmonitor_python.sh @@ -59,7 +59,7 @@ if [ ! -s "${temp_data}" ]; then fi # Check that the script executes and outputs table header -if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then +if ! perf script net_dropmonitor -i "${temp_data}" > "${temp_out}"; then echo "net_dropmonitor.py test failed" err=1 else diff --git a/tools/perf/tests/shell/test_netdev_times_python.sh b/tools/perf/tests/shell/test_netdev_times_python.sh index bb0f8c30207f..393bf446efb2 100755 --- a/tools/perf/tests/shell/test_netdev_times_python.sh +++ b/tools/perf/tests/shell/test_netdev_times_python.sh @@ -93,7 +93,7 @@ if ! perf record -e skb:kfree_skb -a -o "${temp_data}" \ fi # Check that the script executes -if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then +if ! perf script netdev-times -i "${temp_data}" > "${temp_out}"; then echo "netdev-times.py test failed" err=1 else diff --git a/tools/perf/tests/shell/test_powerpc_hcalls_python.sh b/tools/perf/tests/shell/test_powerpc_hcalls_python.sh index 5316c28f243f..19569c6d3613 100755 --- a/tools/perf/tests/shell/test_powerpc_hcalls_python.sh +++ b/tools/perf/tests/shell/test_powerpc_hcalls_python.sh @@ -79,7 +79,7 @@ if [ ! -s "${temp_data}" ]; then fi # Check that the script executes on recorded perf.data -if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then +if ! perf script powerpc-hcalls -i "${temp_data}" > "${temp_out}"; then echo "powerpc-hcalls.py test failed" err=1 else diff --git a/tools/perf/tests/shell/test_rw_by_file_python.sh b/tools/perf/tests/shell/test_rw_by_file_python.sh index 98218bf8a39e..f121597f418f 100755 --- a/tools/perf/tests/shell/test_rw_by_file_python.sh +++ b/tools/perf/tests/shell/test_rw_by_file_python.sh @@ -52,7 +52,7 @@ if [ ! -s "${temp_data}" ]; then fi # Check that the script executes - filtering for "dd" since that's what we ran -if ! "$PYTHON" "$script_path" -i "${temp_data}" "dd" > "${temp_out}"; then +if ! perf script rw-by-file -i "${temp_data}" "dd" > "${temp_out}"; then echo "rw-by-file.py test failed" err=1 else diff --git a/tools/perf/tests/shell/test_rw_by_pid_python.sh b/tools/perf/tests/shell/test_rw_by_pid_python.sh index da8b065d43de..41eaa29097a2 100755 --- a/tools/perf/tests/shell/test_rw_by_pid_python.sh +++ b/tools/perf/tests/shell/test_rw_by_pid_python.sh @@ -58,7 +58,7 @@ if [ ! -s "${temp_data}" ]; then fi # Check that the script executes -if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then +if ! perf script rw-by-pid -i "${temp_data}" > "${temp_out}"; then echo "rw-by-pid.py test failed" err=1 else diff --git a/tools/perf/tests/shell/test_rwtop_python.sh b/tools/perf/tests/shell/test_rwtop_python.sh index 062fca2dad14..05897e384702 100755 --- a/tools/perf/tests/shell/test_rwtop_python.sh +++ b/tools/perf/tests/shell/test_rwtop_python.sh @@ -58,7 +58,7 @@ if [ ! -s "${temp_data}" ]; then fi # Check that the script executes -if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then +if ! perf script rwtop -i "${temp_data}" > "${temp_out}"; then echo "rwtop.py test failed" err=1 else diff --git a/tools/perf/tests/shell/test_sched_migration_python.sh b/tools/perf/tests/shell/test_sched_migration_python.sh index f243e86c6700..75465bcd3791 100755 --- a/tools/perf/tests/shell/test_sched_migration_python.sh +++ b/tools/perf/tests/shell/test_sched_migration_python.sh @@ -58,7 +58,7 @@ if [ ! -s "${temp_data}" ]; then fi # Check that the script executes -if ! "$PYTHON" "$script_path" -v --no-gui -i "${temp_data}" > "${temp_out}"; then +if ! perf script sched-migration -v --no-gui -i "${temp_data}" > "${temp_out}"; then echo "sched-migration.py test failed" err=1 elif [ "$has_sched" -eq 1 ] && ! grep -q "Timeslices:" "${temp_out}"; then diff --git a/tools/perf/tests/shell/test_sctop_python.sh b/tools/perf/tests/shell/test_sctop_python.sh index cd38cdd4794c..007f2584cce6 100755 --- a/tools/perf/tests/shell/test_sctop_python.sh +++ b/tools/perf/tests/shell/test_sctop_python.sh @@ -56,13 +56,13 @@ if [ ! -s "${temp_data}" ]; then fi # Check that the script executes -if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then +if ! perf script sctop -i "${temp_data}" > "${temp_out}"; then echo "sctop.py test failed" err=1 elif ! grep -E -q "[0-9]+$" "${temp_out}"; then echo "Failed to find metric data rows in default run" err=1 -elif ! "$PYTHON" "$script_path" -i "${temp_data}" sleep 1 > "${temp_out}"; then +elif ! perf script sctop -i "${temp_data}" sleep 1 > "${temp_out}"; then echo "sctop.py comm+interval test failed" err=1 else diff --git a/tools/perf/tests/shell/test_stackcollapse_python.sh b/tools/perf/tests/shell/test_stackcollapse_python.sh index a3b6c917e842..ce0f80407eec 100755 --- a/tools/perf/tests/shell/test_stackcollapse_python.sh +++ b/tools/perf/tests/shell/test_stackcollapse_python.sh @@ -47,7 +47,7 @@ if [ ! -s "${temp_data}" ]; then fi # Check that the script executes with default options -if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then +if ! perf script stackcollapse -i "${temp_data}" > "${temp_out}"; then echo "stackcollapse.py test failed" err=1 else @@ -61,12 +61,12 @@ else fi # Test CLI flags (--include-pid, --include-tid, --tidy-java, --kernel) and BrokenPipeError -if ! "$PYTHON" "$script_path" -i "${temp_data}" \ +if ! perf script stackcollapse -i "${temp_data}" \ --include-pid --include-tid --tidy-java --kernel | head -n 1 > "${temp_out}" || \ [ ! -s "${temp_out}" ]; then echo "stackcollapse.py options/pipe test failed" err=1 -elif ! "$PYTHON" "$script_path" -i "${temp_data}" --no-comm > "${temp_out}" || \ +elif ! perf script stackcollapse -i "${temp_data}" --no-comm > "${temp_out}" || \ [ ! -s "${temp_out}" ]; then echo "stackcollapse.py --no-comm test failed" err=1 diff --git a/tools/perf/tests/shell/test_stat_cpi_python.sh b/tools/perf/tests/shell/test_stat_cpi_python.sh index 0960afa4c797..fe7562307634 100755 --- a/tools/perf/tests/shell/test_stat_cpi_python.sh +++ b/tools/perf/tests/shell/test_stat_cpi_python.sh @@ -62,7 +62,7 @@ test_live_mode() { ran=1 # Run live mode for 1 interval in the background, give it a tiny sleep, then interrupt - "$PYTHON" "$script_path" -I 0.1 -p "$workload_pid" > "${temp_out}" & + perf script stat-cpi -I 0.1 -p "$workload_pid" > "${temp_out}" & pid=$! sleep 0.5 kill -INT "$pid" 2>/dev/null || true @@ -96,7 +96,7 @@ test_file_mode() { fi ran=1 - out=$("$PYTHON" "$script_path" -i "${temp_data}") + out=$(perf script stat-cpi -i "${temp_data}") if ! echo "$out" | grep -q "cpi"; then echo "File mode test failed." err=1 diff --git a/tools/perf/tests/shell/test_syscall_counts_by_pid_python.sh b/tools/perf/tests/shell/test_syscall_counts_by_pid_python.sh index 9f2ad27751a2..45bbdc554c8f 100755 --- a/tools/perf/tests/shell/test_syscall_counts_by_pid_python.sh +++ b/tools/perf/tests/shell/test_syscall_counts_by_pid_python.sh @@ -53,7 +53,7 @@ test_file_mode() { -- sleep 0.5 >/dev/null 2>&1 || \ { echo "Skipping test, perf record failed"; exit 2; } - if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + if ! perf script syscall-counts-by-pid -i "${temp_data}" > "${temp_out}"; then echo "File mode test failed." err=1 elif ! grep -E -q "^ [a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then @@ -64,7 +64,7 @@ test_file_mode() { fi # Test with a comm argument - if ! "$PYTHON" "$script_path" -i "${temp_data}" "sleep" > "${temp_out}"; then + if ! perf script syscall-counts-by-pid -i "${temp_data}" "sleep" > "${temp_out}"; then echo "Comm filter test failed." err=1 elif ! grep -E -q "^ [a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then @@ -76,7 +76,8 @@ test_file_mode() { # Extract sleep PID from "sleep []" header and test with a numeric PID filter argument sleep_pid=$(sed -n 's/^sleep \[\([0-9]\+\)\]$/\1/p' "${temp_out}" | head -n 1) - if [ -z "${sleep_pid}" ] || ! "$PYTHON" "$script_path" -i "${temp_data}" "${sleep_pid}" > "${temp_out}"; then + if [ -z "${sleep_pid}" ] || \ + ! perf script syscall-counts-by-pid -i "${temp_data}" "${sleep_pid}" > "${temp_out}"; then echo "PID filter test failed." err=1 elif ! grep -E -q "^ [a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then diff --git a/tools/perf/tests/shell/test_syscall_counts_python.sh b/tools/perf/tests/shell/test_syscall_counts_python.sh index 0c4b75499209..310e05d399f9 100755 --- a/tools/perf/tests/shell/test_syscall_counts_python.sh +++ b/tools/perf/tests/shell/test_syscall_counts_python.sh @@ -54,7 +54,7 @@ test_file_mode() { exit 2 fi - if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then + if ! perf script syscall-counts -i "${temp_data}" > "${temp_out}"; then echo "File mode test failed." err=1 elif ! grep -E -q "^[a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then @@ -65,7 +65,7 @@ test_file_mode() { fi # Test with a comm argument - if ! "$PYTHON" "$script_path" -i "${temp_data}" "sleep" > "${temp_out}"; then + if ! perf script syscall-counts -i "${temp_data}" "sleep" > "${temp_out}"; then echo "Comm filter test failed." err=1 elif ! grep -E -q "^[a-zA-Z0-9_]+ +[0-9]+$" "${temp_out}"; then diff --git a/tools/perf/tests/shell/test_task_analyzer.sh b/tools/perf/tests/shell/test_task_analyzer.sh index 3a97d58eec74..f99a637a6099 100755 --- a/tools/perf/tests/shell/test_task_analyzer.sh +++ b/tools/perf/tests/shell/test_task_analyzer.sh @@ -11,16 +11,6 @@ csv="$tmpdir/csv" csvsummary="$tmpdir/csvsummary" err=0 -# Set up perfdir and PERF_EXEC_PATH -if [ "x$PERF_EXEC_PATH" = "x" ]; then - perfdir="$(dirname "$0")/../.." - if [ -f "$perfdir/python/task-analyzer.py" ]; then - export PERF_EXEC_PATH="$perfdir" - fi -else - perfdir="$PERF_EXEC_PATH" -fi - # Disable lsan to avoid warnings about python memory leaks. export ASAN_OPTIONS=detect_leaks=0 @@ -81,17 +71,17 @@ prepare_perf_data() { # check standard inkvokation with no arguments test_basic() { out="$tmpdir/perf.out" - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" > "$out" - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata}" + perf script -i "${perfdata}" task-analyzer > "$out" + check_exec_0 "perf script -i ${perfdata} task-analyzer" find_str_or_fail "Comm" "$out" "${FUNCNAME[0]}" } test_ns_rename(){ out="$tmpdir/perf.out" - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + perf script -i "${perfdata}" task-analyzer \ --ns \ --rename-comms-by-tids 0:random > "$out" - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + check_exec_0 "perf script -i ${perfdata} task-analyzer \ --ns \ --rename-comms-by-tids 0:random" find_str_or_fail "Comm" "$out" "${FUNCNAME[0]}" @@ -99,20 +89,20 @@ test_ns_rename(){ test_ms_filtertasks_highlight(){ out="$tmpdir/perf.out" - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + perf script -i "${perfdata}" task-analyzer \ --ms --filter-tasks perf --highlight-tasks perf \ > "$out" - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + check_exec_0 "perf script -i ${perfdata} task-analyzer \ --ms --filter-tasks perf --highlight-tasks perf" find_str_or_fail "Comm" "$out" "${FUNCNAME[0]}" } test_extended_times_timelimit_limittasks() { out="$tmpdir/perf.out" - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --extended-times \ + perf script -i "${perfdata}" task-analyzer --extended-times \ --time-limit :99999 \ --limit-to-tasks perf > "$out" - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + check_exec_0 "perf script -i ${perfdata} task-analyzer \ --extended-times \ --time-limit :99999 --limit-to-tasks perf" find_str_or_fail "Out-Out" "$out" "${FUNCNAME[0]}" @@ -120,30 +110,30 @@ test_extended_times_timelimit_limittasks() { test_summary() { out="$tmpdir/perf.out" - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --summary > "$out" - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} --summary" + perf script -i "${perfdata}" task-analyzer --summary > "$out" + check_exec_0 "perf script -i ${perfdata} task-analyzer --summary" find_str_or_fail "Summary" "$out" "${FUNCNAME[0]}" } test_summaryextended() { out="$tmpdir/perf.out" - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --summary-extended > "$out" - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} --summary-extended" + perf script -i "${perfdata}" task-analyzer --summary-extended > "$out" + check_exec_0 "perf script -i ${perfdata} task-analyzer --summary-extended" find_str_or_fail "Inter Task Times" "$out" "${FUNCNAME[0]}" } test_summaryonly() { out="$tmpdir/perf.out" - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --summary-only > "$out" - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} --summary-only" + perf script -i "${perfdata}" task-analyzer --summary-only > "$out" + check_exec_0 "perf script -i ${perfdata} task-analyzer --summary-only" find_str_or_fail "Summary" "$out" "${FUNCNAME[0]}" } test_extended_times_summary_ns() { out="$tmpdir/perf.out" - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --extended-times \ + perf script -i "${perfdata}" task-analyzer --extended-times \ --summary --ns > "$out" - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + check_exec_0 "perf script -i ${perfdata} task-analyzer \ --extended-times \ --summary \ --ns" @@ -152,34 +142,34 @@ test_extended_times_summary_ns() { } test_csv() { - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" --csv "${csv}" > /dev/null - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} --csv ${csv}" + perf script -i "${perfdata}" task-analyzer --csv "${csv}" > /dev/null + check_exec_0 "perf script -i ${perfdata} task-analyzer --csv ${csv}" find_str_or_fail "Comm;" "${csv}" "${FUNCNAME[0]}" } test_csv_extended_times() { - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + perf script -i "${perfdata}" task-analyzer \ --csv "${csv}" \ --extended-times > /dev/null - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + check_exec_0 "perf script -i ${perfdata} task-analyzer \ --csv ${csv} \ --extended-times" find_str_or_fail "Out-Out;" "${csv}" "${FUNCNAME[0]}" } test_csvsummary() { - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + perf script -i "${perfdata}" task-analyzer \ --csv-summary "${csvsummary}" > /dev/null - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + check_exec_0 "perf script -i ${perfdata} task-analyzer \ --csv-summary ${csvsummary}" find_str_or_fail "Comm;" "${csvsummary}" "${FUNCNAME[0]}" } test_csvsummary_extended() { - $PYTHON $perfdir/python/task-analyzer.py -i "${perfdata}" \ + perf script -i "${perfdata}" task-analyzer \ --csv-summary "${csvsummary}" --summary-extended \ >/dev/null - check_exec_0 "$PYTHON $perfdir/python/task-analyzer.py -i ${perfdata} \ + check_exec_0 "perf script -i ${perfdata} task-analyzer \ --csv-summary ${csvsummary} --summary-extended" find_str_or_fail "Out-Out;" "${csvsummary}" "${FUNCNAME[0]}" } diff --git a/tools/perf/tests/shell/test_wakeup_latency_python.sh b/tools/perf/tests/shell/test_wakeup_latency_python.sh index fc39ac117609..cb2571449168 100755 --- a/tools/perf/tests/shell/test_wakeup_latency_python.sh +++ b/tools/perf/tests/shell/test_wakeup_latency_python.sh @@ -57,7 +57,7 @@ if [ ! -s "${temp_data}" ]; then fi # Check that the script executes -if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}"; then +if ! perf script wakeup-latency -i "${temp_data}" > "${temp_out}"; then echo "wakeup-latency.py test failed" err=1 else @@ -71,7 +71,7 @@ fi # Also test zero-wakeups / unhandled events path to verify division-by-zero protection if perf record -e cycles -o "${temp_data}" -- perf test -w noploop >/dev/null 2>&1; then - if ! "$PYTHON" "$script_path" -i "${temp_data}" > "${temp_out}" || \ + if ! perf script wakeup-latency -i "${temp_data}" > "${temp_out}" || \ ! grep -q "avg_wakeup_latency (ns): N/A" "${temp_out}"; then echo "wakeup-latency zero-wakeups guard test failed" err=1 diff --git a/tools/perf/ui/browsers/scripts.c b/tools/perf/ui/browsers/scripts.c index b8a3383ec02a..d6d09b4a348c 100644 --- a/tools/perf/ui/browsers/scripts.c +++ b/tools/perf/ui/browsers/scripts.c @@ -86,75 +86,6 @@ static int scripts_config(const char *var, const char *value, void *data) return 0; } -/* - * Some scripts specify the required events in their "xxx-record" file, - * this function will check if the events in perf.data match those - * mentioned in the "xxx-record". - * - * Fixme: All existing "xxx-record" are all in good formats "-e event ", - * which is covered well now. And new parsing code should be added to - * cover the future complex formats like event groups etc. - */ -static int check_ev_match(int dir_fd, const char *scriptname, struct perf_session *session) -{ - char line[BUFSIZ]; - FILE *fp; - - { - char filename[NAME_MAX + 5]; - int fd; - - scnprintf(filename, sizeof(filename), "bin/%s-record", scriptname); - fd = openat(dir_fd, filename, O_RDONLY); - if (fd == -1) - return -1; - fp = fdopen(fd, "r"); - if (!fp) - return -1; - } - - while (fgets(line, sizeof(line), fp)) { - char *p = skip_spaces(line); - - if (*p == '#') - continue; - - while (strlen(p)) { - int match, len; - struct evsel *pos; - char evname[128]; - - p = strstr(p, "-e"); - if (!p) - break; - - p += 2; - p = skip_spaces(p); - len = strcspn(p, " \t"); - if (!len) - break; - - snprintf(evname, sizeof(evname), "%.*s", (int)len, p); - - match = 0; - evlist__for_each_entry(session->evlist, pos) { - if (evsel__name_is(pos, evname)) { - match = 1; - break; - } - } - - if (!match) { - fclose(fp); - return -1; - } - } - } - - fclose(fp); - return 0; -} - /* * Return -1 if none is found, otherwise the actual scripts number. * @@ -162,95 +93,18 @@ static int check_ev_match(int dir_fd, const char *scriptname, struct perf_sessio * will list all statically runnable scripts, select one, execute it and * show the output in a perf browser. */ -static int find_scripts(char **scripts_array, char **scripts_path_array, int num, - int pathlen) +static int find_scripts(char **scripts_array __maybe_unused, + char **scripts_path_array __maybe_unused, int num __maybe_unused, + int pathlen __maybe_unused) { - int namelen; - struct dirent *script_dirent, *lang_dirent; - int scripts_dir_fd, lang_dir_fd; - DIR *scripts_dir, *lang_dir; - struct perf_session *session; - struct perf_data data = { - .path = input_name, - .mode = PERF_DATA_MODE_READ, - }; - char *temp; int i = 0; - const char *exec_path = get_argv_exec_path(); - - session = perf_session__new(&data, NULL); - if (IS_ERR(session)) - return PTR_ERR(session); - - { - char scripts_path[PATH_MAX]; - - snprintf(scripts_path, sizeof(scripts_path), "%s/scripts", exec_path); - scripts_dir_fd = open(scripts_path, O_DIRECTORY); - } - if (scripts_dir_fd != -1) { - scripts_dir = fdopendir(scripts_dir_fd); - if (scripts_dir) { - while ((lang_dirent = readdir(scripts_dir)) != NULL) { - if (lang_dirent->d_type != DT_DIR && - (lang_dirent->d_type == DT_UNKNOWN && - !is_directory_at(scripts_dir_fd, lang_dirent->d_name))) - continue; - if (!strcmp(lang_dirent->d_name, ".") || - !strcmp(lang_dirent->d_name, "..")) - continue; - - if (strstr(lang_dirent->d_name, "python")) - continue; - - lang_dir_fd = openat(scripts_dir_fd, lang_dirent->d_name, - O_DIRECTORY); - if (lang_dir_fd == -1) - continue; - lang_dir = fdopendir(lang_dir_fd); - if (!lang_dir) { - close(lang_dir_fd); - continue; - } - while ((script_dirent = readdir(lang_dir)) != NULL) { - if (script_dirent->d_type == DT_DIR) - continue; - if (script_dirent->d_type == DT_UNKNOWN && - is_directory_at(lang_dir_fd, script_dirent->d_name)) - continue; - /* Skip those real time scripts: xxxtop.p[yl] */ - if (strstr(script_dirent->d_name, "top.")) - continue; - if (i >= num) - break; - scnprintf(scripts_path_array[i], pathlen, - "%s/scripts/%s/%s", exec_path, - lang_dirent->d_name, - script_dirent->d_name); - temp = strrchr(script_dirent->d_name, '.'); - namelen = temp ? (int)(temp - script_dirent->d_name) - : (int)strlen(script_dirent->d_name); - - if (namelen >= SCRIPT_NAMELEN) - namelen = SCRIPT_NAMELEN - 1; - snprintf(scripts_array[i], namelen + 1, "%s", - script_dirent->d_name); - - if (check_ev_match(lang_dir_fd, scripts_array[i], session)) - continue; - - i++; - } - closedir(lang_dir); - } - closedir(scripts_dir); - } else { - close(scripts_dir_fd); - } - } #ifdef HAVE_PYTHON_MODULE_SUPPORT { + int namelen; + struct dirent *script_dirent; + char *temp; + const char *exec_path = get_argv_exec_path(); char py_scripts_path[PATH_MAX]; int py_scripts_dir_fd; DIR *py_scripts_dir; @@ -299,7 +153,6 @@ static int find_scripts(char **scripts_array, char **scripts_path_array, int num } } #endif - perf_session__delete(session); return i; } diff --git a/tools/perf/util/Build b/tools/perf/util/Build index afcf98dc5006..512f2ca0cd1a 100644 --- a/tools/perf/util/Build +++ b/tools/perf/util/Build @@ -95,7 +95,6 @@ perf-util-y += tool_pmu.o perf-util-y += tp_pmu.o perf-util-y += svghelper.o perf-util-y += trace-event-info.o -perf-util-y += trace-event-scripting.o perf-util-$(CONFIG_LIBTRACEEVENT) += trace-event.o perf-util-$(CONFIG_LIBTRACEEVENT) += trace-event-parse.o perf-util-$(CONFIG_LIBTRACEEVENT) += trace-event-read.o @@ -236,8 +235,6 @@ endif perf-util-y += data-convert-json.o -perf-util-y += scripting-engines/ - perf-util-$(CONFIG_ZLIB) += zlib.o perf-util-$(CONFIG_LZMA) += lzma.o perf-util-$(CONFIG_ZSTD) += zstd.o diff --git a/tools/perf/util/scripting-engines/Build b/tools/perf/util/scripting-engines/Build deleted file mode 100644 index 3f1dc10526f8..000000000000 --- a/tools/perf/util/scripting-engines/Build +++ /dev/null @@ -1,5 +0,0 @@ -ifeq ($(CONFIG_LIBTRACEEVENT),y) - perf-util-$(CONFIG_LIBPERL) += trace-event-perl.o -endif - -CFLAGS_trace-event-perl.o += $(PERL_EMBED_CCOPTS) -Wno-redundant-decls -Wno-strict-prototypes -Wno-unused-parameter -Wno-shadow -Wno-nested-externs -Wno-undef -Wno-switch-default -Wno-bad-function-cast -Wno-declaration-after-statement -Wno-switch-enum -Wno-thread-safety-analysis diff --git a/tools/perf/util/scripting-engines/trace-event-perl.c b/tools/perf/util/scripting-engines/trace-event-perl.c deleted file mode 100644 index 410dc4cd0600..000000000000 --- a/tools/perf/util/scripting-engines/trace-event-perl.c +++ /dev/null @@ -1,770 +0,0 @@ -/* - * trace-event-perl. Feed perf script events to an embedded Perl interpreter. - * - * Copyright (C) 2009 Tom Zanussi - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -/* perl needs the following define, right after including stdbool.h */ -#define HAS_BOOL -#include -#include - -#include "../callchain.h" -#include "../dso.h" -#include "../machine.h" -#include "../map.h" -#include "../symbol.h" -#include "../thread.h" -#include "../event.h" -#include "../trace-event.h" -#include "../evsel.h" -#include "../debug.h" - -void boot_Perf__Trace__Context(pTHX_ CV *cv); -void boot_DynaLoader(pTHX_ CV *cv); -typedef PerlInterpreter * INTERP; - -void xs_init(pTHX); - -void xs_init(pTHX) -{ - const char *file = __FILE__; - dXSUB_SYS; - - newXS("Perf::Trace::Context::bootstrap", boot_Perf__Trace__Context, - file); - newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, file); -} - -INTERP my_perl; - -#define TRACE_EVENT_TYPE_MAX \ - ((1 << (sizeof(unsigned short) * 8)) - 1) - -extern struct scripting_context *scripting_context; - -static char *cur_field_name; -static int zero_flag_atom; - -static void define_symbolic_value(const char *ev_name, - const char *field_name, - const char *field_value, - const char *field_str) -{ - unsigned long long value; - dSP; - - value = eval_flag(field_value); - - ENTER; - SAVETMPS; - PUSHMARK(SP); - - XPUSHs(sv_2mortal(newSVpv(ev_name, 0))); - XPUSHs(sv_2mortal(newSVpv(field_name, 0))); - XPUSHs(sv_2mortal(newSVuv(value))); - XPUSHs(sv_2mortal(newSVpv(field_str, 0))); - - PUTBACK; - if (get_cv("main::define_symbolic_value", 0)) - call_pv("main::define_symbolic_value", G_SCALAR); - SPAGAIN; - PUTBACK; - FREETMPS; - LEAVE; -} - -static void define_symbolic_values(struct tep_print_flag_sym *field, - const char *ev_name, - const char *field_name) -{ - define_symbolic_value(ev_name, field_name, field->value, field->str); - if (field->next) - define_symbolic_values(field->next, ev_name, field_name); -} - -static void define_symbolic_field(const char *ev_name, - const char *field_name) -{ - dSP; - - ENTER; - SAVETMPS; - PUSHMARK(SP); - - XPUSHs(sv_2mortal(newSVpv(ev_name, 0))); - XPUSHs(sv_2mortal(newSVpv(field_name, 0))); - - PUTBACK; - if (get_cv("main::define_symbolic_field", 0)) - call_pv("main::define_symbolic_field", G_SCALAR); - SPAGAIN; - PUTBACK; - FREETMPS; - LEAVE; -} - -static void define_flag_value(const char *ev_name, - const char *field_name, - const char *field_value, - const char *field_str) -{ - unsigned long long value; - dSP; - - value = eval_flag(field_value); - - ENTER; - SAVETMPS; - PUSHMARK(SP); - - XPUSHs(sv_2mortal(newSVpv(ev_name, 0))); - XPUSHs(sv_2mortal(newSVpv(field_name, 0))); - XPUSHs(sv_2mortal(newSVuv(value))); - XPUSHs(sv_2mortal(newSVpv(field_str, 0))); - - PUTBACK; - if (get_cv("main::define_flag_value", 0)) - call_pv("main::define_flag_value", G_SCALAR); - SPAGAIN; - PUTBACK; - FREETMPS; - LEAVE; -} - -static void define_flag_values(struct tep_print_flag_sym *field, - const char *ev_name, - const char *field_name) -{ - define_flag_value(ev_name, field_name, field->value, field->str); - if (field->next) - define_flag_values(field->next, ev_name, field_name); -} - -static void define_flag_field(const char *ev_name, - const char *field_name, - const char *delim) -{ - dSP; - - ENTER; - SAVETMPS; - PUSHMARK(SP); - - XPUSHs(sv_2mortal(newSVpv(ev_name, 0))); - XPUSHs(sv_2mortal(newSVpv(field_name, 0))); - XPUSHs(sv_2mortal(newSVpv(delim, 0))); - - PUTBACK; - if (get_cv("main::define_flag_field", 0)) - call_pv("main::define_flag_field", G_SCALAR); - SPAGAIN; - PUTBACK; - FREETMPS; - LEAVE; -} - -static void define_event_symbols(struct tep_event *event, - const char *ev_name, - struct tep_print_arg *args) -{ - if (args == NULL) - return; - - switch (args->type) { - case TEP_PRINT_NULL: - break; - case TEP_PRINT_ATOM: - define_flag_value(ev_name, cur_field_name, "0", - args->atom.atom); - zero_flag_atom = 0; - break; - case TEP_PRINT_FIELD: - free(cur_field_name); - cur_field_name = strdup(args->field.name); - break; - case TEP_PRINT_FLAGS: - define_event_symbols(event, ev_name, args->flags.field); - define_flag_field(ev_name, cur_field_name, args->flags.delim); - define_flag_values(args->flags.flags, ev_name, cur_field_name); - break; - case TEP_PRINT_SYMBOL: - define_event_symbols(event, ev_name, args->symbol.field); - define_symbolic_field(ev_name, cur_field_name); - define_symbolic_values(args->symbol.symbols, ev_name, - cur_field_name); - break; - case TEP_PRINT_HEX: - case TEP_PRINT_HEX_STR: - define_event_symbols(event, ev_name, args->hex.field); - define_event_symbols(event, ev_name, args->hex.size); - break; - case TEP_PRINT_INT_ARRAY: - define_event_symbols(event, ev_name, args->int_array.field); - define_event_symbols(event, ev_name, args->int_array.count); - define_event_symbols(event, ev_name, args->int_array.el_size); - break; - case TEP_PRINT_BSTRING: - case TEP_PRINT_DYNAMIC_ARRAY: - case TEP_PRINT_DYNAMIC_ARRAY_LEN: - case TEP_PRINT_STRING: - case TEP_PRINT_BITMASK: - break; - case TEP_PRINT_TYPE: - define_event_symbols(event, ev_name, args->typecast.item); - break; - case TEP_PRINT_OP: - if (strcmp(args->op.op, ":") == 0) - zero_flag_atom = 1; - define_event_symbols(event, ev_name, args->op.left); - define_event_symbols(event, ev_name, args->op.right); - break; - case TEP_PRINT_FUNC: - default: - pr_err("Unsupported print arg type\n"); - /* we should warn... */ - return; - } - - if (args->next) - define_event_symbols(event, ev_name, args->next); -} - -static SV *perl_process_callchain(struct perf_sample *sample, - struct addr_location *al) -{ - struct callchain_cursor *cursor; - AV *list; - - list = newAV(); - if (!list) - goto exit; - - if (!symbol_conf.use_callchain || !sample->callchain) - goto exit; - - cursor = get_tls_callchain_cursor(); - - if (thread__resolve_callchain(al->thread, cursor, - sample, NULL, NULL, scripting_max_stack) != 0) { - pr_err("Failed to resolve callchain. Skipping\n"); - goto exit; - } - callchain_cursor_commit(cursor); - - - while (1) { - HV *elem; - struct callchain_cursor_node *node; - node = callchain_cursor_current(cursor); - if (!node) - break; - - elem = newHV(); - if (!elem) - goto exit; - - if (!hv_stores(elem, "ip", newSVuv(node->ip))) { - hv_undef(elem); - goto exit; - } - - if (node->ms.sym) { - HV *sym = newHV(); - if (!sym) { - hv_undef(elem); - goto exit; - } - if (!hv_stores(sym, "start", newSVuv(node->ms.sym->start)) || - !hv_stores(sym, "end", newSVuv(node->ms.sym->end)) || - !hv_stores(sym, "binding", newSVuv(symbol__binding(node->ms.sym))) || - !hv_stores(sym, "name", newSVpvn(node->ms.sym->name, - node->ms.sym->namelen)) || - !hv_stores(elem, "sym", newRV_noinc((SV*)sym))) { - hv_undef(sym); - hv_undef(elem); - goto exit; - } - } - - if (node->ms.map) { - struct map *map = node->ms.map; - struct dso *dso = map ? map__dso(map) : NULL; - const char *dsoname = "[unknown]"; - - if (dso) { - if (symbol_conf.show_kernel_path && dso__long_name(dso)) - dsoname = dso__long_name(dso); - else - dsoname = dso__name(dso); - } - if (!hv_stores(elem, "dso", newSVpv(dsoname,0))) { - hv_undef(elem); - goto exit; - } - } - - callchain_cursor_advance(cursor); - av_push(list, newRV_noinc((SV*)elem)); - } - -exit: - return newRV_noinc((SV*)list); -} - -static void perl_process_tracepoint(struct perf_sample *sample, - struct addr_location *al) -{ - struct thread *thread = al->thread; - struct tep_event *event; - struct tep_format_field *field; - static char handler[256]; - unsigned long long val; - unsigned long s, ns; - int pid; - int cpu = sample->cpu; - void *data = sample->raw_data; - unsigned long long nsecs = sample->time; - const char *comm = thread__comm_str(thread); - DECLARE_BITMAP(events_defined, TRACE_EVENT_TYPE_MAX); - struct evsel *evsel = sample->evsel; - - bitmap_zero(events_defined, TRACE_EVENT_TYPE_MAX); - dSP; - - if (evsel->core.attr.type != PERF_TYPE_TRACEPOINT) - return; - - event = evsel__tp_format(evsel); - if (!event) { - pr_debug("ug! no event found for type %" PRIu64, (u64)evsel->core.attr.config); - return; - } - - pid = raw_field_value(event, "common_pid", data); - - sprintf(handler, "%s::%s", event->system, event->name); - - if (!__test_and_set_bit(event->id, events_defined)) - define_event_symbols(event, handler, event->print_fmt.args); - - s = nsecs / NSEC_PER_SEC; - ns = nsecs - s * NSEC_PER_SEC; - - ENTER; - SAVETMPS; - PUSHMARK(SP); - - XPUSHs(sv_2mortal(newSVpv(handler, 0))); - XPUSHs(sv_2mortal(newSViv(PTR2IV(scripting_context)))); - XPUSHs(sv_2mortal(newSVuv(cpu))); - XPUSHs(sv_2mortal(newSVuv(s))); - XPUSHs(sv_2mortal(newSVuv(ns))); - XPUSHs(sv_2mortal(newSViv(pid))); - XPUSHs(sv_2mortal(newSVpv(comm, 0))); - XPUSHs(sv_2mortal(perl_process_callchain(sample, al))); - - /* common fields other than pid can be accessed via xsub fns */ - - for (field = event->format.fields; field; field = field->next) { - if (field->flags & TEP_FIELD_IS_STRING) { - int offset; - if (field->flags & TEP_FIELD_IS_DYNAMIC) { - offset = *(int *)(data + field->offset); - offset &= 0xffff; - if (tep_field_is_relative(field->flags)) - offset += field->offset + field->size; - } else - offset = field->offset; - XPUSHs(sv_2mortal(newSVpv((char *)data + offset, 0))); - } else { /* FIELD_IS_NUMERIC */ - val = read_size(event, data + field->offset, - field->size); - if (field->flags & TEP_FIELD_IS_SIGNED) { - XPUSHs(sv_2mortal(newSViv(val))); - } else { - XPUSHs(sv_2mortal(newSVuv(val))); - } - } - } - - PUTBACK; - - if (get_cv(handler, 0)) - call_pv(handler, G_SCALAR); - else if (get_cv("main::trace_unhandled", 0)) { - XPUSHs(sv_2mortal(newSVpv(handler, 0))); - XPUSHs(sv_2mortal(newSViv(PTR2IV(scripting_context)))); - XPUSHs(sv_2mortal(newSVuv(cpu))); - XPUSHs(sv_2mortal(newSVuv(nsecs))); - XPUSHs(sv_2mortal(newSViv(pid))); - XPUSHs(sv_2mortal(newSVpv(comm, 0))); - XPUSHs(sv_2mortal(perl_process_callchain(sample, al))); - call_pv("main::trace_unhandled", G_SCALAR); - } - SPAGAIN; - PUTBACK; - FREETMPS; - LEAVE; -} - -static void perl_process_event_generic(union perf_event *event, struct perf_sample *sample) -{ - dSP; - - if (!get_cv("process_event", 0)) - return; - - ENTER; - SAVETMPS; - PUSHMARK(SP); - XPUSHs(sv_2mortal(newSVpvn((const char *)event, event->header.size))); - XPUSHs(sv_2mortal(newSVpvn((const char *)&sample->evsel->core.attr, - sizeof(sample->evsel->core.attr)))); - XPUSHs(sv_2mortal(newSVpvn((const char *)sample, sizeof(*sample)))); - XPUSHs(sv_2mortal(newSVpvn((const char *)sample->raw_data, sample->raw_size))); - PUTBACK; - call_pv("process_event", G_SCALAR); - SPAGAIN; - PUTBACK; - FREETMPS; - LEAVE; -} - -static void perl_process_event(union perf_event *event, - struct perf_sample *sample, - struct addr_location *al, - struct addr_location *addr_al) -{ - scripting_context__update(scripting_context, event, sample, al, addr_al); - perl_process_tracepoint(sample, al); - perl_process_event_generic(event, sample); -} - -static void run_start_sub(void) -{ - dSP; /* access to Perl stack */ - PUSHMARK(SP); - - if (get_cv("main::trace_begin", 0)) - call_pv("main::trace_begin", G_DISCARD | G_NOARGS); -} - -/* - * Start trace script - */ -static int perl_start_script(const char *script, int argc, const char **argv, - struct perf_session *session) -{ - const char **command_line; - int i, err = 0; - - scripting_context->session = session; - - command_line = malloc((argc + 2) * sizeof(const char *)); - if (!command_line) - return -ENOMEM; - - command_line[0] = ""; - command_line[1] = script; - for (i = 2; i < argc + 2; i++) - command_line[i] = argv[i - 2]; - - my_perl = perl_alloc(); - perl_construct(my_perl); - - if (perl_parse(my_perl, xs_init, argc + 2, (char **)command_line, - (char **)NULL)) { - err = -1; - goto error; - } - - if (perl_run(my_perl)) { - err = -1; - goto error; - } - - if (SvTRUE(ERRSV)) { - err = -1; - goto error; - } - - run_start_sub(); - - free(command_line); - return 0; -error: - perl_free(my_perl); - free(command_line); - - return err; -} - -static int perl_flush_script(void) -{ - return 0; -} - -/* - * Stop trace script - */ -static int perl_stop_script(void) -{ - dSP; /* access to Perl stack */ - PUSHMARK(SP); - - if (get_cv("main::trace_end", 0)) - call_pv("main::trace_end", G_DISCARD | G_NOARGS); - - perl_destruct(my_perl); - perl_free(my_perl); - - return 0; -} - -static int perl_generate_script(struct tep_handle *pevent, const char *outfile) -{ - int i, not_first, count, nr_events; - struct tep_event **all_events; - struct tep_event *event = NULL; - struct tep_format_field *f; - char fname[PATH_MAX]; - FILE *ofp; - - sprintf(fname, "%s.pl", outfile); - ofp = fopen(fname, "w"); - if (ofp == NULL) { - fprintf(stderr, "couldn't open %s\n", fname); - return -1; - } - - fprintf(ofp, "# perf script event handlers, " - "generated by perf script -g perl\n"); - - fprintf(ofp, "# Licensed under the terms of the GNU GPL" - " License version 2\n\n"); - - fprintf(ofp, "# The common_* event handler fields are the most useful " - "fields common to\n"); - - fprintf(ofp, "# all events. They don't necessarily correspond to " - "the 'common_*' fields\n"); - - fprintf(ofp, "# in the format files. Those fields not available as " - "handler params can\n"); - - fprintf(ofp, "# be retrieved using Perl functions of the form " - "common_*($context).\n"); - - fprintf(ofp, "# See Context.pm for the list of available " - "functions.\n\n"); - - fprintf(ofp, "use lib \"$ENV{'PERF_EXEC_PATH'}/scripts/perl/" - "Perf-Trace-Util/lib\";\n"); - - fprintf(ofp, "use lib \"./Perf-Trace-Util/lib\";\n"); - fprintf(ofp, "use Perf::Trace::Core;\n"); - fprintf(ofp, "use Perf::Trace::Context;\n"); - fprintf(ofp, "use Perf::Trace::Util;\n\n"); - - fprintf(ofp, "sub trace_begin\n{\n\t# optional\n}\n\n"); - fprintf(ofp, "sub trace_end\n{\n\t# optional\n}\n"); - - - fprintf(ofp, "\n\ -sub print_backtrace\n\ -{\n\ - my $callchain = shift;\n\ - for my $node (@$callchain)\n\ - {\n\ - if(exists $node->{sym})\n\ - {\n\ - printf( \"\\t[\\%%x] \\%%s\\n\", $node->{ip}, $node->{sym}{name});\n\ - }\n\ - else\n\ - {\n\ - printf( \"\\t[\\%%x]\\n\", $node{ip});\n\ - }\n\ - }\n\ -}\n\n\ -"); - - nr_events = tep_get_events_count(pevent); - all_events = tep_list_events(pevent, TEP_EVENT_SORT_ID); - - for (i = 0; all_events && i < nr_events; i++) { - event = all_events[i]; - fprintf(ofp, "sub %s::%s\n{\n", event->system, event->name); - fprintf(ofp, "\tmy ("); - - fprintf(ofp, "$event_name, "); - fprintf(ofp, "$context, "); - fprintf(ofp, "$common_cpu, "); - fprintf(ofp, "$common_secs, "); - fprintf(ofp, "$common_nsecs,\n"); - fprintf(ofp, "\t $common_pid, "); - fprintf(ofp, "$common_comm, "); - fprintf(ofp, "$common_callchain,\n\t "); - - not_first = 0; - count = 0; - - for (f = event->format.fields; f; f = f->next) { - if (not_first++) - fprintf(ofp, ", "); - if (++count % 5 == 0) - fprintf(ofp, "\n\t "); - - fprintf(ofp, "$%s", f->name); - } - fprintf(ofp, ") = @_;\n\n"); - - fprintf(ofp, "\tprint_header($event_name, $common_cpu, " - "$common_secs, $common_nsecs,\n\t " - "$common_pid, $common_comm, $common_callchain);\n\n"); - - fprintf(ofp, "\tprintf(\""); - - not_first = 0; - count = 0; - - for (f = event->format.fields; f; f = f->next) { - if (not_first++) - fprintf(ofp, ", "); - if (count && count % 4 == 0) { - fprintf(ofp, "\".\n\t \""); - } - count++; - - fprintf(ofp, "%s=", f->name); - if (f->flags & TEP_FIELD_IS_STRING || - f->flags & TEP_FIELD_IS_FLAG || - f->flags & TEP_FIELD_IS_SYMBOLIC) - fprintf(ofp, "%%s"); - else if (f->flags & TEP_FIELD_IS_SIGNED) - fprintf(ofp, "%%d"); - else - fprintf(ofp, "%%u"); - } - - fprintf(ofp, "\\n\",\n\t "); - - not_first = 0; - count = 0; - - for (f = event->format.fields; f; f = f->next) { - if (not_first++) - fprintf(ofp, ", "); - - if (++count % 5 == 0) - fprintf(ofp, "\n\t "); - - if (f->flags & TEP_FIELD_IS_FLAG) { - if ((count - 1) % 5 != 0) { - fprintf(ofp, "\n\t "); - count = 4; - } - fprintf(ofp, "flag_str(\""); - fprintf(ofp, "%s::%s\", ", event->system, - event->name); - fprintf(ofp, "\"%s\", $%s)", f->name, - f->name); - } else if (f->flags & TEP_FIELD_IS_SYMBOLIC) { - if ((count - 1) % 5 != 0) { - fprintf(ofp, "\n\t "); - count = 4; - } - fprintf(ofp, "symbol_str(\""); - fprintf(ofp, "%s::%s\", ", event->system, - event->name); - fprintf(ofp, "\"%s\", $%s)", f->name, - f->name); - } else - fprintf(ofp, "$%s", f->name); - } - - fprintf(ofp, ");\n\n"); - - fprintf(ofp, "\tprint_backtrace($common_callchain);\n"); - - fprintf(ofp, "}\n\n"); - } - - fprintf(ofp, "sub trace_unhandled\n{\n\tmy ($event_name, $context, " - "$common_cpu, $common_secs, $common_nsecs,\n\t " - "$common_pid, $common_comm, $common_callchain) = @_;\n\n"); - - fprintf(ofp, "\tprint_header($event_name, $common_cpu, " - "$common_secs, $common_nsecs,\n\t $common_pid, " - "$common_comm, $common_callchain);\n"); - fprintf(ofp, "\tprint_backtrace($common_callchain);\n"); - fprintf(ofp, "}\n\n"); - - fprintf(ofp, "sub print_header\n{\n" - "\tmy ($event_name, $cpu, $secs, $nsecs, $pid, $comm) = @_;\n\n" - "\tprintf(\"%%-20s %%5u %%05u.%%09u %%8u %%-20s \",\n\t " - "$event_name, $cpu, $secs, $nsecs, $pid, $comm);\n}\n"); - - fprintf(ofp, - "\n# Packed byte string args of process_event():\n" - "#\n" - "# $event:\tunion perf_event\tutil/event.h\n" - "# $attr:\tstruct perf_event_attr\tlinux/perf_event.h\n" - "# $sample:\tstruct perf_sample\tutil/event.h\n" - "# $raw_data:\tperf_sample->raw_data\tutil/event.h\n" - "\n" - "sub process_event\n" - "{\n" - "\tmy ($event, $attr, $sample, $raw_data) = @_;\n" - "\n" - "\tmy @event\t= unpack(\"LSS\", $event);\n" - "\tmy @attr\t= unpack(\"LLQQQQQLLQQ\", $attr);\n" - "\tmy @sample\t= unpack(\"QLLQQQQQLL\", $sample);\n" - "\tmy @raw_data\t= unpack(\"C*\", $raw_data);\n" - "\n" - "\tuse Data::Dumper;\n" - "\tprint Dumper \\@event, \\@attr, \\@sample, \\@raw_data;\n" - "}\n"); - - fclose(ofp); - - fprintf(stderr, "generated Perl script: %s\n", fname); - - return 0; -} - -struct scripting_ops perl_scripting_ops = { - .name = "Perl", - .dirname = "perl", - .start_script = perl_start_script, - .flush_script = perl_flush_script, - .stop_script = perl_stop_script, - .process_event = perl_process_event, - .generate_script = perl_generate_script, -}; diff --git a/tools/perf/util/trace-event-parse.c b/tools/perf/util/trace-event-parse.c index 9c015fc2bcfb..374cf82fd86e 100644 --- a/tools/perf/util/trace-event-parse.c +++ b/tools/perf/util/trace-event-parse.c @@ -14,71 +14,6 @@ #include #include -static int get_common_field(struct scripting_context *context, - int *offset, int *size, const char *type) -{ - struct tep_handle *pevent = context->pevent; - struct tep_event *event; - struct tep_format_field *field; - - if (!*size) { - - event = tep_get_first_event(pevent); - if (!event) - return 0; - - field = tep_find_common_field(event, type); - if (!field) - return 0; - *offset = field->offset; - *size = field->size; - } - - return tep_read_number(pevent, context->event_data + *offset, *size); -} - -int common_lock_depth(struct scripting_context *context) -{ - static int offset; - static int size; - int ret; - - ret = get_common_field(context, &size, &offset, - "common_lock_depth"); - if (ret < 0) - return -1; - - return ret; -} - -int common_flags(struct scripting_context *context) -{ - static int offset; - static int size; - int ret; - - ret = get_common_field(context, &size, &offset, - "common_flags"); - if (ret < 0) - return -1; - - return ret; -} - -int common_pc(struct scripting_context *context) -{ - static int offset; - static int size; - int ret; - - ret = get_common_field(context, &size, &offset, - "common_preempt_count"); - if (ret < 0) - return -1; - - return ret; -} - unsigned long long raw_field_value(struct tep_event *event, const char *name, void *data) { diff --git a/tools/perf/util/trace-event-scripting.c b/tools/perf/util/trace-event-scripting.c deleted file mode 100644 index c78b317978dd..000000000000 --- a/tools/perf/util/trace-event-scripting.c +++ /dev/null @@ -1,398 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * trace-event-scripting. Scripting engine common and initialization code. - * - * Copyright (C) 2009-2010 Tom Zanussi - */ - -#include -#include -#include -#include -#ifdef HAVE_LIBTRACEEVENT -#include -#endif - -#include "debug.h" -#include "event.h" -#include "trace-event.h" -#include "evsel.h" -#include -#include -#include "util/sample.h" - -unsigned int scripting_max_stack = PERF_MAX_STACK_DEPTH; - -struct scripting_context *scripting_context; - -struct script_spec { - struct list_head node; - struct scripting_ops *ops; - char spec[]; -}; - -static LIST_HEAD(script_specs); - -static struct script_spec *script_spec__new(const char *spec, - struct scripting_ops *ops) -{ - struct script_spec *s = malloc(sizeof(*s) + strlen(spec) + 1); - - if (s != NULL) { - strcpy(s->spec, spec); - s->ops = ops; - } - - return s; -} - -static void script_spec__add(struct script_spec *s) -{ - list_add_tail(&s->node, &script_specs); -} - -static struct script_spec *script_spec__find(const char *spec) -{ - struct script_spec *s; - - list_for_each_entry(s, &script_specs, node) - if (strcasecmp(s->spec, spec) == 0) - return s; - return NULL; -} - -static int script_spec_register(const char *spec, struct scripting_ops *ops) -{ - struct script_spec *s; - - s = script_spec__find(spec); - if (s) - return -1; - - s = script_spec__new(spec, ops); - if (!s) - return -1; - - script_spec__add(s); - return 0; -} - -struct scripting_ops *script_spec__lookup(const char *spec) -{ - struct script_spec *s = script_spec__find(spec); - - if (!s) - return NULL; - - return s->ops; -} - -int script_spec__for_each(int (*cb)(struct scripting_ops *ops, const char *spec)) -{ - struct script_spec *s; - int ret = 0; - - list_for_each_entry(s, &script_specs, node) { - ret = cb(s->ops, s->spec); - if (ret) - break; - } - return ret; -} - -void scripting_context__update(struct scripting_context *c, - union perf_event *event, - struct perf_sample *sample, - struct addr_location *al, - struct addr_location *addr_al) -{ -#ifdef HAVE_LIBTRACEEVENT - const struct tep_event *tp_format = evsel__tp_format(sample->evsel); - - c->pevent = tp_format ? tp_format->tep : NULL; -#else - c->pevent = NULL; -#endif - c->event_data = sample->raw_data; - c->event = event; - c->sample = sample; - c->al = al; - c->addr_al = addr_al; -} - -static int flush_script_unsupported(void) -{ - return 0; -} - -static int stop_script_unsupported(void) -{ - return 0; -} - -static void process_event_unsupported(union perf_event *event __maybe_unused, - struct perf_sample *sample __maybe_unused, - struct addr_location *al __maybe_unused, - struct addr_location *addr_al __maybe_unused) -{ -} - -static void print_python_unsupported_msg(void) -{ - fprintf(stderr, "Python scripting not supported." - " Install libpython and rebuild perf to enable it.\n" - "For example:\n # apt-get install python-dev (ubuntu)" - "\n # yum install python-devel (Fedora)" - "\n etc.\n"); -} - -static int python_start_script_unsupported(const char *script __maybe_unused, - int argc __maybe_unused, - const char **argv __maybe_unused, - struct perf_session *session __maybe_unused) -{ - print_python_unsupported_msg(); - - return -1; -} - -static int python_generate_script_unsupported(struct tep_handle *pevent - __maybe_unused, - const char *outfile - __maybe_unused) -{ - print_python_unsupported_msg(); - - return -1; -} - -struct scripting_ops python_scripting_unsupported_ops = { - .name = "Python", - .dirname = "python", - .start_script = python_start_script_unsupported, - .flush_script = flush_script_unsupported, - .stop_script = stop_script_unsupported, - .process_event = process_event_unsupported, - .generate_script = python_generate_script_unsupported, -}; - -static void register_python_scripting(struct scripting_ops *scripting_ops) -{ - if (scripting_context == NULL) - scripting_context = malloc(sizeof(*scripting_context)); - - if (scripting_context == NULL || - script_spec_register("Python", scripting_ops) || - script_spec_register("py", scripting_ops)) { - pr_err("Error registering Python script extension: disabling it\n"); - zfree(&scripting_context); - } -} - -void setup_python_scripting(void) -{ - register_python_scripting(&python_scripting_unsupported_ops); -} - -#ifdef HAVE_LIBTRACEEVENT -static void print_perl_unsupported_msg(void) -{ - fprintf(stderr, "Perl scripting not supported." - " Install libperl and rebuild perf to enable it.\n" - "For example:\n # apt-get install libperl-dev (ubuntu)" - "\n # yum install 'perl(ExtUtils::Embed)' (Fedora)" - "\n etc.\n"); -} - -static int perl_start_script_unsupported(const char *script __maybe_unused, - int argc __maybe_unused, - const char **argv __maybe_unused, - struct perf_session *session __maybe_unused) -{ - print_perl_unsupported_msg(); - - return -1; -} - -static int perl_generate_script_unsupported(struct tep_handle *pevent - __maybe_unused, - const char *outfile __maybe_unused) -{ - print_perl_unsupported_msg(); - - return -1; -} - -struct scripting_ops perl_scripting_unsupported_ops = { - .name = "Perl", - .dirname = "perl", - .start_script = perl_start_script_unsupported, - .flush_script = flush_script_unsupported, - .stop_script = stop_script_unsupported, - .process_event = process_event_unsupported, - .generate_script = perl_generate_script_unsupported, -}; - -static void register_perl_scripting(struct scripting_ops *scripting_ops) -{ - if (scripting_context == NULL) - scripting_context = malloc(sizeof(*scripting_context)); - - if (scripting_context == NULL || - script_spec_register("Perl", scripting_ops) || - script_spec_register("pl", scripting_ops)) { - pr_err("Error registering Perl script extension: disabling it\n"); - zfree(&scripting_context); - } -} - -#ifndef HAVE_LIBPERL_SUPPORT -void setup_perl_scripting(void) -{ - register_perl_scripting(&perl_scripting_unsupported_ops); -} -#else -extern struct scripting_ops perl_scripting_ops; - -void setup_perl_scripting(void) -{ - register_perl_scripting(&perl_scripting_ops); -} -#endif -#endif - -static const struct { - u32 flags; - const char *name; -} sample_flags[] = { - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL, "call"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN, "return"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CONDITIONAL, "jcc"}, - {PERF_IP_FLAG_BRANCH, "jmp"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_INTERRUPT, "int"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_INTERRUPT, "iret"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_SYSCALLRET, "syscall"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_SYSCALLRET, "sysret"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_ASYNC, "async"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_ASYNC | PERF_IP_FLAG_INTERRUPT, - "hw int"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TX_ABORT, "tx abrt"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_BEGIN, "tr strt"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_END, "tr end"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMENTRY, "vmentry"}, - {PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMEXIT, "vmexit"}, - {0, NULL} -}; - -static const struct { - u32 flags; - const char *name; -} branch_events[] = { - {PERF_IP_FLAG_BRANCH_MISS, "miss"}, - {PERF_IP_FLAG_NOT_TAKEN, "not_taken"}, - {0, NULL} -}; - -static int sample_flags_to_name(u32 flags, char *str, size_t size) -{ - int i; - const char *prefix; - int pos = 0, ret, ev_idx = 0; - u32 xf = flags & PERF_ADDITIONAL_STATE_MASK; - u32 types, events; - char xs[16] = { 0 }; - - /* Clear additional state bits */ - flags &= ~PERF_ADDITIONAL_STATE_MASK; - - if (flags & PERF_IP_FLAG_TRACE_BEGIN) - prefix = "tr strt "; - else if (flags & PERF_IP_FLAG_TRACE_END) - prefix = "tr end "; - else - prefix = ""; - - ret = snprintf(str + pos, size - pos, "%s", prefix); - if (ret < 0) - return ret; - pos += ret; - - flags &= ~(PERF_IP_FLAG_TRACE_BEGIN | PERF_IP_FLAG_TRACE_END); - - types = flags & ~PERF_IP_FLAG_BRANCH_EVENT_MASK; - for (i = 0; sample_flags[i].name; i++) { - if (sample_flags[i].flags != types) - continue; - - ret = snprintf(str + pos, size - pos, "%s", sample_flags[i].name); - if (ret < 0) - return ret; - pos += ret; - break; - } - - events = flags & PERF_IP_FLAG_BRANCH_EVENT_MASK; - for (i = 0; branch_events[i].name; i++) { - if (!(branch_events[i].flags & events)) - continue; - - ret = snprintf(str + pos, size - pos, !ev_idx ? "/%s" : ",%s", - branch_events[i].name); - if (ret < 0) - return ret; - pos += ret; - ev_idx++; - } - - /* Add an end character '/' for events */ - if (ev_idx) { - ret = snprintf(str + pos, size - pos, "/"); - if (ret < 0) - return ret; - pos += ret; - } - - if (!xf) - return pos; - - snprintf(xs, sizeof(xs), "(%s%s%s)", - flags & PERF_IP_FLAG_IN_TX ? "x" : "", - flags & PERF_IP_FLAG_INTR_DISABLE ? "D" : "", - flags & PERF_IP_FLAG_INTR_TOGGLE ? "t" : ""); - - /* Right align the string if its length is less than the limit */ - if ((pos + strlen(xs)) < SAMPLE_FLAGS_STR_ALIGNED_SIZE) - ret = snprintf(str + pos, size - pos, "%*s", - (int)(SAMPLE_FLAGS_STR_ALIGNED_SIZE - ret), xs); - else - ret = snprintf(str + pos, size - pos, " %s", xs); - if (ret < 0) - return ret; - - return pos + ret; -} - -int perf_sample__sprintf_flags(u32 flags, char *str, size_t sz) -{ - const char *chars = PERF_IP_FLAG_CHARS; - const size_t n = strlen(PERF_IP_FLAG_CHARS); - size_t i, pos = 0; - int ret; - - ret = sample_flags_to_name(flags, str, sz); - if (ret > 0) - return ret; - - for (i = 0; i < n; i++, flags >>= 1) { - if ((flags & 1) && pos < sz) - str[pos++] = chars[i]; - } - for (; i < 32; i++, flags >>= 1) { - if ((flags & 1) && pos < sz) - str[pos++] = '?'; - } - if (pos < sz) - str[pos] = 0; - - return pos; -} diff --git a/tools/perf/util/trace-event.h b/tools/perf/util/trace-event.h index 1c342fce36bb..fa3824cf39fa 100644 --- a/tools/perf/util/trace-event.h +++ b/tools/perf/util/trace-event.h @@ -7,15 +7,9 @@ #include #include -struct evlist; struct machine; -struct perf_sample; -union perf_event; -struct perf_tool; -struct thread; -struct tep_plugin_list; -struct evsel; struct tep_format_field; +struct tep_plugin_list; struct trace_event { struct tep_handle *pevent; @@ -80,70 +74,6 @@ struct tracing_data *tracing_data_get(struct list_head *pattrs, int fd, bool temp); int tracing_data_put(struct tracing_data *tdata); - -struct addr_location; - -struct perf_session; -struct perf_stat_config; - -struct scripting_ops { - const char *name; - const char *dirname; /* For script path .../scripts//... */ - int (*start_script)(const char *script, int argc, const char **argv, - struct perf_session *session); - int (*flush_script) (void); - int (*stop_script) (void); - void (*process_event) (union perf_event *event, - struct perf_sample *sample, - struct addr_location *al, - struct addr_location *addr_al); - void (*process_switch)(union perf_event *event, - struct perf_sample *sample, - struct machine *machine); - void (*process_auxtrace_error)(struct perf_session *session, - union perf_event *event); - void (*process_stat)(struct perf_stat_config *config, - struct evsel *evsel, u64 tstamp); - void (*process_stat_interval)(u64 tstamp); - void (*process_throttle)(union perf_event *event, - struct perf_sample *sample, - struct machine *machine); - int (*generate_script) (struct tep_handle *pevent, const char *outfile); -}; - -extern unsigned int scripting_max_stack; - -struct scripting_ops *script_spec__lookup(const char *spec); -int script_spec__for_each(int (*cb)(struct scripting_ops *ops, const char *spec)); - -void setup_perl_scripting(void); -void setup_python_scripting(void); - -struct scripting_context { - struct tep_handle *pevent; - void *event_data; - union perf_event *event; - struct perf_sample *sample; - struct addr_location *al; - struct addr_location *addr_al; - struct perf_session *session; -}; - -void scripting_context__update(struct scripting_context *scripting_context, - union perf_event *event, - struct perf_sample *sample, - struct addr_location *al, - struct addr_location *addr_al); - -int common_pc(struct scripting_context *context); -int common_flags(struct scripting_context *context); -int common_lock_depth(struct scripting_context *context); - -#define SAMPLE_FLAGS_BUF_SIZE 64 -#define SAMPLE_FLAGS_STR_ALIGNED_SIZE 21 - -int perf_sample__sprintf_flags(u32 flags, char *str, size_t sz); - #if defined(LIBTRACEEVENT_VERSION) && LIBTRACEEVENT_VERSION >= MAKE_LIBTRACEEVENT_VERSION(1, 5, 0) #include From d9ad300f52657e5ee1000476053260f22e21bea3 Mon Sep 17 00:00:00 2001 From: Ian Rogers Date: Fri, 25 Sep 2026 23:20:16 -0700 Subject: [PATCH 224/425] perf Documentation: Update for standalone Python scripts Update perf-script documentation to reflect standalone Python script execution and the removal of embedded Python and Perl scripting: - Remove documentation for the removed -g and -s options and legacy record/report script wrapper modes in perf-script.txt. - Remove references to perf-script-perl and delete obsolete perf-script-perl.txt. - Rewrite perf-script-python.txt to document writing and running standalone Python scripts using the perf module. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers Signed-off-by: Arnaldo Carvalho de Melo --- tools/perf/Documentation/db-export.txt | 16 +- tools/perf/Documentation/perf-script-perl.txt | 216 ----- .../perf/Documentation/perf-script-python.txt | 752 +++--------------- tools/perf/Documentation/perf-script.txt | 82 +- tools/perf/Documentation/tips.txt | 1 - 5 files changed, 140 insertions(+), 927 deletions(-) delete mode 100644 tools/perf/Documentation/perf-script-perl.txt diff --git a/tools/perf/Documentation/db-export.txt b/tools/perf/Documentation/db-export.txt index 20024e1f9164..a736be3b6e84 100644 --- a/tools/perf/Documentation/db-export.txt +++ b/tools/perf/Documentation/db-export.txt @@ -1,11 +1,11 @@ Database Export =============== -perf tool's python scripting engine: +perf tool's python module: - tools/perf/util/scripting-engines/trace-event-python.c + tools/perf/util/python.c -supports scripts: +supports standalone scripts: tools/perf/python/export-to-sqlite.py tools/perf/python/export-to-postgresql.py @@ -31,11 +31,5 @@ backward compatibility by testing for the presence of new tables and columns before using them. e.g. function IsSelectable() in exported-sql-viewer.py 4. The export scripts themselves maintain forward compatibility (i.e. an existing -script will continue to work with new versions of perf) by accepting a variable -number of arguments (e.g. def call_return_table(*x)) i.e. perf can pass more -arguments which old scripts will ignore. - -5. The scripting engine tests for the existence of script handler functions -before calling them. The scripting engine can also test for the support of new -or optional features by checking for the existence and value of script global -variables. +script will continue to work with new versions of perf) by querying event and +sample attributes dynamically from the perf Python module. diff --git a/tools/perf/Documentation/perf-script-perl.txt b/tools/perf/Documentation/perf-script-perl.txt deleted file mode 100644 index 5b479f5e62ff..000000000000 --- a/tools/perf/Documentation/perf-script-perl.txt +++ /dev/null @@ -1,216 +0,0 @@ -perf-script-perl(1) -=================== - -NAME ----- -perf-script-perl - Process trace data with a Perl script - -SYNOPSIS --------- -[verse] -'perf script' [-s [Perl]:script[.pl] ] - -DESCRIPTION ------------ - -This perf script option is used to process perf script data using perf's -built-in Perl interpreter. It reads and processes the input file and -displays the results of the trace analysis implemented in the given -Perl script, if any. - -STARTER SCRIPTS ---------------- - -You can avoid reading the rest of this document by running 'perf script --g perl' in the same directory as an existing perf.data trace file. -That will generate a starter script containing a handler for each of -the event types in the trace file; it simply prints every available -field for each event in the trace file. - -You can also look at the existing scripts in -~/libexec/perf-core/scripts/perl for typical examples showing how to -do basic things like aggregate event data, print results, etc. Also, -the check-perf-script.pl script, while not interesting for its results, -attempts to exercise all of the main scripting features. - -EVENT HANDLERS --------------- - -When perf script is invoked using a trace script, a user-defined -'handler function' is called for each event in the trace. If there's -no handler function defined for a given event type, the event is -ignored (or passed to a 'trace_unhandled' function, see below) and the -next event is processed. - -Most of the event's field values are passed as arguments to the -handler function; some of the less common ones aren't - those are -available as calls back into the perf executable (see below). - -As an example, the following perf record command can be used to record -all sched_wakeup events in the system: - - # perf record -a -e sched:sched_wakeup - -Traces meant to be processed using a script should be recorded with -the above option: -a to enable system-wide collection. - -The format file for the sched_wakeup event defines the following fields -(see /sys/kernel/tracing/events/sched/sched_wakeup/format): - ----- - format: - field:unsigned short common_type; - field:unsigned char common_flags; - field:unsigned char common_preempt_count; - field:int common_pid; - - field:char comm[TASK_COMM_LEN]; - field:pid_t pid; - field:int prio; - field:int success; - field:int target_cpu; ----- - -The handler function for this event would be defined as: - ----- -sub sched::sched_wakeup -{ - my ($event_name, $context, $common_cpu, $common_secs, - $common_nsecs, $common_pid, $common_comm, - $comm, $pid, $prio, $success, $target_cpu) = @_; -} ----- - -The handler function takes the form subsystem::event_name. - -The $common_* arguments in the handler's argument list are the set of -arguments passed to all event handlers; some of the fields correspond -to the common_* fields in the format file, but some are synthesized, -and some of the common_* fields aren't common enough to to be passed -to every event as arguments but are available as library functions. - -Here's a brief description of each of the invariant event args: - - $event_name the name of the event as text - $context an opaque 'cookie' used in calls back into perf - $common_cpu the cpu the event occurred on - $common_secs the secs portion of the event timestamp - $common_nsecs the nsecs portion of the event timestamp - $common_pid the pid of the current task - $common_comm the name of the current process - -All of the remaining fields in the event's format file have -counterparts as handler function arguments of the same name, as can be -seen in the example above. - -The above provides the basics needed to directly access every field of -every event in a trace, which covers 90% of what you need to know to -write a useful trace script. The sections below cover the rest. - -SCRIPT LAYOUT -------------- - -Every perf script Perl script should start by setting up a Perl module -search path and 'use'ing a few support modules (see module -descriptions below): - ----- - use lib "$ENV{'PERF_EXEC_PATH'}/scripts/perl/Perf-Trace-Util/lib"; - use lib "./Perf-Trace-Util/lib"; - use Perf::Trace::Core; - use Perf::Trace::Context; - use Perf::Trace::Util; ----- - -The rest of the script can contain handler functions and support -functions in any order. - -Aside from the event handler functions discussed above, every script -can implement a set of optional functions: - -*trace_begin*, if defined, is called before any event is processed and -gives scripts a chance to do setup tasks: - ----- - sub trace_begin - { - } ----- - -*trace_end*, if defined, is called after all events have been - processed and gives scripts a chance to do end-of-script tasks, such - as display results: - ----- -sub trace_end -{ -} ----- - -*trace_unhandled*, if defined, is called after for any event that - doesn't have a handler explicitly defined for it. The standard set - of common arguments are passed into it: - ----- -sub trace_unhandled -{ - my ($event_name, $context, $common_cpu, $common_secs, - $common_nsecs, $common_pid, $common_comm) = @_; -} ----- - -The remaining sections provide descriptions of each of the available -built-in perf script Perl modules and their associated functions. - -AVAILABLE MODULES AND FUNCTIONS -------------------------------- - -The following sections describe the functions and variables available -via the various Perf::Trace::* Perl modules. To use the functions and -variables from the given module, add the corresponding 'use -Perf::Trace::XXX' line to your perf script script. - -Perf::Trace::Core Module -~~~~~~~~~~~~~~~~~~~~~~~~ - -These functions provide some essential functions to user scripts. - -The *flag_str* and *symbol_str* functions provide human-readable -strings for flag and symbolic fields. These correspond to the strings -and values parsed from the 'print fmt' fields of the event format -files: - - flag_str($event_name, $field_name, $field_value) - returns the string representation corresponding to $field_value for the flag field $field_name of event $event_name - symbol_str($event_name, $field_name, $field_value) - returns the string representation corresponding to $field_value for the symbolic field $field_name of event $event_name - -Perf::Trace::Context Module -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some of the 'common' fields in the event format file aren't all that -common, but need to be made accessible to user scripts nonetheless. - -Perf::Trace::Context defines a set of functions that can be used to -access this data in the context of the current event. Each of these -functions expects a $context variable, which is the same as the -$context variable passed into every event handler as the second -argument. - - common_pc($context) - returns common_preempt count for the current event - common_flags($context) - returns common_flags for the current event - common_lock_depth($context) - returns common_lock_depth for the current event - -Perf::Trace::Util Module -~~~~~~~~~~~~~~~~~~~~~~~~ - -Various utility functions for use with perf script: - - nsecs($secs, $nsecs) - returns total nsecs given secs/nsecs pair - nsecs_secs($nsecs) - returns whole secs portion given nsecs - nsecs_nsecs($nsecs) - returns nsecs remainder given nsecs - nsecs_str($nsecs) - returns printable string in the form secs.nsecs - avg($total, $n) - returns average given a sum and a total number of values - -SEE ALSO --------- -linkperf:perf-script[1] diff --git a/tools/perf/Documentation/perf-script-python.txt b/tools/perf/Documentation/perf-script-python.txt index 27a1cac6fe76..cbf50b3e5f06 100644 --- a/tools/perf/Documentation/perf-script-python.txt +++ b/tools/perf/Documentation/perf-script-python.txt @@ -3,676 +3,182 @@ perf-script-python(1) NAME ---- -perf-script-python - Process trace data with a Python script +perf-script-python - Process trace data with a Python script using perf module SYNOPSIS -------- [verse] -'perf script' [-s [Python]:script[.py] ] +'perf script'