Android Chromium - Unable To Grow Allocated Memory Above 256MB [Confirmed]

Hey everyone,

Our team put together a WebGL memory stress test. You can enter the number of textures to load and press the button to load them. It displays the available memory information we have access to for the platform.
https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html

Here are my findings…

Windows 10 PC

  • Chrome - 1.96GB
  • Edge - 1.95GB
  • Firefox - 1.96GB

Android [Galaxy S20+]

  • Chrome Beta - 219MB
  • Chrome - 219MB
  • Ecosia - 219MB
  • Firefox - 1.96GB
  • Samsung Internet - 219MB
  • Opera - 219MB
  • Edge - 1.93GB

Android [ Galaxy S9]

  • Chrome - 219mb

iOS [iPhone SE 2020]

  • Safari - 426mb
  • Chrome - 426mb
  • Edge - 426mb
  • XRViewer – 426mb

iOS [iPhone 11 Pro Max]

  • Safari - 442mb

iOS [iPhone 8]

  • Safari - 210mb

iOS [iPhone 6]

  • Safari - Fails to open (out of memory)

You can see in my findings that on mobile Android Chrome-based web browsers are unable to increase the size of the memory used by WebGL.

For the iPhone SE 2020, that device only has 2GB of RAM to start with, so it’s no surprise we can’t get to the 2GB limit. Weird that we’re only able to get to around 426mb, I wonder how that was determined (maybe we can’t go larger than 480mb of allocated memory in the heap?).

The iPhone 6 only has 1GB of memory to start with. I’m going to try making a build with 128mb initial memory and see if WebGL runs on that device. [Update: nope!]

This build was made with brotli compression, high code stripping, and WebGL 1.0 via Unity 2020.1.4f1.

I’ve read that by default WebGL will increase the size of the heap in 32mb blocks, with 256mb allocated at the start.

Anyone got any ideas on things I should try? Can anyone else share the results of the test from their devices?

4 Likes

So I made a new build with 128mb memory to start, and used Chrome dev tools to view the console to see what kind of error feedback I could get on Android Chrome. But the actual error message isn’t any different then what I would expect [Could not allocate memory].

Weirdly, the crash still happens when growing the memory above 256mb, it gets past 128mb perfectly fine.

For my next test, I’m going to try to make a build that starts at 512mb allocated, and see what happens on Android Chrome in that scenario.

Well, I made a build that defaults to 512mb, but it still crashes when getting to the same 256mb limit.

This tells me that the PlayerSettings.WebGL.memorySize doesn’t do what I thought it does. And yep, looking it up this value has been deprecated and doesn’t do anything on Web Assembly builds. Doh!

I’m going to continue researching what else can be done about this, having a hard limit of 256mb on mobile Chrome is pretty bad!

So I made some new builds using PlayerSettings.WebGL.emscriptenArgs to set the initial memory.

128MB Initial
512MB Initial

So on Android Chrome, the 128MB build works fine until the memory reaches 256MB, and then gives the [Could Not Allocate] error in the console.

On the 512MB build, I’m able to add images until I get near 512MB, and then the app crashes with the [Could Not Allocate] error in the console.

So, this does show an actual bug! At least I know I’m not crazy. Next step is to figure out the best developers to report this to from the Unity team and file a bug report.

Submitted a bug report to the Unity tracker.
#1296850_jo7dcmobll7c762m

That might also be emscripten or SDL2 emscripten bug
https://github.com/emscripten-core/emscripten
https://github.com/emscripten-ports/SDL2

(And you might want to edit your bug report message to include only the bug number, and not your private link)

1 Like

So for anyone wondering how to work around this issue, what our team is planning on doing is making multiple WebGL builds with different initial heap values { 64, 128, 256, 512, 1024, 2048 }, and then loading only that version and not allowing our application to go above the initial value (by using assets with different levels of detail and memory required).

Then when a memory crash occurs, we will log how much memory we initialized with, and then the next time that device loads our app, we’ll choose the next lowest initial memory version. So at least there’s a limited amount of crashes possible before we automatically start knowing how much memory can be requested.

We’ll probably have to use a device detection API to figure out what device is actually attempting to load our app since there isn’t an easy way to know this out of the box.

Yes, this is a major bug and annoying. But at least with a lot of elbow grease, we can work around it!

I’ve added the test project to a public Github for the Unity QA team and received word from them that they’ve at least seen the issue, no word on the timing for when this will be looked into yet, but I’m hoping sooner rather than later!

1 Like

This bug is now officially listed in the Unity Issue Tracker. Make sure to vote for it to be worked on!

This bug landed on my work desk, and it is unfortunately an issue with Chrome browser.

The root problem is that the Chrome browser is still 32-bit on Android, and it causes memory address space fragmentation issues that limit the creation of β€œlarge” WebAssembly memorys. 256MB in this context does not sound particularly large, but that is the extent that address space is fragmented on Chrome. See Reddit - Dive into anything

Several years ago I worked with King on WebAssembly Candy Crush for mobile Android browsers, where the issue was the same - iirc 120MB memory sizes were still 100% reliable to allocate, but after that, there was a smooth degradation, and iirc by 384 MB heap size, allocation reliability had dropped by several dozens of percents.

This is an issue that we are tracking with Chrome to resolve. Based on How to check whether your Chrome on Android is 32-bit or 64-bit - gHacks Tech News it looks like they are doing small % trials already, so there is hope.

3 Likes

@jukka_j That’s super-valuable to know. I’m going to have our dev team check if the browser is 32-bit, and then if it is we’ll load a WebGL build that is initialized to 256mb, and only use assets that fit within that limitation with a buffer to prevent memory from growing. Hopefully, Google gets around to transitioning chromium-based browsers to 64-bit this year!

I have created a test application that can be used to gauge the maximum WebAssembly application memory size. You can find it here: http://clb.confined.space/dump/mem_growth.html

When testing, tap the button on the page until memory heap size no longer increases.

I would be curious to know what kind of results people get on different mobile devices, especially whether the results differ if one has a few tabs open, or when there is only one tab open; and whether mobile browsers crash with Chrome’s β€œAww, snap.” dialog or similar.

1 Like

@DerrickBarra one thing to test I wonder is whether enabling or disabling Data Caching affects the max size that can be acquired from an Unity project? Also make sure that Decompression Fallback is disabled.

Raised 221530 – Safari browser fails to set boundaries to Wasm application memory against Safari and https://bugs.chromium.org/p/chromium/issues/detail?id=1175564 against Chrome.

@DerrickBarra Profiling in Firefox, initial memory usage (without any textures created using the UI) is:

UI says β€œIn-Use Memory: 16.85 MB”

264.50 MB (100.0%) -- explicit
β”œβ”€β”€244.76 MB (92.54%) -- window-objects/top(https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html, id=34)
β”‚  β”œβ”€β”€243.41 MB (92.02%) -- active/window(https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html)
β”‚  β”‚  β”œβ”€β”€243.12 MB (91.91%) -- js-realm(https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html)
β”‚  β”‚  β”‚  β”œβ”€β”€242.66 MB (91.74%) -- classes
β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€169.13 MB (63.94%) -- class(WebAssembly.Instance)/objects
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€β”€99.38 MB (37.57%) ── non-heap/code/wasm
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€β”€69.76 MB (26.37%) ── malloc-heap/misc
β”‚  β”‚  β”‚  β”‚  β”‚  └────0.00 MB (00.00%) ── gc-heap
β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€β”€72.49 MB (27.41%) -- class(ArrayBuffer)/objects
β”‚  β”‚  β”‚  β”‚  β”‚   β”œβ”€β”€64.00 MB (24.20%) ── non-heap/elements/wasm
β”‚  β”‚  β”‚  β”‚  β”‚   β”œβ”€β”€β”€8.49 MB (03.21%) ── malloc-heap/elements/normal
β”‚  β”‚  β”‚  β”‚  β”‚   └───0.00 MB (00.00%) ── gc-heap
β”‚  β”‚  β”‚  β”‚  └────1.03 MB (00.39%) ++ (6 tiny)
β”‚  β”‚  β”‚  └────0.46 MB (00.17%) ++ (7 tiny)
β”‚  β”‚  └────0.29 MB (00.11%) ++ (3 tiny)
β”‚  └────1.35 MB (00.51%) ++ js-zone(0x2ae7588a000)
β”œβ”€β”€β”€β”€9.14 MB (03.45%) ++ js-non-window
β”œβ”€β”€β”€β”€5.00 MB (01.89%) ++ heap-overhead
β”œβ”€β”€β”€β”€2.91 MB (01.10%) ++ (24 tiny)
└────2.70 MB (01.02%) ++ threads

Then, after creating 50 textures, UI says

β€œIn-Use Memory: 227.61 MB” with

712.54 MB (100.0%) -- explicit
β”œβ”€β”€692.77 MB (97.23%) -- window-objects/top(https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html, id=34)
β”‚  β”œβ”€β”€691.41 MB (97.04%) -- active/window(https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html)
β”‚  β”‚  β”œβ”€β”€691.14 MB (97.00%) -- js-realm(https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html)
β”‚  β”‚  β”‚  β”œβ”€β”€690.67 MB (96.93%) -- classes
β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€520.49 MB (73.05%) -- class(ArrayBuffer)/objects
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€512.00 MB (71.86%) ── non-heap/elements/wasm
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€β”€β”€8.49 MB (01.19%) ── malloc-heap/elements/normal
β”‚  β”‚  β”‚  β”‚  β”‚  └────0.00 MB (00.00%) ── gc-heap
β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€169.13 MB (23.74%) -- class(WebAssembly.Instance)/objects
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€β”€99.38 MB (13.95%) ── non-heap/code/wasm
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€β”€69.76 MB (09.79%) ── malloc-heap/misc
β”‚  β”‚  β”‚  β”‚  β”‚  └────0.00 MB (00.00%) ── gc-heap
β”‚  β”‚  β”‚  β”‚  └────1.04 MB (00.15%) ++ (6 tiny)
β”‚  β”‚  β”‚  └────0.48 MB (00.07%) ++ (7 tiny)
β”‚  β”‚  └────0.27 MB (00.04%) ++ (3 tiny)
β”‚  └────1.35 MB (00.19%) ++ js-zone(0x2ae7588a000)
β”œβ”€β”€β”€10.62 MB (01.49%) ++ (26 tiny)
└────9.15 MB (01.28%) ++ js-non-window

and after creating another 50 textures for a 100 total, UI says

β€œIn-Use Memory: 438.31 MB” with

712.12 MB (100.0%) -- explicit
β”œβ”€β”€692.69 MB (97.27%) -- window-objects/top(https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html, id=34)
β”‚  β”œβ”€β”€691.36 MB (97.08%) -- active/window(https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html)
β”‚  β”‚  β”œβ”€β”€691.08 MB (97.04%) -- js-realm(https://brandxr-discovery.s3.amazonaws.com/WebGL/MemoryStressTestWebGL/index.html)
β”‚  β”‚  β”‚  β”œβ”€β”€690.67 MB (96.99%) -- classes
β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€520.49 MB (73.09%) -- class(ArrayBuffer)/objects
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€512.00 MB (71.90%) ── non-heap/elements/wasm
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€β”€β”€8.49 MB (01.19%) ── malloc-heap/elements/normal
β”‚  β”‚  β”‚  β”‚  β”‚  └────0.00 MB (00.00%) ── gc-heap
β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€169.13 MB (23.75%) -- class(WebAssembly.Instance)/objects
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€β”€99.38 MB (13.95%) ── non-heap/code/wasm
β”‚  β”‚  β”‚  β”‚  β”‚  β”œβ”€β”€β”€69.76 MB (09.80%) ── malloc-heap/misc
β”‚  β”‚  β”‚  β”‚  β”‚  └────0.00 MB (00.00%) ── gc-heap
β”‚  β”‚  β”‚  β”‚  └────1.05 MB (00.15%) ++ (6 tiny)
β”‚  β”‚  β”‚  └────0.40 MB (00.06%) ++ (5 tiny)
β”‚  β”‚  └────0.28 MB (00.04%) ++ (3 tiny)
β”‚  └────1.33 MB (00.19%) ++ js-zone(0x2ae7588a000)
β”œβ”€β”€β”€10.78 MB (01.51%) ++ (26 tiny)
└────8.65 MB (01.21%) ++ js-non-window

so there is a very precise linear growth of 210.76MB for both steps (16.85 MB β†’ 227.61 MB β†’ 438.31 MB), which gives a memory usage of 4.2MB per texture inside the WebAssembly heap.

Outside the WebAssembly heap, things look nominal. There is 169.13 MB of memory used for compiled WebAssembly code, and 8.49 MB used for the virtual filesystem. The heap size overreserves using geometric growth, so 512MB at this point is expected, though a tad aggressive. (I have rewritten the geometric growth logic in Emscripten to be more modest, but that is not yet in effect here - that might save 50MB-100MB).

Something you can also try is to use the new Unity β€œOptimize for Size” build option, if that would shrink some of that 169MB - not sure though if there will be a noticeable correlation to runtime memory usage.

@jukka_j Just saw your Safari & Chrome bug reports, hopefully, we this gets the attention of the developers.

I tested your web assembly app on my Galaxy S20+, and it fails to grow the heap size beyond 501mb (unable to allocate 512mb). But no crash, so that’s nice! Would it be possible to let Unity devs be aware of this memory growth failure via a callback?

The fact that there is no crash is an effect that the test page is a naive/simplistic allocation loop. The test page does not actually use any of the memory it allocates.

In practice if a Unity application does need the memory, there are few options to gracefully falling back to doing other things. Although if the failing allocation is one that comes directly from user C# code, maybe the new operator could throw a System.OutOfMemoryException instead. (That would require enabling exception handling in project build settings, which does increase shipped code size though) Added a task to our board to investigate if that’s possible.

1 Like

Hi @jukka_j , I’m working with @DerrickBarra on this problem and was hoping you could share any insights on the following possible fixes:

Which suggests attempting to allocate WebAssembly.Memory in a loop, starting with the maximum memory and decreasing until it succeeds. If something like this could work, how can it be implemented? For instance, does it have to be part of the framework.js or could it be part of the template files somehow? Is this potentially a feature that we might see for WebGL in the future?

Thanks!

  1. That is unfortunately my understanding as well. There is no good way to detect 32bit vs 64bit. At present time, I would rather try to detect mobile vs desktop, and treat mobile=32-bit, and desktop=64bit. That matches quite well what browsers are currently doing. (With the exception of Safari on iOS, but it imposes memory constraints that make it effectively look like the other 32bit browsers are behaving)

  2. Posted a followup in that thread. That will unfortunately not help much, it will crash Safari, and on 32-bit Chrome, it will likely lead to the browser itself running out of memory later. We are discussing these issues further in WebAssembly CG at Wasm needs a better memory management story Β· Issue #1397 Β· WebAssembly/design Β· GitHub

2 Likes

@jukka_j Just read your write up on the WebAssembly github, that was insightful to read, thanks!

I’m going to instruct our team to treat all mobile devices as having an upper limit of 256mb, and follow your progress on this issue via this thread. When I see other developers talking about this type of issue, I’ll point them here so we can collectively follow the progress.

1 Like