Search Unity

Taking forever to enter Playmode? [Solution Inside]

Discussion in 'Entity Component System' started by Singtaa, Aug 1, 2018.

  1. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    Update: See Post #4 for complete code fix.

    At around 60+ systems in my ECS project, I noticed a dramatic slow down in entering Playmode. After some digging around, I came to the conclusion that this is due to how the System Graphs are being built during
    ScriptBehaviourUpdateOrder.UpdatePlayerLoop


    The dependencies are processed using depth-first recursive functions that pretty much visit every possible path. So for example if you have, 6 UpdateGroups with 10 systems in each (a very normal scenario), the recursive function will at least execute 1,000,000 times (10^6). *Most likely much more than that due to other complexities. On my system, this led to 15 seconds of loading into Playmode (up from the normal 2 seconds).

    Even if you don't use UpdateInGroup, you'll still face the same problem when your ECS project grows. The only way to mitigate this is to keep the dependencies as linear as possible.



    Ending note: hopefully Unity devs can revisit BuildSystemGraph() so that it can handle much more than a measly 6 UpdateGroups.
     
    Last edited: Dec 12, 2018
    macagu and 5argon like this.
  2. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    I have never suspect this, I am at 20-30 systems and the game is kind of slow to get into play mode now. But the non-linear way is much more scalable, since each system only states the order in terms of it's data requirement not a concrete order where I want it to be. (i.e. it can be anywhere that is the most efficient but please give me correct behaviour)

    Yet another roadblock to the 500ms iteration time we are aiming for asides from asssembly reload time. (https://forum.unity.com/threads/unity-incremental-c-compiler.523993/page-4#post-3509758) Perhaps only in editor we could serialize the graph and cache that in EditorPrefs or something? So that entering a play mode without order change could be in an instant.
     
  3. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Thanks, we'll take a look.
     
    SugoiDev likes this.
  4. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    @Joachim_Ante So the performance hog are in ValidateAndFixSingleChainMinPos and ValidateAndFixSingleChainMaxPos which are doing DFS to cover every path. All they are really doing is setting the longest distance. This can be done in linear time if we do a topological sort first.

    I re-implemented this using algorithms described here: https://www.geeksforgeeks.org/find-longest-path-directed-acyclic-graph/

    Performance is now linear instead of exponential. The following code are tested and working:

    Code (CSharp):
    1. private static void ValidateAndFixSystemGraph(Dictionary<Type, DependantBehavior> dependencyGraph) {
    2.  
    3.     ...
    4.  
    5.     // // Validate that the chains are not over constrained with combinations of system and engine dependencies
    6.     // foreach (var typeAndSystem in dependencyGraph)
    7.     // {
    8.     //     var system = typeAndSystem.Value;
    9.     //     if (system.UpdateBefore.Count == 0)
    10.     //         ValidateAndFixSingleChainMaxPos(system, dependencyGraph, system.MaxInsertPos);
    11.     //     if (system.UpdateAfter.Count == 0)
    12.     //         ValidateAndFixSingleChainMinPos(system, dependencyGraph, system.MinInsertPos);
    13.     // }
    14.  
    15.     // Remove the last validation pass that involves
    16.     // ValidateAndFixSingleChainMaxPos and ValidateAndFixSingleChainMinPos
    17.     // And replace with the following:
    18.  
    19.     SetLongestDistance(TopoSort(dependencyGraph, true), dependencyGraph, true);
    20.     SetLongestDistance(TopoSort(dependencyGraph, false), dependencyGraph, false);
    21. }
    22.  
    23. static void SetLongestDistance(DependantBehavior[] deps, Dictionary<Type, DependantBehavior> dependencyGraph, bool after) {
    24.     for (var i = 0; i < deps.Length; i++) {
    25.         var adjs = after ? deps[i].UpdateAfter.ToArray() : deps[i].UpdateBefore.ToArray();
    26.         for (var j = 0; j < adjs.Length; j++) {
    27.             var v = dependencyGraph[adjs[j]];
    28.             if (after) {
    29.                 if (v.LongestSystemsUpdatingAfterChain < deps[i].LongestSystemsUpdatingAfterChain + 1)
    30.                     v.LongestSystemsUpdatingAfterChain = deps[i].LongestSystemsUpdatingAfterChain + 1;
    31.             } else {
    32.                 if (v.LongestSystemsUpdatingBeforeChain < deps[i].LongestSystemsUpdatingBeforeChain + 1)
    33.                     v.LongestSystemsUpdatingBeforeChain = deps[i].LongestSystemsUpdatingBeforeChain + 1;
    34.             }
    35.         }
    36.     }
    37. }
    38.  
    39. static void TopoSortRecursive(DependantBehavior dep, Dictionary<Type, DependantBehavior> dependencyGraph, Stack<DependantBehavior> stack, bool after) {
    40.     dep.visited = true;
    41.     var adjs = after ? dep.UpdateAfter.ToArray() : dep.UpdateBefore.ToArray();
    42.  
    43.     for (var i = 0; i < adjs.Length; i++) {
    44.         var v = dependencyGraph[adjs[i]];
    45.         if (!v.visited)
    46.             TopoSortRecursive(v, dependencyGraph, stack, after);
    47.     }
    48.  
    49.     stack.Push(dep);
    50. }
    51.  
    52. static DependantBehavior[] TopoSort(Dictionary<Type, DependantBehavior> dependencyGraph, bool after) {
    53.     var stack = new Stack<DependantBehavior>();
    54.  
    55.     var visited = new bool[dependencyGraph.Count];
    56.     for (int i = 0; i < dependencyGraph.Count; i++)
    57.         visited[i] = false;
    58.  
    59.     foreach (var typeAndSystem in dependencyGraph) {
    60.         typeAndSystem.Value.visited = false;
    61.     }
    62.  
    63.     foreach (var typeAndSystem in dependencyGraph) {
    64.         if (typeAndSystem.Value.visited == false)
    65.             TopoSortRecursive(typeAndSystem.Value, dependencyGraph, stack, after);
    66.     }
    67.  
    68.     return stack.ToArray();
    69. }
    With this change, my existing ECS project with 6 UpdateGroups and 60 systems can now be build in < 1ms (instead of 15 seconds).
     
  5. Justin_Larrabee

    Justin_Larrabee

    Joined:
    Apr 24, 2018
    Posts:
    106
    Has this been addressed in a recent release? I'm using 0.0.12-preview.21 and my project suddenly took minutes to enter play mode after adding one new update group and I had no idea why until I found this post. I use nested update groups to reduce inter-dependencies among systems when defining update order and it has worked wonderfully until now.

    Edit: Yep this is still an issue and the underlying code is unchanged. I switched to using Creepgin's solution and my entering play mode time went back to normal.
     
    Last edited: Dec 12, 2018
  6. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Not yet. But its on the shortlist to be rewritten right now.
     
    Zoey_O likes this.
  7. Justin_Larrabee

    Justin_Larrabee

    Joined:
    Apr 24, 2018
    Posts:
    106
    I ran into another major issue with the dependency graph discovery that was extremely difficult to track down. Here's an example:

    Code (CSharp):
    1. [UpdateBefore(typeof(Group2))]
    2. public class Group1 {}
    3.  
    4. [UpdateInGroup(typeof(Group1))]
    5. public class System1 : ComponentSystem {}
    6.  
    7. [UpdateAfter(typeof(Group1))]
    8. [UpdateBefore(typeof(Group3))]
    9. public class Group2 {}
    10.  
    11. [UpdateAfter(typeof(Group2))]
    12. public class Group3 {}
    13.  
    14. [UpdateInGroup(typeof(Group3))]
    15. public class System2 : ComponentSystem {}
    As a user I would expect that System2 would be forced to execute after System1. However, since Group2 does not have a system in it yet, the graph discovery process completely skips the dependency described by Group2, leading to System1 and System2 having no dependencies for before/after on each other.

    I spent a full day tracking this issue down after moving a single system to a new group. The previous group became empty and all of a sudden my entire system update order was gibberish and I had no idea why.

    I would suggest that adding a tag interface ISystemGroup (or whatever) that could be used to discover groups, and then in situations like this you could properly recurse into the previous/next non-empty group to correctly gather system dependencies.
     
  8. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    This is exactly why I advocate designing your project to care about order as little as possible. Once a project gets large, you will lose days or even weeks of development time when people make some minor change and suddenly everything does not work again. And this happens over and over again, trust me.

    Now it's not really possible to have no order dependencies, especially with barriers, but our rule of thumb is only when absolutely necessary and never more than 1. 99% of time it does not matter if a system is 1 frame delayed.
     
    Last edited: Jan 31, 2019
    Antypodish and 5argon like this.
  9. Justin_Larrabee

    Justin_Larrabee

    Joined:
    Apr 24, 2018
    Posts:
    106
    This issue has nothing to do with the design of my systems. To your point, I should be able to move a system with little effect on other systems. In this case I did just that and the dependency graph building process failed to pick up the change due to what I consider to be a bug in the dependency graph generation. My other systems had no direct dependency on the system I moved, they simply cared about roughly where in the frame they execute.

    It is a wonderful goal to reduce inter-dependencies among systems, and you will find no argument from me in the benefits of doing this. However it is a gross generalization to assume that a game would rarely need to care when the order of things happen.

    I am working on a non-trivial prototype project with dozens of meaningful systems, and I have run into this issue as a solo developer. The FPS example published by Unity itself had to use manual system update order because they needed to control the simulation more explicitly to account for networking issues.
     
    jdtec likes this.
  10. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    We are working on rewriting system ordering now.
     
  11. Mafutta

    Mafutta

    Joined:
    Sep 30, 2014
    Posts:
    45
    What is the status of this? I am experiencing very slow to play in 2020.2.1f1 (> 30 seconds)
     
  12. jasons-novaleaf

    jasons-novaleaf

    Joined:
    Sep 13, 2012
    Posts:
    181
    Antypodish likes this.