gecko-dev/xpcom/base/AvailableMemoryWatcherWin.cpp
Toshihito Kikuchi 01adc17c9a Bug 1729918 - Annotate the user interaction state. r=haik
This patch adds a crash annotation named *LastInteractionDuration*, that indicates
the length of the user interaction duration if the crash happen when the user is
inactive.

Differential Revision: https://phabricator.services.mozilla.com/D125718
2021-10-05 04:41:53 +00:00

402 lines
13 KiB
C++

/* -*- 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 "AvailableMemoryWatcher.h"
#include "mozilla/Atomics.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_browser.h"
#include "nsAppRunner.h"
#include "nsExceptionHandler.h"
#include "nsICrashReporter.h"
#include "nsIObserver.h"
#include "nsISupports.h"
#include "nsITimer.h"
#include "nsMemoryPressure.h"
#include "nsServiceManagerUtils.h"
#include "nsWindowsHelpers.h"
#include <memoryapi.h>
extern mozilla::Atomic<uint32_t, mozilla::MemoryOrdering::Relaxed>
sNumLowPhysicalMemEvents;
namespace mozilla {
// This class is used to monitor low memory events delivered by Windows via
// memory resource notification objects. When we enter a low memory scenario
// the LowMemoryCallback() is invoked by Windows. This initial call triggers
// an nsITimer that polls to see when the low memory condition has been lifted.
// When it has, we'll stop polling and start waiting for the next
// LowMemoryCallback(). Meanwhile, the polling may be stopped and restarted by
// user-interaction events from the observer service.
class nsAvailableMemoryWatcher final : public nsITimerCallback,
public nsINamed,
public nsAvailableMemoryWatcherBase {
public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_NSIOBSERVER
NS_DECL_NSITIMERCALLBACK
NS_DECL_NSINAMED
nsAvailableMemoryWatcher();
nsresult Init() override;
private:
static VOID CALLBACK LowMemoryCallback(PVOID aContext, BOOLEAN aIsTimer);
static void RecordLowMemoryEvent();
static bool IsCommitSpaceLow();
~nsAvailableMemoryWatcher();
bool RegisterMemoryResourceHandler();
void UnregisterMemoryResourceHandler();
void MaybeSaveMemoryReport(const MutexAutoLock&);
void Shutdown(const MutexAutoLock&);
bool ListenForLowMemory(const MutexAutoLock&);
void OnLowMemory(const MutexAutoLock&);
void OnHighMemory(const MutexAutoLock&);
void StartPollingIfUserInteracting(const MutexAutoLock&);
void StopPolling();
void StopPollingIfUserIdle(const MutexAutoLock&);
void OnUserInteracting(const MutexAutoLock&);
// The publicly available methods (::Observe() and ::Notify()) are called on
// the main thread while the ::LowMemoryCallback() method is called by an
// external thread. All functions called from those must acquire a lock on
// this mutex before accessing the object's fields to prevent races.
Mutex mMutex;
nsCOMPtr<nsITimer> mTimer;
nsAutoHandle mLowMemoryHandle;
HANDLE mWaitHandle;
bool mPolling;
// Indicates whether to start a timer when user interaction is notified.
// This flag is needed because the low-memory callback may be triggered when
// the user is inactive and we want to delay-start the timer.
bool mNeedToRestartTimerOnUserInteracting;
// Indicate that the available commit space is low. The timer handler needs
// this flag because it is triggered by the low physical memory regardless
// of the available commit space.
bool mUnderMemoryPressure;
bool mSavedReport;
bool mIsShutdown;
// Members below this line are used only in the main thread.
// No lock is needed.
// Don't fire a low-memory notification more often than this interval.
uint32_t mPollingInterval;
};
NS_IMPL_ISUPPORTS_INHERITED(nsAvailableMemoryWatcher,
nsAvailableMemoryWatcherBase, nsIObserver,
nsITimerCallback, nsINamed)
nsAvailableMemoryWatcher::nsAvailableMemoryWatcher()
: mMutex("low memory callback mutex"),
mWaitHandle(nullptr),
mPolling(false),
mNeedToRestartTimerOnUserInteracting(false),
mUnderMemoryPressure(false),
mSavedReport(false),
mIsShutdown(false),
mPollingInterval(0) {}
nsresult nsAvailableMemoryWatcher::Init() {
nsresult rv = nsAvailableMemoryWatcherBase::Init();
if (NS_FAILED(rv)) {
return rv;
}
mTimer = NS_NewTimer();
if (!mTimer) {
return NS_ERROR_OUT_OF_MEMORY;
}
// Use a very short interval for GTest to verify the timer's behavior.
mPollingInterval = gIsGtest ? 10 : 10000;
if (!RegisterMemoryResourceHandler()) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
nsAvailableMemoryWatcher::~nsAvailableMemoryWatcher() {
// These handles should have been released during the shutdown phase.
MOZ_ASSERT(!mLowMemoryHandle);
MOZ_ASSERT(!mWaitHandle);
}
// static
VOID CALLBACK nsAvailableMemoryWatcher::LowMemoryCallback(PVOID aContext,
BOOLEAN aIsTimer) {
if (aIsTimer) {
return;
}
// The |watcher| was addref'ed when we registered the wait handle in
// ListenForLowMemory(). It is decremented when exiting the function,
// so please make sure we unregister the wait handle after this line.
RefPtr<nsAvailableMemoryWatcher> watcher =
already_AddRefed<nsAvailableMemoryWatcher>(
static_cast<nsAvailableMemoryWatcher*>(aContext));
MutexAutoLock lock(watcher->mMutex);
if (watcher->mIsShutdown) {
// mWaitHandle should have been unregistered during shutdown
MOZ_ASSERT(!watcher->mWaitHandle);
return;
}
::UnregisterWait(watcher->mWaitHandle);
watcher->mWaitHandle = nullptr;
// On Windows, memory allocations fails when the available commit space is
// not sufficient. It's possible that this callback function is invoked
// but there is still commit space enough for the application to continue
// to run. In such a case, there is no strong need to trigger the memory
// pressure event. So we trigger the event only when the available commit
// space is low.
if (IsCommitSpaceLow()) {
watcher->OnLowMemory(lock);
} else {
// Since we have unregistered the callback, we need to start a timer to
// continue watching memory.
watcher->StartPollingIfUserInteracting(lock);
}
}
// static
void nsAvailableMemoryWatcher::RecordLowMemoryEvent() {
sNumLowPhysicalMemEvents++;
CrashReporter::AnnotateCrashReport(
CrashReporter::Annotation::LowPhysicalMemoryEvents,
sNumLowPhysicalMemEvents);
}
bool nsAvailableMemoryWatcher::RegisterMemoryResourceHandler() {
mLowMemoryHandle.own(
::CreateMemoryResourceNotification(LowMemoryResourceNotification));
if (!mLowMemoryHandle) {
return false;
}
MutexAutoLock lock(mMutex);
return ListenForLowMemory(lock);
}
void nsAvailableMemoryWatcher::UnregisterMemoryResourceHandler() {
if (mWaitHandle) {
bool res = ::UnregisterWait(mWaitHandle);
if (res || ::GetLastError() != ERROR_IO_PENDING) {
// We decrement the refcount only when we're sure the LowMemoryCallback()
// callback won't be invoked, otherwise the callback will do it
this->Release();
}
mWaitHandle = nullptr;
}
mLowMemoryHandle.reset();
}
void nsAvailableMemoryWatcher::Shutdown(const MutexAutoLock&) {
mIsShutdown = true;
mNeedToRestartTimerOnUserInteracting = false;
if (mTimer) {
mTimer->Cancel();
mTimer = nullptr;
}
UnregisterMemoryResourceHandler();
}
bool nsAvailableMemoryWatcher::ListenForLowMemory(const MutexAutoLock&) {
if (mLowMemoryHandle && !mWaitHandle) {
// We're giving ownership of this object to the LowMemoryCallback(). We
// increment the count here so that the object is kept alive until the
// callback decrements it.
this->AddRef();
bool res = ::RegisterWaitForSingleObject(
&mWaitHandle, mLowMemoryHandle, LowMemoryCallback, this, INFINITE,
WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE);
if (!res) {
// We couldn't register the callback, decrement the count
this->Release();
}
// Once we register the wait handle, we no longer need to start
// the timer because we can continue watching memory via callback.
mNeedToRestartTimerOnUserInteracting = false;
return res;
}
return false;
}
void nsAvailableMemoryWatcher::MaybeSaveMemoryReport(const MutexAutoLock&) {
if (mSavedReport) {
return;
}
if (nsCOMPtr<nsICrashReporter> cr =
do_GetService("@mozilla.org/toolkit/crash-reporter;1")) {
mSavedReport = NS_SUCCEEDED(cr->SaveMemoryReport());
}
}
void nsAvailableMemoryWatcher::OnLowMemory(const MutexAutoLock& aLock) {
mUnderMemoryPressure = true;
RecordLowMemoryEvent();
if (NS_IsMainThread()) {
MaybeSaveMemoryReport(aLock);
UpdateLowMemoryTimeStamp();
{
// Don't invoke UnloadTabAsync() with the lock to avoid deadlock
// because nsAvailableMemoryWatcher::Notify may be invoked while
// running the method.
MutexAutoUnlock unlock(mMutex);
mTabUnloader->UnloadTabAsync();
}
} else {
// SaveMemoryReport and mTabUnloader needs to be run in the main thread
// (See nsMemoryReporterManager::GetReportsForThisProcessExtended)
NS_DispatchToMainThread(NS_NewRunnableFunction(
"nsAvailableMemoryWatcher::OnLowMemory", [self = RefPtr{this}]() {
{
MutexAutoLock lock(self->mMutex);
self->MaybeSaveMemoryReport(lock);
self->UpdateLowMemoryTimeStamp();
}
self->mTabUnloader->UnloadTabAsync();
}));
}
StartPollingIfUserInteracting(aLock);
}
void nsAvailableMemoryWatcher::OnHighMemory(const MutexAutoLock& aLock) {
MOZ_ASSERT(NS_IsMainThread());
if (mUnderMemoryPressure) {
RecordTelemetryEventOnHighMemory();
NS_NotifyOfEventualMemoryPressure(MemoryPressureState::NoPressure);
}
mUnderMemoryPressure = false;
mSavedReport = false; // Will save a new report if memory gets low again
StopPolling();
ListenForLowMemory(aLock);
}
// static
bool nsAvailableMemoryWatcher::IsCommitSpaceLow() {
// Other options to get the available page file size:
// - GetPerformanceInfo
// Too slow, don't use it.
// - PdhCollectQueryData and PdhGetRawCounterValue
// Faster than GetPerformanceInfo, but slower than GlobalMemoryStatusEx.
// - NtQuerySystemInformation(SystemMemoryUsageInformation)
// Faster than GlobalMemoryStatusEx, but undocumented.
MEMORYSTATUSEX memStatus = {sizeof(memStatus)};
if (!::GlobalMemoryStatusEx(&memStatus)) {
return false;
}
constexpr size_t kBytesPerMB = 1024 * 1024;
return (memStatus.ullAvailPageFile / kBytesPerMB) <
StaticPrefs::browser_low_commit_space_threshold_mb();
}
void nsAvailableMemoryWatcher::StartPollingIfUserInteracting(
const MutexAutoLock&) {
// When the user is inactive, we mark the flag to delay-start
// the timer when the user becomes active later.
mNeedToRestartTimerOnUserInteracting = true;
if (mInteracting && !mPolling) {
if (NS_SUCCEEDED(mTimer->InitWithCallback(
this, mPollingInterval, nsITimer::TYPE_REPEATING_SLACK))) {
mPolling = true;
}
}
}
void nsAvailableMemoryWatcher::StopPolling() {
mTimer->Cancel();
mPolling = false;
}
void nsAvailableMemoryWatcher::StopPollingIfUserIdle(const MutexAutoLock&) {
if (!mInteracting) {
StopPolling();
}
}
void nsAvailableMemoryWatcher::OnUserInteracting(const MutexAutoLock& aLock) {
if (mNeedToRestartTimerOnUserInteracting) {
StartPollingIfUserInteracting(aLock);
}
}
// Timer callback, polls the low memory resource notification to detect when
// we've freed enough memory or if we have to send more memory pressure events.
// Polling stops automatically when the user is not interacting with the UI.
NS_IMETHODIMP
nsAvailableMemoryWatcher::Notify(nsITimer* aTimer) {
MutexAutoLock lock(mMutex);
StopPollingIfUserIdle(lock);
if (IsCommitSpaceLow()) {
OnLowMemory(lock);
} else {
OnHighMemory(lock);
}
return NS_OK;
}
NS_IMETHODIMP
nsAvailableMemoryWatcher::GetName(nsACString& aName) {
aName.AssignLiteral("nsAvailableMemoryWatcher");
return NS_OK;
}
// Observer service callback, used to stop the polling timer when the user
// stops interacting with Firefox and resuming it when they interact again.
// Also used to shut down the service if the application is quitting.
NS_IMETHODIMP
nsAvailableMemoryWatcher::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
nsresult rv = nsAvailableMemoryWatcherBase::Observe(aSubject, aTopic, aData);
if (NS_FAILED(rv)) {
return rv;
}
MutexAutoLock lock(mMutex);
if (strcmp(aTopic, "xpcom-shutdown") == 0) {
Shutdown(lock);
} else if (strcmp(aTopic, "user-interaction-active") == 0) {
OnUserInteracting(lock);
}
return NS_OK;
}
already_AddRefed<nsAvailableMemoryWatcherBase> CreateAvailableMemoryWatcher() {
RefPtr watcher(new nsAvailableMemoryWatcher);
if (NS_FAILED(watcher->Init())) {
return do_AddRef(new nsAvailableMemoryWatcherBase); // fallback
}
return watcher.forget();
}
} // namespace mozilla