/* -*- 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 "gtest/gtest.h" #include "AvailableMemoryWatcher.h" #include "mozilla/gtest/MozAssertions.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/Unused.h" #include "nsIObserver.h" #include "nsIObserverService.h" #include "nsITimer.h" #include "nsMemoryPressure.h" #include "TelemetryFixture.h" #include "TelemetryTestHelpers.h" using namespace mozilla; namespace { template bool WaitUntil(const ConditionT& aCondition, uint32_t aTimeoutMs) { bool isTimeout = false; // The message queue can be empty and the loop stops // waiting for a new event before detecting timeout. // Creating a timer to fire a timeout event. nsCOMPtr timer; NS_NewTimerWithFuncCallback( getter_AddRefs(timer), [](nsITimer*, void* isTimeout) { *reinterpret_cast(isTimeout) = true; }, &isTimeout, aTimeoutMs, nsITimer::TYPE_ONE_SHOT, __func__); SpinEventLoopUntil("TestAvailableMemoryWatcherMac"_ns, [&]() -> bool { if (isTimeout) { return true; } return aCondition(); }); return !isTimeout; } class Spinner final : public nsIObserver { nsCOMPtr mObserverSvc; nsDependentCString mTopicToWatch; bool mTopicObserved; ~Spinner() = default; public: NS_DECL_ISUPPORTS Spinner(nsIObserverService* aObserverSvc, const char* const aTopic, const char16_t* const aSubTopic) : mObserverSvc(aObserverSvc), mTopicToWatch(aTopic), mTopicObserved(false) {} NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) override { if (mTopicToWatch == aTopic) { mTopicObserved = true; mObserverSvc->RemoveObserver(this, aTopic); // Force the loop to move in case that there is no event in the queue. nsCOMPtr dummyEvent = new Runnable(__func__); NS_DispatchToMainThread(dummyEvent); } return NS_OK; } void StartListening() { mTopicObserved = false; mObserverSvc->AddObserver(this, mTopicToWatch.get(), false); } bool Wait(uint32_t aTimeoutMs) { return WaitUntil([this]() { return this->mTopicObserved; }, aTimeoutMs); } }; NS_IMPL_ISUPPORTS(Spinner, nsIObserver) class MockTabUnloader final : public nsITabUnloader { ~MockTabUnloader() = default; uint32_t mCounter; public: MockTabUnloader() : mCounter(0) {} NS_DECL_THREADSAFE_ISUPPORTS void ResetCounter() { mCounter = 0; } uint32_t GetCounter() const { return mCounter; } NS_IMETHOD UnloadTabAsync() override { ++mCounter; // Issue a memory-pressure to verify OnHighMemory issues // a memory-pressure-stop event. NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory); return NS_OK; } }; NS_IMPL_ISUPPORTS(MockTabUnloader, nsITabUnloader) } // namespace class AvailableMemoryWatcherFixture : public TelemetryTestFixture { nsCOMPtr mObserverSvc; protected: RefPtr mWatcher; RefPtr mHighMemoryObserver; RefPtr mLowMemoryObserver; RefPtr mTabUnloader; static constexpr uint32_t kStateChangeTimeoutMs = 20000; static constexpr uint32_t kNotificationTimeoutMs = 20000; void SetUp() override { TelemetryTestFixture::SetUp(); mObserverSvc = do_GetService(NS_OBSERVERSERVICE_CONTRACTID); ASSERT_TRUE(mObserverSvc); mHighMemoryObserver = new Spinner(mObserverSvc, "memory-pressure-stop", nullptr); mLowMemoryObserver = new Spinner(mObserverSvc, "memory-pressure", nullptr); mTabUnloader = new MockTabUnloader; mWatcher = nsAvailableMemoryWatcherBase::GetSingleton(); mWatcher->RegisterTabUnloader(mTabUnloader); } }; class MemoryWatcherTelemetryEvent { static nsLiteralString sEventCategory; static nsLiteralString sEventMethod; static nsLiteralString sEventObject; uint32_t mLastCountOfEvents; public: explicit MemoryWatcherTelemetryEvent(JSContext* aCx) : mLastCountOfEvents(0) { JS::RootedValue snapshot(aCx); TelemetryTestHelpers::GetEventSnapshot(aCx, &snapshot); nsTArray eventValues = TelemetryTestHelpers::EventValuesToArray( aCx, snapshot, sEventCategory, sEventMethod, sEventObject); mLastCountOfEvents = eventValues.Length(); } void ValidateLastEvent(JSContext* aCx) { JS::RootedValue snapshot(aCx); TelemetryTestHelpers::GetEventSnapshot(aCx, &snapshot); nsTArray eventValues = TelemetryTestHelpers::EventValuesToArray( aCx, snapshot, sEventCategory, sEventMethod, sEventObject); // A new event was generated. EXPECT_EQ(eventValues.Length(), mLastCountOfEvents + 1); if (eventValues.IsEmpty()) { return; } // Update mLastCountOfEvents for a subsequent call to ValidateLastEvent ++mLastCountOfEvents; nsTArray tokens; for (const nsAString& token : eventValues.LastElement().Split(',')) { tokens.AppendElement(token); } EXPECT_EQ(tokens.Length(), 3U); // The third token should be a valid floating number. nsresult rv; tokens[2].ToDouble(&rv); EXPECT_NS_SUCCEEDED(rv); } }; nsLiteralString MemoryWatcherTelemetryEvent::sEventCategory = u"memory_watcher"_ns; nsLiteralString MemoryWatcherTelemetryEvent::sEventMethod = u"on_high_memory"_ns; nsLiteralString MemoryWatcherTelemetryEvent::sEventObject = u"stats"_ns; /* * Test the browser memory pressure reponse by artificially putting the system * into the "critical" level and ensuring 1) a tab unload attempt occurs and * 2) the Gecko memory-pressure notitificiation start and stop events occur. */ TEST_F(AvailableMemoryWatcherFixture, MemoryPressureResponse) { // Set the memory pressure state to normal in case we are already // running in a low memory pressure state. mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eNormal); // Reset state mTabUnloader->ResetCounter(); AutoJSContextWithGlobal cx(mCleanGlobal); MemoryWatcherTelemetryEvent telemetryEvent(cx.GetJSContext()); // Simulate a low memory OS callback and make sure we observe // a memory-pressure event and a tab unload. mLowMemoryObserver->StartListening(); mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eWarning); mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eCritical); EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; }, kStateChangeTimeoutMs)); EXPECT_TRUE(mLowMemoryObserver->Wait(kStateChangeTimeoutMs)); // Simulate the normal memory pressure OS callback and make // sure we observe a memory-pressure-stop event. mHighMemoryObserver->StartListening(); mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eWarning); mWatcher->OnMemoryPressureChanged(MacMemoryPressureLevel::Value::eNormal); EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs)); telemetryEvent.ValidateLastEvent(cx.GetJSContext()); }