Bug 1463431 - [WIP] Display a flame-like animation when a tab uses a lot of resources draft
authorTarek Ziadé <tarek@mozilla.com>
Mon, 28 May 2018 15:36:34 +0200
changeset 800575 43503c301a819fa5f32c7fccbb1f15f4405e1bf3
parent 800457 14457dcb99937801b5adf5956fdf67f5a921fb3e
push id111405
push usertziade@mozilla.com
push dateMon, 28 May 2018 14:25:50 +0000
bugs1463431
milestone62.0a1
Bug 1463431 - [WIP] Display a flame-like animation when a tab uses a lot of resources MozReview-Commit-ID: DeRnFbvVuVY
browser/base/content/tabbrowser.js
browser/themes/shared/tabs.inc.css
dom/base/ChromeUtils.cpp
dom/base/ChromeUtils.h
dom/chrome-webidl/ChromeUtils.webidl
dom/ipc/ContentChild.cpp
dom/ipc/ContentChild.h
dom/ipc/PContent.ipdl
dom/tests/browser/browser_test_performance_metrics.js
toolkit/components/perfmonitoring/PerformanceUtils.cpp
toolkit/components/perfmonitoring/PerformanceUtils.h
toolkit/content/browser-child.js
toolkit/xre/nsAppRunner.cpp
xpcom/tests/gtest/TestThreadMetrics.cpp
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -32,16 +32,18 @@ window._gBrowser = {
     } else if (Services.prefs.getIntPref("browser.display.document_color_use") == 2) {
       this.tabpanels.style.backgroundColor =
         Services.prefs.getCharPref("browser.display.background_color");
     }
 
     let messageManager = window.getGroupMessageManager("browsers");
     if (gMultiProcessBrowser) {
       messageManager.addMessageListener("DOMTitleChanged", this);
+      messageManager.addMessageListener("performance-issue-resolved", this);
+      messageManager.addMessageListener("performance-issue-detected", this);
       messageManager.addMessageListener("DOMWindowClose", this);
       window.messageManager.addMessageListener("contextmenu", this);
       messageManager.addMessageListener("Browser:Init", this);
 
       // If this window has remote tabs, switch to our tabpanels fork
       // which does asynchronous tab switching.
       this.tabpanels.classList.add("tabbrowser-tabpanels");
     } else {
@@ -988,16 +990,17 @@ window._gBrowser = {
           oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
           !oldFindBar.hidden)
         this._lastFindValue = oldFindBar._findField.value;
 
       this.updateTitlebar();
 
       newTab.removeAttribute("titlechanged");
       newTab.removeAttribute("attention");
+      newTab.removeAttribute("greedy");
 
       // The tab has been selected, it's not unselected anymore.
       // (1) Call the current tab's finishUnselectedTabHoverTimer()
       //     to save a telemetry record.
       // (2) Call the current browser's unselectedTabHover() with false
       //     to dispatch an event.
       newTab.finishUnselectedTabHoverTimer();
       newBrowser.unselectedTabHover(false);
@@ -3852,28 +3855,45 @@ window._gBrowser = {
         }
         break;
     }
   },
 
   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 "performance-issue-detected":
+      {
+        let tab = this.getTabForBrowser(browser);
+        if (!tab) {
+          return undefined;
+         }
+        tab.setAttribute("greedy", "true");
+        break;
+      }
+      case "performance-issue-resolved":
+      {
+        let tab = this.getTabForBrowser(browser);
+        if (!tab) {
+          return undefined;
+        }
+        tab.removeAttribute("greedy");
+        break;
+      }
       case "DOMWindowClose":
       {
         if (this.tabs.length == 1) {
           // We already did PermitUnload in the content process
           // for this tab (the only one in the window). So we don't
           // need to do it again for any tabs.
           window.skipNextCanClose = true;
           window.close();
@@ -4053,16 +4073,18 @@ window._gBrowser = {
       Services.els.removeSystemEventListener(document, "keypress", this, false);
     }
     window.removeEventListener("sizemodechange", this);
     window.removeEventListener("occlusionstatechange", this);
 
     if (gMultiProcessBrowser) {
       let messageManager = window.getGroupMessageManager("browsers");
       messageManager.removeMessageListener("DOMTitleChanged", this);
+      messageManager.removeMessageListener("performance-issue-resolved", this);
+      messageManager.removeMessageListener("performance-issue-detected", this);
       window.messageManager.removeMessageListener("contextmenu", this);
 
       if (this._switcher) {
         this._switcher.destroy();
       }
     }
   },
 
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -534,16 +534,17 @@
   background-image: var(--toolbar-bgimage);
   background-repeat: repeat-x;
 }
 
 .tab-line[selected=true] {
   background-color: var(--tab-line-color);
 }
 
+
 /*
  * LightweightThemeConsumer will set the current lightweight theme's header
  * image to the lwt-header-image variable, used in each of the following rulesets.
  */
 
 /* Lightweight theme on tabs */
 #tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab > .tab-stack > .tab-background[selected=true]:-moz-lwtheme {
   background-attachment: scroll, scroll, fixed;
@@ -603,16 +604,17 @@
 
 .tabbrowser-tab:-moz-any([image], [pinned]) > .tab-stack > .tab-content[attention]:not([selected="true"]),
 .tabbrowser-tab > .tab-stack > .tab-content[pinned][titlechanged]:not([selected="true"]) {
   background-image: url(chrome://browser/skin/tabbrowser/indicator-tab-attention.svg);
   background-position: center bottom calc(-4px + @navbarTabsShadowSize@);
   background-repeat: no-repeat;
 }
 
+
 .tabbrowser-tab[image] > .tab-stack > .tab-content[attention]:not([pinned]):not([selected="true"]) {
   background-position-x: left 11px;
 }
 
 .tabbrowser-tab[image] > .tab-stack > .tab-content[attention]:not([pinned]):not([selected="true"]):-moz-locale-dir(rtl) {
   background-position-x: right 11px;
 }
 
@@ -766,8 +768,32 @@
 
 .alltabs-endimage[muted] {
   list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio-muted.svg);
 }
 
 .alltabs-endimage[activemedia-blocked] {
   list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio-blocked.svg);
 }
+
+
+@keyframes pulse {
+  0% {
+    background: radial-gradient(ellipse at bottom, #ff6961 5%, transparent 100%);
+  }
+  25% {
+    background: radial-gradient(ellipse at bottom, #ff6961 25%, transparent 100%);
+  }
+  50% {
+    background: radial-gradient(ellipse at bottom, #ff6961 50%, transparent 100%);
+  }
+  75% {
+    background: radial-gradient(ellipse at bottom, #ff6961 75%, transparent 100%);
+  }
+  100% {
+    background: radial-gradient(ellipse at bottom, #ff6961, #ff6961);
+  }
+}
+
+
+.tabbrowser-tab[greedy] > .tab-stack > .tab-background {
+  animation: pulse 2s ease-in infinite alternate;
+}
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -662,26 +662,38 @@ ChromeUtils::RequestPerformanceMetrics(G
 
   // calling all content processes via IPDL (async)
   nsTArray<ContentParent*> children;
   ContentParent::GetAll(children);
   for (uint32_t i = 0; i < children.Length(); i++) {
     mozilla::Unused << children[i]->SendRequestPerformanceMetrics();
   }
 
-
   // collecting the current process counters and notifying them
   nsTArray<PerformanceInfo> info;
   CollectPerformanceInfo(info);
   SystemGroup::Dispatch(TaskCategory::Performance,
     NS_NewRunnableFunction(
       "RequestPerformanceMetrics",
       [info]() { mozilla::Unused << NS_WARN_IF(NS_FAILED(NotifyPerformanceInfo(info))); }
     )
   );
+}
+
+/* static */ void
+ChromeUtils::StartPerformanceWatcher(GlobalObject&)
+{
+  MOZ_ASSERT(XRE_IsParentProcess());
+
+  // XXX here we will allow passing options to tweak the PerformanceWatcher
+  // behavior.
+  RefPtr<PerformanceWatcher> watcher = PerformanceWatcher::Get();
+  if (!watcher) {
+    return;
+  }
 
 }
 
 constexpr auto kSkipSelfHosted = JS::SavedFrameSelfHosted::Exclude;
 
 /* static */ void
 ChromeUtils::GetCallerLocation(const GlobalObject& aGlobal, nsIPrincipal* aPrincipal,
                                JS::MutableHandle<JSObject*> aRetval)
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -156,16 +156,17 @@ public:
 
   static void GetRecentJSDevError(GlobalObject& aGlobal,
                                   JS::MutableHandleValue aRetval,
                                   ErrorResult& aRv);
 
   static void ClearRecentJSDevError(GlobalObject& aGlobal);
 
   static void RequestPerformanceMetrics(GlobalObject& aGlobal);
+  static void StartPerformanceWatcher(GlobalObject& aGlobal);
 
   static void Import(const GlobalObject& aGlobal,
                      const nsAString& aResourceURI,
                      const Optional<JS::Handle<JSObject*>>& aTargetObj,
                      JS::MutableHandle<JSObject*> aRetval,
                      ErrorResult& aRv);
 
   static void DefineModuleGetter(const GlobalObject& global,
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -343,16 +343,17 @@ partial namespace ChromeUtils {
    */
   [Throws]
   object createError(DOMString message, optional object? stack = null);
 
   /**
    * Request performance metrics to the current process & all ontent processes.
    */
   void requestPerformanceMetrics();
+  void startPerformanceWatcher();
 };
 
 /**
  * Used by principals and the script security manager to represent origin
  * attributes. The first dictionary is designed to contain the full set of
  * OriginAttributes, the second is used for pattern-matching (i.e. does this
  * OriginAttributesDictionary match the non-empty attributes in this pattern).
  *
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -1389,16 +1389,24 @@ ContentChild::RecvRequestPerformanceMetr
   MOZ_ASSERT(mozilla::dom::DOMPrefs::SchedulerLoggingEnabled());
   nsTArray<PerformanceInfo> info;
   CollectPerformanceInfo(info);
   SendAddPerformanceMetrics(info);
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+ContentChild::RecvNotifyPerformanceStatus(const uint64_t& aWid,
+                                          const bool& aNormal)
+{
+  TogglePerformanceStatus(aWid, aNormal);
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 ContentChild::RecvInitRendering(Endpoint<PCompositorManagerChild>&& aCompositor,
                                 Endpoint<PImageBridgeChild>&& aImageBridge,
                                 Endpoint<PVRManagerChild>&& aVRBridge,
                                 Endpoint<PVideoDecoderManagerChild>&& aVideoManager,
                                 nsTArray<uint32_t>&& namespaces)
 {
   MOZ_ASSERT(namespaces.Length() == 3);
 
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -189,16 +189,20 @@ public:
     Endpoint<PVRManagerChild>&& aVRBridge,
     Endpoint<PVideoDecoderManagerChild>&& aVideoManager,
     nsTArray<uint32_t>&& namespaces) override;
 
   mozilla::ipc::IPCResult
   RecvRequestPerformanceMetrics() override;
 
   mozilla::ipc::IPCResult
+  RecvNotifyPerformanceStatus(const uint64_t& aWid,
+                              const bool& aNormal) override;
+
+  mozilla::ipc::IPCResult
   RecvReinitRendering(
     Endpoint<PCompositorManagerChild>&& aCompositor,
     Endpoint<PImageBridgeChild>&& aImageBridge,
     Endpoint<PVRManagerChild>&& aVRBridge,
     Endpoint<PVideoDecoderManagerChild>&& aVideoManager,
     nsTArray<uint32_t>&& namespaces) override;
 
   virtual mozilla::ipc::IPCResult RecvAudioDefaultDeviceChange() override;
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -397,16 +397,19 @@ child:
      */
     async SetProcessSandbox(MaybeFileDesc aBroker);
 
     async RequestMemoryReport(uint32_t generation,
                               bool anonymize,
                               bool minimizeMemoryUsage,
                               MaybeFileDesc DMDFile);
     async RequestPerformanceMetrics();
+    async NotifyPerformanceStatus(uint64_t wid,
+                                  bool normal);
+
 
     /**
      * Communication between the PuppetBidiKeyboard and the actual
      * BidiKeyboard hosted by the parent
      */
     async BidiKeyboardNotify(bool isLangRTL, bool haveBidiKeyboards);
 
     /**
--- a/dom/tests/browser/browser_test_performance_metrics.js
+++ b/dom/tests/browser/browser_test_performance_metrics.js
@@ -34,17 +34,17 @@ function jsonrpc(tab, method, params) {
     });
   });
 }
 
 function postMessageToWorker(tab, message) {
   return jsonrpc(tab, "postMessageToWorker", [WORKER_URL, message]);
 }
 
-add_task(async function test() {
+add_task(async function test_get_metrics() {
   SpecialPowers.setBoolPref('dom.performance.enable_scheduler_timing', true);
   waitForExplicitFinish();
 
   // Load 3 pages and wait. The 3rd one has a worker
   let page1 = await BrowserTestUtils.openNewForegroundTab({
     gBrowser, opening: 'about:about', forceNewProcess: false
   });
 
@@ -122,8 +122,57 @@ add_task(async function test() {
     Assert.ok(worker_total > 0, "Worker count should be positive");
     Assert.ok(duration > 0, "Duration should be positive");
     Assert.ok(total > 0, "Should get a positive count");
     Assert.ok(parent_process_event, "parent process sent back some events");
   });
 
   SpecialPowers.clearUserPref('dom.performance.enable_scheduler_timing');
 });
+
+add_task(async function test_notify_tab() {
+  SpecialPowers.setBoolPref('dom.performance.enable_scheduler_timing', true);
+  waitForExplicitFinish();
+
+  // Load 3 pages and wait. The 3rd one has a worker
+  let page1 = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser, opening: 'about:about', forceNewProcess: false
+  });
+
+  let page2 = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser, opening: 'about:memory', forceNewProcess: false
+  });
+
+  let page3 = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser, opening: "about:performance", forceNewProcess: true
+  });
+
+  // load a 4th tab with a worker
+  await BrowserTestUtils.withNewTab({ gBrowser, url: WORKER_URL },
+    async function(browser) {
+    // grab issues..
+    var detected = [];
+    var resolved = [];
+
+    function issueDetected(subject, topic, value) {
+      detected.push(value);
+    }
+
+    function issueResolved(subject, topic, value) {
+      resolved.push(value);
+    }
+
+    Services.obs.addObserver(issueDetected, "performance-issue-detected");
+    Services.obs.addObserver(issueResolved, "performance-issue-resolved");
+
+    // wait until we get some events back by starting the performance watcher
+    await BrowserTestUtils.waitForCondition(() => {
+      ChromeUtils.startPerformanceWatcher();
+      return detected.length > 200;
+    }, "wait for issues to get triggered", 500, 10);
+
+  });
+
+  BrowserTestUtils.removeTab(page1);
+  BrowserTestUtils.removeTab(page2);
+  BrowserTestUtils.removeTab(page3);
+  SpecialPowers.clearUserPref('dom.performance.enable_scheduler_timing');
+});
--- a/toolkit/components/perfmonitoring/PerformanceUtils.cpp
+++ b/toolkit/components/perfmonitoring/PerformanceUtils.cpp
@@ -1,29 +1,62 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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 "nsIMutableArray.h"
-#include "nsPerformanceMetrics.h"
 #include "nsThreadUtils.h"
+#include "nsIWindowMediator.h"
+#include "mozilla/ClearOnShutdown.h"
 #include "mozilla/PerformanceUtils.h"
+#include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/TabChild.h"
 #include "mozilla/dom/WorkerDebugger.h"
 #include "mozilla/dom/WorkerDebuggerManager.h"
+#include "nsArrayUtils.h"
+
+#include "mozilla/Logging.h"
+static mozilla::LazyLogModule sPerformanceWatcher("PerformanceWatcher");
+#ifdef LOG
+#undef LOG
+#endif
+#define LOG(args) MOZ_LOG(sPerformanceWatcher, mozilla::LogLevel::Debug, args)
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 namespace mozilla {
 
 void
+TogglePerformanceStatus(uint64_t aWid, bool aNormal)
+{
+  // looking for the window aWid
+  RefPtr<nsGlobalWindowOuter> window = nsGlobalWindowOuter::GetOuterWindowWithId(aWid);
+  if (window) {
+    nsCOMPtr<nsIDocument> doc = window->GetDoc();
+    nsString event;
+    if (aNormal) {
+      event = NS_LITERAL_STRING("performance-issue-resolved");
+    } else {
+      event = NS_LITERAL_STRING("performance-issue-detected");
+    }
+
+    nsresult rv = nsContentUtils::DispatchChromeEvent(doc, static_cast<nsIDocument*>(doc),
+                                        event, true, false);
+    if (NS_FAILED(rv)) {
+      LOG(("Dispatching chrome event failed"));
+    }
+  }
+}
+
+
+void
 CollectPerformanceInfo(nsTArray<PerformanceInfo>& aMetrics)
 {
   // collecting ReportPerformanceInfo from all DocGroup instances
   for (const auto& tabChild : TabChild::GetAll()) {
     TabGroup* tabGroup = tabChild->TabGroup();
     for (auto iter = tabGroup->Iter(); !iter.Done(); iter.Next()) {
       DocGroup* docGroup = iter.Get()->mDocGroup;
       aMetrics.AppendElement(docGroup->ReportPerformanceInfo());
@@ -36,16 +69,39 @@ CollectPerformanceInfo(nsTArray<Performa
     return;
   }
   for (uint32_t i = 0; i < wdm->GetDebuggersLength(); i++) {
     WorkerDebugger* debugger = wdm->GetDebuggerAt(i);
     aMetrics.AppendElement(debugger->ReportPerformanceInfo());
   }
 }
 
+
+void
+RequestPerformanceInfo()
+{
+  // calling all content processes via IPDL (async)
+  nsTArray<ContentParent*> children;
+  ContentParent::GetAll(children);
+  for (uint32_t i = 0; i < children.Length(); i++) {
+    mozilla::Unused << children[i]->SendRequestPerformanceMetrics();
+  }
+
+  // collecting the current process counters and notifying them
+  nsTArray<PerformanceInfo> info;
+  CollectPerformanceInfo(info);
+  SystemGroup::Dispatch(TaskCategory::Performance,
+    NS_NewRunnableFunction(
+      "RequestPerformanceInfo",
+      [info]() { mozilla::Unused << NS_WARN_IF(NS_FAILED(NotifyPerformanceInfo(info))); }
+    )
+  );
+}
+
+
 nsresult
 NotifyPerformanceInfo(const nsTArray<PerformanceInfo>& aMetrics)
 {
   nsresult rv;
 
   nsCOMPtr<nsIMutableArray> array = do_CreateInstance(NS_ARRAY_CONTRACTID);
   if (NS_WARN_IF(!array)) {
     return NS_ERROR_FAILURE;
@@ -82,9 +138,229 @@ NotifyPerformanceInfo(const nsTArray<Per
 
   rv = obs->NotifyObservers(array, "performance-metrics", nullptr);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
   return NS_OK;
 }
 
+/**
+ * Class PerformanceWatcher
+ */
+
+NS_IMPL_ISUPPORTS(PerformanceWatcher, nsIObserver, nsITimerCallback);
+
+StaticMutex PerformanceWatcher::sMutex;
+StaticRefPtr<PerformanceWatcher> PerformanceWatcher::sInstance;
+bool PerformanceWatcher::sInShutdown = false;
+
+
+already_AddRefed<PerformanceWatcher>
+PerformanceWatcher::Get()
+{
+  MOZ_ASSERT(XRE_IsContentProcess() || XRE_IsParentProcess());
+
+  if (sInShutdown) {
+    return nullptr;
+  }
+
+  static bool firstTime = true;
+  if (firstTime) {
+    firstTime = false;
+    StaticMutexAutoLock lock(sMutex);
+    sInstance = new PerformanceWatcher();
+
+    if (sInstance->Init()) {
+      ClearOnShutdown(&sInstance);
+    } else {
+      sInstance = nullptr;
+    }
+  }
+  RefPtr<PerformanceWatcher> copy = sInstance.get();
+  return copy.forget();
+}
+
+PerformanceWatcher::~PerformanceWatcher()
+{
+  if (mTimer) {
+    mozilla::Unused << mTimer->Cancel();
+    mTimer = nullptr;
+  }
+}
+
+bool
+PerformanceWatcher::Init()
+{
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (NS_WARN_IF(!obs)) {
+    return false;
+  }
+  if (NS_WARN_IF(NS_FAILED(
+    obs->AddObserver(this, "performance-metrics", false)))) {
+    return false;
+  }
+  // collecting every 5s
+  mTimer = NS_NewTimer();
+  nsresult rv = mTimer->InitWithCallback(this, 5000, nsITimer::TYPE_REPEATING_SLACK);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return false;
+  }
+  return true;
+}
+
+
+NS_IMETHODIMP
+PerformanceWatcher::Notify(nsITimer *timer)
+{
+  RequestPerformanceInfo();
+  return NS_OK;
+}
+
+uint32_t
+PerformanceWatcher::IssueDetection(nsIPerformanceMetricsData* aMetrics)
+{
+  // basic issue detection for the demo, we'll do better later
+  // with something that keeps tracks of the tab behavior over time
+  // here, if the number of sheduler calls is more than 20, we're detecting it
+  // as an issue.
+  nsCString host;
+  aMetrics->GetHost(host);
+  LOG(("Issue detection on %s", host.get()));
+
+  uint64_t pwid = 0;
+  aMetrics->GetPwid(&pwid);
+  bool windowHasAlreadyIssue = mWindowHasIssue.Get(pwid);
+  bool isWorker;
+
+  aMetrics->GetWorker(&isWorker);
+
+  if (isWorker) {
+    LOG(("This is a worker"));
+    // we look for the execution time of the worker in the last 5 s
+    uint64_t duration = 0;
+    aMetrics->GetDuration(&duration);
+    LOG(("Duration (microseconds) is %" PRIu64, duration));
+    if (duration > 1000) {
+      if (!windowHasAlreadyIssue) {
+        LOG(("Issue detected!"));
+        mWindowHasIssue.Put(pwid, true);
+        return PERFWATCHER_ISSUE_DETECTED;
+      }
+      LOG(("Issue still going on!"));
+      return PERFWATCHER_ISSUE_STILL_ON;
+    }
+    if (windowHasAlreadyIssue) {
+      LOG(("Issue resolved!"));
+      mWindowHasIssue.Put(pwid, false);
+      return PERFWATCHER_ISSUE_RESOLVED;
+    }
+    LOG(("No issue"));
+    return PERFWATCHER_NO_ISSUE;
+  }
+
+  nsCOMPtr<nsIArray> items;
+  aMetrics->GetItems(getter_AddRefs(items));
+  if (!items) {
+    if (windowHasAlreadyIssue) {
+      LOG(("Issue resolved!"));
+      mWindowHasIssue.Put(pwid, false);
+      return PERFWATCHER_ISSUE_RESOLVED;
+    }
+    LOG(("No issue"));
+    return PERFWATCHER_NO_ISSUE;
+  }
+  uint32_t total = 0;
+  uint32_t itemsLength = 0;
+  uint32_t count = 0;
+  items->GetLength(&itemsLength);
+  for (uint32_t i = 0; i < itemsLength; ++i) {
+    nsCOMPtr<nsIPerformanceMetricsDispatchCategory> item = do_QueryElementAt(items, i);
+    item->GetCount(&count);
+    total += count;
+  }
+  LOG(("Total is %d", total));
+  if (total > 70) {
+    if (!windowHasAlreadyIssue) {
+      LOG(("Issue detected"));
+      mWindowHasIssue.Put(pwid, true);
+      return PERFWATCHER_ISSUE_DETECTED;
+    }
+    LOG(("Issue still going on!"));
+    return PERFWATCHER_ISSUE_STILL_ON;
+  }
+  if (windowHasAlreadyIssue) {
+    LOG(("Issue resolved"));
+    mWindowHasIssue.Put(pwid, false);
+    return PERFWATCHER_ISSUE_RESOLVED;
+  }
+
+  LOG(("No Issue"));
+  return PERFWATCHER_NO_ISSUE;
+}
+
+void
+PerformanceWatcher::NotifyTab(uint64_t aWid, bool aStatus)
+{
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (NS_WARN_IF(!obs)) {
+    return;
+  }
+  nsAutoString swid;
+  swid.AppendPrintf("%" PRIu64, aWid);
+    // notify observers
+  if (aStatus) {
+    LOG(("RESOLVED"));
+    obs->NotifyObservers(nullptr, "performance-issue-resolved", swid.get());
+  } else {
+    LOG(("DETECTED"));
+    obs->NotifyObservers(nullptr, "performance-issue-detected", swid.get());
+  }
+
+  // Toggling the status if it's a window in the same process
+  TogglePerformanceStatus(aWid, aStatus);
+
+  // calling all content processes via IPDL (async)
+  nsTArray<ContentParent*> children;
+  ContentParent::GetAll(children);
+  for (uint32_t i = 0; i < children.Length(); i++) {
+    mozilla::Unused << children[i]->SendNotifyPerformanceStatus(aWid, aStatus);
+  }
+}
+
+nsresult
+PerformanceWatcher::Observe(nsISupports* aSubject,
+                            const char* aTopic,
+                            const char16_t* aData)
+{
+  nsCOMPtr<nsIArray> array(do_QueryInterface(aSubject));
+  MOZ_ASSERT(array);
+  uint32_t len = 0;
+  array->GetLength(&len);
+  for (uint32_t i = 0; i < len; i++) {
+    nsCOMPtr<nsIPerformanceMetricsData> metrics;
+    array->QueryElementAt(i, NS_GET_IID(nsIPerformanceMetricsData),
+                          getter_AddRefs(metrics));
+    MOZ_ASSERT(metrics);
+    if (!metrics) {
+      continue;
+    }
+
+    uint64_t pwid = 0;
+    metrics->GetPwid(&pwid);
+
+    switch (IssueDetection(metrics)) {
+      case PERFWATCHER_ISSUE_DETECTED:
+        NotifyTab(pwid, false);
+        break;
+      case PERFWATCHER_ISSUE_RESOLVED:
+        NotifyTab(pwid, true);
+        break;
+      default:
+        break;
+    }
+
+  }
+
+  return NS_OK;
+}
+
 } // namespace
--- a/toolkit/components/perfmonitoring/PerformanceUtils.h
+++ b/toolkit/components/perfmonitoring/PerformanceUtils.h
@@ -2,25 +2,73 @@
 /* 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/. */
 
 #ifndef PerformanceCollector_h
 #define PerformanceCollector_h
 
 #include "mozilla/dom/DOMTypes.h"   // defines PerformanceInfo
+#include "nsIObserver.h"
+#include "nsPerformanceMetrics.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+
+#define PERFWATCHER_NO_ISSUE 0
+#define PERFWATCHER_ISSUE_DETECTED 1
+#define PERFWATCHER_ISSUE_RESOLVED 2
+#define PERFWATCHER_ISSUE_STILL_ON 3
 
 namespace mozilla {
 
+void TogglePerformanceStatus(uint64_t aWid, bool aNormal);
+
+void RequestPerformanceInfo();
+
 /**
  * Collects all performance info in the current process
- * and adds then in the aMetrics arrey
+ * and adds then in the aMetrics array
  */
 void CollectPerformanceInfo(nsTArray<dom::PerformanceInfo>& aMetrics);
 
 /**
  * Converts a PerformanceInfo array into a nsIPerformanceMetricsData and
  * sends a performance-metrics notification with it
  */
 nsresult NotifyPerformanceInfo(const nsTArray<dom::PerformanceInfo>& aMetrics);
 
+
+/**
+ * PerformanceWatcher singleton.
+ *
+ * Once initialized, receives performance-metrics
+ * and decides whether a window is acting weird.
+ *
+ * If it does, triggers events accordingly:
+ * - performance-issue-detected : a window starts to use a lot of resources
+ * - performance-issue-resolved: the window is back to normal
+ *
+ * These event can be used in the UI/UX to notify the user.
+ */
+class PerformanceWatcher final: public nsIObserver,
+                                public nsITimerCallback
+{
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSIOBSERVER
+  NS_DECL_NSITIMERCALLBACK
+  static already_AddRefed<PerformanceWatcher> Get();
+private:
+  PerformanceWatcher() {};
+  virtual ~PerformanceWatcher();
+  bool Init();
+  uint32_t IssueDetection(nsIPerformanceMetricsData* aMetrics);
+  void NotifyTab(uint64_t aWid, bool aStatus);
+  static StaticRefPtr<PerformanceWatcher> sInstance;
+  static bool sInShutdown;
+  static StaticMutex sMutex;
+  nsCOMPtr<nsITimer> mTimer;
+  nsDataHashtable<nsUint64HashKey, bool> mWindowHasIssue;
+};
+
+
 } // namespace mozilla
 #endif   // PerformanceCollector_h
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -423,20 +423,26 @@ var ControllerCommands = {
 ControllerCommands.init();
 
 addEventListener("DOMTitleChanged", function(aEvent) {
   if (!aEvent.isTrusted || aEvent.target.defaultView != content)
     return;
   sendAsyncMessage("DOMTitleChanged", { title: content.document.title });
 }, false);
 
-addEventListener("DOMWindowClose", function(aEvent) {
+addEventListener("performance-issue-detected", function(aEvent) {
   if (!aEvent.isTrusted)
     return;
-  sendAsyncMessage("DOMWindowClose");
+  sendAsyncMessage("performance-issue-detected", {});
+}, false);
+
+addEventListener("performance-issue-resolved", function(aEvent) {
+  if (!aEvent.isTrusted)
+    return;
+  sendAsyncMessage("performance-issue-resolved", {});
 }, false);
 
 addEventListener("ImageContentLoaded", function(aEvent) {
   if (content.document instanceof Ci.nsIImageDocument) {
     let req = content.document.imageRequest;
     if (!req.image)
       return;
     sendAsyncMessage("ImageDocumentLoaded", { width: req.image.width,
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -1,13 +1,14 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/PerformanceUtils.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/ipc/GeckoChildProcessHost.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/ChaosMode.h"
 #include "mozilla/CmdLineAndEnvUtils.h"
@@ -4718,16 +4719,18 @@ XREMain::XRE_mainRun()
   CrashReporter::AnnotateCrashReport(
     NS_LITERAL_CSTRING("ContentSandboxCapabilities"), flagsString);
 #endif /* MOZ_SANDBOX && XP_LINUX */
 
 #if defined(MOZ_CONTENT_SANDBOX)
   AddSandboxAnnotations();
 #endif /* MOZ_CONTENT_SANDBOX */
 
+  RefPtr<PerformanceWatcher> watcher = mozilla::PerformanceWatcher::Get();
+
   {
     rv = appStartup->Run();
     if (NS_FAILED(rv)) {
       NS_ERROR("failed to run appstartup");
       gLogConsoleErrors = true;
     }
   }
 
--- a/xpcom/tests/gtest/TestThreadMetrics.cpp
+++ b/xpcom/tests/gtest/TestThreadMetrics.cpp
@@ -130,21 +130,21 @@ protected:
   }
 
   // this is used to get rid of transient events
   void initScheduler() {
     ProcessAllEvents();
   }
 
   nsresult Dispatch(uint32_t aExecutionTime1, uint32_t aExecutionTime2,
-                    bool aRecursive) {
+                    uint32_t aSubExecutionTime) {
     ProcessAllEvents();
     nsCOMPtr<nsIRunnable> runnable = new TimedRunnable(aExecutionTime1,
                                                        aExecutionTime2,
-                                                       aRecursive);
+                                                       aSubExecutionTime);
     runnable = new SchedulerGroup::Runnable(runnable.forget(),
                                             mSchedulerGroup, mDocGroup);
     return mDocGroup->Dispatch(TaskCategory::Other, runnable.forget());
   }
 
   void ProcessAllEvents() {
     mThreadMgr->SpinEventLoopUntilEmpty();
   }