Skip to content

scheduler: add optional preemptive scheduling - #2492

Merged
mkroening merged 1 commit into
hermit-os:mainfrom
stlankes:preempt
Sep 17, 2026
Merged

mkroening merged 1 commit into
hermit-os:mainfrom
stlankes:preempt

Conversation

@stlankes

Copy link
Copy Markdown
Contributor

The scheduler is tickless (a TSC-deadline one-shot timer), so a CPU-bound task that never blocks or yields keeps its core indefinitely. Add an optional preemptive feature that round-robins between ready tasks via a time slice.

A new Source::Preemption timer slot is armed for now + 10ms whenever the scheduler switches to a task while others are ready, and whenever custom_wakeup makes a task ready (so a freshly woken task preempts a non-yielding runner soon). When the slice expires, the timer handler's existing reschedule() switches to the next ready task. With no contention nothing is armed, so the system stays tickless.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark Results

Details
Benchmark Current: 11bf411 Previous: 2e23902 Performance Ratio
startup_benchmark Build Time 93.54 s 80.34 s 1.16
startup_benchmark File Size 0.79 MB 0.80 MB 1.00
Startup Time - 1 core 0.76 s (±0.03 s) 0.75 s (±0.02 s) 1.02
Startup Time - 2 cores 0.76 s (±0.02 s) 0.74 s (±0.02 s) 1.04
Startup Time - 4 cores 0.78 s (±0.02 s) 0.74 s (±0.02 s) 1.05
multithreaded_benchmark Build Time 95.96 s 82.11 s 1.17
multithreaded_benchmark File Size 0.85 MB 0.86 MB 0.99
Multithreaded Pi Efficiency - 2 Threads 90.77 % (±8.39 %) 85.89 % (±6.61 %) 1.06
Multithreaded Pi Efficiency - 4 Threads 44.87 % (±3.35 %) 43.43 % (±2.56 %) 1.03
Multithreaded Pi Efficiency - 8 Threads 26.00 % (±1.51 %) 25.76 % (±1.53 %) 1.01
micro_benchmarks Build Time 93.83 s 80.40 s 1.17
micro_benchmarks File Size 0.86 MB 0.86 MB 0.99
Scheduling time - 1 thread 67.55 ticks (±2.79 ticks) 62.65 ticks (±4.06 ticks) 1.08
Scheduling time - 2 threads 36.28 ticks (±3.61 ticks) 34.08 ticks (±4.10 ticks) 1.06
Micro - Time for syscall (getpid) 3.85 ticks (±0.60 ticks) 3.45 ticks (±0.58 ticks) 1.12
Memcpy speed - (built_in) block size 4096 74947.50 MByte/s (±51969.17 MByte/s) 82448.38 MByte/s (±56997.13 MByte/s) 0.91
Memcpy speed - (built_in) block size 1048576 29505.92 MByte/s (±23983.51 MByte/s) 30585.98 MByte/s (±24707.84 MByte/s) 0.96
Memcpy speed - (built_in) block size 16777216 24039.37 MByte/s (±19938.63 MByte/s) 26340.06 MByte/s (±21720.96 MByte/s) 0.91
Memset speed - (built_in) block size 4096 74711.39 MByte/s (±51798.70 MByte/s) 82292.76 MByte/s (±56891.50 MByte/s) 0.91
Memset speed - (built_in) block size 1048576 30249.36 MByte/s (±24420.09 MByte/s) 31323.85 MByte/s (±25145.86 MByte/s) 0.97
Memset speed - (built_in) block size 16777216 24751.82 MByte/s (±20406.28 MByte/s) 27104.68 MByte/s (±22209.94 MByte/s) 0.91
Memcpy speed - (rust) block size 4096 67439.24 MByte/s (±47243.29 MByte/s) 74097.96 MByte/s (±51811.44 MByte/s) 0.91
Memcpy speed - (rust) block size 1048576 29348.87 MByte/s (±23966.27 MByte/s) 30361.60 MByte/s (±24602.37 MByte/s) 0.97
Memcpy speed - (rust) block size 16777216 24254.13 MByte/s (±20065.72 MByte/s) 27625.34 MByte/s (±22806.88 MByte/s) 0.88
Memset speed - (rust) block size 4096 67951.00 MByte/s (±47588.57 MByte/s) 74373.47 MByte/s (±51976.48 MByte/s) 0.91
Memset speed - (rust) block size 1048576 30104.63 MByte/s (±24450.61 MByte/s) 31110.89 MByte/s (±25033.24 MByte/s) 0.97
Memset speed - (rust) block size 16777216 25011.11 MByte/s (±20581.05 MByte/s) 28386.93 MByte/s (±23265.03 MByte/s) 0.88
alloc_benchmarks Build Time 92.14 s 74.76 s 1.23
alloc_benchmarks File Size 0.87 MB 0.87 MB 1.00
Allocations - Allocation success 91.38 % 91.31 % 1.00
Allocations - Deallocation success 100.00 % 100.00 % 1
Allocations - Pre-fail Allocations 61.60 % 61.44 % 1.00
Allocations - Average Allocation time 11391.38 Ticks (±411.00 Ticks) 5860.58 Ticks (±98.43 Ticks) 1.94
Allocations - Average Allocation time (no fail) 11885.29 Ticks (±468.35 Ticks) 6554.81 Ticks (±92.86 Ticks) 1.81
Allocations - Average Deallocation time 2792.86 Ticks (±910.99 Ticks) 1805.01 Ticks (±250.35 Ticks) 1.55
mutex_benchmark Build Time 85.72 s 79.82 s 1.07
mutex_benchmark File Size 0.86 MB 0.86 MB 0.99
Mutex Stress Test Average Time per Iteration - 1 Threads 13.14 ns (±0.49 ns) 12.10 ns (±0.41 ns) 1.09
Mutex Stress Test Average Time per Iteration - 2 Threads 89.46 ns (±2.73 ns) 40.26 ns (±1.68 ns) 2.22

This comment was automatically generated by workflow using github-action-benchmark.

@stlankes
stlankes force-pushed the preempt branch 2 times, most recently from 29c1147 to 1c59ce1 Compare June 21, 2026 07:41
Comment thread src/scheduler/mod.rs
Comment on lines +413 to +414
let deadline = processor::get_timer_ticks() + PREEMPTION_SLICE_US;
timer_interrupts::create_timer_abs(timer_interrupts::Source::Preemption, deadline);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let deadline = processor::get_timer_ticks() + PREEMPTION_SLICE_US;
timer_interrupts::create_timer_abs(timer_interrupts::Source::Preemption, deadline);
timer_interrupts::create_timer(timer_interrupts::Source::Preemption, PREEMPTION_SLICE_US);

same for all the other calls. The function create_timer needs its #[cfg(feature = "net")] replaced with #[cfg(any(feature = "net", feature = "preemptive"))]

@mkroening mkroening left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address @Gelbpunkt's comment. Then, this is fine with me. 👍

@stlankes

Copy link
Copy Markdown
Contributor Author

Why is feature net required?

@stlankes
stlankes force-pushed the preempt branch 2 times, most recently from 6a2d455 to 0f191e4 Compare September 14, 2026 09:37
@mkroening mkroening assigned mkroening and unassigned stlankes Sep 17, 2026
The scheduler is tickless (a TSC-deadline one-shot timer), so a CPU-bound
task that never blocks or yields keeps its core indefinitely. Add an
optional `preemptive` feature that round-robins between ready tasks via a
time slice.

A new `Source::Preemption` timer slot is armed for `now + 10ms` whenever the
scheduler switches to a task while others are ready, and whenever
`custom_wakeup` makes a task ready (so a freshly woken task preempts a
non-yielding runner soon). When the slice expires, the timer handler's
existing `reschedule()` switches to the next ready task. With no contention
nothing is armed, so the system stays tickless.
@mkroening
mkroening added this pull request to the merge queue Sep 17, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 17, 2026
@mkroening
mkroening added this pull request to the merge queue Sep 17, 2026
Merged via the queue into hermit-os:main with commit 91e8fac Sep 17, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants