Conversation
| /// <summary> | ||
| /// Represents the network update loop injected into low-level player loop in Unity. | ||
| /// </summary> | ||
| public static class NetworkUpdateLoop |
There was a problem hiding this comment.
This is the main class that implements the network update loop which needs to be reviewed thoroughly.
There was a problem hiding this comment.
Should we have a data structure passed into the NetworkUpdate method? This could be useful in the event we want to include other useful information (i.e. network tick).
| /// </summary> | ||
| public static NetworkUpdateStage UpdateStage; | ||
|
|
||
| private static readonly List<INetworkUpdateSystem> m_Initialization_List = new List<INetworkUpdateSystem>(); |
There was a problem hiding this comment.
aaand I forgot to attach List vs HashSet performance comparison to the PR description — I will do that soon :)
There was a problem hiding this comment.
HashSet turned out to be the better choice — HashSet's Add and Remove are faster than List's. @mattwalsh-unity FYI :)
There was a problem hiding this comment.
also 2D array (INetworkUpdateSystem[][] m_UpdateSystem_Arrays aka NetworkUpdateStage → INetworkUpdateSystem[]) performance is on par with multiple linear arrays approach (INetworkUpdateSystem[] m_Initialization_Array, INetworkUpdateSystem[] m_EarlyUpdate_Array, ... etc.) — @jeffreyrainy FYI
I guess it's because once you get hit by pointer indirection, it probably doesn't get much worse if you jump between 2 pointers (2D) vs one pointer only (linear) ¯_(ツ)_/¯
3b55465 to
db058d7
Compare
| { | ||
| var subsystems = playerLoopSystem.subSystemList.ToList(); | ||
| { | ||
| int indexOfScriptRunBehaviourFixedUpdate = subsystems.FindIndex(s => s.type == typeof(FixedUpdate.ScriptRunBehaviourFixedUpdate)); |
There was a problem hiding this comment.
Lines 72 to 84 mostly duplicates lines56 to 68. This is an anti-pattern. Could we have a single block that uses variables/parameters?
Same for lines 88 to 100, 104 to 116, 120 to 132, and 136 to 148.
There was a problem hiding this comment.
repeating this pattern for different PlayerLoopSystems is necessary:
var subsystems = playerLoopSystem.subSystemList.ToList();
...
playerLoopSystem.subSystemList = subsystems.ToArray();
we can't reuse the same List instance and/or set it to subSystemsList as a static array.
we copy out what's in there, modify the list and set it back — we don't orchestrate from scratch.
| PlayerLoop.SetPlayerLoop(customPlayerLoop); | ||
| } | ||
|
|
||
| private struct NetworkInitialization |
There was a problem hiding this comment.
I find the use of multiple classes (NetworkInitialization, NetworkEarlyUpdate, NetworkFixedUpdate, ...) to be very class-heavy. Since the class don't have different functions and members, it seems a single class could do with a parameter, maybe of type NetworkUpdateStage.
There was a problem hiding this comment.
Seems like this would be less error prone / more extensible / easier to read...but @MFatihMAR I thought you had a rationale for going this way?
There was a problem hiding this comment.
they are not exposed and they don't implement anything other than CreateLoopSystem.
2 main reasons:
- it's Unity-like:
EarlyUpdate.ScriptRunDelayedStartupFrame - it allows us to differentiate between
PlayerLoopSystems bytype = typeof(XXX)(that's also how we find specific places to inject our stuff)
so, I'd say they serve a pretty valid purpose here.
There was a problem hiding this comment.
Ok, but why not (similar to my other comment) declare a dictionary with key of PostLateUpdate, etc. and they the value being the function?
| @@ -0,0 +1,525 @@ | |||
| using System.Linq; | |||
There was a problem hiding this comment.
I don't understand why we need this file at all... seems overly complicated for just adding something to the loop, like are we just wanting any sort of object be able to register itself in the loop ?
There was a problem hiding this comment.
From RFC's motivation section:
Even though it is possible to use low-level Player Loop API directly to insert custom
PlayerLoopSystems into the existing player loop, it also requires a non-trivial amount of boilerplate code and could cause fundamental issues in the engine runtime if not done very carefully. We would like to have less boilerplate code and also be less error-prone at runtime.
Beyond that, the proposed design standardizes
NetworkUpdateStages which are going to be executed at specific points in the player loop. This allows other framework systems tied intoNetworkUpdateLoopto be aligned, such as RPCs executing at specific stages. We would like to standardize the execution of network stages as an infrastructure in the framework.
| int indexOfScriptRunDelayedStartupFrame = subsystems.FindIndex(s => s.type == typeof(EarlyUpdate.ScriptRunDelayedStartupFrame)); | ||
| if (indexOfScriptRunDelayedStartupFrame < 0) | ||
| { | ||
| Debug.LogError($"{nameof(NetworkUpdateLoop)}.{nameof(Initialize)}: Cannot find index of `{nameof(EarlyUpdate.ScriptRunDelayedStartupFrame)}` loop system in `{nameof(EarlyUpdate)}`'s subsystem list!"); |
There was a problem hiding this comment.
Is this an error log or should it be a hard assert / crash?
There was a problem hiding this comment.
To Matt point if we return what happens? I assume all other things are just messed up and its not even clear what the implication of the return would be here for code relying on in the rest or other loops being setup.
| } | ||
| else if (playerLoopSystem.type == typeof(Update)) | ||
| { | ||
| var subsystems = playerLoopSystem.subSystemList.ToList(); |
There was a problem hiding this comment.
Could move this repeated logic to a helper function?
| var subsystems = playerLoopSystem.subSystemList.ToList(); | |
| void helper(var subSystemList, var someType) | |
| { | |
| var subsystems = subSystemList.ToList(); | |
| { | |
| int indexOfScriptRunBehaviourUpdate = subsystems.FindIndex(s => s.type == typeof(someType)); | |
| if (indexOfScriptRunBehaviourUpdate < 0) | |
| { | |
| // debug error or probably assert | |
| return; | |
| } | |
| // insert before `Update.ScriptRunBehaviourUpdate` | |
| subsystems.Insert(indexOfScriptRunBehaviourUpdate, NetworkUpdate.CreateLoopSystem()); | |
| } | |
| } |
There was a problem hiding this comment.
PostLateUpdate adds after not before, also Initialization adds to the bottom. we would still keep those two around and can't use helper to work with them :(
| } | ||
| } | ||
|
|
||
| private static readonly HashSet<INetworkUpdateSystem>[] m_UpdateSystem_Sets = |
There was a problem hiding this comment.
Seems like you could declare this as a dictionary with key of type NetworkUpdateStage and be more maintainable (vs. remembering to maintain the comments, having a potentially missing index)
There was a problem hiding this comment.
I was mainly concerned about Dictionary lookup on every update but apparently, one lookup per update stage group is not looking too bad. I verified my performance concerns with a local benchmark, both index-to-array and dictHash-to-array approaches are looking almost identical in terms of runtime performance (tested with 128, 1k, 4k and 8k objects).
I'll update shortly.
| if (!m_UpdateSystem_Sets[updateStage].Contains(updateSystem)) | ||
| { | ||
| m_UpdateSystem_Sets[updateStage].Add(updateSystem); | ||
| m_UpdateSystem_Arrays[updateStage] = m_UpdateSystem_Sets[updateStage].ToArray(); |
There was a problem hiding this comment.
This is going to generate garbage with the ToArray() aka an allocation every time a thing is registered.
| if (m_UpdateSystem_Sets[updateStage].Contains(updateSystem)) | ||
| { | ||
| m_UpdateSystem_Sets[updateStage].Remove(updateSystem); | ||
| m_UpdateSystem_Arrays[updateStage] = m_UpdateSystem_Sets[updateStage].ToArray(); |
* implement NetworkUpdateLoop * remove AdvanceFrame() and FrameCount * update player loop injection points and INetworkUpdateSystem API * add xmldoc and notes * add NetworkUpdateLoopTests * implement NetworkUpdateLoop plain and mixed tests * update xmldoc * small meta fix * replace old network update loop with new RFC-backed network update loop * replace WaitForEndOfFrame with WaitUntil in tests * comment 'default' for NetworkUpdateStage.Update enum value * optimization & refactoring pass * dictionary lookups instead of index access to arrays * optimize array allocation
NetworkUpdateManagerandNetworkUpdateLoopSystemwith RFC-backedNetworkUpdateLoopandINetworkUpdateSystemNetworkingManager,NetworkedBehaviour,RpcQueueProcessor,RpcQueueContainerand other types to the new infrastructureUpdateStagesPlainandUpdateStagesMixedruntime tests inNetworkUpdateLoopTests