mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
To: sj@kernel.org, akinobu.mita@gmail.com, damon@lists.linux.dev,
	linux-mm@kvack.org, linux-kernel@vger.kernel.org,
	linux-doc@vger.kernel.org
Cc: akpm@linux-foundation.org, corbet@lwn.net, bijan311@gmail.com,
	ajayjoshi@micron.com, honggyu.kim@sk.com, yunjeong.mun@sk.com,
	ravis.opensrc@gmail.com, rientjes@google.com, weixugc@google.com,
	jic23@kernel.org, gourry@gourry.net
Subject: [RFC PATCH v2 5/9] mm/damon/core: add per-probe-class report rings and unified drain
Date: Thu, 10 Sep 2026 10:16:19 -0700	[thread overview]
Message-ID: <20260910171623.6638-6-ravis.opensrc@gmail.com> (raw)
In-Reply-To: <20260910171623.6638-1-ravis.opensrc@gmail.com>

damon_report_access() takes a mutex, so it cannot be called from NMI
context, which is where a perf-event overflow handler runs.  Provide
per-CPU SPSC rings instead, partitioned by the globally-meaningful
probe_idx boundary into two classes:

 - Page-fault reports (probe_idx == DAMON_PROBE_IDX_NONE) use a global
   per-CPU ring.  The page_fault primitive has no damon_ctx at report
   time, so a per-context ring is not expressible; the global ring is
   drained by the single context whose page_fault primitive is enabled.

 - Perf-event reports (probe_idx >= 1) use a per-context per-CPU ring.
   The overflow handler carries a pointer to its owning ctx, so each
   perf-driven context enqueues into and drains only its own ring, and no
   cross-context ring owner is needed.

The per-ctx perf ring is allocated when the first perf probe is armed and
freed in damon_destroy_ctx() after all perf events are released, so no
in-flight NMI can reach freed storage.

damon_report_access() routes each report to the global pf ring or to the
report's ctx perf ring by probe_idx, sharing one copy of the NMI-safe
publish sequence so the barrier pairing cannot drift between the two.
Each ring has a per-CPU busy counter to detect and drop re-entrant NMI
nesting on the same CPU.  The producer writes the entry, issues smp_wmb(),
then advances head with WRITE_ONCE(); the consumer reads head and pairs
with smp_rmb() before reading the entry.

Add the kdamond consumer, kdamond_check_reported_accesses(): a dispatcher
that drains the ring(s) the context's enabled primitives feed, with the
per-ring drain body parameterised only by ring and pending mask.  Each
entry carries its own probe_idx, so the drain resolves the probe_hits[]
slot without a list walk.  damon_drains_ring_pf() and
damon_drains_ring_perf() name which ring a context drives, so which ring
is drained and which access-check path runs follow from the enabled
primitive rather than from whether probe weights are set.

Build a per-target sorted region snapshot once per drain so each entry is
matched to its region by binary search rather than a linear region walk.
Reports straddling a region boundary are rejected.  The address space of
the monitoring target selects which address of a report is matched, so a
context whose targets carry a pid matches the virtual address and the
others match the physical address; a report carrying no address for that
space is dropped.  For pid-target contexts, filter by thread group id so
entries from unrelated processes are not credited to the wrong target.  The
report carries that id in a field of its own, beside the thread id the
threads type sample filter matches against.

Ownership of the global pf ring is enforced under damon_lock at
damon_start(), on the live commit path, and cleared on kdamond exit.  The
perf ring needs no such guard because it is per-ctx.  Staleness uses
sample_interval rather than aggr_interval, so stale data does not inflate
hot scores across aggregation boundaries.

Co-developed-by: Akinobu Mita <akinobu.mita@gmail.com>
Signed-off-by: Akinobu Mita <akinobu.mita@gmail.com>
Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
---
 include/linux/damon.h | 106 +++++-
 mm/damon/core.c       | 820 ++++++++++++++++++++++++++++++++++++------
 2 files changed, 817 insertions(+), 109 deletions(-)

diff --git a/include/linux/damon.h b/include/linux/damon.h
index 33929a400aa7a..88a459a60b296 100644
--- a/include/linux/damon.h
+++ b/include/linux/damon.h
@@ -6,6 +6,7 @@
 #ifndef _DAMON_H_
 #define _DAMON_H_
 
+#include <asm/local.h>
 #include <linux/math64.h>
 #include <linux/memcontrol.h>
 #include <linux/mutex.h>
@@ -17,9 +18,19 @@
 #define DAMON_MIN_REGION_SZ	PAGE_SIZE
 /* Maximum number of monitoring probes. */
 #define DAMON_MAX_PROBES	(4)
+/*
+ * Sentinel value for damon_access_report.probe_idx: 0 means no probe
+ * attribution (matches zero-init of struct damon_access_report on the stack).
+ * Perf-event probe indices start at 1.
+ */
+#define DAMON_PROBE_IDX_NONE	0
 /* Max priority score for DAMON-based operation schemes */
 #define DAMOS_MAX_SCORE		(99)
 
+/* Per-CPU SPSC ring: size must be a power of two. */
+#define DAMON_REPORT_RING_SIZE	256
+#define DAMON_REPORT_RING_MASK	(DAMON_REPORT_RING_SIZE - 1)
+
 /**
  * struct damon_addr_range - Represents an address region of [@start, @end).
  * @start:	Start address of the region (inclusive).
@@ -110,7 +121,15 @@ struct damon_target {
  * @size:		The size of the accessed address range.
  * @cpu:		The id of the CPU that made the access.
  * @tid:		The task id of the task that made the access.
+ * @tgid:		The thread group id of the task that made the access.  A
+ *			monitoring target created for a process carries this id,
+ *			so it is the id a report is matched against.
  * @is_write:		Whether the access is write.
+ * @probe_idx:		Index into probe_hits[] for the reporting probe; set by
+ *			the perf-event overflow handler so the drain can credit
+ *			the correct slot without a list walk.
+ *			0 is reserved (no probe attribution; matches zero-init);
+ *			perf-event probe indices start at 1.
  *
  * Any DAMON API callers that notified access events can report the information
  * to DAMON using damon_report_access().  This struct contains the reporting
@@ -122,11 +141,53 @@ struct damon_access_report {
 	unsigned long size;
 	unsigned int cpu;
 	pid_t tid;
+	pid_t tgid;
 	bool is_write;
+	int probe_idx;
+	/*
+	 * Owning context for perf-event reports (probe_idx >= 1): the overflow
+	 * handler sets this so the producer enqueues into that ctx's own perf
+	 * ring.  NULL for page_fault reports (probe_idx == DAMON_PROBE_IDX_NONE),
+	 * which route to the global pf ring by address at drain time.
+	 */
+	struct damon_ctx *ctx;
 /* private: */
 	unsigned long report_jiffies;	/* when this report is made */
 };
 
+/**
+ * struct damon_report_ring - Per-CPU SPSC ring for NMI-safe access reports.
+ *
+ * @head:	Write index; updated by the NMI producer.
+ * @tail:	Read index; updated by the kdamond consumer.
+ * @entries:	Ring buffer entries.
+ *
+ * One ring per CPU: page-fault reports use a global set, and each context
+ * with a perf-event probe has its own set.  The producer (NMI overflow
+ * handler) writes to @head; the consumer (kdamond) reads from @tail.  Both
+ * indices are unsigned and wrap modulo DAMON_REPORT_RING_SIZE.
+ */
+struct damon_report_ring {
+	unsigned int head;	/* written by producer (NMI) */
+	unsigned int tail	/* written by consumer (kdamond) */
+		____cacheline_aligned_in_smp;
+	struct damon_access_report entries[DAMON_REPORT_RING_SIZE]
+		____cacheline_aligned_in_smp;
+};
+
+/*
+ * struct damon_target_lookup - Cached, sorted region snapshot for one target.
+ * @regions:	Array of region pointers, sorted by ar.start (address order).
+ * @nr_regions:	Number of entries in @regions.
+ *
+ * Built once per aggregation tick by damon_build_target_lookup() so the ring
+ * drain can binary-search a target's regions instead of walking the list.
+ */
+struct damon_target_lookup {
+	struct damon_region **regions;
+	unsigned int nr_regions;
+};
+
 /**
  * enum damos_action - Represents an action of a Data Access Monitoring-based
  * Operation Scheme.
@@ -874,6 +935,7 @@ struct damon_filter {
  */
 struct damon_probe {
 	unsigned int weight;
+	bool event_driven;	/* hits arrive via ring drain, not apply_probes */
 /* private: */
 	/* Preparation actions to apply to each probing memory. */
 	struct list_head preps;
@@ -1090,6 +1152,34 @@ struct damon_ctx {
 
 	/* @rnd_state:	Per-ctx PRNG state for damon_rand(). */
 	struct rnd_state rnd_state;
+
+	/* Reusable drain-loop snapshot buffer (avoids per-tick kmalloc). */
+	struct {
+		struct damon_target_lookup *lookups;
+		unsigned int nr_lookups;
+		struct damon_region **region_buf;
+		unsigned int region_buf_cap;
+	} drain_snapshot;
+
+	/*
+	 * Per-context perf-event report ring.  Unlike the page_fault primitive
+	 * (which has no ctx at report time and so uses a global ring), a perf
+	 * overflow handler is armed by -- and carries a pointer to -- its owning
+	 * ctx (damon_access_report.ctx), so its reports route to this per-ctx
+	 * ring.  This gives full per-context isolation (two perf-driven ctxs
+	 * never share a ring) and removes any need for a cross-ctx perf ring
+	 * owner guard.
+	 *
+	 * Allocated lazily when the first perf probe is armed
+	 * (damon_ctx_alloc_perf_ring, from damon_perf_probe_setup) and freed in
+	 * damon_destroy_ctx() AFTER all perf events are released, so no in-flight
+	 * NMI can reach freed storage.  perf_rings == NULL means "no perf ring
+	 * yet" and any perf report is dropped, so a build/config without a perf
+	 * source simply never allocates it (lazy alloc = zero cost when unused).
+	 */
+	struct damon_report_ring __percpu *perf_rings;
+	int __percpu *perf_ring_busy;
+	cpumask_t perf_pending;
 };
 
 /* Get a random number in [@l, @r) using @ctx's lockless PRNG. */
@@ -1208,6 +1298,7 @@ void damon_destroy_filter(struct damon_filter *f);
 
 struct damon_probe *damon_new_probe(void);
 void damon_add_probe(struct damon_ctx *ctx, struct damon_probe *probe);
+bool damon_has_event_driven_probes(struct damon_ctx *ctx);
 
 struct damon_region *damon_new_region(unsigned long start, unsigned long end);
 unsigned int damon_nr_accesses_mvsum(struct damon_region *r,
@@ -1297,7 +1388,8 @@ int damon_kdamond_pid(struct damon_ctx *ctx);
 int damon_call(struct damon_ctx *ctx, struct damon_call_control *control);
 int damos_walk(struct damon_ctx *ctx, struct damos_walk_control *control);
 
-void damon_report_access(struct damon_access_report *report);
+bool damon_report_access(struct damon_access_report *report);
+int damon_ctx_alloc_perf_ring(struct damon_ctx *ctx);
 #ifdef CONFIG_MMU
 void damon_report_page_fault(struct vm_fault *vmf, bool huge_pmd);
 #else
@@ -1311,16 +1403,22 @@ int damon_set_region_system_rams_default(struct damon_target *t,
 				unsigned long addr_unit,
 				unsigned long min_region_sz);
 
-#ifdef CONFIG_ACMA
 
+unsigned long damon_get_report_overflow(void);
+unsigned long damon_get_report_ring_full(void);
+unsigned long damon_get_report_busy_drop(void);
+unsigned long damon_get_samples_drained(void);
+unsigned long damon_get_samples_stale_drained(void);
+unsigned long damon_get_samples_no_region(void);
+#ifdef CONFIG_ACMA
 unsigned long damon_alloced_bytes(void);
-
 #endif
 
 #else	/* CONFIG_DAMON */
 
-static inline void damon_report_access(struct damon_access_report *report)
+static inline bool damon_report_access(struct damon_access_report *report)
 {
+	return false;
 }
 static inline void damon_report_page_fault(struct vm_fault *vmf, bool huge_pmd)
 {
diff --git a/mm/damon/core.c b/mm/damon/core.c
index 6b56a3e961c4e..850880f791c9e 100644
--- a/mm/damon/core.c
+++ b/mm/damon/core.c
@@ -22,22 +22,136 @@
 #define CREATE_TRACE_POINTS
 #include <trace/events/damon.h>
 
-#define DAMON_ACCESS_REPORTS_CAP 1000
+/*
+ * Report rings are partitioned by the globally-meaningful probe_idx boundary,
+ * and the two classes live in different places:
+ *
+ *   page-fault reports (probe_idx == DAMON_PROBE_IDX_NONE, 0): a GLOBAL per-CPU
+ *     ring (damon_report_rings_pf).  The page_fault primitive has no damon_ctx
+ *     at report time, so a per-context ring is not expressible; the global ring
+ *     is drained by the single context whose page_fault primitive is enabled
+ *     (damon_report_ring_owner_pf).
+ *   perf-event reports (probe_idx >= 1): a PER-CONTEXT per-CPU ring
+ *     (ctx->perf_rings).  The overflow handler carries the owning ctx, so each
+ *     perf-driven context drains only its own ring -- no global ring and no
+ *     cross-context owner needed.  See struct damon_ctx.
+ *
+ * Each ring (the global pf ring, and every context's perf ring) has its OWN
+ * per-CPU storage, per-CPU busy flag, pending cpumask, and overflow counter;
+ * the producer/consumer barrier sequence in damon_report_access() is shared so
+ * the pairing cannot drift between the two.
+ */
+/*
+ * Report drops are counted per reason.  A ring-full drop means the consumer
+ * did not keep up with the producer; a busy-guard drop means an NMI nested on
+ * top of a same-CPU producer.
+ */
+static DEFINE_PER_CPU(unsigned long, damon_report_ring_full_pf);
+static DEFINE_PER_CPU(unsigned long, damon_report_ring_full_perf);
+static DEFINE_PER_CPU(unsigned long, damon_report_busy_drop_pf);
+static DEFINE_PER_CPU(unsigned long, damon_report_busy_drop_perf);
+static DEFINE_PER_CPU(unsigned long, damon_samples_drained);
+static DEFINE_PER_CPU(unsigned long, damon_samples_stale_drained);
+static DEFINE_PER_CPU(unsigned long, damon_samples_no_region);
+
+static DEFINE_PER_CPU(struct damon_report_ring, damon_report_rings_pf);
+static DEFINE_PER_CPU(int, damon_report_ring_busy_pf);
+/*
+ * Producer (NMI) sets after publishing a report; consumer (kdamond) clears
+ * before draining the corresponding ring.  Hot-write under sampling load -
+ * do NOT mark __read_mostly.  One pending mask per ring.
+ */
+static cpumask_t damon_rings_pending_pf;
+
+unsigned long damon_get_report_ring_full(void)
+{
+	unsigned long sum = 0;
+	int cpu;
+
+	for_each_possible_cpu(cpu) {
+		sum += per_cpu(damon_report_ring_full_pf, cpu);
+		sum += per_cpu(damon_report_ring_full_perf, cpu);
+	}
+	return sum;
+}
+EXPORT_SYMBOL_GPL(damon_get_report_ring_full);
+
+unsigned long damon_get_report_busy_drop(void)
+{
+	unsigned long sum = 0;
+	int cpu;
+
+	for_each_possible_cpu(cpu) {
+		sum += per_cpu(damon_report_busy_drop_pf, cpu);
+		sum += per_cpu(damon_report_busy_drop_perf, cpu);
+	}
+	return sum;
+}
+EXPORT_SYMBOL_GPL(damon_get_report_busy_drop);
+
+/* Reports dropped for either reason.  Kept so existing users need no change. */
+unsigned long damon_get_report_overflow(void)
+{
+	return damon_get_report_ring_full() + damon_get_report_busy_drop();
+}
+EXPORT_SYMBOL_GPL(damon_get_report_overflow);
+
+unsigned long damon_get_samples_drained(void)
+{
+	unsigned long sum = 0;
+	int cpu;
+
+	for_each_possible_cpu(cpu)
+		sum += per_cpu(damon_samples_drained, cpu);
+	return sum;
+}
+EXPORT_SYMBOL_GPL(damon_get_samples_drained);
+
+unsigned long damon_get_samples_stale_drained(void)
+{
+	unsigned long sum = 0;
+	int cpu;
+
+	for_each_possible_cpu(cpu)
+		sum += per_cpu(damon_samples_stale_drained, cpu);
+	return sum;
+}
+EXPORT_SYMBOL_GPL(damon_get_samples_stale_drained);
+
+unsigned long damon_get_samples_no_region(void)
+{
+	unsigned long sum = 0;
+	int cpu;
 
+	for_each_possible_cpu(cpu)
+		sum += per_cpu(damon_samples_no_region, cpu);
+	return sum;
+}
+EXPORT_SYMBOL_GPL(damon_get_samples_no_region);
 static DEFINE_MUTEX(damon_lock);
 static int nr_running_ctxs;
 static bool running_exclusive_ctxs;
 
+/*
+ * Single-consumer owner of each global report ring.  A ring is a destructive
+ * SPSC channel that must be drained by exactly one kdamond.  The pf ring is
+ * claimed by the (single) ctx whose page_fault primitive is enabled; the perf
+ * ring by the (single) ctx that has event-driven probes.  Because the two
+ * rings are independent, they have independent owners: distinct ctxs may
+ * concurrently own the pf ring and the perf ring, but no ring may be shared
+ * by two draining ctxs.  Accessed only under damon_lock.
+ *
+ * Only the pf ring has a global owner: it is a global ring with no ctx at
+ * report time.  The perf ring is per-ctx (ctx->perf_rings), so it needs no
+ * cross-ctx owner -- each ctx drains exclusively its own perf ring.
+ */
+static struct damon_ctx *damon_report_ring_owner_pf;
+
 static DEFINE_MUTEX(damon_ops_lock);
 static struct damon_operations damon_registered_ops[NR_DAMON_OPS];
 
 static struct kmem_cache *damon_region_cache __ro_after_init;
 
-static DEFINE_MUTEX(damon_access_reports_lock);
-static struct damon_access_report damon_access_reports[
-	DAMON_ACCESS_REPORTS_CAP];
-static int damon_access_reports_len;
-
 /* Should be called under damon_ops_lock with id smaller than NR_DAMON_OPS */
 static bool __damon_is_registered_ops(enum damon_ops_id id)
 {
@@ -288,6 +402,38 @@ static bool damon_has_probe_weights(struct damon_ctx *c)
 	return false;
 }
 
+/**
+ * damon_has_event_driven_probes() - return true if @ctx has any event-driven
+ * probes registered.
+ *
+ * Event-driven probes (e.g. perf-event IBS/PEBS) populate probe_hits[] via
+ * the SPSC ring drain rather than the apply_probes vtable.  Callers use this
+ * to decide whether to arm hardware sampling.
+ */
+bool damon_has_event_driven_probes(struct damon_ctx *ctx)
+{
+	struct damon_probe *p;
+
+	damon_for_each_probe(p, ctx) {
+		if (p->event_driven)
+			return true;
+	}
+	return false;
+}
+EXPORT_SYMBOL_GPL(damon_has_event_driven_probes);
+
+/* Does @ctx drive (and thus need exclusive drain of) the pf report ring? */
+static bool damon_drains_ring_pf(struct damon_ctx *ctx)
+{
+	return ctx->sample_control.primitives_enabled.page_fault;
+}
+
+/* Does @ctx drive (and thus need exclusive drain of) the perf report ring? */
+static bool damon_drains_ring_perf(struct damon_ctx *ctx)
+{
+	return damon_has_event_driven_probes(ctx);
+}
+
 /*
  * damon_mvsum() - Returns pseudo moving sum value for a time window.
  * @current_nr:		The value of the current aggregation window.
@@ -973,13 +1119,6 @@ static struct damon_sample_filter *damon_nth_sample_filter(int n,
 	return NULL;
 }
 
-static struct damon_sample_filter *damon_last_sample_filter_or_null(
-		struct damon_sample_control *ctrl)
-{
-	return list_last_entry_or_null(&ctrl->sample_filters,
-			struct damon_sample_filter, list);
-}
-
 struct damon_ctx *damon_new_ctx(void)
 {
 	struct damon_ctx *ctx;
@@ -1024,6 +1163,40 @@ struct damon_ctx *damon_new_ctx(void)
 	return ctx;
 }
 
+/*
+ * Lazily allocate the per-ctx perf report ring.  Called from the perf probe
+ * setup path BEFORE any perf event is armed, so an overflow can never observe
+ * a half-built ring.  Idempotent: a ctx with several perf probes allocates
+ * once.  The ring is freed in damon_destroy_ctx() after all perf events are
+ * released (damon_perf_probe_teardown), so no in-flight NMI can reach it.
+ */
+int damon_ctx_alloc_perf_ring(struct damon_ctx *ctx)
+{
+	if (ctx->perf_rings)
+		return 0;	/* already allocated for an earlier probe */
+	ctx->perf_rings = alloc_percpu(struct damon_report_ring);
+	if (!ctx->perf_rings)
+		return -ENOMEM;
+	ctx->perf_ring_busy = alloc_percpu(int);
+	if (!ctx->perf_ring_busy) {
+		free_percpu(ctx->perf_rings);
+		ctx->perf_rings = NULL;
+		return -ENOMEM;
+	}
+	cpumask_clear(&ctx->perf_pending);
+	return 0;
+}
+EXPORT_SYMBOL_GPL(damon_ctx_alloc_perf_ring);
+
+/* Free the per-ctx perf ring.  Caller must ensure no perf event is armed. */
+static void damon_ctx_free_perf_ring(struct damon_ctx *ctx)
+{
+	free_percpu(ctx->perf_rings);
+	ctx->perf_rings = NULL;
+	free_percpu(ctx->perf_ring_busy);
+	ctx->perf_ring_busy = NULL;
+}
+
 static void damon_destroy_targets(struct damon_ctx *ctx)
 {
 	struct damon_target *t, *next_t;
@@ -1049,6 +1222,16 @@ void damon_destroy_ctx(struct damon_ctx *ctx)
 	damon_for_each_sample_filter_safe(f, next_f, &ctx->sample_control)
 		damon_destroy_sample_filter(f, &ctx->sample_control);
 
+	/*
+	 * All perf events were released by damon_perf_probe_teardown() in the
+	 * probe loop above, so no overflow handler can still reach the ring.
+	 * Safe to free now, before kfree(ctx).  No-op if never allocated.
+	 */
+	damon_ctx_free_perf_ring(ctx);
+
+	/* Free the reusable ring-drain region snapshot buffers. */
+	kfree(ctx->drain_snapshot.lookups);
+	kfree(ctx->drain_snapshot.region_buf);
 	kfree(ctx);
 }
 
@@ -2061,7 +2244,8 @@ static int damon_commit_sample_control(
 	return damon_commit_sample_filters(dst, src);
 }
 
-static int __damon_commit_ctx(struct damon_ctx *dst, struct damon_ctx *src)
+static int __damon_commit_ctx(struct damon_ctx *dst, struct damon_ctx *src,
+		bool commit_live)
 {
 	int err;
 	struct damos *scheme;
@@ -2116,6 +2300,37 @@ static int __damon_commit_ctx(struct damon_ctx *dst, struct damon_ctx *src)
 			&src->sample_control);
 	if (err)
 		return err;
+
+	/*
+	 * A running ctx that (still) drives a global report ring must hold sole
+	 * ownership of it; enforce it here since damon_start()'s claim only
+	 * covers the not-running -> running transition.  This runs only for the
+	 * live commit (commit_live) into the running dst; dst's probes and
+	 * sample_control were already committed above, so damon_drains_ring_*()
+	 * reflect the post-commit config.  A not-running dst claims ownership
+	 * later, in damon_start().  Each ring is claimed/released independently.
+	 */
+	if (commit_live && damon_is_running(dst)) {
+		mutex_lock(&damon_lock);
+		if (damon_drains_ring_pf(dst)) {
+			if (damon_report_ring_owner_pf &&
+					damon_report_ring_owner_pf != dst) {
+				mutex_unlock(&damon_lock);
+				return -EBUSY;
+			}
+			damon_report_ring_owner_pf = dst;
+		} else if (damon_report_ring_owner_pf == dst) {
+			damon_report_ring_owner_pf = NULL;
+		}
+		/*
+		 * No perf ring owner check: the perf ring is per-ctx, so a live
+		 * commit that (re)configures perf probes never contends a shared
+		 * ring.  This is what lets a running perf-driven ctx accept a
+		 * commit without the cross-ctx -EBUSY the global ring imposed.
+		 */
+		mutex_unlock(&damon_lock);
+	}
+
 	dst->addr_unit = src->addr_unit;
 	dst->min_region_sz = src->min_region_sz;
 
@@ -2131,7 +2346,7 @@ static struct damon_ctx *damon_new_test_ctx(struct damon_ctx *dst)
 	test_ctx = damon_new_ctx();
 	if (!test_ctx)
 		return NULL;
-	err = __damon_commit_ctx(test_ctx, dst);
+	err = __damon_commit_ctx(test_ctx, dst, false);
 	if (err) {
 		damon_destroy_ctx(test_ctx);
 		return NULL;
@@ -2160,10 +2375,10 @@ int damon_commit_ctx(struct damon_ctx *dst, struct damon_ctx *src)
 	test_ctx = damon_new_test_ctx(dst);
 	if (!test_ctx)
 		return -ENOMEM;
-	err = __damon_commit_ctx(test_ctx, src);
+	err = __damon_commit_ctx(test_ctx, src, false);
 	if (err)
 		goto out;
-	err = __damon_commit_ctx(dst, src);
+	err = __damon_commit_ctx(dst, src, true);
 out:
 	damon_destroy_ctx(test_ctx);
 	return err;
@@ -2257,6 +2472,37 @@ static int __damon_start(struct damon_ctx *ctx)
 	return err;
 }
 
+/*
+ * Claim the owner of ONE global report ring for a damon_start() batch.
+ * @drains:	per-ring predicate (damon_drains_ring_pf / _perf).
+ * @owner:	per-ring owner slot.
+ *
+ * A ring is a destructive drain owned by exactly one kdamond.  Reject the
+ * start (-EBUSY) if another already-running ctx owns this ring, or if more
+ * than one ctx in this batch drains it.  On success @owner points at the sole
+ * draining ctx (or is left NULL if no batch ctx drains this ring).
+ * Must be called under damon_lock.  Returns 0 or -EBUSY.
+ */
+static int damon_claim_ring_owner_start(struct damon_ctx **ctxs, int nr_ctxs,
+		bool (*drains)(struct damon_ctx *), struct damon_ctx **owner)
+{
+	int i, j;
+
+	for (i = 0; i < nr_ctxs; i++) {
+		if (!drains(ctxs[i]))
+			continue;
+		if (*owner)
+			return -EBUSY;
+		for (j = i + 1; j < nr_ctxs; j++) {
+			if (drains(ctxs[j]))
+				return -EBUSY;
+		}
+		*owner = ctxs[i];
+		break;
+	}
+	return 0;
+}
+
 /**
  * damon_start() - Starts the monitorings for a given group of contexts.
  * @ctxs:	an array of the pointers for contexts to start monitoring
@@ -2284,7 +2530,7 @@ int damon_start(struct damon_ctx **ctxs, int nr_ctxs, bool exclusive)
 		if (!test_ctx)
 			return -ENOMEM;
 
-		err = __damon_commit_ctx(test_ctx, ctxs[i]);
+		err = __damon_commit_ctx(test_ctx, ctxs[i], false);
 		damon_destroy_ctx(test_ctx);
 		if (err)
 			return err;
@@ -2297,11 +2543,43 @@ int damon_start(struct damon_ctx **ctxs, int nr_ctxs, bool exclusive)
 		return -EBUSY;
 	}
 
-	for (i = 0; i < nr_ctxs; i++) {
-		err = __damon_start(ctxs[i]);
-		if (err)
-			break;
-		nr_running_ctxs++;
+	/*
+	 * Each global report ring is drained by exactly one kdamond.  Claim the
+	 * pf and perf ring owners independently; distinct ctxs may own the two
+	 * rings, but neither ring may be shared by two draining ctxs.  Remember
+	 * whether this batch newly set each owner so the failure paths release
+	 * only those, never another batch's still-running owner.
+	 */
+	{
+		struct damon_ctx *prev_owner_pf = damon_report_ring_owner_pf;
+
+		err = damon_claim_ring_owner_start(ctxs, nr_ctxs,
+				damon_drains_ring_pf,
+				&damon_report_ring_owner_pf);
+		if (err) {
+			/* Release only owners this batch just claimed. */
+			if (damon_report_ring_owner_pf != prev_owner_pf)
+				damon_report_ring_owner_pf = prev_owner_pf;
+			mutex_unlock(&damon_lock);
+			return err;
+		}
+
+		for (i = 0; i < nr_ctxs; i++) {
+			err = __damon_start(ctxs[i]);
+			if (err)
+				break;
+			nr_running_ctxs++;
+		}
+		/*
+		 * A ctx that failed to start has no running kdamond, so its
+		 * exit path will not release the ring owner assigned above.
+		 * Release each owner set above whose kdamond is not running.
+		 */
+		if (err) {
+			if (damon_report_ring_owner_pf != prev_owner_pf &&
+					!damon_report_ring_owner_pf->kdamond)
+				damon_report_ring_owner_pf = prev_owner_pf;
+		}
 	}
 	if (exclusive && nr_running_ctxs)
 		running_exclusive_ctxs = true;
@@ -2483,30 +2761,121 @@ int damos_walk(struct damon_ctx *ctx, struct damos_walk_control *control)
  * damon_report_access() - Report identified access events to DAMON.
  * @report:	The reporting access information.
  *
- * Report access events to DAMON.
+ * Report access events to DAMON via a per-CPU SPSC lockless ring.  Producer
+ * is the local CPU (typically NMI from a hardware-sampling backend);
+ * consumer is the kdamond drain in kdamond_check_reported_accesses().
+ *
+ * The destination ring is selected by this_cpu_ptr(), i.e. by the CPU calling
+ * this function, not by @report->cpu, which is sample metadata used by the
+ * drain-side filter.  The two coincide for a sample delivered by an interrupt
+ * on the CPU that produced it.
  *
- * Context: May sleep.
+ * A backend whose PMU writes a record stream into a memory buffer instead of
+ * raising a per-sample interrupt, or one reading a device counter table, must
+ * therefore decode CPU N's buffer on CPU N -- for example by queueing per-CPU
+ * work with queue_work_on() -- rather than calling this function in a loop
+ * from one thread.  A single-thread loop puts every report in that thread's
+ * ring, which caps machine-wide capacity at DAMON_REPORT_RING_SIZE - 1
+ * reports per drain regardless of the number of producing CPUs, and does not
+ * satisfy the single-producer invariant if the thread can migrate.
  *
- * NOTE: we may be able to implement this as a lockless queue, and allow any
- * context.  As the overhead is unknown, and region-based DAMON logics would
- * guarantee the reports would be not made that frequently, let's start with
- * this simple implementation.
+ * Context: any (NMI-safe).  An NMI nesting on top of a process-context
+ * producer on the same CPU would otherwise stomp the same entries[head]
+ * slot; the busy guard detects and drops in that case.
+ *
+ * If the ring is full, the sample is dropped and the per-CPU ring-full
+ * counter incremented; a busy-guard drop increments the busy-drop counter.
+ *
+ * Return: true if the report was queued, false if it was dropped.  A producer
+ * holding a single report may ignore this.  A producer decoding a batch out
+ * of a hardware buffer should stop on false and leave the remainder in that
+ * buffer for the next round, since a report released from the buffer but not
+ * queued here is not delivered.
  */
-void damon_report_access(struct damon_access_report *report)
+bool damon_report_access(struct damon_access_report *report)
 {
-	struct damon_access_report *dst;
+	/*
+	 * Route by the globally-meaningful probe_idx boundary:
+	 *   probe_idx == DAMON_PROBE_IDX_NONE (0) -> pf ring   (page_fault)
+	 *   probe_idx >= 1                        -> perf ring (event probes)
+	 * The two rings are fully independent (own storage, busy flag, pending
+	 * mask, overflow counter); only the selected per-CPU symbols differ.
+	 * The NMI ordering sequence below is a single copy shared by both rings
+	 * so the subtle producer/consumer barrier pairing cannot drift.
+	 */
+	bool is_perf = report->probe_idx != DAMON_PROBE_IDX_NONE;
+	struct damon_report_ring *ring;
+	cpumask_t *pending;
+	int __percpu *busy_pcpu;
+	unsigned int head, next;
+	int busy;
+	bool queued = false;
+	struct damon_ctx *pctx = is_perf ? report->ctx : NULL;
 
-	/* silently fail for races */
-	if (!mutex_trylock(&damon_access_reports_lock))
-		return;
-	dst = &damon_access_reports[damon_access_reports_len++];
-	/* just drop all existing reports in favor of simplicity. */
-	if (damon_access_reports_len == DAMON_ACCESS_REPORTS_CAP)
-		damon_access_reports_len = 0;
-	*dst = *report;
-	dst->report_jiffies = jiffies;
-	mutex_unlock(&damon_access_reports_lock);
+	/*
+	 * A perf report must carry its owning ctx (set by the overflow handler)
+	 * and that ctx must have an allocated per-ctx perf ring.  If either is
+	 * missing (e.g. an overflow racing teardown after the ring was freed, or
+	 * a report raised before the ring was allocated), drop the sample rather
+	 * than touch NULL/freed storage.
+	 */
+	if (is_perf && (!pctx || !pctx->perf_rings || !pctx->perf_ring_busy))
+		return false;
+
+	/* Pin to a CPU so the SPSC invariant holds for preemptible callers. */
+	preempt_disable();
+	if (is_perf)
+		busy_pcpu = pctx->perf_ring_busy;
+	else
+		busy_pcpu = &damon_report_ring_busy_pf;
+	busy = this_cpu_inc_return(*busy_pcpu);
+	if (busy != 1) {
+		/* NMI nested on a process-context producer; drop. */
+		if (is_perf)
+			this_cpu_inc(damon_report_busy_drop_perf);
+		else
+			this_cpu_inc(damon_report_busy_drop_pf);
+		goto out;
+	}
+
+	if (is_perf) {
+		ring = this_cpu_ptr(pctx->perf_rings);
+		pending = &pctx->perf_pending;
+	} else {
+		ring = this_cpu_ptr(&damon_report_rings_pf);
+		pending = &damon_rings_pending_pf;
+	}
+	head = ring->head;
+	next = (head + 1) & DAMON_REPORT_RING_MASK;
+
+	if (next == READ_ONCE(ring->tail)) {
+		if (is_perf)
+			this_cpu_inc(damon_report_ring_full_perf);
+		else
+			this_cpu_inc(damon_report_ring_full_pf);
+		goto out;
+	}
+
+	ring->entries[head] = *report;
+	ring->entries[head].report_jiffies = jiffies;
+	smp_wmb(); /* publish entry before head advance */
+	WRITE_ONCE(ring->head, next);
+	/*
+	 * Order the head advance before publishing the pending bit so
+	 * that the consumer, on observing the bit, is also guaranteed
+	 * to observe the new head.  cpumask_set_cpu / set_bit are
+	 * documented as unordered RMW (atomic_bitops.txt), hence the
+	 * explicit barrier.
+	 */
+	smp_mb__before_atomic();
+	cpumask_set_cpu(smp_processor_id(), pending);
+	queued = true;
+out:
+	this_cpu_dec(*busy_pcpu);
+	preempt_enable();
+	return queued;
 }
+EXPORT_SYMBOL_GPL(damon_report_access);
 
 #ifdef CONFIG_MMU
 void damon_report_page_fault(struct vm_fault *vmf, bool huge_pmd)
@@ -2515,7 +2884,9 @@ void damon_report_page_fault(struct vm_fault *vmf, bool huge_pmd)
 		.vaddr = vmf->address,
 		.size = 1,	/* todo: set appripriately */
 		.tid = task_pid_vnr(current),
+		.tgid = task_tgid_vnr(current),
 		.is_write = vmf->flags & FAULT_FLAG_WRITE,
+		/* probe_idx intentionally zero (DAMON_PROBE_IDX_NONE): no probe */
 	};
 
 	if (huge_pmd)
@@ -4187,6 +4558,24 @@ static void kdamond_init_ctx(struct damon_ctx *ctx)
 	}
 }
 
+static unsigned int kdamond_apply_zero_access_report(struct damon_ctx *ctx)
+{
+	struct damon_target *t;
+	struct damon_region *r;
+	unsigned int max_nr_accesses = 0;
+
+	damon_for_each_target(t, ctx) {
+		damon_for_each_region(r, t) {
+			if (r->access_reported)
+				r->access_reported = false;
+			else
+				damon_update_region_access_rate(r, false);
+			max_nr_accesses = max(max_nr_accesses, r->nr_accesses);
+		}
+	}
+	return max_nr_accesses;
+}
+
 static bool damon_sample_filter_matching(struct damon_access_report *report,
 		struct damon_sample_filter *filter)
 {
@@ -4214,89 +4603,301 @@ static bool damon_sample_filter_matching(struct damon_access_report *report,
 	return matched == filter->matching;
 }
 
+/*
+ * Decide whether a drained report should be dropped per the ctx sample
+ * filters.  A matching "!allow" (filter-out) filter drops the report; if no
+ * filter matched, the last filter's @allow acts as the default policy.
+ */
 static bool damon_sample_filter_out(struct damon_access_report *report,
 		struct damon_sample_control *ctrl)
 {
-	struct damon_sample_filter *filter;
+	struct damon_sample_filter *filter, *last = NULL;
 
 	damon_for_each_sample_filter(filter, ctrl) {
+		last = filter;
 		if (damon_sample_filter_matching(report, filter) &&
 				!filter->allow)
 			return true;
 	}
-	filter = damon_last_sample_filter_or_null(ctrl);
-	if (!filter)
+	if (!last)
 		return false;
-	return !filter->allow;
+	return !last->allow;
 }
 
-static void kdamond_apply_access_report(struct damon_access_report *report,
-		struct damon_target *t, struct damon_ctx *ctx)
+/*
+ * Build a snapshot of the ctx's targets and their region arrays for use by
+ * the ring drain loop.  The snapshot buffer is reused across ticks, grown via
+ * krealloc only when a new high water mark is reached.
+ *
+ * The two-pass walk over adaptive_targets is safe even though krealloc_array()
+ * may sleep: target list mutation is funneled through damon_call onto the
+ * kdamond itself, so no other thread can mutate the list while kdamond runs
+ * this function.  Regions within a target are kept address-sorted by DAMON, so
+ * the snapshot arrays are directly binary-searchable.
+ */
+static struct damon_target_lookup *damon_build_target_lookup(
+		struct damon_ctx *ctx, unsigned int *nr_targets_out)
 {
-	struct damon_region *r;
-	unsigned long addr;
+	struct damon_target *t;
+	struct damon_target_lookup *tbl;
+	unsigned int nr_targets = 0, total_regions = 0, ti = 0, ri = 0;
 
-	if (damon_sample_filter_out(report, &ctx->sample_control))
-		return;
-	if (damon_target_has_pid(ctx))
-		addr = report->vaddr;
-	else
-		addr = report->paddr;
+	damon_for_each_target(t, ctx) {
+		nr_targets++;
+		total_regions += damon_nr_regions(t);
+	}
 
-	/* todo: make search faster, e.g., binary search? */
-	damon_for_each_region(r, t) {
-		if (addr < r->ar.start)
-			continue;
-		if (r->ar.end < addr + report->size)
-			continue;
-		if (!r->access_reported)
-			damon_update_region_access_rate(r, true);
-		r->access_reported = true;
+	if (nr_targets > ctx->drain_snapshot.nr_lookups) {
+		tbl = krealloc_array(ctx->drain_snapshot.lookups,
+				nr_targets, sizeof(*tbl), GFP_KERNEL);
+		if (!tbl)
+			return NULL;
+		ctx->drain_snapshot.lookups = tbl;
+		ctx->drain_snapshot.nr_lookups = nr_targets;
+	}
+	tbl = ctx->drain_snapshot.lookups;
+
+	if (total_regions > ctx->drain_snapshot.region_buf_cap) {
+		struct damon_region **buf;
+
+		buf = krealloc_array(ctx->drain_snapshot.region_buf,
+				total_regions, sizeof(*buf), GFP_KERNEL);
+		if (!buf)
+			return NULL;
+		ctx->drain_snapshot.region_buf = buf;
+		ctx->drain_snapshot.region_buf_cap = total_regions;
+	}
+
+	damon_for_each_target(t, ctx) {
+		struct damon_region *r;
+
+		tbl[ti].regions = &ctx->drain_snapshot.region_buf[ri];
+		tbl[ti].nr_regions = damon_nr_regions(t);
+		damon_for_each_region(r, t)
+			ctx->drain_snapshot.region_buf[ri++] = r;
+		ti++;
 	}
+
+	*nr_targets_out = nr_targets;
+	return tbl;
 }
 
-static unsigned int kdamond_apply_zero_access_report(struct damon_ctx *ctx)
-{
+/*
+ * Binary-search a sorted region snapshot for the region containing @addr and,
+ * on a hit, credit the report to @pidx.  Returns true if a region was credited
+ * (straddling reports that spill past the region end are rejected).
+ */
+static bool damon_credit_report_bsearch(struct damon_region **regions,
+		unsigned int nr_regions, unsigned long addr,
+		unsigned long size, int pidx)
+{
+	struct damon_region *r = NULL;
+	int left = 0, right = (int)nr_regions - 1, mid;
+
+	while (left <= right) {
+		/* Avoid (left + right) overflow at large nr_regions. */
+		mid = left + (right - left) / 2;
+		if (addr < regions[mid]->ar.start)
+			right = mid - 1;
+		else if (addr >= regions[mid]->ar.end)
+			left = mid + 1;
+		else {
+			r = regions[mid];
+			break;
+		}
+	}
+	if (!r)
+		return false;
+	/* Reject reports straddling a region boundary. */
+	if (addr + size > r->ar.end)
+		return false;
+
+	/*
+	 * pidx == 0 (page_fault / non-probe): credit access rate only.
+	 * Ring probe_idx is 1-based (0 == DAMON_PROBE_IDX_NONE sentinel), but
+	 * probe_hits[] storage is 0-based to match all readers (wsum, mvsum,
+	 * update, aggregate reset, merge). Convert here: probe_hits[pidx - 1].
+	 */
+	if (pidx > 0)
+		r->probe_hits[pidx - 1]++;
+	damon_update_region_access_rate(r, true);
+	r->access_reported = true;
+	return true;
+}
+
+/*
+ * __kdamond_drain_ring - drain ONE global per-CPU SPSC ring into region probe_hits.
+ * @ctx:		draining context (owns @ring for this run).
+ * @tbl:		pre-built sorted per-target region snapshot (shared).
+ * @ring_pcpu:		the per-CPU ring base (pf or perf ring).
+ * @pending:		the matching per-ring pending cpumask.
+ *
+ * The global page-fault ring and each context's per-CPU perf ring
+ * (ctx->perf_rings) are partitioned by probe_idx but drained by
+ * an identical loop; this helper is that loop, parameterised only by which
+ * ring + pending mask to consume.  The bsearch / straddle-reject / tid-filter /
+ * stale-window / sample-filter / probe_hits-crediting logic is unchanged from
+ * the prior single-ring drain.
+ *
+ * Each ring entry carries its own probe_idx (set by the overflow handler), so
+ * no list walk is needed to resolve the probe_hits[] slot.
+ *
+ * A per-target sorted region snapshot is built once per drain (by the caller)
+ * so each entry is matched to its region via O(log R) binary search rather
+ * than a linear damon_for_each_region() walk.  Iterates the ring's pending
+ * cpumask to drain only CPUs with published reports.
+ */
+static void __kdamond_drain_ring(struct damon_ctx *ctx,
+		struct damon_target_lookup *tbl,
+		struct damon_report_ring __percpu *ring_pcpu,
+		cpumask_t *pending)
+{
+	int cpu;
+	struct damon_report_ring *ring;
+	unsigned int tail, head;
+	struct damon_access_report *entry;
 	struct damon_target *t;
-	struct damon_region *r;
-	unsigned int max_nr_accesses = 0;
+	unsigned long match_addr;
+	bool found;
+	unsigned int ti;
 
-	damon_for_each_target(t, ctx) {
-		damon_for_each_region(r, t) {
-			if (r->access_reported)
-				r->access_reported = false;
+	/*
+	 * Unified paddr/vaddr drain.  The address space of the monitoring
+	 * target selects which address of the report is matched: contexts
+	 * whose targets carry a pid are monitoring a virtual address space and
+	 * match report->vaddr, the others match report->paddr.
+	 *
+	 * For pid-target contexts, filter by thread group id so entries from
+	 * unrelated processes are not credited to the wrong target.
+	 * damon_target_has_pid(ctx) gates this filter: it is false for paddr
+	 * ops, whose targets have no pid, so paddr crediting is unfiltered.
+	 */
+	for_each_cpu(cpu, pending) {
+		ring = per_cpu_ptr(ring_pcpu, cpu);
+		cpumask_clear_cpu(cpu, pending);
+		/*
+		 * Pair with the producer's smp_mb__before_atomic() between
+		 * the head publish and cpumask_set_cpu(): order the bit clear
+		 * before the head read so a producer publishing between the
+		 * clear and the READ_ONCE(head) is observed via the bit it
+		 * re-sets, not lost as a stale-head drain.
+		 */
+		smp_mb__after_atomic();
+		head = READ_ONCE(ring->head);
+		smp_rmb(); /* pair with smp_wmb in producer */
+		tail = ring->tail;
+
+		while (tail != head) {
+			unsigned long stale_before;
+			int pidx;
+
+			entry = &ring->entries[tail];
+			/*
+			 * Use sample_interval (not aggr_interval) as the
+			 * staleness window: entries older than one sample
+			 * interval are from a previous monitoring tick and
+			 * should not inflate the current aggregation window.
+			 */
+			stale_before = jiffies -
+				usecs_to_jiffies(ctx->attrs.sample_interval);
+			if (time_before(entry->report_jiffies, stale_before)) {
+				this_cpu_inc(damon_samples_stale_drained);
+				goto next;
+			}
+			pidx = entry->probe_idx;
+			/*
+			 * probe_idx == 0 (DAMON_PROBE_IDX_NONE) is the
+			 * page_fault / non-probe credit path: no probe_hits[]
+			 * slot, but it still credits the region access rate.
+			 * Reject only out-of-range indices (>= DAMON_MAX_PROBES)
+			 * and, defensively, any negative value.
+			 */
+			if (pidx < 0 || pidx >= DAMON_MAX_PROBES)
+				goto next;
+
+			/* Drop reports rejected by the ctx sample filters. */
+			if (damon_sample_filter_out(entry, &ctx->sample_control))
+				goto next;
+
+			/*
+			 * Select the address that matches the address space
+			 * the targets of this context are monitoring.  A
+			 * report that carries no address for that space
+			 * cannot be credited.
+			 */
+			if (damon_target_has_pid(ctx))
+				match_addr = entry->vaddr;
 			else
-				damon_update_region_access_rate(r, false);
-			max_nr_accesses = max(max_nr_accesses, r->nr_accesses);
+				match_addr = entry->paddr;
+			if (!match_addr)
+				goto next;
+
+			found = false;
+			ti = 0;
+			damon_for_each_target(t, ctx) {
+				/* pid targets: match the reporting process */
+				if (damon_target_has_pid(ctx) &&
+				    pid_vnr(t->pid) != entry->tgid) {
+					ti++;
+					continue;
+				}
+				if (damon_credit_report_bsearch(tbl[ti].regions,
+						tbl[ti].nr_regions, match_addr,
+						entry->size, pidx)) {
+					this_cpu_inc(damon_samples_drained);
+					found = true;
+					break;
+				}
+				ti++;
+			}
+			if (!found)
+				this_cpu_inc(damon_samples_no_region);
+next:
+			tail = (tail + 1) & DAMON_REPORT_RING_MASK;
 		}
+		WRITE_ONCE(ring->tail, tail);
 	}
-	return max_nr_accesses;
 }
 
-static unsigned int kdamond_check_reported_accesses(struct damon_ctx *ctx)
+/*
+ * kdamond_check_reported_accesses - drain the global report ring(s) this ctx feeds.
+ * Called from kdamond main loop after each sampling interval.
+ *
+ * The global page-fault ring and each context's per-CPU perf ring
+ * (ctx->perf_rings) are partitioned by probe_idx: the pf ring holds
+ * page_fault (probe_idx == DAMON_PROBE_IDX_NONE) reports, the perf ring holds
+ * event-driven probe (probe_idx >= 1) reports.  A ctx drains the ring(s) its
+ * enabled primitives feed:
+ *   - page_fault primitive enabled       -> drain pf ring.
+ *   - event-driven probes registered     -> drain perf ring.
+ *   - both enabled                        -> drain both.
+ *
+ * The per-target sorted region snapshot is built once and shared across both
+ * ring drains (it is ring-agnostic).
+ */
+static void kdamond_check_reported_accesses(struct damon_ctx *ctx)
 {
-	int i;
-	struct damon_access_report *report;
-	struct damon_target *t;
+	struct damon_target_lookup *tbl;
+	unsigned int nr_targets = 0;
 
-	/* currently damon_access_report supports only physical address */
-	if (damon_target_has_pid(ctx))
-		return 0;
-
-	mutex_lock(&damon_access_reports_lock);
-	for (i = 0; i < damon_access_reports_len; i++) {
-		report = &damon_access_reports[i];
-		if (time_before(report->report_jiffies,
-					jiffies -
-					usecs_to_jiffies(
-						ctx->attrs.sample_interval)))
-			continue;
-		damon_for_each_target(t, ctx)
-			kdamond_apply_access_report(report, t, ctx);
+	/*
+	 * Build the sorted region snapshot once for this drain.  If the alloc
+	 * fails, skip the drain this tick rather than falling back to a linear
+	 * scan (a missed tick self-heals; a linear scan does not).
+	 */
+	tbl = damon_build_target_lookup(ctx, &nr_targets);
+	if (!tbl) {
+		pr_warn_ratelimited(
+			"damon: target-lookup alloc failed; ring drain skipped this tick\n");
+		return;
 	}
-	mutex_unlock(&damon_access_reports_lock);
-	/* For nr_accesses_bp, absence of access should also be reported. */
-	return kdamond_apply_zero_access_report(ctx);
+
+	if (damon_drains_ring_pf(ctx))
+		__kdamond_drain_ring(ctx, tbl, &damon_report_rings_pf,
+				&damon_rings_pending_pf);
+	if (damon_drains_ring_perf(ctx))
+		__kdamond_drain_ring(ctx, tbl, ctx->perf_rings,
+				&ctx->perf_pending);
 }
 
 /*
@@ -4345,7 +4946,9 @@ static int kdamond_fn(void *data)
 
 		do_prep = ctx->ops.prep_probes && damon_has_prep(ctx);
 
-		if (!access_check_disabled && ctx->ops.prepare_access_checks)
+		/* Page-fault sampling installs its markers from this callback. */
+		if ((!access_check_disabled || damon_drains_ring_pf(ctx)) &&
+				ctx->ops.prepare_access_checks)
 			ctx->ops.prepare_access_checks(ctx);
 		if (do_prep)
 			ctx->ops.prep_probes(ctx, access_check_disabled);
@@ -4353,14 +4956,18 @@ static int kdamond_fn(void *data)
 		kdamond_usleep(sample_interval);
 		ctx->passed_sample_intervals++;
 
-		if (!access_check_disabled) {
-			/* todo: make these non-exclusive */
-			if (ctx->sample_control.primitives_enabled.page_fault)
-				max_merge_score =
-					kdamond_check_reported_accesses(ctx);
-			else if (ctx->ops.check_accesses)
-				max_merge_score = ctx->ops.check_accesses(ctx);
-		}
+		/*
+		 * Both perf-event and page-fault primitives feed damon_report_access()
+		 * into the global per-CPU ring; the same drain consumes both.
+		 */
+		if (damon_drains_ring_perf(ctx) || damon_drains_ring_pf(ctx))
+			kdamond_check_reported_accesses(ctx);
+
+		/* Page-fault sampling reports only the accessed regions. */
+		if (damon_drains_ring_pf(ctx))
+			max_merge_score = kdamond_apply_zero_access_report(ctx);
+		else if (!access_check_disabled && ctx->ops.check_accesses)
+			max_merge_score = ctx->ops.check_accesses(ctx);
 
 		if (ctx->ops.apply_probes) {
 			if (time_after_eq(ctx->passed_sample_intervals,
@@ -4475,6 +5082,9 @@ static int kdamond_fn(void *data)
 	nr_running_ctxs--;
 	if (!nr_running_ctxs && running_exclusive_ctxs)
 		running_exclusive_ctxs = false;
+	/* Release the global pf report ring if this ctx owned it. */
+	if (damon_report_ring_owner_pf == ctx)
+		damon_report_ring_owner_pf = NULL;
 	mutex_unlock(&damon_lock);
 
 	return 0;
-- 
2.43.0


  parent reply	other threads:[~2026-09-10 17:16 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-10 17:16 [RFC PATCH v2 0/9] mm/damon: hardware-sampled access reports Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 1/9] mm/damon/vaddr: support page fault access check primitive Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 2/9] mm/damon/core: read the CPU number with preemption disabled Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 3/9] mm/damon/paddr: lock the folio for the page fault primitive rmap walk Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 4/9] mm/damon: add damos_node_eligible_mem_bp tracepoint Ravi Jonnalagadda
2026-09-10 17:16 ` Ravi Jonnalagadda [this message]
2026-09-10 17:16 ` [RFC PATCH v2 6/9] mm/damon: add perf-event overflow handler feeding the report ring Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 7/9] mm/damon/ops-common: use probe-weighted score when probe weights are set Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 8/9] mm/damon: add perf_event prep for PMU-driven hotness probes Ravi Jonnalagadda
2026-09-10 17:16 ` [RFC PATCH v2 9/9] mm/damon/tests/drain-kunit: kunit for report rings and unified drain Ravi Jonnalagadda
2026-09-11  0:34 ` [RFC PATCH v2 0/9] mm/damon: hardware-sampled access reports SJ Park
2026-09-12  1:38 ` SJ Park

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260910171623.6638-6-ravis.opensrc@gmail.com \
    --to=ravis.opensrc@gmail.com \
    --cc=ajayjoshi@micron.com \
    --cc=akinobu.mita@gmail.com \
    --cc=akpm@linux-foundation.org \
    --cc=bijan311@gmail.com \
    --cc=corbet@lwn.net \
    --cc=damon@lists.linux.dev \
    --cc=gourry@gourry.net \
    --cc=honggyu.kim@sk.com \
    --cc=jic23@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=rientjes@google.com \
    --cc=sj@kernel.org \
    --cc=weixugc@google.com \
    --cc=yunjeong.mun@sk.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®