Skip to content

feat: Network Update Loop (RFC #8)#483

Merged
0xFA11 merged 20 commits intodevelopfrom
feature/network-update-loop
Feb 24, 2021
Merged

feat: Network Update Loop (RFC #8)#483
0xFA11 merged 20 commits intodevelopfrom
feature/network-update-loop

Conversation

@0xFA11
Copy link
Copy Markdown
Contributor

@0xFA11 0xFA11 commented Feb 16, 2021

  • Implements RFC #8: Network Update Loop (tracked by Track RFC #8: Network Update Loop #479)
  • Replaces existing NetworkUpdateManager and NetworkUpdateLoopSystem with RFC-backed NetworkUpdateLoop and INetworkUpdateSystem
  • Migrates NetworkingManager, NetworkedBehaviour, RpcQueueProcessor, RpcQueueContainer and other types to the new infrastructure
  • Implements UpdateStagesPlain and UpdateStagesMixed runtime tests in NetworkUpdateLoopTests

/// <summary>
/// Represents the network update loop injected into low-level player loop in Unity.
/// </summary>
public static class NetworkUpdateLoop
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main class that implements the network update loop which needs to be reviewed thoroughly.

Copy link
Copy Markdown
Member

@NoelStephensUnity NoelStephensUnity Feb 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaand I forgot to attach List vs HashSet performance comparison to the PR description — I will do that soon :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HashSet turned out to be the better choice — HashSet's Add and Remove are faster than List's. @mattwalsh-unity FYI :)

Copy link
Copy Markdown
Contributor Author

@0xFA11 0xFA11 Feb 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also 2D array (INetworkUpdateSystem[][] m_UpdateSystem_Arrays aka NetworkUpdateStageINetworkUpdateSystem[]) 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) ¯_(ツ)_/¯

@0xFA11 0xFA11 force-pushed the feature/network-update-loop branch from 3b55465 to db058d7 Compare February 16, 2021 14:10
Comment thread com.unity.multiplayer.mlapi/Runtime/Core/NetworkUpdateLoop.cs Outdated
{
var subsystems = playerLoopSystem.subSystemList.ToList();
{
int indexOfScriptRunBehaviourFixedUpdate = subsystems.FindIndex(s => s.type == typeof(FixedUpdate.ScriptRunBehaviourFixedUpdate));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

@0xFA11 0xFA11 Feb 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they are not exposed and they don't implement anything other than CreateLoopSystem.

2 main reasons:

  1. it's Unity-like: EarlyUpdate.ScriptRunDelayedStartupFrame
  2. it allows us to differentiate between PlayerLoopSystems by type = 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but why not (similar to my other comment) declare a dictionary with key of PostLateUpdate, etc. and they the value being the function?

Comment thread com.unity.multiplayer.mlapi/Runtime/Core/NetworkUpdateLoop.cs Outdated
Comment thread com.unity.multiplayer.mlapi/Runtime/Core/NetworkUpdateLoop.cs Outdated
@@ -0,0 +1,525 @@
using System.Linq;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 into NetworkUpdateLoop to 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!");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an error log or should it be a hard assert / crash?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could move this repeated logic to a helper function?

Suggested change
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());
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@mattwalsh-unity mattwalsh-unity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented

if (!m_UpdateSystem_Sets[updateStage].Contains(updateSystem))
{
m_UpdateSystem_Sets[updateStage].Add(updateSystem);
m_UpdateSystem_Arrays[updateStage] = m_UpdateSystem_Sets[updateStage].ToArray();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

@0xFA11 0xFA11 merged commit bae18df into develop Feb 24, 2021
jeffreyrainy pushed a commit that referenced this pull request Feb 24, 2021
* 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
@0xFA11 0xFA11 deleted the feature/network-update-loop branch March 5, 2021 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants