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
28 using namespace mozilla;
29
30 namespace {
31
32 static constexpr size_t kBytesInMB = 1024 * 1024;
33
34 template <typename ConditionT>
WaitUntil(const ConditionT & aCondition,uint32_t aTimeoutMs)35 bool WaitUntil(const ConditionT& aCondition, uint32_t aTimeoutMs) {
36 const uint64_t t0 = ::GetTickCount64();
37 bool isTimeout = false;
38
39 // The message queue can be empty and the loop stops
40 // waiting for a new event before detecting timeout.
41 // Creating a timer to fire a timeout event.
42 nsCOMPtr<nsITimer> timer;
43 NS_NewTimerWithFuncCallback(
44 getter_AddRefs(timer),
45 [](nsITimer*, void* isTimeout) {
46 *reinterpret_cast<bool*>(isTimeout) = true;
47 },
48 &isTimeout, aTimeoutMs, nsITimer::TYPE_ONE_SHOT, __func__);
49
50 SpinEventLoopUntil([&]() -> bool {
51 if (isTimeout) {
52 return true;
53 }
54
55 bool done = aCondition();
56 if (done) {
57 fprintf(stderr, "Done in %llu msec\n", ::GetTickCount64() - t0);
58 }
59 return done;
60 });
61
62 return !isTimeout;
63 }
64
65 class Spinner final : public nsIObserver {
66 nsCOMPtr<nsIObserverService> mObserverSvc;
67 nsDependentCString mTopicToWatch;
68 Maybe<nsDependentString> mSubTopicToWatch;
69 bool mTopicObserved;
70
71 ~Spinner() = default;
72
73 public:
74 NS_DECL_ISUPPORTS
75
Spinner(nsIObserverService * aObserverSvc,const char * const aTopic,const char16_t * const aSubTopic)76 Spinner(nsIObserverService* aObserverSvc, const char* const aTopic,
77 const char16_t* const aSubTopic)
78 : mObserverSvc(aObserverSvc),
79 mTopicToWatch(aTopic),
80 mSubTopicToWatch(aSubTopic ? Some(nsDependentString(aSubTopic))
81 : Nothing()),
82 mTopicObserved(false) {}
83
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)84 NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
85 const char16_t* aData) override {
86 if (mTopicToWatch == aTopic) {
87 if ((mSubTopicToWatch.isNothing() && !aData) ||
88 mSubTopicToWatch.ref() == aData) {
89 mTopicObserved = true;
90 mObserverSvc->RemoveObserver(this, aTopic);
91
92 // Force the loop to move in case that there is no event in the queue.
93 nsCOMPtr<nsIRunnable> dummyEvent = new Runnable(__func__);
94 NS_DispatchToMainThread(dummyEvent);
95 }
96 } else {
97 fprintf(stderr, "Unexpected topic: %s\n", aTopic);
98 }
99
100 return NS_OK;
101 }
102
StartListening()103 void StartListening() {
104 mTopicObserved = false;
105 mObserverSvc->AddObserver(this, mTopicToWatch.get(), false);
106 }
107
Wait(uint32_t aTimeoutMs)108 bool Wait(uint32_t aTimeoutMs) {
109 return WaitUntil([this]() { return this->mTopicObserved; }, aTimeoutMs);
110 }
111 };
112
113 NS_IMPL_ISUPPORTS(Spinner, nsIObserver)
114
115 /**
116 * Starts a new thread with a message queue to process
117 * memory allocation/free requests
118 */
119 class MemoryEater {
120 using PageT = UniquePtr<void, VirtualFreeDeleter>;
121
ThreadStart(LPVOID aParam)122 static DWORD WINAPI ThreadStart(LPVOID aParam) {
123 return reinterpret_cast<MemoryEater*>(aParam)->ThreadProc();
124 }
125
TouchMemory(void * aAddr,size_t aSize)126 static void TouchMemory(void* aAddr, size_t aSize) {
127 constexpr uint32_t kPageSize = 4096;
128 volatile uint8_t x = 0;
129 auto base = reinterpret_cast<uint8_t*>(aAddr);
130 for (int64_t i = 0, pages = aSize / kPageSize; i < pages; ++i) {
131 // Pick a random place in every allocated page
132 // and dereference it.
133 x ^= *(base + i * kPageSize + rand() % kPageSize);
134 }
135 }
136
GetAvailablePhysicalMemoryInMb()137 static uint32_t GetAvailablePhysicalMemoryInMb() {
138 MEMORYSTATUSEX statex = {sizeof(statex)};
139 if (!::GlobalMemoryStatusEx(&statex)) {
140 return 0;
141 }
142
143 return static_cast<uint32_t>(statex.ullAvailPhys / kBytesInMB);
144 }
145
AddWorkingSet(size_t aSize,Vector<PageT> & aOutput)146 static bool AddWorkingSet(size_t aSize, Vector<PageT>& aOutput) {
147 constexpr size_t kMinGranularity = 64 * 1024;
148
149 size_t currentSize = aSize;
150 size_t consumed = 0;
151 while (aSize >= kMinGranularity) {
152 if (!GetAvailablePhysicalMemoryInMb()) {
153 // If the available physical memory is less than 1MB, we finish
154 // allocation though there may be still the available commit space.
155 fprintf(stderr, "No enough physical memory.\n");
156 return false;
157 }
158
159 PageT page(::VirtualAlloc(nullptr, currentSize, MEM_RESERVE | MEM_COMMIT,
160 PAGE_READWRITE));
161 if (!page) {
162 DWORD gle = ::GetLastError();
163 if (gle != ERROR_COMMITMENT_LIMIT) {
164 return false;
165 }
166
167 // Try again with a smaller allocation size.
168 currentSize /= 2;
169 continue;
170 }
171
172 aSize -= currentSize;
173 consumed += currentSize;
174
175 // VirtualAlloc consumes the commit space, but we need to *touch* memory
176 // to consume physical memory
177 TouchMemory(page.get(), currentSize);
178 Unused << aOutput.emplaceBack(std::move(page));
179 }
180 return true;
181 }
182
183 DWORD mThreadId;
184 nsAutoHandle mThread;
185 nsAutoHandle mMessageQueueReady;
186 Atomic<bool> mTaskStatus;
187
188 enum class TaskType : UINT {
189 Alloc = WM_USER, // WPARAM = Allocation size
190 Free,
191
192 Last,
193 };
194
ThreadProc()195 DWORD ThreadProc() {
196 Vector<PageT> stock;
197 MSG msg;
198
199 // Force the system to create a message queue
200 ::PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
201
202 // Ready to get a message. Unblock the main thread.
203 ::SetEvent(mMessageQueueReady.get());
204
205 for (;;) {
206 BOOL result = ::GetMessage(&msg, reinterpret_cast<HWND>(-1), WM_QUIT,
207 static_cast<UINT>(TaskType::Last));
208 if (result == -1) {
209 return ::GetLastError();
210 }
211 if (!result) {
212 // Got WM_QUIT
213 break;
214 }
215
216 switch (static_cast<TaskType>(msg.message)) {
217 case TaskType::Alloc:
218 mTaskStatus = AddWorkingSet(msg.wParam, stock);
219 break;
220 case TaskType::Free:
221 stock = Vector<PageT>();
222 mTaskStatus = true;
223 break;
224 default:
225 MOZ_ASSERT_UNREACHABLE("Unexpected message in the queue");
226 break;
227 }
228 }
229
230 return static_cast<DWORD>(msg.wParam);
231 }
232
PostTask(TaskType aTask,WPARAM aW=0,LPARAM aL=0) const233 bool PostTask(TaskType aTask, WPARAM aW = 0, LPARAM aL = 0) const {
234 return !!::PostThreadMessageW(mThreadId, static_cast<UINT>(aTask), aW, aL);
235 }
236
237 public:
MemoryEater()238 MemoryEater()
239 : mThread(::CreateThread(nullptr, 0, ThreadStart, this, 0, &mThreadId)),
240 mMessageQueueReady(::CreateEventW(nullptr, /*bManualReset*/ TRUE,
241 /*bInitialState*/ FALSE, nullptr)) {
242 ::WaitForSingleObject(mMessageQueueReady.get(), INFINITE);
243 }
244
~MemoryEater()245 ~MemoryEater() {
246 ::PostThreadMessageW(mThreadId, WM_QUIT, 0, 0);
247 if (::WaitForSingleObject(mThread.get(), 30000) != WAIT_OBJECT_0) {
248 ::TerminateThread(mThread.get(), 0);
249 }
250 }
251
GetTaskStatus() const252 bool GetTaskStatus() const { return mTaskStatus; }
RequestAlloc(size_t aSize)253 void RequestAlloc(size_t aSize) { PostTask(TaskType::Alloc, aSize); }
RequestFree()254 void RequestFree() { PostTask(TaskType::Free); }
255 };
256
257 class MockTabUnloader final : public nsITabUnloader {
258 ~MockTabUnloader() = default;
259
260 uint32_t mCounter;
261
262 public:
MockTabUnloader()263 MockTabUnloader() : mCounter(0) {}
264
265 NS_DECL_THREADSAFE_ISUPPORTS
266
ResetCounter()267 void ResetCounter() { mCounter = 0; }
GetCounter() const268 uint32_t GetCounter() const { return mCounter; }
269
UnloadTabAsync()270 NS_IMETHOD UnloadTabAsync() override {
271 ++mCounter;
272 // Issue a memory-pressure to verify OnHighMemory issues
273 // a memory-pressure-stop event.
274 NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory);
275 return NS_OK;
276 }
277 };
278
279 NS_IMPL_ISUPPORTS(MockTabUnloader, nsITabUnloader)
280
281 } // namespace
282
283 class AvailableMemoryWatcherFixture : public ::testing::Test {
284 static const char kPrefLowCommitSpaceThreshold[];
285
286 RefPtr<nsAvailableMemoryWatcherBase> mWatcher;
287 nsCOMPtr<nsIObserverService> mObserverSvc;
288
289 protected:
IsPageFileExpandable()290 static bool IsPageFileExpandable() {
291 const auto kMemMgmtKey =
292 u"SYSTEM\\CurrentControlSet\\Control\\"
293 u"Session Manager\\Memory Management"_ns;
294
295 nsresult rv;
296 nsCOMPtr<nsIWindowsRegKey> regKey =
297 do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
298 if (NS_FAILED(rv)) {
299 return false;
300 }
301
302 rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, kMemMgmtKey,
303 nsIWindowsRegKey::ACCESS_READ);
304 if (NS_FAILED(rv)) {
305 return false;
306 }
307
308 nsAutoString pagingFiles;
309 rv = regKey->ReadStringValue(u"PagingFiles"_ns, pagingFiles);
310 if (NS_FAILED(rv)) {
311 return false;
312 }
313
314 // The value data is REG_MULTI_SZ and each element is "<path> <min> <max>".
315 // If the page file size is automatically managed for all drives, the <path>
316 // is set to "?:\pagefile.sys".
317 // If the page file size is configured per drive, for a drive whose page
318 // file is set to "system managed size", both <min> and <max> are set to 0.
319 return !pagingFiles.IsEmpty() &&
320 (pagingFiles[0] == u'?' || FindInReadable(u" 0 0"_ns, pagingFiles));
321 }
322
GetAllocationSizeToTriggerMemoryNotification()323 static size_t GetAllocationSizeToTriggerMemoryNotification() {
324 // The percentage of the used physical memory to the total physical memory
325 // size which is big enough to trigger a memory resource notification.
326 constexpr uint32_t kThresholdPercentage = 98;
327 // If the page file is not expandable, leave a little commit space.
328 const uint32_t kMinimumSafeCommitSpaceMb =
329 IsPageFileExpandable() ? 0 : 1024;
330
331 MEMORYSTATUSEX statex = {sizeof(statex)};
332 EXPECT_TRUE(::GlobalMemoryStatusEx(&statex));
333
334 // How much memory needs to be used to trigger the notification
335 const size_t targetUsedTotalMb =
336 (statex.ullTotalPhys / kBytesInMB) * kThresholdPercentage / 100;
337
338 // How much memory is currently consumed
339 const size_t currentConsumedMb =
340 (statex.ullTotalPhys - statex.ullAvailPhys) / kBytesInMB;
341
342 if (currentConsumedMb >= targetUsedTotalMb) {
343 fprintf(stderr, "The available physical memory is already low.\n");
344 return 0;
345 }
346
347 // How much memory we need to allocate to trigger the notification
348 const uint32_t allocMb = targetUsedTotalMb - currentConsumedMb;
349
350 // If we allocate the target amount, how much commit space will be
351 // left available.
352 const uint32_t estimtedAvailCommitSpace = std::max(
353 0,
354 static_cast<int32_t>((statex.ullAvailPageFile / kBytesInMB) - allocMb));
355
356 // If the available commit space will be too low, we should not continue
357 if (estimtedAvailCommitSpace < kMinimumSafeCommitSpaceMb) {
358 fprintf(stderr, "The available commit space will be short - %d\n",
359 estimtedAvailCommitSpace);
360 return 0;
361 }
362
363 fprintf(stderr,
364 "Total physical memory = %ul\n"
365 "Available commit space = %ul\n"
366 "Amount to allocate = %ul\n"
367 "Future available commit space after allocation = %d\n",
368 static_cast<uint32_t>(statex.ullTotalPhys / kBytesInMB),
369 static_cast<uint32_t>(statex.ullAvailPageFile / kBytesInMB),
370 allocMb, estimtedAvailCommitSpace);
371 return allocMb * kBytesInMB;
372 }
373
SetThresholdAsPercentageOfCommitSpace(uint32_t aPercentage)374 static void SetThresholdAsPercentageOfCommitSpace(uint32_t aPercentage) {
375 aPercentage = std::min(100u, aPercentage);
376
377 MEMORYSTATUSEX statex = {sizeof(statex)};
378 EXPECT_TRUE(::GlobalMemoryStatusEx(&statex));
379
380 const uint32_t newVal = static_cast<uint32_t>(
381 (statex.ullAvailPageFile / kBytesInMB) * aPercentage / 100);
382 fprintf(stderr, "Setting %s to %u\n", kPrefLowCommitSpaceThreshold, newVal);
383
384 Preferences::SetUint(kPrefLowCommitSpaceThreshold, newVal);
385 }
386
387 static constexpr uint32_t kStateChangeTimeoutMs = 10000;
388 static constexpr uint32_t kNotificationTimeoutMs = 5000;
389
390 RefPtr<Spinner> mHighMemoryObserver;
391 RefPtr<MockTabUnloader> mTabUnloader;
392 MemoryEater mMemEater;
393 nsAutoHandle mLowMemoryHandle;
394
SetUp()395 void SetUp() override {
396 mObserverSvc = do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
397 ASSERT_TRUE(mObserverSvc);
398
399 mHighMemoryObserver =
400 new Spinner(mObserverSvc, "memory-pressure-stop", nullptr);
401 mTabUnloader = new MockTabUnloader;
402
403 mWatcher = CreateAvailableMemoryWatcher();
404 mWatcher->RegisterTabUnloader(mTabUnloader);
405
406 mLowMemoryHandle.own(
407 ::CreateMemoryResourceNotification(LowMemoryResourceNotification));
408 ASSERT_TRUE(mLowMemoryHandle);
409
410 // We set the threshold to 50% of the current available commit space.
411 // This means we declare low-memory when the available commit space
412 // gets lower than this threshold, otherwise we declare high-memory.
413 SetThresholdAsPercentageOfCommitSpace(50);
414 }
415
TearDown()416 void TearDown() override {
417 StopUserInteraction();
418 Preferences::ClearUser(kPrefLowCommitSpaceThreshold);
419 }
420
WaitForMemoryResourceNotification()421 bool WaitForMemoryResourceNotification() {
422 if (::WaitForSingleObject(mLowMemoryHandle, kNotificationTimeoutMs) !=
423 WAIT_OBJECT_0) {
424 fprintf(stderr, "The memory notification was not triggered.\n");
425 return false;
426 }
427 return true;
428 }
429
StartUserInteraction()430 void StartUserInteraction() {
431 mObserverSvc->NotifyObservers(nullptr, "user-interaction-active", nullptr);
432 }
433
StopUserInteraction()434 void StopUserInteraction() {
435 mObserverSvc->NotifyObservers(nullptr, "user-interaction-inactive",
436 nullptr);
437 }
438 };
439
440 const char AvailableMemoryWatcherFixture::kPrefLowCommitSpaceThreshold[] =
441 "browser.low_commit_space_threshold_mb";
442
TEST_F(AvailableMemoryWatcherFixture,AlwaysActive)443 TEST_F(AvailableMemoryWatcherFixture, AlwaysActive) {
444 StartUserInteraction();
445
446 const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
447 if (!allocSize) {
448 // Not enough memory to safely create a low-memory situation.
449 // Aborting the test without failure.
450 return;
451 }
452
453 mTabUnloader->ResetCounter();
454 mMemEater.RequestAlloc(allocSize);
455 if (!WaitForMemoryResourceNotification()) {
456 // If the notification was not triggered, abort the test without failure
457 // because it's not a fault in nsAvailableMemoryWatcher.
458 return;
459 }
460
461 EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
462 kStateChangeTimeoutMs));
463
464 mHighMemoryObserver->StartListening();
465 mMemEater.RequestFree();
466 EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
467 }
468
TEST_F(AvailableMemoryWatcherFixture,InactiveToActive)469 TEST_F(AvailableMemoryWatcherFixture, InactiveToActive) {
470 const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification();
471 if (!allocSize) {
472 // Not enough memory to safely create a low-memory situation.
473 // Aborting the test without failure.
474 return;
475 }
476
477 mTabUnloader->ResetCounter();
478 mMemEater.RequestAlloc(allocSize);
479 if (!WaitForMemoryResourceNotification()) {
480 // If the notification was not triggered, abort the test without failure
481 // because it's not a fault in nsAvailableMemoryWatcher.
482 return;
483 }
484
485 mHighMemoryObserver->StartListening();
486 EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; },
487 kStateChangeTimeoutMs));
488
489 mMemEater.RequestFree();
490
491 // OnHighMemory should not be triggered during no user interaction
492 // eve after all memory was freed. Expecting false.
493 EXPECT_FALSE(mHighMemoryObserver->Wait(3000));
494
495 StartUserInteraction();
496
497 // After user is active, we expect true.
498 EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs));
499 }
500