Closed Bug 1717085 Opened 2 years ago Closed 2 years ago

Fix many no-op calls of checkPublicRecipientsLimit() for public bulk mail notification and follow Fluent best practice


(Thunderbird :: Message Compose Window, task, P3)

Thunderbird 89


(thunderbird_esr78 wontfix)

91 Branch
Tracking Status
thunderbird_esr78 --- wontfix


(Reporter: thomas8, Assigned: thomas8)




(Keywords: perf)


(1 file, 1 obsolete file)

+++ This bug was initially created as a clone of Bug #271405 +++

  • It's quite amazing how often we're running checkPublicRecipientsLimit() without doing anything, which might affect performance with many recipients (think mass mails, the very use case of this feature!) and when using recipient autocomplete. Something wrong with the way we listen for changes here.
  • Also our Fluent isn't quite fluent yet (see comment 3).

  // Observe the changes of message recipient rows.
  gRecipientObserver = new MutationObserver(function(mutations) {
    if (mutations.some(m => m.type == "childList")) {
  gRecipientObserver.observe(document.getElementById("toAddrContainer"), {
    childList: true,

We're constructing the observer correctly, but then observing only the To field...
Yet the notification also works for Cc, so there must be something else... here:

function onRecipientsChanged(automatic) {
  if (!automatic) {
    gContentChanged = true;
  if (gRecipientObserver) {

Right, here's where we start going into overdrive:

  • onRecipientsChanged() gets called here for every input event of every autocomplete address input field (including Bcc!), which in turn calls checkPublicRecipientsLimit().

Here's the net result:

  • When you type, it will call checkPublicRecipientsLimit() for each character entered: that's 20 calls.
  • If your topmost autocomplete result changes with each character, that may add up to 20 calls again.
  • So you may have 40 calls for a single address, however, as long as that address is still plain text (not pillified), we won't even count it for the notification... So that's a lot of no-op!

Plus, every pill which you add or remove manually in the To field will needlessly be double-checked: once by the direct gRecipientObserver, and then again by onRecipientsChanged().

  • So adding 20 pills manually in To field will do 40 checks (in addition to the check-as-you-type calls) - more no-op!
  • Btw, reply-all triggers the observer for each and every reply address, whereas pressing Enter after comma-separated addresses gets counted by the observer as a single childList change - should look into that difference, too! (in another bug)

So the total no-op calls of checkPublicRecipientsLimit when entering 20 To recipients manually may be like this:
20 recipients x 20 chars x2 for character autocompletion + 20 for final autocompletion + 20 from observer for adding pill.
That's a whopping 840 calls where 20 should suffice if we're only counting pills.

Btw dragging an address from one field to another field which has pills already creates as many as 6 onRecipientsChanged() calls...

Great analysis and great findings.
Indeed, the checkPublicRecipientsLimit() should only happen for these scenarios:

  • Pill created
  • Pill deleted

And we should create some smart conditions to avoid running that method if pills are moved from a field to another, as the number of currently available pills didn't change.

Definitely nothing should be hooked to the onRecipientsChanged() method as that method runs at every input to enable the send button, trigger the "dirty" status for draft saving, and other things I don't remember, but that method needs to run at every input.

(In reply to Thomas D. (:thomas8) from comment #0)

That's a whopping 120 840 calls where 20 should suffice if we're only counting pills.

I've corrected the total number of calls, it's up to 840 calls for 20 fully manually entered addresses with 20 characters each.

(In reply to Thomas D. (:thomas8) from comment #0)

  • Also our Fluent isn't quite fluent yet (see comment 3).

  msgText.setAttribute("data-l10n-id", "many-public-recipients-info");
    JSON.stringify({ count: publicAddressPillsCount })

When you need to use JSON.stringify, there's something wrong...
We're trying to localize the msgText element dynamically, based on a JS variable.
Here's the standard way of achieving the same result with less and cleaner code:

    { count: publicAddressPillsCount }

Notably, that will even work when msgText element has been created, but not yet appended to the document.

It always looks so simple when it's done... getting there isn't!

  • Ensure that gRecipientObserver which calls checkPublicRecipientsLimit()
    when pills get added or removed is observing both To and Cc field to check
    if we need to show public bulk mail notification.
  • In turn, stop calling checkPublicRecipientsLimit() for every input event in
    onRecipientsChanged(), even twice if autocomplete changes the input text,
    which is a no-op because we're currently ignoring non-pillified addresses for
    the notification.
  • In a nutshell, prevent that entering a single plain text recipient with 20
    characters may call checkPublicRecipientsLimit() up to 42 times where
    a single call for the new pill would suffice.
  • Normalize dynamic Fluent attribute changes to the notification text.
  • I've checked that this works correctly for:
    • Manually adding/removing recipients in the address row.
    • Reply all with >= 15 recipients.
    • Starting TB via CLI with -compose
    • Drag and drop of recipients between rows and from contacts sidebar.
Attachment #9227742 - Flags: review?(alessandro)

@Aleca: Pls note that before and after this patch, we won't warn when there's >=15 CSV recipients as plain text in the input, and user hits Send immediately without going through pillification. Counting plain text CSV addresses is non-trivial (unless if we find an existing Mime helper for that), but if we'd want that, it would be for another bug. We'd then probably drop the Mutation observer and use only onRecipientsChanged(), which of course would also reverse most of the performance gains of this bug.

Summary: Fix duplicate and no-op calls of checkPublicRecipientsLimit() for public bulk mail notification and follow Fluent best practice → Fix many no-op calls of checkPublicRecipientsLimit() for public bulk mail notification and follow Fluent best practice
Comment on attachment 9227742 [details] [diff] [review]

Review of attachment 9227742 [details] [diff] [review]:

Great improvements, thanks for taking are of this.
I launched a try-run to be sure we're not breaking any tests:

::: mail/components/compose/content/MsgComposeCommands.js
@@ +5149,5 @@
>    msgText.classList.add("consider-bcc-notification-text");
> +  document.l10n.setAttributes(
> +    msgText,
> +    "many-public-recipients-info",
> +    { count: publicAddressPillsCount }

nit: the linter wants this formatted this way.
document.l10n.setAttributes(msgText, "many-public-recipients-info", {
  count: publicAddressPillsCount,
Attachment #9227742 - Flags: review?(alessandro) → review+

By looking at this I also noticed a wrong usage of the notification element.
We're adding extra elements with classes and divs which are redundant and also semantically inaccurate.
I'll create a follow up bug dependent on this to fix it.

Appreciate the rapid review & positive feedback!

Linting nit fixed. r+ from Aleca's comment 6.

Attachment #9227854 - Flags: review+

Pushed by
Fix frequent no-op calls of checkPublicRecipientsLimit() and related Fluent cleanup. r=aleca

Closed: 2 years ago
Resolution: --- → FIXED
Comment on attachment 9227854 [details] [diff] [review]

Review of attachment 9227854 [details] [diff] [review]:

::: mail/components/compose/content/MsgComposeCommands.js
@@ +5147,5 @@
>    let msgText = document.createElement("div");
>    msgText.classList.add("consider-bcc-notification-text");
> +  document.l10n.setAttributes(msgText, "many-public-recipients-info", {
> +    count: publicAddressPillsCount

Lint error. This requires a comma at the end.

You really gotta set up a linter in your system :P
Pushed by
Follow-up: Fix eslint error. r=eslint

This probably also broke the testMailingListMembersCounted test as it was relying on the l10nArgs dataset to get the count.
I'll fix it in bug 1717127.

Target Milestone: --- → 91 Branch
You need to log in before you can comment on or make changes to this bug.