Repository navigation
Map scheduler
How the map scheduler works as built, and why: every map ticked to its own deadline, on a shared pool or a thread of its own. The facts to look up are on Map scheduler ticks and Map scheduler metrics; what a map does inside a tick is on Owners map, and what's still to build is on Map scheduler design.
flowchart TD
MD["Map directory"] -->|"register, unregister"| MS["Map scheduler"]
MS -->|"earliest due deadline"| P["Pool threads"]
MS -->|"placed dedicated"| D["Dedicated thread"]
P -->|"one tick, to completion"| M["A map"]
D -->|"its ticks only"| H["A heavy map"]
A map is ticked, never threaded. A pool of threads takes whichever map's deadline is earliest and has come, runs one tick to completion, and puts the map back with its next deadline. A map placed on a dedicated thread runs alone on it. No map ticks on two threads at once.
A deadline missed is skipped, not replayed. A map's next deadline is the first one on its period not yet past, so a slow tick never turns into a burst of catch-up ticks, and the deadlines passed over are counted.
Nothing ticks in step. Each map's first deadline is staggered by a random fraction of one tick, so maps opened together don't all tick in the same millisecond.
The wait is precise on both systems. On Windows a thread waits on a high-resolution timer, because .NET's timed waits there step in 10 to 16 ms, too coarse for a 25 ms tick; on Linux it waits on its wake event with the deadline as the timeout, which also wakes a pool thread at once when a map registers. A play session on Windows held 40 ticks a second with none skipped.
A fault costs one tick, not the map's place. A tick that throws is logged and counted, and the map ticks again at its next deadline.
Stopping waits for the tick in progress. Unregistering a running map completes when its current tick ends, and the server's shutdown stops every map while the scheduler still ticks, so each map hands its characters back on its own last tick.
Every house is an instance, so a server holds far more maps than cores, and most of them are idle. A pool runs the long tail cheaply, and a dedicated thread is there for the few maps heavy enough to need one.
| Idea | What killed it |
|---|---|
| A thread per map | Every house is an instance, so thousands of mostly idle threads would each wake for their own tick, and once they outnumber the cores they're preempted mid-tick |
The scheduler's settings, with their defaults and what the host checks, are on Host settings.
Maps:PoolThreads is the shared pool that runs every map without a thread of its own; by default it leaves three logical processors for the gateway, the realm and account owners and the garbage collector. Maps:DedicatedThreads is how many heavy maps may each hold a thread; a placement past that runs in the pool. Maps:TickMs is the tick, 25, 50, 125 or 250 ms, each dividing 250 so an effect timed on a multiple of 250 ms lands on a tick. The tick times Maps:ReplicationEveryTicks is the update interval, the one the client is told to send its movement at, and it has to fall within 25 to 1,000 ms.
- Every map shares one tick and one update interval; per instance type comes later (Map scheduler design).
- A map that keeps overrunning isn't promoted to a dedicated thread, and repeated faults don't stop a map.
- Nothing holds pool and dedicated threads together within the cores; only the pool's default leaves three free.
| Piece | Path |
|---|---|
| The scheduler, its deadline queue and its waits | Source/NexusTomorrow.Maps/Scheduling/ |
| The settings and their rules |
Source/NexusTomorrow.Maps/MapOptions.cs, checked by Source/NexusTomorrow.Settings/
|
| The metrics | Source/NexusTomorrow.Maps/MapMetrics.cs |
| Tests | Tests/NexusTomorrow.Maps.Tests/Scheduling/ |
Start here
How the server works
The protocol
Working on it