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/. */
7 #include "AvailableMemoryWatcher.h"
8 #include "mozilla/Atomics.h"
9 #include "mozilla/Services.h"
10 #include "mozilla/StaticPrefs_browser.h"
11 #include "nsAppRunner.h"
12 #include "nsExceptionHandler.h"
13 #include "nsICrashReporter.h"
14 #include "nsIObserver.h"
15 #include "nsISupports.h"
17 #include "nsMemoryPressure.h"
18 #include "nsServiceManagerUtils.h"
19 #include "nsWindowsHelpers.h"
21 #include <memoryapi.h>
23 extern mozilla::Atomic
<uint32_t, mozilla::MemoryOrdering::Relaxed
>
24 sNumLowPhysicalMemEvents
;
28 // This class is used to monitor low memory events delivered by Windows via
29 // memory resource notification objects. When we enter a low memory scenario
30 // the LowMemoryCallback() is invoked by Windows. This initial call triggers
31 // an nsITimer that polls to see when the low memory condition has been lifted.
32 // When it has, we'll stop polling and start waiting for the next
33 // LowMemoryCallback(). Meanwhile, the polling may be stopped and restarted by
34 // user-interaction events from the observer service.
35 class nsAvailableMemoryWatcher final
: public nsITimerCallback
,
37 public nsAvailableMemoryWatcherBase
{
39 NS_DECL_ISUPPORTS_INHERITED
41 NS_DECL_NSITIMERCALLBACK
44 nsAvailableMemoryWatcher();
45 nsresult
Init() override
;
48 static VOID CALLBACK
LowMemoryCallback(PVOID aContext
, BOOLEAN aIsTimer
);
49 static void RecordLowMemoryEvent();
50 static bool IsCommitSpaceLow();
52 ~nsAvailableMemoryWatcher();
53 bool RegisterMemoryResourceHandler(const MutexAutoLock
& aLock
)
55 void UnregisterMemoryResourceHandler(const MutexAutoLock
&)
57 void MaybeSaveMemoryReport(const MutexAutoLock
&) MOZ_REQUIRES(mMutex
);
58 void Shutdown(const MutexAutoLock
& aLock
) MOZ_REQUIRES(mMutex
);
59 bool ListenForLowMemory(const MutexAutoLock
&) MOZ_REQUIRES(mMutex
);
60 void OnLowMemory(const MutexAutoLock
&) MOZ_REQUIRES(mMutex
);
61 void OnHighMemory(const MutexAutoLock
&) MOZ_REQUIRES(mMutex
);
62 void StartPollingIfUserInteracting(const MutexAutoLock
& aLock
)
64 void StopPolling(const MutexAutoLock
&) MOZ_REQUIRES(mMutex
);
65 void StopPollingIfUserIdle(const MutexAutoLock
&) MOZ_REQUIRES(mMutex
);
66 void OnUserInteracting(const MutexAutoLock
&) MOZ_REQUIRES(mMutex
);
68 nsCOMPtr
<nsITimer
> mTimer
MOZ_GUARDED_BY(mMutex
);
69 nsAutoHandle mLowMemoryHandle
MOZ_GUARDED_BY(mMutex
);
70 HANDLE mWaitHandle
MOZ_GUARDED_BY(mMutex
);
71 bool mPolling
MOZ_GUARDED_BY(mMutex
);
73 // Indicates whether to start a timer when user interaction is notified.
74 // This flag is needed because the low-memory callback may be triggered when
75 // the user is inactive and we want to delay-start the timer.
76 bool mNeedToRestartTimerOnUserInteracting
MOZ_GUARDED_BY(mMutex
);
77 // Indicate that the available commit space is low. The timer handler needs
78 // this flag because it is triggered by the low physical memory regardless
79 // of the available commit space.
80 bool mUnderMemoryPressure
MOZ_GUARDED_BY(mMutex
);
82 bool mSavedReport
MOZ_GUARDED_BY(mMutex
);
83 bool mIsShutdown
MOZ_GUARDED_BY(mMutex
);
85 // Members below this line are used only in the main thread.
88 // Don't fire a low-memory notification more often than this interval.
89 uint32_t mPollingInterval
;
92 NS_IMPL_ISUPPORTS_INHERITED(nsAvailableMemoryWatcher
,
93 nsAvailableMemoryWatcherBase
, nsIObserver
,
94 nsITimerCallback
, nsINamed
)
96 nsAvailableMemoryWatcher::nsAvailableMemoryWatcher()
97 : mWaitHandle(nullptr),
99 mNeedToRestartTimerOnUserInteracting(false),
100 mUnderMemoryPressure(false),
103 mPollingInterval(0) {}
105 nsresult
nsAvailableMemoryWatcher::Init() {
106 nsresult rv
= nsAvailableMemoryWatcherBase::Init();
111 MutexAutoLock
lock(mMutex
);
112 mTimer
= NS_NewTimer();
114 return NS_ERROR_OUT_OF_MEMORY
;
117 // Use a very short interval for GTest to verify the timer's behavior.
118 mPollingInterval
= gIsGtest
? 10 : 10000;
120 if (!RegisterMemoryResourceHandler(lock
)) {
121 return NS_ERROR_FAILURE
;
124 static_assert(sizeof(sNumLowPhysicalMemEvents
) == sizeof(uint32_t));
126 CrashReporter::RegisterAnnotationU32(
127 CrashReporter::Annotation::LowPhysicalMemoryEvents
,
128 reinterpret_cast<uint32_t*>(&sNumLowPhysicalMemEvents
));
133 nsAvailableMemoryWatcher::~nsAvailableMemoryWatcher() {
134 // These handles should have been released during the shutdown phase.
135 MOZ_ASSERT(!mLowMemoryHandle
);
136 MOZ_ASSERT(!mWaitHandle
);
140 VOID CALLBACK
nsAvailableMemoryWatcher::LowMemoryCallback(PVOID aContext
,
146 // The |watcher| was addref'ed when we registered the wait handle in
147 // ListenForLowMemory(). It is decremented when exiting the function,
148 // so please make sure we unregister the wait handle after this line.
149 RefPtr
<nsAvailableMemoryWatcher
> watcher
=
150 already_AddRefed
<nsAvailableMemoryWatcher
>(
151 static_cast<nsAvailableMemoryWatcher
*>(aContext
));
153 MutexAutoLock
lock(watcher
->mMutex
);
154 if (watcher
->mIsShutdown
) {
155 // mWaitHandle should have been unregistered during shutdown
156 MOZ_ASSERT(!watcher
->mWaitHandle
);
160 ::UnregisterWait(watcher
->mWaitHandle
);
161 watcher
->mWaitHandle
= nullptr;
163 // On Windows, memory allocations fails when the available commit space is
164 // not sufficient. It's possible that this callback function is invoked
165 // but there is still commit space enough for the application to continue
166 // to run. In such a case, there is no strong need to trigger the memory
167 // pressure event. So we trigger the event only when the available commit
169 if (IsCommitSpaceLow()) {
170 watcher
->OnLowMemory(lock
);
172 // Since we have unregistered the callback, we need to start a timer to
173 // continue watching memory.
174 watcher
->StartPollingIfUserInteracting(lock
);
179 void nsAvailableMemoryWatcher::RecordLowMemoryEvent() {
180 sNumLowPhysicalMemEvents
++;
183 bool nsAvailableMemoryWatcher::RegisterMemoryResourceHandler(
184 const MutexAutoLock
& aLock
) {
185 mLowMemoryHandle
.own(
186 ::CreateMemoryResourceNotification(LowMemoryResourceNotification
));
188 if (!mLowMemoryHandle
) {
192 return ListenForLowMemory(aLock
);
195 void nsAvailableMemoryWatcher::UnregisterMemoryResourceHandler(
196 const MutexAutoLock
&) {
198 bool res
= ::UnregisterWait(mWaitHandle
);
199 if (res
|| ::GetLastError() != ERROR_IO_PENDING
) {
200 // We decrement the refcount only when we're sure the LowMemoryCallback()
201 // callback won't be invoked, otherwise the callback will do it
204 mWaitHandle
= nullptr;
207 mLowMemoryHandle
.reset();
210 void nsAvailableMemoryWatcher::Shutdown(const MutexAutoLock
& aLock
) {
212 mNeedToRestartTimerOnUserInteracting
= false;
219 UnregisterMemoryResourceHandler(aLock
);
222 bool nsAvailableMemoryWatcher::ListenForLowMemory(const MutexAutoLock
&) {
223 if (mLowMemoryHandle
&& !mWaitHandle
) {
224 // We're giving ownership of this object to the LowMemoryCallback(). We
225 // increment the count here so that the object is kept alive until the
226 // callback decrements it.
228 bool res
= ::RegisterWaitForSingleObject(
229 &mWaitHandle
, mLowMemoryHandle
, LowMemoryCallback
, this, INFINITE
,
230 WT_EXECUTEDEFAULT
| WT_EXECUTEONLYONCE
);
232 // We couldn't register the callback, decrement the count
235 // Once we register the wait handle, we no longer need to start
236 // the timer because we can continue watching memory via callback.
237 mNeedToRestartTimerOnUserInteracting
= false;
244 void nsAvailableMemoryWatcher::MaybeSaveMemoryReport(const MutexAutoLock
&) {
249 if (nsCOMPtr
<nsICrashReporter
> cr
=
250 do_GetService("@mozilla.org/toolkit/crash-reporter;1")) {
251 mSavedReport
= NS_SUCCEEDED(cr
->SaveMemoryReport());
255 void nsAvailableMemoryWatcher::OnLowMemory(const MutexAutoLock
& aLock
) {
256 mUnderMemoryPressure
= true;
257 RecordLowMemoryEvent();
259 if (NS_IsMainThread()) {
260 MaybeSaveMemoryReport(aLock
);
261 UpdateLowMemoryTimeStamp();
263 // Don't invoke UnloadTabAsync() with the lock to avoid deadlock
264 // because nsAvailableMemoryWatcher::Notify may be invoked while
265 // running the method.
266 MutexAutoUnlock
unlock(mMutex
);
267 mTabUnloader
->UnloadTabAsync();
270 // SaveMemoryReport and mTabUnloader needs to be run in the main thread
271 // (See nsMemoryReporterManager::GetReportsForThisProcessExtended)
272 NS_DispatchToMainThread(NS_NewRunnableFunction(
273 "nsAvailableMemoryWatcher::OnLowMemory", [self
= RefPtr
{this}]() {
275 MutexAutoLock
lock(self
->mMutex
);
276 self
->MaybeSaveMemoryReport(lock
);
277 self
->UpdateLowMemoryTimeStamp();
279 self
->mTabUnloader
->UnloadTabAsync();
283 StartPollingIfUserInteracting(aLock
);
286 void nsAvailableMemoryWatcher::OnHighMemory(const MutexAutoLock
& aLock
) {
287 MOZ_ASSERT(NS_IsMainThread());
289 if (mUnderMemoryPressure
) {
290 RecordTelemetryEventOnHighMemory(aLock
);
291 NS_NotifyOfEventualMemoryPressure(MemoryPressureState::NoPressure
);
294 mUnderMemoryPressure
= false;
295 mSavedReport
= false; // Will save a new report if memory gets low again
297 ListenForLowMemory(aLock
);
301 bool nsAvailableMemoryWatcher::IsCommitSpaceLow() {
302 // Other options to get the available page file size:
303 // - GetPerformanceInfo
304 // Too slow, don't use it.
305 // - PdhCollectQueryData and PdhGetRawCounterValue
306 // Faster than GetPerformanceInfo, but slower than GlobalMemoryStatusEx.
307 // - NtQuerySystemInformation(SystemMemoryUsageInformation)
308 // Faster than GlobalMemoryStatusEx, but undocumented.
309 MEMORYSTATUSEX memStatus
= {sizeof(memStatus
)};
310 if (!::GlobalMemoryStatusEx(&memStatus
)) {
314 constexpr size_t kBytesPerMB
= 1024 * 1024;
315 return (memStatus
.ullAvailPageFile
/ kBytesPerMB
) <
316 StaticPrefs::browser_low_commit_space_threshold_mb();
319 void nsAvailableMemoryWatcher::StartPollingIfUserInteracting(
320 const MutexAutoLock
&) {
321 // When the user is inactive, we mark the flag to delay-start
322 // the timer when the user becomes active later.
323 mNeedToRestartTimerOnUserInteracting
= true;
325 if (mInteracting
&& !mPolling
) {
326 if (NS_SUCCEEDED(mTimer
->InitWithCallback(
327 this, mPollingInterval
, nsITimer::TYPE_REPEATING_SLACK
))) {
333 void nsAvailableMemoryWatcher::StopPolling(const MutexAutoLock
&) {
338 void nsAvailableMemoryWatcher::StopPollingIfUserIdle(
339 const MutexAutoLock
& aLock
) {
345 void nsAvailableMemoryWatcher::OnUserInteracting(const MutexAutoLock
& aLock
) {
346 if (mNeedToRestartTimerOnUserInteracting
) {
347 StartPollingIfUserInteracting(aLock
);
351 // Timer callback, polls the low memory resource notification to detect when
352 // we've freed enough memory or if we have to send more memory pressure events.
353 // Polling stops automatically when the user is not interacting with the UI.
355 nsAvailableMemoryWatcher::Notify(nsITimer
* aTimer
) {
356 MutexAutoLock
lock(mMutex
);
357 StopPollingIfUserIdle(lock
);
359 if (IsCommitSpaceLow()) {
369 nsAvailableMemoryWatcher::GetName(nsACString
& aName
) {
370 aName
.AssignLiteral("nsAvailableMemoryWatcher");
374 // Observer service callback, used to stop the polling timer when the user
375 // stops interacting with Firefox and resuming it when they interact again.
376 // Also used to shut down the service if the application is quitting.
378 nsAvailableMemoryWatcher::Observe(nsISupports
* aSubject
, const char* aTopic
,
379 const char16_t
* aData
) {
380 nsresult rv
= nsAvailableMemoryWatcherBase::Observe(aSubject
, aTopic
, aData
);
385 MutexAutoLock
lock(mMutex
);
387 if (strcmp(aTopic
, "xpcom-shutdown") == 0) {
389 } else if (strcmp(aTopic
, "user-interaction-active") == 0) {
390 OnUserInteracting(lock
);
396 already_AddRefed
<nsAvailableMemoryWatcherBase
> CreateAvailableMemoryWatcher() {
397 RefPtr
watcher(new nsAvailableMemoryWatcher
);
398 if (NS_FAILED(watcher
->Init())) {
399 return do_AddRef(new nsAvailableMemoryWatcherBase
); // fallback
401 return watcher
.forget();
404 } // namespace mozilla