[PATCH v2 11/14] perf tests: Check CoreSight IRQ entry and exit
Leo Yan
leo.yan at arm.com
Wed Sep 23 08:21:51 PDT 2026
Add an AArch64 branch_not_taken_loop workload and a CoreSight shell test
checking IRQ entry and exit PCs. Use cpu-clock generates timer
interrupts.
Retry at different timer frequencies and skip if no complete pair is
captured.
For example, an expected hw int/iret pair is:
hw int 5cc928 => ffff800080010c80 insn: 63 04 00 f1
...
iret ffff800080012284 => 5cc928 insn: e0 03 9f d6
The entry PC is the architectural resume address, here SUBS X3, X3, #1
at 0x5cc928. After the IRQ is handled, ERET returns to the same PC to
continue execution.
Assisted-by: Codex:gpt-6
Signed-off-by: Leo Yan <leo.yan at arm.com>
---
tools/perf/tests/builtin-test.c | 1 +
tools/perf/tests/shell/coresight/irq_entry_exit.sh | 37 +++++
tools/perf/tests/shell/lib/coresight_exception.sh | 165 +++++++++++++++++++++
tools/perf/tests/tests.h | 1 +
tools/perf/tests/workloads/Build | 2 +
tools/perf/tests/workloads/branch_not_taken_loop.c | 33 +++++
6 files changed, 239 insertions(+)
diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-test.c
index 6259ed805c5f75799d52ac742b77cf278f98bd9d..8eab221f115e75a3ef90be975d4ed0558ceb2b16 100644
--- a/tools/perf/tests/builtin-test.c
+++ b/tools/perf/tests/builtin-test.c
@@ -171,6 +171,7 @@ static struct test_workload *workloads[] = {
&workload__datasym,
&workload__landlock,
&workload__traploop,
+ &workload__branch_not_taken_loop,
&workload__inlineloop,
&workload__jitdump,
&workload__context_switch_loop,
diff --git a/tools/perf/tests/shell/coresight/irq_entry_exit.sh b/tools/perf/tests/shell/coresight/irq_entry_exit.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f164daa222b0550ceb106aa6a1215740a1432017
--- /dev/null
+++ b/tools/perf/tests/shell/coresight/irq_entry_exit.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# CoreSight IRQ entry and exit (exclusive)
+
+# shellcheck source=../lib/coresight_exception.sh
+. "$(dirname "$0")/../lib/coresight_exception.sh"
+
+test_irq_entry_exit()
+{
+ # IRQs must return to the interrupted PC. Retry at different timer
+ # frequencies if trace gaps leave no complete entry/exit pair.
+ for freq in 1000 4000 10000; do
+ echo "Recording timer IRQs at $freq Hz"
+ if ! record_trace_with_cpu_clock "$freq" branch_not_taken_loop; then
+ cat "$tmpdir/record.log"
+ echo "Failed to record the IRQ workload"
+ return 1
+ fi
+
+ decode_trace || return 1
+
+ check_exception_pairs "hw int" "iret" 0
+ result=$?
+ case $result in
+ 0) return 0 ;;
+ 1) cat "$tmpdir/script.log"; return 1 ;;
+ 2) echo "No complete IRQ pair in this recording" ;;
+ *) return "$result" ;;
+ esac
+ done
+
+ echo "[Skip] No complete IRQ pair after three recordings"
+ return 2
+}
+
+setup_exception_test || exit $?
+test_irq_entry_exit
diff --git a/tools/perf/tests/shell/lib/coresight_exception.sh b/tools/perf/tests/shell/lib/coresight_exception.sh
new file mode 100644
index 0000000000000000000000000000000000000000..f0133c258625147405ba4641ff7e442437fc66b8
--- /dev/null
+++ b/tools/perf/tests/shell/lib/coresight_exception.sh
@@ -0,0 +1,165 @@
+# SPDX-License-Identifier: GPL-2.0
+# Common helpers for CoreSight exception entry and return tests.
+
+cleanup()
+{
+ case $? in
+ 0|2) rm -rf "$tmpdir" ;;
+ *) echo "Test files retained in $tmpdir" ;;
+ esac
+}
+
+record_trace_with_cpu_clock()
+{
+ local clock_opts=()
+
+ record_freq=$1
+ shift
+
+ # A zero frequency omits the cpu-clock event.
+ if [ "$record_freq" -gt 0 ]; then
+ # Generate timer IRQs without delivering a signal to the workload.
+ clock_opts=(-e cpu-clock:u -F "$record_freq")
+ fi
+
+ # FIFO control bounds the trace to the workload.
+ # --kcore supplies the running kernel's instructions for decoding.
+ taskset -c "$cpu" perf record -B --no-bpf-event --per-thread --kcore \
+ -e cs_etm/timestamp=0/uk "${clock_opts[@]}" \
+ -m,4M -D -1 --control fifo:"$tmpdir/ctl","$tmpdir/ack" \
+ -o "$tmpdir/data" -- \
+ perf test --record-ctl fifo:"$tmpdir/ctl","$tmpdir/ack" -w "$@" \
+ > "$tmpdir/record.log" 2>&1
+}
+
+record_trace()
+{
+ record_trace_with_cpu_clock 0 "$@"
+}
+
+decode_trace()
+{
+ local sw_fields=()
+
+ # perf script rejects -F sw: when no software event was recorded.
+ if [ "$record_freq" -gt 0 ]; then
+ sw_fields=(-F sw:)
+ fi
+
+ if ! perf script -i "$tmpdir/data" --itrace=b \
+ -F hw:ip,addr,flags,insn "${sw_fields[@]}" \
+ > "$tmpdir/script" 2> "$tmpdir/script.log"; then
+ cat "$tmpdir/script.log"
+ echo "Failed to decode the exception trace"
+ return 1
+ fi
+}
+
+# Arguments: entry pattern, exit pattern, expected return PC - entry PC.
+# Return 2 if no complete pair survives the trace gaps.
+check_exception_pairs()
+{
+ local difference=$3 entry_pc resume_pc pairs=0
+
+ awk -v entry_pattern="$1" -v exit_pattern="$2" '
+ function address(pc) {
+ sub(/^0[xX]/, "", pc)
+ sub(/^0+/, "", pc)
+ return "0x" (pc == "" ? "0" : tolower(pc))
+ }
+ BEGIN {
+ entry_pattern = "^[[:space:]]*(" entry_pattern ")([[:space:]]|$)"
+ exit_pattern = "^[[:space:]]*(" exit_pattern ")([[:space:]]|$)"
+ }
+ NF {
+ # A trace boundary invalidates all pending entries, including nested ones.
+ if ($1 == "tr" && ($2 == "strt" || $2 == "end")) {
+ depth = 0
+ next
+ }
+
+ is_entry = ($1 == "hw" && $2 == "int") || $1 == "int" || $1 == "syscall"
+ is_exit = $1 == "iret" || $1 == "sysret"
+ if (!is_entry && !is_exit)
+ next
+
+ # Branch fields are: flags source => destination [insn: bytes].
+ arrow = 0
+ for (i = 1; i <= NF; i++) {
+ if ($i == "=>") {
+ arrow = i
+ break
+ }
+ }
+ if (arrow < 3 || arrow == NF) {
+ print "FAIL: Unexpected perf script output: " $0 > "/dev/stderr"
+ errors++
+ depth = 0
+ next
+ }
+
+ # Track every exception so an unrelated nested IRET cannot close
+ # a selected outer entry. Match the requested flags at each depth.
+ if (is_entry) {
+ entry_pc[++depth] = address($(arrow - 1))
+ selected[depth] = $0 ~ entry_pattern
+ next
+ }
+ if (!depth)
+ next
+
+ if (selected[depth] && $0 ~ exit_pattern)
+ print entry_pc[depth], address($(arrow + 1))
+ depth--
+ }
+ END {
+ if (errors)
+ exit 1
+ }' "$tmpdir/script" > "$tmpdir/exception_pairs" || return 1
+
+ # Bash uses integer arithmetic, preserving all bits of 64-bit PCs.
+ while read -r entry_pc resume_pc; do
+ if [ "$((resume_pc - entry_pc))" -ne "$difference" ]; then
+ printf "FAIL: Entry %s returns to %s, expected PC difference %d\n" \
+ "$entry_pc" "$resume_pc" "$difference"
+ return 1
+ fi
+ pairs=$((pairs + 1))
+ done < "$tmpdir/exception_pairs"
+
+ if [ "$pairs" -eq 0 ]; then
+ echo "No complete exception entry/exit pair"
+ return 2
+ fi
+ printf "Checked %d exception pairs with PC difference %d\n" "$pairs" "$difference"
+}
+
+setup_exception_test()
+{
+ [ "$(uname -m)" = aarch64 ] || return 2
+ perf check feature -q libopencsd || return 2
+ [ -d /sys/bus/event_source/devices/cs_etm ] || return 2
+
+ export LC_ALL=C
+ command -v taskset >/dev/null 2>&1 || return 2
+
+ tmpdir=$(mktemp -d /tmp/perf-cs-etm-exception.XXXXXX) || return 1
+ trap cleanup EXIT
+ trap 'exit 1' HUP INT TERM
+ mkfifo "$tmpdir/ctl" "$tmpdir/ack" || return 1
+
+ # Keep each exception entry and return in the same trace queue.
+ cpu=$(awk '/Cpus_allowed_list:/ {
+ split($2, cpus, /[-,]/)
+ print cpus[1]
+ }' /proc/self/status)
+
+ # Check permissions, FIFO control and the trace sink before testing.
+ if ! record_trace callchain; then
+ cat "$tmpdir/record.log"
+ echo "[Skip] Cannot record user/kernel CoreSight trace with --kcore"
+ return 2
+ fi
+
+ return 0
+}
diff --git a/tools/perf/tests/tests.h b/tools/perf/tests/tests.h
index b2520a564417b61718f1dcb23da1f8cc0f601906..0a61129b31ab6997616f7d04d29af73961f4f861 100644
--- a/tools/perf/tests/tests.h
+++ b/tools/perf/tests/tests.h
@@ -246,6 +246,7 @@ DECLARE_WORKLOAD(brstack);
DECLARE_WORKLOAD(datasym);
DECLARE_WORKLOAD(landlock);
DECLARE_WORKLOAD(traploop);
+DECLARE_WORKLOAD(branch_not_taken_loop);
DECLARE_WORKLOAD(inlineloop);
DECLARE_WORKLOAD(jitdump);
DECLARE_WORKLOAD(context_switch_loop);
diff --git a/tools/perf/tests/workloads/Build b/tools/perf/tests/workloads/Build
index 048e371eb63e316453b6b46ebd0a02794c3d25d7..ca22a82073ed08de5d4a27d0a3b33ddcba2e3f3e 100644
--- a/tools/perf/tests/workloads/Build
+++ b/tools/perf/tests/workloads/Build
@@ -9,6 +9,7 @@ perf-test-y += brstack.o
perf-test-y += datasym.o
perf-test-y += landlock.o
perf-test-y += traploop.o
+perf-test-y += branch_not_taken_loop.o
perf-test-y += inlineloop.o
perf-test-y += jitdump.o
perf-test-y += context_switch_loop.o
@@ -25,6 +26,7 @@ CFLAGS_leafloop.o = -g -O0 -fno-inline -fno-omit-frame-pointer -U_FORTIF
CFLAGS_brstack.o = -g -O0 -fno-inline -U_FORTIFY_SOURCE
CFLAGS_datasym.o = -g -O0 -fno-inline -U_FORTIFY_SOURCE
CFLAGS_traploop.o = -g -O0 -fno-inline -U_FORTIFY_SOURCE
+CFLAGS_branch_not_taken_loop.o = -g -O0 -fno-inline -U_FORTIFY_SOURCE
CFLAGS_inlineloop.o = -g -O2
CFLAGS_deterministic.o = -g -O0 -fno-inline -U_FORTIFY_SOURCE
CFLAGS_named_threads.o = -g -O0 -fno-inline -U_FORTIFY_SOURCE
diff --git a/tools/perf/tests/workloads/branch_not_taken_loop.c b/tools/perf/tests/workloads/branch_not_taken_loop.c
new file mode 100644
index 0000000000000000000000000000000000000000..6376b1c476e2f4814a94c91a5321ae919a818826
--- /dev/null
+++ b/tools/perf/tests/workloads/branch_not_taken_loop.c
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/compiler.h>
+#include "../tests.h"
+
+#ifdef __aarch64__
+static void branch_not_taken(void)
+{
+ /* Keep x1 > x2 so B.LS is never taken. */
+ asm volatile(
+ "mov x1, #2\n"
+ "mov x2, #1\n"
+ "movz x3, #0xffff\n"
+ "movk x3, #0x0080, lsl #16\n"
+ "1: cmp x1, x2\n"
+ ".Ltest_branch:\n"
+ "b.ls 2f\n"
+ ".Lfallthrough:\n"
+ "subs x3, x3, #1\n"
+ "b.ne 1b\n"
+ "2:\n"
+ : : : "x1", "x2", "x3", "cc");
+}
+#else
+static void branch_not_taken(void) { }
+#endif
+
+static int branch_not_taken_loop(int argc __maybe_unused, const char **argv __maybe_unused)
+{
+ branch_not_taken();
+ return 0;
+}
+
+DEFINE_WORKLOAD(branch_not_taken_loop);
--
2.34.1
More information about the linux-arm-kernel
mailing list