RSoC 2026: EEVDF for Redox

Lobsters Hottest News

Summary

Redox OS has implemented the EEVDF scheduler, replacing DWRR, resulting in significant improvements in fairness, context switch time, and throughput.

<p><a href="https://lobste.rs/s/p92osu/rsoc_2026_eevdf_for_redox">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 08/23/26, 07:22 PM

# RSoC 2026: EEVDF for Redox - Redox Source: [https://www.redox-os.org/news/rsoc-eevdf/](https://www.redox-os.org/news/rsoc-eevdf/) ##### By Akshit Gaur onSaturday, August 22, 2026 First of all, read[this post](https://www.redox-os.org/news/rsoc-dwrr/)to get the background \(Redox OS, basic scheduling, Round Robin and Deficit Weighted Round Robin Schedulers\)\. ## TL;DR Redox OS now uses a EEVDF\-based scheduler\. The move from DWRR has netted us very significant gains in nearly every measure, a 782x improvement in fairness, a reduction of 82% in context switch time, 2\.6x increase in throughput and more\!\! A special thanks to Jacob Lorentzon \(4lDO2\) and Wildan Mubarok for the help and guidance they have provided throughout the journey, I don’t think this would have been possible without them or the others in Redox community that have helped me\! ## Sobriety in the Bar Let’s see the situation we left our bar in the last post, VIPs are well fed \(or well drunk??\) with our Interleaved DWRR approach, unfortunately the poor masses are starving \(being sober in a free\-to\-drink bar may be worse than starving\)\! And although we did not yet implement many complex heuristics like the neighbouring bar called “Linoox” had done many years ago, we would have had to, had we stuck with DWRR, because our bouncer would eventually need complex ‘heuristics’ \(guessing games\) to figure out when to cut off the VIPs so the regular folks don’t die of thirst\. Our bartenders need to think again\. :::note Although this breaks the flow of the post, I would like to emphasise that I am not criticising Linux here, to avoid any misunderstanding\. Linux used CFS for many years which was much more complex \(and different\) than a simple DWRR\. It used complex heuristics to guess the nature of the application, which over the years became bloated\. Linux replaced it with EEVDF, and it is after they have proved it, that we are even implementing it\! ::: After much discussion the bartenders come up with a new system based upon a newer Tab system in which the bartenders keep track of the importance of the client and whether they actually deserve a beer at the moment\. The way they figure it out is using lag, they track exactly how many drinks they have poured out to you and how much you actually deserved\! If you are owed beer, you have positive lag, if you drank too fast, you have negative lag\! They keep track of it as your eligible time, the point in time, where your lag is no longer negative\! Although a poor man will tolerate some time where he does not have any beer in his hand despite being owed some \(he is getting free drinks after all\!\), the more important the client is, the more impatient he will be\. So the bartenders calculate the deadline for your next drink\! The deadline is equal to your eligible time plus a baseline wait time divided by your importance \(wait / w\)\. The more important you are, the tighter the deadline\! What it results in is that the VIPs are not only owed more drinks, they get it as quickly as possible in their hands owing to their tighter deadlines, but the less important clients are not starving either as the introduction of the deadline system ensures they have a drink in their glasses before they become sober\! ## A formal introduction Earliest Eligible Virtual Deadline First Scheduler, as evident by what a mouthful of a name it has, is certainly amongst the “best” schedulers, created by Ion Stoica and Hussein Abdel\-Wahab in their 1995 paper[“Earliest Eligible Virtual Deadline First : A Flexible and Accurate Mechanism for Proportional Share Resource Allocation”](https://people.eecs.berkeley.edu/~istoica/papers/eevdf-tr-95.pdf) I am going to try to explain it\! ### Assumptions a\. We can only assign the CPU to a process in a quantum of time,`q`\. b\. A process is said to be active if it is competing for resources, passive otherwise\. A process active at time`t`belongs to the Active Set,`A\(t\)`\. c\. Each process has an associated weight with it`w`, that determines its share of resources`f`\. $$ f\_i\(t\) = \\frac\{w\_i\}\{\\sum\_\{j \\in A\(t\)\} w\_j\} $$ d\. Due to various reasons, it is not possible for a client to always receive exactly the service time it is entitled to\. Thus we assign a value,`lag`, to this difference in time it should receive and it actually receives\. $$ lag\_i\(t\) = \\underbrace\{S\_i\(t\_0^i, t\)\}\_\{\\text\{Theor\.\}\} \- \\underbrace\{s\_i\(t\_0^i, t\)\}\_\{\\text\{Actual\}\} $$ where $$ \\tag\{1\} S\_i\(t\_1, t\_2\) = w\_i \\int\_\{t\_1\}^\{t\_2\} \\frac\{1\}\{\\sum\_\{j \\in A\(t\)\} w\_j\} d\\tau $$ ### Prelude A client/process issues a request which specifies the duration of service it needs,`r`\. Therefore, in an ideal system we can solve for the deadline`d`before which the request must be serviced, given`r`\(service duration\) and`t`\(time at which the request was made\), by solving the equation\- $$ r = S\(t, d\) $$ Assuming that the share`f`of our process does not change in the interval, $$ S\(t, d\) = f \* \(d \- t\) $$$$ r = f \* \(d \- t\) $$$$ d = t \+ \\frac\{r\}\{f\} $$ Instead of clock time, EEVDF uses Virtual Time which is defined as follows\- $$ \\tag\{2\} V\(t\) = \\int\_0^t \\frac\{1\}\{\\sum\_\{j \\in A\(t\)\} w\_j\} d\\tau $$ One nice property here is that the flow of this virtual time is inversely proportional to the current competition for the resources\. When the competition is high, virtual time slows down, when it is low, it speeds up\! From 1 & 2, $$ S\(t\_1, t\_2\) = w\_i \(V\(t\_2\) \- V\(t\_1\)\) $$ ### Algorithm The basic idea behind EEVDF is quite simple, you associate two \(more\) numbers to each request \(or client\)\- 1. An eligible time`e`is the exact time that a request becomes eligible to be serviced\- $$ S\_i\(t\_0^i, e\) = s\_i\(t\_0^i, t\) $$ 2. Deadline`d`, chosen such that the service the client receives between`e`and`d`is equal to the service time requested`r`, i\.e\., $$ S\_i\(e, d\) = r $$In other words, if the client started receiving its fair share exactly at`e`,`d`is the point in time by which its request would be fully served\. One thing to keep in mind though is that this is a scheduling deadline rather than a hard real\-time guarantee, it determines ordering between eligible requests\. Before we can use them though, we need to convert them to the virtual clock\. $$ V\(e\) = V\(t\_0^i\) \+ \\frac\{s\_i\(t\_0^i, t\)\}\{w\_i\} $$$$ V\(d\) = V\(e\) \+ \\frac\{r\}\{w\_i\} $$ Now that we have all the values, we can finally define the policy\! Quoted from the original paper\- > **EEVDF ALGORITHM**\.*A new quantum is allocated to the client that has the eligible request with the earliest virtual deadline\.* Now let us define Virtual Eligible Time and Virtual Deadline at $k^\{th\}$ request, $\{ve\}^\{\(k\)\}$ & $\{vd\}^\{\(k\)\}$, $$ ve^\{\(1\)\} = V\(t\_0^i\), $$$$ vd^\{\(k\)\} = ve^\{\(k\)\} \+ \\frac\{r^\{\(k\)\}\}\{w\_i\} $$$$ ve^\{\(k \+ 1\)\} = vd^\{\(k\)\} $$ If for some reason \(eg\., early yield or block\) the service time it actually received during the $k^\{\(th\)\}$ request \($u^\{\(k\)\}$\) is not equal to $r^\{\(k\)\}$, we only need to change the last equation, $$ ve^\{\(k \+ 1\)\} = ve^\{\(k\)\} \+ \\frac\{u^\{\(k\)\}\}\{w\_i\} $$ If a client does not consume its entire slice, its next $ve$ and $vd$ are brought forward giving it precedence over an identical process that did consume its slice fully\. So to reiterate the policy by which we select the next client to serve, we choose the client with positive \(or zero\) lag \(i\.e\., Si\>= si\) with the earliest deadline\! ## Implementation in Redox I am going to walk you through the`select\_next\_context`function that contains the actual scheduling logic\. One thing to keep in mind is that we do not explicitly calculate lag \(signed variable\), instead store the local`V\(t\)`of the context which is proportional to $s\_i$, thus $$ lag = V\_\{global\} \- V\_\{local\} $$ Keep in mind that this is virtual/normalised lag\. If you want absolute lag, $$ lag\_i = w\_i \* \(V\_\{global\} \- V\_\{local\}\) $$ ### Can we still run the previous client? The first thing we do is check whether we can still run the previous context/client\. This helps us if no other context is eligible to run\. We also update its`ve`\(`vtime`in the code\) and`vd`here\. If it yielded early, we apply a penalty \(inversely scaled to its weight/priority\) to prevent processes from repeatedly yielding early to manipulate their lag and monopolise CPU time\. We also figure out if the`prev\_context`is still eligible to run \(`vtime`<`V`\)\. ### The walk through the tree All the runnable/active contexts are stored in a BTreeMap stored per\-core\. The BTreeMap has \(`vd`,`rem\_slice`\(remaining slice of service time\),`ctxt\_id`\) as its key, which ensures that the map is sorted first with vd and uses the remaining slice \(out of`BASE\_SLICE`, the amount of time, in terms of contextswitchinvocations, a client is allocated CPU time\) as a tie breaker, and their id as a last resort\. This ensures that amongst two contexts with the same virtual deadline, the one which has already started running and not completed its slice is preferred\! The values of the BTreeMap are \(`vtime`,`context\_weight`and`context\_ref`\)\. Although`vtime`and`context\_weight`are accessible after locking`context\_ref`, storing them explicitly allows us to quickly see if the context is eligible without locking, which improves the performance at the cost of some minor storage amount\. We walk through this BTreeMap, and as soon as we find an eligible context \(`vtime`<=`V`\), we break the walk and switch to it\! :::note The original paper describes an augmented tree for this, which we do not use right now because of its added implementation complexity\. I opted to use the standard BTreeMap as it is quite optimised and a standard component\. Had I chosen to create an augmented tree myself, there would have been more opportunities for bugs to sneak in while**I**was sleeping\. Regardless, most of the time, a simple BTreeMap should perform similarly to the augmented tree\. It is only in the worst case scenario \(no/minimal eligible contexts\) that the augmented tree gets an edge in the time complexity \(O\(logN\) vs O\(N\)\), but given the length of the trees in real\-world usage and its cache friendliness, I decided BTreeMap was good enough**for now**\. ::: In case that there is no eligible context present in the tree, we find the context with the minimum`vtime`, and fast\-forward our per\-core`V`to its value, thus making it eligible to run, this ensures that we do not idly waste the CPU cycles\. ### Work Stealing With the move to per\-core residence of our data\-structures, it is now possible for one core to have no contexts in its BTreeMap while another core is fully loaded\!\! To prevent this, we implement work\-stealing\! Work Stealing triggers in the following cases\- 1. The BTreeMap of our current core is empty\. 2. Once every`STEAL\_INTERVAL`with the added condition that the difference between the number of contexts in our tree and any other core is \>`STEAL\_THRESHOLD`\. If triggered we calculate the number of contexts to steal from core X as `Num of Contexts to Steal \(N\) = min\(\(X\.queue\.len\(\) \- local\.queue\.len\(\)\) / 2, MAX\_STEAL\)` We then steal the first`N`\(interspersed, i\.e\., 1st, 3rd, 5th…\) contexts from the tree of X to our own tree and adjust their`vtimes`using $$ offset = context\.vtime \- X\.V $$$$ context\.vtime = max\(0, local\.V \+ offset\) $$ ### Advancing the virtual clock When all this is done and dusted, we finally advance our virtual clock\! $$ V\_\{\\text\{local\}\} \\mathrel\{\+\}= \\frac\{\\text\{elapsed\\\_ticks\}\}\{\\text\{total\\\_weight\}\} $$ ## Other Optimisations Apart from changing the scheduler from DWRR to EEVDF, I also did the following optimisations that were significant\. ### Moving`RUN\_CONTEXTS`from GLOBAL to PerCPU A global run queue meant that two cores could not context switch at the same time and had to wait for the earlier core to release the lock\. This meant that as the number of cores increased, so did the lock contention and thus, the time taken for a context switch\. This[MR](https://gitlab.redox-os.org/redox-os/kernel/-/merge_requests/660)provided each core with its own separate run queue\. It also implemented the work stealing made necessary with this change\! ### Moving`RUN\_CONTEXTS`from VecDeque to a BTreeMap The initial implementation of EEVDF used a simple VecDeque to store the active contexts\. This[MR](https://gitlab.redox-os.org/redox-os/kernel/-/merge_requests/618)changed that to a BTreeMap dropping the time complexity of the scan from O\(N\) to O\(logN\)\. ### Removing Linear Scan This is how we handled blocked tasks earlier\- 1. When a context blocked, it was removed from the`RUN\_CONTEXTS`and moved to another global list,`IDLE\_CONTEXTS`\. 2. On each context switch, we would scan through the`IDLE\_CONTEXTS`, and check if any context became runnable, moving them from`IDLE\_CONTEXTS`to global`RUN\_CONTEXTS`\. These blocked tasks were of two types, timers and non\-timers, so it was handled in two passes\. Timers: We separated the timers and now store them in a BTreeSet which allows us to extract all the timers that will fire at the current instant\. \(Relevant[MR](https://gitlab.redox-os.org/redox-os/kernel/-/merge_requests/649)\)\. Thus the time complexity was reduced from O\(N\) to O\(logN\)\. Non\-timers: Earlier, the unblocking code only switched the flag to mark a context as Runnable, now that code is also responsible for actually placing the context in the run queue, reducing the time complexity from O\(N\) to O\(1\)\! \(Relevant[MR](https://gitlab.redox-os.org/redox-os/kernel/-/merge_requests/656)\) ## Did it change anything? Now, lets take a look at the numbers to actually quantify what this change in scheduler resulted in\! ### Fairness Fairness \(along with the next section\) are the clearest wins for our migration\. I spun up 16 identical CPU\-bound processes that do nothing except increment their counter, at the end we compare these counters to get an estimate for their CPU\-time\. Variance is min/max deviation here\- SetupDWRR VarianceEEVDF Variance16 procs / 4 cores389\-617%1\.09\-1\.37%16 procs / 1 core1940\.52%2\.48%A 782x improvement in fairness\!\! ### Context Switch Times Not directly from the move to EEVDF, but the associated move of the RunQueue from a Global to per\-core state, allowed the time required for a voluntary \(`yield\_now`\) context switch to drop from 2µs down to 350ns \(Do note that these values contain some overhead from the testing harness too, so the real numbers are probably less than reported\)\!\! For blocking context switches, see the table below, Avg\. LatencyLinux, Pinned to Core 0, Native Host0\.552µsRedox EEVDF, Single Core, QEMU0\.923µsRedox EEVDF, 4 Cores, QEMU0\.931µsLinux, Unpinned, Native Host1\.230µsRedox DWRR, Single Core, QEMU1\.367µsRedox DWRR, 4 Cores, QEMU4\.253µsThe comparision to Linux is not apples\-to\-apples, as Linux is running natively on the host while Redox is running under QEMU\. ### Starvation If you remember from this[brief announcement](https://www.redox-os.org/news/eevdf/)when EEVDF was merged, the starvation of the lower priority processes made it very difficult to even measure if the priorities were being followed properly, giving us a ratio of 1\.4x as compared to the theoretical 86\.8x\. With EEVDF, we have this ratio at 76\.87x, ~89% of the theoretical value\. The remaining difference is small and may be attributable to scheduling noise and imperfect starting points\. ### Wakeup heavy workloads For workloads where there are many sleeping threads, the new scheduler pulls out a very significant lead, more due to the various optimisations rather than the mathematical algorithm, but still… I initiated 10,000 sleeping processes and two message passing processes \(that block/wake on message sent/received\) that force a context switch\. Round Trips / secDWRR, Single Core2197DWRR, 4 Cores765EEVDF, Single Core107945EEVDF, 4 Cores109386A Round Trip here is defined as A \-\> B \-\> A\. The thing to note here is that not only does EEVDF win, by a large ~143x margin too, but also the timing remains flat under multiple cores too\! This improvement is attributable to both EEVDF and the removal of linear scan too\! ### Throughput The raw throughput, running only pixelcannon on a single core, DWRR gives ~1600 fps, which drops down to 150 fps when moving the mouse, the GUI freezes up though so you cannot see the cursor moving\. On EEVDF, the base FPS is ~1700 dropping down to ~190 when moving the cursor, yes the cursor as the GUI is still smooth and responsive with EEVDF\! DWRR Single Core\- ``` Starting Benchmarks! Message Threads: 2 Worker Threads: 2 Runtime : 30s Operations : 5 ===Results=== Runtime: 31.86s Total operations: 3935 Operations/sec: 123.50 Wakeup Latencies (usec): 50.0th: 4481024 90.0th: 4677632 99.0th: 5054464 99.9th: 5120000 min: 11388, max: 5140463 samples: 3935 Request Latencies (usec): 50.0th: 2244 90.0th: 2484 99.0th: 2556 99.9th: 2580 min: 2107, max: 2663 samples: 3935 ``` EEVDF Single Core\- ``` Starting Benchmarks! Message Threads: 2 Worker Threads: 2 Runtime : 30s Operations : 5 ===Results=== Runtime: 31.09s Total operations: 10057 Operations/sec: 323.50 Wakeup Latencies (usec): 50.0th: 1538048 90.0th: 1755136 99.0th: 1927168 99.9th: 2021376 min: 8178, max: 2051283 samples: 10057 Request Latencies (usec): 50.0th: 2148 90.0th: 2180 99.0th: 2188 99.9th: 2196 min: 2128, max: 2513 samples: 10057 ``` There is a 2\.6x improvement in ops/sec, and a significant reduction in wakeup and request latencies\!\! ## Conclusion The move to EEVDF was worth it\! This concludes my Redox Summer of Code\! This was my first real “internship” and the first time I have worked properly on a codebase that wasn’t my own\. So I am quite thankful to the entire Redox community and especially Ron Williams\!\! You can follow more of my low\-level systems deep\-dives and follow\-up work on my personal blog at[himwant\.org](https://himwant.org/)\! And my watch is ended`\_/\\\_`

Similar Articles

This Month in Redox - May 2026 - Redox - Your Next(Gen) OS

Lobsters Hottest

Redox OS, a Unix-like microkernel OS written in Rust, shares its May 2026 updates including the announcement of Summer of Code projects, implementation of EEVDF scheduler, and significant performance improvements in I/O events and filesystem inode caching.

ReactOS

Hacker News Top

ReactOS celebrates 30 years as an open-source Windows-compatible operating system, with recent updates including test suite improvements, WDDM investigation, and new developer hiring.

Deepseek V4 Flash running on RTX 5090 MoE

Reddit r/LocalLLaMA

User shares optimization benchmarks for DeepSeek-V4-Flash (Q2_K) running on an RTX 5090 using a fork of llama.cpp, achieving 21.3 tokens/s generation and 1 million context size.

QSOE: QNX-inspired OS with dual-kernel architecture

Hacker News Top

QSOE project v0.1 is released, providing a QNX-compatible operating system with two microkernel variants (custom Skimmer kernel and seL4-based), shared userspace, and support for SiFive Unmatched RISC-V hardware.

Persistent Recursive Worlds Enable Autonomous Software Evolution

Hugging Face Daily Papers

Introduces EvoX Genesis, a framework that organizes long-horizon software development around a persistent project rather than persistent agents, enabling autonomous construction of a Rust-based C compiler and reimplementation of MESA numerical modules with low cost and high performance.