Skip to content
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

A multi-tab broadcast version of chrome.tabs.sendMessage #722

Open
tophf opened this issue Nov 12, 2024 · 7 comments
Open

A multi-tab broadcast version of chrome.tabs.sendMessage #722

tophf opened this issue Nov 12, 2024 · 7 comments
Labels
needs-triage: chrome Chrome needs to assess this issue for the first time neutral: firefox Not opposed or supportive from Firefox neutral: safari Not opposed or supportive from Safari

Comments

@tophf
Copy link

tophf commented Nov 12, 2024

The problem I'm solving is efficiency when sending the same message to a lot of tabs as the extension may lock up the background script for many seconds and won't be able to process user activity during that time.

const results = await Promise.all(
  (await chrome.tabs.query({...}))
    .map(t => chrome.tabs.sendMessage(t.id, message)
));
serialization
+ API roundtrip
tabs total time
1ms1010ms
100100ms
10001,000ms
10ms10100ms
1001,000ms
100010,000ms
100ms101,000ms
10010,000ms
1000100,000ms

Workarounds?

  1. The extension can throttle the process by issuing 1-10 requests in one JS event loop task, but how many extension authors would bother? I'd say less than 1%. Anyway, it won't eliminate the redundancy of the entire process (serializing + sending the data to the browser process which handles the extension API).

  2. Broadcasting a Blob (in Firefox) or Blob URL (in Chrome) and fetching it in the recipient would still need the redundant calls to the sendMessage API, which will take at least 1ms and even that is problematic when there are many tabs, see the table above.

This is a problem that should be solved at the platform level.

Something like this:

chrome.tabs.broadcast([
  1,
  2,
  { tabId: 3, frameIds: [0,1,2] },
  { tabId: 4, documentIds: ['........'] },
], message)

The result may be an array with one element per tab from the first frame to respond (similarly to chrome.tabs.sendMessage):

[
  {tabId: 1, frameId: 12254524, result: 'foo', documentId: '...'},
  {tabId: 2, error: {message: 'no receiving end'}},
  {tabId: 3, frameId: 0, result: 'bar', documentId: '...'},
  // .....
]

A weaker solution would be to accept an array of tabIds in the existing chrome.tabs.sendMessage([1,2,3], message, options), but it would limit options.frameId to just 0 or null/undefined/unspecified and it would change the result to an array, which is not how the API works currently (the type of the result doesn't change depending on the argument).

@github-actions github-actions bot added needs-triage: chrome Chrome needs to assess this issue for the first time needs-triage: firefox Firefox needs to assess this issue for the first time needs-triage: safari Safari needs to assess this issue for the first time labels Nov 12, 2024
@xeenon xeenon added neutral: safari Not opposed or supportive from Safari and removed needs-triage: safari Safari needs to assess this issue for the first time labels Nov 21, 2024
@xeenon
Copy link
Collaborator

xeenon commented Nov 21, 2024

I would be in favor of a tabs.broadcast() that does not wait for replies. Otherwise, there likely wont be any gain here other than saving on JSON serialization savings.

@tophf
Copy link
Author

tophf commented Nov 21, 2024

wont be any gain here other than saving on JSON serialization savings.

This is the primary goal. Serialization cost can be disproportionally exorbitant.

does not wait for replies

That would correspond to the implied meaning of "broadcast", but it would also reduce usability of this API.

An unreliable workaround would be for the content script to send a new message with some kind of id from the broadcast payload so that the sender can accumulate the responses, but it's unreliable as the sender will have to assume that the responses arrive within some preset timeout, increasing which would delay the extension's reaction in case some tabs didn't have content scripts and thus won't send anything.

I see two solutions:

  1. rename the method to avoid the implications of broadcast, e.g. chrome.tabs.sendMessageToTabs
  2. add an event to track responses asynchronously e.g. chrome.tabs.onBroadcastResponse, so that chrome.tabs.broadcast call resolves to some internal id that will be then reported in onBroadcastResponse.

@Rob--W
Copy link
Member

Rob--W commented Nov 21, 2024

See https://github.com/w3c/webextensions/blob/main/_minutes/2024-11-21-wecg.md (merged from #726) for the discussion during today's meeting.

At the start of the meeting I noted that I'd like to avoid broadcast semantics to enable optimizations, that refers to the discussion in #293, specifically #293 (comment).

I can also see some use cases for intentional broadcast semantics, and am willing to consider supporting that. For use cases that really need broadcast semantics, broadcasting once is more efficient than JS->internal serialization followed by IPC (which also incurs serialization) and dispatch overhead.

IMO the default behavior should be to not have responses. The reason for that is that maintaining responses in a general way is very expensive: the browser's main process has to wait for all content processes to respond before returning to the extension. And each content process would have to maintain internal state to keep alive the broadcast message until the extension contexts within have responded (asynchronously). This is what we already do with runtime.onMessage, and expensive.

If you have compelling use cases for needing response support, please state them.

@Rob--W Rob--W added neutral: firefox Not opposed or supportive from Firefox and removed needs-triage: firefox Firefox needs to assess this issue for the first time labels Nov 21, 2024
@tophf
Copy link
Author

tophf commented Nov 21, 2024

compelling use cases for needing response support

The use case is to ensure that all tabs got the data.

See my comment above where I described why the workaround is problematic and suggested two solutions:

  1. rename the method to avoid the implications of broadcast, e.g. chrome.tabs.sendMessageToTabs
  2. add an event to track responses asynchronously e.g. chrome.tabs.onBroadcastResponse, so that chrome.tabs.broadcast call resolves to some internal id that will be then reported in onBroadcastResponse.

The key aspect of the proposal is not broadcasting per se, but having a version of Promise.all + chrome.tabs.query + chrome.tabs.sendMessage that removes the redundant serialization/IPC while retaining the ability to track the overall result.

Your idea is essentially a huge step further. It is certainly useful in some cases, so maybe it would make sense to add both chrome.tabs.broadcast (ideally with onBroadcastResponse event) and chrome.tabs.sendMessageToTabs.

@fregante
Copy link

fregante commented Nov 21, 2024

Just a nit: Does this have to be a dedicated method with custom behavior? tabs.sendMessage already has a lot of the required logic for this when targeting frames:

  • it targets any number of frames at once
  • it returns any number of values from every frame

Why not just extend sendMessage() to accept {tabId: [1,2,3]} or the suggested "array of targets" parameter?

While I agree that there could be better APIs for this, I don't think they're strictly related to multi-tab broadcasts (i.e. if you want to introduce a better messaging API, it could apply to tabs and runtime too)

FWIW runtime.sendMessage() can already broadcast to all runtime pages at once too, as long as they don't return data.

@tophf
Copy link
Author

tophf commented Nov 21, 2024

These are my reasons for a dedicated method:

  1. to detect support without checking userAgent version, which is fragile and clunky
  2. to avoid complicating the existing simple signature: either the first parameter becomes optional and we specify multiple combinations of tabId and frameId inside the options parameter or the first parameter accepts an array of target descriptors.
  3. to avoid changing the shape of the returned result depending on the parameters

@twschiller
Copy link

twschiller commented Nov 22, 2024

If you have compelling use cases for needing response support, please state them.

We've implemented our own awaitable broadcast wrapper/behavior in PixieBrix. We've seen most usage for compliance/governance/auditing use cases. For example:

  • Checking/auditing if the user has an active dialer call or customer chat session, e.g., to restrict customer support agent access to order management systems except when handling cases
  • Training/governance/auditing to ensure that the user references the correct information while performing an action, e.g., a refund, account action

We currently warn if a message is being broadcast to more than 20 tabs (we picked the number as a heuristic based on observation 2+ years ago)

Sometimes we can determine via querying URLs, but often for SPAs you have to check the actual content of the page.

Prior to MV3 we also relied on retrieving information across tabs via broadcast. But since then, session storage has become the most effective/performant way to share data across tabs on an ongoing basis

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-triage: chrome Chrome needs to assess this issue for the first time neutral: firefox Not opposed or supportive from Firefox neutral: safari Not opposed or supportive from Safari
Projects
None yet
Development

No branches or pull requests

5 participants