* [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC
@ 2026-08-03 9:06 Amir Ayupov
2026-08-03 9:06 ` [PATCH 2/9] perf thread-stack: Report branch stack hw_idx as not available Amir Ayupov
` (7 more replies)
0 siblings, 8 replies; 13+ messages in thread
From: Amir Ayupov @ 2026-08-03 9:06 UTC (permalink / raw)
To: linux-perf-users, coresight, linux-arm-kernel, Suzuki K Poulose,
James Clark, Leo Yan, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon
Cc: linux-doc, Mike Leach, Jonathan Corbet, Shuah Khan,
Swapnil Sapkal
process_group_desc() rejects the entire perf.data file ("invalid group
desc" -> "incompatible file format") whenever the group description is
inconsistent with the event list. Group information is optional metadata
and is not needed to decode samples, so a single bad group descriptor
should not make an otherwise valid file unreadable.
This is observable with AUX area recordings (Arm CoreSight ETM, Intel PT)
that use aux-action pause/resume: the aux-action regrouping inflates the
AUX group leader's nr_members, producing a group descriptor that the
strict reader rejects, even though the file is otherwise fine (older perf
and other tooling read it by rebuilding groups from event records).
Warn and fall back to a consistent ungrouped event list instead of
failing the read.
Signed-off-by: Amir Ayupov <aaupov@fb.com>
---
| 42 +++++++++++++++++++++++++++++++---------
1 file changed, 33 insertions(+), 9 deletions(-)
--git a/tools/perf/util/header.c b/tools/perf/util/header.c
index e90e541f546b4..ddc59624d639b 100644
--- a/tools/perf/util/header.c
+++ b/tools/perf/util/header.c
@@ -3373,6 +3373,19 @@ static int process_group_desc(struct feat_fd *ff, void *data __maybe_unused)
i = nr = 0;
evlist__for_each_entry(session->evlist, evsel) {
if (i < nr_groups && evsel->core.idx == (int) desc[i].leader_idx) {
+ if (!desc[i].nr_members)
+ goto out_inconsistent;
+
+ if (nr > 0) {
+ /*
+ * A new leader was found before the previous
+ * group's members were all consumed, so the
+ * group description is inconsistent with the
+ * event list.
+ */
+ goto out_inconsistent;
+ }
+
evsel__set_leader(evsel, evsel);
/* {anon_group} is a dummy name */
if (strcmp(desc[i].name, "{anon_group}")) {
@@ -3381,11 +3394,6 @@ static int process_group_desc(struct feat_fd *ff, void *data __maybe_unused)
}
evsel->core.nr_members = desc[i].nr_members;
- if (i >= nr_groups || nr > 0) {
- pr_debug("invalid group desc\n");
- goto out_free;
- }
-
leader = evsel;
nr = evsel->core.nr_members - 1;
i++;
@@ -3397,10 +3405,8 @@ static int process_group_desc(struct feat_fd *ff, void *data __maybe_unused)
}
}
- if (i != nr_groups || nr != 0) {
- pr_debug("invalid group desc\n");
- goto out_free;
- }
+ if (i != nr_groups || nr != 0)
+ goto out_inconsistent;
ret = 0;
out_free:
@@ -3409,6 +3415,24 @@ static int process_group_desc(struct feat_fd *ff, void *data __maybe_unused)
free(desc);
return ret;
+
+out_inconsistent:
+ /*
+ * Group information is optional metadata and is not required to decode
+ * samples. Rather than rejecting the whole file, warn and fall back to
+ * a consistent ungrouped event list. This can happen with otherwise
+ * valid perf.data files, e.g. AUX area (Intel PT, Arm CoreSight ETM)
+ * recordings using aux-action pause/resume.
+ */
+ pr_warning("Inconsistent HEADER_GROUP_DESC, ignoring group information\n");
+ env->nr_groups = 0;
+ evlist__for_each_entry(session->evlist, evsel) {
+ evsel__set_leader(evsel, evsel);
+ evsel->core.nr_members = 1;
+ zfree(&evsel->group_name);
+ }
+ ret = 0;
+ goto out_free;
}
static int process_auxtrace(struct feat_fd *ff, void *data __maybe_unused)
--
2.52.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH 2/9] perf thread-stack: Report branch stack hw_idx as not available
2026-08-03 9:06 [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC Amir Ayupov
@ 2026-08-03 9:06 ` Amir Ayupov
2026-08-03 9:19 ` sashiko-bot
2026-08-03 9:06 ` [PATCH 3/9] perf thread-stack: Bound wrapped branch stack copy Amir Ayupov
` (6 subsequent siblings)
7 siblings, 1 reply; 13+ messages in thread
From: Amir Ayupov @ 2026-08-03 9:06 UTC (permalink / raw)
To: linux-perf-users, coresight, linux-arm-kernel, Suzuki K Poulose,
James Clark, Leo Yan, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon
Cc: linux-doc, Mike Leach, Jonathan Corbet, Shuah Khan,
Swapnil Sapkal
thread_stack__br_sample() and thread_stack__br_sample_late() fill a
caller-supplied branch_stack that is typically allocated with zalloc(),
leaving hw_idx as 0. Zero is a valid hardware index, so consumers that
honour PERF_SAMPLE_BRANCH_HW_INDEX see a reconstructed branch stack
claiming to start at LBR TOS entry 0.
These branch stacks are reconstructed from instruction trace and have no
hardware index at all. Set hw_idx to -1ULL, which is the established way
to say "not available" and matches what intel-pt and cs-etm already put
in the branch stacks they synthesise directly.
Signed-off-by: Amir Ayupov <aaupov@fb.com>
---
tools/perf/util/thread-stack.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tools/perf/util/thread-stack.c b/tools/perf/util/thread-stack.c
index c5ce741b07446..1a3dffa83bde2 100644
--- a/tools/perf/util/thread-stack.c
+++ b/tools/perf/util/thread-stack.c
@@ -624,6 +624,7 @@ void thread_stack__br_sample(struct thread *thread, int cpu,
unsigned int nr;
dst->nr = 0;
+ dst->hw_idx = -1ULL;
if (!ts)
return;
@@ -686,6 +687,7 @@ void thread_stack__br_sample_late(struct thread *thread, int cpu,
bool start = false;
dst->nr = 0;
+ dst->hw_idx = -1ULL;
if (!ts)
return;
--
2.52.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH 3/9] perf thread-stack: Bound wrapped branch stack copy
2026-08-03 9:06 [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC Amir Ayupov
2026-08-03 9:06 ` [PATCH 2/9] perf thread-stack: Report branch stack hw_idx as not available Amir Ayupov
@ 2026-08-03 9:06 ` Amir Ayupov
2026-08-03 9:26 ` sashiko-bot
2026-08-03 9:06 ` [PATCH 4/9] perf dlfilter: Add non-empty branch stack filter Amir Ayupov
` (5 subsequent siblings)
7 siblings, 1 reply; 13+ messages in thread
From: Amir Ayupov @ 2026-08-03 9:06 UTC (permalink / raw)
To: linux-perf-users, coresight, linux-arm-kernel, Suzuki K Poulose,
James Clark, Leo Yan, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon
Cc: linux-doc, Mike Leach, Jonathan Corbet, Shuah Khan,
Swapnil Sapkal
When the internal branch ring has wrapped, thread_stack__br_sample()
computes the number of entries that still fit in the destination:
nr = min(ts->br_stack_pos, sz);
but then copies ts->br_stack_pos entries regardless, overrunning the
destination whenever sz is smaller than ts->br_stack_pos.
No caller can trigger this today: both intel-pt and cs-etm size the
thread stack ring and the output buffer from the same
synth_opts.last_branch_sz, so sz is never less than ts->br_stack_sz and
the two values always agree. It becomes reachable as soon as a caller
keeps a larger reconstruction ring than the requested output depth,
which is what --itrace=L does for late branch sampling.
Copy nr entries instead, so the destination bound is honoured whatever
the caller asks for.
Signed-off-by: Amir Ayupov <aaupov@fb.com>
---
tools/perf/util/thread-stack.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tools/perf/util/thread-stack.c b/tools/perf/util/thread-stack.c
index 1a3dffa83bde2..51eaedb47bb1d 100644
--- a/tools/perf/util/thread-stack.c
+++ b/tools/perf/util/thread-stack.c
@@ -643,7 +643,7 @@ void thread_stack__br_sample(struct thread *thread, int cpu,
sz -= nr;
be = &dst->entries[nr];
nr = min(ts->br_stack_pos, sz);
- memcpy(be, &src->entries[0], bsz * ts->br_stack_pos);
+ memcpy(be, &src->entries[0], bsz * nr);
}
}
--
2.52.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH 4/9] perf dlfilter: Add non-empty branch stack filter
2026-08-03 9:06 [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC Amir Ayupov
2026-08-03 9:06 ` [PATCH 2/9] perf thread-stack: Report branch stack hw_idx as not available Amir Ayupov
2026-08-03 9:06 ` [PATCH 3/9] perf thread-stack: Bound wrapped branch stack copy Amir Ayupov
@ 2026-08-03 9:06 ` Amir Ayupov
2026-08-03 9:06 ` [PATCH 5/9] perf cs-etm: Split up cs_etm__process_timestamped_queues() Amir Ayupov
` (4 subsequent siblings)
7 siblings, 0 replies; 13+ messages in thread
From: Amir Ayupov @ 2026-08-03 9:06 UTC (permalink / raw)
To: linux-perf-users, coresight, linux-arm-kernel, Suzuki K Poulose,
James Clark, Leo Yan, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon
Cc: linux-doc, Mike Leach, Jonathan Corbet, Shuah Khan,
Swapnil Sapkal
--itrace=L adds decoded branch history to existing samples, but a sample
that was recorded while the decoder had no trace for that thread keeps an
empty branch stack. Consumers of the resulting perf script output, such
as profile generators for context-sensitive PGO, have no use for those
samples.
Add an opt-in dlfilter that drops samples whose parsed branch stack is
empty, so users can exclude them without changing default sample
semantics. Build and install it alongside perf's existing dlfilters.
Signed-off-by: Amir Ayupov <aaupov@fb.com>
---
tools/perf/Makefile.perf | 1 +
.../dlfilters/dlfilter-nonempty-brstack.c | 26 +++++++++++++++++++
2 files changed, 27 insertions(+)
create mode 100644 tools/perf/dlfilters/dlfilter-nonempty-brstack.c
diff --git a/tools/perf/Makefile.perf b/tools/perf/Makefile.perf
index 0031112c036e8..aeb8085b0756d 100644
--- a/tools/perf/Makefile.perf
+++ b/tools/perf/Makefile.perf
@@ -427,6 +427,7 @@ PROGRAMS += $(OUTPUT)$(LIBJVMTI)
endif
DLFILTERS := dlfilter-test-api-v0.so dlfilter-test-api-v2.so dlfilter-show-cycles.so
+DLFILTERS += dlfilter-nonempty-brstack.so
DLFILTERS := $(patsubst %,$(OUTPUT)dlfilters/%,$(DLFILTERS))
# what 'all' will build and 'install' will install, in perfexecdir
diff --git a/tools/perf/dlfilters/dlfilter-nonempty-brstack.c b/tools/perf/dlfilters/dlfilter-nonempty-brstack.c
new file mode 100644
index 0000000000000..9e66205b841d5
--- /dev/null
+++ b/tools/perf/dlfilters/dlfilter-nonempty-brstack.c
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * dlfilter-nonempty-brstack.c: Filter out samples with no branch stack
+ * Copyright (c) 2026, Meta Platforms, Inc.
+ */
+#include <stddef.h>
+
+#include <perf/perf_dlfilter.h>
+
+int filter_event(void *data, const struct perf_dlfilter_sample *sample, void *ctx)
+{
+ /* Return 1 to filter out the sample, 0 to keep it */
+ return !sample->brstack_nr;
+}
+
+const char *filter_description(const char **long_description)
+{
+ static char *long_desc =
+ "Instruction trace decoders can add branch history to existing "
+ "samples, but samples that were recorded while no trace was "
+ "being collected get an empty branch stack. Filter those out so "
+ "that only samples carrying branch history remain.";
+
+ *long_description = long_desc;
+ return "Keep only samples with a non-empty branch stack";
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH 5/9] perf cs-etm: Split up cs_etm__process_timestamped_queues()
2026-08-03 9:06 [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC Amir Ayupov
` (2 preceding siblings ...)
2026-08-03 9:06 ` [PATCH 4/9] perf dlfilter: Add non-empty branch stack filter Amir Ayupov
@ 2026-08-03 9:06 ` Amir Ayupov
2026-08-03 9:06 ` [PATCH 6/9] perf cs-etm: Add branch history to existing samples Amir Ayupov
` (3 subsequent siblings)
7 siblings, 0 replies; 13+ messages in thread
From: Amir Ayupov @ 2026-08-03 9:06 UTC (permalink / raw)
To: linux-perf-users, coresight, linux-arm-kernel, Suzuki K Poulose,
James Clark, Leo Yan, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon
Cc: linux-doc, Mike Leach, Jonathan Corbet, Shuah Khan,
Swapnil Sapkal
cs_etm__process_timestamped_queues() currently does three things: it seeds
the auxtrace heap with one entry per queue, it decodes until the heap is
empty, and it then walks every traceID queue to flush whatever is left in
the branch stacks. That is fine while the only caller is
cs_etm__flush_events(), which runs once, but it does not survive the
function being called repeatedly.
Seeding cannot be repeated because a queue that still holds a heap slot
would be seeded again, adding duplicate entries and growing the heap
without bound. Flushing cannot be repeated either, because ending a block
finalises state that later trace still needs.
Move both out. Seeding becomes cs_etm__update_queues(), gated on
queues.new_data so it only runs when new AUX data has been queued, with
etmq->on_heap tracking whether a queue currently occupies a heap slot;
this mirrors intel_pt_update_queues() and intel_pt_queue::on_heap.
Flushing becomes cs_etm__flush_timestamped_queues(). What remains is the
decode loop on its own, which a later patch can then drive incrementally.
No functional change: the sole caller performs the same three steps in the
same order.
Signed-off-by: Amir Ayupov <aaupov@fb.com>
---
tools/perf/util/cs-etm.c | 71 ++++++++++++++++++++++++++++++++++------
1 file changed, 61 insertions(+), 10 deletions(-)
diff --git a/tools/perf/util/cs-etm.c b/tools/perf/util/cs-etm.c
index 114b3cd2da495..4d895f11deb7f 100644
--- a/tools/perf/util/cs-etm.c
+++ b/tools/perf/util/cs-etm.c
@@ -136,9 +136,13 @@ struct cs_etm_queue {
*/
struct intlist *own_traceid_list;
u32 sink_id;
+ /* Whether this queue currently occupies a slot in etm->heap */
+ bool on_heap;
};
+static int cs_etm__update_queues(struct cs_etm_auxtrace *etm);
static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm);
+static int cs_etm__flush_timestamped_queues(struct cs_etm_auxtrace *etm);
static int cs_etm__process_timeless_queues(struct cs_etm_auxtrace *etm,
pid_t tid);
static int cs_etm__get_data_block(struct cs_etm_queue *etmq);
@@ -939,6 +943,8 @@ static int cs_etm__flush_events(struct perf_session *session,
struct cs_etm_auxtrace *etm = container_of(session->auxtrace,
struct cs_etm_auxtrace,
auxtrace);
+ int ret;
+
if (dump_trace)
return 0;
@@ -953,7 +959,15 @@ static int cs_etm__flush_events(struct perf_session *session,
return cs_etm__process_timeless_queues(etm, -1);
}
- return cs_etm__process_timestamped_queues(etm);
+ ret = cs_etm__update_queues(etm);
+ if (ret)
+ return ret;
+
+ ret = cs_etm__process_timestamped_queues(etm);
+ if (ret)
+ return ret;
+
+ return cs_etm__flush_timestamped_queues(etm);
}
static void cs_etm__free_traceid_queues(struct cs_etm_queue *etmq)
@@ -1330,6 +1344,8 @@ static int cs_etm__queue_first_cs_timestamp(struct cs_etm_auxtrace *etm,
*/
cs_queue_nr = TO_CS_QUEUE_NR(queue_nr, trace_chan_id);
ret = auxtrace_heap__add(&etm->heap, cs_queue_nr, cs_timestamp);
+ if (!ret)
+ etmq->on_heap = true;
out:
return ret;
}
@@ -2767,23 +2783,30 @@ static int cs_etm__process_timeless_queues(struct cs_etm_auxtrace *etm,
return 0;
}
-static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
+/*
+ * Seed the heap with one entry from each queue that is not already
+ * represented in it, so that decoding proceeds in time order across all
+ * queues. Only queues that have newly queued data need to be considered.
+ */
+static int cs_etm__update_queues(struct cs_etm_auxtrace *etm)
{
int ret = 0;
- unsigned int cs_queue_nr, queue_nr, i;
- u8 trace_chan_id;
- u64 cs_timestamp;
- struct auxtrace_queue *queue;
+ unsigned int i;
struct cs_etm_queue *etmq;
- struct cs_etm_traceid_queue *tidq;
+
+ if (!etm->queues.new_data)
+ return 0;
+
+ etm->queues.new_data = false;
/*
* Pre-populate the heap with one entry from each queue so that we can
- * start processing in time order across all queues.
+ * start processing in time order across all queues. Skip queues that
+ * already occupy a heap slot, otherwise they would be added twice.
*/
for (i = 0; i < etm->queues.nr_queues; i++) {
etmq = etm->queues.queue_array[i].priv;
- if (!etmq)
+ if (!etmq || etmq->on_heap)
continue;
ret = cs_etm__queue_first_cs_timestamp(etm, etmq, i);
@@ -2791,6 +2814,19 @@ static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
return ret;
}
+ return ret;
+}
+
+static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
+{
+ int ret = 0;
+ unsigned int cs_queue_nr, queue_nr;
+ u8 trace_chan_id;
+ u64 cs_timestamp;
+ struct auxtrace_queue *queue;
+ struct cs_etm_queue *etmq;
+ struct cs_etm_traceid_queue *tidq;
+
while (1) {
if (!etm->heap.heap_cnt)
break;
@@ -2807,6 +2843,7 @@ static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
* to process it.
*/
auxtrace_heap__pop(&etm->heap);
+ etmq->on_heap = false;
tidq = cs_etm__etmq_get_traceid_queue(etmq, trace_chan_id);
if (!tidq) {
@@ -2874,7 +2911,21 @@ static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
*/
cs_queue_nr = TO_CS_QUEUE_NR(queue_nr, trace_chan_id);
ret = auxtrace_heap__add(&etm->heap, cs_queue_nr, cs_timestamp);
+ if (ret)
+ goto out;
+ etmq->on_heap = true;
}
+out:
+ return ret;
+}
+
+/* Flush any branch stack entries left over once all trace is decoded */
+static int cs_etm__flush_timestamped_queues(struct cs_etm_auxtrace *etm)
+{
+ int ret = 0;
+ unsigned int i;
+ struct cs_etm_queue *etmq;
+ struct cs_etm_traceid_queue *tidq;
for (i = 0; i < etm->queues.nr_queues; i++) {
struct int_node *inode;
@@ -2893,7 +2944,7 @@ static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
return ret;
}
}
-out:
+
return ret;
}
--
2.52.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH 6/9] perf cs-etm: Add branch history to existing samples
2026-08-03 9:06 [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC Amir Ayupov
` (3 preceding siblings ...)
2026-08-03 9:06 ` [PATCH 5/9] perf cs-etm: Split up cs_etm__process_timestamped_queues() Amir Ayupov
@ 2026-08-03 9:06 ` Amir Ayupov
2026-08-03 9:23 ` sashiko-bot
2026-08-03 9:06 ` [PATCH 7/9] perf test cs-etm: Test branch history on " Amir Ayupov
` (2 subsequent siblings)
7 siblings, 1 reply; 13+ messages in thread
From: Amir Ayupov @ 2026-08-03 9:06 UTC (permalink / raw)
To: linux-perf-users, coresight, linux-arm-kernel, Suzuki K Poulose,
James Clark, Leo Yan, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon
Cc: linux-doc, Mike Leach, Jonathan Corbet, Shuah Khan,
Swapnil Sapkal
Implement --itrace=L for CoreSight ETM: decode timestamped trace up to
each existing PMU sample and attach the branch history that led to it.
The sample keeps its own ip, callchain and event identity, and a sample
that already carries a branch stack is left alone.
Samples are correlated with the trace by time, so this requires virtual
ETM timestamps that are correlated to perf time; timeless decoding is
rejected. The decode loop, which the previous patch left on its own in
cs_etm__process_timestamped_queues(), grows a timestamp argument and
stops once the decode frontier reaches it, so on return the
thread stack holds the branches that executed before the sample and none
that executed after. Attaching then reduces to the same
thread_stack__br_sample_late() call intel-pt uses.
No explicit sample-to-queue matching is needed:
thread_stack__br_sample_late() keys on the thread, and the thread stack
is already emptied whenever the decoder reports a discontinuity. The one
case that was not covered is a queue whose trace runs out: flush the
thread stack there too, otherwise samples recorded after the last trace
would pick up stale history.
A sample that lands in a gap between two trace windows is still attached
the preceding window's branches at this point: the discontinuity that
ends the gap is only decoded once trace resumes, so the thread stack has
not been emptied yet. Whenever the trace is duty cycled that is the
common case rather than a corner case. The next patch suppresses it.
As with intel-pt, the internal reconstruction ring is kept deeper than
the requested output depth to cover branches decoded between the sampled
ip and the point at which the sample time was recorded, so --itrace=L<n>
can actually return n entries. Kernel-inclusive trace gets the same
conservative 1024-entry headroom that intel-pt uses.
Signed-off-by: Amir Ayupov <aaupov@fb.com>
---
tools/perf/util/cs-etm.c | 176 +++++++++++++++++++++++++++++++++++++--
1 file changed, 168 insertions(+), 8 deletions(-)
diff --git a/tools/perf/util/cs-etm.c b/tools/perf/util/cs-etm.c
index 4d895f11deb7f..048ff97caa936 100644
--- a/tools/perf/util/cs-etm.c
+++ b/tools/perf/util/cs-etm.c
@@ -72,6 +72,11 @@ struct cs_etm_auxtrace {
bool use_callchain;
int num_cpu;
+ /* Output depth requested with --itrace=L<n> */
+ unsigned int br_stack_sz;
+ /* Internal reconstruction depth, see cs_etm__br_stack_init() */
+ unsigned int br_stack_sz_plus;
+ struct branch_stack *br_stack;
u64 latest_kernel_timestamp;
u32 auxtrace_type;
u32 branches_filter;
@@ -91,6 +96,7 @@ struct cs_etm_traceid_queue {
u64 kernel_start;
union perf_event *event_buf;
unsigned int br_stack_sz;
+ unsigned int br_stack_sz_plus;
struct branch_stack *last_branch;
struct ip_callchain *callchain;
struct cs_etm_packet *prev_packet;
@@ -141,7 +147,8 @@ struct cs_etm_queue {
};
static int cs_etm__update_queues(struct cs_etm_auxtrace *etm);
-static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm);
+static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm,
+ u64 timestamp);
static int cs_etm__flush_timestamped_queues(struct cs_etm_auxtrace *etm);
static int cs_etm__process_timeless_queues(struct cs_etm_auxtrace *etm,
pid_t tid);
@@ -165,6 +172,7 @@ static int cs_etm__metadata_set_trace_id(u8 trace_chan_id, u64 *cpu_metadata);
#define TO_QUEUE_NR(cs_queue_nr) (cs_queue_nr >> 16)
#define TO_TRACE_CHAN_ID(cs_queue_nr) (cs_queue_nr & 0x0000ffff)
#define SINK_UNSET ((u32) -1)
+#define MAX_TIMESTAMP (~0ULL)
static u32 cs_etm__get_v7_protocol_version(u32 etmidr)
{
@@ -674,7 +682,8 @@ static int cs_etm__init_traceid_queue(struct cs_etm_queue *etmq,
if (!tidq->last_branch)
goto out_free;
- tidq->br_stack_sz = etm->synth_opts.last_branch_sz;
+ tidq->br_stack_sz = etm->br_stack_sz;
+ tidq->br_stack_sz_plus = etm->br_stack_sz_plus;
}
if (etm->synth_opts.callchain) {
@@ -794,7 +803,7 @@ static void cs_etm__packet_swap(struct cs_etm_auxtrace *etm,
struct cs_etm_packet *tmp;
if (etm->synth_opts.branches || etm->synth_opts.last_branch ||
- etm->synth_opts.instructions) {
+ etm->synth_opts.add_last_branch || etm->synth_opts.instructions) {
/*
* Swap PACKET with PREV_PACKET: PACKET becomes PREV_PACKET for
* the next incoming packet.
@@ -963,7 +972,7 @@ static int cs_etm__flush_events(struct perf_session *session,
if (ret)
return ret;
- ret = cs_etm__process_timestamped_queues(etm);
+ ret = cs_etm__process_timestamped_queues(etm, MAX_TIMESTAMP);
if (ret)
return ret;
@@ -1060,6 +1069,7 @@ static void cs_etm__free(struct perf_session *session)
zfree(&aux->metadata[i]);
zfree(&aux->metadata);
+ zfree(&aux->br_stack);
zfree(&aux);
}
@@ -1597,7 +1607,8 @@ static void cs_etm__add_stack_event(struct cs_etm_queue *etmq,
u64 from, to;
int size;
- if (!etm->synth_opts.branches && !etm->synth_opts.instructions)
+ if (!etm->synth_opts.branches && !etm->synth_opts.instructions &&
+ !etm->synth_opts.add_last_branch)
return;
if (!cs_etm__packet_has_taken_branch(tidq->prev_packet))
@@ -1614,7 +1625,7 @@ static void cs_etm__add_stack_event(struct cs_etm_queue *etmq,
tidq->prev_packet->flags, from, to, size,
etmq->buffer->buffer_nr + 1,
etmq->etm->use_callchain,
- tidq->br_stack_sz, 0);
+ tidq->br_stack_sz_plus, 0);
} else {
thread_stack__set_trace_nr(tidq->frontend_thread,
tidq->prev_packet->cpu,
@@ -2817,7 +2828,8 @@ static int cs_etm__update_queues(struct cs_etm_auxtrace *etm)
return ret;
}
-static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
+static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm,
+ u64 timestamp)
{
int ret = 0;
unsigned int cs_queue_nr, queue_nr;
@@ -2831,6 +2843,9 @@ static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
if (!etm->heap.heap_cnt)
break;
+ if (etm->heap.heap_array[0].ordinal >= timestamp)
+ break;
+
/* Take the entry at the top of the min heap */
cs_queue_nr = etm->heap.heap_array[0].queue_nr;
queue_nr = TO_QUEUE_NR(cs_queue_nr);
@@ -2878,8 +2893,16 @@ static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
* No more auxtrace_buffers to process in this etmq, simply
* move on to another entry in the auxtrace_heap.
*/
- if (!ret)
+ if (!ret) {
+ /*
+ * The trace for this queue is exhausted. Drop any
+ * branch history so that samples arriving later
+ * cannot pick up entries decoded before the gap.
+ */
+ if (etm->synth_opts.add_last_branch)
+ thread_stack__flush(tidq->frontend_thread);
continue;
+ }
ret = cs_etm__decode_data_block(etmq);
if (ret)
@@ -3011,6 +3034,116 @@ static int cs_etm__process_switch_cpu_wide(struct cs_etm_auxtrace *etm,
return 0;
}
+static bool cs_etm__tracing_kernel(struct cs_etm_auxtrace *etm,
+ struct perf_session *session)
+{
+ struct evsel *evsel;
+
+ evlist__for_each_entry(session->evlist, evsel) {
+ if (evsel->core.attr.type == etm->pmu_type)
+ return !evsel->core.attr.exclude_kernel;
+ }
+
+ return false;
+}
+
+static int cs_etm__br_stack_init(struct cs_etm_auxtrace *etm,
+ struct perf_session *session)
+{
+ struct evsel *evsel;
+
+ evlist__for_each_entry(session->evlist, evsel) {
+ /*
+ * Only timestamped events can be matched against the decoded
+ * trace, so do not advertise a branch stack on any other.
+ */
+ if (!(evsel->core.attr.sample_type & PERF_SAMPLE_TIME))
+ continue;
+ if (!(evsel->core.attr.sample_type & PERF_SAMPLE_BRANCH_STACK))
+ evsel->synth_sample_type |= PERF_SAMPLE_BRANCH_STACK;
+ }
+
+ /*
+ * Additional branch stack depth to cater for the branches decoded
+ * between the sampled ip and the point at which the sample time was
+ * recorded. Those are trimmed by thread_stack__br_sample_late(), so
+ * the extra depth keeps the requested output depth achievable. If
+ * kernel space is not traced, only the branch into the kernel needs
+ * to be accounted for.
+ */
+ if (cs_etm__tracing_kernel(etm, session))
+ etm->br_stack_sz_plus += 1024;
+ else
+ etm->br_stack_sz_plus += 1;
+
+ etm->br_stack = zalloc(sizeof(struct branch_stack) +
+ etm->br_stack_sz * sizeof(struct branch_entry));
+ if (!etm->br_stack)
+ return -ENOMEM;
+
+ return 0;
+}
+
+/*
+ * Add decoded branch history to an existing sample. The sample keeps its own
+ * ip, callchain and event identity; only an absent branch stack is filled in.
+ */
+static int cs_etm__process_sample(struct cs_etm_auxtrace *etm,
+ struct perf_session *session,
+ struct perf_sample *sample)
+{
+ struct machine *machine = &session->machines.host;
+ struct thread *thread;
+ int err;
+
+ if (!etm->synth_opts.add_last_branch || sample->branch_stack ||
+ !sample->ip || !sample->time || sample->time == (u64)-1)
+ return 0;
+
+ /* Adding branch history to existing samples supports the host only */
+ if (sample->cpumode == PERF_RECORD_MISC_GUEST_KERNEL ||
+ sample->cpumode == PERF_RECORD_MISC_GUEST_USER)
+ return 0;
+
+ err = cs_etm__update_queues(etm);
+ if (err)
+ return err;
+
+ /*
+ * Decode every queue up to this sample's time. Afterwards the thread
+ * stack holds the branches that executed before the sample, and
+ * nothing that executed after it.
+ */
+ err = cs_etm__process_timestamped_queues(etm, sample->time);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(machine, sample->pid, sample->tid);
+ if (!thread)
+ return -ENOMEM;
+
+ /*
+ * The thread stack is emptied when the decoder reports a
+ * discontinuity and when a queue runs out of trace, so branches from
+ * before either of those are never reported.
+ *
+ * Note that a sample landing in a gap between two trace windows is
+ * not covered by that: the discontinuity that ends the gap has not
+ * been decoded at this point, so the preceding window is still in
+ * the thread stack and gets attached. Filtering those out needs a
+ * per-window end time that the decoder does not currently expose.
+ */
+ thread_stack__br_sample_late(thread, sample->cpu, etm->br_stack,
+ etm->br_stack_sz, sample->ip,
+ machine__kernel_start(machine));
+ if (etm->br_stack->nr)
+ sample->branch_stack = etm->br_stack;
+
+ thread__put(thread);
+
+ return 0;
+}
+
static int cs_etm__process_event(struct perf_session *session,
union perf_event *event,
struct perf_sample *sample,
@@ -3049,6 +3182,9 @@ static int cs_etm__process_event(struct perf_session *session,
case PERF_RECORD_SWITCH_CPU_WIDE:
return cs_etm__process_switch_cpu_wide(etm, event);
+ case PERF_RECORD_SAMPLE:
+ return cs_etm__process_sample(etm, session, sample);
+
case PERF_RECORD_AUX:
/*
* Record the latest kernel timestamp available in the header
@@ -3752,11 +3888,34 @@ int cs_etm__process_auxtrace_info_full(union perf_event *event,
etm->use_thread_stack = etm->synth_opts.thread_stack ||
etm->synth_opts.last_branch ||
+ etm->synth_opts.add_last_branch ||
etm->synth_opts.callchain;
etm->use_callchain = etm->synth_opts.thread_stack ||
etm->synth_opts.callchain;
+ if (etm->synth_opts.last_branch || etm->synth_opts.add_last_branch) {
+ etm->br_stack_sz = etm->synth_opts.last_branch_sz;
+ etm->br_stack_sz_plus = etm->br_stack_sz;
+ }
+
+ if (etm->synth_opts.add_last_branch) {
+ /*
+ * Existing samples are matched to decoded trace by time, so
+ * the trace must carry timestamps that are correlated to perf
+ * time and the queues must be decoded in time order.
+ */
+ if (etm->timeless_decoding || !etm->has_virtual_ts) {
+ pr_err("CS ETM Trace: --itrace=L requires virtual timestamped trace\n");
+ err = -EINVAL;
+ goto err_free_queues;
+ }
+
+ err = cs_etm__br_stack_init(etm, session);
+ if (err)
+ goto err_free_queues;
+ }
+
err = cs_etm__synth_events(etm, session);
if (err)
goto err_free_queues;
@@ -3812,6 +3971,7 @@ int cs_etm__process_auxtrace_info_full(union perf_event *event,
auxtrace_queues__free(&etm->queues);
session->auxtrace = NULL;
err_free_etm:
+ zfree(&etm->br_stack);
zfree(&etm);
err_free_metadata:
/* No need to check @metadata[j], free(NULL) is supported */
--
2.52.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH 7/9] perf test cs-etm: Test branch history on existing samples
2026-08-03 9:06 [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC Amir Ayupov
` (4 preceding siblings ...)
2026-08-03 9:06 ` [PATCH 6/9] perf cs-etm: Add branch history to existing samples Amir Ayupov
@ 2026-08-03 9:06 ` Amir Ayupov
2026-08-03 9:21 ` sashiko-bot
2026-08-03 9:06 ` [PATCH 8/9] perf cs-etm: Consume branch history when attaching it to a sample Amir Ayupov
2026-08-03 9:06 ` [PATCH 9/9] Documentation: coresight: Document context-sensitive PGO workflow Amir Ayupov
7 siblings, 1 reply; 13+ messages in thread
From: Amir Ayupov @ 2026-08-03 9:06 UTC (permalink / raw)
To: linux-perf-users, coresight, linux-arm-kernel, Suzuki K Poulose,
James Clark, Leo Yan, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon
Cc: linux-doc, Mike Leach, Jonathan Corbet, Shuah Khan,
Swapnil Sapkal
Add a CoreSight shell test for --itrace=L. Record timestamped ETM trace
with explicit -T sample timestamps and AUX pause/resume events, then
check that the pause samples carry both a multi-frame callchain and a
non-empty branch stack for each of the workload's two processes.
Decode the same recording with L4 and L64 and reject any branch stack
deeper than the requested depth.
The test skips when cs_etm is absent, when not run as root, or when the
recording turns out to lack virtual timestamps. It exercises the
timestamp-gated path and the requested-depth bound; it does not attempt
to verify that the attached history is correlated to the sample.
Signed-off-by: Amir Ayupov <aaupov@fb.com>
---
.../tests/shell/coresight/add_last_branch.sh | 175 ++++++++++++++++++
1 file changed, 175 insertions(+)
create mode 100755 tools/perf/tests/shell/coresight/add_last_branch.sh
diff --git a/tools/perf/tests/shell/coresight/add_last_branch.sh b/tools/perf/tests/shell/coresight/add_last_branch.sh
new file mode 100755
index 0000000000000..4654069ad651f
--- /dev/null
+++ b/tools/perf/tests/shell/coresight/add_last_branch.sh
@@ -0,0 +1,175 @@
+#!/bin/bash -e
+# SPDX-License-Identifier: GPL-2.0
+# CoreSight branch history on existing samples (exclusive)
+
+perf list pmu | grep -q 'cs_etm//' || exit 2
+
+if [ "$(id -u)" != 0 ]; then
+ echo "[Skip] No root permission"
+ exit 2
+fi
+
+tmpdir=$(mktemp -d /tmp/perf-cs-add-last-branch.XXXXX)
+
+cleanup()
+{
+ rm -rf "$tmpdir"
+ trap - EXIT TERM INT
+}
+
+# shellcheck disable=SC2317 # Called through trap.
+trap_cleanup()
+{
+ cleanup
+ exit 1
+}
+trap trap_cleanup EXIT TERM INT
+
+record_data()
+{
+ if perf record -T -o "$tmpdir/data" -C 0 \
+ -e cs_etm/aux-action=start-paused,timestamp/u \
+ -e cycles/aux-action=resume,period=550019/u \
+ -e cycles/aux-action=pause,period=100003,call-graph=fp/u -- \
+ taskset --cpu-list 0 perf test -w context_switch_loop 100000 \
+ >/dev/null 2>"$tmpdir/stderr"; then
+ return 0
+ fi
+
+ echo "Failed to record ETM trace with AUX pause/resume" >&2
+ cat "$tmpdir/stderr" >&2
+ return 1
+}
+
+decode()
+{
+ local size=$1
+ local output=$2
+
+ if perf script -i "$tmpdir/data" --itrace="L$size" \
+ -F comm,pid,tid,event,ip,brstack >"$output" \
+ 2>"$tmpdir/stderr"; then
+ return 0
+ fi
+
+ if grep -q "itrace=L requires virtual timestamped trace" \
+ "$tmpdir/stderr"; then
+ echo "[Skip] Virtual CoreSight timestamps are not available"
+ cleanup
+ exit 2
+ fi
+
+ cat "$tmpdir/stderr" >&2
+ return 1
+}
+
+check_process_samples()
+{
+ local output=$1
+ local comm
+
+ for comm in proc1 proc2; do
+ awk -v comm="$comm" '
+ $1 == comm && /cycles\/aux-action=pause/ {
+ in_sample = 1
+ next
+ }
+ !NF {
+ in_sample = 0
+ next
+ }
+ in_sample && /0x[[:xdigit:]]+\/0x[[:xdigit:]]+\// {
+ found = 1
+ }
+ END { exit !found }
+ ' "$output" || {
+ echo "No pause-event branch stack found for $comm" >&2
+ return 1
+ }
+ done
+}
+
+check_callchains()
+{
+ local output="$tmpdir/script-callchain"
+
+ perf script -i "$tmpdir/data" -F comm,event,ip >"$output" 2>/dev/null
+
+ awk '
+ /cycles\/aux-action=pause/ {
+ in_sample = 1
+ frames = 0
+ next
+ }
+ !NF {
+ if (in_sample && frames >= 2)
+ found = 1
+ in_sample = 0
+ next
+ }
+ in_sample && /^[[:space:]]+[[:xdigit:]]+([[:space:]]|$)/ {
+ frames++
+ }
+ END {
+ if (in_sample && frames >= 2)
+ found = 1
+ exit !found
+ }
+ ' "$output" || {
+ echo "No multi-frame pause-event callchain found" >&2
+ return 1
+ }
+}
+
+check_branch_stacks()
+{
+ local output=$1
+ local max_entries=$2
+
+ local ret
+
+ if awk -v max="$max_entries" '
+ /0x[[:xdigit:]]+\/0x[[:xdigit:]]+\// {
+ entries = 0
+ for (i = 1; i <= NF; i++)
+ if ($i ~ /^0x[[:xdigit:]]+\/0x[[:xdigit:]]+\//)
+ entries++
+ if (entries)
+ found = 1
+ if (entries > max) {
+ status = 2
+ exit
+ }
+ }
+ END {
+ if (status)
+ exit status
+ if (!found)
+ exit 1
+ }
+ ' "$output"; then
+ return 0
+ else
+ ret=$?
+ fi
+
+ case $ret in
+ 1) echo "No ETM branch stacks found" >&2 ;;
+ 2) echo "Branch stack exceeds requested L$max_entries depth" >&2 ;;
+ esac
+ return 1
+}
+
+record_data
+check_callchains
+
+decode 4 "$tmpdir/script-l4"
+check_process_samples "$tmpdir/script-l4"
+check_branch_stacks "$tmpdir/script-l4" 4
+
+decode 64 "$tmpdir/script-l64"
+check_process_samples "$tmpdir/script-l64"
+check_branch_stacks "$tmpdir/script-l64" 64
+
+cleanup
+exit 0
--
2.52.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH 8/9] perf cs-etm: Consume branch history when attaching it to a sample
2026-08-03 9:06 [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC Amir Ayupov
` (5 preceding siblings ...)
2026-08-03 9:06 ` [PATCH 7/9] perf test cs-etm: Test branch history on " Amir Ayupov
@ 2026-08-03 9:06 ` Amir Ayupov
2026-08-03 9:06 ` [PATCH 9/9] Documentation: coresight: Document context-sensitive PGO workflow Amir Ayupov
7 siblings, 0 replies; 13+ messages in thread
From: Amir Ayupov @ 2026-08-03 9:06 UTC (permalink / raw)
To: linux-perf-users, coresight, linux-arm-kernel, Suzuki K Poulose,
James Clark, Leo Yan, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon
Cc: linux-doc, Mike Leach, Jonathan Corbet, Shuah Khan,
Swapnil Sapkal
--itrace=L attaches whatever the thread stack holds when a sample is
processed. With a duty cycled trace most samples fire while the trace is
off. Those samples have nothing newly decoded, but the thread stack still
holds the previous window, so they were being given branches that ran an
arbitrary amount of time earlier as if they immediately preceded the
sample.
The reconstruction is discarded on a trace discontinuity, but that does
not help here: the discontinuity that ends a gap is only decoded once
trace resumes, which is after the samples in the gap have been processed.
A trace window belongs to exactly one sample. With AUX pause and resume
the sample is what stops the trace, so the pairing is one to one by
construction. Take the branch history when attaching it instead of
copying it, and a later sample with nothing newly decoded then finds an
empty branch stack, which dlfilter-nonempty-brstack.so removes.
This is not a small correction. On a 12 s single-threaded capture with
pause period 100003 and resume period 8350251, of 335291 samples that
previously received branch history only 3371 were backed by trace decoded
for that sample; the other 331920 repeated an earlier window. The
proportion follows the ETM duty cycle, so it holds for any low duty cycle
configuration.
Note that thread_stack__br_sample(), used by lowercase --itrace=l, still
copies, so synthesised instruction samples keep the overlapping branch
stacks they have today.
Signed-off-by: Amir Ayupov <aaupov@fb.com>
---
tools/perf/util/cs-etm.c | 17 ++++++++---------
tools/perf/util/thread-stack.c | 17 +++++++++++++++++
tools/perf/util/thread-stack.h | 1 +
3 files changed, 26 insertions(+), 9 deletions(-)
diff --git a/tools/perf/util/cs-etm.c b/tools/perf/util/cs-etm.c
index 048ff97caa936..a3498a0a96a05 100644
--- a/tools/perf/util/cs-etm.c
+++ b/tools/perf/util/cs-etm.c
@@ -3123,19 +3123,18 @@ static int cs_etm__process_sample(struct cs_etm_auxtrace *etm,
return -ENOMEM;
/*
- * The thread stack is emptied when the decoder reports a
- * discontinuity and when a queue runs out of trace, so branches from
- * before either of those are never reported.
- *
- * Note that a sample landing in a gap between two trace windows is
- * not covered by that: the discontinuity that ends the gap has not
- * been decoded at this point, so the preceding window is still in
- * the thread stack and gets attached. Filtering those out needs a
- * per-window end time that the decoder does not currently expose.
+ * Take the branch history rather than copying it. The trace window
+ * belongs to the sample that ends it, so once it has been attached a
+ * later sample with nothing newly decoded finds an empty stack rather
+ * than being given an earlier window's branches. That is the common
+ * case whenever the trace is duty cycled, by AUX pause/resume or by
+ * ETM strobing.
*/
thread_stack__br_sample_late(thread, sample->cpu, etm->br_stack,
etm->br_stack_sz, sample->ip,
machine__kernel_start(machine));
+ thread_stack__br_stack_consume(thread, sample->cpu);
+
if (etm->br_stack->nr)
sample->branch_stack = etm->br_stack;
diff --git a/tools/perf/util/thread-stack.c b/tools/perf/util/thread-stack.c
index 51eaedb47bb1d..374a291aa48fe 100644
--- a/tools/perf/util/thread-stack.c
+++ b/tools/perf/util/thread-stack.c
@@ -614,6 +614,23 @@ void thread_stack__sample_late(struct thread *thread, int cpu,
}
}
+/*
+ * Branch history belongs to the sample that ends the trace window, so a
+ * decoder that attaches it to an existing sample should take it rather than
+ * copy it. A later sample with no newly decoded trace then finds an empty
+ * branch stack instead of the previous window's branches.
+ */
+void thread_stack__br_stack_consume(struct thread *thread, int cpu)
+{
+ struct thread_stack *ts = thread__stack(thread, cpu);
+
+ if (!ts || !ts->br_stack_rb)
+ return;
+
+ ts->br_stack_pos = 0;
+ ts->br_stack_rb->nr = 0;
+}
+
void thread_stack__br_sample(struct thread *thread, int cpu,
struct branch_stack *dst, unsigned int sz)
{
diff --git a/tools/perf/util/thread-stack.h b/tools/perf/util/thread-stack.h
index b3cd09beb62f0..2aec292bd1bcb 100644
--- a/tools/perf/util/thread-stack.h
+++ b/tools/perf/util/thread-stack.h
@@ -88,6 +88,7 @@ void thread_stack__sample(struct thread *thread, int cpu, struct ip_callchain *c
void thread_stack__sample_late(struct thread *thread, int cpu,
struct ip_callchain *chain, size_t sz, u64 ip,
u64 kernel_start);
+void thread_stack__br_stack_consume(struct thread *thread, int cpu);
void thread_stack__br_sample(struct thread *thread, int cpu,
struct branch_stack *dst, unsigned int sz);
void thread_stack__br_sample_late(struct thread *thread, int cpu,
--
2.52.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH 9/9] Documentation: coresight: Document context-sensitive PGO workflow
2026-08-03 9:06 [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC Amir Ayupov
` (6 preceding siblings ...)
2026-08-03 9:06 ` [PATCH 8/9] perf cs-etm: Consume branch history when attaching it to a sample Amir Ayupov
@ 2026-08-03 9:06 ` Amir Ayupov
7 siblings, 0 replies; 13+ messages in thread
From: Amir Ayupov @ 2026-08-03 9:06 UTC (permalink / raw)
To: linux-perf-users, coresight, linux-arm-kernel, Suzuki K Poulose,
James Clark, Leo Yan, Peter Zijlstra, Ingo Molnar,
Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
John Garry, Will Deacon
Cc: linux-doc, Mike Leach, Jonathan Corbet, Shuah Khan,
Swapnil Sapkal
Document combining callchain-bearing AUX pause samples with the ETM
branch history that precedes them, which is the pairing that
context-sensitive PGO tools consume, and the --itrace=L64 decode with the
non-empty branch-stack dlfilter.
Signed-off-by: Amir Ayupov <aaupov@fb.com>
---
.../trace/coresight/coresight-perf.rst | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
diff --git a/Documentation/trace/coresight/coresight-perf.rst b/Documentation/trace/coresight/coresight-perf.rst
index 0a77741a431ef..c0c82b3d26ea8 100644
--- a/Documentation/trace/coresight/coresight-perf.rst
+++ b/Documentation/trace/coresight/coresight-perf.rst
@@ -109,6 +109,68 @@ Example for triggering AUX pause and resume with PMU event::
-e cycles/aux-action=pause,period=10000000/ \
-e cycles/aux-action=resume,period=1050000/ -- sleep 1
+Context-sensitive sampled PGO (CSSPGO) profiling
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A callchain-bearing pause event can be combined with the branch history from
+its preceding ETM trace window. This provides the synchronized callchain and
+branch stack consumed by context-sensitive PGO tools without continuously
+recording ETM trace for a long-running process.
+
+For example, record user-space ETM trace, resume it periodically, and pause it
+with a cycle event that also captures a frame-pointer callchain::
+
+ perf record -T \
+ -e cs_etm/aux-action=start-paused,timestamp/u \
+ -e cycles/aux-action=resume,period=8350251/u \
+ -e cycles/aux-action=pause,period=100003,call-graph=fp/u \
+ -- ./workload
+
+The two cycle events count independently. With pause period ``P`` and resume
+period ``R``, each trace window is approximately 0 to ``P`` cycles long, so the
+average duty cycle is ``P / (2 * R)``. The periods above give about 0.6% duty.
+
+Do not make ``R`` an integer multiple of ``P``: coincident pause and resume
+interrupts can produce zero-length windows. Choosing ``R`` near
+``(k + 1/2) * P``, as above, moves the resume phase across the pause interval.
+Pause events that fire while ETM is already paused have no branch history; the
+dlfilter below removes those samples.
+
+The ``-T`` option timestamps the pause samples, while ``timestamp`` enables
+ETM timestamp packets. Both are required to correlate the sample with ETM
+trace. This mode also requires virtual ETM timestamps correlated to perf time
+and a callchain on the pause event. Use ``call-graph=dwarf`` instead of
+``call-graph=fp`` when the workload does not preserve frame pointers.
+
+Tuning duty cycle
+^^^^^^^^^^^^^^^^^
+
+The example above favors low recording overhead for fleet collection. With
+independent counters, ``P / R`` is the nominal window ratio while
+``P / (2 * R)`` is the expected average ETM-on duty. For ``P = 100003``, two
+measured Neoverse V2 operating points are:
+
+- Fleet collection: ``R = 8350251``, 1.2% nominal ratio and 0.6% average duty.
+- Targeted profiling: ``R = 1050031``, 9.5% nominal ratio and 4.8% average duty.
+
+AUX buffer size must also scale with trace volume. A 128 KiB AUX buffer worked
+at 0.6% duty but was unstable at some higher-duty points, where it increased
+output size or overran and reduced useful-sample yield. Use 4 MiB as a
+conservative starting point around ``R/P = 6.7`` to ``12.5``. These values are
+workload and platform dependent; verify useful samples per MiB and lost AUX
+records when tuning another system.
+
+Add up to 64 decoded ETM branches to each existing pause sample and emit the
+hybrid samples in the regular perf-script format::
+
+ perf script -i perf.data --itrace=L64 \
+ --dlfilter=dlfilter-nonempty-brstack.so > perf.script
+
+The ``dlfilter-nonempty-brstack.so`` filter drops samples that ended up with
+no branch history at all, for example samples from a thread that was never
+traced, or samples recorded before the first or after the last trace window.
+It is built and installed with perf's other dlfilters.
+
Perf test - Verify kernel and userspace perf CoreSight work
-----------------------------------------------------------
--
2.52.0
^ permalink raw reply related [flat|nested] 13+ messages in thread
* Re: [PATCH 2/9] perf thread-stack: Report branch stack hw_idx as not available
2026-08-03 9:06 ` [PATCH 2/9] perf thread-stack: Report branch stack hw_idx as not available Amir Ayupov
@ 2026-08-03 9:19 ` sashiko-bot
0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-08-03 9:19 UTC (permalink / raw)
To: Amir Ayupov; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [Critical] Pre-existing heap buffer overflow in thread_stack__br_sample due to incorrect memcpy length.
--
--- Patch [2]: [PATCH 2/9] perf thread-stack: Report branch stack hw_idx as not available ---
commit 51033b3641484a31783c5c945c25ef5ee0bfb23e
Author: Amir Ayupov <aaupov@fb.com>
perf thread-stack: Report branch stack hw_idx as not available
This commit sets hw_idx to -1ULL for synthesised branch stacks to indicate
that the hardware index is not available. This prevents consumers from
erroneously assuming these branch stacks start at LBR TOS entry 0, matching
the behaviour of intel-pt and cs-etm.
> diff --git a/tools/perf/util/thread-stack.c b/tools/perf/util/thread-stack.c
> --- a/tools/perf/util/thread-stack.c
> +++ b/tools/perf/util/thread-stack.c
> @@ -624,6 +624,7 @@ void thread_stack__br_sample(struct thread *thread, int cpu,
> unsigned int nr;
>
> dst->nr = 0;
> + dst->hw_idx = -1ULL;
>
> if (!ts)
> return;
[ ... ]
> if (src->nr >= ts->br_stack_sz) {
> sz -= nr;
> be = &dst->entries[nr];
> nr = min(ts->br_stack_pos, sz);
> memcpy(be, &src->entries[0], bsz * ts->br_stack_pos);
[Severity: Critical]
This is a pre-existing issue, but does this memcpy() write past the end of
the dst buffer?
We calculate nr as min(ts->br_stack_pos, sz), which limits the count
to the available size in the buffer. However, the memcpy() uses
bsz * ts->br_stack_pos instead of bsz * nr.
When running perf commands that parse Intel PT traces, ts->br_stack_sz can
be larger than the sample size sz. This could cause ts->br_stack_pos to
exceed nr, leading to a heap buffer overflow when copying to be.
> }
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260803090640.2412336-1-aaupov@fb.com?part=2
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH 7/9] perf test cs-etm: Test branch history on existing samples
2026-08-03 9:06 ` [PATCH 7/9] perf test cs-etm: Test branch history on " Amir Ayupov
@ 2026-08-03 9:21 ` sashiko-bot
0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-08-03 9:21 UTC (permalink / raw)
To: Amir Ayupov; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] `check_callchains` does not verify callchains for each process as claimed in the commit message.
--
--- Patch [7]: [PATCH 7/9] perf test cs-etm: Test branch history on existing samples ---
commit e252ec70028dd10b925f2e4ea78b626688f184d4
Author: Amir Ayupov <aaupov@fb.com>
perf test cs-etm: Test branch history on existing samples
This commit adds a CoreSight shell test to verify branch history on existing
samples using explicitly timestamped ETM traces. It decodes traces with L4 and
L64 depth settings and checks that pause samples contain valid multi-frame
callchains and bounded branch stacks.
> diff --git a/tools/perf/tests/shell/coresight/add_last_branch.sh b/tools/perf/tests/shell/coresight/add_last_branch.sh
> new file mode 100755
> --- /dev/null
> +++ b/tools/perf/tests/shell/coresight/add_last_branch.sh
[ ... ]
> +check_callchains()
> +{
> + local output="$tmpdir/script-callchain"
> +
> + perf script -i "$tmpdir/data" -F comm,event,ip >"$output" 2>/dev/null
> +
> + awk '
> + /cycles\/aux-action=pause/ {
> + in_sample = 1
> + frames = 0
> + next
> + }
> + !NF {
> + if (in_sample && frames >= 2)
> + found = 1
> + in_sample = 0
> + next
> + }
[Severity: Medium]
The commit message states the test will "check that the pause samples carry
both a multi-frame callchain and a non-empty branch stack for each of the
workload's two processes."
Does this awk script in check_callchains() verify the callchains for each
process?
Unlike check_process_samples(), which checks for proc1 and proc2, this
script appears to set found=1 as soon as any sample contains a multi-frame
callchain, without validating the comm field.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260803090640.2412336-1-aaupov@fb.com?part=7
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH 6/9] perf cs-etm: Add branch history to existing samples
2026-08-03 9:06 ` [PATCH 6/9] perf cs-etm: Add branch history to existing samples Amir Ayupov
@ 2026-08-03 9:23 ` sashiko-bot
0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-08-03 9:23 UTC (permalink / raw)
To: Amir Ayupov; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Stale branch history is left unflushed for multiplexed trace IDs when a queue is exhausted.
- [Medium] cs_etm__tracing_kernel only evaluates the exclude_kernel attribute of the first matching cs_etm event.
--
--- Patch [6]: [PATCH 6/9] perf cs-etm: Add branch history to existing samples ---
commit fed4660720fbc50c143c4ee374a10328c53686f9
Author: Amir Ayupov <aaupov@fb.com>
perf cs-etm: Add branch history to existing samples
This commit implements --itrace=L for CoreSight ETM to decode timestamped
trace up to each existing PMU sample and attach the branch history. It adds
virtual ETM timestamp correlation and keeps an internal reconstruction ring
deeper than the requested output depth to cover branches decoded between the
sampled IP and the recorded sample time.
> diff --git a/tools/perf/util/cs-etm.c b/tools/perf/util/cs-etm.c
> index 4d895f11deb7f..048ff97caa936 100644
> --- a/tools/perf/util/cs-etm.c
> +++ b/tools/perf/util/cs-etm.c
[ ... ]
> @@ -2878,8 +2893,16 @@ static int cs_etm__process_timestamped_queues(struct cs_etm_auxtrace *etm)
> * No more auxtrace_buffers to process in this etmq, simply
> * move on to another entry in the auxtrace_heap.
> */
> - if (!ret)
> + if (!ret) {
> + /*
> + * The trace for this queue is exhausted. Drop any
> + * branch history so that samples arriving later
> + * cannot pick up entries decoded before the gap.
> + */
> + if (etm->synth_opts.add_last_branch)
> + thread_stack__flush(tidq->frontend_thread);
[Severity: Medium]
When an ETM queue (etmq) multiplexing multiple trace channels runs out of
auxtrace data blocks, is it sufficient to flush only the active trace ID?
Since this only flushes the specific tidq (from trace_chan_id) that was
popped from the min-heap, could other trace IDs sharing the exhausted queue
be skipped? This might leave stale branch history that could be incorrectly
attributed to PMU samples in a later trace window.
> continue;
> + }
>
> ret = cs_etm__decode_data_block(etmq);
[ ... ]
> @@ -3011,6 +3034,116 @@ static int cs_etm__process_switch_cpu_wide(struct cs_etm_auxtrace *etm,
> return 0;
> }
>
> +static bool cs_etm__tracing_kernel(struct cs_etm_auxtrace *etm,
> + struct perf_session *session)
> +{
> + struct evsel *evsel;
> +
> + evlist__for_each_entry(session->evlist, evsel) {
> + if (evsel->core.attr.type == etm->pmu_type)
> + return !evsel->core.attr.exclude_kernel;
[Severity: Medium]
If the evlist contains multiple cs_etm events (such as one excluding kernel
and a later one including it), does returning on the first match prevent
subsequent events from being evaluated?
If a subsequent event includes kernel tracing, missing it here could lead to
an undersized internal branch reconstruction stack (br_stack_sz_plus), which
might cause branches leading up to the sample to be overwritten.
> + }
> +
> + return false;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260803090640.2412336-1-aaupov@fb.com?part=6
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH 3/9] perf thread-stack: Bound wrapped branch stack copy
2026-08-03 9:06 ` [PATCH 3/9] perf thread-stack: Bound wrapped branch stack copy Amir Ayupov
@ 2026-08-03 9:26 ` sashiko-bot
0 siblings, 0 replies; 13+ messages in thread
From: sashiko-bot @ 2026-08-03 9:26 UTC (permalink / raw)
To: Amir Ayupov; +Cc: linux-perf-users
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [Medium] Unconditional dereferences of `ts->br_stack_rb` cause a segmentation fault if thread stack initialization fails or if it was initially created without a branch stack.
--
--- Patch [3]: [PATCH 3/9] perf thread-stack: Bound wrapped branch stack copy ---
commit 3efa75c3f04529593a5a6a18971bae8fa6a9617c
Author: Amir Ayupov <aaupov@fb.com>
perf thread-stack: Bound wrapped branch stack copy
This commit fixes a potential destination buffer overrun when copying
wrapped branch stack entries. It correctly limits the number of entries
copied to the remaining destination size.
> diff --git a/tools/perf/util/thread-stack.c b/tools/perf/util/thread-stack.c
> index 1a3dffa83bde2..51eaedb47bb1d 100644
> --- a/tools/perf/util/thread-stack.c
> +++ b/tools/perf/util/thread-stack.c
> @@ -643,7 +643,7 @@ void thread_stack__br_sample(struct thread *thread, int cpu,
[Severity: Medium]
This is a pre-existing issue, but I noticed a potential NULL pointer
dereference earlier in this function.
Could src be NULL when we evaluate src->nr?
tools/perf/util/thread-stack.c:thread_stack__br_sample() {
...
src = ts->br_stack_rb;
if (!src->nr)
return;
...
}
It looks like this might occur if thread_stack__init() fails to allocate the
branch stack ring buffer, as the partially initialized thread_stack is
already attached to the thread:
tools/perf/util/thread-stack.c:thread_stack__new() {
...
thread__set_ts(thread, new_ts);
ts = new_ts;
...
if (!ts->stack &&
thread_stack__init(ts, thread, crp, callstack, br_stack_sz))
return NULL;
...
}
If a caller ignores the error from thread_stack__event(), they might proceed
to call thread_stack__br_sample() or thread_stack__update_br_stack() with a
NULL branch stack ring buffer. Is there a need to check if ts->br_stack_rb
is populated before dereferencing it here?
> sz -= nr;
> be = &dst->entries[nr];
> nr = min(ts->br_stack_pos, sz);
> - memcpy(be, &src->entries[0], bsz * ts->br_stack_pos);
> + memcpy(be, &src->entries[0], bsz * nr);
> }
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260803090640.2412336-1-aaupov@fb.com?part=3
^ permalink raw reply [flat|nested] 13+ messages in thread
end of thread, other threads:[~2026-08-03 9:27 UTC | newest]
Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-03 9:06 [PATCH 1/9] perf header: Tolerate inconsistent HEADER_GROUP_DESC Amir Ayupov
2026-08-03 9:06 ` [PATCH 2/9] perf thread-stack: Report branch stack hw_idx as not available Amir Ayupov
2026-08-03 9:19 ` sashiko-bot
2026-08-03 9:06 ` [PATCH 3/9] perf thread-stack: Bound wrapped branch stack copy Amir Ayupov
2026-08-03 9:26 ` sashiko-bot
2026-08-03 9:06 ` [PATCH 4/9] perf dlfilter: Add non-empty branch stack filter Amir Ayupov
2026-08-03 9:06 ` [PATCH 5/9] perf cs-etm: Split up cs_etm__process_timestamped_queues() Amir Ayupov
2026-08-03 9:06 ` [PATCH 6/9] perf cs-etm: Add branch history to existing samples Amir Ayupov
2026-08-03 9:23 ` sashiko-bot
2026-08-03 9:06 ` [PATCH 7/9] perf test cs-etm: Test branch history on " Amir Ayupov
2026-08-03 9:21 ` sashiko-bot
2026-08-03 9:06 ` [PATCH 8/9] perf cs-etm: Consume branch history when attaching it to a sample Amir Ayupov
2026-08-03 9:06 ` [PATCH 9/9] Documentation: coresight: Document context-sensitive PGO workflow Amir Ayupov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox