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
;
127 nsAvailableMemoryWatcher::~nsAvailableMemoryWatcher() {
128 // These handles should have been released during the shutdown phase.
129 MOZ_ASSERT(!mLowMemoryHandle
);
130 MOZ_ASSERT(!mWaitHandle
);
134 VOID CALLBACK
nsAvailableMemoryWatcher::LowMemoryCallback(PVOID aContext
,
140 // The |watcher| was addref'ed when we registered the wait handle in
141 // ListenForLowMemory(). It is decremented when exiting the function,
142 // so please make sure we unregister the wait handle after this line.
143 RefPtr
<nsAvailableMemoryWatcher
> watcher
=
144 already_AddRefed
<nsAvailableMemoryWatcher
>(
145 static_cast<nsAvailableMemoryWatcher
*>(aContext
));
147 MutexAutoLock
lock(watcher
->mMutex
);
148 if (watcher
->mIsShutdown
) {
149 // mWaitHandle should have been unregistered during shutdown
150 MOZ_ASSERT(!watcher
->mWaitHandle
);
154 ::UnregisterWait(watcher
->mWaitHandle
);
155 watcher
->mWaitHandle
= nullptr;
157 // On Windows, memory allocations fails when the available commit space is
158 // not sufficient. It's possible that this callback function is invoked
159 // but there is still commit space enough for the application to continue
160 // to run. In such a case, there is no strong need to trigger the memory
161 // pressure event. So we trigger the event only when the available commit
163 if (IsCommitSpaceLow()) {
164 watcher
->OnLowMemory(lock
);
166 // Since we have unregistered the callback, we need to start a timer to
167 // continue watching memory.
168 watcher
->StartPollingIfUserInteracting(lock
);
173 void nsAvailableMemoryWatcher::RecordLowMemoryEvent() {
174 sNumLowPhysicalMemEvents
++;
175 CrashReporter::AnnotateCrashReport(
176 CrashReporter::Annotation::LowPhysicalMemoryEvents
,
177 sNumLowPhysicalMemEvents
);
180 bool nsAvailableMemoryWatcher::RegisterMemoryResourceHandler(
181 const MutexAutoLock
& aLock
) {
182 mLowMemoryHandle
.own(
183 ::CreateMemoryResourceNotification(LowMemoryResourceNotification
));
185 if (!mLowMemoryHandle
) {
189 return ListenForLowMemory(aLock
);
192 void nsAvailableMemoryWatcher::UnregisterMemoryResourceHandler(
193 const MutexAutoLock
&) {
195 bool res
= ::UnregisterWait(mWaitHandle
);
196 if (res
|| ::GetLastError() != ERROR_IO_PENDING
) {
197 // We decrement the refcount only when we're sure the LowMemoryCallback()
198 // callback won't be invoked, otherwise the callback will do it
201 mWaitHandle
= nullptr;
204 mLowMemoryHandle
.reset();
207 void nsAvailableMemoryWatcher::Shutdown(const MutexAutoLock
& aLock
) {
209 mNeedToRestartTimerOnUserInteracting
= false;
216 UnregisterMemoryResourceHandler(aLock
);
219 bool nsAvailableMemoryWatcher::ListenForLowMemory(const MutexAutoLock
&) {
220 if (mLowMemoryHandle
&& !mWaitHandle
) {
221 // We're giving ownership of this object to the LowMemoryCallback(). We
222 // increment the count here so that the object is kept alive until the
223 // callback decrements it.
225 bool res
= ::RegisterWaitForSingleObject(
226 &mWaitHandle
, mLowMemoryHandle
, LowMemoryCallback
, this, INFINITE
,
227 WT_EXECUTEDEFAULT
| WT_EXECUTEONLYONCE
);
229 // We couldn't register the callback, decrement the count
232 // Once we register the wait handle, we no longer need to start
233 // the timer because we can continue watching memory via callback.
234 mNeedToRestartTimerOnUserInteracting
= false;
241 void nsAvailableMemoryWatcher::MaybeSaveMemoryReport(const MutexAutoLock
&) {
246 if (nsCOMPtr
<nsICrashReporter
> cr
=
247 do_GetService("@mozilla.org/toolkit/crash-reporter;1")) {
248 mSavedReport
= NS_SUCCEEDED(cr
->SaveMemoryReport());
252 void nsAvailableMemoryWatcher::OnLowMemory(const MutexAutoLock
& aLock
) {
253 mUnderMemoryPressure
= true;
254 RecordLowMemoryEvent();
256 if (NS_IsMainThread()) {
257 MaybeSaveMemoryReport(aLock
);
258 UpdateLowMemoryTimeStamp();
260 // Don't invoke UnloadTabAsync() with the lock to avoid deadlock
261 // because nsAvailableMemoryWatcher::Notify may be invoked while
262 // running the method.
263 MutexAutoUnlock
unlock(mMutex
);
264 mTabUnloader
->UnloadTabAsync();
267 // SaveMemoryReport and mTabUnloader needs to be run in the main thread
268 // (See nsMemoryReporterManager::GetReportsForThisProcessExtended)
269 NS_DispatchToMainThread(NS_NewRunnableFunction(
270 "nsAvailableMemoryWatcher::OnLowMemory", [self
= RefPtr
{this}]() {
272 MutexAutoLock
lock(self
->mMutex
);
273 self
->MaybeSaveMemoryReport(lock
);
274 self
->UpdateLowMemoryTimeStamp();
276 self
->mTabUnloader
->UnloadTabAsync();
280 StartPollingIfUserInteracting(aLock
);
283 void nsAvailableMemoryWatcher::OnHighMemory(const MutexAutoLock
& aLock
) {
284 MOZ_ASSERT(NS_IsMainThread());
286 if (mUnderMemoryPressure
) {
287 RecordTelemetryEventOnHighMemory(aLock
);
288 NS_NotifyOfEventualMemoryPressure(MemoryPressureState::NoPressure
);
291 mUnderMemoryPressure
= false;
292 mSavedReport
= false; // Will save a new report if memory gets low again
294 ListenForLowMemory(aLock
);
298 bool nsAvailableMemoryWatcher::IsCommitSpaceLow() {
299 // Other options to get the available page file size:
300 // - GetPerformanceInfo
301 // Too slow, don't use it.
302 // - PdhCollectQueryData and PdhGetRawCounterValue
303 // Faster than GetPerformanceInfo, but slower than GlobalMemoryStatusEx.
304 // - NtQuerySystemInformation(SystemMemoryUsageInformation)
305 // Faster than GlobalMemoryStatusEx, but undocumented.
306 MEMORYSTATUSEX memStatus
= {sizeof(memStatus
)};
307 if (!::GlobalMemoryStatusEx(&memStatus
)) {
311 constexpr size_t kBytesPerMB
= 1024 * 1024;
312 return (memStatus
.ullAvailPageFile
/ kBytesPerMB
) <
313 StaticPrefs::browser_low_commit_space_threshold_mb();
316 void nsAvailableMemoryWatcher::StartPollingIfUserInteracting(
317 const MutexAutoLock
&) {
318 // When the user is inactive, we mark the flag to delay-start
319 // the timer when the user becomes active later.
320 mNeedToRestartTimerOnUserInteracting
= true;
322 if (mInteracting
&& !mPolling
) {
323 if (NS_SUCCEEDED(mTimer
->InitWithCallback(
324 this, mPollingInterval
, nsITimer::TYPE_REPEATING_SLACK
))) {
330 void nsAvailableMemoryWatcher::StopPolling(const MutexAutoLock
&) {
335 void nsAvailableMemoryWatcher::StopPollingIfUserIdle(
336 const MutexAutoLock
& aLock
) {
342 void nsAvailableMemoryWatcher::OnUserInteracting(const MutexAutoLock
& aLock
) {
343 if (mNeedToRestartTimerOnUserInteracting
) {
344 StartPollingIfUserInteracting(aLock
);
348 // Timer callback, polls the low memory resource notification to detect when
349 // we've freed enough memory or if we have to send more memory pressure events.
350 // Polling stops automatically when the user is not interacting with the UI.
352 nsAvailableMemoryWatcher::Notify(nsITimer
* aTimer
) {
353 MutexAutoLock
lock(mMutex
);
354 StopPollingIfUserIdle(lock
);
356 if (IsCommitSpaceLow()) {
366 nsAvailableMemoryWatcher::GetName(nsACString
& aName
) {
367 aName
.AssignLiteral("nsAvailableMemoryWatcher");
371 // Observer service callback, used to stop the polling timer when the user
372 // stops interacting with Firefox and resuming it when they interact again.
373 // Also used to shut down the service if the application is quitting.
375 nsAvailableMemoryWatcher::Observe(nsISupports
* aSubject
, const char* aTopic
,
376 const char16_t
* aData
) {
377 nsresult rv
= nsAvailableMemoryWatcherBase::Observe(aSubject
, aTopic
, aData
);
382 MutexAutoLock
lock(mMutex
);
384 if (strcmp(aTopic
, "xpcom-shutdown") == 0) {
386 } else if (strcmp(aTopic
, "user-interaction-active") == 0) {
387 OnUserInteracting(lock
);
393 already_AddRefed
<nsAvailableMemoryWatcherBase
> CreateAvailableMemoryWatcher() {
394 RefPtr
watcher(new nsAvailableMemoryWatcher
);
395 if (NS_FAILED(watcher
->Init())) {
396 return do_AddRef(new nsAvailableMemoryWatcherBase
); // fallback
398 return watcher
.forget();
401 } // namespace mozilla