mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 05:41:12 +00:00
01adc17c9a
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
402 lines
13 KiB
C++
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
|