-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Transfer huge binary data buffer between service worker and content script (more efficiently) #293
Comments
We've discussed this issue, not the solutions as phrased, but more the general capability request. The overall sentiment is that the browser vendors are in favor of the better-designed capabilities from the web platform. I'm tentatively marking this as Note: the current extension messaging APIs have broadcast semantics, which means that there may potentially be more than one receiver. Therefore, if there is a desire to have transferable semantics, then the API needs to modified be such that the message targets on receiver only (e.g. tabId + the existing |
The Chrome Extensions is supportive of exposing Unfortunately I didn't have a chance to check in on the possibility of extending the Chrome's extension messaging system to support transerables. Based on previous conversations in the past, my impression is that this would be a significant amount of work and it may be preferable to simply replace the extensions-specific implementation of messaging with something based on web platform primitives. Given the amount of work this would likely require, I suspect it will likely be at least a year before we're able to seriously evaluate this change. I'm mostly thinking out loud here, but maybe we should tentatively consider redesigning extension messaging in the next manifest version bump? |
@Rob--W and @xeenon, we currently have supportive tags for Firefox and Safari. Can you clarify what specific aspect of the concerns raised or ideas suggested you are each supportive of? I think you were both expressing support for the idea of transferables in the extension messaging APIs, but I'd prefer to have you confirm rather than run with assumptions. |
We discussed the feature request in general terms as noted in #293 (comment). Here is my train of thoughts on the translation from the capability request to an actual API:
Currently a unique frame can be targeted (tabId + frameId/documentId) but not extension recipient (e.g. background, devtools, action popup, options_ui, ...). Let's cover that in #294. Note: |
I share @Rob--W 's desire to see if we can leverage existing web APIs for this, and have lots of interest in how we can incorporate this into messaging. I think this will take a good amount of looking into, but in principle, we're supportive. |
I renamed the issue because it's not possible to do this instantly. Regardless of how this is implemented data will have to be copied across processes. While we cannot make this instant, we are open to exploring ways to efficiently move data between a service worker and content script. |
Transferring a Blob is effectively instant because only the handle is |
Also very interested in this topic as I'm facing the same limitation. Excited to hear that the Chrome team is positive about getting a possible solution implemented. In the meanwhile I'd be interested in giving the 1. suggested workaround by @tophf a try. @tophf do you happen to know whether there is any public example of the workaround pattern you've described I could study as a reference implementation? Thank you! |
@schickling, see "Web messaging (two-way MessagePort)" in https://stackoverflow.com/a/68689866 |
Thanks a lot. I was able to make it work with that great explanation but it seems it doesn't work within Incognito tabs. Can you confirm this limitation @tophf? |
You'll have to use |
Works. Thanks a lot! Appreciate your help with this a lot. There are only very few learning resources about this topic. 🙏 |
Hey all, @schickling There's still a few small glitches in all of this, which is, that if you follow the stackoverflow example code, the Promise that is awaited in order for the first Goal: We will establish a bi-directional communication between the content script of an extension and the worker / background script. It will pass all kind of data structures around using transferable objects instantly and the page won't be able to intercept with this easily. Here is a fully working, battle-tested, TypeScript and documented example (I'm passing a Machine Learning model around):
"incognito": "split",
"web_accessible_resources": [
{
"resources": ["tunnel.html", "tunnel.js"],
"matches": ["<all_urls>"],
"use_dynamic_url": true
}
]
<!-- we are allowed to inject this script, as it is registered in manifest.json's web_accessible_resources -->
<script src=tunnel.js></script>
// one-time postMessage to the service worker
// this callback is executed once; the MessageChannel object
// passed down from the content script is passed to the service worker
// to establish a DIRECT two-way communication channel between the content script and the service worker
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
// and that's why we free the event listener instantly
window.onmessage = null;
// once the self.onmessage event listener is set up in the service worker
// we pass it the MessagePort object from the content script
navigator.serviceWorker.ready.then(swr => {
swr.active.postMessage('port', [e.ports[0]]);
});
}
};
// A: content script
// B: injected iframe and script
// C: background script/worker
// A (content script) cannot pass transferables (e.g. Blobs with GB of data) to C (background script/worker) directly
// So we create a tunnel by passing a MessageChannel to B (injected iframe/script) that passes it to C (background script)
// via the navigator.serviceWorker.messageChannel API which isn't available in content scripts
// the injected iframe/script dissolves itself after the first message is received
// once the tunnel is established, A can talk to C directly using transferabbles by using the standard MessageChannel API
async function makeTunnel(
path: string,
onMessage: (e: MessageEvent) => void,
) {
// we need a new secret for each tunnel to become unique, non-cached
const secret = Math.random().toString(36);
const url = new URL(chrome.runtime.getURL(path));
// this is why we need to set use_dynamic_url in manifest.json's web_accessible_resources entry
url.searchParams.set("secret", secret);
const el = document.createElement("div");
// we attach the element to the shadow DOM to prevent it from bleeding
const root = el.attachShadow({ mode: "closed" });
const iframe = document.createElement("iframe");
iframe.hidden = true;
root.appendChild(iframe);
(document.body || document.documentElement).appendChild(el);
// wait for the iframe to be loaded
await new Promise((resolve, reject) => {
iframe.onload = resolve;
iframe.onerror = reject;
iframe.contentWindow!.location.href = url.toString();
});
// once the iframe is loaded, we send the MessageChannel object to the iframe
// by reference (transferable); this only happens once
const mc = new MessageChannel();
iframe.contentWindow!.postMessage(secret, "*", [mc.port2]);
// we need to wait for the iframe to respond with its port
// and assign onMessage to the port first so that addEventListener
// would be called (new behavior in Chrome)
await new Promise((cb) => {
mc.port1.onmessage = onMessage;
// fulfill the promise after the first message (port is ready, bi-directionally)
mc.port1.addEventListener("message", cb, { once: true });
});
// we can safely remove the injected element and it's iframe now
if (el.parentNode) {
el.parentNode.removeChild(el);
}
// we return the port to the caller as well (`port.postMessage(...)`)
return mc.port1 as MessagePort;
} You should call this function once.
const port = await makeTunnel(
"/tunnel.html",
(e: MessageEvent) => {
console.log("received from worker:", e.data);
},
);
// for demonstration, let's pass down some transferables...
console.log("port", port);
console.log("postMesaages...");
port.postMessage(123);
port.postMessage({ foo: "bar" });
port.postMessage(new Blob(["foo"]));
const addTunnelListener = (
onContentMessage: (port: MessagePort, e: MessageEvent) => void,
) => {
self.onmessage = (connEstablishedEvt) => {
if (connEstablishedEvt.data === "port") {
// as we use the reference to the MessagePort here
// the callback assignment will last as long as the MessagePort
// so we can use it to communicate with the content script
connEstablishedEvt.ports[0].onmessage = (messageEvent) =>
onContentMessage(connEstablishedEvt.ports[0], messageEvent);
// initial ack/resolve, as we were receiving the port via the tunnel script
// and it needs to be passed back to the content script, for the last step's
// Promise to resolve
connEstablishedEvt.ports[0].postMessage(null);
}
};
};
// example implementation; addTunnelListener should be called *once*
addTunnelListener((port: MessagePort, e: MessageEvent) => {
// prints both in the background console and in the worker script console
console.log("from content script:", e.data);
// example to simply echo all data, so we can demonstrate that Blobs are passed back to the content script as they have been passed down here
port.postMessage(e.data);
}); I hope you'll enjoy this solution and that it works well for ya'all :) Have fun! And please, after all these years... can't we simply have a spec that won't stand in the way of developers ;)) 🤗 ? I mean, it's fun to hack stuff, but.. this one has been interesting mental gymnastics =) |
Here's a more decent implementation that also supports multiple calls, doesn't collide with an existing Worker's |
Could you please let me know if your solution is compatible with Safari on iOS? |
I'm sorry, I haven't checked it. My extension isn't compatible with mobile devices (it's a desktop only UI in a business setting), so there was no need on my side. It would be amazing, if you could try it out an report back wether it works as others might be interested in that as well. Thank you in advance! |
We are currently redesigning our messaging architecture to support the OP's workaround 1. However, that directly conflicts with the fact that
Which are both limiting and costly paths. |
You also have to make sure to regularly send a message to the worker via the MessageChannel. Otherwise, the worker will become inactive and the connection disconnected and GC'ed. You'll notice that after a few minutes of inactivity, MessageChannel becomes unstable - even when trying to re-establish the connection. I've implemented a 1 minute scheduled "ping" protocol to prevent this. |
Currently it's impossible to transfer 1GB ArrayBuffer/Blob/Uint8Array and the like between an extension background script (service worker) and the extension's content script both instantly (~1ms for Blob, 1sec for buffers) and directly.
Current workarounds
The fastest workaround is to create a web_accessible_resources iframe -> then use navigator.serviceWorker messaging and secure message passing with the parent window, all the while using the last parameter of postMessage to enable instant transfers. This workaround is fragile because the web page can delete the iframe, even if we hide it inside shadow DOM. It also adds a considerable overhead to create and initialize the iframe, which is a waste of time (at least 50ms), CPU, and memory.
Sending binary data via extension sendMessage API via structured clone algorithm. Chrome can't do it yet, Firefox can. This is quite slow and blocks both processes (extension and the web page) for the duration of the internal serialization/deserialization, which can be long for a huge binary data, thus introducing janks and lags.
Better solutions wanted
URL.createObjectURL + asynchronous fetch in the content script doesn't block the page. Currently it is disabled in service workers due to concerns with the lifetime of the blob. The web doesn't suffer from this restriction because a web SW is only used by the same origin pages/workers which all can use postMessage or self.onfetch + Response API. The extension's SW usage patterns have nothing in common with the web SW in 99.9% of cases, so we need this restriction lifted - just for the duration of SW lifetime.
Add
transfer
parameter to chrome.tabs.sendMessage or at least to long-lived messaging via port.postMessage, that transfers ownership of the data instantly same as in the web postMessage's transferables.The text was updated successfully, but these errors were encountered: