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