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/. */
6
7 #include "AvailableMemoryWatcher.h"
8 #include "mozilla/Services.h"
9 #include "mozilla/StaticPrefs_browser.h"
10 #include "mozilla/Unused.h"
11 #include "nsAppRunner.h"
12 #include "nsExceptionHandler.h"
13 #include "nsICrashReporter.h"
14 #include "nsIObserverService.h"
15 #include "nsISupports.h"
16 #include "nsITimer.h"
17 #include "nsMemoryPressure.h"
18 #include "nsWindowsHelpers.h"
19
20 #include <memoryapi.h>
21
22 namespace mozilla {
23
24 // This class is used to monitor low memory events delivered by Windows via
25 // memory resource notification objects. When we enter a low memory scenario
26 // the LowMemoryCallback() is invoked by Windows. This initial call triggers
27 // an nsITimer that polls to see when the low memory condition has been lifted.
28 // When it has, we'll stop polling and start waiting for the next
29 // LowMemoryCallback(). Meanwhile, the polling may be stopped and restarted by
30 // user-interaction events from the observer service.
31 class nsAvailableMemoryWatcher final : public nsIObserver,
32 public nsITimerCallback,
33 public nsAvailableMemoryWatcherBase {
34 public:
35 NS_DECL_ISUPPORTS_INHERITED
36 NS_DECL_NSIOBSERVER
37 NS_DECL_NSITIMERCALLBACK
38
39 nsAvailableMemoryWatcher();
40 nsresult Init(uint32_t aPollingInterval);
41
42 private:
43 // Observer topics we subscribe to, see below.
44 static const char* const kObserverTopics[];
45
46 static VOID CALLBACK LowMemoryCallback(PVOID aContext, BOOLEAN aIsTimer);
47 static void RecordLowMemoryEvent();
48 static bool IsCommitSpaceLow();
49
50 ~nsAvailableMemoryWatcher();
51 bool RegisterMemoryResourceHandler();
52 void UnregisterMemoryResourceHandler();
53 void MaybeSaveMemoryReport(const MutexAutoLock&);
54 void Shutdown(const MutexAutoLock&);
55 bool ListenForLowMemory();
56 void OnLowMemory(const MutexAutoLock&);
57 void OnHighMemory(const MutexAutoLock&);
58 void StartPollingIfUserInteracting(const MutexAutoLock&);
59 void StopPolling();
60 void StopPollingIfUserIdle(const MutexAutoLock&);
61 void OnUserInteracting(const MutexAutoLock&);
62 void OnUserIdle(const MutexAutoLock&);
63
64 // The publicly available methods (::Observe() and ::Notify()) are called on
65 // the main thread while the ::LowMemoryCallback() method is called by an
66 // external thread. All functions called from those must acquire a lock on
67 // this mutex before accessing the object's fields to prevent races.
68 Mutex mMutex;
69 nsCOMPtr<nsITimer> mTimer;
70 nsAutoHandle mLowMemoryHandle;
71 HANDLE mWaitHandle;
72 bool mPolling;
73 bool mInteracting;
74 // Indicates whether to start a timer when user interaction is notified
75 bool mUnderMemoryPressure;
76 bool mSavedReport;
77 bool mIsShutdown;
78 // These members are used only in the main thread. No lock is needed.
79 bool mInitialized;
80 uint32_t mPollingInterval;
81 nsCOMPtr<nsIObserverService> mObserverSvc;
82 };
83
84 const char* const nsAvailableMemoryWatcher::kObserverTopics[] = {
85 // Use this shutdown phase to make sure the instance is destroyed in GTest
86 "xpcom-shutdown",
87 "user-interaction-active",
88 "user-interaction-inactive",
89 };
90
NS_IMPL_ISUPPORTS_INHERITED(nsAvailableMemoryWatcher,nsAvailableMemoryWatcherBase,nsIObserver,nsITimerCallback)91 NS_IMPL_ISUPPORTS_INHERITED(nsAvailableMemoryWatcher,
92 nsAvailableMemoryWatcherBase, nsIObserver,
93 nsITimerCallback)
94
95 nsAvailableMemoryWatcher::nsAvailableMemoryWatcher()
96 : mMutex("low memory callback mutex"),
97 mWaitHandle(nullptr),
98 mPolling(false),
99 mInteracting(false),
100 mUnderMemoryPressure(false),
101 mSavedReport(false),
102 mIsShutdown(false),
103 mInitialized(false),
104 mPollingInterval(0) {}
105
Init(uint32_t aPollingInterval)106 nsresult nsAvailableMemoryWatcher::Init(uint32_t aPollingInterval) {
107 MOZ_ASSERT(
108 NS_IsMainThread(),
109 "nsAvailableMemoryWatcher needs to be initialized in the main thread.");
110 if (mInitialized) {
111 return NS_ERROR_ALREADY_INITIALIZED;
112 }
113
114 mTimer = NS_NewTimer();
115 if (!mTimer) {
116 return NS_ERROR_OUT_OF_MEMORY;
117 }
118
119 mObserverSvc = services::GetObserverService();
120 MOZ_ASSERT(mObserverSvc);
121 mPollingInterval = aPollingInterval;
122
123 if (!RegisterMemoryResourceHandler()) {
124 return NS_ERROR_FAILURE;
125 }
126
127 for (auto topic : kObserverTopics) {
128 nsresult rv = mObserverSvc->AddObserver(this, topic,
129 /* ownsWeak */ false);
130 NS_ENSURE_SUCCESS(rv, rv);
131 }
132
133 mInitialized = true;
134 return NS_OK;
135 }
136
~nsAvailableMemoryWatcher()137 nsAvailableMemoryWatcher::~nsAvailableMemoryWatcher() {
138 // These handles should have been released during the shutdown phase.
139 MOZ_ASSERT(!mLowMemoryHandle);
140 MOZ_ASSERT(!mWaitHandle);
141 }
142
143 // static
LowMemoryCallback(PVOID aContext,BOOLEAN aIsTimer)144 VOID CALLBACK nsAvailableMemoryWatcher::LowMemoryCallback(PVOID aContext,
145 BOOLEAN aIsTimer) {
146 RefPtr<nsAvailableMemoryWatcher> watcher =
147 already_AddRefed<nsAvailableMemoryWatcher>(
148 static_cast<nsAvailableMemoryWatcher*>(aContext));
149 if (!aIsTimer) {
150 MutexAutoLock lock(watcher->mMutex);
151 if (watcher->mIsShutdown) {
152 // mWaitHandle should have been unregistered during shutdown
153 MOZ_ASSERT(!watcher->mWaitHandle);
154 return;
155 }
156
157 ::UnregisterWait(watcher->mWaitHandle);
158 watcher->mWaitHandle = nullptr;
159
160 // On Windows, memory allocations fails when the available commit space is
161 // not sufficient. It's possible that this callback function is invoked
162 // but there is still commit space enough for the application to continue
163 // to run. In such a case, there is no strong need to trigger the memory
164 // pressure event. So we trigger the event only when the available commit
165 // space is low.
166 if (IsCommitSpaceLow()) {
167 watcher->OnLowMemory(lock);
168 }
169 }
170 }
171
172 // static
RecordLowMemoryEvent()173 void nsAvailableMemoryWatcher::RecordLowMemoryEvent() {
174 sNumLowPhysicalMemEvents++;
175 CrashReporter::AnnotateCrashReport(
176 CrashReporter::Annotation::LowPhysicalMemoryEvents,
177 sNumLowPhysicalMemEvents);
178 }
179
RegisterMemoryResourceHandler()180 bool nsAvailableMemoryWatcher::RegisterMemoryResourceHandler() {
181 mLowMemoryHandle.own(
182 ::CreateMemoryResourceNotification(LowMemoryResourceNotification));
183
184 if (!mLowMemoryHandle) {
185 return false;
186 }
187
188 return ListenForLowMemory();
189 }
190
UnregisterMemoryResourceHandler()191 void nsAvailableMemoryWatcher::UnregisterMemoryResourceHandler() {
192 if (mWaitHandle) {
193 bool res = ::UnregisterWait(mWaitHandle);
194 if (res || ::GetLastError() != ERROR_IO_PENDING) {
195 // We decrement the refcount only when we're sure the LowMemoryCallback()
196 // callback won't be invoked, otherwise the callback will do it
197 this->Release();
198 }
199 mWaitHandle = nullptr;
200 }
201
202 mLowMemoryHandle.reset();
203 }
204
Shutdown(const MutexAutoLock &)205 void nsAvailableMemoryWatcher::Shutdown(const MutexAutoLock&) {
206 mIsShutdown = true;
207
208 for (auto topic : kObserverTopics) {
209 Unused << mObserverSvc->RemoveObserver(this, topic);
210 }
211
212 if (mTimer) {
213 mTimer->Cancel();
214 mTimer = nullptr;
215 }
216
217 UnregisterMemoryResourceHandler();
218 }
219
ListenForLowMemory()220 bool nsAvailableMemoryWatcher::ListenForLowMemory() {
221 if (mLowMemoryHandle && !mWaitHandle) {
222 // We're giving ownership of this object to the LowMemoryCallback(). We
223 // increment the count here so that the object is kept alive until the
224 // callback decrements it.
225 this->AddRef();
226 bool res = ::RegisterWaitForSingleObject(
227 &mWaitHandle, mLowMemoryHandle, LowMemoryCallback, this, INFINITE,
228 WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE);
229 if (!res) {
230 // We couldn't register the callback, decrement the count
231 this->Release();
232 }
233 return res;
234 }
235
236 return false;
237 }
238
MaybeSaveMemoryReport(const MutexAutoLock &)239 void nsAvailableMemoryWatcher::MaybeSaveMemoryReport(const MutexAutoLock&) {
240 if (mSavedReport) {
241 return;
242 }
243
244 if (nsCOMPtr<nsICrashReporter> cr =
245 do_GetService("@mozilla.org/toolkit/crash-reporter;1")) {
246 mSavedReport = NS_SUCCEEDED(cr->SaveMemoryReport());
247 }
248 }
249
OnLowMemory(const MutexAutoLock & aLock)250 void nsAvailableMemoryWatcher::OnLowMemory(const MutexAutoLock& aLock) {
251 mUnderMemoryPressure = true;
252 RecordLowMemoryEvent();
253
254 if (NS_IsMainThread()) {
255 MaybeSaveMemoryReport(aLock);
256 {
257 // Don't invoke UnloadTabAsync() with the lock to avoid deadlock
258 // because nsAvailableMemoryWatcher::Notify may be invoked while
259 // running the method.
260 MutexAutoUnlock unlock(mMutex);
261 mTabUnloader->UnloadTabAsync();
262 }
263 } else {
264 // SaveMemoryReport and mTabUnloader needs to be run in the main thread
265 // (See nsMemoryReporterManager::GetReportsForThisProcessExtended)
266 NS_DispatchToMainThread(NS_NewRunnableFunction(
267 "nsAvailableMemoryWatcher::OnLowMemory", [self = RefPtr{this}]() {
268 {
269 MutexAutoLock lock(self->mMutex);
270 self->MaybeSaveMemoryReport(lock);
271 }
272 self->mTabUnloader->UnloadTabAsync();
273 }));
274 }
275
276 StartPollingIfUserInteracting(aLock);
277 }
278
OnHighMemory(const MutexAutoLock &)279 void nsAvailableMemoryWatcher::OnHighMemory(const MutexAutoLock&) {
280 MOZ_ASSERT(NS_IsMainThread());
281
282 mUnderMemoryPressure = false;
283 mSavedReport = false; // Will save a new report if memory gets low again
284 NS_NotifyOfEventualMemoryPressure(MemoryPressureState::NoPressure);
285 StopPolling();
286 ListenForLowMemory();
287 }
288
289 // static
IsCommitSpaceLow()290 bool nsAvailableMemoryWatcher::IsCommitSpaceLow() {
291 // Other options to get the available page file size:
292 // - GetPerformanceInfo
293 // Too slow, don't use it.
294 // - PdhCollectQueryData and PdhGetRawCounterValue
295 // Faster than GetPerformanceInfo, but slower than GlobalMemoryStatusEx.
296 // - NtQuerySystemInformation(SystemMemoryUsageInformation)
297 // Faster than GlobalMemoryStatusEx, but undocumented.
298 MEMORYSTATUSEX memStatus = {sizeof(memStatus)};
299 if (!::GlobalMemoryStatusEx(&memStatus)) {
300 return false;
301 }
302
303 constexpr size_t kBytesPerMB = 1024 * 1024;
304 return (memStatus.ullAvailPageFile / kBytesPerMB) <
305 StaticPrefs::browser_low_commit_space_threshold_mb();
306 }
307
StartPollingIfUserInteracting(const MutexAutoLock &)308 void nsAvailableMemoryWatcher::StartPollingIfUserInteracting(
309 const MutexAutoLock&) {
310 if (mInteracting && !mPolling) {
311 if (NS_SUCCEEDED(mTimer->InitWithCallback(
312 this, mPollingInterval, nsITimer::TYPE_REPEATING_SLACK))) {
313 mPolling = true;
314 }
315 }
316 }
317
StopPolling()318 void nsAvailableMemoryWatcher::StopPolling() {
319 mTimer->Cancel();
320 mPolling = false;
321 }
322
StopPollingIfUserIdle(const MutexAutoLock &)323 void nsAvailableMemoryWatcher::StopPollingIfUserIdle(const MutexAutoLock&) {
324 if (!mInteracting) {
325 StopPolling();
326 }
327 }
328
OnUserInteracting(const MutexAutoLock & aLock)329 void nsAvailableMemoryWatcher::OnUserInteracting(const MutexAutoLock& aLock) {
330 mInteracting = true;
331 if (mUnderMemoryPressure) {
332 StartPollingIfUserInteracting(aLock);
333 }
334 }
335
OnUserIdle(const MutexAutoLock &)336 void nsAvailableMemoryWatcher::OnUserIdle(const MutexAutoLock&) {
337 mInteracting = false;
338 }
339
340 // Timer callback, polls the low memory resource notification to detect when
341 // we've freed enough memory or if we have to send more memory pressure events.
342 // Polling stops automatically when the user is not interacting with the UI.
343 NS_IMETHODIMP
Notify(nsITimer * aTimer)344 nsAvailableMemoryWatcher::Notify(nsITimer* aTimer) {
345 MutexAutoLock lock(mMutex);
346 StopPollingIfUserIdle(lock);
347
348 if (IsCommitSpaceLow()) {
349 OnLowMemory(lock);
350 } else {
351 OnHighMemory(lock);
352 }
353
354 return NS_OK;
355 }
356
357 // Observer service callback, used to stop the polling timer when the user
358 // stops interacting with Firefox and resuming it when they interact again.
359 // Also used to shut down the service if the application is quitting.
360 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)361 nsAvailableMemoryWatcher::Observe(nsISupports* aSubject, const char* aTopic,
362 const char16_t* aData) {
363 MutexAutoLock lock(mMutex);
364
365 if (strcmp(aTopic, "xpcom-shutdown") == 0) {
366 Shutdown(lock);
367 } else if (strcmp(aTopic, "user-interaction-inactive") == 0) {
368 OnUserIdle(lock);
369 } else if (strcmp(aTopic, "user-interaction-active") == 0) {
370 OnUserInteracting(lock);
371 } else {
372 MOZ_ASSERT_UNREACHABLE("Unknown topic");
373 }
374
375 return NS_OK;
376 }
377
CreateAvailableMemoryWatcher()378 already_AddRefed<nsAvailableMemoryWatcherBase> CreateAvailableMemoryWatcher() {
379 // Don't fire a low-memory notification more often than this interval.
380 // (Use a very short interval for GTest to verify the timer's behavior)
381 const uint32_t kLowMemoryNotificationIntervalMS = gIsGtest ? 10 : 10000;
382
383 RefPtr watcher(new nsAvailableMemoryWatcher);
384 if (NS_FAILED(watcher->Init(kLowMemoryNotificationIntervalMS))) {
385 return do_AddRef(new nsAvailableMemoryWatcherBase); // fallback
386 }
387 return watcher.forget();
388 }
389
390 } // namespace mozilla
391