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.