diff options
Diffstat (limited to 'xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp')
-rw-r--r-- | xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp | 663 |
1 files changed, 663 insertions, 0 deletions
diff --git a/xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp b/xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp new file mode 100644 index 0000000000..409d547aaa --- /dev/null +++ b/xpcom/tests/gtest/TestAvailableMemoryWatcherWin.cpp @@ -0,0 +1,663 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <algorithm> +#include <windows.h> +#include <memoryapi.h> +#include "gtest/gtest.h" + +#include "AvailableMemoryWatcher.h" +#include "mozilla/Atomics.h" +#include "mozilla/gtest/MozAssertions.h" +#include "mozilla/Preferences.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/Unused.h" +#include "mozilla/Vector.h" +#include "nsComponentManagerUtils.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsServiceManagerUtils.h" +#include "nsITimer.h" +#include "nsMemoryPressure.h" +#include "nsWindowsHelpers.h" +#include "nsIWindowsRegKey.h" +#include "nsXULAppAPI.h" +#include "TelemetryFixture.h" +#include "TelemetryTestHelpers.h" + +using namespace mozilla; + +namespace { + +static constexpr size_t kBytesInMB = 1024 * 1024; + +template <typename ConditionT> +bool WaitUntil(const ConditionT& aCondition, uint32_t aTimeoutMs) { + const uint64_t t0 = ::GetTickCount64(); + bool isTimeout = false; + + // The message queue can be empty and the loop stops + // waiting for a new event before detecting timeout. + // Creating a timer to fire a timeout event. + nsCOMPtr<nsITimer> timer; + NS_NewTimerWithFuncCallback( + getter_AddRefs(timer), + [](nsITimer*, void* isTimeout) { + *reinterpret_cast<bool*>(isTimeout) = true; + }, + &isTimeout, aTimeoutMs, nsITimer::TYPE_ONE_SHOT, __func__); + + SpinEventLoopUntil("xpcom-tests:WaitUntil"_ns, [&]() -> bool { + if (isTimeout) { + return true; + } + + bool done = aCondition(); + if (done) { + fprintf(stderr, "Done in %llu msec\n", ::GetTickCount64() - t0); + } + return done; + }); + + return !isTimeout; +} + +class Spinner final : public nsIObserver { + nsCOMPtr<nsIObserverService> mObserverSvc; + nsDependentCString mTopicToWatch; + Maybe<nsDependentString> mSubTopicToWatch; + bool mTopicObserved; + + ~Spinner() = default; + + public: + NS_DECL_ISUPPORTS + + Spinner(nsIObserverService* aObserverSvc, const char* const aTopic, + const char16_t* const aSubTopic) + : mObserverSvc(aObserverSvc), + mTopicToWatch(aTopic), + mSubTopicToWatch(aSubTopic ? Some(nsDependentString(aSubTopic)) + : Nothing()), + mTopicObserved(false) {} + + NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override { + if (mTopicToWatch == aTopic) { + if ((mSubTopicToWatch.isNothing() && !aData) || + mSubTopicToWatch.ref() == aData) { + mTopicObserved = true; + mObserverSvc->RemoveObserver(this, aTopic); + + // Force the loop to move in case that there is no event in the queue. + nsCOMPtr<nsIRunnable> dummyEvent = new Runnable(__func__); + NS_DispatchToMainThread(dummyEvent); + } + } else { + fprintf(stderr, "Unexpected topic: %s\n", aTopic); + } + + return NS_OK; + } + + void StartListening() { + mTopicObserved = false; + mObserverSvc->AddObserver(this, mTopicToWatch.get(), false); + } + + bool Wait(uint32_t aTimeoutMs) { + return WaitUntil([this]() { return this->mTopicObserved; }, aTimeoutMs); + } +}; + +NS_IMPL_ISUPPORTS(Spinner, nsIObserver) + +/** + * Starts a new thread with a message queue to process + * memory allocation/free requests + */ +class MemoryEater { + using PageT = UniquePtr<void, VirtualFreeDeleter>; + + static DWORD WINAPI ThreadStart(LPVOID aParam) { + return reinterpret_cast<MemoryEater*>(aParam)->ThreadProc(); + } + + static void TouchMemory(void* aAddr, size_t aSize) { + constexpr uint32_t kPageSize = 4096; + volatile uint8_t x = 0; + auto base = reinterpret_cast<uint8_t*>(aAddr); + for (int64_t i = 0, pages = aSize / kPageSize; i < pages; ++i) { + // Pick a random place in every allocated page + // and dereference it. + x ^= *(base + i * kPageSize + rand() % kPageSize); + } + (void)x; + } + + static uint32_t GetAvailablePhysicalMemoryInMb() { + MEMORYSTATUSEX statex = {sizeof(statex)}; + if (!::GlobalMemoryStatusEx(&statex)) { + return 0; + } + + return static_cast<uint32_t>(statex.ullAvailPhys / kBytesInMB); + } + + static bool AddWorkingSet(size_t aSize, Vector<PageT>& aOutput) { + constexpr size_t kMinGranularity = 64 * 1024; + + size_t currentSize = aSize; + while (aSize >= kMinGranularity) { + if (!GetAvailablePhysicalMemoryInMb()) { + // If the available physical memory is less than 1MB, we finish + // allocation though there may be still the available commit space. + fprintf(stderr, "No enough physical memory.\n"); + return false; + } + + PageT page(::VirtualAlloc(nullptr, currentSize, MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE)); + if (!page) { + DWORD gle = ::GetLastError(); + if (gle != ERROR_COMMITMENT_LIMIT) { + return false; + } + + // Try again with a smaller allocation size. + currentSize /= 2; + continue; + } + + aSize -= currentSize; + + // VirtualAlloc consumes the commit space, but we need to *touch* memory + // to consume physical memory + TouchMemory(page.get(), currentSize); + Unused << aOutput.emplaceBack(std::move(page)); + } + return true; + } + + DWORD mThreadId; + nsAutoHandle mThread; + nsAutoHandle mMessageQueueReady; + Atomic<bool> mTaskStatus; + + enum class TaskType : UINT { + Alloc = WM_USER, // WPARAM = Allocation size + Free, + + Last, + }; + + DWORD ThreadProc() { + Vector<PageT> stock; + MSG msg; + + // Force the system to create a message queue + ::PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE); + + // Ready to get a message. Unblock the main thread. + ::SetEvent(mMessageQueueReady.get()); + + for (;;) { + BOOL result = ::GetMessage(&msg, reinterpret_cast<HWND>(-1), WM_QUIT, + static_cast<UINT>(TaskType::Last)); + if (result == -1) { + return ::GetLastError(); + } + if (!result) { + // Got WM_QUIT + break; + } + + switch (static_cast<TaskType>(msg.message)) { + case TaskType::Alloc: + mTaskStatus = AddWorkingSet(msg.wParam, stock); + break; + case TaskType::Free: + stock = Vector<PageT>(); + mTaskStatus = true; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected message in the queue"); + break; + } + } + + return static_cast<DWORD>(msg.wParam); + } + + bool PostTask(TaskType aTask, WPARAM aW = 0, LPARAM aL = 0) const { + return !!::PostThreadMessageW(mThreadId, static_cast<UINT>(aTask), aW, aL); + } + + public: + MemoryEater() + : mThread(::CreateThread(nullptr, 0, ThreadStart, this, 0, &mThreadId)), + mMessageQueueReady(::CreateEventW(nullptr, /*bManualReset*/ TRUE, + /*bInitialState*/ FALSE, nullptr)) { + ::WaitForSingleObject(mMessageQueueReady.get(), INFINITE); + } + + ~MemoryEater() { + ::PostThreadMessageW(mThreadId, WM_QUIT, 0, 0); + if (::WaitForSingleObject(mThread.get(), 30000) != WAIT_OBJECT_0) { + ::TerminateThread(mThread.get(), 0); + } + } + + bool GetTaskStatus() const { return mTaskStatus; } + void RequestAlloc(size_t aSize) { PostTask(TaskType::Alloc, aSize); } + void RequestFree() { PostTask(TaskType::Free); } +}; + +class MockTabUnloader final : public nsITabUnloader { + ~MockTabUnloader() = default; + + uint32_t mCounter; + + public: + MockTabUnloader() : mCounter(0) {} + + NS_DECL_THREADSAFE_ISUPPORTS + + void ResetCounter() { mCounter = 0; } + uint32_t GetCounter() const { return mCounter; } + + NS_IMETHOD UnloadTabAsync() override { + ++mCounter; + // Issue a memory-pressure to verify OnHighMemory issues + // a memory-pressure-stop event. + NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory); + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(MockTabUnloader, nsITabUnloader) + +} // namespace + +class AvailableMemoryWatcherFixture : public TelemetryTestFixture { + static const char kPrefLowCommitSpaceThreshold[]; + + RefPtr<nsAvailableMemoryWatcherBase> mWatcher; + nsCOMPtr<nsIObserverService> mObserverSvc; + + protected: + static bool IsPageFileExpandable() { + const auto kMemMgmtKey = + u"SYSTEM\\CurrentControlSet\\Control\\" + u"Session Manager\\Memory Management"_ns; + + nsresult rv; + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + if (NS_FAILED(rv)) { + return false; + } + + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, kMemMgmtKey, + nsIWindowsRegKey::ACCESS_READ); + if (NS_FAILED(rv)) { + return false; + } + + nsAutoString pagingFiles; + rv = regKey->ReadStringValue(u"PagingFiles"_ns, pagingFiles); + if (NS_FAILED(rv)) { + return false; + } + + // The value data is REG_MULTI_SZ and each element is "<path> <min> <max>". + // If the page file size is automatically managed for all drives, the <path> + // is set to "?:\pagefile.sys". + // If the page file size is configured per drive, for a drive whose page + // file is set to "system managed size", both <min> and <max> are set to 0. + return !pagingFiles.IsEmpty() && + (pagingFiles[0] == u'?' || FindInReadable(u" 0 0"_ns, pagingFiles)); + } + + static size_t GetAllocationSizeToTriggerMemoryNotification() { + // The percentage of the used physical memory to the total physical memory + // size which is big enough to trigger a memory resource notification. + constexpr uint32_t kThresholdPercentage = 98; + // If the page file is not expandable, leave a little commit space. + const uint32_t kMinimumSafeCommitSpaceMb = + IsPageFileExpandable() ? 0 : 1024; + + MEMORYSTATUSEX statex = {sizeof(statex)}; + EXPECT_TRUE(::GlobalMemoryStatusEx(&statex)); + + // How much memory needs to be used to trigger the notification + const size_t targetUsedTotalMb = + (statex.ullTotalPhys / kBytesInMB) * kThresholdPercentage / 100; + + // How much memory is currently consumed + const size_t currentConsumedMb = + (statex.ullTotalPhys - statex.ullAvailPhys) / kBytesInMB; + + if (currentConsumedMb >= targetUsedTotalMb) { + fprintf(stderr, "The available physical memory is already low.\n"); + return 0; + } + + // How much memory we need to allocate to trigger the notification + const uint32_t allocMb = targetUsedTotalMb - currentConsumedMb; + + // If we allocate the target amount, how much commit space will be + // left available. + const uint32_t estimtedAvailCommitSpace = std::max( + 0, + static_cast<int32_t>((statex.ullAvailPageFile / kBytesInMB) - allocMb)); + + // If the available commit space will be too low, we should not continue + if (estimtedAvailCommitSpace < kMinimumSafeCommitSpaceMb) { + fprintf(stderr, "The available commit space will be short - %d\n", + estimtedAvailCommitSpace); + return 0; + } + + fprintf(stderr, + "Total physical memory = %ul\n" + "Available commit space = %ul\n" + "Amount to allocate = %ul\n" + "Future available commit space after allocation = %d\n", + static_cast<uint32_t>(statex.ullTotalPhys / kBytesInMB), + static_cast<uint32_t>(statex.ullAvailPageFile / kBytesInMB), + allocMb, estimtedAvailCommitSpace); + return allocMb * kBytesInMB; + } + + static void SetThresholdAsPercentageOfCommitSpace(uint32_t aPercentage) { + aPercentage = std::min(100u, aPercentage); + + MEMORYSTATUSEX statex = {sizeof(statex)}; + EXPECT_TRUE(::GlobalMemoryStatusEx(&statex)); + + const uint32_t newVal = static_cast<uint32_t>( + (statex.ullAvailPageFile / kBytesInMB) * aPercentage / 100); + fprintf(stderr, "Setting %s to %u\n", kPrefLowCommitSpaceThreshold, newVal); + + Preferences::SetUint(kPrefLowCommitSpaceThreshold, newVal); + } + + static constexpr uint32_t kStateChangeTimeoutMs = 20000; + static constexpr uint32_t kNotificationTimeoutMs = 20000; + + RefPtr<Spinner> mHighMemoryObserver; + RefPtr<MockTabUnloader> mTabUnloader; + MemoryEater mMemEater; + nsAutoHandle mLowMemoryHandle; + + void SetUp() override { + TelemetryTestFixture::SetUp(); + + mObserverSvc = do_GetService(NS_OBSERVERSERVICE_CONTRACTID); + ASSERT_TRUE(mObserverSvc); + + mHighMemoryObserver = + new Spinner(mObserverSvc, "memory-pressure-stop", nullptr); + mTabUnloader = new MockTabUnloader; + + mWatcher = nsAvailableMemoryWatcherBase::GetSingleton(); + mWatcher->RegisterTabUnloader(mTabUnloader); + + mLowMemoryHandle.own( + ::CreateMemoryResourceNotification(LowMemoryResourceNotification)); + ASSERT_TRUE(mLowMemoryHandle); + + // We set the threshold to 50% of the current available commit space. + // This means we declare low-memory when the available commit space + // gets lower than this threshold, otherwise we declare high-memory. + SetThresholdAsPercentageOfCommitSpace(50); + } + + void TearDown() override { + StopUserInteraction(); + Preferences::ClearUser(kPrefLowCommitSpaceThreshold); + } + + bool WaitForMemoryResourceNotification() { + uint64_t t0 = ::GetTickCount64(); + if (::WaitForSingleObject(mLowMemoryHandle, kNotificationTimeoutMs) != + WAIT_OBJECT_0) { + fprintf(stderr, "The memory notification was not triggered.\n"); + return false; + } + fprintf(stderr, "Notified in %llu msec\n", ::GetTickCount64() - t0); + return true; + } + + void StartUserInteraction() { + mObserverSvc->NotifyObservers(nullptr, "user-interaction-active", nullptr); + } + + void StopUserInteraction() { + mObserverSvc->NotifyObservers(nullptr, "user-interaction-inactive", + nullptr); + } +}; + +const char AvailableMemoryWatcherFixture::kPrefLowCommitSpaceThreshold[] = + "browser.low_commit_space_threshold_mb"; + +class MemoryWatcherTelemetryEvent { + static nsLiteralString sEventCategory; + static nsLiteralString sEventMethod; + static nsLiteralString sEventObject; + + uint32_t mLastCountOfEvents; + + public: + explicit MemoryWatcherTelemetryEvent(JSContext* aCx) : mLastCountOfEvents(0) { + JS::RootedValue snapshot(aCx); + TelemetryTestHelpers::GetEventSnapshot(aCx, &snapshot); + nsTArray<nsString> eventValues = TelemetryTestHelpers::EventValuesToArray( + aCx, snapshot, sEventCategory, sEventMethod, sEventObject); + mLastCountOfEvents = eventValues.Length(); + } + + void ValidateLastEvent(JSContext* aCx) { + JS::RootedValue snapshot(aCx); + TelemetryTestHelpers::GetEventSnapshot(aCx, &snapshot); + nsTArray<nsString> eventValues = TelemetryTestHelpers::EventValuesToArray( + aCx, snapshot, sEventCategory, sEventMethod, sEventObject); + + // A new event was generated. + EXPECT_EQ(eventValues.Length(), mLastCountOfEvents + 1); + if (eventValues.IsEmpty()) { + return; + } + + // Update mLastCountOfEvents for a subsequent call to ValidateLastEvent + ++mLastCountOfEvents; + + nsTArray<nsString> tokens; + for (const nsAString& token : eventValues.LastElement().Split(',')) { + tokens.AppendElement(token); + } + EXPECT_EQ(tokens.Length(), 3U); + if (tokens.Length() != 3U) { + const wchar_t* valueStr = eventValues.LastElement().get(); + fprintf(stderr, "Unexpected event value: %S\n", valueStr); + return; + } + + // Since this test does not involve TabUnloader, the first two numbers + // are always expected to be zero. + EXPECT_STREQ(tokens[0].get(), L"0"); + EXPECT_STREQ(tokens[1].get(), L"0"); + + // The third token should be a valid floating number. + nsresult rv; + tokens[2].ToDouble(&rv); + EXPECT_NS_SUCCEEDED(rv); + } +}; + +nsLiteralString MemoryWatcherTelemetryEvent::sEventCategory = + u"memory_watcher"_ns; +nsLiteralString MemoryWatcherTelemetryEvent::sEventMethod = + u"on_high_memory"_ns; +nsLiteralString MemoryWatcherTelemetryEvent::sEventObject = u"stats"_ns; + +TEST_F(AvailableMemoryWatcherFixture, AlwaysActive) { + AutoJSContextWithGlobal cx(mCleanGlobal); + MemoryWatcherTelemetryEvent telemetryEvent(cx.GetJSContext()); + StartUserInteraction(); + + const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification(); + if (!allocSize) { + // Not enough memory to safely create a low-memory situation. + // Aborting the test without failure. + return; + } + + mTabUnloader->ResetCounter(); + mMemEater.RequestAlloc(allocSize); + if (!WaitForMemoryResourceNotification()) { + // If the notification was not triggered, abort the test without failure + // because it's not a fault in nsAvailableMemoryWatcher. + return; + } + + EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; }, + kStateChangeTimeoutMs)); + + mHighMemoryObserver->StartListening(); + mMemEater.RequestFree(); + EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs)); + + telemetryEvent.ValidateLastEvent(cx.GetJSContext()); +} + +TEST_F(AvailableMemoryWatcherFixture, InactiveToActive) { + AutoJSContextWithGlobal cx(mCleanGlobal); + MemoryWatcherTelemetryEvent telemetryEvent(cx.GetJSContext()); + const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification(); + if (!allocSize) { + // Not enough memory to safely create a low-memory situation. + // Aborting the test without failure. + return; + } + + mTabUnloader->ResetCounter(); + mMemEater.RequestAlloc(allocSize); + if (!WaitForMemoryResourceNotification()) { + // If the notification was not triggered, abort the test without failure + // because it's not a fault in nsAvailableMemoryWatcher. + return; + } + + mHighMemoryObserver->StartListening(); + EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; }, + kStateChangeTimeoutMs)); + + mMemEater.RequestFree(); + + // OnHighMemory should not be triggered during no user interaction + // eve after all memory was freed. Expecting false. + EXPECT_FALSE(mHighMemoryObserver->Wait(3000)); + + StartUserInteraction(); + + // After user is active, we expect true. + EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs)); + + telemetryEvent.ValidateLastEvent(cx.GetJSContext()); +} + +TEST_F(AvailableMemoryWatcherFixture, HighCommitSpace_AlwaysActive) { + // Setting a low threshold simulates a high commit space. + SetThresholdAsPercentageOfCommitSpace(1); + StartUserInteraction(); + + const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification(); + if (!allocSize) { + // Not enough memory to safely create a low-memory situation. + // Aborting the test without failure. + return; + } + + mTabUnloader->ResetCounter(); + mMemEater.RequestAlloc(allocSize); + if (!WaitForMemoryResourceNotification()) { + // If the notification was not triggered, abort the test without failure + // because it's not a fault in nsAvailableMemoryWatcher. + return; + } + + // Tab unload will not be triggered because the commit space is not low. + EXPECT_FALSE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; }, + kStateChangeTimeoutMs / 2)); + + mMemEater.RequestFree(); + ::Sleep(kStateChangeTimeoutMs / 2); + + // Set a high threshold and make sure the watcher will trigger the tab + // unloader next time. + SetThresholdAsPercentageOfCommitSpace(50); + + mMemEater.RequestAlloc(allocSize); + if (!WaitForMemoryResourceNotification()) { + return; + } + + EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; }, + kStateChangeTimeoutMs)); + + mHighMemoryObserver->StartListening(); + mMemEater.RequestFree(); + EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs)); +} + +TEST_F(AvailableMemoryWatcherFixture, HighCommitSpace_InactiveToActive) { + // Setting a low threshold simulates a high commit space. + SetThresholdAsPercentageOfCommitSpace(1); + + const size_t allocSize = GetAllocationSizeToTriggerMemoryNotification(); + if (!allocSize) { + // Not enough memory to safely create a low-memory situation. + // Aborting the test without failure. + return; + } + + mTabUnloader->ResetCounter(); + mMemEater.RequestAlloc(allocSize); + if (!WaitForMemoryResourceNotification()) { + // If the notification was not triggered, abort the test without failure + // because it's not a fault in nsAvailableMemoryWatcher. + return; + } + + // Tab unload will not be triggered because the commit space is not low. + EXPECT_FALSE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; }, + kStateChangeTimeoutMs / 2)); + + mMemEater.RequestFree(); + ::Sleep(kStateChangeTimeoutMs / 2); + + // Set a high threshold and make sure the watcher will trigger the tab + // unloader next time. + SetThresholdAsPercentageOfCommitSpace(50); + + // When the user becomes active, the watcher will resume the timer. + StartUserInteraction(); + + mMemEater.RequestAlloc(allocSize); + if (!WaitForMemoryResourceNotification()) { + return; + } + + EXPECT_TRUE(WaitUntil([this]() { return mTabUnloader->GetCounter() >= 1; }, + kStateChangeTimeoutMs)); + + mHighMemoryObserver->StartListening(); + mMemEater.RequestFree(); + EXPECT_TRUE(mHighMemoryObserver->Wait(kStateChangeTimeoutMs)); +} |