[RFC 3/3] rv: collect per-edge dwell time for per-cpu DA/HA monitors

From: Tobias Schaffner

Date: Thu Aug 27 2026 - 03:26:11 EST


With the core facility in place, hook it into the DA/HA layer so that any
per-cpu automaton monitor also reports how long it lingers in each state.

Add a selftest that enables a per-cpu monitor, checks the stats file
appears and is populated under load, and skips cleanly otherwise.

Signed-off-by: Tobias Schaffner <tobias.schaffner@xxxxxxxxxxx>
---
include/linux/rv.h | 4 ++
include/rv/da_monitor.h | 53 +++++++++++++++++++
tools/testing/selftests/verification/config | 1 +
.../verification/test.d/rv_edge_stats.tc | 32 +++++++++++
4 files changed, 90 insertions(+)
create mode 100644 tools/testing/selftests/verification/test.d/rv_edge_stats.tc

diff --git a/include/linux/rv.h b/include/linux/rv.h
index 7eeecce17e50..09363f79ca90 100644
--- a/include/linux/rv.h
+++ b/include/linux/rv.h
@@ -27,6 +27,10 @@
struct da_monitor {
bool monitoring;
unsigned int curr_state;
+#ifdef CONFIG_RV_EDGE_STAT
+ /* local_clock() ns when curr_state was entered; 0 = not yet stamped. */
+ u64 state_ns;
+#endif
};

#ifdef CONFIG_RV_LTL_MONITOR
diff --git a/include/rv/da_monitor.h b/include/rv/da_monitor.h
index 34b8fba9ecd4..59ca9a286c10 100644
--- a/include/rv/da_monitor.h
+++ b/include/rv/da_monitor.h
@@ -16,6 +16,8 @@

#include <rv/automata.h>
#include <linux/rv.h>
+#include <linux/rv_edge_stat.h>
+#include <linux/sched/clock.h>
#include <linux/stringify.h>
#include <linux/bug.h>
#include <linux/sched.h>
@@ -30,6 +32,54 @@

static struct rv_monitor rv_this;

+/* per-edge dwell statistics, wired up for per-cpu monitors. */
+#if defined(CONFIG_RV_EDGE_STAT) && RV_MON_TYPE == RV_MON_PER_CPU
+static void
+rv_this_edge_name(unsigned int edge, char *buf, size_t len)
+{
+ snprintf(buf, len, "%s:%s", model_get_state_name(edge / EVENT_MAX),
+ model_get_event_name(edge % EVENT_MAX));
+}
+
+static const struct rv_edge_cfg rv_this_edge_cfg = {
+ .n_edges = STATE_MAX * EVENT_MAX,
+ .edge_name = rv_this_edge_name,
+};
+
+/* Hand the model's edge descriptor to the core; called from da_monitor_init(). */
+static inline void rv_edge_bind(void)
+{
+ rv_this.edge_cfg = &rv_this_edge_cfg;
+}
+
+/* Stamp the moment a state is entered, so its dwell can be timed on exit. */
+static __always_inline void rv_da_edge_enter(struct da_monitor *da_mon)
+{
+ da_mon->state_ns = local_clock();
+}
+
+/* Account the dwell in @curr, then stamp entry into the next state. */
+static __always_inline void
+rv_da_edge_account(struct da_monitor *da_mon, enum states curr, enum events ev)
+{
+ u64 now = local_clock();
+ u64 prev = da_mon->state_ns;
+
+ da_mon->state_ns = now;
+ /*
+ * local_clock() is not guaranteed monotonic; drop the sample if it did
+ * not advance so a backward step cannot underflow into a bogus dwell.
+ */
+ if (rv_this.edge_pcpu && prev && now > prev)
+ rv_edge_account(&rv_this, curr * EVENT_MAX + ev, now - prev);
+}
+#else
+static inline void rv_edge_bind(void) { }
+static inline void rv_da_edge_enter(struct da_monitor *da_mon) { }
+static inline void
+rv_da_edge_account(struct da_monitor *da_mon, enum states curr, enum events ev) { }
+#endif /* CONFIG_RV_EDGE_STAT && RV_MON_PER_CPU */
+
/*
* Hook to allow the implementation of hybrid automata: define it with a
* function that takes curr_state, event and next_state and returns true if the
@@ -113,6 +163,7 @@ static inline void da_monitor_reset(struct da_monitor *da_mon)
static inline void da_monitor_start(struct da_monitor *da_mon)
{
da_mon->curr_state = model_get_initial_state();
+ rv_da_edge_enter(da_mon);
da_monitor_init_hook(da_mon);
/* Pairs with smp_load_acquire in da_monitoring(). */
smp_store_release(&da_mon->monitoring, 1);
@@ -275,6 +326,7 @@ static inline void da_monitor_reset_state_all(void)
*/
static inline int da_monitor_init(void)
{
+ rv_edge_bind();
da_monitor_reset_state_all();
return 0;
}
@@ -696,6 +748,7 @@ static inline bool da_event(struct da_monitor *da_mon, enum events event, da_id_
if (likely(try_cmpxchg(&da_mon->curr_state, &curr_state, next_state))) {
if (!da_monitor_event_hook(da_mon, curr_state, event, next_state, id))
return false;
+ rv_da_edge_account(da_mon, curr_state, event);
da_trace_event(da_mon, model_get_state_name(curr_state),
model_get_event_name(event),
model_get_state_name(next_state),
diff --git a/tools/testing/selftests/verification/config b/tools/testing/selftests/verification/config
index 43072c1c38f4..de229777d5c8 100644
--- a/tools/testing/selftests/verification/config
+++ b/tools/testing/selftests/verification/config
@@ -1 +1,2 @@
CONFIG_RV=y
+CONFIG_RV_EDGE_STAT=y
diff --git a/tools/testing/selftests/verification/test.d/rv_edge_stats.tc b/tools/testing/selftests/verification/test.d/rv_edge_stats.tc
new file mode 100644
index 000000000000..5fb2384d938c
--- /dev/null
+++ b/tools/testing/selftests/verification/test.d/rv_edge_stats.tc
@@ -0,0 +1,32 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0-or-later
+# description: Test per-edge dwell-time statistics (stats)
+
+MON=wip
+
+check_requires "$MON:monitor"
+
+# The "stats" file is created on first enable; if it is still missing the
+# kernel was built without CONFIG_RV_EDGE_STAT, so skip.
+echo 1 > "monitors/$MON/enable"
+if [ ! -e "monitors/$MON/stats" ]; then
+ echo 0 > "monitors/$MON/enable"
+ echo "CONFIG_RV_EDGE_STAT is not enabled."
+ exit_unsupported
+fi
+
+# The first line is the column header; the body has one line per (cpu, edge).
+head -n1 "monitors/$MON/stats" | grep -q "^# cpu edge label count max_ns sum_ns"
+[ "$(grep -cvE '^#' "monitors/$MON/stats")" -gt 0 ]
+
+# Drive some scheduler activity so the automaton records transitions.
+for _ in 1 2 3 4 5 6 7 8 9 10; do
+ (true) &
+ wait
+done
+
+# At least one edge must now show a non-zero count, with sum_ns >= max_ns > 0.
+grep -vE '^#' "monitors/$MON/stats" | \
+ awk '$4 > 0 && $5 > 0 && $6 >= $5 { hit = 1 } END { exit !hit }'
+
+echo 0 > "monitors/$MON/enable"
--
2.43.0