gecko-dev/dom/ipc/ProcessPriorityManager.cpp

351 lines
9.9 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=8 et 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/ipc/ProcessPriorityManager.h"
#include "mozilla/Hal.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/HalTypes.h"
#include "mozilla/TimeStamp.h"
#include "prlog.h"
#include "nsWeakPtr.h"
#include "nsXULAppAPI.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsITimer.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsIDocument.h"
#include "nsIDOMEventListener.h"
#include "nsIDOMWindow.h"
#include "nsIDOMEvent.h"
#include "nsIDOMEventTarget.h"
#include "nsIDOMDocument.h"
#include "nsPIDOMWindow.h"
#ifdef XP_WIN
#include <process.h>
#define getpid _getpid
#else
#include <unistd.h>
#endif
using namespace mozilla::hal;
namespace mozilla {
namespace dom {
namespace ipc {
namespace {
static bool sInitialized = false;
// Some header defines a LOG macro, but we don't want it here.
#ifdef LOG
#undef LOG
#endif
// Enable logging by setting
//
// NSPR_LOG_MODULES=ProcessPriorityManager:5
//
// in your environment.
#ifdef PR_LOGGING
static PRLogModuleInfo*
GetPPMLog()
{
static PRLogModuleInfo *sLog;
if (!sLog)
sLog = PR_NewLogModule("ProcessPriorityManager");
return sLog;
}
#define LOG(fmt, ...) \
PR_LOG(GetPPMLog(), PR_LOG_DEBUG, \
("[%d] ProcessPriorityManager - " fmt, getpid(), ##__VA_ARGS__))
#else
#define LOG(fmt, ...)
#endif
/**
* This class listens to window creation and visibilitychange events and
* informs the hal back-end when this process transitions between having no
* visible top-level windows, and when it has at least one visible top-level
* window.
*
*
* An important heuristic here is that we don't mark a process as background
* until it's had no visible top-level windows for some amount of time.
*
* We do this because the notion of visibility is tied to inner windows
* (actually, to documents). When we navigate a page with outer window W, we
* first destroy W's inner window and document, then insert a new inner window
* and document into W. If not for our grace period, this transition could
* cause us to inform hal that this process quickly transitioned from
* foreground to background to foreground again.
*
*/
class ProcessPriorityManager MOZ_FINAL
: public nsIObserver
, public nsIDOMEventListener
{
public:
ProcessPriorityManager();
void Init();
NS_DECL_ISUPPORTS
NS_DECL_NSIOBSERVER
NS_DECL_NSIDOMEVENTLISTENER
private:
void SetPriority(ProcessPriority aPriority);
void OnContentDocumentGlobalCreated(nsISupports* aOuterWindow);
void OnInnerWindowDestroyed();
void OnGracePeriodTimerFired();
void RecomputeNumVisibleWindows();
// mProcessPriority tracks the priority we've given this process in hal,
// except that, when the grace period timer is active,
// mProcessPriority == BACKGROUND even though hal still thinks we're a
// foreground process.
ProcessPriority mProcessPriority;
nsTArray<nsWeakPtr> mWindows;
nsCOMPtr<nsITimer> mGracePeriodTimer;
TimeStamp mStartupTime;
};
NS_IMPL_ISUPPORTS2(ProcessPriorityManager, nsIObserver, nsIDOMEventListener);
ProcessPriorityManager::ProcessPriorityManager()
: mProcessPriority(PROCESS_PRIORITY_FOREGROUND)
, mStartupTime(TimeStamp::Now())
{
}
void
ProcessPriorityManager::Init()
{
LOG("Starting up.");
// We can't do this in the constructor because we need to hold a strong ref
// to |this| before calling these methods.
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
os->AddObserver(this, "content-document-global-created", /* ownsWeak = */ false);
os->AddObserver(this, "inner-window-destroyed", /* ownsWeak = */ false);
SetPriority(PROCESS_PRIORITY_FOREGROUND);
}
NS_IMETHODIMP
ProcessPriorityManager::Observe(
nsISupports* aSubject,
const char* aTopic,
const PRUnichar* aData)
{
if (!strcmp(aTopic, "content-document-global-created")) {
OnContentDocumentGlobalCreated(aSubject);
} else if (!strcmp(aTopic, "inner-window-destroyed")) {
OnInnerWindowDestroyed();
} else if (!strcmp(aTopic, "timer-callback")) {
OnGracePeriodTimerFired();
} else {
MOZ_ASSERT(false);
}
return NS_OK;
}
NS_IMETHODIMP
ProcessPriorityManager::HandleEvent(
nsIDOMEvent* aEvent)
{
LOG("Got visibilitychange.");
RecomputeNumVisibleWindows();
return NS_OK;
}
void
ProcessPriorityManager::OnContentDocumentGlobalCreated(
nsISupports* aOuterWindow)
{
// Get the inner window (the topic of content-document-global-created is
// the /outer/ window!).
nsCOMPtr<nsPIDOMWindow> outerWindow = do_QueryInterface(aOuterWindow);
NS_ENSURE_TRUE_VOID(outerWindow);
nsCOMPtr<nsPIDOMWindow> innerWindow = outerWindow->GetCurrentInnerWindow();
NS_ENSURE_TRUE_VOID(innerWindow);
// We're only interested in top-level windows.
nsCOMPtr<nsIDOMWindow> parentOuterWindow;
innerWindow->GetScriptableParent(getter_AddRefs(parentOuterWindow));
NS_ENSURE_TRUE_VOID(parentOuterWindow);
if (parentOuterWindow != outerWindow) {
return;
}
nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(innerWindow);
NS_ENSURE_TRUE_VOID(target);
nsWeakPtr weakWin = do_GetWeakReference(innerWindow);
NS_ENSURE_TRUE_VOID(weakWin);
if (mWindows.Contains(weakWin)) {
return;
}
target->AddSystemEventListener(NS_LITERAL_STRING("visibilitychange"),
this,
/* useCapture = */ false,
/* wantsUntrusted = */ false);
mWindows.AppendElement(weakWin);
RecomputeNumVisibleWindows();
}
void
ProcessPriorityManager::OnInnerWindowDestroyed()
{
RecomputeNumVisibleWindows();
}
void
ProcessPriorityManager::RecomputeNumVisibleWindows()
{
// We could try to be clever and count the number of visible windows, instead
// of iterating over mWindows every time one window's visibility state changes.
// But experience suggests that iterating over the windows is prone to fewer
// errors (and one mistake doesn't mess you up for the entire session).
// Moreover, mWindows should be a very short list, since it contains only
// top-level content windows.
bool allHidden = true;
for (uint32_t i = 0; i < mWindows.Length(); i++) {
nsCOMPtr<nsIDOMWindow> window = do_QueryReferent(mWindows[i]);
if (!window) {
mWindows.RemoveElementAt(i);
i--;
continue;
}
nsCOMPtr<nsIDOMDocument> doc;
window->GetDocument(getter_AddRefs(doc));
if (!doc) {
continue;
}
bool hidden = false;
doc->GetHidden(&hidden);
#ifdef DEBUG
nsAutoString spec;
doc->GetDocumentURI(spec);
LOG("Document at %s has visibility %d.", NS_ConvertUTF16toUTF8(spec).get(), !hidden);
#endif
allHidden = allHidden && hidden;
// We could break out early from this loop if
// !hidden && mProcessPriority == BACKGROUND,
// but then we might not clean up all the weak refs.
}
SetPriority(allHidden ?
PROCESS_PRIORITY_BACKGROUND :
PROCESS_PRIORITY_FOREGROUND);
}
void
ProcessPriorityManager::SetPriority(ProcessPriority aPriority)
{
if (aPriority == mProcessPriority) {
return;
}
if (aPriority == PROCESS_PRIORITY_BACKGROUND) {
// If this is a foreground --> background transition, give ourselves a
// grace period before informing hal.
uint32_t gracePeriodMS = Preferences::GetUint("dom.ipc.processPriorityManager.gracePeriodMS", 1000);
if (mGracePeriodTimer) {
LOG("Grace period timer already active.");
return;
}
LOG("Initializing grace period timer.");
mProcessPriority = aPriority;
mGracePeriodTimer = do_CreateInstance("@mozilla.org/timer;1");
mGracePeriodTimer->Init(this, gracePeriodMS, nsITimer::TYPE_ONE_SHOT);
} else if (aPriority == PROCESS_PRIORITY_FOREGROUND) {
// If this is a background --> foreground transition, do it immediately, and
// cancel the outstanding grace period timer, if there is one.
if (mGracePeriodTimer) {
mGracePeriodTimer->Cancel();
mGracePeriodTimer = nullptr;
}
LOG("Setting priority to %d.", aPriority);
mProcessPriority = aPriority;
hal::SetProcessPriority(getpid(), aPriority);
} else {
MOZ_ASSERT(false);
}
}
void
ProcessPriorityManager::OnGracePeriodTimerFired()
{
LOG("Grace period timer fired; setting priority to %d.",
PROCESS_PRIORITY_BACKGROUND);
// mProcessPriority should already be BACKGROUND: We set it in
// SetPriority(BACKGROUND), and we canceled this timer if there was an
// intervening SetPriority(FOREGROUND) call.
MOZ_ASSERT(mProcessPriority == PROCESS_PRIORITY_BACKGROUND);
mGracePeriodTimer = nullptr;
hal::SetProcessPriority(getpid(), PROCESS_PRIORITY_BACKGROUND);
// We're in the background; dump as much memory as we can.
nsCOMPtr<nsIMemoryReporterManager> mgr =
do_GetService("@mozilla.org/memory-reporter-manager;1");
if (mgr) {
mgr->MinimizeMemoryUsage(/* callback = */ nullptr);
}
}
} // anonymous namespace
void
InitProcessPriorityManager()
{
if (sInitialized) {
return;
}
// If IPC tabs aren't enabled at startup, don't bother with any of this.
if (!Preferences::GetBool("dom.ipc.processPriorityManager.enabled") ||
Preferences::GetBool("dom.ipc.tabs.disabled")) {
return;
}
sInitialized = true;
// If we're the master process, mark ourselves as such and don't create a
// ProcessPriorityManager (we never want to mark the master process as
// backgrounded).
if (XRE_GetProcessType() == GeckoProcessType_Default) {
LOG("This is the master process.");
hal::SetProcessPriority(getpid(), PROCESS_PRIORITY_MASTER);
return;
}
// This object is held alive by the observer service.
nsRefPtr<ProcessPriorityManager> mgr = new ProcessPriorityManager();
mgr->Init();
}
} // namespace ipc
} // namespace dom
} // namespace mozilla