Bug 204515 - WebKit doesn't garbage collect MessageEvent objects with transferables quickly enough
Summary: WebKit doesn't garbage collect MessageEvent objects with transferables quickl...
Status: RESOLVED DUPLICATE of bug 203990
Alias: None
Product: WebKit
Classification: Unclassified
Component: New Bugs (show other bugs)
Version: Safari Technology Preview
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Nobody
Keywords: InRadar
Depends on:
Reported: 2019-11-22 07:50 PST by Konstantin Kaefer
Modified: 2020-06-04 12:12 PDT (History)
4 users (show)

See Also:

postMessage with a long list of transferables (601 bytes, text/html)
2019-11-22 07:50 PST, Konstantin Kaefer
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Konstantin Kaefer 2019-11-22 07:50:31 PST
Created attachment 384152 [details]
postMessage with a long list of transferables

Over in the the Mapbox GL JS project, we've seen severe memory growth in WebKit and have been tracking the issue on https://github.com/mapbox/mapbox-gl-js/issues/8771. Severe memory growth means that the page's process can quickly jump to more than a gigabyte of memory when continually loading tiles (e.g. panning or zooming).

We've observed that this only happens when using web workers. We use web workers to load and process tiled map data for use with WebGL, then send it to the main thread. A typical tile can have up to a few hundred ArrayBuffers. We are using `postMessage` with an array of transferable objects to avoid buffer copies.

Furthermore, we've observed that _disabling_ transferable objects mostly fixes the memory growth issue and the process stays at ~500-600 MB.

I've dug in to find out why this happens:

When sending a message to another thread with `postMessage` (https://github.com/WebKit/webkit/blob/6a89f57fb9482c4f5afef8a863d7b0dc08ab7b94/Source/WebCore/workers/Worker.cpp#L133), WebKit creates a `SerializedScriptValue` object, passing in a list of transferable objects. The message gets serialized by the `CloneSerializer`, which adds indices to the ArrayBuffers stored directly in the `SerializedScriptValue` (https://github.com/WebKit/webkit/blob/6a89f57fb9482c4f5afef8a863d7b0dc08ab7b94/Source/WebCore/bindings/js/SerializedScriptValue.h#L120) object.

The message with the `SerializedScriptValue` gets pushed to the worker's or main thread's queue. Once the event get's dispatched (https://github.com/WebKit/webkit/blob/6a89f57fb9482c4f5afef8a863d7b0dc08ab7b94/Source/WebCore/workers/WorkerMessagingProxy.cpp#L120), a new `MessageEvent` object is created, which obtains ownership of the `SerializedScriptValue`.

WebKit doesn't seem to garbage collect `MessageEvent` objects frequently enough. I instrumented `SerializedScriptValue` and found that it can accumulate several thousand of those objects until GC kicks in, causing the process to allocate more and more memory. This means it's not technically a leak, but still causes excessive memory growth.

A few other observations:

The `MessageEvent` has to actually be dispatched into JS land. If there's no event listener, the `SerializedScriptValue`s get destructed immediately when the message is processed on the other thread. This means that the mere creation of a `MessageEvent` doesn't cause the leak.

Memory grows regardless of whether the value is ever _deserialized_. I rebuilt WebKit with deserialization disabled entirely and still observed the memory growth caused by WebKit hanging on to `MessageEvent` objects.

While the memory growth is impacted by the byte count of the transferred ArrayBuffers, the _number_ of elements in the transferables list has at least an equal effect. I created a small benchmark that allocates zero-size ArrayBuffer, then adds the same object 8192 times to the list of transferables. This causes the vector allocated to hold the ArrayBufferContent objects to become pretty large even if all of its members are zero because WebKit filters duplicates.

When attaching a smallish ArrayBuffer to the `MessageEvent` object when it is _received_ by just executing `event.foo = new ArrayBuffer(32768)`, I was able to trigger GC of those `MessageEvent` objects much more frequently, mitigating the memory growth somewhat. This leads me to believe that GC isn't aware of the actual size of `MessageEvent` objects. It should be a combination of the byte size of the stored ArrayBuffers and the count of them.

I've used the GC Heap Inspector and it looks like the `MessageEvent` objects aren't referenced from a GC root.

The direction of transfer (worker → main, or main → worker) doesn't seem to matter for how frequently they are GCed.
Comment 1 Radar WebKit Bug Importer 2019-11-22 15:17:51 PST
Comment 2 Yusuke Suzuki 2020-06-04 12:12:40 PDT

*** This bug has been marked as a duplicate of bug 203990 ***