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