Patrick Lidstone
Self-hosted

Development log

What the scheduler actually costs

· Path OS

The scheduler has not been reviewed since it was written. It keeps a global process table, scans all the candidates, weights four priority levels, adds a credit for each missed turn and breaks ties round-robin. That is a starvation-resistant choice of turns rather than a division of CPU time, and it has no blocked state at all - an application with an empty inbox yields and is immediately eligible again, so every open application competes for a turn while doing nothing.

There are several obvious improvements. The review deliberately stopped short of making any of them, because the first thing it found was that the available measurements could not tell which of them would help.

What the first figures actually said

The X1's log collector showed a boot running at roughly thirty to thirty-seven thousand boot-loop passes a second with contended_per_thousand: 999 in the lock report, and a later sample from the same machine at 58 passes a second while the cores were still ticking.

That looks like a damning number and it is not a diagnosis. The lock's contended counter records any spin at all rather than how long it lasted, the collector's lines truncate the JSON, and nobody knows what the machine was being asked to do at the time. A 999-in-a-thousand contention rate with no duration beside it is consistent with a catastrophe and with a lock that is briefly busy very often.

The first work was instruments, then. Hold and wait duration measured by checkpoint rather than by spin count, per-process elapsed time, voluntary yields against involuntary preemptions, migrations, and how often a ready task waits while an eligible core sits idle. Fixed-size histograms and maxima rather than a log entry per switch. The Console's ps shows the live logical CPU and the most recent one separately, because there is no permanent assignment and a task between runs has no live CPU at all.

The first segmented figures from the X1 gave the stage of each long hold and not its cause: the input leg of the boot core's pass had a maximum hold of 5.8 seconds across four different pollers, and "syscall" had 4.3 seconds across every operation an application can invoke. A stage name is not something a policy decision can be made from, so the attribution was made finer before anything was changed - the input leg is five segments by device now, and a system call is charged to the name of the operation it invoked rather than to the word "syscall".

The cores were not slow, they were queueing

The first thing the better instruments found was not a scheduler problem.

Seven secondary cores are started about four hundred lines of boot log before the boot core reaches its main loop, and in that interval there is nothing whatever for them to do - there is no process until the process table is initialised, and the only way a core can discover that is to take the kernel lock. Which the boot core is holding for seconds at a time while it takes both USB controllers off the firmware, sends the Bluetooth radio its firmware, uploads the wireless card's and surveys the band, mounts the store and reads the graphics hardware.

On one X1 boot that was the 999 contention figure, a summed wait growing by about five and a half seconds per second across the seven cores, and five watchdog banners. The cores were queueing for a lock to be told there was no work.

They are held off instead. Starting them later was the other option and it was rejected for two reasons: the bring-up order is itself load-bearing, since every core calibrates its timer against a frequency measured a few lines above it, and a core that is online and ticking is a core the instruments can see - moving the start would have made "no core answered" indistinguishable from "the start has not happened yet" for the whole of the bring-up. All seven come up exactly where they did and what waits is the work. The hold-off loop takes no lock at all.

The audio core was the largest single contributor to the measured contention on its own, because its pass takes the kernel lock up to three times a millisecond. It still comes up when it is meant to and ticks at a thousand hertz from the moment it exists; what it holds off is the pass, which has nothing to feed anyway because no sound can be played before the boot core's loop.

What is not built

None of the policy has changed. The measurements are built, the hold-off has been made, and the chooser is the same chooser. Blocking idle applications is the strongest next move, and it has not been made - it needs a blocked state, an operation that atomically checks the inbox and parks the caller if it is empty, and a wake that cannot lose an event, and none of that is written.

The acceptance criteria for the phase that has been acted on are unmet, which is worth saying plainly: the changes are host-verified only, and no lock hold has been measured on hardware since they were made. The figures above are the reason for the change, not evidence that it worked.

Per-core run queues are deliberately left unbuilt until a measurement asks for them. At a few dozen applications a global scan is simpler and the contention that would justify splitting it has not been demonstrated - the contention that was demonstrated turned out to be seven idle cores asking a question.