Transparent rpc between web worker and mainland(the main page context)
Looks like via.js, But more transparent at the cost of halt the web worker and continue on operation finished with Atomic.wait
.
You can try this with chromium(and its friends) 76+ with --js-flags='--harmony-weak-refs'
flag from cli
-
DOMProxy.create(
ia32
,getRoot
) =>[[Payload Object]]
- Call this on both side that want to sue the proxy with same Int32Array baked by SharedArrayBuffer
- arguments
- ia32: Int32Array baked by SharedArrayBuffer
- getRoot : function that return object that represent this thread
- must use only once per thread or it will hang
-
[[Payload Object]]
- properties
- current: Id of current payload
- getRemote(
remoteId
) =>[[proxied remote object]]
- arguments
- remoteId: other thread's
[[Payload Object]].current
- remoteId: other thread's
- arguments
- properties
-
No other api required, no async await things, no value wrapper, nothing else
Host
<!-- polyfill -->
<script src="await-async.js"></script>
<script src="rpc.js"></script>
<script src="dom-proxy.js"></script>
<script>
var sab = new SharedArrayBuffer(1024 * 1024 * 8)
var ia32 = new Int32Array(sab)
var proxy = DomProxy.create(ia32, () => window)
myWorker.postMessage({
hostId: proxy.current,
ia32: ia32
})
</script>
Worker
importScripts('await-async.js')
importScripts('rpc.js')
importScripts('dom-proxy.js')
self.addEventListener('message', function (event) {
var payload = event.data
var proxy = DomProxy.create(payload.ia32, () => self)
var remoteWindow = proxy.getRemote(payload.hostId)
// access the window object as if it is actully in webworker
console.log(remoteWindow.location.href)
console.log(remoteWindow.document.title)
// strict equal works as is thanks to WeakRef
console.log(remoteWindow.document === remoteWindow.document)
// object keys got proxied
console.log(Object.keys(remoteWindow))
console.log(Object.getOwnPropertyNames(remoteWindow.location))
// modify dom as is
var el = remoteWindow.document.createElement('div')
el.innerHTML = 'You have been hacked from web worker lol'
remoteWindow.document.body.appendChild(el)
// while applied directly (no async await required!!!)
console.log(remoteWindow.document.body.innerHTML)
// reverse proxied object as is
remoteWindow.a = {}
// reverse proxied function also works as is
el.addEventListener('click', () => remoteWindow.alert('LOLLLL'))
})
This library merely proxy traps of Proxy
to the mainland,
so everything should just works as if you are operate on these object in the mainland.
The mainland then receive the request async with Atomic.asyncAwait
.
And do things it need to do, response to worker with Atomic.notify
.
- This library marked the object it send to worker with an id and save it to a collection with that id.
// mainland
var id = createId()
var map = new Map()
map.set(objectToSend, id)
sendTheIdToWorker(id)
// worker
- The worker then receive the id, build a fake object with it and save the fake object to a collection
// mainland
// worker
var id = getIdFromMainLand()
var map = new Map()
var fakeObject = createProxy(id)
map.set(id, fakeObject)
- When the next time, the worker is requesting the same object,
the mainland will find the item in map and send the same id to worker.
The worker then find the same object in map with that id
// mainland
var id = map.get(objectToSend)
sendTheIdToWorker(id)
// worker
var id = getIdFromMainLand()
map.get(id)
Continue from section above.
How could you prevent the map in mainland and worker from leaking?
The WeakRef
proposal introduced two new APIs that allow you to observe the time garbage collection happened.
So we make use of them.
- The worker was modified to hold the fakeObject with a
WeakRef
instead of directly to allow the proxy being collected.
// mainland
// worker
-- map.set(id, fakeObject)
++ map.set(id, new WeakRef(fakeObject))
- The worker use the
FinalizationGroup
to track when will the garbage collection could happen (no one is holding the proxy anymore).
And send the event to mainland.
// mainland
// worker
const cleaner = new FinalizationGroup(id => {
// remove the cache from entry
map.delete(id)
tellTheMainlandToDropTheRef(id)
})
cleaner.register(fakeObject, id)
- The mainland then drop the cache it has with the id received.
At this point, the knowledge of that object is totally gone from the proxy system.
No memory leak happened, cheers.
// mainland
const id = getIdToDropFromWorker()
findIdInMapAndDropIt(map, id)
// worker
-
Due to the lack of native
Atomic.asyncAwait
, polyfill is used, the operation is actually far more expensive then it should (each call cost about 0.5ms).This means calls like
Object.keys(fakeObject)
will be very slow because it requires to callgetOwnPropertyDescriptor
on every single property (call it onwindow
will result in about 200 requests). -
Due to the
FinalizationGroup
andWeakRef
isn't ship on all stable browser version.It isn't possible to use this library without edit the browser setting from cli directly currently. (that's why this is a POC, WeakRef can't be polyfilled at all)
- Mapping symbol between workers (current throws)
- Fix worker count limit (current 256)
- Fix object id limit (current 8388608, aka
2**23
)