MessageChannels with both ends unreferenced are not collected
Reported by
a...@scirra.com,
Jan 3 2018
|
||||
Issue descriptionUserAgent: 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.
,
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).
,
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.
,
Jan 4 2018
,
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
,
Jan 19 2018
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!
,
Jan 19 2018
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.
,
Mar 1 2018
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.
,
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 |
||||
Comment 1 by mek@chromium.org
, Jan 3 2018