/* -*- 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 // For memory-locking. #include "gtest/gtest.h" #include "AvailableMemoryWatcher.h" #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/StaticPrefs_browser.h" #include "nsIObserverService.h" #include "nsISupports.h" #include "nsITimer.h" #include "nsMemoryPressure.h" using namespace mozilla; namespace { // Dummy tab unloader whose one job is to dispatch a low memory event. class MockTabUnloader final : public nsITabUnloader { NS_DECL_THREADSAFE_ISUPPORTS public: MockTabUnloader() = default; NS_IMETHOD UnloadTabAsync() override { // We want to issue a memory pressure event for NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory); return NS_OK; } private: ~MockTabUnloader() = default; }; NS_IMPL_ISUPPORTS(MockTabUnloader, nsITabUnloader) // Class that gradually increases the percent memory threshold // until it reaches 100%, which should guarantee a memory pressure // notification. class AvailableMemoryChecker final : public nsITimerCallback, public nsINamed { public: NS_DECL_ISUPPORTS NS_DECL_NSITIMERCALLBACK NS_DECL_NSINAMED AvailableMemoryChecker(); void Init(); void Shutdown(); private: ~AvailableMemoryChecker() = default; bool mResolved; nsCOMPtr mTimer; RefPtr mWatcher; RefPtr mTabUnloader; const uint32_t kPollingInterval = 50; const uint32_t kPrefIncrement = 5; }; AvailableMemoryChecker::AvailableMemoryChecker() : mResolved(false) {} NS_IMPL_ISUPPORTS(AvailableMemoryChecker, nsITimerCallback, nsINamed); void AvailableMemoryChecker::Init() { mTabUnloader = new MockTabUnloader; mWatcher = nsAvailableMemoryWatcherBase::GetSingleton(); mWatcher->RegisterTabUnloader(mTabUnloader); mTimer = NS_NewTimer(); mTimer->InitWithCallback(this, kPollingInterval, nsITimer::TYPE_REPEATING_SLACK); } void AvailableMemoryChecker::Shutdown() { if (mTimer) { mTimer->Cancel(); } Preferences::ClearUser("browser.low_commit_space_threshold_percent"); } // Timer callback to increase the pref threshold. NS_IMETHODIMP AvailableMemoryChecker::Notify(nsITimer* aTimer) { uint32_t threshold = StaticPrefs::browser_low_commit_space_threshold_percent(); if (threshold >= 100) { mResolved = true; return NS_OK; } threshold += kPrefIncrement; Preferences::SetUint("browser.low_commit_space_threshold_percent", threshold); return NS_OK; } NS_IMETHODIMP AvailableMemoryChecker::GetName(nsACString& aName) { aName.AssignLiteral("AvailableMemoryChecker"); return NS_OK; } // Class that listens for a given notification, then records // if it was received. class Spinner final : public nsIObserver { nsCOMPtr mObserverSvc; nsDependentCString mTopic; bool mTopicObserved; ~Spinner() = default; public: NS_DECL_ISUPPORTS Spinner(nsIObserverService* aObserverSvc, const char* aTopic) : mObserverSvc(aObserverSvc), mTopic(aTopic), mTopicObserved(false) {} NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) override { if (mTopic == aTopic) { mTopicObserved = true; mObserverSvc->RemoveObserver(this, aTopic); // Force the loop to move in case there is no event in the queue. nsCOMPtr dummyEvent = new Runnable(__func__); NS_DispatchToMainThread(dummyEvent); } return NS_OK; } void StartListening() { mObserverSvc->AddObserver(this, mTopic.get(), false); } bool TopicObserved() { return mTopicObserved; } bool WaitForNotification(); }; NS_IMPL_ISUPPORTS(Spinner, nsIObserver); bool Spinner::WaitForNotification() { bool isTimeout = false; nsCOMPtr timer; // This timer should time us out if we never observe our notification. // Set to 5000 since the memory checker should finish incrementing the // pref by then, and if it hasn't then it is probably stuck somehow. NS_NewTimerWithFuncCallback( getter_AddRefs(timer), [](nsITimer*, void* isTimeout) { *reinterpret_cast(isTimeout) = true; }, &isTimeout, 5000, nsITimer::TYPE_ONE_SHOT, __func__); SpinEventLoopUntil("Spinner:WaitForNotification"_ns, [&]() -> bool { if (isTimeout) { return true; } return mTopicObserved; }); return !isTimeout; } void StartUserInteraction(const nsCOMPtr& aObserverSvc) { aObserverSvc->NotifyObservers(nullptr, "user-interaction-active", nullptr); } TEST(AvailableMemoryWatcher, BasicTest) { nsCOMPtr observerSvc = services::GetObserverService(); RefPtr aSpinner = new Spinner(observerSvc, "memory-pressure"); aSpinner->StartListening(); // Start polling for low memory. StartUserInteraction(observerSvc); RefPtr checker = new AvailableMemoryChecker(); checker->Init(); aSpinner->WaitForNotification(); // The checker should have dispatched a low memory event before reaching 100% // memory pressure threshold, so the topic should be observed by the spinner. EXPECT_TRUE(aSpinner->TopicObserved()); checker->Shutdown(); } TEST(AvailableMemoryWatcher, MemoryLowToHigh) { // Setting this pref to 100 ensures we start in a low memory scenario. Preferences::SetUint("browser.low_commit_space_threshold_percent", 100); nsCOMPtr observerSvc = services::GetObserverService(); RefPtr lowMemorySpinner = new Spinner(observerSvc, "memory-pressure"); lowMemorySpinner->StartListening(); StartUserInteraction(observerSvc); // Start polling for low memory. We should start with low memory when we start // the checker. RefPtr checker = new AvailableMemoryChecker(); checker->Init(); lowMemorySpinner->WaitForNotification(); EXPECT_TRUE(lowMemorySpinner->TopicObserved()); RefPtr highMemorySpinner = new Spinner(observerSvc, "memory-pressure-stop"); highMemorySpinner->StartListening(); // Now that we are definitely low on memory, let's reset the pref to 0 to // exit low memory. Preferences::SetUint("browser.low_commit_space_threshold_percent", 0); highMemorySpinner->WaitForNotification(); EXPECT_TRUE(highMemorySpinner->TopicObserved()); checker->Shutdown(); } } // namespace