diff mbox series

[03/10] mm: shrinker: Add new stats for .to_text()

Message ID 20240824191020.3170516-4-kent.overstreet@linux.dev (mailing list archive)
State New
Headers show
Series shrinker debugging, .to_text() report (resend) | expand

Commit Message

Kent Overstreet Aug. 24, 2024, 7:10 p.m. UTC
Add a few new shrinker stats.

number of objects requested to free, number of objects freed:

Shrinkers won't necessarily free all objects requested for a variety of
reasons, but if the two counts are wildly different something is likely
amiss.

.scan_objects runtime:

If one shrinker is taking an excessive amount of time to free
objects that will block kswapd from running other shrinkers.

Cc: Andrew Morton <akpm@linux-foundation.org>
Cc: Qi Zheng <zhengqi.arch@bytedance.com>
Cc: Roman Gushchin <roman.gushchin@linux.dev>
Cc: linux-mm@kvack.org
Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
---
 include/linux/shrinker.h |  6 ++++++
 mm/shrinker.c            | 23 ++++++++++++++++++++++-
 2 files changed, 28 insertions(+), 1 deletion(-)

Comments

Dave Chinner Aug. 28, 2024, 2:36 a.m. UTC | #1
On Sat, Aug 24, 2024 at 03:10:10PM -0400, Kent Overstreet wrote:
> Add a few new shrinker stats.
> 
> number of objects requested to free, number of objects freed:
> 
> Shrinkers won't necessarily free all objects requested for a variety of
> reasons, but if the two counts are wildly different something is likely
> amiss.
> 
> .scan_objects runtime:
> 
> If one shrinker is taking an excessive amount of time to free
> objects that will block kswapd from running other shrinkers.
> 
> Cc: Andrew Morton <akpm@linux-foundation.org>
> Cc: Qi Zheng <zhengqi.arch@bytedance.com>
> Cc: Roman Gushchin <roman.gushchin@linux.dev>
> Cc: linux-mm@kvack.org
> Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
> ---
>  include/linux/shrinker.h |  6 ++++++
>  mm/shrinker.c            | 23 ++++++++++++++++++++++-
>  2 files changed, 28 insertions(+), 1 deletion(-)
> 
> diff --git a/include/linux/shrinker.h b/include/linux/shrinker.h
> index 6193612617a1..106622ddac77 100644
> --- a/include/linux/shrinker.h
> +++ b/include/linux/shrinker.h
> @@ -118,6 +118,12 @@ struct shrinker {
>  #endif
>  	/* objs pending delete, per node */
>  	atomic_long_t *nr_deferred;
> +
> +	atomic_long_t	objects_requested_to_free;
> +	atomic_long_t	objects_freed;
> +	unsigned long	last_freed;	/* timestamp, in jiffies */
> +	unsigned long	last_scanned;	/* timestamp, in jiffies */
> +	atomic64_t	ns_run;
>  };
>  #define DEFAULT_SEEKS 2 /* A good number if you don't know better. */
>  
> diff --git a/mm/shrinker.c b/mm/shrinker.c
> index ad52c269bb48..feaa8122afc9 100644
> --- a/mm/shrinker.c
> +++ b/mm/shrinker.c
> @@ -430,13 +430,24 @@ static unsigned long do_shrink_slab(struct shrink_control *shrinkctl,
>  	       total_scan >= freeable) {
>  		unsigned long ret;
>  		unsigned long nr_to_scan = min(batch_size, total_scan);
> +		u64 start_time = ktime_get_ns();
> +
> +		atomic_long_add(nr_to_scan, &shrinker->objects_requested_to_free);
>  
>  		shrinkctl->nr_to_scan = nr_to_scan;
>  		shrinkctl->nr_scanned = nr_to_scan;
>  		ret = shrinker->scan_objects(shrinker, shrinkctl);
> +
> +		atomic64_add(ktime_get_ns() - start_time, &shrinker->ns_run);
>  		if (ret == SHRINK_STOP)
>  			break;
>  		freed += ret;
> +		unsigned long now = jiffies;
> +		if (ret) {
> +			atomic_long_add(ret, &shrinker->objects_freed);
> +			shrinker->last_freed = now;
> +		}
> +		shrinker->last_scanned = now;
>  
>  		count_vm_events(SLABS_SCANNED, shrinkctl->nr_scanned);
>  		total_scan -= shrinkctl->nr_scanned;

Doing this inside the tight loop (adding 3 atomics and two calls to
ktime_get_ns()) is total overkill. Such fine grained accounting
doesn't given any extra insight into behaviour compared to
accounting the entire loop once.

e.g. the actual time the shrinker takes to run is the time the whole
loop takes to run, not only the individual shrinker->scan_objects
call.

The shrinker code already calculates the total objects scanned and
the total objects freed by the entire loop, so there is no reason to
be calculating this again using much more expensive atomic
operations.

And these are diagnostic stats - the do not need to be perfectly
correct and so atomics are unnecessary overhead the vast majority of
the time.  This code will have much lower impact on runtime overhead
written like this:

	start_time = ktime_get_ns();

	while (total_scan >= batch_size ||
               total_scan >= freeable) {
	.....
	}

	shrinker->objects_requested_to_free += scanned;
	shrinker->objects_freed += freed;

	end_time = ktime_get_ns();
	shrinker->ns_run += end_time - start_time;
	shrinker->last_scanned = end_time;
	if (freed)
		shrinker->last_freed = end_time;

And still give pretty much the exact same debug information without
any additional runtime overhead....

-Dave.
Kent Overstreet Aug. 28, 2024, 2:42 a.m. UTC | #2
On Wed, Aug 28, 2024 at 12:36:08PM GMT, Dave Chinner wrote:
> Doing this inside the tight loop (adding 3 atomics and two calls to
> ktime_get_ns()) is total overkill. Such fine grained accounting
> doesn't given any extra insight into behaviour compared to
> accounting the entire loop once.
> 
> e.g. the actual time the shrinker takes to run is the time the whole
> loop takes to run, not only the individual shrinker->scan_objects
> call.
> 
> The shrinker code already calculates the total objects scanned and
> the total objects freed by the entire loop, so there is no reason to
> be calculating this again using much more expensive atomic
> operations.
> 
> And these are diagnostic stats - the do not need to be perfectly
> correct and so atomics are unnecessary overhead the vast majority of
> the time.  This code will have much lower impact on runtime overhead
> written like this:
> 
> 	start_time = ktime_get_ns();
> 
> 	while (total_scan >= batch_size ||
>                total_scan >= freeable) {
> 	.....
> 	}
> 
> 	shrinker->objects_requested_to_free += scanned;
> 	shrinker->objects_freed += freed;
> 
> 	end_time = ktime_get_ns();
> 	shrinker->ns_run += end_time - start_time;
> 	shrinker->last_scanned = end_time;
> 	if (freed)
> 		shrinker->last_freed = end_time;
> 
> And still give pretty much the exact same debug information without
> any additional runtime overhead....

*nod*

This is all going to be noise compared to the expense of ->scan()
itself, but there's no reason not to, I'll make the change...
diff mbox series

Patch

diff --git a/include/linux/shrinker.h b/include/linux/shrinker.h
index 6193612617a1..106622ddac77 100644
--- a/include/linux/shrinker.h
+++ b/include/linux/shrinker.h
@@ -118,6 +118,12 @@  struct shrinker {
 #endif
 	/* objs pending delete, per node */
 	atomic_long_t *nr_deferred;
+
+	atomic_long_t	objects_requested_to_free;
+	atomic_long_t	objects_freed;
+	unsigned long	last_freed;	/* timestamp, in jiffies */
+	unsigned long	last_scanned;	/* timestamp, in jiffies */
+	atomic64_t	ns_run;
 };
 #define DEFAULT_SEEKS 2 /* A good number if you don't know better. */
 
diff --git a/mm/shrinker.c b/mm/shrinker.c
index ad52c269bb48..feaa8122afc9 100644
--- a/mm/shrinker.c
+++ b/mm/shrinker.c
@@ -430,13 +430,24 @@  static unsigned long do_shrink_slab(struct shrink_control *shrinkctl,
 	       total_scan >= freeable) {
 		unsigned long ret;
 		unsigned long nr_to_scan = min(batch_size, total_scan);
+		u64 start_time = ktime_get_ns();
+
+		atomic_long_add(nr_to_scan, &shrinker->objects_requested_to_free);
 
 		shrinkctl->nr_to_scan = nr_to_scan;
 		shrinkctl->nr_scanned = nr_to_scan;
 		ret = shrinker->scan_objects(shrinker, shrinkctl);
+
+		atomic64_add(ktime_get_ns() - start_time, &shrinker->ns_run);
 		if (ret == SHRINK_STOP)
 			break;
 		freed += ret;
+		unsigned long now = jiffies;
+		if (ret) {
+			atomic_long_add(ret, &shrinker->objects_freed);
+			shrinker->last_freed = now;
+		}
+		shrinker->last_scanned = now;
 
 		count_vm_events(SLABS_SCANNED, shrinkctl->nr_scanned);
 		total_scan -= shrinkctl->nr_scanned;
@@ -812,9 +823,19 @@  EXPORT_SYMBOL_GPL(shrinker_free);
 void shrinker_to_text(struct seq_buf *out, struct shrinker *shrinker)
 {
 	struct shrink_control sc = { .gfp_mask = GFP_KERNEL, };
+	unsigned long nr_freed = atomic_long_read(&shrinker->objects_freed);
 
 	seq_buf_puts(out, shrinker->name);
-	seq_buf_printf(out, " objects: %lu\n", shrinker->count_objects(shrinker, &sc));
+	seq_buf_putc(out, '\n');
+
+	seq_buf_printf(out, "objects:             %lu\n", shrinker->count_objects(shrinker, &sc));
+	seq_buf_printf(out, "requested to free:   %lu\n", atomic_long_read(&shrinker->objects_requested_to_free));
+	seq_buf_printf(out, "objects freed:       %lu\n", nr_freed);
+	seq_buf_printf(out, "last scanned:        %li sec ago\n", (jiffies - shrinker->last_scanned) / HZ);
+	seq_buf_printf(out, "last freed:          %li sec ago\n", (jiffies - shrinker->last_freed) / HZ);
+	seq_buf_printf(out, "ns per object freed: %llu\n", nr_freed
+		       ? div64_ul(atomic64_read(&shrinker->ns_run), nr_freed)
+		       : 0);
 
 	if (shrinker->to_text) {
 		shrinker->to_text(out, shrinker);