Re: [PATCH 2/2] selftests/sched_ext: Add lazy preemption tests

From: Tejun Heo

Date: Wed Sep 16 2026 - 17:19:38 EST


Hello, Andrea.

This is an AI review. The tests pass here under vng in lazy, full and none
modes. The timing below was measured with the target CPU on nohz_full.

On Tue, Sep 15, 2026 at 09:45:13PM +0200, Andrea Righi wrote:
> +void BPF_STRUCT_OPS(kick_stopping, struct task_struct *p, bool runnable)
> +{
> + if (p->pid == victim_pid && state == KICK_STATE_RESCHED)
> + state = KICK_STATE_DONE;
> +}

In the kick and tick scenarios the victim is the only runnable task on the
target CPU, so when its slice is cleared the kernel keeps running it with a
refilled slice and ops.stopping() is never called. DONE only arrives when some
other task pinned to that CPU wakes up. With the target CPU on nohz_full here,
the first such wakeup was a kworker 2.86s after the victim started, against
the 3s wait. Without nohz_full the scenarios still spend about half a second
each waiting for one. SCX_OPS_ENQ_LAST with the SCX_ENQ_LAST kick as in
nohz_tick.bpf.c would put the victim through ops.stopping() on its own.

> +SEC("fexit/__resched_curr")
> +int BPF_PROG(kick_need_resched, struct rq *rq, int tif)
> +{
> + struct task_struct *task = BPF_CORE_READ(rq, curr);
> +
> + if (!task || BPF_CORE_READ(task, pid) != victim_pid ||
> + BPF_CORE_READ(rq, cpu) != target_cpu || state != KICK_STATE_QUEUED)
> + return 0;

This latches the first resched of the victim whatever the cause. A higher
class wakeup on the target CPU inside the window, which is the whole 20ms
slice in TICK_EXPIRY, records a full slice and fails observation_valid().
Skipping observations with a non-zero slice would be more robust.

> + SCX_EQ(lazy_wait.resched_tif, immediate.resched_tif);
> + SCX_GT(lazy_wait.nr_wait_callbacks, 0);

DONE is set from ops.stopping() before the context switch and the balance
callback runs after it, so this read can race the increment. Poll for it.

> + file = fopen("/sys/kernel/debug/sched/preempt", "r");
> + if (!file) {
> +#if defined(CONFIG_PREEMPT_LAZY) && !defined(CONFIG_PREEMPT_DYNAMIC)
> + return 1;
> +#else
> + return -1;
> +#endif

This reflects the build tree, not the running kernel. Also, none and voluntary
return -1 and skip the comparison, but the TIFs must match in those modes too.

> + skel->rodata->scenario = scenario;
> + if (kick__load(skel))
> + goto out;

If __resched_curr() isn't in BTF, this fails too and kick_invalid FAILs along
with the other four although it doesn't use the tracer. Skipping when the
target is missing would be better.

> + if (!start_gated_worker(&victim) ||
> + !wait_for_counter(&skel->bss->nr_lazy_victim_running, 1,
> + PHASE_TIMEOUT_MS)) {
> + SCX_ERR("Lazy-enqueue victim was not scheduled");
> + goto out;
> + }
> + usleep(100000);

Neither lazy phase checks that the tick actually stopped before the request,
so they pass with the tick running the whole time.

> Extend the NO_HZ_FULL test with infinite-slice victims. Verify that lazy
> enqueue and kick requests restart a stopped tick and make forward
> progress. Keep invalid kick flag coverage and skip modes not exposed by
> the running kernel.

kick.c is new, nothing is kept. The controller mask and timeout changes to
nohz_tick aren't mentioned either.

Thanks.

--
tejun