WeakRefs could solve the memory leak problem, but I understand that they’re contentious due to making GC observable. However I think it should be possible to solve this without making GC observable.
The fundamental problem is demonstrated by the following code:
// ON THE WORKER:
// This records the call and returns a placeholder
// Proxy which is assigned object id 1
const placeholderDiv = via.document.createElement("div");
// This is then sent to the main thread as a command similar to:
// 1. call "document.createElement" with argument "div" and assign
// the return value object id 1
// Any subsequent calls then refer to the object id, e.g.:
placeholderDiv.textContent = "foo";
// results in a command like:
// 2. assign object id 1 property "textContent" to "foo"
// ON THE MAIN THREAD:
// Upon receiving the first command the main thread does the real call:
const realDiv = document.createElement("div");
// Then assigns the intended object ID:
idMap.set(1, realDiv);
// The map is used to look up future commands, e.g. to run command 2 we
// need to start by looking up object id 1, similar to this:
const realDiv = idMap.get(1);
realDiv.textContent = "foo";
// However, now we have a permanent strong reference to the div, so
// it will never be collected. We can't use a WeakMap here since
// the key is not an object. We don't know when to delete the entry,
// since GC is not observable and we don't know when the placeholder
// Proxy on the worker will be collected.
To solve this, there could be a special WeakKey
object. This is like a reduced WeakRef that only serves to be used as a key in a WeakMap
. If a WeakKey
can then be posted between a Worker and the main thread, this should solve the problem by using it in place of the object ID:
// ON THE WORKER:
// This records the call and returns a placeholder
// Proxy which is assigned its own WeakKey
const placeholderDiv = via.document.createElement("div");
// internally, this will do something like:
// placeholderProxy._key = new WeakKey(placeholderProxy)
// This is then sent to the main thread as a command similar to:
// 1. call "document.createElement" with argument "div" and
// here is a WeakKey representing the return value
// Any subsequent calls then refer to the WeakKey, e.g.:
placeholderDiv.textContent = "foo";
// results in a command like:
// 2. assign this WeakKey property "textContent" to "foo"
// ON THE MAIN THREAD:
// Upon receiving the first command the main thread does the real call:
const realDiv = document.createElement("div");
// Then assigns the intended object by its WeakKey:
weakMap.set(weakKey, realDiv);
// The map is used to look up future commands, e.g. to run command 2 we
// need to start by looking up the same WeakKey, similar to this:
const realDiv = weakMap.get(weakKey);
realDiv.textContent = "foo";
This WeakKey approach then behaves how we want:
- The main thread can still look up real objects from messages sent from the worker.
- If a placeholder Proxy is collected on the worker, then there are no more references to its WeakKey. This allows the entry in the weak map on the main thread to be collected.
- GC is not observable.
I guess the downsides are this is pretty specific to this library, I’m not sure there are any use cases for this outside of Via.js. It also looks like it involves cross-context garbage collection which may be tricky for implementors, but I don’t know much about that.
Anyone have thoughts on this idea?