--- 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();
}