All of lore.kernel.org
 help / color / mirror / Atom feed
From: Kunwu Chan <kunwu.chan@gmail.com>
To: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
Cc: Kunwu Chan <kunwu.chan@gmail.com>, SJ Park <sj@kernel.org>,
	Andrew Morton <akpm@linux-foundation.org>,
	damon@lists.linux.dev, linux-mm@kvack.org,
	linux-kernel@vger.kernel.org, Gregory Price <gourry@gourry.net>,
	David Rientjes <rientjes@google.com>, Wei Xu <weixugc@google.com>,
	Jonathan Corbet <corbet@lwn.net>,
	Bijan Tabatabai <bijan311@gmail.com>,
	Ajay Joshi <ajayjoshi@micron.com>,
	Honggyu Kim <honggyu.kim@sk.com>,
	Yunjeong Mun <yunjeong.mun@sk.com>,
	Akinobu Mita <akinobu.mita@gmail.com>,
	Lian Wang <lianux.mm@gmail.com>,
	Kunwu Chan <kunwu.chan@linux.dev>,
	Jonathan Cameron <jic23@kernel.org>
Subject: Re: [RFC PATCH v3 2/9] mm/damon/core: replace the access report buffer with per-context rings
Date: Sun,  4 Oct 2026 16:30:24 +0800	[thread overview]
Message-ID: <20261004083036.613169-1-kunwu.chan@gmail.com> (raw)
In-Reply-To: <20261003-damon-perf-rfc-v3-send-2026-10-03-v3-2-0f00417b41bc@gmail.com>

On Sat, 03 Oct 2026 14:07:55 -0700 Ravi Jonnalagadda <ravis.opensrc@gmail.com> wrote:

> damon_report_access() queues reports into the global damon_access_reports[]
> buffer under a mutex, and kdamond applies them from there.  A perf-event
> overflow handler runs in NMI context and cannot take that mutex, and since
> the previous patch the buffer has no other producer.
> 
> Replace the buffer with a per-context, per-CPU SPSC report ring
> (ctx->perf_rings) that an NMI-context producer can publish into, and a
> kdamond drain that credits regions from each ring's pending reports.
> 
> NMI safety: the producer uses a busy counter to drop re-entrant reports on
> the same CPU, publishes with smp_wmb() before advancing the head, and sets
> a pending-CPU bitmask with smp_mb__before_atomic() so the consumer catches
> any report published between the bit-clear and the READ_ONCE(head).  The
> ring is allocated before the first PMU arm and freed after the last PMU
> release in damon_destroy_ctx(), so no in-flight NMI can reach freed
> storage.
> 
> The drain matches each report to a region by binary search over a
> per-target region snapshot built in ar.start order, the region-list
> invariant damon_credit_report_bsearch() relies on.  A credited region's
> access rate is incremented once per drained sample rather than once per
> aggregation tick, so a region that drains several samples in one tick
> reflects that instead of being clamped to the signal a lone sample gives.
> 
> While here, widen probe_hits[] and last_probe_hits[] in struct
> damon_region, and the damon_probe_hits_mvsum() return type, from unsigned
> char to unsigned int.  An unsigned char wraps at 256; a PEBS event at
> 5000 Hz overflows it within a single 1-second aggregation window.
> 
> Signed-off-by: Ravi Jonnalagadda <ravis.opensrc@gmail.com>
> ---
>  include/linux/damon.h | 110 ++++++++-
>  mm/damon/core.c       | 625 +++++++++++++++++++++++++++++++++++++++++++-------
>  2 files changed, 642 insertions(+), 93 deletions(-)
> 
> diff --git a/include/linux/damon.h b/include/linux/damon.h
> index 10582f669673..217299aa6c03 100644
> --- a/include/linux/damon.h
> +++ b/include/linux/damon.h
> @@ -17,9 +17,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).
> @@ -66,14 +76,14 @@ struct damon_region {
>  	struct damon_addr_range ar;
>  	unsigned long sampling_addr;
>  	unsigned int nr_accesses;
> -	unsigned char probe_hits[DAMON_MAX_PROBES];
> +	unsigned int  probe_hits[DAMON_MAX_PROBES];
>  	unsigned int age;
>  /* private: internal use only. */
>  	/* List head for siblings. */
>  	struct list_head list;
>  	/* for age calculation. */
>  	unsigned int last_nr_accesses;
> -	unsigned char last_probe_hits[DAMON_MAX_PROBES];
> +	unsigned int  last_probe_hits[DAMON_MAX_PROBES];
>  	bool access_reported;
>  };
>  
> @@ -110,7 +120,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 +140,54 @@ 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 a report (set by the perf-event overflow handler
> +	 * so the producer enqueues into that ctx's own perf ring).  NULL for a
> +	 * report with no owning context (probe_idx == DAMON_PROBE_IDX_NONE);
> +	 * such a report has no ring to feed and is dropped by
> +	 * damon_report_access().
> +	 */
> +	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, per context with a perf-event probe.  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;
> @@ -1091,6 +1153,32 @@ 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.  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 needs no cross-ctx 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. */
> @@ -1209,11 +1297,12 @@ 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,
>  		struct damon_ctx *ctx);
> -unsigned char damon_probe_hits_mvsum(int probe_idx, struct damon_region *r,
> +unsigned int damon_probe_hits_mvsum(int probe_idx, struct damon_region *r,
>  		struct damon_ctx *ctx);
>  unsigned int damon_probe_hits_wsum(struct damon_region *r, bool last, bool mv,
>  		struct damon_ctx *ctx);
> @@ -1298,23 +1387,30 @@ 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);
>  
>  int damon_set_region_system_rams_default(struct damon_target *t,
>  				unsigned long *start, unsigned long *end,
>  				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;
>  }
>  
>  #endif	/* CONFIG_DAMON */
> diff --git a/mm/damon/core.c b/mm/damon/core.c
> index 886e068e7844..4fd1db12bc49 100644
> --- a/mm/damon/core.c
> +++ b/mm/damon/core.c
> @@ -22,8 +22,87 @@
>  #define CREATE_TRACE_POINTS
>  #include <trace/events/damon.h>
>  
> -#define DAMON_ACCESS_REPORTS_CAP 1000
> +/*
> + * Reports are fed to DAMON via a PER-CONTEXT per-CPU SPSC 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 context's ring has its own per-CPU storage, per-CPU busy flag,
> + * pending cpumask, and overflow counter.
> + */
> +/*
> + * 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_perf);
> +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);
> +
> +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_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_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;
> @@ -33,11 +112,6 @@ 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 +362,32 @@ 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 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.
> @@ -353,7 +453,7 @@ unsigned int damon_nr_accesses_mvsum(struct damon_region *r,
>  			left_window_bp);
>  }
>  
> -unsigned char damon_probe_hits_mvsum(int probe_idx, struct damon_region *r,
> +unsigned int damon_probe_hits_mvsum(int probe_idx, struct damon_region *r,
>  		struct damon_ctx *ctx)
>  {
>  	unsigned long sample_interval, aggr_interval;
> @@ -979,7 +1079,6 @@ static struct damon_sample_filter *damon_last_sample_filter_or_null(
>  	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;
> @@ -1025,6 +1124,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;
> @@ -1050,6 +1183,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);
>  }
>  
> @@ -2519,30 +2662,107 @@ 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-context per-CPU SPSC lockless ring
> + * (ctx->perf_rings).  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.
> + *
> + * 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.
> + *
> + * 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.
>   *
> - * Context: May sleep.
> + * 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.
>   *
> - * 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.
> + * 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;
> +	/*
> +	 * Only perf-event reports (probe_idx >= 1) have a ring to feed: the
> +	 * global page_fault ring this dispatch also fed has been removed.
> +	 * A probe_idx == DAMON_PROBE_IDX_NONE report has nowhere to go and is
> +	 * dropped here rather than at each caller.
> +	 */
> +	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 = report->ctx;
> +
> +	if (report->probe_idx == DAMON_PROBE_IDX_NONE)
> +		return false;
>  
> -	/* 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 (!pctx || !pctx->perf_rings || !pctx->perf_ring_busy)
> +		return false;
> +
> +	/* Pin to a CPU so the SPSC invariant holds for preemptible callers. */
> +	preempt_disable();
> +	busy_pcpu = pctx->perf_ring_busy;
> +	busy = this_cpu_inc_return(*busy_pcpu);
> +	if (busy != 1) {
> +		/* NMI nested on a process-context producer; drop. */
> +		this_cpu_inc(damon_report_busy_drop_perf);
> +		goto out;
> +	}
> +
> +	ring = this_cpu_ptr(pctx->perf_rings);
> +	pending = &pctx->perf_pending;
> +	head = ring->head;
> +	next = (head + 1) & DAMON_REPORT_RING_MASK;
> +
> +	if (next == READ_ONCE(ring->tail)) {
> +		this_cpu_inc(damon_report_ring_full_perf);
> +		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);
>  
>  /*
>   * Reset the aggregated monitoring results ('nr_accesses' of each region).
> @@ -4202,6 +4422,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)
>  {
> @@ -4229,6 +4467,11 @@ 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)
>  {
> @@ -4245,73 +4488,276 @@ static bool damon_sample_filter_out(struct damon_access_report *report,
>  	return !filter->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 maintains each target's region list sorted by ar.start.
> +	 * damon_credit_report_bsearch() binary-searches by address, so the
> +	 * snapshot built here must preserve that order.  If the region-list
> +	 * ordering invariant ever changes, this builder must sort explicitly.
> +	 */
> +	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 is always >= 1 here: __kdamond_drain_ring rejects
> +	 * DAMON_PROBE_IDX_NONE (0) entries before calling this.  Ring
> +	 * probe_idx is 1-based, but probe_hits[] storage is 0-based to match
> +	 * all readers (wsum, mvsum, update, aggregate reset, merge).
> +	 * Convert here: probe_hits[pidx - 1].
> +	 */
> +	r->probe_hits[pidx - 1]++;
> +	damon_update_region_access_rate(r, true);

Hi Ravi,

Thanks for the updated version.

I have a question about the access count semantics with
hardware-sampled reports.

In damon_credit_report_bsearch(), each hardware report updates both
probe_hits and the region access rate through 
damon_update_region_access_rate.

Unlike page-table or page-fault based access checks, where an access
report is generated from a deterministic access observation, a
hardware-sampled report represents a statistical hardware event whose
frequency depends on the PMU configuration.

For the same workload, nr_accesses may therefore have different
scales when monitored with different PMU sampling configurations
(for example, PEBS at a given frequency versus IBS with a different
sample period).

Should we document this distinction, so that users configuring DAMOS
schemes with nr_accesses thresholds understand that the value depends
on the hardware sampling source?

Thanks,
Kunwu

> +	r->access_reported = true;
> +	return true;
> +}
> +
> +/*
> + * __kdamond_drain_ring - drain a 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 (ctx's perf ring).
> + * @pending:		the matching pending cpumask.
> + *
> + * Each context's per-CPU perf ring (ctx->perf_rings) is drained by this
> + * 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 shared by every ring this loop drains.
> + *
> + * 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;
> +			/*
> +			 * Every entry in this ring is a perf-event report
> +			 * (probe_idx >= 1); damon_report_access() drops any
> +			 * DAMON_PROBE_IDX_NONE report before it reaches a ring.
> +			 * Reject only out-of-range indices (>= DAMON_MAX_PROBES)
> +			 * and, defensively, any non-positive 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 per-ctx perf report ring this
> + * ctx feeds.  Called from kdamond main loop after each sampling interval.
> + *
> + * Each context's per-CPU perf ring (ctx->perf_rings) holds event-driven
> + * probe (probe_idx >= 1) reports.  A ctx drains it when it has event-driven
> + * probes registered.
> + *
> + * The per-target sorted region snapshot is built once and shared across the
> + * ring drain (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;
> -
> -	/* currently damon_access_report supports only physical address */
> -	if (damon_target_has_pid(ctx))
> -		return 0;
> +	struct damon_target_lookup *tbl;
> +	unsigned int nr_targets = 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_perf(ctx))
> +		__kdamond_drain_ring(ctx, tbl, ctx->perf_rings,
> +				&ctx->perf_pending);
>  }
>  
>  /*
> @@ -4360,7 +4806,10 @@ 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 ||
> +				ctx->sample_control.primitives_enabled.page_fault) &&
> +				ctx->ops.prepare_access_checks)
>  			ctx->ops.prepare_access_checks(ctx);
>  		if (do_prep)
>  			ctx->ops.prep_probes(ctx, access_check_disabled);
> @@ -4368,14 +4817,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);
> -		}
> +		/*
> +		 * Perf-event probes feed damon_report_access() into the per-ctx
> +		 * ring; drain it here.
> +		 */
> +		if (damon_drains_ring_perf(ctx))
> +			kdamond_check_reported_accesses(ctx);
> +
> +		/* Page-fault sampling reports only the accessed regions. */
> +		if (ctx->sample_control.primitives_enabled.page_fault)
> +			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,
> 
> -- 
> Git-157)
> 
> 

Sent using hkml (https://github.com/sjp38/hackermail)


  parent reply	other threads:[~2026-10-04  8:30 UTC|newest]

Thread overview: 22+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-03 21:07 [RFC PATCH v3 0/9] mm/damon: hardware-sampled access reports Ravi Jonnalagadda
2026-10-03 21:07 ` [RFC PATCH v3 1/9] mm/damon/paddr: remove page_fault access check primitive Ravi Jonnalagadda
2026-10-03 21:20   ` sashiko-bot
2026-10-03 21:07 ` [RFC PATCH v3 2/9] mm/damon/core: replace the access report buffer with per-context rings Ravi Jonnalagadda
2026-10-03 21:22   ` sashiko-bot
2026-10-04  8:30   ` Kunwu Chan [this message]
2026-10-05  9:09     ` Ravi Jonnalagadda
2026-10-04  9:10   ` Kunwu Chan
2026-10-05  9:11     ` Ravi Jonnalagadda
2026-10-03 21:07 ` [RFC PATCH v3 3/9] mm/damon: add perf-event overflow handler feeding the report ring Ravi Jonnalagadda
2026-10-03 21:22   ` sashiko-bot
2026-10-03 21:07 ` [RFC PATCH v3 4/9] mm/damon/ops-common: use probe-weighted score when probe weights are set Ravi Jonnalagadda
2026-10-03 21:15   ` sashiko-bot
2026-10-03 21:07 ` [RFC PATCH v3 5/9] mm/damon: add perf_event prep type, core lifecycle, and PMU arm/disarm Ravi Jonnalagadda
2026-10-03 21:07 ` [RFC PATCH v3 6/9] mm/damon/sysfs: expose perf_event prep attributes Ravi Jonnalagadda
2026-10-03 21:20   ` sashiko-bot
2026-10-03 21:08 ` [RFC PATCH v3 7/9] mm/damon/tests/drain-kunit: kunit for report rings and ring drain Ravi Jonnalagadda
2026-10-03 21:17   ` sashiko-bot
2026-10-03 21:08 ` [RFC PATCH v3 8/9] mm/damon/core: cap the region merge threshold per target Ravi Jonnalagadda
2026-10-03 21:18   ` sashiko-bot
2026-10-03 21:08 ` [RFC PATCH v3 9/9] mm/damon/core: allow both primitives disabled when a perf probe is present Ravi Jonnalagadda
2026-10-03 21:20   ` sashiko-bot

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=20261004083036.613169-1-kunwu.chan@gmail.com \
    --to=kunwu.chan@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=kunwu.chan@linux.dev \
    --cc=lianux.mm@gmail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=ravis.opensrc@gmail.com \
    --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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.