/* -*- 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 #define getpid _getpid #else #include #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* logModule = PR_NewLogModule("ProcessPriorityManager"); #define LOG(fmt, ...) \ PR_LOG(logModule, 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 mWindows; nsCOMPtr 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 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 outerWindow = do_QueryInterface(aOuterWindow); NS_ENSURE_TRUE(outerWindow, ); nsCOMPtr innerWindow = outerWindow->GetCurrentInnerWindow(); NS_ENSURE_TRUE(innerWindow, ); // We're only interested in top-level windows. nsCOMPtr parentOuterWindow; innerWindow->GetScriptableParent(getter_AddRefs(parentOuterWindow)); NS_ENSURE_TRUE(parentOuterWindow, ); if (parentOuterWindow != outerWindow) { return; } nsCOMPtr target = do_QueryInterface(innerWindow); NS_ENSURE_TRUE(target, ); nsWeakPtr weakWin = do_GetWeakReference(innerWindow); NS_ENSURE_TRUE(weakWin, ); if (mWindows.Contains(weakWin)) { return; } target->AddSystemEventListener(NS_LITERAL_STRING("mozvisibilitychange"), 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 (PRUint32 i = 0; i < mWindows.Length(); i++) { nsCOMPtr window = do_QueryReferent(mWindows[i]); if (!window) { mWindows.RemoveElementAt(i); i--; continue; } nsCOMPtr doc; window->GetDocument(getter_AddRefs(doc)); if (!doc) { continue; } bool hidden = false; doc->GetMozHidden(&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. PRUint32 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); } } // 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 mgr = new ProcessPriorityManager(); mgr->Init(); } } // namespace ipc } // namespace dom } // namespace mozilla