Skip to content

CosmoOS roadmap A5 in Nova: CPU time and the CPU-time timers (plan and report) ​

Status: PR 1 (LX-14) merged into CosmoOS, 2026-10-27 (#461, c052f48d). The owner merged it with its last CI runs cancelled: the last completed run, on its earlier head f96c80c1 (the feature before the in-tree move, the agent guide and the exited-thread check), had passed every job, and the final branch passed the local matrix and the probe on both arches; main's run on the merge commit is the first CI run of the merged code. PR 2 (LX-13's interface, nice values) merged into CosmoOS, 2026-10-28 (#476, 1d98fc72). PR 3 (a weighted fair share at the default level) merged into CosmoOS, 2026-10-28 (#477, f87a808f): A5's kernel lines are done, and LX-13 and LX-14 with them.

CosmoOS's roadmap 1.2 unit A5 (OpenSSH and git, docs/roadmap-1.2.md in CosmoOS) absorbs two kernel gaps that both suites touch: LX-13 (no nice values, no setpriority/nice) and LX-14 (ITIMER_VIRTUAL and ITIMER_PROF are -EINVAL). On 2026-10-26 the owner asked to "continue adding features to CosmoOS using Nova", starting with A5, and chose its kernel lines in Nova, with nice as a weighted share. This is the first CosmoOS feature written in Nova, where C9 replaced existing C: no C original exists, so the port is held to tests written first, not to a differential comparison.

Plan ​

PRBacklogWhatNova writesC keeps
1LX-14CPU time split into user and system time; ITIMER_VIRTUAL/ITIMER_PROF; getrusage, times, the CPU-time clocks, wait4's rusage, /proc/<pid>/stat fields 14–17the tick's sample, the timers' state machine, the exact user/system split (CosmoOS's kernel/process/cputime.nova)the tick hook, locks, signals, system calls, /proc
2LX-13setpriority/getpriority/nice with Linux's ranges and permission rules, per process, inherited by fork, shown in /proc and psthe rulesthe system calls
3LX-13a weighted fair share at the default priority level (Linux's weight table, nice 0 = 1024), the other 63 levels unchangedthe weights and the fair queue's orderingthe run queue

PR 1 audit (CosmoOS 68925d14, read, not copied) ​

ItemWhereWhat it means
A thread's run time is exact, charged at switch-outsched.c:403, sched_thread_run_ns (sched.c:1188)the total to split
No user/system split anywhere; /proc/<pid>/stat reports all of it as utimeprocfs.c (render_pid_stat)the gap
A process's CPU time sums its live threads only: an exited thread's time was lostprocfs.c proc_threads, process.c process_listfixed: gone_run_ns, folded in by thread_put under the process's lock
The tick is periodic (250 Hz) and gets the trap frametimer.c (g_tick_hook), sched_tick(now, frame)sampling is possible; arch_trap_frame_is_user on both arches
sched_tick holds rq->lock, a leaf, while calling the policysched.c sched_tickthe sample is taken before it
signal_send takes only the process's spinlock and is already called from interrupt contextitimer.c (itimer_fire), tty_inputthe timers' signals can be sent from the tick
ITIMER_REAL exists: fork disarms, exec keeps, value 0 clears the interval, remaining time never 0 while armeditimer.cthe new timers follow the same rules
getrusage and times are not implemented (-ENOSYS); wait4 zeroes rusagecompat/linux/syscalls.cadded
clock_gettime with CLOCK_PROCESS_CPUTIME_ID (2) or CLOCK_THREAD_CPUTIME_ID (3) returned the monotonic clocklx_clock_gettime (ids 0..7 all read clock_now_ns except the realtime ones)a bug, fixed
SIGVTALRM (26) and SIGPROF (27) were not defined; their default (terminate) needs no table entrysignal.h, signal_default_is_ignoredefined

PR 1 design ​

CosmoOS's docs/kernel/process/design.md, "CPU time", is the normative description. In short:

  • Each tick charges TICK_NS to the interrupted thread and its process, as user time if the trap frame came from user mode. A report splits the exact run time in the samples' proportion, never below the previous report: Linux's cputime_adjust. The division is exact: scale keeps the 128-bit product as two 64-bit halves and divides bit by bit, since generated C may not call compiler-rt (c-backend.md) and Nova has no 128-bit type.
  • ITIMER_VIRTUAL runs on the sampled user time, ITIMER_PROF on user plus system time; a new value gets one tick added, as Linux adds one; a periodic timer reloads from its expiry.
  • The process's spinlock guards it all. The tick takes it in interrupt context before rq->lock and sends a due signal after releasing it. A waited-for child's split reaches its parent with the child's lock, then the parent's, never both.

The Nova module is about 190 lines (cputime.nova); the API and structs are CosmoOS's kernel/include/kernel/cputime.h, imported (type-system §31). It runs in interrupt context, so nothing in it may fail on any input: every sum that could pass 2^64 saturates (saturatingAdd), and the remaining checked operations are ones the arithmetic rules out (the subtractions in adjust and remaining follow comparisons). unsafe appears only in the exports, where the header's pointers become references.

Where the sources live ​

The PR was first built with the source in Nova's repository (integrations/cosmoos/cputime/cputime.nova) and a generalised vendor.py, as C9b had done for GPT. On 2026-10-27 the owner chose to move every CosmoOS Nova source into CosmoOS, beside the C it generates, and to fold the move into this PR (build-integration.md §3.1). Its first commit moves gpt.nova and adds make nova with a pinned novac (0.1.1) and a CI job that fails on drift; cputime.nova arrives with the feature. Regenerated with 0.1.1, the C differs from 0.2.0-dev's only in the banner, the version line and the source paths in its locations.

What the language did and did not need ​

No language change was needed. One friction point: an imported C struct is C state that never moves (§33.2), so let timer = if (which == VIRTUAL) c.virt else c.prof; is refused (N0294, N0251) even though only a reference was wanted; the exported getter repeats the two branches instead. (A borrow-valued if expression would avoid the repetition; not proposed here.)

Tests ​

Written first and run against the unchanged kernel (2026-10-26, x86_64 debug, CosmoOS 68925d14 plus the test): the new lxtest block failed 27 checks: getrusage and times were -ENOSYS, the CPU-time clocks advanced through a 200 ms sleep, and both timers were -EINVAL.

TestWhereChecks
lxtest's CPU-time block (guest, both arches)tests/linux/lxtest.c test_cputime, marker lxtest: cputime oksleep costs no CPU time; 100 ms of user-mode CPU is ≥ 50 ms of user time in getrusage (process and thread) and times; neither part goes down; a forked child's 100 ms reaches wait4's rusage and then RUSAGE_CHILDREN and times; both timers set, read (value plus at most a tick), cleared; both fire on CPU time and one-shots disarm; bad which/who are -EINVAL
test_cputime (host)tests/host/test_cputime.csamples and saturation; set/get, the added tick, clearing; expiry, periodic reload, both at once, "due" reads a tick; scale against 128-bit arithmetic (a value grid and 200,000 random cases); the split's proportion, monotonicity and edge cases
fuzz_cputimetests/fuzz/fuzz_cputime.carbitrary sequences of ticks, sets, gets, splits and scalings: no Nova check fails, the split adds up and never decreases, a disarmed timer reads 0 and never fires, an armed one never reads 0

Results (run 2026-10-26, macOS host, CosmoOS branch cputime-itimers on 68925d14) ​

Checkx86_64aarch64
make host-test (all host tests, test_cputime included)PASS(host)
make fuzz (all targets, fuzz_cputime included, 20,000 mutations each)PASS(host)
make analyzeclean: 7 baselined diagnostics, 0 unexpectedclean
debug make test (lockdep on)PASS: 514 self-tests; lxtest: cputime ok, LINUXTEST: PASS; no lockdep report beyond the self-test's expected onesPASS, the same
BUILD=release make testPASS (19.4 s)PASS (21.8 s)
vendor.py --checkmatch

Two failures on the way, both in the tests, not the code. The first debug run failed one check: the test read the thread's CPU clock after the process's, so the thread's could legitimately be larger. The test now reads the thread's first. The first fuzz run found an input where ITIMER_VIRTUAL fired on a system tick. That is correct: a periodic timer whose interval is shorter than a tick falls behind and fires once per tick until it catches up, as Linux's does. The invariant now allows exactly that case.

CosmoOS's conventions also ask for a probe whose --old mode shows each defect failing on main. The PR fixes two: the CPU-time clocks read the monotonic clock, and a process lost an exited thread's CPU time (/proc summed live threads only). lxtest gained a check for the second (a thread spins 300 ms and exits; /proc/self/stat's utime plus stime must grow by at least 150 ms). tools/cputime-probe.py (run 2026-10-27, after the move):

Modex86_64aarch64
--old (the CPU-time commit reverted)PASS: 29 checks fail, every expected one; the process CPU clock moved 203,583 µs through a 200 ms sleep, the exited thread added 0 ticksPASS: the same; 204,499 µs, 0 ticks
fixed (debug make test)PASS: lxtest: cputime ok, LINUXTEST: PASSPASS

PR 2: nice values (CosmoOS #476, branch a5-nice from 1ccfd9eb; merged 1d98fc72) ​

CosmoOS's report is its docs/audit/2026-10-28-a5-nice-report.md, and its process design's "Nice values" section is normative. In short: a process's nice value (-20..19) is copied by fork and spawn and kept by exec. Linux's getpriority/setpriority reach a process, a group or a user. PRIV_PROC stands in for CAP_SYS_NICE. A new COSMO_RLIMIT_NICE defaults to 0. /proc/<pid>/stat shows priority 20 + nice and nice, and BusyBox nice/renice are enabled. The scheduler does not read the value yet: that is PR 3.

What Nova writes: the rules, in kernel/process/nice.nova (75 lines; header kernel/include/kernel/nice.h, imported): the clamp; the permission rule (Linux's set_one_prio_perm and can_nice, using the target's limit); and how setpriority's and getpriority's results fold over a group. The rules run under the target's spin lock. The single generated check (20 - clamp(n)) cannot fail, and unsafe appears only in one export. C keeps the walks, the locks and the system calls. No language change was needed. The pinned novac 0.1.1 was built from Nova's v0.1.1 tag, and make nova-check matches.

Tests (run 2026-10-28, macOS host) ​

  • Written first (1f5a61d6). Against the unchanged kernel, lxtest's nice block failed 26 checks (x86_64 debug).
  • test_nice (host) compares the rule with Linux's over all 950,400 permission inputs. The input space is small enough to try exhaustively, so there is no fuzz target.
Checkx86_64aarch64
debug make test (lockdep)PASS (178.8 s): lxtest: nice ok, LINUXTEST: PASS, SHTEST: PASS; no unexpected lockdep reportPASS (187.9 s)
make test-busybox (nice, renice, ps -o nice, in the static and the dynamic shell)PASSPASS
BUILD=release make testPASS (19.4 s)PASS (21.6 s)
make analyzeclean (7 baselined)clean
make host-test, make nova-checkPASS, match(host)

PR 3: a weighted fair share at the default level (CosmoOS #477, branch a5-fair from 1d98fc72; merged f87a808f) ​

CosmoOS's report is its docs/audit/2026-10-28-a5-fair-report.md. Its scheduler design's "The fair level" section and invariant S35 are normative. SCHED_PRIO_DEFAULT, where every user thread runs, shares a CPU by Linux's weights (nice 0 = 1024): CFS without groups. The other 63 levels stay round-robin.

What Nova writes: kernel/scheduler/fair.nova (101 lines; API kernel/include/kernel/fair.h):

  • Linux's weight table;
  • the weighted charge, exact (q·1024 + ⌊r·1024/w⌋) and saturating;
  • placement, rebasing between queues, the order, the preemption rule and the queue's minimum.

It runs under a run-queue lock, including in the tick. Its generated checks (an index, a divide and a remainder by the weight, one multiply) cannot fail. C keeps the lists, the locks and two new policy hooks. No language change was needed.

One finding matters for future kernel work, Nova or C. A yield mark first set under interrupts off slowed aarch64 debug boots by about 15 %, measured back to back in both orders, because kernel spin-waits yield in loops. Bisection put the cost in the new code paths, not the policy, and a thread-owned field fixed it.

Tests (run 2026-10-28, macOS host) ​

  • sched-fair, written first (4a3eeb3c), failed on main: nice 0 and nice 5 shared 306 and 297 ms.
  • On the branch it measures 456 and 147 ms (3.1 to 1; Linux's weights give 3.06), and nice 0 against 19 793 and 11 ms (72; 68).
  • process-nice-threads checks that a process's value reaches its threads.
  • test_fair (host) checks the arithmetic against Linux's table and 128-bit arithmetic, and a one-CPU simulation keeps shares within 2 % of the weights.
Checkx86_64aarch64
debug make test (lockdep; 522 self-tests)PASS (201.2 s)PASS (203.1 s)
self-test time against main, back to back150.1 s / 147.9 s155.4 s / 143.1 and 153.8 s
make test-busyboxPASSPASS
BUILD=release make testPASSPASS
make analyzecleanclean
make host-test, make nova-checkPASS, match(host)

Pre-1.0: the language and its tools are still changing.