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 <[email protected]>
---
 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 <[email protected]>
+ */
+#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


Reply via email to