A server CPU pass asked for by the owner: "decrease the amount of cpu it costs to run the servers; full deep analysis on every cpu cycle and if it needs to exist and ways to reduce them". Every change gives the same game results (checked by hashing the whole game state every tick, before and after), except the tick-rate check, which the owner asked to change. Network protocol unchanged (42); demos unaffected.
Where the CPU went (8 players, 1000 Hz, before)
- The wait loop was most of it. It spun the last 0.3 ms of every tick by re-running the whole frame, with a socket read each pass, and it woke for every input packet (8 players send 8,000 a second). On Linux that cost 25-30% of a core: 29-31% of a vCPU measured on the Oracle box, plus 30% steal. On Windows it cost about 8%, because Windows' 1 ms timer grid wakes late and the spin often started after the deadline.
- The game itself was small: 4-10 µs a tick (0.5-1% of a core). Visibility rays (which players each client is sent) were about 70% of that with 8 humans.
- Snapshot encoding cost about 3 µs per client, 60% of it in libm rounding calls.
- Syscalls (Linux): about 2,400 recvfrom a second (80% of them empty), one sendto per snapshot (4,000 a second), and about 7,000 wake-ups a second.
Wait loop (src/common/net.*, src/game/server.cpp idle(), src/server/main.cpp)
- Sleep on a timer, then spin in place. With humans on, the server sleeps on a timer alone until a short spin before the tick, then spins on the clock. It no longer reads the socket on every pass, and it no longer wakes for each input packet.
- The game results are identical. Packets wait in the socket and are read in arrival order before the tick, against the same world, clock and command tokens, and nothing a command does is sent before that tick's snapshot.
- Only info, connect and map-chunk replies can now wait up to one tick (about 1 ms).
- Linux: the server threads get 1 ns timer slack (the default is 50 µs). On the Oracle box this cut the median wake from 74 to 25 µs late.
- Receives use
recvmmsg, and a tick's snapshots go out in onesendmmsg. - Measured with humans on: about 1 sleep, 1 receive batch and 1 send batch per tick, instead of 2,500 recvfrom + 430 select + 660 sendto a second.
- Receives use
- The spin adapts (
WakeSpin). It covers the host's p99 wake lateness plus 20 µs, from 20 to 300 µs.- On a host whose lateness tail is beyond that (a capped or shared VM), it spins 60 µs at most. On the Oracle box, spinning longer made ticks later: it burned the CPU quota and the hypervisor paused the server. A 300 µs spin had p99 lateness of 6.9 ms; no spin had 0.3-1 ms.
- Windows keeps the 300 µs spin so its tick timing stays as before: p99 about 700 µs late, against about 1,000 µs without it. Its timer only fires on the 1 ms clock interrupt, and the owner's priority is latency.
--tick-spin auto|Nsets the spin; 0 never spins and uses the least CPU.
- Measured, Windows (8 humans, harbor): 8.3% → 7.4% of a core, or 2.7% with
--tick-spin 0. - Measured, Oracle box (humans on): 29-31% → 10-12% of a vCPU, and steal 30% → 11-14%. That box also stepped down to 500 Hz under the new rate check (below), so part of this drop comes from the lower rate.
Tick cost (same results)
- Visibility (
Map::raycastAnyHinted). Each viewer/target pair remembers the brush that last blocked its sight and tries it first. That answer is used only whereraycastAny's own grid walk would reach that brush and hit it, so every ray gets the answer it got before.- Checked: 2.3 billion rays compared with zero differences, and per-tick visibility state hashed identical over 14 scenarios.
- The pair scan also skips dead slots in bulk.
- About 2x faster: snapbench midtown with 32 clients went from 97.8 to 47.5 µs.
- Snapshot encoding: an exact
roundHalfAwayreplacesstd::round/lround/remainderin the quantizers. It was checked over all 2^32 floats, and the wire bytes hash identical. World encoding is 50% faster. - Bots: each bot caches its tick-rate and skill constants (exp terms, chance thresholds, sight-history offsets, turn steps), and angle wrapping takes an exact fast path when the angle is already in range. Bit-identical over 45 seeded matches at every tick rate; bot think is 15-18% faster.
- Lag compensation: the rewind finds its history record by binary search inside the contiguous run, and the old walk's checks finish it. A rifle shot rewound 250 ms costs 0.8 µs instead of 7.7.
- Hit feedback walks a per-attacker bitmask instead of a 32x32 scan.
- Reachability check: with no prober, it no longer rebuilds its status string 100 times a second.
- serverbench (pinned core): harbor average tick 12.9 → 10.3 µs, hamlet 39.6 → 28.2 µs.
Tick-rate check (owner; behaviour change)
- Why. Owner: "if clients connected to the server are reporting server based time problems, aka the ticks are not all actually the supposed length, then the server should take the hz down". On the Oracle box the server was slow and never stepped down. Late ticks only counted when the server's own CPU use was over 20%, and on a capped VM the CPU reads idle while the host pauses it.
- Late ticks. A tick that starts more than 2 snapshot intervals late (at least 3 ms, so 4 ms at 1000 Hz) has used up the client's interpolation buffer, and players see a hitch.
- If over 1% of a 3 s window's ticks are that late, the tick rate steps down whatever the CPU reads.
- Steps follow the existing rules: at the next break, or at once only when the server is over 90% busy or its own step up didn't hold. Step-ups keep the doubling wait.
- Healthy Windows hosts measured 0.0% such ticks. The Oracle box measured 5-15%, so it runs at 500 Hz or lower.
- Bandwidth. Owner: "it should do the same for network bandwidth"; the owner chose snapshots first.
- Upload and download are now counted.
- The server is short of bandwidth when it uses over 90% of
--max-upload-kbps, or when at least 2 players, and 2 in 3 of those on, lose snapshots or have their ping rise at once. - It then lowers the snapshot rate (500, 333, 250, 125 a second), at once, since the game doesn't change. Once snapshots are at their floor, it lowers the tick rate.
- Snapshots are most of the upload, and they were already capped at 500 a second apart from the tick rate, so going from 1000 to 500 Hz saved almost no upload.
- Clients follow the snapshot rate from the snapshot header.
- One lossy player no longer lowers the server's rate. Before, any single client reporting over 2% loss did.
- Reporting. Every change is logged and announced in chat with its reason.
statusshows the rates, the share of late ticks, upload/download and the last change's reason. - Tests. New tests in the tickrate group cover stall-driven steps, healthy jitter, an upload cap, and shared versus single-player congestion.
Other
- The dedicated server's
Serverobject (about 1 MB) lives on the heap instead of the main thread's stack.