Backed out 2 changesets (bug 1594752) for causing bug 1638148 on a CLOSED TREE
authorAndreea Pavel <apavel@mozilla.com>
Fri, 15 May 2020 13:47:49 +0300
changeset 530248 0df949d97649310e97294a27092e11c665a209ca
parent 530247 3c239920e8fb5a102ce55d13741dbc796663b9f1
child 530249 82aeb12966b3b086c099d843258590df370b9ba7
push id37420
push usernerli@mozilla.com
push dateFri, 15 May 2020 21:52:36 +0000
treeherdermozilla-central@f340bbb582d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1594752, 1638148
milestone78.0a1
backs out66cc44b67170c7e48411180d5785f861b04a7719
e781cf38f088c02d2b336aa8ec990756447683c9
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 2 changesets (bug 1594752) for causing bug 1638148 on a CLOSED TREE Backed out changeset 66cc44b67170 (bug 1594752) Backed out changeset e781cf38f088 (bug 1594752)
browser/base/content/tabbrowser.js
browser/components/extensions/ExtensionPopups.jsm
browser/components/sessionstore/test/browser_tab_label_during_restore.js
devtools/client/responsive/browser/tunnel.js
devtools/server/actors/webbrowser.js
dom/chrome-webidl/WindowGlobalActors.webidl
dom/ipc/WindowGlobalParent.cpp
dom/ipc/WindowGlobalParent.h
dom/media/mediacontrol/MediaStatusManager.cpp
toolkit/content/browser-child.js
toolkit/content/widgets/browser-custom-element.js
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -52,16 +52,17 @@
           "browser.display.background_color"
         );
       }
 
       let messageManager = window.getGroupMessageManager("browsers");
       window.messageManager.addMessageListener("contextmenu", this);
 
       if (gMultiProcessBrowser) {
+        messageManager.addMessageListener("DOMTitleChanged", this);
         messageManager.addMessageListener("DOMWindowClose", this);
         messageManager.addMessageListener("Browser:Init", this);
       } else {
         this._outerWindowIDBrowserMap.set(
           this.selectedBrowser.outerWindowID,
           this.selectedBrowser
         );
       }
@@ -5117,16 +5118,27 @@
       }
     },
 
     receiveMessage(aMessage) {
       let data = aMessage.data;
       let browser = aMessage.target;
 
       switch (aMessage.name) {
+        case "DOMTitleChanged": {
+          let tab = this.getTabForBrowser(browser);
+          if (!tab || tab.hasAttribute("pending")) {
+            return undefined;
+          }
+          let titleChanged = this.setTabTitle(tab);
+          if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) {
+            tab.setAttribute("titlechanged", "true");
+          }
+          break;
+        }
         case "contextmenu": {
           openContextMenu(aMessage);
           break;
         }
         case "Browser:Init": {
           let tab = this.getTabForBrowser(browser);
           if (!tab) {
             return undefined;
@@ -5271,16 +5283,18 @@
           false
         );
       }
       window.removeEventListener("sizemodechange", this);
       window.removeEventListener("occlusionstatechange", this);
       window.removeEventListener("framefocusrequested", this);
 
       if (gMultiProcessBrowser) {
+        let messageManager = window.getGroupMessageManager("browsers");
+        messageManager.removeMessageListener("DOMTitleChanged", this);
         window.messageManager.removeMessageListener("contextmenu", this);
 
         if (this._switcher) {
           this._switcher.destroy();
         }
       }
     },
 
@@ -5330,39 +5344,16 @@
           // If we don't preventDefault on the DOMWindowClose event, then
           // in the parent-process browser case, we're telling the platform
           // to close the entire window. Calling preventDefault is our way of
           // saying we took care of this close request by closing the tab.
           event.preventDefault();
         }
       });
 
-      this.addEventListener("pagetitlechanged", event => {
-        let browser = event.target;
-        let tab = this.getTabForBrowser(browser);
-        if (!tab || tab.hasAttribute("pending")) {
-          return;
-        }
-
-        // Ignore empty title changes on internal pages. This prevents the title
-        // from changing while Fluent is populating the (initially-empty) title
-        // element.
-        if (
-          !browser.contentTitle &&
-          browser.contentPrincipal.isSystemPrincipal
-        ) {
-          return;
-        }
-
-        let titleChanged = this.setTabTitle(tab);
-        if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) {
-          tab.setAttribute("titlechanged", "true");
-        }
-      });
-
       this.addEventListener(
         "DOMWillOpenModalDialog",
         event => {
           if (!event.isTrusted) {
             return;
           }
 
           let targetIsWindow = event.target instanceof Window;
@@ -5440,16 +5431,57 @@
           }
 
           // If permissions/origins dictate so, bring tab to the front.
           this.selectedTab = tabForEvent;
         },
         true
       );
 
+      this.addEventListener("DOMTitleChanged", event => {
+        if (!event.isTrusted) {
+          return;
+        }
+
+        var contentWin = event.target.defaultView;
+        if (contentWin != contentWin.top) {
+          return;
+        }
+
+        let browser = contentWin.docShell.chromeEventHandler;
+        var tab = this.getTabForBrowser(browser);
+        if (!tab || tab.hasAttribute("pending")) {
+          return;
+        }
+
+        if (!browser.docShell) {
+          return;
+        }
+        // Ensure `docShell.document` (an nsIWebNavigation idl prop) is there:
+        browser.docShell.QueryInterface(Ci.nsIWebNavigation);
+        if (event.target != browser.docShell.document) {
+          return;
+        }
+
+        // Ignore empty title changes on internal pages. This prevents the title
+        // from changing while Fluent is populating the (initially-empty) title
+        // element.
+        if (
+          !browser.contentTitle &&
+          browser.contentPrincipal.isSystemPrincipal
+        ) {
+          return;
+        }
+
+        var titleChanged = this.setTabTitle(tab);
+        if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) {
+          tab.setAttribute("titlechanged", "true");
+        }
+      });
+
       let onTabCrashed = event => {
         if (!event.isTrusted || !event.isTopFrame) {
           return;
         }
 
         let browser = event.originalTarget;
 
         // Preloaded browsers do not actually have any tabs. If one crashes,
--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -167,23 +167,23 @@ class BasePopup {
   }
 
   destroyBrowser(browser, finalize = false) {
     let mm = browser.messageManager;
     // If the browser has already been removed from the document, because the
     // popup was closed externally, there will be no message manager here, so
     // just replace our receiveMessage method with a stub.
     if (mm) {
+      mm.removeMessageListener("DOMTitleChanged", this);
       mm.removeMessageListener("Extension:BrowserBackgroundChanged", this);
       mm.removeMessageListener("Extension:BrowserContentLoaded", this);
       mm.removeMessageListener("Extension:BrowserResized", this);
     } else if (finalize) {
       this.receiveMessage = () => {};
     }
-    browser.removeEventListener("pagetitlechanged", this);
     browser.removeEventListener("DOMWindowClose", this);
   }
 
   // Returns the name of the event fired on `viewNode` when the popup is being
   // destroyed. This must be implemented by every subclass.
   get DESTROY_EVENT() {
     throw new Error("Not implemented");
   }
@@ -206,16 +206,20 @@ class BasePopup {
     while (panel && panel.localName != "panel") {
       panel = panel.parentNode;
     }
     return panel;
   }
 
   receiveMessage({ name, data }) {
     switch (name) {
+      case "DOMTitleChanged":
+        this.viewNode.setAttribute("aria-label", this.browser.contentTitle);
+        break;
+
       case "Extension:BrowserBackgroundChanged":
         this.setBackground(data.background);
         break;
 
       case "Extension:BrowserContentLoaded":
         this.browserLoadedDeferred.resolve();
         break;
 
@@ -251,20 +255,16 @@ class BasePopup {
               );
             })
             .catch(() => {
               // If the panel closes too fast an exception is raised here and tests will fail.
             });
         }
         break;
 
-      case "pagetitlechanged":
-        this.viewNode.setAttribute("aria-label", this.browser.contentTitle);
-        break;
-
       case "DOMWindowClose":
         this.closePopup();
         break;
     }
   }
 
   createBrowser(viewNode, popupURL = null) {
     let document = viewNode.ownerDocument;
@@ -319,20 +319,20 @@ class BasePopup {
       // that, but we should get rid of it in the long term.
       browser.contentWindow; // eslint-disable-line no-unused-expressions
     }
 
     ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
 
     let setupBrowser = browser => {
       let mm = browser.messageManager;
+      mm.addMessageListener("DOMTitleChanged", this);
       mm.addMessageListener("Extension:BrowserBackgroundChanged", this);
       mm.addMessageListener("Extension:BrowserContentLoaded", this);
       mm.addMessageListener("Extension:BrowserResized", this);
-      browser.addEventListener("pagetitlechanged", this);
       browser.addEventListener("DOMWindowClose", this);
       return browser;
     };
 
     if (!popupURL) {
       // For remote browsers, we can't do any setup until the frame loader is
       // created. Non-remote browsers get a message manager immediately, so
       // there's no need to wait for the load event.
--- a/browser/components/sessionstore/test/browser_tab_label_during_restore.js
+++ b/browser/components/sessionstore/test/browser_tab_label_during_restore.js
@@ -25,21 +25,17 @@ add_task(async function() {
   function observeLabelChanges(tab, expectedLabels) {
     let seenLabels = [tab.label];
     function TabAttrModifiedListener(event) {
       if (event.detail.changed.some(attr => attr == "label")) {
         seenLabels.push(tab.label);
       }
     }
     tab.addEventListener("TabAttrModified", TabAttrModifiedListener);
-    return async () => {
-      await BrowserTestUtils.waitForCondition(
-        () => seenLabels.length == expectedLabels.length,
-        "saw " + seenLabels.length + " TabAttrModified events"
-      );
+    return () => {
       tab.removeEventListener("TabAttrModified", TabAttrModifiedListener);
       is(
         JSON.stringify(seenLabels),
         JSON.stringify(expectedLabels || []),
         "observed tab label changes"
       );
     };
   }
@@ -94,57 +90,57 @@ add_task(async function() {
   browserLoadedPromise = BrowserTestUtils.browserLoaded(
     tab2.linkedBrowser,
     false,
     ABOUT_ROBOTS_URI
   );
   gBrowser.selectedTab = tab2;
   await browserLoadedPromise;
   ok(!tab2.hasAttribute("pending"), "second tab isn't pending anymore");
-  await finishObservingLabelChanges();
   ok(
     document.title.startsWith(ABOUT_ROBOTS_TITLE),
     "title bar displays content title"
   );
+  finishObservingLabelChanges();
 
   info("selecting the third tab");
   finishObservingLabelChanges = observeLabelChanges(tab3, [
     "example.com/",
     REMOTE_TITLE,
   ]);
   browserLoadedPromise = BrowserTestUtils.browserLoaded(
     tab3.linkedBrowser,
     false,
     REMOTE_URL
   );
   gBrowser.selectedTab = tab3;
   await browserLoadedPromise;
   ok(!tab3.hasAttribute("pending"), "third tab isn't pending anymore");
-  await finishObservingLabelChanges();
   ok(
     document.title.startsWith(REMOTE_TITLE),
     "title bar displays content title"
   );
+  finishObservingLabelChanges();
 
   info("selecting the fourth tab");
   finishObservingLabelChanges = observeLabelChanges(tab4, [NO_TITLE_URL]);
   browserLoadedPromise = BrowserTestUtils.browserLoaded(
     tab4.linkedBrowser,
     false,
     NO_TITLE_URL
   );
   gBrowser.selectedTab = tab4;
   await browserLoadedPromise;
   ok(!tab4.hasAttribute("pending"), "fourth tab isn't pending anymore");
-  await finishObservingLabelChanges();
   is(
     document.title,
     document.getElementById("bundle_brand").getString("brandFullName"),
     "title bar doesn't display content title since page doesn't have one"
   );
+  finishObservingLabelChanges();
 
   info("restoring the modified browser state");
   gBrowser.selectedTab = tab3;
   await TabStateFlusher.flushWindow(window);
   await promiseBrowserState(SessionStore.getBrowserState());
   [tab1, tab2, tab3, tab4, tab5] = gBrowser.tabs;
   is(tab3, gBrowser.selectedTab, "third tab is selected after restoring");
   ok(
@@ -171,12 +167,12 @@ add_task(async function() {
   );
   gBrowser.selectedTab = tab1;
   ok(
     document.title.startsWith(REMOTE_TITLE),
     "title bar displays content title"
   );
   await tabContentRestored;
   ok(!tab1.hasAttribute("pending"), "first tab isn't pending anymore");
-  await finishObservingLabelChanges();
+  finishObservingLabelChanges();
 
   await promiseBrowserState(BACKUP_STATE);
 });
--- a/devtools/client/responsive/browser/tunnel.js
+++ b/devtools/client/responsive/browser/tunnel.js
@@ -24,16 +24,17 @@ function debug(msg) {
 /**
  * Properties swapped between browsers by browser.js's `swapDocShells`.
  */
 const SWAPPED_BROWSER_STATE = [
   "_remoteFinder",
   "_securityUI",
   "_documentURI",
   "_documentContentType",
+  "_contentTitle",
   "_characterSet",
   "_contentPrincipal",
   "_isSyntheticDocument",
   "_innerWindowID",
 ];
 
 /**
  * Various parts of the Firefox code base expect to access properties on the browser
@@ -96,16 +97,17 @@ function tunnelToInnerBrowser(outer, inn
         inner._documentContentType = outer._documentContentType;
       }
     },
 
     onLocationChange: (webProgress, request, location, flags) => {
       if (webProgress?.isTopLevel) {
         inner._documentURI = outer._documentURI;
         inner._documentContentType = outer._documentContentType;
+        inner._contentTitle = outer._contentTitle;
         inner._characterSet = outer._characterSet;
         inner._contentPrincipal = outer._contentPrincipal;
         inner._isSyntheticDocument = outer._isSyntheticDocument;
         inner._innerWindowID = outer._innerWindowID;
         inner._remoteWebNavigation._currentURI =
           outer._remoteWebNavigation._currentURI;
       }
     },
@@ -450,16 +452,17 @@ MessageManagerTunnel.prototype = {
     "SessionStore:restoreHistory",
     "SessionStore:restoreTabContent",
   ],
 
   INNER_TO_OUTER_MESSAGES: [
     // Messages sent to browser.js
     "PageStyle:StyleSheets",
     // Messages sent to browser.js
+    "DOMTitleChanged",
     "InPermitUnload",
     "PermitUnload",
     // Messages sent to SessionStore.jsm
     "SessionStore:update",
     // Messages sent to BrowserTestUtils.jsm
     "browser-test-utils:loadEvent",
   ],
 
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -46,16 +46,21 @@ loader.lazyRequireGetter(
   "devtools/server/actors/process",
   true
 );
 loader.lazyImporter(
   this,
   "AddonManager",
   "resource://gre/modules/AddonManager.jsm"
 );
+loader.lazyImporter(
+  this,
+  "AppConstants",
+  "resource://gre/modules/AppConstants.jsm"
+);
 
 /**
  * Browser-specific actors.
  */
 
 /**
  * Retrieve the window type of the top-level window |window|.
  */
@@ -168,16 +173,25 @@ exports.createRootActor = function creat
  * linked browser's content window objects do).
  *
  * However, while we could thus assume that each tab stays with the XUL window
  * it belonged to when it was created, I'm not sure this is behavior one should
  * rely upon. When a XUL window is closed, we take the less efficient, more
  * conservative approach of simply searching the entire table for actors that
  * belong to the closing XUL window, rather than trying to somehow track which
  * XUL window each tab belongs to.
+ *
+ * - Title changes:
+ *
+ * For tabs living in the child process, we listen for DOMTitleChange message
+ * via the top-level window's message manager.
+ * But as these messages aren't sent for tabs loaded in the parent process,
+ * we also listen for TabAttrModified event, which is fired only on Firefox
+ * desktop.
+ * Also, we listen DOMTitleChange event on Android document.
  */
 function BrowserTabList(connection) {
   this._connection = connection;
 
   /*
    * The XUL document of a tabbed browser window has "tab" elements, whose
    * 'linkedBrowser' JavaScript properties are "browser" elements; those
    * browsers' 'contentWindow' properties are wrappers on the tabs' content
@@ -212,17 +226,17 @@ function BrowserTabList(connection) {
    * True if we've been iterated over since we last called our onListChanged
    * hook.
    */
   this._mustNotify = false;
 
   /* True if we're testing, and should throw if consistency checks fail. */
   this._testing = false;
 
-  this._onPageTitleChangedEvent = this._onPageTitleChangedEvent.bind(this);
+  this._onAndroidDocumentEvent = this._onAndroidDocumentEvent.bind(this);
 }
 
 BrowserTabList.prototype.constructor = BrowserTabList;
 
 BrowserTabList.prototype.destroy = function() {
   this._actorByBrowser.clear();
   this.onListChanged = null;
 };
@@ -468,24 +482,43 @@ BrowserTabList.prototype._checkListening
    * only way to find out about tabs that come and go when top-level windows
    * are opened and closed.
    */
   this._listenToMediatorIf(
     (this._onListChanged && this._mustNotify) || this._actorByBrowser.size > 0
   );
 
   /*
-   * We also listen for title changed events on the browser.
+   * We also listen for title changed from the child process.
+   * This allows listening for title changes from OOP tabs.
+   * OOP tabs are running browser-child.js frame script which sends DOMTitleChanged
+   * events through the message manager.
    */
-  this._listenForEventsIf(
+  this._listenForMessagesIf(
     this._onListChanged && this._mustNotify,
     "_listeningForTitleChange",
-    ["pagetitlechanged"],
-    this._onPageTitleChangedEvent
+    ["DOMTitleChanged"]
   );
+
+  /*
+   * We also listen for title changed event on Android document.
+   * Android document events are used for single process Gecko View and Firefox for
+   * Android. They do no execute browser-child.js because of single process, instead
+   * DOMTitleChanged events are emitted on the top level document.
+   * Also, Multi process Gecko View is not covered by here since that receives title
+   * updates via DOMTitleChanged messages.
+   */
+  if (AppConstants.platform === "android") {
+    this._listenForEventsIf(
+      this._onListChanged && this._mustNotify,
+      "_listeningForAndroidDocument",
+      ["DOMTitleChanged"],
+      this._onAndroidDocumentEvent
+    );
+  }
 };
 
 /*
  * Add or remove event listeners for all XUL windows.
  *
  * @param shouldListen boolean
  *    True if we should add event handlers; false if we should remove them.
  * @param guard string
@@ -509,29 +542,73 @@ BrowserTabList.prototype._listenForEvent
         win[op](name, listener, false);
       }
     }
     this[guard] = shouldListen;
   }
 };
 
 /*
- * Event listener for pagetitlechanged event.
+ * Add or remove message listeners for all XUL windows.
+ *
+ * @param shouldListen boolean
+ *    True if we should add message listeners; false if we should remove them.
+ * @param guard string
+ *    The name of a guard property of 'this', indicating whether we're
+ *    already listening for those messages.
+ * @param messageNames array of strings
+ *    An array of message names.
  */
-BrowserTabList.prototype._onPageTitleChangedEvent = function(event) {
+BrowserTabList.prototype._listenForMessagesIf = function(
+  shouldListen,
+  guard,
+  messageNames
+) {
+  if (!shouldListen !== !this[guard]) {
+    const op = shouldListen ? "addMessageListener" : "removeMessageListener";
+    for (const win of Services.wm.getEnumerator(
+      DevToolsServer.chromeWindowType
+    )) {
+      for (const name of messageNames) {
+        win.messageManager[op](name, this);
+      }
+    }
+    this[guard] = shouldListen;
+  }
+};
+
+/*
+ * This function assumes to be used as a event listener for Android document.
+ */
+BrowserTabList.prototype._onAndroidDocumentEvent = function(event) {
   switch (event.type) {
-    case "pagetitlechanged": {
+    case "DOMTitleChanged": {
       const window = event.currentTarget.ownerGlobal;
       this._onDOMTitleChanged(window.browser);
       break;
     }
   }
 };
 
 /**
+ * Implement nsIMessageListener.
+ */
+BrowserTabList.prototype.receiveMessage = DevToolsUtils.makeInfallible(function(
+  message
+) {
+  const browser = message.target;
+  switch (message.name) {
+    case "DOMTitleChanged": {
+      this._onDOMTitleChanged(browser);
+      break;
+    }
+  }
+});
+
+/**
  * Handle "DOMTitleChanged" event.
  */
 BrowserTabList.prototype._onDOMTitleChanged = DevToolsUtils.makeInfallible(
   function(browser) {
     const actor = this._actorByBrowser.get(browser);
     if (actor) {
       this._notifyListChanged();
       this._checkListening();
@@ -541,17 +618,18 @@ BrowserTabList.prototype._onDOMTitleChan
 
 /**
  * Implement nsIDOMEventListener.
  */
 BrowserTabList.prototype.handleEvent = DevToolsUtils.makeInfallible(function(
   event
 ) {
   // If event target has `linkedBrowser`, the event target can be assumed <tab> element.
-  // Else, event target is assumed <browser> element, use the target as it is.
+  // Else (in Android case), because event target is assumed <browser> element,
+  // use the target as it is.
   const browser = event.target.linkedBrowser || event.target;
   switch (event.type) {
     case "TabOpen":
     case "TabSelect": {
       /* Don't create a new actor; iterate will take care of that. Just notify. */
       this._notifyListChanged();
       this._checkListening();
       break;
--- a/dom/chrome-webidl/WindowGlobalActors.webidl
+++ b/dom/chrome-webidl/WindowGlobalActors.webidl
@@ -48,17 +48,16 @@ interface WindowGlobalParent : WindowCon
   readonly attribute FrameLoader? rootFrameLoader; // Embedded (browser) only
 
   readonly attribute WindowGlobalChild? childActor; // in-process only
 
   // Information about the currently loaded document.
   readonly attribute Principal documentPrincipal;
   readonly attribute Principal? contentBlockingAllowListPrincipal;
   readonly attribute URI? documentURI;
-  readonly attribute DOMString documentTitle;
 
   // Bit mask containing content blocking events that are recorded in
   // the document's content blocking log.
   readonly attribute unsigned long contentBlockingEvents;
 
   // String containing serialized content blocking log.
   readonly attribute DOMString contentBlockingLog;
 
--- a/dom/ipc/WindowGlobalParent.cpp
+++ b/dom/ipc/WindowGlobalParent.cpp
@@ -1,17 +1,16 @@
 /* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
 /* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/WindowGlobalParent.h"
 
-#include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/ipc/InProcessParent.h"
 #include "mozilla/dom/BrowserBridgeParent.h"
 #include "mozilla/dom/CanonicalBrowsingContext.h"
 #include "mozilla/dom/ClientInfo.h"
 #include "mozilla/dom/ClientIPCTypes.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/BrowserHost.h"
@@ -275,37 +274,17 @@ IPCResult WindowGlobalParent::RecvUpdate
                     "Trying to reuse WindowGlobalParent but the principal of "
                     "the new document does not match the old one");
   }
   mDocumentPrincipal = aNewDocumentPrincipal;
   return IPC_OK();
 }
 mozilla::ipc::IPCResult WindowGlobalParent::RecvUpdateDocumentTitle(
     const nsString& aTitle) {
-  if (mDocumentTitle == aTitle) {
-    return IPC_OK();
-  }
-
   mDocumentTitle = aTitle;
-
-  // Send a pagetitlechanged event only for changes to the title
-  // for top-level frames.
-  if (!BrowsingContext()->IsTop()) {
-    return IPC_OK();
-  }
-
-  Element* frameElement = BrowsingContext()->GetEmbedderElement();
-  if (!frameElement) {
-    return IPC_OK();
-  }
-
-  (new AsyncEventDispatcher(frameElement, NS_LITERAL_STRING("pagetitlechanged"),
-                            CanBubble::eYes, ChromeOnlyDispatch::eYes))
-      ->RunDOMEventWhenSafe();
-
   return IPC_OK();
 }
 
 IPCResult WindowGlobalParent::RecvUpdateDocumentHasLoaded(
     bool aDocumentHasLoaded) {
   mDocumentHasLoaded = aDocumentHasLoaded;
   return IPC_OK();
 }
--- a/dom/ipc/WindowGlobalParent.h
+++ b/dom/ipc/WindowGlobalParent.h
@@ -115,17 +115,17 @@ class WindowGlobalParent final : public 
   // which this WindowGlobal is a part of. This will be the nsFrameLoader
   // holding the BrowserParent for remote tabs, and the root content frameloader
   // for non-remote tabs.
   already_AddRefed<nsFrameLoader> GetRootFrameLoader();
 
   // The current URI which loaded in the document.
   nsIURI* GetDocumentURI() override { return mDocumentURI; }
 
-  void GetDocumentTitle(nsAString& aTitle) const { aTitle = mDocumentTitle; }
+  const nsString& GetDocumentTitle() const { return mDocumentTitle; }
 
   nsIPrincipal* GetContentBlockingAllowListPrincipal() const {
     return mDocContentBlockingAllowListPrincipal;
   }
 
   Maybe<ClientInfo> GetClientInfo() { return mClientInfo; }
 
   uint64_t ContentParentId();
--- a/dom/media/mediacontrol/MediaStatusManager.cpp
+++ b/dom/media/mediacontrol/MediaStatusManager.cpp
@@ -176,17 +176,17 @@ nsString MediaStatusManager::GetDefaultT
       nsCString appName;
       appInfo->GetName(appName);
       CopyUTF8toUTF16(appName, defaultTitle);
     } else {
       defaultTitle.AssignLiteral("Firefox");
     }
     defaultTitle.AppendLiteral(" is playing media");
   } else {
-    globalParent->GetDocumentTitle(defaultTitle);
+    defaultTitle = globalParent->GetDocumentTitle();
   }
   return defaultTitle;
 }
 
 nsString MediaStatusManager::GetDefaultFaviconURL() const {
 #ifdef MOZ_PLACES
   nsCOMPtr<nsIURI> faviconURI;
   nsresult rv = NS_NewURI(getter_AddRefs(faviconURI),
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -21,16 +21,42 @@ try {
 }
 
 // This message is used to measure content process startup performance in Talos
 // tests.
 sendAsyncMessage("Content:BrowserChildReady", {
   time: Services.telemetry.msSystemNow(),
 });
 
+addEventListener(
+  "DOMTitleChanged",
+  function(aEvent) {
+    if (
+      !aEvent.isTrusted ||
+      // Check that we haven't been closed (DOM code dispatches this event
+      // asynchronously).
+      content.closed
+    ) {
+      return;
+    }
+    // Ensure `docShell.document` (an nsIWebNavigation idl prop) is there:
+    docShell.QueryInterface(Ci.nsIWebNavigation);
+    if (
+      // Check that the document whose title changed is the toplevel document,
+      // rather than a subframe, and check that it is still the current
+      // document in the docshell - we may have started loading another one.
+      docShell.document != aEvent.target
+    ) {
+      return;
+    }
+    sendAsyncMessage("DOMTitleChanged", { title: content.document.title });
+  },
+  false
+);
+
 // This is here for now until we find a better way of forcing an about:blank load
 // with a particular principal that doesn't involve the message manager. We can't
 // do this with JS Window Actors for now because JS Window Actors are tied to the
 // document principals themselves, so forcing the load with a new principal is
 // self-destructive in that case.
 addMessageListener("BrowserElement:CreateAboutBlank", message => {
   if (!content.document || content.document.documentURI != "about:blank") {
     throw new Error("Can't create a content viewer unless on about:blank");
--- a/toolkit/content/widgets/browser-custom-element.js
+++ b/toolkit/content/widgets/browser-custom-element.js
@@ -326,16 +326,18 @@
       this._innerWindowID = null;
 
       this._lastSearchString = null;
 
       this._remoteWebNavigation = null;
 
       this._remoteWebProgress = null;
 
+      this._contentTitle = "";
+
       this._characterSet = "";
 
       this._mayEnableCharacterEncodingMenu = null;
 
       this._charsetAutodetected = false;
 
       this._contentPrincipal = null;
 
@@ -723,17 +725,17 @@
     }
 
     get sessionHistory() {
       return this.webNavigation.sessionHistory;
     }
 
     get contentTitle() {
       return this.isRemoteBrowser
-        ? this.browsingContext.currentWindowGlobal?.documentTitle
+        ? this._contentTitle
         : this.contentDocument.title;
     }
 
     set characterSet(val) {
       if (this.isRemoteBrowser) {
         this.sendMessageToActor(
           "UpdateCharacterSet",
           { value: val },
@@ -1213,16 +1215,17 @@
           aboutBlank,
           this.loadContext
         );
         // CSP for about:blank is null; if we ever change _contentPrincipal above,
         // we should re-evaluate the CSP here.
         this._csp = null;
 
         this.messageManager.addMessageListener("Browser:Init", this);
+        this.messageManager.addMessageListener("DOMTitleChanged", this);
 
         let jsm = "resource://gre/modules/RemoteWebProgress.jsm";
         let { RemoteWebProgressManager } = ChromeUtils.import(jsm, {});
 
         let oldManager = this._remoteWebProgressManager;
         this._remoteWebProgressManager = new RemoteWebProgressManager(this);
         if (oldManager) {
           // We're transitioning from one remote type to another. This means that
@@ -1338,16 +1341,19 @@
 
     receiveMessage(aMessage) {
       if (this.isRemoteBrowser) {
         const data = aMessage.data;
         switch (aMessage.name) {
           case "Browser:Init":
             this._outerWindowID = data.outerWindowID;
             break;
+          case "DOMTitleChanged":
+            this._contentTitle = data.title;
+            break;
           default:
             break;
         }
       }
     }
 
     updateSecurityUIForSecurityChange(aSecurityInfo, aState, aIsSecureContext) {
       if (this.isRemoteBrowser && this.messageManager) {
@@ -1411,16 +1417,17 @@
         }
 
         if (aContentType != null) {
           this._documentContentType = aContentType;
         }
 
         this._remoteWebNavigation._currentURI = aLocation;
         this._documentURI = aDocumentURI;
+        this._contentTitle = aTitle;
         this._contentPrincipal = aContentPrincipal;
         this._contentStoragePrincipal = aContentStoragePrincipal;
         this._csp = aCSP;
         this._referrerInfo = aReferrerInfo;
         this._isSyntheticDocument = aIsSynthetic;
         this._innerWindowID = aInnerWindowID;
         this._contentRequestContextID = aHaveRequestContextID
           ? aRequestContextID
@@ -1805,16 +1812,17 @@
           ...[
             "_remoteWebNavigation",
             "_remoteWebProgressManager",
             "_remoteWebProgress",
             "_remoteFinder",
             "_securityUI",
             "_documentURI",
             "_documentContentType",
+            "_contentTitle",
             "_characterSet",
             "_mayEnableCharacterEncodingMenu",
             "_charsetAutodetected",
             "_contentPrincipal",
             "_contentStoragePrincipal",
             "_isSyntheticDocument",
             "_innerWindowID",
           ]