Bug 1910362 - Create new Nimbus helper r=aaronmt,ohorvath
[gecko.git] / xpcom / base / AvailableMemoryWatcherWin.cpp
blob1effbe3226576be137717c2e6b448c15aa3bba14
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 static_assert(sizeof(sNumLowPhysicalMemEvents) == sizeof(uint32_t));
126 CrashReporter::RegisterAnnotationU32(
127 CrashReporter::Annotation::LowPhysicalMemoryEvents,
128 reinterpret_cast<uint32_t*>(&sNumLowPhysicalMemEvents));
130 return NS_OK;
133 nsAvailableMemoryWatcher::~nsAvailableMemoryWatcher() {
134 // These handles should have been released during the shutdown phase.
135 MOZ_ASSERT(!mLowMemoryHandle);
136 MOZ_ASSERT(!mWaitHandle);
139 // static
140 VOID CALLBACK nsAvailableMemoryWatcher::LowMemoryCallback(PVOID aContext,
141 BOOLEAN aIsTimer) {
142 if (aIsTimer) {
143 return;
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);
157 return;
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
168 // space is low.
169 if (IsCommitSpaceLow()) {
170 watcher->OnLowMemory(lock);
171 } else {
172 // Since we have unregistered the callback, we need to start a timer to
173 // continue watching memory.
174 watcher->StartPollingIfUserInteracting(lock);
178 // static
179 void nsAvailableMemoryWatcher::RecordLowMemoryEvent() {
180 sNumLowPhysicalMemEvents++;
183 bool nsAvailableMemoryWatcher::RegisterMemoryResourceHandler(
184 const MutexAutoLock& aLock) {
185 mLowMemoryHandle.own(
186 ::CreateMemoryResourceNotification(LowMemoryResourceNotification));
188 if (!mLowMemoryHandle) {
189 return false;
192 return ListenForLowMemory(aLock);
195 void nsAvailableMemoryWatcher::UnregisterMemoryResourceHandler(
196 const MutexAutoLock&) {
197 if (mWaitHandle) {
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
202 this->Release();
204 mWaitHandle = nullptr;
207 mLowMemoryHandle.reset();
210 void nsAvailableMemoryWatcher::Shutdown(const MutexAutoLock& aLock) {
211 mIsShutdown = true;
212 mNeedToRestartTimerOnUserInteracting = false;
214 if (mTimer) {
215 mTimer->Cancel();
216 mTimer = nullptr;
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.
227 this->AddRef();
228 bool res = ::RegisterWaitForSingleObject(
229 &mWaitHandle, mLowMemoryHandle, LowMemoryCallback, this, INFINITE,
230 WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE);
231 if (!res) {
232 // We couldn't register the callback, decrement the count
233 this->Release();
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;
238 return res;
241 return false;
244 void nsAvailableMemoryWatcher::MaybeSaveMemoryReport(const MutexAutoLock&) {
245 if (mSavedReport) {
246 return;
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();
269 } else {
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();
280 }));
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
296 StopPolling(aLock);
297 ListenForLowMemory(aLock);
300 // static
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)) {
311 return false;
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))) {
328 mPolling = true;
333 void nsAvailableMemoryWatcher::StopPolling(const MutexAutoLock&) {
334 mTimer->Cancel();
335 mPolling = false;
338 void nsAvailableMemoryWatcher::StopPollingIfUserIdle(
339 const MutexAutoLock& aLock) {
340 if (!mInteracting) {
341 StopPolling(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.
354 NS_IMETHODIMP
355 nsAvailableMemoryWatcher::Notify(nsITimer* aTimer) {
356 MutexAutoLock lock(mMutex);
357 StopPollingIfUserIdle(lock);
359 if (IsCommitSpaceLow()) {
360 OnLowMemory(lock);
361 } else {
362 OnHighMemory(lock);
365 return NS_OK;
368 NS_IMETHODIMP
369 nsAvailableMemoryWatcher::GetName(nsACString& aName) {
370 aName.AssignLiteral("nsAvailableMemoryWatcher");
371 return NS_OK;
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.
377 NS_IMETHODIMP
378 nsAvailableMemoryWatcher::Observe(nsISupports* aSubject, const char* aTopic,
379 const char16_t* aData) {
380 nsresult rv = nsAvailableMemoryWatcherBase::Observe(aSubject, aTopic, aData);
381 if (NS_FAILED(rv)) {
382 return rv;
385 MutexAutoLock lock(mMutex);
387 if (strcmp(aTopic, "xpcom-shutdown") == 0) {
388 Shutdown(lock);
389 } else if (strcmp(aTopic, "user-interaction-active") == 0) {
390 OnUserInteracting(lock);
393 return NS_OK;
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