1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include <algorithm>
8 #include <windows.h>
9 #include <memoryapi.h>
10 #include "gtest/gtest.h"
11 
12 #include "AvailableMemoryWatcher.h"
13 #include "mozilla/Atomics.h"
14 #include "mozilla/Preferences.h"
15 #include "mozilla/SpinEventLoopUntil.h"
16 #include "mozilla/Unused.h"
17 #include "mozilla/Vector.h"
18 #include "nsComponentManagerUtils.h"
19 #include "nsIObserver.h"
20 #include "nsIObserverService.h"
21 #include "nsServiceManagerUtils.h"
22 #include "nsITimer.h"
23 #include "nsMemoryPressure.h"
24 #include "nsWindowsHelpers.h"
25 #include "nsIWindowsRegKey.h"
26 #include "nsXULAppAPI.h"
27 #include "TelemetryFixture.h"
28 #include "TelemetryTestHelpers.h"
29 
30 using namespace mozilla;
31 
32 namespace {
33 
34 static constexpr size_t kBytesInMB = 1024 * 1024;
35 
36 template <typename ConditionT>
WaitUntil(const ConditionT & aCondition,uint32_t aTimeoutMs)37 bool WaitUntil(const ConditionT& aCondition, uint32_t aTimeoutMs) {
38   const uint64_t t0 = ::GetTickCount64();
39   bool isTimeout = false;
40 
41   // The message queue can be empty and the loop stops
42   // waiting for a new event before detecting timeout.
43   // Creating a timer to fire a timeout event.
44   nsCOMPtr<nsITimer> timer;
45   NS_NewTimerWithFuncCallback(
46       getter_AddRefs(timer),
47       [](nsITimer*, void* isTimeout) {
48         *reinterpret_cast<bool*>(isTimeout) = true;
49       },
50       &isTimeout, aTimeoutMs, nsITimer::TYPE_ONE_SHOT, __func__);
51 
52   SpinEventLoopUntil("xpcom-tests:WaitUntil"_ns, [&]() -> bool {
53     if (isTimeout) {
54       return true;
55     }
56 
57     bool done = aCondition();
58     if (done) {
59       fprintf(stderr, "Done in %llu msec\n", ::GetTickCount64() - t0);
60     }
61     return done;
62   });
63 
64   return !isTimeout;
65 }
66 
67 class Spinner final : public nsIObserver {
68   nsCOMPtr<nsIObserverService> mObserverSvc;
69   nsDependentCString mTopicToWatch;
70   Maybe<nsDependentString> mSubTopicToWatch;
71   bool mTopicObserved;
72 
73   ~Spinner() = default;
74 
75  public:
76   NS_DECL_ISUPPORTS
77 
Spinner(nsIObserverService * aObserverSvc,const char * const aTopic,const char16_t * const aSubTopic)78   Spinner(nsIObserverService* aObserverSvc, const char* const aTopic,
79           const char16_t* const aSubTopic)
80       : mObserverSvc(aObserverSvc),
81         mTopicToWatch(aTopic),
82         mSubTopicToWatch(aSubTopic ? Some(nsDependentString(aSubTopic))
83                                    : Nothing()),
84         mTopicObserved(false) {}
85 
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)86   NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
87                      const char16_t* aData) override {
88     if (mTopicToWatch == aTopic) {
89       if ((mSubTopicToWatch.isNothing() && !aData) ||
90           mSubTopicToWatch.ref() == aData) {
91         mTopicObserved = true;
92         mObserverSvc->RemoveObserver(this, aTopic);
93 
94         // Force the loop to move in case that there is no event in the queue.
95         nsCOMPtr<nsIRunnable> dummyEvent = new Runnable(__func__);
96         NS_DispatchToMainThread(dummyEvent);
97       }
98     } else {
99       fprintf(stderr, "Unexpected topic: %s\n", aTopic);
100     }
101 
102     return NS_OK;
103   }
104 
StartListening()105   void StartListening() {
106     mTopicObserved = false;
107     mObserverSvc->AddObserver(this, mTopicToWatch.get(), false);
108   }
109 
Wait(uint32_t aTimeoutMs)110   bool Wait(uint32_t aTimeoutMs) {
111     return WaitUntil([this]() { return this->mTopicObserved; }, aTimeoutMs);
112   }
113 };
114 
115 NS_IMPL_ISUPPORTS(Spinner, nsIObserver)
116 
117 /**
118  * Starts a new thread with a message queue to process
119  * memory allocation/free requests
120  */
121 class MemoryEater {
122   using PageT = UniquePtr<void, VirtualFreeDeleter>;
123 
ThreadStart(LPVOID aParam)124   static DWORD WINAPI ThreadStart(LPVOID aParam) {
125     return reinterpret_cast<MemoryEater*>(aParam)->ThreadProc();
126   }
127 
TouchMemory(void * aAddr,size_t aSize)128   static void TouchMemory(void* aAddr, size_t aSize) {
129     constexpr uint32_t kPageSize = 4096;
130     volatile uint8_t x = 0;
131     auto base = reinterpret_cast<uint8_t*>(aAddr);
132     for (int64_t i = 0, pages = aSize / kPageSize; i < pages; ++i) {
133       // Pick a random place in every allocated page
134       // and dereference it.
135       x ^= *(base + i * kPageSize + rand() % kPageSize);
136     }
137     (void)x;
138   }
139 
GetAvailablePhysicalMemoryInMb()140   static uint32_t GetAvailablePhysicalMemoryInMb() {
141     MEMORYSTATUSEX statex = {sizeof(statex)};
142     if (!::GlobalMemoryStatusEx(&statex)) {
143       return 0;
144     }
145 
146     return static_cast<uint32_t>(statex.ullAvailPhys / kBytesInMB);
147   }
148 
AddWorkingSet(size_t aSize,Vector<PageT> & aOutput)149   static bool AddWorkingSet(size_t aSize, Vector<PageT>& aOutput) {
150     constexpr size_t kMinGranularity = 64 * 1024;
151 
152     size_t currentSize = aSize;
153     while (aSize >= kMinGranularity) {
154       if (!GetAvailablePhysicalMemoryInMb()) {
155         // If the available physical memory is less than 1MB, we finish
156         // allocation though there may be still the available commit space.
157         fprintf(stderr, "No enough physical memory.\n");
158         return false;
159       }
160 
161       PageT page(::VirtualAlloc(nullptr, currentSize, MEM_RESERVE | MEM_COMMIT,
162                                 PAGE_READWRITE));
163       if (!page) {
164         DWORD gle = ::GetLastError();
165         if (gle != ERROR_COMMITMENT_LIMIT) {
166           return false;
167         }
168 
169         // Try again with a smaller allocation size.
170         currentSize /= 2;
171         continue;
172       }
173 
174       aSize -= currentSize;
175 
176       // VirtualAlloc consumes the commit space, but we need to *touch* memory
177       // to consume physical memory
178       TouchMemory(page.get(), currentSize);
179       Unused << aOutput.emplaceBack(std::move(page));
180     }
181     return true;
182   }
183 
184   DWORD mThreadId;
185   nsAutoHandle mThread;
186   nsAutoHandle mMessageQueueReady;
187   Atomic<bool> mTaskStatus;
188 
189   enum class TaskType : UINT {
190     Alloc = WM_USER,  // WPARAM = Allocation size
191     Free,
192 
193     Last,
194   };
195 
ThreadProc()196   DWORD ThreadProc() {
197     Vector<PageT> stock;
198     MSG msg;
199 
200     // Force the system to create a message queue
201     ::PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
202 
203     // Ready to get a message.  Unblock the main thread.
204     ::SetEvent(mMessageQueueReady.get());
205 
206     for (;;) {
207       BOOL result = ::GetMessage(&msg, reinterpret_cast<HWND>(-1), WM_QUIT,
208                                  static_cast<UINT>(TaskType::Last));
209       if (result == -1) {
210         return ::GetLastError();
211       }
212       if (!result) {
213         // Got WM_QUIT
214         break;
215       }
216 
217       switch (static_cast<TaskType>(msg.message)) {
218         case TaskType::Alloc:
219           mTaskStatus = AddWorkingSet(msg.wParam, stock);
220           break;
221         case TaskType::Free:
222           stock = Vector<PageT>();
223           mTaskStatus = true;
224           break;
225         default:
226           MOZ_ASSERT_UNREACHABLE("Unexpected message in the queue");
227           break;
228       }
229     }
230 
231     return static_cast<DWORD>(msg.wParam);
232   }
233 
PostTask(TaskType aTask,WPARAM aW=0,LPARAM aL=0) const234   bool PostTask(TaskType aTask, WPARAM aW = 0, LPARAM aL = 0) const {
235     return !!::PostThreadMessageW(mThreadId, static_cast<UINT>(aTask), aW, aL);
236   }
237 
238  public:
MemoryEater()239   MemoryEater()
240       : mThread(::CreateThread(nullptr, 0, ThreadStart, this, 0, &mThreadId)),
241         mMessageQueueReady(::CreateEventW(nullptr, /*bManualReset*/ TRUE,
242                                           /*bInitialState*/ FALSE, nullptr)) {
243     ::WaitForSingleObject(mMessageQueueReady.get(), INFINITE);
244   }
245 
~MemoryEater()246   ~MemoryEater() {
247     ::PostThreadMessageW(mThreadId, WM_QUIT, 0, 0);
248     if (::WaitForSingleObject(mThread.get(), 30000) != WAIT_OBJECT_0) {
249       ::TerminateThread(mThread.get(), 0);
250     }
251   }
252 
GetTaskStatus() const253   bool GetTaskStatus() const { return mTaskStatus; }
RequestAlloc(size_t aSize)254   void RequestAlloc(size_t aSize) { PostTask(TaskType::Alloc, aSize); }
RequestFree()255   void RequestFree() { PostTask(TaskType::Free); }
256 };
257 
258 class MockTabUnloader final : public nsITabUnloader {
259   ~MockTabUnloader() = default;
260 
261   uint32_t mCounter;
262 
263  public:
MockTabUnloader()264   MockTabUnloader() : mCounter(0) {}
265 
266   NS_DECL_THREADSAFE_ISUPPORTS
267 
ResetCounter()268   void ResetCounter() { mCounter = 0; }
GetCounter() const269   uint32_t GetCounter() const { return mCounter; }
270 
UnloadTabAsync()271   NS_IMETHOD UnloadTabAsync() override {
272     ++mCounter;
273     // Issue a memory-pressure to verify OnHighMemory issues
274     // a memory-pressure-stop event.
275     NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory);
276     return NS_OK;
277   }
278 };
279 
280 NS_IMPL_ISUPPORTS(MockTabUnloader, nsITabUnloader)
281 
282 }  // namespace
283 
284 class AvailableMemoryWatcherFixture : public TelemetryTestFixture {
285   static const char kPrefLowCommitSpaceThreshold[];
286 
287   RefPtr<nsAvailableMemoryWatcherBase> mWatcher;
288   nsCOMPtr<nsIObserverService> mObserverSvc;
289 
290  protected:
IsPageFileExpandable()291   static bool IsPageFileExpandable() {
292     const auto kMemMgmtKey =
293         u"SYSTEM\\CurrentControlSet\\Control\\"
294         u"Session Manager\\Memory Management"_ns;
295 
296     nsresult rv;
297     nsCOMPtr<nsIWindowsRegKey> regKey =
298         do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
299     if (NS_FAILED(rv)) {
300       return false;
301     }
302 
303     rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, kMemMgmtKey,
304                       nsIWindowsRegKey::ACCESS_READ);
305     if (NS_FAILED(rv)) {
306       return false;
307     }
308 
309     nsAutoString pagingFiles;
310     rv = regKey->ReadStringValue(u"PagingFiles"_ns, pagingFiles);
311     if (NS_FAILED(rv)) {
312       return false;
313     }
314 
315     // The value data is REG_MULTI_SZ and each element is "<path> <min> <max>".
316     // If the page file size is automatically managed for all drives, the <path>
317     // is set to "?:\pagefile.sys".
318     // If the page file size is configured per drive, for a drive whose page
319     // file is set to "system managed size", both <min> and <max> are set to 0.
320     return !pagingFiles.IsEmpty() &&
321            (pagingFiles[0] == u'?' || FindInReadable(u" 0 0"_ns, pagingFiles));
322   }
323 
GetAllocationSizeToTriggerMemoryNotification()324   static size_t GetAllocationSizeToTriggerMemoryNotification() {
325     // The percentage of the used physical memory to the total physical memory
326     // size which is big enough to trigger a memory resource notification.
327     constexpr uint32_t kThresholdPercentage = 98;
328     // If the page file is not expandable, leave a little commit space.
329     const uint32_t kMinimumSafeCommitSpaceMb =
330         IsPageFileExpandable() ? 0 : 1024;
331 
332     MEMORYSTATUSEX statex = {sizeof(statex)};
333     EXPECT_TRUE(::GlobalMemoryStatusEx(&statex));
334 
335     // How much memory needs to be used to trigger the notification
336     const size_t targetUsedTotalMb =
337         (statex.ullTotalPhys / kBytesInMB) * kThresholdPercentage / 100;
338 
339     // How much memory is currently consumed
340     const size_t currentConsumedMb =
341         (statex.ullTotalPhys - statex.ullAvailPhys) / kBytesInMB;
342 
343     if (currentConsumedMb >= targetUsedTotalMb) {
344       fprintf(stderr, "The available physical memory is already low.\n");
345       return 0;
346     }
347 
348     // How much memory we need to allocate to trigger the notification
349     const uint32_t allocMb = targetUsedTotalMb - currentConsumedMb;
350 
351     // If we allocate the target amount, how much commit space will be
352     // left available.
353     const uint32_t estimtedAvailCommitSpace = std::max(
354         0,
355         static_cast<int32_t>((statex.ullAvailPageFile / kBytesInMB) - allocMb));
356 
357     // If the available commit space will be too low, we should not continue
358     if (estimtedAvailCommitSpace < kMinimumSafeCommitSpaceMb) {
359       fprintf(stderr, "The available commit space will be short - %d\n",
360               estimtedAvailCommitSpace);
361       return 0;
362     }
363 
364     fprintf(stderr,
365             "Total physical memory  = %ul\n"
366             "Available commit space = %ul\n"
367             "Amount to allocate     = %ul\n"
368             "Future available commit space after allocation = %d\n",
369             static_cast<uint32_t>(statex.ullTotalPhys / kBytesInMB),
370             static_cast<uint32_t>(statex.ullAvailPageFile / kBytesInMB),
371             allocMb, estimtedAvailCommitSpace);
372     return allocMb * kBytesInMB;
373   }
374 
SetThresholdAsPercentageOfCommitSpace(uint32_t aPercentage)375   static void SetThresholdAsPercentageOfCommitSpace(uint32_t aPercentage) {
376     aPercentage = std::min(100u, aPercentage);
377 
378     MEMORYSTATUSEX statex = {sizeof(statex)};
379     EXPECT_TRUE(::GlobalMemoryStatusEx(&statex));
380 
381     const uint32_t newVal = static_cast<uint32_t>(
382         (statex.ullAvailPageFile / kBytesInMB) * aPercentage / 100);
383     fprintf(stderr, "Setting %s to %u\n", kPrefLowCommitSpaceThreshold, newVal);
384 
385     Preferences::SetUint(kPrefLowCommitSpaceThreshold, newVal);
386   }
387 
388   static constexpr uint32_t kStateChangeTimeoutMs = 20000;
389   static constexpr uint32_t kNotificationTimeoutMs = 20000;
390 
391   RefPtr<Spinner> mHighMemoryObserver;
392   RefPtr<MockTabUnloader> mTabUnloader;
393   MemoryEater mMemEater;
394   nsAutoHandle mLowMemoryHandle;
395 
SetUp()396   void SetUp() override {
397     TelemetryTestFixture::SetUp();
398 
399     mObserverSvc = do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
400     ASSERT_TRUE(mObserverSvc);
401 
402     mHighMemoryObserver =
403         new Spinner(mObserverSvc, "memory-pressure-stop", nullptr);
404     mTabUnloader = new MockTabUnloader;
405 
406     mWatcher = nsAvailableMemoryWatcherBase::GetSingleton();
407     mWatcher->RegisterTabUnloader(mTabUnloader);
408 
409     mLowMemoryHandle.own(
410         ::CreateMemoryResourceNotification(LowMemoryResourceNotification));
411     ASSERT_TRUE(mLowMemoryHandle);
412 
413     // We set the threshold to 50% of the current available commit space.
414     // This means we declare low-memory when the available commit space
415     // gets lower than this threshold, otherwise we declare high-memory.
416     SetThresholdAsPercentageOfCommitSpace(50);
417   }
418 
TearDown()419   void TearDown() override {
420     StopUserInteraction();
421     Preferences::ClearUser(kPrefLowCommitSpaceThreshold);
422   }
423 
WaitForMemoryResourceNotification()424   bool WaitForMemoryResourceNotification() {
425     uint64_t t0 = ::GetTickCount64();
426     if (::WaitForSingleObject(mLowMemoryHandle, kNotificationTimeoutMs) !=
427         WAIT_OBJECT_0) {
428       fprintf(stderr, "The memory notification was not triggered.\n");
429       return false;
430     }
431     fprintf(stderr, "Notified in %llu msec\n", ::GetTickCount64() - t0);
432     return true;
433   }
434 
StartUserInteraction()435   void StartUserInteraction() {
436     mObserverSvc->NotifyObservers(nullptr, "user-interaction-active", nullptr);
437   }
438 
StopUserInteraction()439   void StopUserInteraction() {
440     mObserverSvc->NotifyObservers(nullptr, "user-interaction-inactive",
441                                   nullptr);
442   }
443 };
444 
445 const char AvailableMemoryWatcherFixture::kPrefLowCommitSpaceThreshold[] =
446     "browser.low_commit_space_threshold_mb";
447 
448 class MemoryWatcherTelemetryEvent {
449   static nsLiteralString sEventCategory;
450   static nsLiteralString sEventMethod;
451   static nsLiteralString sEventObject;
452 
453   uint32_t mLastCountOfEvents;
454 
455  public:
MemoryWatcherTelemetryEvent(JSContext * aCx)456   explicit MemoryWatcherTelemetryEvent(JSContext* aCx) : mLastCountOfEvents(0) {
457     JS::RootedValue snapshot(aCx);
458     TelemetryTestHelpers::GetEventSnapshot(aCx, &snapshot);
459     nsTArray<nsString> eventValues = TelemetryTestHelpers::EventValuesToArray(
460         aCx, snapshot, sEventCategory, sEventMethod, sEventObject);
461     mLastCountOfEvents = eventValues.Length();
462   }
463 
ValidateLastEvent(JSContext * aCx)464   void ValidateLastEvent(JSContext* aCx) {
465     JS::RootedValue snapshot(aCx);
466     TelemetryTestHelpers::GetEventSnapshot(aCx, &snapshot);
467     nsTArray<nsString> eventValues = TelemetryTestHelpers::EventValuesToArray(
468         aCx, snapshot, sEventCategory, sEventMethod, sEventObject);
469 
470     // A new event was generated.
471     EXPECT_EQ(eventValues.Length(), mLastCountOfEvents + 1);
472     if (eventValues.IsEmpty()) {
473       return;
474     }
475 
476     // Update mLastCountOfEvents for a subsequent call to ValidateLastEvent
477     ++mLastCountOfEvents;
478 
479     nsTArray<nsString> tokens;
480     for (const nsAString& token : eventValues.LastElement().Split(',')) {
481       tokens.AppendElement(token);
482     }
483     EXPECT_EQ(tokens.Length(), 3);
484     if (tokens.Length() != 3) {
485       const wchar_t* valueStr = eventValues.LastElement().get();
486       fprintf(stderr, "Unexpected event value: %S\n", valueStr);
487       return;
488     }
489 
490     // Since this test does not involve TabUnloader, the first two numbers
491     // are always expected to be zero.
492     EXPECT_STREQ(tokens[0].get(), L"0");
493     EXPECT_STREQ(tokens[1].get(), L"0");
494 
495     // The third token should be a valid floating number.
496     nsresult rv;
497     tokens[2].ToDouble(&rv);
498     EXPECT_TRUE(NS_SUCCEEDED(rv));
499   }
500 };
501 
502 nsLiteralString MemoryWatcherTelemetryEvent::sEventCategory =
503     u"memory_watcher"_ns;
504 nsLiteralString MemoryWatcherTelemetryEvent::sEventMethod =
505     u"on_high_memory"_ns;
506 nsLiteralString MemoryWatcherTelemetryEvent::sEventObject = u"stats"_ns;
507 
TEST_F(AvailableMemoryWatcherFixture,AlwaysActive)508 TEST_F(AvailableMemoryWatcherFixture, AlwaysActive) {
509   AutoJSContextWithGlobal cx(mCleanGlobal);
510   MemoryWatcherTelemetryEvent telemetryEvent(cx.GetJSContext());
511   StartUserInteraction();
512 
513   const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
514   if (!allocSize) {
515     // Not enough memory to safely create a low-memory situation.
516     // Aborting the test without failure.
517     return;
518   }
519 
520   mTabUnloader->ResetCounter();
521   mMemEater.RequestAlloc(allocSize);
522   if (!WaitForMemoryResourceNotification()) {
523     // If the notification was not triggered, abort the test without failure
524     // because it's not a fault in nsAvailableMemoryWatcher.
525     return;
526   }
527 
528   EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
529                         kStateChangeTimeoutMs));
530 
531   mHighMemoryObserver->StartListening();
532   mMemEater.RequestFree();
533   EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
534 
535   telemetryEvent.ValidateLastEvent(cx.GetJSContext());
536 }
537 
TEST_F(AvailableMemoryWatcherFixture,InactiveToActive)538 TEST_F(AvailableMemoryWatcherFixture, InactiveToActive) {
539   AutoJSContextWithGlobal cx(mCleanGlobal);
540   MemoryWatcherTelemetryEvent telemetryEvent(cx.GetJSContext());
541   const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
542   if (!allocSize) {
543     // Not enough memory to safely create a low-memory situation.
544     // Aborting the test without failure.
545     return;
546   }
547 
548   mTabUnloader->ResetCounter();
549   mMemEater.RequestAlloc(allocSize);
550   if (!WaitForMemoryResourceNotification()) {
551     // If the notification was not triggered, abort the test without failure
552     // because it's not a fault in nsAvailableMemoryWatcher.
553     return;
554   }
555 
556   mHighMemoryObserver->StartListening();
557   EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
558                         kStateChangeTimeoutMs));
559 
560   mMemEater.RequestFree();
561 
562   // OnHighMemory should not be triggered during no user interaction
563   // eve after all memory was freed.  Expecting false.
564   EXPECT_FALSE(mHighMemoryObserver->Wait(3000));
565 
566   StartUserInteraction();
567 
568   // After user is active, we expect true.
569   EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
570 
571   telemetryEvent.ValidateLastEvent(cx.GetJSContext());
572 }
573 
TEST_F(AvailableMemoryWatcherFixture,HighCommitSpace_AlwaysActive)574 TEST_F(AvailableMemoryWatcherFixture, HighCommitSpace_AlwaysActive) {
575   // Setting a low threshold simulates a high commit space.
576   SetThresholdAsPercentageOfCommitSpace(1);
577   StartUserInteraction();
578 
579   const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
580   if (!allocSize) {
581     // Not enough memory to safely create a low-memory situation.
582     // Aborting the test without failure.
583     return;
584   }
585 
586   mTabUnloader->ResetCounter();
587   mMemEater.RequestAlloc(allocSize);
588   if (!WaitForMemoryResourceNotification()) {
589     // If the notification was not triggered, abort the test without failure
590     // because it's not a fault in nsAvailableMemoryWatcher.
591     return;
592   }
593 
594   // Tab unload will not be triggered because the commit space is not low.
595   EXPECT_FALSE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
596                          kStateChangeTimeoutMs / 2));
597 
598   mMemEater.RequestFree();
599   ::Sleep(kStateChangeTimeoutMs / 2);
600 
601   // Set a high threshold and make sure the watcher will trigger the tab
602   // unloader next time.
603   SetThresholdAsPercentageOfCommitSpace(50);
604 
605   mMemEater.RequestAlloc(allocSize);
606   if (!WaitForMemoryResourceNotification()) {
607     return;
608   }
609 
610   EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
611                         kStateChangeTimeoutMs));
612 
613   mHighMemoryObserver->StartListening();
614   mMemEater.RequestFree();
615   EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
616 }
617 
TEST_F(AvailableMemoryWatcherFixture,HighCommitSpace_InactiveToActive)618 TEST_F(AvailableMemoryWatcherFixture, HighCommitSpace_InactiveToActive) {
619   // Setting a low threshold simulates a high commit space.
620   SetThresholdAsPercentageOfCommitSpace(1);
621 
622   const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
623   if (!allocSize) {
624     // Not enough memory to safely create a low-memory situation.
625     // Aborting the test without failure.
626     return;
627   }
628 
629   mTabUnloader->ResetCounter();
630   mMemEater.RequestAlloc(allocSize);
631   if (!WaitForMemoryResourceNotification()) {
632     // If the notification was not triggered, abort the test without failure
633     // because it's not a fault in nsAvailableMemoryWatcher.
634     return;
635   }
636 
637   // Tab unload will not be triggered because the commit space is not low.
638   EXPECT_FALSE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
639                          kStateChangeTimeoutMs / 2));
640 
641   mMemEater.RequestFree();
642   ::Sleep(kStateChangeTimeoutMs / 2);
643 
644   // Set a high threshold and make sure the watcher will trigger the tab
645   // unloader next time.
646   SetThresholdAsPercentageOfCommitSpace(50);
647 
648   // When the user becomes active, the watcher will resume the timer.
649   StartUserInteraction();
650 
651   mMemEater.RequestAlloc(allocSize);
652   if (!WaitForMemoryResourceNotification()) {
653     return;
654   }
655 
656   EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
657                         kStateChangeTimeoutMs));
658 
659   mHighMemoryObserver->StartListening();
660   mMemEater.RequestFree();
661   EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
662 }
663