Memory profiler's "All of Memory" values

Hi,

I want to obtain the following values.

Untracked - All
Untracked - Private
Untracked - Graphics
Managed - Reserved
Native - Reserved

I know that the function to get direct numbers is not implemented, but since the Memory Profiler can display them, I don’t think it’s impossible to get them on the game side.
If it takes a long time to get it, I’ll try not to call it every frame.

We don’t think it makes sense to get an approximate value on the game side.
What we want is a function that can get the same values as this MemoryProfiler.

I’m sure there are countless Unity users out there who are suffering from these numbers.
How many people would be saved if this could be obtained…

Yes-ish… This is getting a bit wordy but you explicitly asked for complicated, so here goes:

You can use the ProfilerRecorder API to retrieve or derive some of these in a Player (also see the Profiler Counter Reference for the full details of the available counters):

Untrracked - All

App Committed Memory - Total Reserved Memory (Unless you are in a Release build in which graphics memory is not tracked for performance reasons, i.e. not reported as part of Total Reserved Memory)

And you’ll have to implement a native plug-in to query platform APIs for the Executables and Mapped number to subtract from that.

Untracked - Private
Untracked - Graphics

The Untracked sub-categories are platform reported and therefore depend on the platform. You could maybe build platform specific native Plugins that tap into platform specific native APIs to get some of these but based on that info, you have no chance of knowing which of these, and how much of these, are tracked by Unity vs not.

Managed - Reserved

GC Reserved Memory - GC Used Memory

[Edit: note that Managed VM memory is not tracked via runtime counters in Mono in versions pre 6 and pre 2022.3.54f1 (after that, it’s tracked thes same as IL2CPP by using our native allocators), and in IL2CPP uses our native allocators meaning it’s included in the Total Reserved Memory and Total User Memory and subtracted from there based on the allocation name in the Memory Profiler UI)

Weeeeelll, sort of. Boehm tracks memory usage via the free blocks and free lists. Any word sized field (i.e. on 64 that’d be any long, ulong, double, void* as well as regular reference fields) that looks like an address to an object it knows about, is seen as holding that object in memory (because it is a conservative GC).

The Memory Profiler has a heap dump with all the bytes the GC monitors, but not the free lists, so doesn’t know where the objects are. It mimics Boehm as close as possible and, like Boehm, starts crawling based on static field information and GCHandles, both of which are effectively strong roots for what they reference to. The Memory Profiler then crawls the fields of all the objects it finds, recursively until it has exhausted all references it can find.

In version 1.1.3, it currently only follows reference type fields, not pointers or pointer sized fields. I’m working on that, but since we don’t have the free lists, I can’t be sure if some random bytes on the heap are an actual object, or just random data that looks like an address to a type. I’ve already restricted it, as best as possible, to only consider:

  • types that can be instantiated
  • fit into the heap section at the address they are at
  • can reasonably be held by the field, based on the field type

So far, all of that is in 1.1.3. now if I add even just IntPtr as a type to treat it’s void* m_value field as reference, I start getting objects that overlap each other. Worse, which of these (for the sale of argument, let’s say I only find 2 overlapping objects) 2 objects I find first is, now that the crawler is jobified, timing dependent. (It needed to be jobified because we had snapshots with long managed struct arrays that took hours to open). I’ve not yet found a reasonable way to determine

  • which object is the valid one
  • remove it from the list of found objects, and the NativeHashMap simultaneously used in other jobs to crawl, without destroying crawler performance again. Also: removing any reference already recorded to this index, AND adjusting all other indexes, and references… And at that point I’d probably better mark the object as invalid and loose an index, as well as native array slot and NativeHashMap entries for its connections to the void
  • if I can’t, reduce the size of one or both so I don’t double count their memory usage

The likely solution will probably be to just note down the memory page they refer to, and if nothing else is found as alive in there, at least attribute that page as impossible to unload by the GC due to that apparent reference. That also means I likely have to give up on being 100% certain that I’m not just overlooking objects that, through their fields, are still holding on to other objects that are otherwise unreachable by the crawler.

I still have to triple and quadruple read the Boehm source code to be able to tell you just how much that might fuzzy the numbers of what the Memory Profiler package sees as Managed Objects vs Reserved Memory, but it’ll likely get a bit closer once that’s solved.

Native - Reserved

Leaving an easy one for last:
[Edit, previous but wrong:]

Total Reserved Memory - Total Used Memory -
GC Reserved Memory - Gfx Reserved Memory

[Edit, correct:]
Total Reserved Memory - Total Used Memory -
GC Reserved Memory + GC Used Memory

(Total Used Memory includes GC Uses Memory and Gfx Used Memory. Gfx Used Memory==Gfx Reserved Memory currently. Once again with the caveat that Gfx memory is not tracked in Release builds)

All in all, the key issue with all of this is: what’s reasonable or feasible to do at runtime vs post mortem in a snapshot are two entirely different things.

I didn’t know ProfilerRecorder existed! Very informative!

However, I was unable to match the MemoryProfiler values for Managed Reserved, Untracked, and Native Reserved. Only 91.1 MB of graphics appear to match.

Attached are the memory values as they appear on the Android screen and the memory profiler screen.

C# Code

void OnEnable()
{
	systemUsedMemoryRecorder    = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "System Used Memory");
	totalReservedMemoryRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "Total Reserved Memory");
	gcReservedMemoryRecorder    = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC Reserved Memory");
	gcUsedMemoryRecorder        = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC Used Memory");
	totalUsedMemoryRecorder     = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "Total Used Memory");
	gfxReservedMemoryRecorder   = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "Gfx Reserved Memory");
	assetCountMemoryRecorder	= ProfilerRecorder.StartNew(ProfilerCategory.Memory, "Asset Count");
	gameObjectMemoryRecorder	= ProfilerRecorder.StartNew(ProfilerCategory.Memory, "Game Object Count");
	sceneObjectMemoryRecorder	= ProfilerRecorder.StartNew(ProfilerCategory.Memory, "Scene Object Count");
}

	
// Managed Reserved
m_TextMonoReserved.SetText(
	string.Format("{0} = GC Reserved Memory({1}) - GC Used Memory({2})",
		toMB( gcReservedMemoryRecorder.LastValue - gcUsedMemoryRecorder.LastValue ),
		toMB( gcReservedMemoryRecorder.LastValue ),
		toMB( gcUsedMemoryRecorder.LastValue ) )
);
// Untracked
m_TextUntracked.SetText(
	string.Format("{0} = Total Committed Memory({1}) - Total Reserved Memory({2})",
		toMB( systemUsedMemoryRecorder.LastValue - totalReservedMemoryRecorder.LastValue ),
		toMB( systemUsedMemoryRecorder.LastValue ),
		toMB( totalReservedMemoryRecorder.LastValue)
	)
);
// Native Reserved
m_TextNativeReserved.SetText(
	string.Format("{0} = Total Reserved({1}) - Total Used({2}) - GC Reserved({3}) - Gfx Reserved({4})",
		toMB( totalReservedMemoryRecorder.LastValue - totalUsedMemoryRecorder.LastValue - gcReservedMemoryRecorder.LastValue - gfxReservedMemoryRecorder.LastValue ),
		toMB( totalReservedMemoryRecorder.LastValue ),
		toMB( totalUsedMemoryRecorder.LastValue ),
		toMB( gcReservedMemoryRecorder.LastValue ),
		toMB( gfxReservedMemoryRecorder.LastValue )
	)
);
// count
m_TextAssetCount.SetText( $"{assetCountMemoryRecorder.LastValue}");
m_TextGameObjectCount.SetText( $"{gameObjectMemoryRecorder.LastValue}");
m_TextSceneObjectCount.SetText( $"{sceneObjectMemoryRecorder.LastValue}");

Unity6
DevelopmentBuild
Target:Androi Build

I am trying in the above environment, is it difficult at this stage to get the same values as in Memory Profiler? Or am I doing something wrong?

Oh, right, sorry. I forgot about the Managed VM memory. That’s tracked as native for IL2CPP and has no counter for mono, so it’s also not included in the “Total X” counters.

And I goofed my math: the Total Memory Used already contains Gfx Reserved (or Gfx Used which is the same value ATM) as well as the GC Used, so you only need to do:

Total Reserved Memory - Total Used Memory - GC Reserved Memory + GC User Memory

And given how the GC Used number is off by about 17 MB (due to either/or: managed objects the crawler didn’t find, partially used and therefore un-unloadable managed heap pages), I think that then roughly matches for the Native Reserved?

And another correction: Mono VM memory is also tracked as part of the Native Memory runtime counters for Unity 6, and as of 2022.3.54f1, in 2022 as well. Totally forgot I just backported that last month :sweat_smile:

I corrected my first answer accordingly.

As for why your Untracked value is off:

is not what I wrote:

System Used Memory == App Resident Memory, not App Committed Memory

1 Like

The closest counter to calculate the “Untracked” would be “App Committed Memory” which should give the memory requested from the system. However on linux platforms we use VmRSS from /proc/self/statm as the fastest approximation for the runtime api. While Memory profiler itself uses very slow parsing of the /proc/self/smaps file which gives very accurate picture of all allocations. But it is not feasible for the runtime api due to high costs (20-50ms). On Android there is equivalent getProcessMemoryInfo api which iirc uses the same smaps approach and gives full information (although it is even slower and uses internal caching). You can try that api.

We could expose slow memory stats api potentially, but didn’t want to do that so far to avoid affecting performance heavily. If you feel like this is not a problem for you, please let us know :smiley:

1 Like

Native Reserve(in game display): 106.26 = Total Reserved(454.70) - Total Used(345.73) - GC Reserved(58.82) + GC Used(56.12)
Native Reserved(in Memory Profiler): 104.9 MB

The numbers almost the same.

1 Like

How is App Committed Memory obtained? Only ‘Total Committed Memory’ was listed document.

Thanks, I will try to use getProcessMemoryInfo as well. However, I believe it is easier to investigate memory leaks if the value is closer to the memory profiler.

I would like to repeat the transition from PointA to PointB many times, and capture the memory profiler at the two points, A and B, to see the difference and investigate, We would like to output the memory values at the timing of this transition to the debug log. If there is a large difference in the memory values, we can detect signs of a memory leak (or memory expansion). I think this method of investigation is the most effective because the memory profiler has evolved wonderfully since Unity 2022.

Therefore, runtime cost and values close to the actual device are not important.
We believe that it is important to be close to the value displayed in the memory profiler.

1 Like

It’s documented here. And that number comes directly from the OS.

・App Committed Memory - Total Reserved Memory
but it seemed to be a different value from Untracked All.

・GC Reserved Memory - GC Used Memory
This also seems to be different from Memory Profiler’s Managed Reserverd.

GC Reserved Memory == Profiler.GetMonoHeapSizeLong()
GC Used Memory == Profiler.GetMonoUsedSizeLong()
it is not obvious that ‘Managed Reserved’ of Memory Profiler has increased in this value,
This is a factor that makes it difficult to investigate problems caused by ManagedReserved.

Yeah, as I said, it’s not gonna be an exact match.