[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