From: Kunwu Chan Implement the observe_*() core: per-CPU monotonic counters, the event-lifecycle hooks with lazily-allocated per-event per-CPU state alongside the global per-CPU stage, NMI-safe sample classification, ring enqueue/dequeue/peak, match/miss/update, and framework init. The global per-CPU stage advances monotonically so a late create/bind/ enable cannot regress a CPU that is already RUNNING. damon_perf_observe_sample() records the execution context (process/softirq/hardirq/NMI) so overflow callbacks can be verified to fire in the expected context. Co-developed-by: Lian Wang Signed-off-by: Lian Wang Signed-off-by: Kunwu Chan --- mm/damon/perf/Makefile | 2 + mm/damon/perf/stats.c | 295 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 mm/damon/perf/stats.c diff --git a/mm/damon/perf/Makefile b/mm/damon/perf/Makefile index cc0d4f1d1d28..5c46d3da7ef8 100644 --- a/mm/damon/perf/Makefile +++ b/mm/damon/perf/Makefile @@ -1,3 +1,5 @@ # SPDX-License-Identifier: GPL-2.0 # Observability: per-CPU counters, tracepoints, debugfs perf_stats +obj-$(CONFIG_DAMON_PERF_OBSERVE) += damon-perf.o +damon-perf-objs := stats.o diff --git a/mm/damon/perf/stats.c b/mm/damon/perf/stats.c new file mode 100644 index 000000000000..a869f115bb26 --- /dev/null +++ b/mm/damon/perf/stats.c @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * DAMON Perf Observability — Per-CPU Statistics & Observe API + * + * All hardware-sampling backends funnel through the observe functions + * defined here. Counters always increment when + * CONFIG_DAMON_PERF_OBSERVE=y; tracepoints are guarded by + * trace_*_enabled() for zero overhead when ftrace is not attached. + * + * Per-CPU counters are best-effort: individual u64 writes are atomic + * on 64-bit platforms, but no cross-field consistency is guaranteed. + * Snapshot reads may race with concurrent writers. This is debug data + * — do not build policy on it. + */ + +#include +#include + +#include + +#include "perf.h" + +/* + * Per-CPU statistics + */ +static DEFINE_PER_CPU(struct damon_perf_stats, damon_perf_stats); + +/* + * Per-CPU event state for the state machine — the maximum lifecycle + * stage ever reached on each CPU (any event). Set by the NMI + * observe_sample() (ENABLED→RUNNING) and by lifecycle functions as a + * monotonic "best CPU so far". Never reset to UNINIT — destroying one + * event must not hide that another event is still running on the same + * CPU. Per-event per-CPU state (event->cpu_state) tracks individual + * event lifecycle for correctness when multiple events exist. + */ +static DEFINE_PER_CPU(int, damon_perf_cpu_state); + +/* + * Event lifecycle — process context (kdamond / cpuhp callbacks). + * + * Each event tracks its own per-CPU lifecycle state via + * event->cpu_state (allocated on first CREATED, freed on DESTROYED). + * The global damon_perf_cpu_state is also advanced (monotonic) so that + * debugfs perf_stats shows the "best" stage any event reached on each + * CPU. Diagnostics are emitted via tracepoints, not dmesg. + */ + +/* + * Advance the global per-CPU lifecycle state monotonically. The global + * reflects the best stage any event ever reached on this CPU; a late + * create/bind/enable must not regress a CPU that is already RUNNING. + */ +static void damon_perf_cpu_state_advance(int cpu, int state) +{ + int cur = READ_ONCE(per_cpu(damon_perf_cpu_state, cpu)); + + if (state > cur) + WRITE_ONCE(per_cpu(damon_perf_cpu_state, cpu), state); +} + +void damon_perf_observe_event_created(struct damon_perf_event *event, int cpu) +{ + if (!event->cpu_state) { + event->cpu_state = alloc_percpu(int); + if (!event->cpu_state) + return; + } + *per_cpu_ptr(event->cpu_state, cpu) = DAMON_PERF_STATE_CREATED; + damon_perf_cpu_state_advance(cpu, DAMON_PERF_STATE_CREATED); +} + +void damon_perf_observe_event_bound(struct damon_perf_event *event, + int cpu, struct perf_event *perf_event) +{ + if (event->cpu_state) + *per_cpu_ptr(event->cpu_state, cpu) = DAMON_PERF_STATE_BOUND; + damon_perf_cpu_state_advance(cpu, DAMON_PERF_STATE_BOUND); +} + +void damon_perf_observe_event_enabled(struct damon_perf_event *event, + int cpu, int state, int oncpu) +{ + if (event->cpu_state) + *per_cpu_ptr(event->cpu_state, cpu) = DAMON_PERF_STATE_ENABLED; + damon_perf_cpu_state_advance(cpu, DAMON_PERF_STATE_ENABLED); +} + +void damon_perf_observe_event_disabled(struct damon_perf_event *event, + int cpu, int state) +{ + /* State unchanged: the event may be re-enabled later. */ +} + +void damon_perf_observe_event_destroyed(struct damon_perf_event *event, int cpu) +{ + /* + * Reset only this CPU's slot. The per-CPU array is owned by the + * event as a whole and must NOT be freed here: destroyed() is + * invoked from the per-CPU CPU-offline callback, so freeing the + * whole array on the first offline CPU would leave every other + * still-online CPU with a dangling pointer. The array is freed + * once by damon_perf_observe_event_free() at event teardown. + */ + if (event->cpu_state) + *per_cpu_ptr(event->cpu_state, cpu) = DAMON_PERF_STATE_UNINIT; +} + +void damon_perf_observe_event_free(struct damon_perf_event *event) +{ + if (event->cpu_state) { + free_percpu(event->cpu_state); + event->cpu_state = NULL; + } +} + +/* + * Sample observed — NMI-safe. + * + * The @cpu argument is the CPU the sample fired on; it is passed + * through to the tracepoint but stats are always written on the + * current CPU via this_cpu_ptr(). + */ + +void damon_perf_observe_sample(unsigned long addr, u64 data_src, + u64 period, int cpu, u8 reason, + u64 sample_flags, u64 sample_type) +{ + this_cpu_inc(damon_perf_stats.callback); + + switch (reason) { + case 0: + this_cpu_inc(damon_perf_stats.sample_valid); + break; + case 1: + this_cpu_inc(damon_perf_stats.sample_null); + break; + case 2: + this_cpu_inc(damon_perf_stats.sample_addr_zero); + break; + case 3: + this_cpu_inc(damon_perf_stats.sample_kernel); + break; + case 4: + this_cpu_inc(damon_perf_stats.sample_invalid_phys); + break; + } + + /* First callback advances state to RUNNING */ + if (this_cpu_read(damon_perf_cpu_state) == DAMON_PERF_STATE_ENABLED) + this_cpu_write(damon_perf_cpu_state, DAMON_PERF_STATE_RUNNING); + + /* + * Record the execution context so callers can verify whether + * overflow callbacks actually fire in the expected context + * (e.g. NMI for IBS, process for SPE AUX drain). + * + * 0 = process, 1 = softirq, 2 = hardirq, 3 = NMI + */ + if (trace_damon_perf_sample_enabled()) + trace_damon_perf_sample(addr, data_src, period, cpu, reason, + sample_flags, sample_type, + in_nmi() ? 3 : in_hardirq() ? 2 : + in_serving_softirq() ? 1 : 0); +} + +/* + * Ring operations — enqueue / overflow are NMI-safe + */ + +void damon_perf_observe_ring_enqueue(void) +{ + this_cpu_inc(damon_perf_stats.enqueue); +} + +void damon_perf_observe_ring_overflow(int cpu) +{ + this_cpu_inc(damon_perf_stats.overflow); + if (trace_damon_perf_ring_overflow_enabled()) + trace_damon_perf_ring_overflow(cpu); +} + +void damon_perf_observe_ring_dequeue(int cpu) +{ + per_cpu_ptr(&damon_perf_stats, cpu)->dequeue++; +} + +void damon_perf_observe_ring_peak(unsigned int occupancy) +{ + struct damon_perf_stats *st = this_cpu_ptr(&damon_perf_stats); + + if (occupancy > READ_ONCE(st->ring_peak)) + WRITE_ONCE(st->ring_peak, occupancy); +} + +/* + * Matching — process context (kdamond drain loop) + */ + +void damon_perf_observe_match(unsigned long addr, int cpu) +{ + per_cpu_ptr(&damon_perf_stats, cpu)->match++; +} + +void damon_perf_observe_miss(unsigned long addr, int cpu, int reason) +{ + struct damon_perf_stats *st = per_cpu_ptr(&damon_perf_stats, cpu); + + switch (reason) { + case DAMON_REPORT_MISS_TGID: + st->miss_tgid++; + break; + case DAMON_REPORT_MISS_NOREGION: + st->miss_region++; + break; + case DAMON_REPORT_MISS_BOUNDARY: + st->miss_boundary++; + break; + } + + if (trace_damon_perf_report_missed_enabled()) + trace_damon_perf_report_missed(addr, cpu, reason); +} + +void damon_perf_observe_update(int cpu) +{ + per_cpu_ptr(&damon_perf_stats, cpu)->update++; +} + +void damon_perf_observe_drain(unsigned int total, unsigned int matched) +{ + if (trace_damon_perf_drain_enabled()) + trace_damon_perf_drain(total, matched); +} + +/* + * Stats accessors (for debugfs) + */ + +/* + * Best-effort per-CPU snapshot. Individual u64 writes are atomic on + * 64-bit platforms; no cross-field consistency is guaranteed. This is + * debug data only — do not build policy on it. + */ +void damon_perf_stats_snapshot(int cpu, struct damon_perf_stats *dst) +{ + struct damon_perf_stats *st = per_cpu_ptr(&damon_perf_stats, cpu); + + *dst = *st; + dst->cpu_state = per_cpu(damon_perf_cpu_state, cpu); +} + +void damon_perf_stats_aggregate(struct damon_perf_stats *dst) +{ + int cpu; + + memset(dst, 0, sizeof(*dst)); + dst->cpu_state = DAMON_PERF_STATE_RUNNING; /* start optimistic, take min */ + cpus_read_lock(); + for_each_online_cpu(cpu) { + struct damon_perf_stats *st = per_cpu_ptr(&damon_perf_stats, cpu); + + dst->callback += st->callback; + dst->sample_valid += st->sample_valid; + dst->sample_null += st->sample_null; + dst->sample_addr_zero += st->sample_addr_zero; + dst->sample_kernel += st->sample_kernel; + dst->sample_invalid_phys += st->sample_invalid_phys; + dst->enqueue += st->enqueue; + dst->dequeue += st->dequeue; + dst->overflow += st->overflow; + dst->ring_peak = max(dst->ring_peak, st->ring_peak); + dst->match += st->match; + dst->miss_tgid += st->miss_tgid; + dst->miss_region += st->miss_region; + dst->miss_boundary += st->miss_boundary; + dst->update += st->update; + /* + * Aggregate per-CPU state: the pipeline hasn't passed a + * stage until ALL CPUs have passed it, so take the min. + */ + dst->cpu_state = min_t(int, dst->cpu_state, + per_cpu(damon_perf_cpu_state, cpu)); + } + cpus_read_unlock(); +} + +/* + * Subsystem init + */ + +int damon_perf_framework_init(void) +{ + return 0; +} -- 2.43.0