Skip to content

sched: Fix round-robin and watchdog re-arm under CONFIG_SCHED_TICKLESS. - #20047

Open
FelipeMdeO wants to merge 2 commits into
apache:masterfrom
FelipeMdeO:fix/sched-rr-timeslice-tickless
Open

sched: Fix round-robin and watchdog re-arm under CONFIG_SCHED_TICKLESS.#20047
FelipeMdeO wants to merge 2 commits into
apache:masterfrom
FelipeMdeO:fix/sched-rr-timeslice-tickless

Conversation

@FelipeMdeO

Copy link
Copy Markdown
Contributor

Summary

apps/testing/ostest's full suite hangs or crashes under
CONFIG_SCHED_TICKLESS due to two separate, generic (arch-independent)
scheduler bugs. Both are required together to get the suite passing
end-to-end under tickless.

  • sched/sched_addreadytorun.c: nxsched_reassess_timer() (which
    arms the round-robin timeslice timer) was only ever called from
    sched_unlock.c, gated on the task that was running having
    already exhausted its timeslice. Nothing armed the timer when a
    new SCHED_RR task started running via an ordinary context
    switch, so under tickless (no periodic tick to fall back on) two
    same-priority SCHED_RR threads never interleaved -- one just ran
    to completion before the other's first iteration was recorded.
  • sched/wdog/wdog.h: wd_timer_start() computed the delay to the
    next watchdog expiration with no clamp. An already-due watchdog
    (common with back-to-back short-delay wd_start()/wd_cancel()
    calls) made that delta go negative -- which arch code's
    NSEC_2_CTICK()-style macros then cast to unsigned, turning
    "already due" into a multi-year alarm that never fires. Clamping to
    zero isn't sufficient either: oneshot hardware comparators (e.g.
    the ESP32-S3 systimer) only interrupt on the counter crossing the
    target from below, so arming for "right now" can miss the edge and
    never fire at all.

Neither bug is specific to any one chip -- both are in generic
sched/ code reachable by any architecture combining
CONFIG_SCHED_TICKLESS with CONFIG_RR_INTERVAL > 0 (round-robin
bug) or any watchdog user issuing short/zero delays (wdog bug). The
round-robin bug is additionally confirmed on 3 independent tickless
backends across 2 architectures (Xtensa ESP32-S3, RISC-V ESP32-C3
legacy and shared backends).

The in-tree boards/xtensa/esp32s3/esp32s3-devkit/configs/tickless example
(CONFIG_ESP32S3_TICKLESS + CONFIG_TESTING_OSTEST, both already
enabled by default in that defconfig) reproduces the round-robin crash
immediately and deterministically on real hardware -- see the
Reproduction and Testing sections below.

Impact

  • Impact on hardware (will arch(s) / board(s) / driver(s) change)?
    YES -- fixes generic sched/ code exercised by any board using CONFIG_SCHED_TICKLESS; no board/driver files touched.

  • Anything else to consider or add? The CONFIG_SMP path of the round-robin fix (nxsched_switch_running()) is intentionally not addressed here -- no SMP hardware was available to validate against. Flagging for maintainer input on whether that gap should block this PR or be tracked separately.

Testing

I confirm that changes are verified on local setup and works as intended:

  • Build Host(s): Ubuntu 24.04.4 LTS, x86_64, GCC (xtensa-esp-elf-gcc, crosstool-NG esp-14.2.0_20241119, 14.2.0).
  • Target(s):
    • xtensa (esp32s3-xiao): boards/xtensa/esp32s3/esp32s3-xiao/configs/usbnsh +
      CONFIG_ESP32S3_TICKLESS=y + CONFIG_TESTING_OSTEST=y (kconfig-tweak).
    • xtensa (esp32s3-devkit): in-tree esp32s3-devkit:tickless example config, unmodified.
    • riscv (esp32c3-legacy-devkit): in-tree :tickless config (own esp32c3_tickless.c backend) -- round-robin fix only.
    • riscv (esp32c3-devkit): in-tree :tickless config (shared esp_tickless.c backend) -- round-robin fix only.

How to reproduce (before this fix)

Simplest repro uses only in-tree, unmodified files -- no board-specific
changes needed:

./tools/configure.sh esp32s3-devkit:tickless
make -j$(nproc)
# flash nuttx.bin, open the console, then at the nsh> prompt:
nsh> ostest

(Any board with CONFIG_ESP32S3_TICKLESS/equivalent tickless backend

  • CONFIG_TESTING_OSTEST + CONFIG_RR_INTERVAL > 0 reproduces the
    round-robin crash identically -- this is just the simplest way to get
    there with zero custom config, since it's an existing in-tree example.)

Console output before the fix (esp32s3-devkit:tickless, stock master):

user_main: round-robin scheduler test
rr_test: Set thread priority to 1
rr_test: Set thread policy to SCHED_RR
rr_test: Starting first get_primes_thread
         First get_primes_thread: 88
rr_test: Starting second get_primes_thread
         Second get_primes_thread: 89
rr_test: Waiting for threads to complete -- this should take awhile
         If RR scheduling is working, they should start and complete at
         about the same time
get_primes_thread id=1 started, looking for primes < 30000, doing 10 run(s)
get_primes_thread id=1 finished, found 3246 primes, last one was 29989
get_primes_thread id=2 started, looking for primes < 30000, doing 10 run(s)
get_primes_thread id=2 finished, found 3246 primes, last one was 29989
rr_test: Roundrobin Failed
dump_assert_info: Current Version: NuttX  13.0.1-RC0 f595ba31e2 Sep  1 2026 19:54:51 xtensa
dump_assert_info: Assertion failed : at file: :0 task: ostest process: ostest 0x420239f0
up_dump_register:    PC: 42022bc1    PS: 00060523
up_dump_register:    A0: 80376c78    A1: 3fc98bd0    A2: 00000000    A3: 3fc8d9f0
[... full register + stack dump omitted ...]
ostest_main: Exiting with status 256

Note id=1 fully finishing (found 3246 primes) before id=2 even
starts -- the two same-priority SCHED_RR threads never
interleaved, so rr_test's own pass check
(apps/testing/ostest/roundrobin.c:263-280, which requires a thread
switch within the first CONFIG_TESTING_OSTEST_RR_RUNS entries of
g_rr_values[]) fails and hits ASSERT(false).

The watchdog bug (bug 2/2 in this PR) is timing-dependent and didn't
trigger on that particular esp32s3-devkit run, but reliably hung
apps/testing/ostest/wdog.c's wdog_test() forever on
esp32s3-xiao (confirmed via a temporary progress printf that every
one of its 4 threads got stuck on the very first iteration of
wdtest_rand()'s busy-wait for its own watchdog callback -- the
callback never fired).

After this fix

Full, unmodified apps/testing/ostest suite, esp32s3-xiao,
CONFIG_SCHED_TICKLESS + CONFIG_TESTING_OSTEST, both commits
applied -- runs to completion with no hangs, no failed asserts:

user_main: wdog test
wdog_test start...
[... 4 threads x wdtest_once/wdtest_recursive/wdtest_rand, all complete ...]
wdog_test end...

End of test memory usage:
VARIABLE  BEFORE   AFTER
======== ======== ========
arena       5de60    5de60
...

user_main: round-robin scheduler test
rr_test: Set thread priority to 1
rr_test: Set thread policy to SCHED_RR
rr_test: Starting first get_primes_thread
         First get_primes_thread: 249
rr_test: Starting second get_primes_thread
         Second get_primes_thread: 250
rr_test: Waiting for threads to complete -- this should take awhile
         If RR scheduling is working, they should start and complete at
         about the same time
get_primes_thread id=1 started, looking for primes < 30000, doing 10 run(s)
get_primes_thread id=2 started, looking for primes < 30000, doing 10 run(s)
get_primes_thread id=1 finished, found 3246 primes, last one was 29989
get_primes_thread id=2 finished, found 3246 primes, last one was 29989
rr_test: Done

[... sporadic, dual sporadic, barrier (8 threads), setjmp, priority
     inheritance, scheduler lock, smp call, nxevent, perf event
     counter -- all pass ...]

Final memory usage:
VARIABLE  BEFORE   AFTER
======== ======== ========
arena       5de60    5de60
ordblks         2        5
mxordblk    58658    54320
uordblks     5758     57e0
fordblks    58708    58680
user_main: Exiting
ostest_main: Exiting with status 0

Note both id=1 and id=2 now start together (get_primes_thread id=1 started immediately followed by id=2 started, instead of one
finishing before the other starts), and rr_test: Done (not
Roundrobin Failed) confirms roundrobin.c's own interleaving check
passed.

The round-robin fix alone was additionally validated with just
rr_test() (skipping the rest of the suite) on both
esp32c3-legacy-devkit:tickless and esp32c3-devkit:tickless --
PASS on both, confirming the fix is backend- and architecture-independent.

Under CONFIG_SCHED_TICKLESS, nxsched_reassess_timer() (which programs
the round-robin timeslice timer) was only called from sched_unlock.c,
guarded by "the task that was just running is itself SCHED_RR and had
already exhausted its timeslice". There was no hook to arm the
timeslice timer when a *new* SCHED_RR task starts running via an
ordinary context switch (e.g. a non-RR task blocking and handing off
to a same-priority RR task). Since tickless mode has no periodic tick
interrupt to fall back on, the round-robin accounting for that task
was never re-armed, and it would run to completion instead of yielding
to other same-priority SCHED_RR tasks.

Reproducer: apps/testing/ostest's rr_test (two SCHED_RR pthreads at
the same priority) never interleaves under tickless -- one thread
runs all its iterations before the other's first iteration is even
recorded, and zero calls to the arch's up_timer_start() occur during
the whole test.

Fix: when a task transitions to TSTATE_TASK_RUNNING in
nxsched_add_readytorun() (non-SMP path) and is SCHED_RR, explicitly
call nxsched_reassess_timer() so its timeslice timer is armed.

Validated on real hardware with apps/testing/ostest's rr_test:
* esp32c3-legacy-devkit:tickless (own esp32c3_tickless.c backend) -- PASS
* esp32c3-devkit:tickless (shared esp_tickless.c backend) -- PASS
* esp32s3-xiao (own esp32s3_tickless.c backend) -- PASS

The equivalent CONFIG_SMP path (nxsched_switch_running()) is not
addressed here -- no SMP hardware was available to validate against.

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Felipe Moura <moura.fmo@gmail.com>
Under CONFIG_SCHED_TICKLESS (non-alarm), wd_timer_start() computed the
delay to the next watchdog expiration as a plain subtraction of two
absolute clock_t values (next_tick - clock_systime_ticks()) with no
clamping, then handed it to up_timer_tick_start(). Two related bugs
followed from that:

1. If the watchdog at the head of the active list had already expired
   by the time wd_timer_start() ran (routine when wd_start()/wd_cancel()
   are called back-to-back with very short or zero delays, and clock_t
   is a signed int64_t), the subtraction went negative. That negative
   value flows through clock_ticks2time() into a timespec with a
   negative tv_nsec, which arch code's NSEC_2_CTICK()-style macros then
   cast to unsigned before multiplying -- turning "already due" into a
   multi-year hardware alarm that never fires.

2. Even clamping just to zero isn't enough: many oneshot hardware
   timers (e.g. the ESP32-S3 systimer) only raise their interrupt when
   the running counter *crosses* the target from below. Arming for
   "right now" can miss that edge if the target is already at or behind
   the counter by the time the register write lands, silently dropping
   the alarm forever.

Fix: clamp the computed delta to a minimum of one tick, so arch code
always gets a target strictly in the future. This matches the coarser
but dependable behavior tick-based (non-tickless) mode already has for
a zero-delay wd_start() -- it, too, only ever fires on the next
periodic tick.

Reproducer: apps/testing/ostest's wdog_test() spawns 4 threads that
hammer wd_start()/wd_cancel() with delays from 0ns up to ~12us across
1024 iterations each (wdtest_rand()). Under CONFIG_SCHED_TICKLESS on
esp32s3-xiao, this reliably hung forever inside the first iteration's
busy-wait for its own callback, with zero further watchdog callbacks
ever firing system-wide.

Validated on real hardware (esp32s3-xiao): apps/testing/ostest run to
completion end-to-end (all subtests, including wdog_test, hrtimer,
round-robin, sporadic, barrier, priority inheritance, etc.) with
CONFIG_SCHED_TICKLESS + CONFIG_TESTING_OSTEST enabled, no hangs, no
failed asserts -- "ostest_main: Exiting with status 0".

Assisted-by: Claude:claude-sonnet-5
Signed-off-by: Felipe Moura <moura.fmo@gmail.com>
@github-actions github-actions Bot added Area: OS Components OS Components issues Size: S The size of the change in this PR is small labels Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: OS Components OS Components issues Size: S The size of the change in this PR is small

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant