Unity WebGL Memory Optimization: Part Deux

This article was originally posted on Kongregate’s Developer Blog.

Now that the Unity WebPlayer is mostly a thing of the past, making sure your Unity WebGL content runs smoothly is more important than ever. The main pain point that we hear from developers relates to the dreaded “out of memory” errors that end users encounter when trying to play WebGL games. This is a particularly frustrating issue for players using 32-bit versions of browsers, since they are much less likely to have a large contiguous block of free memory for the Unity heap.

This article will cover some more tips and tricks for diagnosing and resolving memory-related issues with your Unity WebGL game.

Monitoring Memory Usage

When profiling and optimizing your Unity WebGL game, it is important to keep track of multiple kinds of memory usage. The first is the Unity heap, which defaults to 256MB and can be changed in the Publishing Settings interface under “WebGL Memory Size.” We touched on some optimization techniques for this chunk of memory (and why you want to keep it as small as possible) in a previous blog post. To reiterate, the less memory you need to reserve for the Unity heap, the more memory the browser will have available for other things such as audio, IndexedDB databases, etc.

Since reducing the size of your Unity heap is so important, we have added a very simple memory stats class (based on the Unity team’s excellent blog) to our WebGL utilities package in order to help developers track this information and report it to the browser console.

Simply import the package into your Unity project, add the “WebGL Memory Stats” script to a GameObject in your scene, and log entries about free/total memory will be made in your browser console at an interval of your choosing:

Is is important to note that the data Unity/Emscripten provides here appears to be a high watermark for memory usage. That is, when objects are cleaned up, the used/free memory does not change. However, it is still crucial as a tool for measuring and tuning how much memory to allocate for your Unity heap. You should use this tool to keep track of the highest amount of memory used, and then add a safety net on top of that while rounding up to the next highest 16MB.

In this example, you can see that our application is requesting a whopping 256MB (the default) for the heap, when we really should be requesting about 32MB:

Asset Bundles & IDBFS: The Silent Killers

Another source of memory-related problems is the IndexedDB filesystem used by Unity. Any time you cache an asset bundle or use any filesystem-related methods, they are stored in a virtual filesystem backed by IndexedDB.

What you might not realize is that this virtual filesystem is loaded into and persisted in memory as soon as your Unity application starts. This means that if you are using the default Unity caching mechanism for Asset Bundles, you are adding the size of all of those bundles to the memory requirements for your game, even if they are not being loaded.

You can track this memory usage in Chrome by using the “Application” tab of the Developer Tools. As you can see here, there are several bundles stored in the cache, and the selected one is nearly 20MB:

In order to see the effect this has on total memory consumption, we can use Chrome (or Firefox) to take heap snapshots at various points during the application lifecycle. For this demonstration, we take the first snapshot when the application loads for the very first time and has nothing cached, then again after loading some large asset bundles, and once again after reloading the page.

Here is an example of loading a WebGL project that utilizes WWW.LoadFromCacheOrDownload.

You can see that after the initial load, we are using roughly 230MB of memory. After loading up all the asset bundles we are at 300MB, and then after reloading the page (but not the asset bundles) we are still at close to 300MB. This is not good.

Luckily, the fine folks on the Unity team have us covered with an addon called CachedXMLHttpRequest. When combined with a non-caching UnityWebRequest call, CachedXMLHttpRequest uses a separate IndexedDB cache for storing downloaded files that does not remain persistent in memory, resulting in decreased memory usage. Let’s take a look at our example when using CachedXMLHttpRequest:

Memory usage starts out at around 236MB, then goes up to 237MB once the asset bundles are loaded and cached, and then back down to 226MB after a page reload. This is fantastic, as we have eliminated the permanent memory bloat caused by the IndexedDB virtual filesystem.

It is important to note that when using CachedXMLHttpRequest your WebGL heap usage can increase due to the bundles being handled differently, so you should be careful to not load too many bundles simultaneously (which is a best practice anyway), and always make sure to unload them when you are done. Optimizing asset bundles is a bit out of scope for this article, but in general you want to make them as granular as possible so that you don’t keep unused assets in memory and avoid memory spikes that can be caused by loading large asset bundles.

Kongregate WebGL Utilities Package

The original version of CachedXMLHttpRequest unfortunately has a few bugs:

  • An error dialog is displayed in Firefox private browsing mode
  • When used with Safari in an iframe the plugin is non-functional
  • Synchronous XHR requests are used to revalidate resources

We have released an updated version as part of our WebGL Utilities Package that is a drop-in replacement, resolves the aforementioned issues, and adds the following functionality:

  • Supports asynchronously querying the cache from Unity to determine if an item exists
  • Adds the ability to configure blacklists for items that should not be cached and items that should not be re-validated
  • Allows clearing of the cache from Unity

We will attempt to reach out to the Unity WebGL team to get these features merged into the official plugin, as we have found them necessary when converting several projects to CachedXMLHttpRequest.

Converting to CachedXMLHttpRequest

If you are planning to convert your project to use CachedXMLHttpRequest, you will likely want to use methods on the Caching class to clean up your previously cached asset bundles or to set the maximum used disk space for IndexedDB to a low amount to make sure your old assets are not taking up any space on disk or in memory.

If you are going to delete the cache, you need to do it before you access any asset bundles or other files, ideally in the Awake method of a GameObject that exists in the initial scene. We have found that with some versions of Unity the CleanCache method may be unreliable. In that case, you can use the following code to clear the cache using IndexedDB directly, though it is important to note that this will clear the cache for an entire domain. This should be inserted into your WebGL index.html or template before the inclusion of the Unity Loader.

(function clearCache() {
 var idb = window.indexedDB || window.mozIndexedDB ||
 window.webkitIndexedDB || window.msIndexedDB;
 if (!idb) return;

var open;
 try { open = idb.open(‘/idbfs’) } catch(e) { return; }

var errorHandler = function(e){ e.preventDefault(); e.stopImmediatePropagation(); };
 open.onabort = open.onerror = errorHandler;
 open.onupgradeneeded = function(upgradeEvent) {
 upgradeEvent.target.transaction.abort();
 };

open.onsuccess = function(openEvent) {
 var db = openEvent.target.result;
 db.onerror = db.onabort = errorHandler;

try {
 var store = db.transaction(‘FILE_DATA’, ‘readwrite’).objectStore(‘FILE_DATA’);
 store.openCursor().onsuccess = function(cursorEvent) {
 var cursor = cursorEvent.target.result;
 if (cursor) {
 if (cursor.key.indexOf(‘/UnityCache/Shared/’) !== -1) cursor.delete();
 cursor.continue();
 }
 };
 } catch(e) {}

db.close();
 };
})();

There is a minor bug in the current version CachedXMLHttpRequest that will cause it to generate an error dialog in Firefox private browsing mode. This can be resolved by running the following JavaScript code before loading the UnityLoader.js script, and is not needed if you are using the Kongregate version of the addon:

window.addEventListener(‘error’, function(e){
 if (e.message.indexOf(‘InvalidStateError’) !== -1) {
 e.stopImmediatePropagation();
 }
}, false);

You may also need to modify your CORS configuration expose the ETag header, as well as allow HEAD HTTP requests for any assets you load via CachedXMLHttpRequest.

One more thing to note when using CachedXMLHttpRequest is that all GET requests made with WWW and UnityWebRequest will be cached by default. This can be problematic if you are generating requests with a cache buster or timestamp on the query string. The Kongregate version of the class allows you to configure a blacklist to prevent caching of assets as needed.

WebAudio Memory Usage

Another important thing to note is that audio played by your game is stored in memory uncompressed. Depending on your usage situation this can cause large spikes in memory usage.

Once again, the Unity team has an asset in the store to help with this problem as well. Unfortunately it appears the current solution has problems with gaps in looping audio, so it may not be too helpful for music, which is likely to be the main source of large audio files for a game.

References