summaryrefslogtreecommitdiffstats
path: root/xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp663
1 files changed, 663 insertions, 0 deletions
diff --git a/xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp b/xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp
new file mode 100644
index 0000000000..409d547aaa
--- /dev/null
+++ b/xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp
@@ -0,0 +1,663 @@
+/* -*- 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 <algorithm>
+#include <windows.h>
+#include <memoryapi.h>
+#include "gtest/gtest.h"
+
+#include "AvailableMemoryWatcher.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Vector.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsITimer.h"
+#include "nsMemoryPressure.h"
+#include "nsWindowsHelpers.h"
+#include "nsIWindowsRegKey.h"
+#include "nsXULAppAPI.h"
+#include "TelemetryFixture.h"
+#include "TelemetryTestHelpers.h"
+
+using namespace mozilla;
+
+namespace {
+
+static constexpr size_t kBytesInMB = 1024 * 1024;
+
+template <typename ConditionT>
+bool WaitUntil(const ConditionT& aCondition, uint32_t aTimeoutMs) {
+ const uint64_t t0 = ::GetTickCount64();
+ 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<nsITimer> timer;
+ NS_NewTimerWithFuncCallback(
+ getter_AddRefs(timer),
+ [](nsITimer*, void* isTimeout) {
+ *reinterpret_cast<bool*>(isTimeout) = true;
+ },
+ &isTimeout, aTimeoutMs, nsITimer::TYPE_ONE_SHOT, __func__);
+
+ SpinEventLoopUntil("xpcom-tests:WaitUntil"_ns, [&]() -> bool {
+ if (isTimeout) {
+ return true;
+ }
+
+ bool done = aCondition();
+ if (done) {
+ fprintf(stderr, "Done in %llu msec\n", ::GetTickCount64() - t0);
+ }
+ return done;
+ });
+
+ return !isTimeout;
+}
+
+class Spinner final : public nsIObserver {
+ nsCOMPtr<nsIObserverService> mObserverSvc;
+ nsDependentCString mTopicToWatch;
+ Maybe<nsDependentString> mSubTopicToWatch;
+ bool mTopicObserved;
+
+ ~Spinner() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ Spinner(nsIObserverService* aObserverSvc, const char* const aTopic,
+ const char16_t* const aSubTopic)
+ : mObserverSvc(aObserverSvc),
+ mTopicToWatch(aTopic),
+ mSubTopicToWatch(aSubTopic ? Some(nsDependentString(aSubTopic))
+ : Nothing()),
+ mTopicObserved(false) {}
+
+ NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) override {
+ if (mTopicToWatch == aTopic) {
+ if ((mSubTopicToWatch.isNothing() && !aData) ||
+ mSubTopicToWatch.ref() == aData) {
+ mTopicObserved = true;
+ mObserverSvc->RemoveObserver(this, aTopic);
+
+ // Force the loop to move in case that there is no event in the queue.
+ nsCOMPtr<nsIRunnable> dummyEvent = new Runnable(__func__);
+ NS_DispatchToMainThread(dummyEvent);
+ }
+ } else {
+ fprintf(stderr, "Unexpected topic: %s\n", aTopic);
+ }
+
+ 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)
+
+/**
+ * Starts a new thread with a message queue to process
+ * memory allocation/free requests
+ */
+class MemoryEater {
+ using PageT = UniquePtr<void, VirtualFreeDeleter>;
+
+ static DWORD WINAPI ThreadStart(LPVOID aParam) {
+ return reinterpret_cast<MemoryEater*>(aParam)->ThreadProc();
+ }
+
+ static void TouchMemory(void* aAddr, size_t aSize) {
+ constexpr uint32_t kPageSize = 4096;
+ volatile uint8_t x = 0;
+ auto base = reinterpret_cast<uint8_t*>(aAddr);
+ for (int64_t i = 0, pages = aSize / kPageSize; i < pages; ++i) {
+ // Pick a random place in every allocated page
+ // and dereference it.
+ x ^= *(base + i * kPageSize + rand() % kPageSize);
+ }
+ (void)x;
+ }
+
+ static uint32_t GetAvailablePhysicalMemoryInMb() {
+ MEMORYSTATUSEX statex = {sizeof(statex)};
+ if (!::GlobalMemoryStatusEx(&statex)) {
+ return 0;
+ }
+
+ return static_cast<uint32_t>(statex.ullAvailPhys / kBytesInMB);
+ }
+
+ static bool AddWorkingSet(size_t aSize, Vector<PageT>& aOutput) {
+ constexpr size_t kMinGranularity = 64 * 1024;
+
+ size_t currentSize = aSize;
+ while (aSize >= kMinGranularity) {
+ if (!GetAvailablePhysicalMemoryInMb()) {
+ // If the available physical memory is less than 1MB, we finish
+ // allocation though there may be still the available commit space.
+ fprintf(stderr, "No enough physical memory.\n");
+ return false;
+ }
+
+ PageT page(::VirtualAlloc(nullptr, currentSize, MEM_RESERVE | MEM_COMMIT,
+ PAGE_READWRITE));
+ if (!page) {
+ DWORD gle = ::GetLastError();
+ if (gle != ERROR_COMMITMENT_LIMIT) {
+ return false;
+ }
+
+ // Try again with a smaller allocation size.
+ currentSize /= 2;
+ continue;
+ }
+
+ aSize -= currentSize;
+
+ // VirtualAlloc consumes the commit space, but we need to *touch* memory
+ // to consume physical memory
+ TouchMemory(page.get(), currentSize);
+ Unused << aOutput.emplaceBack(std::move(page));
+ }
+ return true;
+ }
+
+ DWORD mThreadId;
+ nsAutoHandle mThread;
+ nsAutoHandle mMessageQueueReady;
+ Atomic<bool> mTaskStatus;
+
+ enum class TaskType : UINT {
+ Alloc = WM_USER, // WPARAM = Allocation size
+ Free,
+
+ Last,
+ };
+
+ DWORD ThreadProc() {
+ Vector<PageT> stock;
+ MSG msg;
+
+ // Force the system to create a message queue
+ ::PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
+
+ // Ready to get a message. Unblock the main thread.
+ ::SetEvent(mMessageQueueReady.get());
+
+ for (;;) {
+ BOOL result = ::GetMessage(&msg, reinterpret_cast<HWND>(-1), WM_QUIT,
+ static_cast<UINT>(TaskType::Last));
+ if (result == -1) {
+ return ::GetLastError();
+ }
+ if (!result) {
+ // Got WM_QUIT
+ break;
+ }
+
+ switch (static_cast<TaskType>(msg.message)) {
+ case TaskType::Alloc:
+ mTaskStatus = AddWorkingSet(msg.wParam, stock);
+ break;
+ case TaskType::Free:
+ stock = Vector<PageT>();
+ mTaskStatus = true;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected message in the queue");
+ break;
+ }
+ }
+
+ return static_cast<DWORD>(msg.wParam);
+ }
+
+ bool PostTask(TaskType aTask, WPARAM aW = 0, LPARAM aL = 0) const {
+ return !!::PostThreadMessageW(mThreadId, static_cast<UINT>(aTask), aW, aL);
+ }
+
+ public:
+ MemoryEater()
+ : mThread(::CreateThread(nullptr, 0, ThreadStart, this, 0, &mThreadId)),
+ mMessageQueueReady(::CreateEventW(nullptr, /*bManualReset*/ TRUE,
+ /*bInitialState*/ FALSE, nullptr)) {
+ ::WaitForSingleObject(mMessageQueueReady.get(), INFINITE);
+ }
+
+ ~MemoryEater() {
+ ::PostThreadMessageW(mThreadId, WM_QUIT, 0, 0);
+ if (::WaitForSingleObject(mThread.get(), 30000) != WAIT_OBJECT_0) {
+ ::TerminateThread(mThread.get(), 0);
+ }
+ }
+
+ bool GetTaskStatus() const { return mTaskStatus; }
+ void RequestAlloc(size_t aSize) { PostTask(TaskType::Alloc, aSize); }
+ void RequestFree() { PostTask(TaskType::Free); }
+};
+
+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 {
+ static const char kPrefLowCommitSpaceThreshold[];
+
+ RefPtr<nsAvailableMemoryWatcherBase> mWatcher;
+ nsCOMPtr<nsIObserverService> mObserverSvc;
+
+ protected:
+ static bool IsPageFileExpandable() {
+ const auto kMemMgmtKey =
+ u"SYSTEM\\CurrentControlSet\\Control\\"
+ u"Session Manager\\Memory Management"_ns;
+
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, kMemMgmtKey,
+ nsIWindowsRegKey::ACCESS_READ);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ nsAutoString pagingFiles;
+ rv = regKey->ReadStringValue(u"PagingFiles"_ns, pagingFiles);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ // The value data is REG_MULTI_SZ and each element is "<path> <min> <max>".
+ // If the page file size is automatically managed for all drives, the <path>
+ // is set to "?:\pagefile.sys".
+ // If the page file size is configured per drive, for a drive whose page
+ // file is set to "system managed size", both <min> and <max> are set to 0.
+ return !pagingFiles.IsEmpty() &&
+ (pagingFiles[0] == u'?' || FindInReadable(u" 0 0"_ns, pagingFiles));
+ }
+
+ static size_t GetAllocationSizeToTriggerMemoryNotification() {
+ // The percentage of the used physical memory to the total physical memory
+ // size which is big enough to trigger a memory resource notification.
+ constexpr uint32_t kThresholdPercentage = 98;
+ // If the page file is not expandable, leave a little commit space.
+ const uint32_t kMinimumSafeCommitSpaceMb =
+ IsPageFileExpandable() ? 0 : 1024;
+
+ MEMORYSTATUSEX statex = {sizeof(statex)};
+ EXPECT_TRUE(::GlobalMemoryStatusEx(&statex));
+
+ // How much memory needs to be used to trigger the notification
+ const size_t targetUsedTotalMb =
+ (statex.ullTotalPhys / kBytesInMB) * kThresholdPercentage / 100;
+
+ // How much memory is currently consumed
+ const size_t currentConsumedMb =
+ (statex.ullTotalPhys - statex.ullAvailPhys) / kBytesInMB;
+
+ if (currentConsumedMb >= targetUsedTotalMb) {
+ fprintf(stderr, "The available physical memory is already low.\n");
+ return 0;
+ }
+
+ // How much memory we need to allocate to trigger the notification
+ const uint32_t allocMb = targetUsedTotalMb - currentConsumedMb;
+
+ // If we allocate the target amount, how much commit space will be
+ // left available.
+ const uint32_t estimtedAvailCommitSpace = std::max(
+ 0,
+ static_cast<int32_t>((statex.ullAvailPageFile / kBytesInMB) - allocMb));
+
+ // If the available commit space will be too low, we should not continue
+ if (estimtedAvailCommitSpace < kMinimumSafeCommitSpaceMb) {
+ fprintf(stderr, "The available commit space will be short - %d\n",
+ estimtedAvailCommitSpace);
+ return 0;
+ }
+
+ fprintf(stderr,
+ "Total physical memory = %ul\n"
+ "Available commit space = %ul\n"
+ "Amount to allocate = %ul\n"
+ "Future available commit space after allocation = %d\n",
+ static_cast<uint32_t>(statex.ullTotalPhys / kBytesInMB),
+ static_cast<uint32_t>(statex.ullAvailPageFile / kBytesInMB),
+ allocMb, estimtedAvailCommitSpace);
+ return allocMb * kBytesInMB;
+ }
+
+ static void SetThresholdAsPercentageOfCommitSpace(uint32_t aPercentage) {
+ aPercentage = std::min(100u, aPercentage);
+
+ MEMORYSTATUSEX statex = {sizeof(statex)};
+ EXPECT_TRUE(::GlobalMemoryStatusEx(&statex));
+
+ const uint32_t newVal = static_cast<uint32_t>(
+ (statex.ullAvailPageFile / kBytesInMB) * aPercentage / 100);
+ fprintf(stderr, "Setting %s to %u\n", kPrefLowCommitSpaceThreshold, newVal);
+
+ Preferences::SetUint(kPrefLowCommitSpaceThreshold, newVal);
+ }
+
+ static constexpr uint32_t kStateChangeTimeoutMs = 20000;
+ static constexpr uint32_t kNotificationTimeoutMs = 20000;
+
+ RefPtr<Spinner> mHighMemoryObserver;
+ RefPtr<MockTabUnloader> mTabUnloader;
+ MemoryEater mMemEater;
+ nsAutoHandle mLowMemoryHandle;
+
+ void SetUp() override {
+ TelemetryTestFixture::SetUp();
+
+ mObserverSvc = do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
+ ASSERT_TRUE(mObserverSvc);
+
+ mHighMemoryObserver =
+ new Spinner(mObserverSvc, "memory-pressure-stop", nullptr);
+ mTabUnloader = new MockTabUnloader;
+
+ mWatcher = nsAvailableMemoryWatcherBase::GetSingleton();
+ mWatcher->RegisterTabUnloader(mTabUnloader);
+
+ mLowMemoryHandle.own(
+ ::CreateMemoryResourceNotification(LowMemoryResourceNotification));
+ ASSERT_TRUE(mLowMemoryHandle);
+
+ // We set the threshold to 50% of the current available commit space.
+ // This means we declare low-memory when the available commit space
+ // gets lower than this threshold, otherwise we declare high-memory.
+ SetThresholdAsPercentageOfCommitSpace(50);
+ }
+
+ void TearDown() override {
+ StopUserInteraction();
+ Preferences::ClearUser(kPrefLowCommitSpaceThreshold);
+ }
+
+ bool WaitForMemoryResourceNotification() {
+ uint64_t t0 = ::GetTickCount64();
+ if (::WaitForSingleObject(mLowMemoryHandle, kNotificationTimeoutMs) !=
+ WAIT_OBJECT_0) {
+ fprintf(stderr, "The memory notification was not triggered.\n");
+ return false;
+ }
+ fprintf(stderr, "Notified in %llu msec\n", ::GetTickCount64() - t0);
+ return true;
+ }
+
+ void StartUserInteraction() {
+ mObserverSvc->NotifyObservers(nullptr, "user-interaction-active", nullptr);
+ }
+
+ void StopUserInteraction() {
+ mObserverSvc->NotifyObservers(nullptr, "user-interaction-inactive",
+ nullptr);
+ }
+};
+
+const char AvailableMemoryWatcherFixture::kPrefLowCommitSpaceThreshold[] =
+ "browser.low_commit_space_threshold_mb";
+
+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<nsString> eventValues = TelemetryTestHelpers::EventValuesToArray(
+ aCx, snapshot, sEventCategory, sEventMethod, sEventObject);
+ mLastCountOfEvents = eventValues.Length();
+ }
+
+ void ValidateLastEvent(JSContext* aCx) {
+ JS::RootedValue snapshot(aCx);
+ TelemetryTestHelpers::GetEventSnapshot(aCx, &snapshot);
+ nsTArray<nsString> 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<nsString> tokens;
+ for (const nsAString& token : eventValues.LastElement().Split(',')) {
+ tokens.AppendElement(token);
+ }
+ EXPECT_EQ(tokens.Length(), 3U);
+ if (tokens.Length() != 3U) {
+ const wchar_t* valueStr = eventValues.LastElement().get();
+ fprintf(stderr, "Unexpected event value: %S\n", valueStr);
+ return;
+ }
+
+ // Since this test does not involve TabUnloader, the first two numbers
+ // are always expected to be zero.
+ EXPECT_STREQ(tokens[0].get(), L"0");
+ EXPECT_STREQ(tokens[1].get(), L"0");
+
+ // 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_F(AvailableMemoryWatcherFixture, AlwaysActive) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+ MemoryWatcherTelemetryEvent telemetryEvent(cx.GetJSContext());
+ StartUserInteraction();
+
+ const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
+ if (!allocSize) {
+ // Not enough memory to safely create a low-memory situation.
+ // Aborting the test without failure.
+ return;
+ }
+
+ mTabUnloader->ResetCounter();
+ mMemEater.RequestAlloc(allocSize);
+ if (!WaitForMemoryResourceNotification()) {
+ // If the notification was not triggered, abort the test without failure
+ // because it's not a fault in nsAvailableMemoryWatcher.
+ return;
+ }
+
+ EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
+ kStateChangeTimeoutMs));
+
+ mHighMemoryObserver->StartListening();
+ mMemEater.RequestFree();
+ EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
+
+ telemetryEvent.ValidateLastEvent(cx.GetJSContext());
+}
+
+TEST_F(AvailableMemoryWatcherFixture, InactiveToActive) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+ MemoryWatcherTelemetryEvent telemetryEvent(cx.GetJSContext());
+ const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
+ if (!allocSize) {
+ // Not enough memory to safely create a low-memory situation.
+ // Aborting the test without failure.
+ return;
+ }
+
+ mTabUnloader->ResetCounter();
+ mMemEater.RequestAlloc(allocSize);
+ if (!WaitForMemoryResourceNotification()) {
+ // If the notification was not triggered, abort the test without failure
+ // because it's not a fault in nsAvailableMemoryWatcher.
+ return;
+ }
+
+ mHighMemoryObserver->StartListening();
+ EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
+ kStateChangeTimeoutMs));
+
+ mMemEater.RequestFree();
+
+ // OnHighMemory should not be triggered during no user interaction
+ // eve after all memory was freed. Expecting false.
+ EXPECT_FALSE(mHighMemoryObserver->Wait(3000));
+
+ StartUserInteraction();
+
+ // After user is active, we expect true.
+ EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
+
+ telemetryEvent.ValidateLastEvent(cx.GetJSContext());
+}
+
+TEST_F(AvailableMemoryWatcherFixture, HighCommitSpace_AlwaysActive) {
+ // Setting a low threshold simulates a high commit space.
+ SetThresholdAsPercentageOfCommitSpace(1);
+ StartUserInteraction();
+
+ const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
+ if (!allocSize) {
+ // Not enough memory to safely create a low-memory situation.
+ // Aborting the test without failure.
+ return;
+ }
+
+ mTabUnloader->ResetCounter();
+ mMemEater.RequestAlloc(allocSize);
+ if (!WaitForMemoryResourceNotification()) {
+ // If the notification was not triggered, abort the test without failure
+ // because it's not a fault in nsAvailableMemoryWatcher.
+ return;
+ }
+
+ // Tab unload will not be triggered because the commit space is not low.
+ EXPECT_FALSE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
+ kStateChangeTimeoutMs / 2));
+
+ mMemEater.RequestFree();
+ ::Sleep(kStateChangeTimeoutMs / 2);
+
+ // Set a high threshold and make sure the watcher will trigger the tab
+ // unloader next time.
+ SetThresholdAsPercentageOfCommitSpace(50);
+
+ mMemEater.RequestAlloc(allocSize);
+ if (!WaitForMemoryResourceNotification()) {
+ return;
+ }
+
+ EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
+ kStateChangeTimeoutMs));
+
+ mHighMemoryObserver->StartListening();
+ mMemEater.RequestFree();
+ EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
+}
+
+TEST_F(AvailableMemoryWatcherFixture, HighCommitSpace_InactiveToActive) {
+ // Setting a low threshold simulates a high commit space.
+ SetThresholdAsPercentageOfCommitSpace(1);
+
+ const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
+ if (!allocSize) {
+ // Not enough memory to safely create a low-memory situation.
+ // Aborting the test without failure.
+ return;
+ }
+
+ mTabUnloader->ResetCounter();
+ mMemEater.RequestAlloc(allocSize);
+ if (!WaitForMemoryResourceNotification()) {
+ // If the notification was not triggered, abort the test without failure
+ // because it's not a fault in nsAvailableMemoryWatcher.
+ return;
+ }
+
+ // Tab unload will not be triggered because the commit space is not low.
+ EXPECT_FALSE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
+ kStateChangeTimeoutMs / 2));
+
+ mMemEater.RequestFree();
+ ::Sleep(kStateChangeTimeoutMs / 2);
+
+ // Set a high threshold and make sure the watcher will trigger the tab
+ // unloader next time.
+ SetThresholdAsPercentageOfCommitSpace(50);
+
+ // When the user becomes active, the watcher will resume the timer.
+ StartUserInteraction();
+
+ mMemEater.RequestAlloc(allocSize);
+ if (!WaitForMemoryResourceNotification()) {
+ return;
+ }
+
+ EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
+ kStateChangeTimeoutMs));
+
+ mHighMemoryObserver->StartListening();
+ mMemEater.RequestFree();
+ EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
+}