Backed out changeset f1426851ae30 (bug 1844755) for causing failures on test_printpre...
[gecko.git] / xpcom / base / AvailableMemoryWatcherWin.cpp
blobcd027366debbba55532981cc5cda97a107c5177d
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"
16 #include "nsITimer.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;
26 namespace mozilla {
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,
36 public nsINamed,
37 public nsAvailableMemoryWatcherBase {
38 public:
39 NS_DECL_ISUPPORTS_INHERITED
40 NS_DECL_NSIOBSERVER
41 NS_DECL_NSITIMERCALLBACK
42 NS_DECL_NSINAMED
44 nsAvailableMemoryWatcher();
45 nsresult Init() override;
47 private:
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)
54 MOZ_REQUIRES(mMutex);
55 void UnregisterMemoryResourceHandler(const MutexAutoLock&)
56 MOZ_REQUIRES(mMutex);
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)
63 MOZ_REQUIRES(mMutex);
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.
86 // No lock is needed.
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),
98 mPolling(false),
99 mNeedToRestartTimerOnUserInteracting(false),
100 mUnderMemoryPressure(false),
101 mSavedReport(false),
102 mIsShutdown(false),
103 mPollingInterval(0) {}
105 nsresult nsAvailableMemoryWatcher::Init() {
106 nsresult rv = nsAvailableMemoryWatcherBase::Init();
107 if (NS_FAILED(rv)) {
108 return rv;
111 MutexAutoLock lock(mMutex);
112 mTimer = NS_NewTimer();
113 if (!mTimer) {
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 return NS_OK;
127 nsAvailableMemoryWatcher::~nsAvailableMemoryWatcher() {
128 // These handles should have been released during the shutdown phase.
129 MOZ_ASSERT(!mLowMemoryHandle);
130 MOZ_ASSERT(!mWaitHandle);
133 // static
134 VOID CALLBACK nsAvailableMemoryWatcher::LowMemoryCallback(PVOID aContext,
135 BOOLEAN aIsTimer) {
136 if (aIsTimer) {
137 return;
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);
151 return;
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
162 // space is low.
163 if (IsCommitSpaceLow()) {
164 watcher->OnLowMemory(lock);
165 } else {
166 // Since we have unregistered the callback, we need to start a timer to
167 // continue watching memory.
168 watcher->StartPollingIfUserInteracting(lock);
172 // static
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) {
186 return false;
189 return ListenForLowMemory(aLock);
192 void nsAvailableMemoryWatcher::UnregisterMemoryResourceHandler(
193 const MutexAutoLock&) {
194 if (mWaitHandle) {
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
199 this->Release();
201 mWaitHandle = nullptr;
204 mLowMemoryHandle.reset();
207 void nsAvailableMemoryWatcher::Shutdown(const MutexAutoLock& aLock) {
208 mIsShutdown = true;
209 mNeedToRestartTimerOnUserInteracting = false;
211 if (mTimer) {
212 mTimer->Cancel();
213 mTimer = nullptr;
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.
224 this->AddRef();
225 bool res = ::RegisterWaitForSingleObject(
226 &mWaitHandle, mLowMemoryHandle, LowMemoryCallback, this, INFINITE,
227 WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE);
228 if (!res) {
229 // We couldn't register the callback, decrement the count
230 this->Release();
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;
235 return res;
238 return false;
241 void nsAvailableMemoryWatcher::MaybeSaveMemoryReport(const MutexAutoLock&) {
242 if (mSavedReport) {
243 return;
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();
266 } else {
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();
277 }));
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
293 StopPolling(aLock);
294 ListenForLowMemory(aLock);
297 // static
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)) {
308 return false;
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))) {
325 mPolling = true;
330 void nsAvailableMemoryWatcher::StopPolling(const MutexAutoLock&) {
331 mTimer->Cancel();
332 mPolling = false;
335 void nsAvailableMemoryWatcher::StopPollingIfUserIdle(
336 const MutexAutoLock& aLock) {
337 if (!mInteracting) {
338 StopPolling(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.
351 NS_IMETHODIMP
352 nsAvailableMemoryWatcher::Notify(nsITimer* aTimer) {
353 MutexAutoLock lock(mMutex);
354 StopPollingIfUserIdle(lock);
356 if (IsCommitSpaceLow()) {
357 OnLowMemory(lock);
358 } else {
359 OnHighMemory(lock);
362 return NS_OK;
365 NS_IMETHODIMP
366 nsAvailableMemoryWatcher::GetName(nsACString& aName) {
367 aName.AssignLiteral("nsAvailableMemoryWatcher");
368 return NS_OK;
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.
374 NS_IMETHODIMP
375 nsAvailableMemoryWatcher::Observe(nsISupports* aSubject, const char* aTopic,
376 const char16_t* aData) {
377 nsresult rv = nsAvailableMemoryWatcherBase::Observe(aSubject, aTopic, aData);
378 if (NS_FAILED(rv)) {
379 return rv;
382 MutexAutoLock lock(mMutex);
384 if (strcmp(aTopic, "xpcom-shutdown") == 0) {
385 Shutdown(lock);
386 } else if (strcmp(aTopic, "user-interaction-active") == 0) {
387 OnUserInteracting(lock);
390 return NS_OK;
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