Search Unity

In the previous Unity WebGL blog post, we explained that memory works differently compared to other platforms; we told you the Unity Heap should be as small as possible and we also emphasized that there are other allocations in browser memory.

This time we would like to dive into what’s inside the Unity Heap so that when it comes to reducing its size, you can do that based on actual data, instead of having to find out a small value that works with your content by trial and error.

So, let’s see what’s the Unity Heap, what’s inside it, and how to profile it.

What is the Unity Heap?

First of all, just a reminder that Unity Heap is not to be confused with the Browser Heap, in fact it’s a block of memory inside the Browser Heap. You will find more information in the previous blog post.

In general terms, the Heap is an area of memory used for dynamic allocations in which native applications allocate via malloc/free or new/delete.

In Unity, we use our own Memory Allocators for better memory utilization as well as profiling and debugging purposes, but at the low-level, we still use malloc/free.

In Unity WebGL, we refer to this memory, which contains all run-time Unity engine objects, as Unity Heap. Allocations in the Unity Heap are done using dlmalloc.

On console platforms, the size of the heap is defined by the hardware specs and by how much memory is reserved by the OS, so applications must ensure they will not exceed the maximum amount of memory available at run-time.

Similarly on WebGL, we must define the size of the Unity Heap in advance (at build-time). What that means is that the Unity Heap can never shrink or grow after initialization.

What is in the Unity Heap?

On Unity WebGL, we can categorize allocations in the Unity Heap as follows:

  • Static memory area
  • Stack
  • Dynamic Memory
  • Unallocated memory

image00

The first blocks to be allocated are the stack and the area for all static objects. The size of the stack is usually 5mb and the size of the static area depends on the code compiled, which basically means the version of Unity and the project.

Once those are allocated, all remaining memory is available for Dynamic allocations that will occur at run-time.

As code is executed, the Dynamic area will start to occupy more space in the Unity Heap and if it grows too much, it will cause the Unity content to run out of memory.

image03

Over time, even though some objects will be freed and others allocated, the size of the Dynamic area will not shrink because there is no compacting mechanism. Instead, this will create gaps of free memory within the Dynamic area.

image05

So, just be aware that there can be fragmentation.

By this time, you might be wondering, what about Managed Memory?

Well…one (or more) of those run-time allocations in the Dynamic area is the Managed Heap where all your scripting objects will be created. So, the Managed Heap is inside the Unity Heap which is inside the Browser JS VM Heap. It does sound a bit complicated so think about it as the movie Inception, or The Matrix. A Heap inside another Heap, and so on…

image06

Managed Memory

This is the memory used to store all scripting objects. It’s called managed because when an object is no longer referenced, its memory will be automatically reclaimed by the garbage collector (Boehm).

The first important thing to understand here is that this memory is allocated from the Unity Heap (or from the OS on other platforms). Second, this memory is never returned to the system, therefore the Managed Heap can only grow. In fact, when an object is garbage collected, its memory is kept inside the Managed heap for future use.

In Unity WebGL terms, when we say that the memory is not returned to the system we actually mean it is not returned to the pool of available memory blocks in the Unity Heap.

It’s also important to mention that unlike the Unity Heap, which is a single block of memory, Boehm GC can allocate multiple buffers. In addition, each of them can be subdivided into smaller blocks if necessary. However, when a scripting object is created, a contiguous amount of memory large enough for the object is required. If that is not available among the free blocks managed by Boehm GC, a new one block will be taken from the Unity Heap.

For more information about managed memory, read the manual.

What happens when you run out of Managed Memory?

If Boehm GC fails to find a free block of memory for a new object and then fails to allocate from the Unity Heap, the Unity WebGL content will stop execution with an out-of-memory error suggesting to increase WebGLMemorySize.

Does System.GC.Collect() not work on WebGL?

Calling GC.Collect() has no effect on Unity WebGL. This is because we can’t garbage collect when the call stack is not empty. More details about this limitation can be found in the manual.

At the moment, Unity WebGL will try to perform a little bit of garbage collection at the beginning of every frame. Then when a new scene is loaded, a full garbage collection is performed.

What does System.GC.GetTotalMemory() do ?

On Unity WebGL, this method works identically as on other platforms, with the exception of the optional garbage collection: System.GC.GetTotalMemory() returns the total managed memory currently used, like our Profiler.GetMonoUsedSize(). To know the total size of the managed heap (used+free), use Profiler.GetMonoHeapSize() instead.

How can I reserve a certain amount of memory for the Managed Heap ?

If you ever used c++ std containers like string, vector, etc you should know that they might resize when appending/inserting new elements to the container. In games and other applications that need to keep memory usage under control this can be a problem, which can be mitigated by reserving memory upfront using the reserve method (for example: std::string::reserve, std::vector::reserve)

Unlike c++ std containers, in Unity there is no API to reserve memory upfront for the Managed Heap. Having said that, there might be a way around it.

Assuming you know the maximum size of Managed Heap that your content will ever need, you could create an unused array of that size early on at run-time, then get the garbage collector to run. That way you would implicitly reserve that memory for the Managed Heap so that it will never need to grow!

That sounds like a great idea! …except that, as we mentioned earlier, calling GC.Collect() does not do anything, and the only time a full GC pass is performed is upon scene-load. Well… that’s easily solved by starting your content with a preload scene with a single game object and one script to allocate the array:       

then load the first scene of your project. Now, the only thing you are missing is the size of Managed Heap you need to pre-allocate. To know that you could run your content from start to finish, then use Profiler.GetMonoHeapSize() to get the total reserved memory.

Just keep in mind that, with this approach, you will always pay the cost for the maximum usage of managed memory.

Choosing the size of the Unity Heap

After explaining what the Unity Heap is and making a necessary detour to scripting memory land, let’s go back to the original problem: what’s the best strategy for choosing the size of the Unity Heap?

The basic idea is that you need to determine the maximum amount of memory that your content will ever use, so that WebGLMemorySize can be set to a slightly larger value (rounded to the next multiple of 16).

In practice, what you have to do is test your content extensively on WebGL, while keeping track of the high-watermark total reserved memory. Then take that final high watermark value, add a few extra megs just to be safe and round it to 16mb.

Thankfully, the Profiler API provides a function to get the Total Reserved Memory:

Profiler.GetTotalReservedMemory(), which corresponds to the ReservedTotal value in the Unity Profiler Window:
image07

However, there are two problems with this approach. The first one regards memory spikes: temporary allocations that are deallocated within the same frame, will not be taken into account in ReservedTotal. The second problem is that the Unity Profiler does not track all allocations.

Untracked Allocations

The information you get from the profiler is very useful, however, there is something important you need to take into consideration: the profiler will tell you everything it knows but it won’t tell you what it does not! Obvious right?

Unity keeps track of current memory used because memory is allocated via our internal MemoryManager::Allocate(), which will store additional information about name and size of the new memory allocation. However, there are other allocations that for some reason we don’t track and this is obviously a problem if you need to know exactly how much memory is actually used inside the Unity Heap.

This is usually due to internal sub-systems or 3rd party libraries allocating/freeing memory using malloc/free instead of our internal MemoryManager. In other cases, this is caused by memory allocations in user’s plugins, via malloc() in c/c++, or _malloc() in JavaScript  (e.g. StringReturnValueFunction in the example JS plugin), or by writing to file (which will actually write to memory).

To give you an idea about how much untracked memory you need to take into account, for a simple project in Unity 5.5 this is ~7mb. The good news is that we are working on fixing this problem wherever possible, so you can expect the total amount of Untracked memory to decrease in the future.

Is there really no way to know EXACTLY how much memory is used?

Actually, there is a way. If we look again a the first image of the Heap, it’s pretty straightforward to realize that Total Reserved Memory=static+stack+dynamic area sizes.

image02

Luckily, we can get the size of those areas of memory at run-time from the emscripten-generated variables and constants. Here is a simple plugin for that:

Just drop it into a new jslib file in your project and create the corresponding c-sharp bindings:

 

An additional benefit of this method is that, unlike the profiler API, it will work in release builds too.

Just be aware that the above jslib code relies on the emscripten-generated JS code, so for future versions of Unity, the plugin might need to be updated. Having said that, we might look at adding a Unity WebGL-specific API that would make such code unnecessary.

How can I profile the Unity Heap?

First of all by using the Unity Memory Profiler, which will provide you both an overview of memory and a more detailed view of all allocation categories.

If you are specifically looking for memory leaks you can use the CPU Profiler GC Alloc column, which shows how much memory has been allocated in a particular frame:

image04

By the way, if you experienced problems in using the Profiler, there was a bug in 5.3, that is now fixed in 5.3.6 Patch 8.

If you want to dig further, you could try the new (Unity 5.3) Memory Profiler:

image01

Which can be found at this repo: https://bitbucket.org/Unity-Technologies/memoryprofiler

This is a great tool but comes with caveats: it is il2cpp-specific (not a problem because that’s what we use on Unity WebGL) and it is still an unsupported preview feature so there might be issues here and there.

Conclusions

Does this blog post answer all your remaining memory questions? This is a very broad and important topic that we could write much more about, but the really vital thing is that now you have the tools that allow you to know how much memory your Unity WebGL content requires. If you are interested in knowing more about profiling and optimizations there are a lot of available resources, including the Performance Optimization guides which we just published as well as the Unite Europe 2016 talk Optimizing Mobile Applications.

Please let us know if anything is not clear and remember that you can always reach us in the forums.

 

10 评论

订阅评论

评论被关闭。

  1. Hi ~
    Thanks for this useful article. It’s good to know how memory work on webgl ,cus memory management is quite import in this platform.
    I have a question about the GC. You mention about the full GC will only happen at the scene change.
    Does it need to unload all the scene to make it happen ? If our game have one scene always there, and
    we only add extra scene and remove this extra scene when we don’t need it. Does this extra scene will be
    GC fully when unloaded ? Or we need to unload all the scene to get the full GC work ?

    1. That is a great question. A full GC pass will be executed anytime you load a scene, even if you have persistent game objects.

  2. Was serialization suddenly added to WebGL? I was testing a game, and it surprised me that save data seemed to be working in a browser, which I thought was never possible? I thought web browsers were limited to PlayerPrefs only?

    1. It depends on how data is saved. On WebGL, it’s not possible to access the file system, however, you can save data to the browser persistent storage (via IndexedDB) by saving to the persistentDataPath. If you have more questions on this topic, please start a thread on the forum.

  3. I wish no (fps)spikes in Unity in an empty scene.

  4. Luiz Felipe Beneton

    十二月 7, 2016 7:10 下午

    Can you elaborate a bit more about reserving memory for the Managed Heap?
    I fail to see the point of the technique. :(

    I assume I’m already reserving memory space when unity initialized in WebGL. That’s why I need to set a number for the Heap size (multiple of 16, never forget).

    So why should I allocate it all in the beginning and than free it up with a GC?

    Thanks!

    1. So you end up with little managed heap fragmentation?

      1. Yes, you are reserving memory space on startup but you don’t have any control on the Managed Heap size.

        I think that “reserving” Managed Heap memory with the technique described here can reduce the risk of Managed Heap growth when actually there is enough free Managed Memory for the new object being allocated via scripting (but there is not enough contiguous amount of free managed memory). So, yes, it is a fragmentation issue.

        Having said that, this might not be an issue in most projects, where most scripting allocations are fairly small.

  5. Very useful thanks!

    I specially liked how much meta it was in some cases when you were talking about the managed heap freeing memory which is not freed for the rest of unity heap. It is meta enough even if you don’t think about the js vm heap and the OS heap of the browser :) Like inception indeed :)

  6. very useful, thanks for writing these.