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/. */
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"
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
;
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 {
58 bool done
= aCondition();
60 fprintf(stderr
, "Done in %llu msec\n", ::GetTickCount64() - t0
);
68 class Spinner final
: public nsIObserver
{
69 nsCOMPtr
<nsIObserverService
> mObserverSvc
;
70 nsDependentCString mTopicToWatch
;
71 Maybe
<nsDependentString
> mSubTopicToWatch
;
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
))
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
);
100 fprintf(stderr
, "Unexpected topic: %s\n", aTopic
);
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
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
);
141 static uint32_t GetAvailablePhysicalMemoryInMb() {
142 MEMORYSTATUSEX statex
= {sizeof(statex
)};
143 if (!::GlobalMemoryStatusEx(&statex
)) {
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");
162 PageT
page(::VirtualAlloc(nullptr, currentSize
, MEM_RESERVE
| MEM_COMMIT
,
165 DWORD gle
= ::GetLastError();
166 if (gle
!= ERROR_COMMITMENT_LIMIT
) {
170 // Try again with a smaller allocation size.
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
));
186 nsAutoHandle mThread
;
187 nsAutoHandle mMessageQueueReady
;
188 Atomic
<bool> mTaskStatus
;
190 enum class TaskType
: UINT
{
191 Alloc
= WM_USER
, // WPARAM = Allocation size
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());
208 BOOL result
= ::GetMessage(&msg
, reinterpret_cast<HWND
>(-1), WM_QUIT
,
209 static_cast<UINT
>(TaskType::Last
));
211 return ::GetLastError();
218 switch (static_cast<TaskType
>(msg
.message
)) {
219 case TaskType::Alloc
:
220 mTaskStatus
= AddWorkingSet(msg
.wParam
, stock
);
223 stock
= Vector
<PageT
>();
227 MOZ_ASSERT_UNREACHABLE("Unexpected message in the queue");
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
);
241 : mThread(::CreateThread(nullptr, 0, ThreadStart
, this, 0, &mThreadId
)),
242 mMessageQueueReady(::CreateEventW(nullptr, /*bManualReset*/ TRUE
,
243 /*bInitialState*/ FALSE
, nullptr)) {
244 ::WaitForSingleObject(mMessageQueueReady
.get(), INFINITE
);
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;
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
{
274 // Issue a memory-pressure to verify OnHighMemory issues
275 // a memory-pressure-stop event.
276 NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory
);
281 NS_IMPL_ISUPPORTS(MockTabUnloader
, nsITabUnloader
)
285 class AvailableMemoryWatcherFixture
: public TelemetryTestFixture
{
286 static const char kPrefLowCommitSpaceThreshold
[];
288 RefPtr
<nsAvailableMemoryWatcherBase
> mWatcher
;
289 nsCOMPtr
<nsIObserverService
> mObserverSvc
;
292 static bool IsPageFileExpandable() {
293 const auto kMemMgmtKey
=
294 u
"SYSTEM\\CurrentControlSet\\Control\\"
295 u
"Session Manager\\Memory Management"_ns
;
298 nsCOMPtr
<nsIWindowsRegKey
> regKey
=
299 do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv
);
304 rv
= regKey
->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE
, kMemMgmtKey
,
305 nsIWindowsRegKey::ACCESS_READ
);
310 nsAutoString pagingFiles
;
311 rv
= regKey
->ReadStringValue(u
"PagingFiles"_ns
, pagingFiles
);
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");
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
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
);
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
) !=
429 fprintf(stderr
, "The memory notification was not triggered.\n");
432 fprintf(stderr
, "Notified in %llu msec\n", ::GetTickCount64() - t0
);
436 void StartUserInteraction() {
437 mObserverSvc
->NotifyObservers(nullptr, "user-interaction-active", nullptr);
440 void StopUserInteraction() {
441 mObserverSvc
->NotifyObservers(nullptr, "user-interaction-inactive",
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
;
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()) {
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
);
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.
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();
516 // Not enough memory to safely create a low-memory situation.
517 // Aborting the test without failure.
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.
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();
544 // Not enough memory to safely create a low-memory situation.
545 // Aborting the test without failure.
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.
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();
582 // Not enough memory to safely create a low-memory situation.
583 // Aborting the test without failure.
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.
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()) {
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();
625 // Not enough memory to safely create a low-memory situation.
626 // Aborting the test without failure.
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.
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()) {
657 EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader
->GetCounter() >= 1; },
658 kStateChangeTimeoutMs
));
660 mHighMemoryObserver
->StartListening();
661 mMemEater
.RequestFree();
662 EXPECT_TRUE(mHighMemoryObserver
->Wait(kStateChangeTimeoutMs
));