Re: [PATCH] sched/stats: Fix run_delay over-count for proxy-migrated blocked donors

From: Kayra Cizmeci

Date: Sun Oct 25 2026 - 15:28:50 EST


On Sun, Oct 25, 2026 at 09:07:40PM +0800, albin_yang@xxxxxxx wrote:

Hi Wei,

> From: Wei Yang <albinwyang@xxxxxxxxxxx>
>
> When a task is blocked on a kernel mutex (a proxy-exec donor) and its mutex
> owner runs on another CPU, find_proxy_task() migrates the still-blocked donor
> to the owner's CPU via proxy_migrate_task(). This happens when the donor is
> picked as next while not current, and the owner is on_rq and not delayed. The
> migration re-attaches the donor with attach_task() -> activate_task(rq, p,
> ENQUEUE_NOCLOCK), i.e. without ENQUEUE_RESTORE, so sched_info_enqueue() arms
> last_queued even though the donor is not runnable at all -- a fake enqueue.
>
> The real wakeup does not correct it: the donor is still on_rq and is not
> sched_delayed, so ttwu_runnable() never calls enqueue_task(), and last_queued
> keeps the migration timestamp. sched_info_arrive() then charges the whole
> interval from the migration until the donor actually runs, i.e. the time it

Well it normally should be like that but, sched_info_arrive() only gets
called when we *hit* the CPU. The issue is already that we start the
counter when we are not waiting for a CPU. The counter gets
charged on dequeue.

ttwu_runnable() calls proxy_needs_return() in the proxy case, then that
calls dequeue_block_task() with TASK_WAKING, where we call
sched_info_dequeue(). Or Am I missing something?

> spent blocked on the mutex is counted as time spent waiting for a CPU.
>
> Fix by not re-arming last_queued while the task is still blocked on a mutex.
> A delayed dequeue sleeper has no blocked_on, so this is an additional
> condition rather than a replacement for the se.sched_delayed check, and the
> DELAY_DEQUEUE fix is unaffected.
>
> Measured on a 2 CPU VM: the same ~170 ms block is charged ~167 ms of
> run_delay when the mutex owner runs on another CPU, and 0.01 ms with this
> patch.
>
> Fixes: b049b81bdff6 ("sched: Handle blocked-waiter migration (and return migration)")
> Signed-off-by: Wei Yang <albinwyang@xxxxxxxxxxx>
> ---
>
> Reproducer notes and full numbers (5 iterations each, run_delay is field 2 of
> /proc/<tid>/schedstat). A userspace futex mutex never sets blocked_on, so this
> needs a small test module exposing a kernel mutex:
>
> config block run_delay delta migrated
> ----------------------------- ------- --------------- --------
> cross-cpu, unfixed 169.9 ms 167.1 ms 5/5
> cross-cpu, DELAY_DEQUEUE fix 169.9 ms 167.1 ms 5/5
> cross-cpu, with this patch 169.8 ms 0.01 ms 5/5
> same-cpu (control) 167.3 ms 0.00 ms 0/5
>
> Sanity check: with the donor runnable and contending for the CPU, run_delay
> still grows by ~39 ms over an 80 ms window (5/5), so the gate suppresses only
> the fake enqueue, not real waits.
>
> Applies on top of:
> [PATCH v4] sched/stats: Fix run_delay over-count for migrated sched_delayed tasks
> https://lore.kernel.org/all/20261024042753.2276779-1-albin_yang@xxxxxxx/

This should be a series instead of separate patches if this goes above.

Also AFAICT this could go wrong once John’s series is applied:

https://lore.kernel.org/lkml/20261023220431.1106611-8-jstultz@xxxxxxxxxx/

Adding John to CC.

With this patch, now waiters get dragged from their beloved rq's and
gets in to owner's own list. In this case, on_rq can be false, and
ttwu_runnable() could early return without clearing blocked_on.

Waiters normally wake up with their owners, and wait the CPU for them
so normally we should not start the timer. But on a hierarchy like this,

Where A sleeps and holds the mutex M while B wants it, B with
mutex_lock_interruptible(), and when B gets a kill signal
B need to enqueue separately from A, so it starts to wait.
But blocked_on is set, so B's counter never starts.
This only happens when both pathes are applied.

There's a chance I'm getting something wrong tho. :-).

Thanks,

Kayra