New issue
Advanced search Search tips

Issue 798855 link

Starred by 14 users

Issue metadata

Status: Available
Owner: ----
Cc:
Components:
EstimatedDays: ----
NextAction: ----
OS: Windows
Pri: 2
Type: Bug



Sign in to add a comment

MessageChannels with both ends unreferenced are not collected

Reported by a...@scirra.com, Jan 3 2018

Issue description

UserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36

Steps to reproduce the problem:
1. Visit https://www.scirra.com/labs/bugs/comlink-leak/
2. Open Chrome's task manager
3. Observe tab memory use

What is the expected behavior?
The test uses Google's Comlink library (https://github.com/GoogleChromeLabs/comlink). This relies on using Proxy objects in both the main document and a worker, connected by a MessageChannel. For the library to be useful, the memory usage needs to stay consistent.

What went wrong?
When the Proxys on both ends become unreferenced, they cannot be collected, presumably because the MessageChannel is kept alive and keeps a reference to the Proxy objects. The MessageChannel cannot be explicitly closed by JS when Proxys are collected, because GC is not observable. (The WeakRef proposal would allow this, but makes GC observable.)

Did this work before? N/A 

Chrome version: 63.0.3239.84  Channel: n/a
OS Version: 10.0
Flash Version: 

Comlink already has >500 stars so this ought to be fixed so the library is useful in production and does not permanently leak memory. The same approach could also allow a library like via.js (https://github.com/AshleyScirra/via.js) to become usable, allowing DOM access from a web worker.
 

Comment 1 by mek@chromium.org, Jan 3 2018

Components: -Blink Blink>Messaging

Comment 2 by mek@chromium.org, Jan 4 2018

There seem to be two parts to this, an easy part and a hard part:

The easy part is the case where one port of a channel is explicitly closed. In that case it should be fairly straight-forward to let the other end of the channel figure out that the channel is dead, and thus can be garbage collected.

The much harder part is the case you're describing, where neither side of the channel is explicitly closed. This is particularly tricky since it's not enough just for port A to message port B letting it know it has no more references: there could be a message from port B in flight back to port A that would reinstate a reference to port A, making the earlier "I'm dead" assertion invalid.

Also not sure if v8/blink bindings currently even exposes a "you would have been GC'ed if it wasn't for HasPendingActivity returning true" signal.

And I guess there is also a possible performance concern. In a (possibly common?) case where a page listens to messages on a port and replies to those, but otherwise doesn't keep a reference around to the port we'd now possibly end up sending a "I'm dead" message after every message being handled? Although I suppose if that happens frequently enough to matter for performance GC wouldn't actually have triggered the "I'm dead" thing yet before the next message comes in so it's probably not too bad (but I'm speculating here about how our GC actually works).

Comment 3 by a...@scirra.com, Jan 4 2018

In the case of via.js (and maybe Comlink, I'm not sure how it's architected), the channel is used one way, e.g. the worker side sends commands to the document side. It could in theory close the channel when it's not got any messages in-flight, which in theory would only need the easy part to be implemented.

The problem though is from a quick test, once you close a MessagePort, you can't start it again. So after the first command if you close the MessagePort you can never use it again, and subsequent commands won't work. So again, because GC is not observable, it's impossible to know when you can close the MessagePort. Either MessagePort needs to be changed to be re-startable, or we have to rule out the easy case.

I'm not sure but it may turn out that we need the hard case anyway - things like forwarding callbacks tend to need the reverse communication, and then there are other memory management questions around how things like addEventListener and removeEventListener can correctly release their referenced functions.
Labels: Needs-Triage-M63
Project Member

Comment 5 by bugdroid1@chromium.org, Jan 9 2018

The following revision refers to this bug:
  https://chromium.googlesource.com/chromium/src.git/+/5a3125dbb84d7a4ce30d5d78ed4b9e159aca7a54

commit 5a3125dbb84d7a4ce30d5d78ed4b9e159aca7a54
Author: Marijn Kruisselbrink <mek@chromium.org>
Date: Tue Jan 09 17:31:24 2018

Close MessagePort if the underlying mojo pipe is closed.

This will let the remote MessagePort of a MessageChannel be garbage
collected if the local end is closed (assuming no other references exist
for that port). Before only ports that are explicitly closed directly could
be garbage collected.

Bug: 798855
Change-Id: I617c0063b6559fe4eb9c5e670680942472e09ee8
Reviewed-on: https://chromium-review.googlesource.com/850546
Reviewed-by: Jeremy Roman <jbroman@chromium.org>
Commit-Queue: Marijn Kruisselbrink <mek@chromium.org>
Cr-Commit-Position: refs/heads/master@{#528029}
[modify] https://crrev.com/5a3125dbb84d7a4ce30d5d78ed4b9e159aca7a54/third_party/WebKit/Source/core/messaging/MessagePort.cpp

Labels: Triaged-ET
Owner: mek@chromium.org
Status: Assigned (was: Unconfirmed)
Assigning to @mek who authored the CL in comment#5.

@mek: Could you please change the status of the bug if issue is fixed completely.

Thanks!

Comment 7 by mek@chromium.org, Jan 19 2018

Cc: mek@chromium.org
Owner: ----
Status: Available (was: Assigned)
Definitely not fixed completely. My CL only address the easy part. Getting the right hooks into the GC system to do the full fix is much harder and probably not something I'll have time to work on any time soon.
Something to note is that the underlying use case could be solved by another mechanism (that doesn't exist). If there some form of token API that when messaged would hold the same identity if passed to the same worker, then one could trivially hold it in a WeakMap to store data for the remote object.

For example what I mean by a same identity token:

// main.js
const token = new Token()
const worker = new Worker("./worker.js")
worker.postMessage(token)
worker.postMessage(token)

// worker.js
const seenTokens = new WeakSet()
self.onmessage = ({ data: token }) => {
    if (seenTokens.has(token)) {
        console.log("SeenToken")
    } else {
        console.log("NewToken")
    }
    seenTokens.add(token)
}
// would print NewToken then SeenToken as identity is preserved

I'm not sure whether this idea would be simpler than the Garbage collection of a pair of MessagePorts so it might not be worth investigating.

Comment 9 by a...@scirra.com, Mar 1 2018

That looks similar to an alternative idea I had called WeakKey: https://discourse.wicg.io/t/a-way-to-the-use-dom-in-a-web-worker/2483/2

From what V8 engineers have said though, it would require cross-heap collection, which would be a major change.

Sign in to add a comment