Tasharen Entertainment Forum

Support => NGUI 3 Support => Topic started by: Marckeeling on February 27, 2014, 05:32:19 PM

Title: GC Alloc each frame
Post by: Marckeeling on February 27, 2014, 05:32:19 PM
Ive been profiling our game after we were getting some GC.Collect calls thrashing the FPS. We've narrowed it down to two NGUI related calls. UIPanel.LateUpdate and the OnMouse events im guessing it creates. Ive attached a picture of the profiler.

All our UI does each frame is update a FilledSprites fill. It can be seen on the 3 green bars and the thermometer. That is ALL that we have updating here but eventually this seems to make the GC collect causing an FPS drop for us.

Ive tried moving the none static Sprites to their own panel to reduce having to redraw the entire UI but the allocation eventually comes.

Could I be doing something wrong or are these potential leaks here?
Title: Re: GC Alloc each frame
Post by: ArenMook on February 27, 2014, 05:36:09 PM
That 56 bytes allocation only happens inside Unity. If you look closely, that entire function's content is inside #if UNITY_EDITOR.

NGUI also doesn't use OnMouse events. That's a Unity event, so if you are using it in one of your functions, remove it and replace it with OnHover (bool isOver), which is an NGUI event.
Title: Re: GC Alloc each frame
Post by: GavinWoods on February 28, 2014, 04:27:10 AM
Hi, my reg email arrived so i'll continue this.

This still happens when profiling on Vita.

The deep profile (on PC) shows its reallocating for the mesh which is linked to my UI panel. Im not adding anything to this panel. All im doing is moving the fill amounts for 2 sprites and enabling/disabling something floating GUI over the player to show hes in danger.

See attachments for the profiler
Title: Re: GC Alloc each frame
Post by: GavinWoods on February 28, 2014, 05:41:35 AM
I identified the issue was with enabling/disabling a sprite. I tried just fading the alpha to see if that would stop a re-draw but no luck.

What are the common things that cause a re-draw? I need 3 or so icons to appear at specific events:

Notification icon when near an interactable object
Exit indicator (displayed when over X distance away from exit)
Damage indicator (displayed when taking damage)

These are world space sprites so they do require me moving their position a lot too. Is it the position change causing the redraw or the enabling?
Title: Re: GC Alloc each frame
Post by: dillrye on February 28, 2014, 02:43:43 PM
Im pretty sure you setting a value on the sprite every frame is causing it to be seen as "dirty" and its rebuilding the vertex buffers.  That being said, the buffers themselves are reused and shouldn't be causing any memory allocation. I would try to only set values on it when changed and see what happens then.  Also, make sure you are upgraded to the latest version, there are some vertex buffer size fixes.
Title: Re: GC Alloc each frame
Post by: GavinWoods on March 03, 2014, 03:10:14 AM
Theres definitely an issue here. Ive moved the culprit into its own panel so it should be a draw call of one Sprite. Same thing happens (see attached)

Here is the code I use to update that Sprite:

  1. private void UpdateExitSymbol()
  2.     {
  3.         if (Vector3.Distance(Blaze.transform.position, new Vector3(512, 0, -512)) > 20f)
  4.         {
  5.             Vector3 screenPos = Camera.main.WorldToScreenPoint(new Vector3(512, 8, -512));
  6.             float fBounds = 17.5f;
  7.  
  8.             // Move the sprite in screen space
  9.             screenPos.x = Mathf.Clamp(screenPos.x, fBounds, Screen.width - fBounds);
  10.             screenPos.y = Mathf.Clamp(screenPos.y, fBounds, Screen.height - fBounds);
  11.  
  12.             Vector2 worldPos = Level.Instance.GetUICamera().ScreenToWorldPoint(screenPos);
  13.             FireExitSymbol.transform.position = worldPos;
  14.  
  15.             NGUITools.SetActive(FireExitSymbol.gameObject, true);
  16.         }
  17.         else
  18.         {
  19.             NGUITools.SetActive(FireExitSymbol.gameObject, false);
  20.         }
  21.     }

The GC alloc happens constantly when the active state changes and only frequently when I move the Sprite. Is this a bug or am I doing something seriously wrong here?
Title: Re: GC Alloc each frame
Post by: doggan on March 03, 2014, 04:13:58 AM
Not sure if this is related, but I've spent the past couple hours tracking down a GC spike in my application as well. If you have multiple panels active at the same time, a seemingly harmless change to a widget (that causes a re-draw) may cause the shared buffers of the two panels to fight with one another and cause alloc/GC issues: http://www.tasharen.com/forum/index.php?topic=8403.0
Title: Re: GC Alloc each frame
Post by: Nicki on March 03, 2014, 04:16:27 AM
Strange.

I mean there are multiple potential causes: Vector3.Distance, worldToScreenPoint, ScreenToWorldPoint,setting position every update, NGUI's SetActive.

Try to turn them off one by one and see if the GC goes away.
Title: Re: GC Alloc each frame
Post by: GavinWoods on March 03, 2014, 04:34:36 AM
Thanks. In this case it was NGUITools.SetActive which was causing the alloc. The GetComponents inside that function was triggering it.

Setting the transform was also causing an alloc too. I removed the sprite transform set to a panel transform set and this issue is no longer present.

I am however having the same issue as doggan. With a filled sprite needing an update each frame is causing a re-draw which inturn causes a GC alloc to throw.
Title: Re: GC Alloc each frame
Post by: ArenMook on March 04, 2014, 12:58:15 AM
Latest Pro version has per-draw call buffers, so this should address the allocation issues.
Title: Re: GC Alloc each frame
Post by: wom on March 04, 2014, 02:18:03 AM
Be aware of the 2nd page of this topic: http://forum.unity3d.com/threads/228630-Notes-on-Avoiding-Memory-Allocations/page2

Looks like the latest-ish version of Unity may have some kind of regression involving extraneous memory allocation for GetComponent() calls, especially if you're looking for a component that isn't there (which is a valid usecase).
Title: Re: GC Alloc each frame
Post by: GavinWoods on March 04, 2014, 06:51:56 AM
Latest Pro version has per-draw call buffers, so this should address the allocation issues.

Hey Aren, im still get GC alloc triggers in late update. Although less frequently now. Just on a panel that animates a FilledSprite.
Title: Re: GC Alloc each frame
Post by: GavinWoods on March 04, 2014, 10:31:18 AM
Hey Aren

After some hacking and twiddling ive come up with a solution to this. No idea if its perfect or what it'll break internally. In BetterList the trim methods were getting called when the list size shrunk which caused a re-alloc everytime.

I removed this call to make it retain the max memory it has allocated thus no re-allocation is needed. I also increased the initial allocation on better list to 64 and the max check now clamps to 128. Might be overkill.

Like I say I dont know if this trimming causes issue elsewhere but this has completely removed and GC allocations NGUI was causing in re-building.

Here is what I changed inside BetterList:

  1.         void AllocateMore ()
  2.         {
  3.                 T[] newList = (buffer != null) ? new T[Mathf.Max(buffer.Length << 1, 128)] : new T[64];
  4.                 if (buffer != null && size > 0) buffer.CopyTo(newList, 0);
  5.                 buffer = newList;
  6.         }
  7.  
  8.         /// <summary>
  9.         /// Trim the unnecessary memory, resizing the buffer to be of 'Length' size.
  10.         /// Call this function only if you are sure that the buffer won't need to resize anytime soon.
  11.         /// </summary>
  12.  
  13.         void Trim ()
  14.         {
  15.         //if (size > 0)
  16.         //{
  17.         //    //if (size < buffer.Length)
  18.         //    //{
  19.         //    //    T[] newList = new T[size];
  20.         //    //    for (int i = 0; i < size; ++i) newList[i] = buffer[i];
  21.         //    //    buffer = newList;
  22.         //    //}
  23.         //}
  24.         //else buffer = null;
  25.         }
Title: Re: GC Alloc each frame
Post by: ArenMook on March 05, 2014, 08:01:15 AM
Yea, don't do that. Trim() is necessary so that you don't waste memory. Trim() only gets called when the draw buffer size shrinks.
Title: Re: GC Alloc each frame
Post by: GavinWoods on March 05, 2014, 08:26:38 AM
This seems to solve all problems with GC allocations. Now the buffer only needs to grow to accommodate new items thus reducing the need to re-alloc when an items is enabled/disabled. Simply hiding a sprite for a few frames caused Trim to re-allocate and when it became visible caused it to re-allocate a higher amount.

This way it constantly has the max size it needs. Ive monitored the memory usage and it stays constant without any GC allocations.
Title: Re: GC Alloc each frame
Post by: ArenMook on March 05, 2014, 08:31:57 AM
Yes, and try using that with a scroll view.

In a scroll view when you are dragging something, ALL items are visible, resulting in a very large draw call. When you stop dragging, items get culled and the buffer returns to a much smaller version. In your case the buffer will always be large.

Furthermore draw calls are recycled, so if you have one large scroll view it may cause all of your other draw calls to become huge in size as well because you never release the memory.
Title: Re: GC Alloc each frame
Post by: GavinWoods on March 05, 2014, 09:31:40 AM
Im currently not using ScrollViews and dont think I have need to. You think its ok to keep it like this for now? The GC allocations were dropping our game from 60-30 and this has resolved it. In between scenes I will most likely Trim the buffers manually to stop them getting too large. As for now though it seems pretty steady at 128 allocations.
Title: Re: GC Alloc each frame
Post by: Nicki on March 05, 2014, 01:54:21 PM
You can always do something like periodic calls to trim things if you want; like when you switch from one screen to another, or when a game ends or whatever - a convenient loading place. A hiccup isn't felt during a transition.
Title: Re: GC Alloc each frame
Post by: blechowski on March 06, 2014, 03:40:24 AM
Yes, and try using that with a scroll view.

In a scroll view when you are dragging something, ALL items are visible, resulting in a very large draw call. When you stop dragging, items get culled and the buffer returns to a much smaller version. In your case the buffer will always be large.

Furthermore draw calls are recycled, so if you have one large scroll view it may cause all of your other draw calls to become huge in size as well because you never release the memory.
Could we get a feature that would allow us to regulate this behavior?

Maybe a flag in UIPanel: AutoTrimBuffers on/off?
And an interface to trim the buffers manually would be nice.
Title: Re: GC Alloc each frame
Post by: ArenMook on March 06, 2014, 11:04:06 AM
Currently trimming doesn't happen unless the buffer is less than half of what it used to be in size:
  1.                                 // If the number of vertices in the buffer is less than half of the full buffer, trim it
  2.                                 if (!trim && (verts.size << 1) < verts.buffer.Length) trim = true;
So unless the buffer size keeps changing drastically, this shouldn't be an issue at all.