Profiler Not Enough Information to find Allocation

Hello.

We are right now working in performance checking in the game, so we are doing profiling. Usually it works pretty well, and we can see that we are creating new objects, allocating new arrays, triggering UnityEvents, etc etc. However, there are some cases where it’s very hard to figure out what’s allocating memory, and I’m looking at some pointers.

Our configuration is complex so I’ll try to make it as simple as possible. In general:

  • We use Action Events quite extensively
  • We separate some of our complex code into non-MonoBehaviours so we can unit test them

The code that I’m trying to analyze is doing this (skipping logic like filtering or debugs):

  • CollectionTile is in a GameObject that has a Trigger Collider2D on it
  • When something that we care about triggers OnEnter, it sends the info to the InteractingObjects MB which manages the objects you can interact with
  • InteractingObjects has a non-MB class (Handler) that has most of the logic, and when it adds a new item it sends the info back to InteractingObjects
  • InteractingObjects triggers an Event. And CurrentItemInteraction is listening for it. CurrentItemInteraction handles prompts, player using or interacting with objects, etc etc.
  • CurrentItemInteraction checks to see if there is anything to do with an object after the event had triggered.

Now my issue: I’m getting some spikes when a new object enters the Collider so I want to see what is the culprit. After stripping some code that I don’t care about I end up with 1.5Kb generated by the logic that I explained above. The problem is that it ends in InteractingObjects method and that’s it. I tried adding ProfileMarkers but it then reports garbage in MonoJit which again reports a high level method, and I’m again in the same place I started. I have tried moving my code around to no avail (triggering methods early, etc etc).

My questions:

  • How does Unity Profiler deal with Action Events triggers? Could that be the issue that I cannot go deeper?
  • Is the issue that I’m using non-monobehaviours and it stops analyzing?
  • If I add ProfilerMarkers to a method that calls 4 other methods, would it add markers to the other methods? or do you need to add ProfilerMarker to each method?
  • What happens if the ProfilerMarker.End is not reached because a return was triggered before hand?
  • If I want to see whether a piece of code is the one that is generating garbage is there a way that I can surface it more? Assuming is very hard to test in a new empty game. Maybe moving it in an Update, Start, or something similar?
  • What defines that you can actually see the last method that is causing the allocation (for example: the word new, resizing arrays when adding items to a list, etc etc)? How can I surface that?

Thanks a lot! and Here is a summarized sample of the code:

public class CollectionTile : MonoBehaviour
{
	//...
	private void OnTriggerEnter2D(Collider2D collision)
	{
		//...
		
		interactingObjects.EnterOnPriority(currentObject);

	}
	//...

}
public class InteractingObjects : MonoBehaviour
{
	//...

	public static event Action OnChangingInteractables;
	private InteractingObjectsHandler interactingObjectsHandler;
	
	private void OnEnable()
	{
		interactingObjectsHandler.OnChangingInteractingObjects += OnModifyingInteractables;
	}
	
	public void EnterOnPriority(IInteractable currentObject)
	{
		interactingObjectsHandler.SetInteractingObject_Priority(currentObject);
	}
	
	private void OnModifyingInteractables()
	{
		OnChangingInteractables?.Invoke();
	}
	
		public class InteractingObjectsHandler
		{
			public event Action OnChangingInteractingObjects;
			
			//...
			
			public void SetInteractingObject_Priority(IInteractable interactable)
			{
				//...
				interactingObjectsPriority.Add(interactable);
				OnChangingInteractingObjects?.Invoke();
			}
		}
}

public class CurrentItemInteraction : MonoBehaviour
{
	private void OnEnable()
	{	
		InteractingObjects.OnChangingInteractables += GenerateUseAndInteractionPrompt;
	}

	//...
	 private void GenerateUseAndInteractionPrompt()
	 {
		 if (debug)
			 Debug.Log("CurrentItemInteraction | GenerateUseAndInteractionPrompt");

		 OnResettingGliphs?.Invoke();
		 ContextualInputGlyphManager.Instance.ResetGlyphs();
		 GenerateUsePrompts();
		 GenerateInteractionPrompts();
		 CheckForSettingItem();
	 }
	 
	//...
	private void GenerateInteractionPrompts()
	{
		int amountOfInteractables = interactingObjects.HowManyInteractableObjects(false);

		interactingObjects.IndexToCheck = -1;

		if (amountOfInteractables == 0) 
			return;

		if (debug)
			Debug.Log($"CurrentItemInteraction | GenerateInteractionPrompts. AmountOfInteractables: {amountOfInteractables}");

		for (int i = 0; i < amountOfInteractables; i++)
		{
			LocalizedPromptAndStatus prompt = interactingObjects.WhatCanIDo(false, i);

			if (Helper.AreTheseLocalizedStringTheSame(prompt.Prompt, Helper.NothingToPromptPlaceholder()) is false)
			{
				interactingObjects.IndexToCheck = i;
				ContextualInputGlyphManager.Instance.ShowInteractionGlyph(prompt.Prompt, interactingObjects.GetObjectPosition(false, i), prompt.CanItBeUsed);
				return;
			}
		}
	}
	//...
}
1 Like

we use AllocatingGCMemoryConstraint at Unity to keep an eye on functions that should not allocate, you can add a test potentially and see if this function allocates in isolation

otherwise, yes I would move the call to some frequent method like OnUpdate, enable Call Stacks button in Profiler Window and see if there is a better callstack available for the GC.Alloc sample (in the Editor or Mono player it would be the most accurate) and use search in hierarchy view of the profiler window to locate method marker and see the callstack

1 Like

Thanks for the suggestion @alexeyzakharov! I was not aware of this test. I’m assuming you you replicate the code in your unit test script and run it in the Editor runner or do you need the Play version? Or is there a fancy version where you can call the existing function?

I have the Call Stacks button enabled, but I will also try then moving the scripts to an Update, maybe have it controlled with Input so I can see the triggering.

1 Like

yes, you can replicate the code as a Playmode test and run in the Test Runner window.

it probably will look like

using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools.Constraints;
using Is = UnityEngine.TestTools.Constraints.Is;

public class MyTest
{
    GameObject m_GO;
   
    public void CleanUp()
    {
           Object.DestroyImmediate(m_GO);
    }

    [Test]
    public void GenerateInteractionPrompts_DoesNotAllocate()
    {
        m_GO = new GameObject("Test");
        var component = m_GO.AddComponent<CurrentItemInteraction>();
        // Do other setup here
        component.GenerateInteractionPrompts(); // Prewarm to ensure jit allocations happen
        Assert.That(() => { component.GenerateInteractionPrompts(); }, Is.Not.AllocatingGCMemory());
    }
}

you can also add ProfilerMarker to easier find the test allocation in profiler if allocation happens

Thanks @alexeyzakharov! I had tried using Profile Marker before but I have some doubts about the functionality. Not sure if you can shed some light on these question:

  • How does Unity Profiler deal with Action Events triggers? Are they an issue and it cannot continue analyzing the hierarchy (stack)?
    
  • Is there an issue with using non-monobehaviours and does it stop analyzing?
    
  • If I add ProfilerMarkers to a method that calls 4 other methods, would it add markers to the other methods? or do you need to add ProfilerMarker to each method?
    
  • What happens if the ProfilerMarker.End is not reached because a return was triggered before hand?
    
  • What defines that you can actually see the last method that is causing the allocation (for example: the word new, resizing arrays when adding items to a list, etc etc)? How can I surface that?
    

I found one of the answers:

  • “What happens if the ProfilerMarker.End is not reached because a return was triggered before hand?”, you get an exception. Now I’m using the Auto() implementation as that would handle the exit more gracefully.

Actions are like normal function calls pretty much in terms of CPU profiling - the method which is called should be visible in deep profiler mode, but without deep profiler only if you add ProfilerMarker to the method body. Same wrt the callstacks of GC allocations - the callstack should normally include the method which triggers event (unless the caller itselfis optimized and inlined ofc)

no issues - similarly as above non-monobehavior calls a same method calls and there is no difference for callstack taking or deep profile level instrumentation if function is not optimized out

the latter - you would need to add profiler marker to each method as those are not recursive

  • Unity Profiler artificial callstack size limitation - currently I believe it is 64 methods
  • Whether or not a method is inlined - inlining can be suppressed for tests with [MethodImplAttribute(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] attribute
1 Like

To clarify this point a bit: that’s 64 call sites. Some of those might come without symbols and those are filtered out of the UI by default as listing just their address usually won’t help much. There is an option in the UI to not clear out lines without symbols, which then also displays the address of the call site at the beginning of each line.

1 Like