SDK MainUpdate
NV:MP game servers run a lot of code that cannot be multi-threaded within its main update. This runs on the application's main thread and iterates finitely until the program is shut down by any means. The main-thread update also dispatches worker threads with jobs that can be run multi-threaded, and it awaits for all jobs to complete before advancing into the next frame. This makes the main thread update very sensitive to the code it runs, as when we constrain the server to 60Hz tick-rate, there is only 16.6ms of work available per frame before the main-thread goes over, and it tries to catch up in later frames.
Duties in order for the main-thread execution
- Single-threaded .NET plugin "Update" functions.
- Dispatching and waiting on multi-threaded object update jobs to complete
- Dispatching and waiting on multi-threaded PVS jobs to complete
- Single-threaded world synchronisation
- Single-threaded post-sync updates
- Single-threaded socket servicing
- Any unspent time before the next frame is expected is waited on
Seeing the flow of execution during a single frame shows how much this update can be sensitive to large operations made on the .NET update. If for example, the .NET plugin in use spent 10ms completing a database transaction, that could prevent a network sync update from arriving in time on player's machines, creating a micro stutter. If this time was 1000ms due to some sort of API timeout being exhausted, then players will freeze for a second before resuming.
Best practices for plugin update functions
Only update services or systems at frequency rates needed.
If you run a lottery system on your server, you may only need to update the logic for this every 30 minutes or so - or you could time up the next update needed for it at the exact time a lottery finishes its game. Utilizing DateTimeOffset as a clock to schedule main-thread units of work against grants more freedom over how much you wish to allocate to the main thread update.
For tasks that require exhausting operations, use the TaskPool
Running an expensive operation (such as an I/O operation) will incur performance issues that are unsuitable for the main-thread update. You may use built in .NET worker threads to off-load a lot of this work to the thread-pool, as long as your operations are safe enough that it wont unsafely access memory resources that are not multi-thread safe. For example, an expensive REST callback can be wrapped in Task.Run and forgotten about. You could also have this written back to a thread-safe state if you require the result at a later frame. The key point is to always write long-running code with the intention of it calling back on a future frame, and not on the same frame unless the overhead is minuscule.
Be careful and aware of the implications of detached thread updates
Not only for the safety of any memory or objects it accesses, but detached threaded updates can interfere with the dispatching and waiting of multi-threaded jobs performed by the game server. If your thread runs at the same time as one of the cores processing any jobs within the server's main thread update, then you could block that core entirely, or incur excessive context-switches on the core. Use NativeServer.IsInJobDispatchUpdate to avoid hurting the job updates