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