[PATCH v3 2/4] rv: add per-edge dwell-time statistics

From: Tobias Schaffner

Date: Tue Oct 20 2026 - 06:10:07 EST


Add CONFIG_RV_EDGE_STAT, an optional facility that records how long a
monitor's automaton dwells in a state before leaving it and accounts that
dwell to the edge it leaves by.

Keep the state and its entry timestamp in one word so a transition commits
both with a single cmpxchg and nested transitions cannot charge a dwell to
the wrong edge. This requires a native 64-bit cmpxchg.

The counters are per-CPU and lock-free, so a monitor's hot path can update
them without disabling interrupts and without perturbing the latency being
measured. Only the owning CPU writes them, so a reader snapshots them with
local64_read() with no IPI and no locking on the accounting path.

Storage is a static per-CPU array of STATE_MAX * EVENT_MAX edges per
monitor, exposed through a per-monitor "stats" tracefs file. Report whether
the model defines each edge, so a count on an edge that should never be
taken is visible. Nothing uses the facility yet.

Signed-off-by: Tobias Schaffner <tobias.schaffner@xxxxxxxxxxx>
---
include/linux/rv.h | 7 ++
include/rv/edge_stat.h | 211 ++++++++++++++++++++++++++++++++++++++++
kernel/trace/rv/Kconfig | 12 +++
3 files changed, 230 insertions(+)
create mode 100644 include/rv/edge_stat.h

diff --git a/include/linux/rv.h b/include/linux/rv.h
index 541ba404926a..1dc7c45f476c 100644
--- a/include/linux/rv.h
+++ b/include/linux/rv.h
@@ -21,6 +21,13 @@
#include <linux/list.h>
#include <linux/types.h>

+/* Edge statistics pack the state and its entry timestamp into one word. */
+#ifdef CONFIG_RV_EDGE_STAT
+#define da_state_t unsigned long
+#else
+#define da_state_t unsigned int
+#endif
+
/*
* Deterministic automaton per-object variables.
*/
diff --git a/include/rv/edge_stat.h b/include/rv/edge_stat.h
new file mode 100644
index 000000000000..8b99e8520e1e
--- /dev/null
+++ b/include/rv/edge_stat.h
@@ -0,0 +1,211 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Per-edge dwell-time statistics for RV monitors.
+ *
+ * Copyright (C) 2026 Siemens AG
+ * Author: Tobias Schaffner <tobias.schaffner@xxxxxxxxxxx>
+ */
+#ifndef _RV_EDGE_STAT_H
+#define _RV_EDGE_STAT_H
+
+#include <linux/rv.h>
+
+#ifdef CONFIG_RV_EDGE_STAT
+
+#include <linux/args.h>
+#include <linux/atomic.h>
+#include <linux/bitfield.h>
+#include <linux/bug.h>
+#include <linux/compiler.h>
+#include <linux/percpu.h>
+#include <linux/sched/clock.h>
+#include <linux/seq_file.h>
+#include <linux/types.h>
+#include <asm/local64.h>
+#include <rv/automata.h>
+#include "rv.h"
+
+/*
+ * Per-CPU counters kept in local64_t so accounting is safe against interrupt
+ * and NMI nesting on the owning CPU without disabling interrupts. Only the
+ * owning CPU writes.
+ */
+struct rv_edge_stat {
+ local64_t count;
+ local64_t sum_ns;
+ local64_t max_ns;
+};
+
+/*
+ * State and entry timestamp share one word so a transition commits both with
+ * one cmpxchg. Comparing the timestamp also detects nested transitions that
+ * return to the same state.
+ */
+#define RV_STATE_BITS 8
+#define RV_STATE_MASK GENMASK(RV_STATE_BITS - 1, 0)
+#define RV_TS_MASK GENMASK(BITS_PER_LONG - 1, RV_STATE_BITS)
+
+#define da_state_of(w) FIELD_GET(RV_STATE_MASK, w)
+#define da_ts_of(w) FIELD_GET(RV_TS_MASK, w)
+#define da_state_pack(s, ts) (FIELD_PREP(RV_TS_MASK, ts) | \
+ FIELD_PREP(RV_STATE_MASK, s))
+
+static __always_inline
+void rv_edge_stat_account(struct rv_edge_stat *s, u64 dwell_ns)
+{
+ s64 max;
+ int i;
+
+ local64_inc(&s->count);
+ local64_add(dwell_ns, &s->sum_ns);
+
+ /* Keep the largest dwell; bound retries if a nested update races us. */
+ max = local64_read(&s->max_ns);
+ for (i = 0; dwell_ns > (u64)max; i++) {
+ if (i == MAX_DA_RETRY_RACING_EVENTS) {
+ WARN_ONCE(1, "rv: edge-stat max update exceeded %d retries\n",
+ MAX_DA_RETRY_RACING_EVENTS);
+ break;
+ }
+ if (local64_try_cmpxchg(&s->max_ns, &max, dwell_ns))
+ break;
+ }
+}
+
+static_assert(STATE_MAX <= (1U << RV_STATE_BITS),
+ "automaton has more states than the packed state word can hold");
+
+#if RV_MON_TYPE == RV_MON_PER_CPU
+
+/*
+ * One edge per (state, event) pair. Static storage avoids allocation lifetime
+ * races. Only the owning CPU writes, so readers can snapshot counters with
+ * local64_read().
+ */
+#define RV_THIS_NR_EDGES (STATE_MAX * EVENT_MAX)
+
+#define DA_EDGE(state, event) ((state) * EVENT_MAX + (event))
+#define DA_EDGE_STATE(edge) ((edge) / EVENT_MAX)
+#define DA_EDGE_EVENT(edge) ((edge) % EVENT_MAX)
+
+#define DA_MON_EDGES CONCATENATE(da_mon_edges_, MONITOR_NAME)
+
+static DEFINE_PER_CPU(struct rv_edge_stat[RV_THIS_NR_EDGES], DA_MON_EDGES);
+static struct dentry *rv_this_stats_file;
+
+/* Called before tracepoints are registered, so memset cannot race an update. */
+static void rv_edge_stats_reset(void)
+{
+ int cpu;
+
+ for_each_possible_cpu(cpu)
+ memset(per_cpu_ptr(DA_MON_EDGES, cpu), 0, sizeof(DA_MON_EDGES));
+}
+
+static int rv_edge_stats_show(struct seq_file *seq, void *v)
+{
+ unsigned int e;
+ int cpu;
+
+ seq_puts(seq, "# cpu edge label valid count sum_ns max_ns\n");
+ /* Include offline CPUs and keep output stable across CPU hotplug. */
+ for_each_possible_cpu(cpu) {
+ struct rv_edge_stat *edges = per_cpu_ptr(DA_MON_EDGES, cpu);
+
+ for (e = 0; e < RV_THIS_NR_EDGES; e++) {
+ enum states state = DA_EDGE_STATE(e);
+ enum events event = DA_EDGE_EVENT(e);
+
+ /* Edges the model does not define must stay at zero. */
+ seq_printf(seq, "%d %u %s:%s %d %llu %llu %llu\n",
+ cpu, e,
+ model_get_state_name(state),
+ model_get_event_name(event),
+ model_get_next_state(state, event) != INVALID_STATE,
+ (u64)local64_read(&edges[e].count),
+ (u64)local64_read(&edges[e].sum_ns),
+ (u64)local64_read(&edges[e].max_ns));
+ }
+ }
+ return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(rv_edge_stats);
+
+static int rv_edge_stats_create(struct dentry *parent)
+{
+ rv_edge_stats_reset();
+ rv_this_stats_file = rv_create_file("stats", RV_MODE_READ, parent,
+ NULL, &rv_edge_stats_fops);
+ return rv_this_stats_file ? 0 : -ENOMEM;
+}
+
+static void rv_edge_stats_remove(void)
+{
+ rv_remove(rv_this_stats_file);
+ rv_this_stats_file = NULL;
+}
+
+/* A zero timestamp marks resets, which can run on a different CPU. */
+#define da_state_entered(s) da_state_pack((s), local_clock())
+
+static __always_inline void
+rv_edge_account(da_state_t old, da_state_t new, enum states curr, enum events ev)
+{
+ struct rv_edge_stat *edges;
+ u64 prev = da_ts_of(old);
+
+ if (unlikely(!prev))
+ return;
+
+ edges = this_cpu_ptr(DA_MON_EDGES);
+ rv_edge_stat_account(&edges[DA_EDGE(curr, ev)],
+ (da_ts_of(new) - prev) & FIELD_MAX(RV_TS_MASK));
+}
+
+#else /* per-cpu accounting off for this monitor type */
+
+#define da_state_entered(s) ((da_state_t)(s))
+
+static __always_inline void
+rv_edge_account(da_state_t old, da_state_t new, enum states curr, enum events ev) { }
+
+#endif /* RV_MON_TYPE == RV_MON_PER_CPU */
+
+static __always_inline bool
+da_state_try_commit(da_state_t *word, da_state_t *old, enum states next,
+ enum events event)
+{
+ da_state_t prev = *old;
+ da_state_t new = da_state_entered(next);
+
+ if (!try_cmpxchg(word, old, new))
+ return false;
+
+ rv_edge_account(prev, new, da_state_of(prev), event);
+ return true;
+}
+
+#else /* !CONFIG_RV_EDGE_STAT */
+
+/*
+ * Feature off: only the hooks the DA/HA layer calls are provided, as trivial
+ * pass-throughs. da_state_t is a plain state word with no packed timestamp.
+ */
+#define da_state_of(w) (w)
+#define da_state_entered(s) (s)
+
+static __always_inline bool
+da_state_try_commit(da_state_t *word, da_state_t *old, enum states next,
+ enum events event)
+{
+ return try_cmpxchg(word, old, next);
+}
+
+#if RV_MON_TYPE == RV_MON_PER_CPU
+static inline int rv_edge_stats_create(struct dentry *parent) { return 0; }
+static inline void rv_edge_stats_remove(void) { }
+#endif
+
+#endif /* CONFIG_RV_EDGE_STAT */
+
+#endif /* _RV_EDGE_STAT_H */
diff --git a/kernel/trace/rv/Kconfig b/kernel/trace/rv/Kconfig
index efa930f94ea4..54f7c50a74a8 100644
--- a/kernel/trace/rv/Kconfig
+++ b/kernel/trace/rv/Kconfig
@@ -59,6 +59,18 @@ config RV_PER_TASK_MONITORS
This option configures the maximum number of per-task RV monitors that can run
simultaneously.

+config RV_EDGE_STAT
+ bool "Per-edge dwell-time statistics"
+ depends on RV && 64BIT && DA_MON_EVENTS_IMPLICIT
+ help
+ Record per-edge dwell-time statistics for per-cpu deterministic and
+ hybrid automaton monitors and expose them through a per-monitor
+ "stats" tracefs file. This times each monitored automaton transition
+ with local_clock(), so leave it off if you do not need the
+ statistics.
+
+ If unsure, say N.
+
source "kernel/trace/rv/monitors/wip/Kconfig"
source "kernel/trace/rv/monitors/wwnr/Kconfig"

--
2.53.0