Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Unexpected Performance Spikes in Unity UI with No Interaction (PC build) - Unity 2021.3.12f1

Discussion in 'Scripting' started by GuirieSanchez, Jul 17, 2023.

  1. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Hi everyone,

    I've recently been using the profiler to study Unity UI's performance and I've been trying to identify the root causes of some performance spikes associated with PostLateUpdate.PlayerUpdateCanvases and PreLateUpdate.ScriptRunBehaviourLateUpdate calls. These spikes appear to happen even when seemingly doing nothing.

    In my project setup, I have a few canvases, each hosting a few images, a Scroll Rect with a child Content that has a Vertical Layout Group attached, and a few inactive prefab objects within the Content, which contain TextMeshProUGUI-text components. I am aware that these elements can be performance-intensive, especially when redraws or dirty flag set operations occur.

    Here's the issue I'm facing: During testing, I'm not interacting with or modifying any values (such as transform, color, alpha, text, etc.), so there shouldn't be any triggers for redraws or other expensive Canvas operations. My setup is as follows: Only one canvas is set to be enabled while the rest are disabled, including their Graphic Raycasters. The enabled canvas doesn't contain a scroll rect, images, or any V/H layout groups. Despite this, I notice these performance spikes when simply observing the profiler, even without any direct interaction with the application.

    Let me share a couple of screenshots: The first one shows the usual time it takes to compute the "PostLateUpdate.PlayerUpdateCanvases"

    upload_2023-7-17_18-10-38.png

    The second one illustrates the time it takes to compute during one of the many spikes:

    upload_2023-7-17_18-10-43.png

    The "PreLateUpdate.ScriptRunBehaviourLateUpdate" section also appears to exhibit similar fluctuations:

    Normal operation:

    upload_2023-7-17_18-10-48.png

    During a spike:

    upload_2023-7-17_18-10-51.png


    There is a clear discrepancy in these values. If anyone has insights on what could be causing these spikes, your help would be greatly appreciated.


    PS: Please note that there are no issues related to garbage generation or GC. Additionally, these spikes occur regardless of whether Deep Profile is enabled or disabled.


    PS 2: As an aside, to isolate the problematic element causing the spikes, I created a new project and gradually added objects to a canvas. However, right after adding a single canvas to the project, I noticed periodic spikes on the Camera Render/Culling, and some larger, periodic spikes on "GUITexture.Draw" (every 3-5 seconds):

    upload_2023-7-17_17-20-59.png

    As you can see, there is a 103.38 ms spike on the PostLateUpdate.FinishFrameRendering > WatermarkRender > "GUITexture.Draw".

    This profiling data is from a build with Deep Profile enabled.

    For anyone interested in reproducing this, here are the steps

    (1) Create a new project (the default objects are)
    • SampleScene
    • Main Camera
    • Directional Light
    • EventSystem
    (2) Add the following:
    • Canvas (Set to Screen Space - Camera with the Main Camera assigned, Canvas Scaler UI Scale Mode: Scale With Screen Size. Everything else is set to default).
     
  2. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    280
    GuirieSanchez likes this.
  3. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    280
    Also, this is just a sort of rule of thumb performance tip off the top of my head for Unity UI. If you subdivide your UI into sub-canvases it can potentially increase your performance. Canvas components can be added anywhere in the UI hierarchy. If you only have one Canvas component at the root and something becomes dirty, concerning layout rebuild or graphics rebuild, it goes through the entire canvas heirarchy calling the relevant layout and graphics rebuild methods on every UI component. If you add more canvas elements to the hierarchy, breaking the big monolithic canvas into sub-canvases, it only rebuilds the dirty canvas, and won't rebuild any sub-canvases that are not dirty. However, you also have to consider draw call batching. Elements in separate canvases cannot be batched into one draw call on the GPU. Batching in the UI can be a little weird because you might have objects that are logically organized one way according to how you would think of it as a human, but each of those objects is in layers. You could actually see a performance increase by batching the draw call of each layer of all of those separate objects. So, the background panels of all 12 inventory objects could all be done in a single draw call, then the inventory images could be rendered on top of those panels all as a single draw call, if their sprites come from the same texture atlas. Etc. etc. That means breaking your UI up into weird branches maybe, but it's relevant in thinking how to break up your canvas into sub-canvases as well. You can analyze and see for yourself how the UI is drawn to the screen draw call by draw call by opening up the Frame Debugger in Windows > Analysis > Frame Debugger. It will pause the game and you can step through step by step seeing exactly how that one frame of the game gets pieced together and rendered to the screen. It definitely helps me to see that with my own eyes when thinking about how to break up my UI.
     
    GuirieSanchez likes this.
  4. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Hi @CodeRonnie

    Thanks for jumping in. I tried doing that before posting. Specifically, I completely removed the "Canvas.ForceUpdateCanvases()" function from the ScrollRect for testing:


    Code (CSharp):
    1.  private void EnsureLayoutHasRebuilt()
    2.         {
    3.             //if (!m_HasRebuiltLayout && !CanvasUpdateRegistry.IsRebuildingLayout())
    4.             //    Canvas.ForceUpdateCanvases();
    5.         }
    This function is invoked from the LateUpdate method, so I suspected it might be a potential culprit, as suggested by the profiler's findings.

    However, despite the drastic improvement in frame computation time, the performance spikes persist. For instance, let's say prior to these modifications, ScrollRect.LateUpdate() consumed 1 ms and the spikes consumed 10 ms. Now, with the modifications, ScrollRect.LateUpdate() only takes 0.2 ms and the spikes take 2 ms. It's important to note that the ratio between standard computation and spike computation remains approximately the same, at about 1:10.


    I would like to identify the root cause of these spikes to ensure consistent computation times, regardless of any other potential improvements (that I will probably make :D).
     
    CodeRonnie likes this.
  5. GuirieSanchez

    GuirieSanchez

    Joined:
    Oct 12, 2021
    Posts:
    387
    Thanks for the tip. In fact, I've been using nested Canvases as suggested by Ian in his Unite Europe 2017 talk. I've been enabling and disabling Canvases as needed, and I noticed a slight improvement compared to using a single Canvas.

    Obviously, it's not as efficient as enabling and disabling game objects because Unity still needs to compute them. However, doing so would result in significant spikes in performance every time a game object is activated/deactivated, so it's kind of a trade-off: we get more consistent, albeit higher, computation times vs lower computation times with big spikes.

    Also, I think your point about splitting these Canvases into static and dynamic to save on batching draw calls is really important. I haven't tried it yet mainly because I can't for the life of me figure out how to do this with my current hierarchy, but it's definitely something I'm looking forward to tackling.