diff --git a/widget/uikit/nsAppShell.mm b/widget/uikit/nsAppShell.mm index 2af03be1d50b1..1fd995f3e8add 100644 --- a/widget/uikit/nsAppShell.mm +++ b/widget/uikit/nsAppShell.mm @@ -92,7 +92,7 @@ - (void)applicationWillResignActive:(UIApplication*)application { - (void)applicationDidReceiveMemoryWarning:(UIApplication*)application { ALOG("[AppShellDelegate applicationDidReceiveMemoryWarning:]"); - NS_DispatchMemoryPressure(MemPressure_New); + NS_NotifyOfMemoryPressure(MemoryPressureState::LowMemory); } @end diff --git a/xpcom/base/AvailableMemoryWatcherWin.cpp b/xpcom/base/AvailableMemoryWatcherWin.cpp index 393ff68da2ffe..f95652244dc58 100644 --- a/xpcom/base/AvailableMemoryWatcherWin.cpp +++ b/xpcom/base/AvailableMemoryWatcherWin.cpp @@ -247,7 +247,7 @@ void nsAvailableMemoryWatcher::OnLowMemory(const MutexAutoLock&) { } RecordLowMemoryEvent(); - NS_DispatchEventualMemoryPressure(MemPressure_New); + NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory); } StartPollingIfUserInteracting(); @@ -256,7 +256,7 @@ void nsAvailableMemoryWatcher::OnLowMemory(const MutexAutoLock&) { void nsAvailableMemoryWatcher::OnHighMemory(const MutexAutoLock&) { mUnderMemoryPressure = false; mSavedReport = false; // Will save a new report if memory gets low again - NS_DispatchEventualMemoryPressure(MemPressure_Stopping); + NS_NotifyOfEventualMemoryPressure(MemoryPressureState::NoPressure); StopPolling(); ListenForLowMemory(); } @@ -333,7 +333,7 @@ nsAvailableMemoryWatcher::Notify(nsITimer* aTimer) { } if (IsCommitSpaceLow()) { - NS_DispatchEventualMemoryPressure(MemPressure_Ongoing); + NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory); } return NS_OK; diff --git a/xpcom/tests/gtest/TestMemoryPressure.cpp b/xpcom/tests/gtest/TestMemoryPressure.cpp new file mode 100644 index 0000000000000..a8eb1017d34ee --- /dev/null +++ b/xpcom/tests/gtest/TestMemoryPressure.cpp @@ -0,0 +1,193 @@ +/* -*- 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 +#include "gtest/gtest.h" + +#include "mozilla/Atomics.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsMemoryPressure.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" + +using namespace mozilla; + +namespace { + +enum class MemoryPressureEventType { + LowMemory, + LowMemoryOngoing, + Stop, +}; + +class MemoryPressureObserver final : public nsIObserver { + nsCOMPtr mObserverSvc; + Vector mEvents; + + ~MemoryPressureObserver() { + EXPECT_TRUE( + NS_SUCCEEDED(mObserverSvc->RemoveObserver(this, kTopicMemoryPressure))); + EXPECT_TRUE(NS_SUCCEEDED( + mObserverSvc->RemoveObserver(this, kTopicMemoryPressureStop))); + } + + public: + NS_DECL_ISUPPORTS + + MemoryPressureObserver() + : mObserverSvc(do_GetService(NS_OBSERVERSERVICE_CONTRACTID)) { + EXPECT_TRUE(NS_SUCCEEDED(mObserverSvc->AddObserver( + this, kTopicMemoryPressure, /* ownsWeak */ false))); + EXPECT_TRUE(NS_SUCCEEDED(mObserverSvc->AddObserver( + this, kTopicMemoryPressureStop, /* ownsWeak */ false))); + } + + NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override { + Maybe event; + if (strcmp(aTopic, kTopicMemoryPressure) == 0) { + if (nsDependentString(aData) == kSubTopicLowMemoryNew) { + event = Some(MemoryPressureEventType::LowMemory); + } else if (nsDependentString(aData) == kSubTopicLowMemoryOngoing) { + event = Some(MemoryPressureEventType::LowMemoryOngoing); + } else { + fprintf(stderr, "Unexpected subtopic: %S\n", + reinterpret_cast(aData)); + EXPECT_TRUE(false); + } + } else if (strcmp(aTopic, kTopicMemoryPressureStop) == 0) { + event = Some(MemoryPressureEventType::Stop); + } else { + fprintf(stderr, "Unexpected topic: %s\n", aTopic); + EXPECT_TRUE(false); + } + + if (event) { + Unused << mEvents.emplaceBack(event.value()); + } + return NS_OK; + } + + uint32_t GetCount() const { return mEvents.length(); } + void Reset() { mEvents.clear(); } + MemoryPressureEventType Top() const { return mEvents[0]; } + + bool ValidateTransitions() const { + if (mEvents.length() == 0) { + return true; + } + + for (size_t i = 1; i < mEvents.length(); ++i) { + MemoryPressureEventType eventFrom = mEvents[i - 1]; + MemoryPressureEventType eventTo = mEvents[i]; + if ((eventFrom == MemoryPressureEventType::LowMemory && + eventTo == MemoryPressureEventType::LowMemoryOngoing) || + (eventFrom == MemoryPressureEventType::LowMemoryOngoing && + eventTo == MemoryPressureEventType::LowMemoryOngoing) || + (eventFrom == MemoryPressureEventType::Stop && + eventTo == MemoryPressureEventType::LowMemory) || + (eventFrom == MemoryPressureEventType::LowMemoryOngoing && + eventTo == MemoryPressureEventType::Stop) || + (eventFrom == MemoryPressureEventType::LowMemory && + eventTo == MemoryPressureEventType::Stop)) { + // Only these transitions are valid. + continue; + } + + fprintf(stderr, "Invalid transition: %d -> %d\n", eventFrom, eventTo); + return false; + } + return true; + } +}; + +NS_IMPL_ISUPPORTS(MemoryPressureObserver, nsIObserver) + +template +void PressureSender(Atomic& aContinue) { + while (aContinue) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + NS_NotifyOfEventualMemoryPressure(State); + } +} + +template +void PressureSenderQuick(Atomic& aContinue) { + while (aContinue) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + NS_NotifyOfMemoryPressure(State); + } +} + +} // anonymous namespace + +TEST(MemoryPressure, Singlethread) +{ + RefPtr observer(new MemoryPressureObserver); + NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory); + SpinEventLoopUntil([&observer]() { return observer->GetCount() == 1; }); + EXPECT_EQ(observer->Top(), MemoryPressureEventType::LowMemory); + + observer->Reset(); + NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory); + SpinEventLoopUntil([&observer]() { return observer->GetCount() == 1; }); + EXPECT_EQ(observer->Top(), MemoryPressureEventType::LowMemoryOngoing); + + observer->Reset(); + NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory); + SpinEventLoopUntil([&observer]() { return observer->GetCount() == 1; }); + EXPECT_EQ(observer->Top(), MemoryPressureEventType::LowMemoryOngoing); + + observer->Reset(); + NS_NotifyOfEventualMemoryPressure(MemoryPressureState::NoPressure); + SpinEventLoopUntil([&observer]() { return observer->GetCount() == 1; }); + EXPECT_EQ(observer->Top(), MemoryPressureEventType::Stop); +} + +TEST(MemoryPressure, Multithread) +{ + // Start |kNumThreads| threads each for the following thread type: + // - LowMemory via NS_NotifyOfEventualMemoryPressure + // - LowMemory via NS_NotifyOfMemoryPressure + // - LowMemoryOngoing via NS_NotifyOfEventualMemoryPressure + // - LowMemoryOngoing via NS_NotifyOfMemoryPressure + // and keep them running until |kNumEventsToValidate| memory-pressure events + // are received. + constexpr int kNumThreads = 5; + constexpr int kNumEventsToValidate = 200; + + Atomic shouldContinue(true); + Vector threads; + for (int i = 0; i < kNumThreads; ++i) { + Unused << threads.emplaceBack( + PressureSender, + std::ref(shouldContinue)); + Unused << threads.emplaceBack( + PressureSender, + std::ref(shouldContinue)); + Unused << threads.emplaceBack( + PressureSenderQuick, + std::ref(shouldContinue)); + Unused << threads.emplaceBack( + PressureSenderQuick, + std::ref(shouldContinue)); + } + + RefPtr observer(new MemoryPressureObserver); + + // We cannot sleep here because the main thread needs to keep running. + SpinEventLoopUntil( + [&observer]() { return observer->GetCount() >= kNumEventsToValidate; }); + + shouldContinue = false; + for (auto& thread : threads) { + thread.join(); + } + + EXPECT_TRUE(observer->ValidateTransitions()); +} diff --git a/xpcom/tests/gtest/TestNonBlockingAsyncInputStream.cpp b/xpcom/tests/gtest/TestNonBlockingAsyncInputStream.cpp index a961d8c14f80e..b26d48cddea07 100644 --- a/xpcom/tests/gtest/TestNonBlockingAsyncInputStream.cpp +++ b/xpcom/tests/gtest/TestNonBlockingAsyncInputStream.cpp @@ -3,6 +3,7 @@ #include "mozilla/NonBlockingAsyncInputStream.h" #include "mozilla/SpinEventLoopUntil.h" #include "nsIAsyncInputStream.h" +#include "nsIThread.h" #include "nsStreamUtils.h" #include "nsString.h" #include "nsStringStream.h" diff --git a/xpcom/tests/gtest/TestThreads.cpp b/xpcom/tests/gtest/TestThreads.cpp index bc8ca6227eee2..91fc8709d5c83 100644 --- a/xpcom/tests/gtest/TestThreads.cpp +++ b/xpcom/tests/gtest/TestThreads.cpp @@ -9,10 +9,13 @@ #include #include "nspr.h" #include "nsCOMPtr.h" +#include "nsIThread.h" #include "nsXPCOM.h" #include "mozilla/Monitor.h" #include "gtest/gtest.h" +using namespace mozilla; + class nsRunner final : public Runnable { ~nsRunner() = default; diff --git a/xpcom/tests/gtest/moz.build b/xpcom/tests/gtest/moz.build index 73662c2000ab9..ada3262f778c5 100644 --- a/xpcom/tests/gtest/moz.build +++ b/xpcom/tests/gtest/moz.build @@ -27,6 +27,7 @@ UNIFIED_SOURCES += [ "TestInputStreamLengthHelper.cpp", "TestLogCommandLineHandler.cpp", "TestLogging.cpp", + "TestMemoryPressure.cpp", "TestMoveString.cpp", "TestMozPromise.cpp", "TestMruCache.cpp", diff --git a/xpcom/threads/nsMemoryPressure.cpp b/xpcom/threads/nsMemoryPressure.cpp index 4f0ee3a33201a..e0051fcbf7fc9 100644 --- a/xpcom/threads/nsMemoryPressure.cpp +++ b/xpcom/threads/nsMemoryPressure.cpp @@ -12,38 +12,91 @@ using namespace mozilla; -static Atomic sMemoryPressurePending; -static_assert(MemPressure_None == 0, - "Bad static initialization with the default constructor."); +const char* const kTopicMemoryPressure = "memory-pressure"; +const char* const kTopicMemoryPressureStop = "memory-pressure-stop"; +const char16_t* const kSubTopicLowMemoryNew = u"low-memory"; +const char16_t* const kSubTopicLowMemoryOngoing = u"low-memory-ongoing"; -MemoryPressureState NS_GetPendingMemoryPressure() { - int32_t value = sMemoryPressurePending.exchange(MemPressure_None); - return MemoryPressureState(value); -} +// This is accessed from any thread through NS_NotifyOfEventualMemoryPressure +static Atomic sMemoryPressurePending( + MemoryPressureState::NoPressure); + +void NS_NotifyOfEventualMemoryPressure(MemoryPressureState aState) { + MOZ_ASSERT(aState != MemoryPressureState::None); -void NS_DispatchEventualMemoryPressure(MemoryPressureState aState) { /* * A new memory pressure event erases an ongoing (or stop of) memory pressure, * but an existing "new" memory pressure event takes precedence over a new * "ongoing" or "stop" memory pressure event. */ switch (aState) { - case MemPressure_None: - sMemoryPressurePending = MemPressure_None; + case MemoryPressureState::None: + case MemoryPressureState::LowMemory: + sMemoryPressurePending = aState; break; - case MemPressure_New: - sMemoryPressurePending = MemPressure_New; - break; - case MemPressure_Ongoing: - case MemPressure_Stopping: - sMemoryPressurePending.compareExchange(MemPressure_None, aState); + case MemoryPressureState::NoPressure: + sMemoryPressurePending.compareExchange(MemoryPressureState::None, aState); break; } } -nsresult NS_DispatchMemoryPressure(MemoryPressureState aState) { - NS_DispatchEventualMemoryPressure(aState); +nsresult NS_NotifyOfMemoryPressure(MemoryPressureState aState) { + NS_NotifyOfEventualMemoryPressure(aState); nsCOMPtr event = new Runnable("NS_DispatchEventualMemoryPressure"); return NS_DispatchToMainThread(event); } + +void NS_DispatchMemoryPressure() { + MOZ_ASSERT(NS_IsMainThread()); + static MemoryPressureState sMemoryPressureStatus = + MemoryPressureState::NoPressure; + + MemoryPressureState mpPending = + sMemoryPressurePending.exchange(MemoryPressureState::None); + if (mpPending == MemoryPressureState::None) { + return; + } + + nsCOMPtr os = services::GetObserverService(); + if (!os) { + NS_WARNING("Can't get observer service!"); + return; + } + + switch (mpPending) { + case MemoryPressureState::None: + MOZ_ASSERT_UNREACHABLE("Already handled this case above."); + break; + case MemoryPressureState::LowMemory: + switch (sMemoryPressureStatus) { + case MemoryPressureState::None: + MOZ_ASSERT_UNREACHABLE("The internal status should never be None."); + break; + case MemoryPressureState::NoPressure: + sMemoryPressureStatus = MemoryPressureState::LowMemory; + os->NotifyObservers(nullptr, kTopicMemoryPressure, + kSubTopicLowMemoryNew); + break; + case MemoryPressureState::LowMemory: + os->NotifyObservers(nullptr, kTopicMemoryPressure, + kSubTopicLowMemoryOngoing); + break; + } + break; + case MemoryPressureState::NoPressure: + switch (sMemoryPressureStatus) { + case MemoryPressureState::None: + MOZ_ASSERT_UNREACHABLE("The internal status should never be None."); + break; + case MemoryPressureState::NoPressure: + // Already no pressure. Do nothing. + break; + case MemoryPressureState::LowMemory: + sMemoryPressureStatus = MemoryPressureState::NoPressure; + os->NotifyObservers(nullptr, kTopicMemoryPressureStop, nullptr); + break; + } + break; + } +} diff --git a/xpcom/threads/nsMemoryPressure.h b/xpcom/threads/nsMemoryPressure.h index eadde20bb3c92..5a68b0bce5009 100644 --- a/xpcom/threads/nsMemoryPressure.h +++ b/xpcom/threads/nsMemoryPressure.h @@ -9,58 +9,48 @@ #include "nscore.h" -enum MemoryPressureState { - /* - * No memory pressure. - */ - MemPressure_None = 0, - - /* - * New memory pressure deteced. - * - * On a new memory pressure, we stop everything to start cleaning - * aggresively the memory used, in order to free as much memory as - * possible. - */ - MemPressure_New, - - /* - * Repeated memory pressure. - * - * A repeated memory pressure implies to clean softly recent allocations. - * It is supposed to happen after a new memory pressure which already - * cleaned aggressivley. So there is no need to damage the reactivity of - * Gecko by stopping the world again. - * - * In case of conflict with an new memory pressue, the new memory pressure - * takes precedence over an ongoing memory pressure. The reason being - * that if no events are processed between 2 notifications (new followed - * by ongoing, or ongoing followed by a new) we want to be as aggresive as - * possible on the clean-up of the memory. After all, we are trying to - * keep Gecko alive as long as possible. - */ - MemPressure_Ongoing, - - /* - * Memory pressure stopped. - * - * We're no longer under acute memory pressure, so we might want to have a - * chance of (cautiously) re-enabling some things we previously turned off. - * As above, an already enqueued new memory pressure event takes precedence. - * The priority ordering between concurrent attempts to queue both stopped - * and ongoing memory pressure is currently not defined. - */ - MemPressure_Stopping -}; - -/** - * Return and erase the latest state of the memory pressure event set by any of - * the corresponding dispatch functions. +/* + * These pre-defined strings are the topic to pass to the observer + * service to declare the memory-pressure or lift the memory-pressure. * - * This is called when processing events on the main thread to check whether to - * fire a memory pressure notification. + * 1. Notify kTopicMemoryPressure with kSubTopicLowMemoryNew + * New memory pressure deteced + * On a new memory pressure, we stop everything to start cleaning + * aggresively the memory used, in order to free as much memory as + * possible. + * + * 2. Notify kTopicMemoryPressure with kSubTopicLowMemoryOngoing + * Repeated memory pressure. + * A repeated memory pressure implies to clean softly recent allocations. + * It is supposed to happen after a new memory pressure which already + * cleaned aggressivley. So there is no need to damage the reactivity of + * Gecko by stopping the world again. + * + * In case of conflict with an new memory pressue, the new memory pressure + * takes precedence over an ongoing memory pressure. The reason being + * that if no events are processed between 2 notifications (new followed + * by ongoing, or ongoing followed by a new) we want to be as aggresive as + * possible on the clean-up of the memory. After all, we are trying to + * keep Gecko alive as long as possible. + * + * 3. Notify kTopicMemoryPressureStop with nullptr + * Memory pressure stopped. + * We're no longer under acute memory pressure, so we might want to have a + * chance of (cautiously) re-enabling some things we previously turned off. + * As above, an already enqueued new memory pressure event takes precedence. + * The priority ordering between concurrent attempts to queue both stopped + * and ongoing memory pressure is currently not defined. */ -MemoryPressureState NS_GetPendingMemoryPressure(); +extern const char* const kTopicMemoryPressure; +extern const char* const kTopicMemoryPressureStop; +extern const char16_t* const kSubTopicLowMemoryNew; +extern const char16_t* const kSubTopicLowMemoryOngoing; + +enum class MemoryPressureState : uint32_t { + None, // For internal use. Don't use this value. + LowMemory, + NoPressure, +}; /** * This function causes the main thread to fire a memory pressure event @@ -71,17 +61,17 @@ MemoryPressureState NS_GetPendingMemoryPressure(); * * You may call this function from any thread. */ -void NS_DispatchEventualMemoryPressure(MemoryPressureState aState); +void NS_NotifyOfEventualMemoryPressure(MemoryPressureState aState); /** * This function causes the main thread to fire a memory pressure event * before processing the next event. We wake up the main thread by adding a * dummy event to its event loop, so, unlike with - * NS_DispatchEventualMemoryPressure, this memory-pressure event is always + * NS_NotifyOfEventualMemoryPressure, this memory-pressure event is always * fired relatively quickly, even if the event loop is otherwise empty. * * You may call this function from any thread. */ -nsresult NS_DispatchMemoryPressure(MemoryPressureState aState); +nsresult NS_NotifyOfMemoryPressure(MemoryPressureState aState); #endif // nsMemoryPressure_h__ diff --git a/xpcom/threads/nsThread.cpp b/xpcom/threads/nsThread.cpp index 2e2632abaf032..1f6b33465383a 100644 --- a/xpcom/threads/nsThread.cpp +++ b/xpcom/threads/nsThread.cpp @@ -1317,6 +1317,8 @@ void nsThread::SetScriptObserver( mScriptObserver = aScriptObserver; } +void NS_DispatchMemoryPressure(); + void nsThread::DoMainThreadSpecificProcessing() const { MOZ_ASSERT(mIsMainThread); @@ -1324,23 +1326,7 @@ void nsThread::DoMainThreadSpecificProcessing() const { // Fire a memory pressure notification, if one is pending. if (!ShuttingDown()) { - MemoryPressureState mpPending = NS_GetPendingMemoryPressure(); - if (mpPending != MemPressure_None) { - nsCOMPtr os = services::GetObserverService(); - - if (os) { - if (mpPending == MemPressure_Stopping) { - os->NotifyObservers(nullptr, "memory-pressure-stop", nullptr); - } else { - os->NotifyObservers(nullptr, "memory-pressure", - mpPending == MemPressure_New - ? u"low-memory" - : u"low-memory-ongoing"); - } - } else { - NS_WARNING("Can't get observer service!"); - } - } + NS_DispatchMemoryPressure(); } }