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 <sys/sysctl.h>
11 #include "AvailableMemoryWatcher.h"
13 #include "mozilla/Preferences.h"
14 #include "nsICrashReporter.h"
15 #include "nsISupports.h"
17 #include "nsMemoryPressure.h"
18 #include "nsPrintfCString.h"
20 #define MP_LOG(...) MOZ_LOG(gMPLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
21 static mozilla::LazyLogModule
gMPLog("MemoryPressure");
26 * The Mac AvailableMemoryWatcher works as follows. When the OS memory pressure
27 * level changes on macOS, nsAvailableMemoryWatcher::OnMemoryPressureChanged()
28 * is called with the new memory pressure level. The level is represented in
29 * Gecko by a MacMemoryPressureLevel instance and represents the states of
30 * normal, warning, or critical which correspond to the native levels. When the
31 * browser launches, the initial level is determined using a sysctl. Which
32 * actions are taken in the browser in response to memory pressure, and the
33 * level (warning or critical) which trigger the reponse is configurable with
34 * prefs to make it easier to perform experiments to study how the response
35 * affects the user experience.
37 * By default, the browser responds by attempting to reduce memory use when the
38 * OS transitions to the critical level and while it stays in the critical
39 * level. i.e., "critical" OS memory pressure is the default threshold for the
40 * low memory response. Setting pref "browser.lowMemoryResponseOnWarn" to true
41 * changes the memory response to occur at the "warning" level which is less
42 * severe than "critical". When entering the critical level, we begin polling
43 * the memory pressure level every 'n' milliseconds (specified via the pref
44 * "browser.lowMemoryPollingIntervalMS"). Each time the poller wakes up and
45 * finds the OS still under memory pressure, the low memory response is
48 * By default, the memory pressure response is, in order, to
49 * 1) call nsITabUnloader::UnloadTabAsync(),
50 * 2) if no tabs could be unloaded, issue a Gecko
51 * MemoryPressureState::LowMemory notification.
52 * The response can be changed via the pref "browser.lowMemoryResponseMask" to
53 * limit the actions to only tab unloading or Gecko memory pressure
56 * Polling occurs on the main thread because, at each polling interval, we
57 * call into the tab unloader which requires being on the main thread.
58 * Polling only occurs while under OS memory pressure at the critical (by
61 class nsAvailableMemoryWatcher final
: public nsITimerCallback
,
63 public nsAvailableMemoryWatcherBase
{
65 NS_DECL_ISUPPORTS_INHERITED
67 NS_DECL_NSITIMERCALLBACK
70 nsAvailableMemoryWatcher();
71 nsresult
Init() override
;
73 void OnMemoryPressureChanged(MacMemoryPressureLevel aLevel
) override
;
74 void AddChildAnnotations(
75 const UniquePtr
<ipc::CrashReporterHost
>& aCrashReporter
) override
;
78 ~nsAvailableMemoryWatcher(){};
80 void OnMemoryPressureChangedInternal(MacMemoryPressureLevel aNewLevel
,
81 bool aIsInitialLevel
);
83 // Override OnUnloadAttemptCompleted() so that we can control whether
84 // or not a Gecko memory-pressure event is sent after a tab unload attempt.
85 // This method is called externally by the tab unloader after a tab unload
86 // attempt. It is used internally when tab unloading is disabled in
88 nsresult
OnUnloadAttemptCompleted(nsresult aResult
) override
;
93 void InitParentAnnotations();
94 void UpdateParentAnnotations();
96 void AddParentAnnotation(CrashReporter::Annotation aAnnotation
,
97 nsAutoCString aString
) {
98 CrashReporter::AnnotateCrashReport(aAnnotation
, aString
);
100 void AddParentAnnotation(CrashReporter::Annotation aAnnotation
,
102 CrashReporter::AnnotateCrashReport(aAnnotation
, aData
);
105 void LowMemoryResponse();
108 void RestartPolling();
109 inline bool IsPolling() { return mTimer
; }
113 // This enum represents the allowed values for the pref that controls
114 // the low memory response - "browser.lowMemoryResponseMask". Specifically,
115 // whether or not we unload tabs and/or issue the Gecko "memory-pressure"
116 // internal notification. For tab unloading, the pref
117 // "browser.tabs.unloadOnLowMemory" must also be set.
121 eInternalMemoryPressure
= 0x2,
124 static constexpr char kResponseMask
[] = "browser.lowMemoryResponseMask";
125 static const uint32_t kResponseMaskDefault
;
126 static const uint32_t kResponseMaskMax
;
128 // Pref for controlling how often we wake up during an OS memory pressure
129 // time period. At each wakeup, we unload tabs and issue the Gecko
130 // "memory-pressure" internal notification. When not under OS memory pressure,
131 // polling is disabled.
132 static constexpr char kPollingIntervalMS
[] =
133 "browser.lowMemoryPollingIntervalMS";
134 static const uint32_t kPollingIntervalMaxMS
;
135 static const uint32_t kPollingIntervalMinMS
;
136 static const uint32_t kPollingIntervalDefaultMS
;
138 static constexpr char kResponseOnWarn
[] = "browser.lowMemoryResponseOnWarn";
139 static const bool kResponseLevelOnWarnDefault
= false;
141 // Init has been called.
144 // The memory pressure reported to the application by macOS.
145 MacMemoryPressureLevel mLevel
;
147 // The OS memory pressure level that triggers the response.
148 MacMemoryPressureLevel mResponseLevel
;
150 // The value of the kern.memorystatus_vm_pressure_level sysctl. The OS
151 // notifies the application when the memory pressure level changes,
152 // but the sysctl value can be read at any time. Unofficially, the sysctl
153 // value corresponds to the OS memory pressure level with 4=>critical,
154 // 2=>warning, and 1=>normal (values from kernel event.h file).
155 uint32_t mLevelSysctl
;
156 static const int kSysctlLevelNormal
= 0x1;
157 static const int kSysctlLevelWarning
= 0x2;
158 static const int kSysctlLevelCritical
= 0x4;
160 // The value of the kern.memorystatus_level sysctl. Unofficially,
161 // this is the percentage of available memory. (Also readable
162 // via the undocumented memorystatus_get_level syscall.)
165 // The string representation of `mLevel`. i.e., normal, warning, or critical.
166 // Set to "unset" until a memory pressure change is reported to the process
168 nsAutoCString mLevelStr
;
170 // Timestamps for memory pressure level changes. Specifically, the Unix
171 // time in string form. Saved as Unix time to allow comparisons with
173 nsAutoCString mNormalTimeStr
;
174 nsAutoCString mWarningTimeStr
;
175 nsAutoCString mCriticalTimeStr
;
177 nsCOMPtr
<nsITimer
> mTimer
; // non-null indicates the timer is active
179 // Saved pref values.
180 uint32_t mPollingInterval
;
181 uint32_t mResponseMask
;
184 const uint32_t nsAvailableMemoryWatcher::kResponseMaskDefault
=
186 const uint32_t nsAvailableMemoryWatcher::kResponseMaskMax
= ResponseMask::eAll
;
189 const uint32_t nsAvailableMemoryWatcher::kPollingIntervalDefaultMS
= 10'000;
191 const uint32_t nsAvailableMemoryWatcher::kPollingIntervalMaxMS
= 600'000;
193 const uint32_t nsAvailableMemoryWatcher::kPollingIntervalMinMS
= 100;
195 NS_IMPL_ISUPPORTS_INHERITED(nsAvailableMemoryWatcher
,
196 nsAvailableMemoryWatcherBase
, nsIObserver
,
197 nsITimerCallback
, nsINamed
)
199 nsAvailableMemoryWatcher::nsAvailableMemoryWatcher()
200 : mInitialized(false),
201 mLevel(MacMemoryPressureLevel::Value::eUnset
),
202 mResponseLevel(MacMemoryPressureLevel::Value::eCritical
),
203 mLevelSysctl(0xFFFFFFFF),
206 mNormalTimeStr("Unset"),
207 mWarningTimeStr("Unset"),
208 mCriticalTimeStr("Unset"),
210 mResponseMask(ResponseMask::eAll
) {}
212 nsresult
nsAvailableMemoryWatcher::Init() {
213 nsresult rv
= nsAvailableMemoryWatcherBase::Init();
218 // Users of nsAvailableMemoryWatcher should use
219 // nsAvailableMemoryWatcherBase::GetSingleton() and not call Init directly.
220 MOZ_ASSERT(!mInitialized
);
222 return NS_ERROR_ALREADY_INITIALIZED
;
225 // Read polling frequency pref
227 Preferences::GetUint(kPollingIntervalMS
, kPollingIntervalDefaultMS
);
228 mPollingInterval
= std::clamp(mPollingInterval
, kPollingIntervalMinMS
,
229 kPollingIntervalMaxMS
);
231 // Read response bitmask pref which (along with the main tab unloading
232 // preference) controls whether or not tab unloading and Gecko (internal)
233 // memory pressure notifications will be sent. The main tab unloading
234 // preference must also be enabled for tab unloading to occur.
235 mResponseMask
= Preferences::GetUint(kResponseMask
, kResponseMaskDefault
);
236 if (mResponseMask
> kResponseMaskMax
) {
237 mResponseMask
= kResponseMaskMax
;
240 // Read response level pref
241 if (Preferences::GetBool(kResponseOnWarn
, kResponseLevelOnWarnDefault
)) {
242 mResponseLevel
= MacMemoryPressureLevel::Value::eWarning
;
244 mResponseLevel
= MacMemoryPressureLevel::Value::eCritical
;
248 MP_LOG("Initial memory pressure sysctl: %d", mLevelSysctl
);
249 MP_LOG("Initial available memory sysctl: %d", mAvailMemSysctl
);
251 // Set the initial state of all annotations for parent crash reports.
252 // Content process crash reports are set when a crash occurs and
253 // AddChildAnnotations() is called.
254 CrashReporter::AnnotateCrashReport(
255 CrashReporter::Annotation::MacMemoryPressure
, mLevelStr
);
256 CrashReporter::AnnotateCrashReport(
257 CrashReporter::Annotation::MacMemoryPressureNormalTime
, mNormalTimeStr
);
258 CrashReporter::AnnotateCrashReport(
259 CrashReporter::Annotation::MacMemoryPressureWarningTime
, mWarningTimeStr
);
260 CrashReporter::AnnotateCrashReport(
261 CrashReporter::Annotation::MacMemoryPressureCriticalTime
,
263 CrashReporter::AnnotateCrashReport(
264 CrashReporter::Annotation::MacMemoryPressureSysctl
, mLevelSysctl
);
265 CrashReporter::AnnotateCrashReport(
266 CrashReporter::Annotation::MacAvailableMemorySysctl
, mAvailMemSysctl
);
268 // To support running experiments, handle pref
269 // changes without requiring a browser restart.
270 rv
= Preferences::AddStrongObserver(this, kResponseMask
);
273 nsPrintfCString("Failed to add %s observer", kResponseMask
).get());
275 rv
= Preferences::AddStrongObserver(this, kPollingIntervalMS
);
278 nsPrintfCString("Failed to add %s observer", kPollingIntervalMS
).get());
280 rv
= Preferences::AddStrongObserver(this, kResponseOnWarn
);
283 nsPrintfCString("Failed to add %s observer", kResponseOnWarn
).get());
286 // Use the memory pressure sysctl to initialize our memory pressure state.
287 MacMemoryPressureLevel initialLevel
;
288 switch (mLevelSysctl
) {
289 case kSysctlLevelNormal
:
290 initialLevel
= MacMemoryPressureLevel::Value::eNormal
;
292 case kSysctlLevelWarning
:
293 initialLevel
= MacMemoryPressureLevel::Value::eWarning
;
295 case kSysctlLevelCritical
:
296 initialLevel
= MacMemoryPressureLevel::Value::eCritical
;
299 initialLevel
= MacMemoryPressureLevel::Value::eUnexpected
;
302 OnMemoryPressureChangedInternal(initialLevel
, /* aIsInitialLevel */ true);
307 already_AddRefed
<nsAvailableMemoryWatcherBase
> CreateAvailableMemoryWatcher() {
308 // Users of nsAvailableMemoryWatcher should use
309 // nsAvailableMemoryWatcherBase::GetSingleton().
310 RefPtr
watcher(new nsAvailableMemoryWatcher());
312 return watcher
.forget();
315 // Update the memory pressure level, level change timestamps, and sysctl
316 // level crash report annotations.
317 void nsAvailableMemoryWatcher::UpdateParentAnnotations() {
318 // Generate a string representation of the current Unix time.
319 time_t timeChanged
= time(NULL
);
320 nsAutoCString timeChangedString
;
322 nsPrintfCString("%" PRIu64
, static_cast<uint64_t>(timeChanged
));
324 nsAutoCString pressureLevelString
;
325 Maybe
<CrashReporter::Annotation
> pressureLevelKey
;
327 switch (mLevel
.GetValue()) {
328 case MacMemoryPressureLevel::Value::eNormal
:
329 mNormalTimeStr
= timeChangedString
;
330 pressureLevelString
= "Normal";
331 pressureLevelKey
.emplace(
332 CrashReporter::Annotation::MacMemoryPressureNormalTime
);
334 case MacMemoryPressureLevel::Value::eWarning
:
335 mWarningTimeStr
= timeChangedString
;
336 pressureLevelString
= "Warning";
337 pressureLevelKey
.emplace(
338 CrashReporter::Annotation::MacMemoryPressureWarningTime
);
340 case MacMemoryPressureLevel::Value::eCritical
:
341 mCriticalTimeStr
= timeChangedString
;
342 pressureLevelString
= "Critical";
343 pressureLevelKey
.emplace(
344 CrashReporter::Annotation::MacMemoryPressureCriticalTime
);
347 pressureLevelString
= "Unexpected";
351 // Save the current memory pressure level.
352 AddParentAnnotation(CrashReporter::Annotation::MacMemoryPressure
,
353 pressureLevelString
);
355 // Save the time we transitioned to the current memory pressure level.
356 if (pressureLevelKey
.isSome()) {
357 AddParentAnnotation(pressureLevelKey
.value(), timeChangedString
);
360 AddParentAnnotation(CrashReporter::Annotation::MacMemoryPressureSysctl
,
362 AddParentAnnotation(CrashReporter::Annotation::MacAvailableMemorySysctl
,
366 void nsAvailableMemoryWatcher::ReadSysctls() {
369 size_t size
= sizeof(level
);
370 if (sysctlbyname("kern.memorystatus_vm_pressure_level", &level
, &size
, NULL
,
372 MP_LOG("Failure reading memory pressure sysctl");
374 mLevelSysctl
= level
;
376 // Available memory percent
378 size
= sizeof(availPercent
);
379 if (sysctlbyname("kern.memorystatus_level", &availPercent
, &size
, NULL
, 0) ==
381 MP_LOG("Failure reading available memory level");
383 mAvailMemSysctl
= availPercent
;
387 void nsAvailableMemoryWatcher::OnMemoryPressureChanged(
388 MacMemoryPressureLevel aNewLevel
) {
389 MOZ_ASSERT(mInitialized
);
390 OnMemoryPressureChangedInternal(aNewLevel
, /* aIsInitialLevel */ false);
393 void nsAvailableMemoryWatcher::OnMemoryPressureChangedInternal(
394 MacMemoryPressureLevel aNewLevel
, bool aIsInitialLevel
) {
395 MOZ_ASSERT(mInitialized
|| aIsInitialLevel
);
396 MP_LOG("MemoryPressureChange: existing level: %s, new level: %s",
397 mLevel
.ToString(), aNewLevel
.ToString());
399 // If 'aNewLevel' is not one of normal, warning, or critical, ASSERT
400 // here so we can debug this scenario. For non-debug builds, ignore
401 // the unexpected value which will be logged in crash reports.
402 MOZ_ASSERT(aNewLevel
.IsNormal() || aNewLevel
.IsWarningOrAbove());
404 if (mLevel
== aNewLevel
) {
408 // Start the memory pressure response if the new level is high enough
409 // and the existing level was not.
410 if ((mLevel
< mResponseLevel
) && (aNewLevel
>= mResponseLevel
)) {
411 UpdateLowMemoryTimeStamp();
418 // End the memory pressure reponse if the new level is not high enough.
419 if ((mLevel
>= mResponseLevel
) && (aNewLevel
< mResponseLevel
)) {
421 MutexAutoLock
lock(mMutex
);
422 RecordTelemetryEventOnHighMemory(lock
);
425 MP_LOG("Issuing MemoryPressureState::NoPressure");
426 NS_NotifyOfMemoryPressure(MemoryPressureState::NoPressure
);
431 if (!aIsInitialLevel
) {
432 // Sysctls are already read by ::Init().
434 MP_LOG("level sysctl: %d, available memory: %d percent", mLevelSysctl
,
437 UpdateParentAnnotations();
441 // Add all annotations to the provided crash reporter instance.
442 void nsAvailableMemoryWatcher::AddChildAnnotations(
443 const UniquePtr
<ipc::CrashReporterHost
>& aCrashReporter
) {
444 aCrashReporter
->AddAnnotation(CrashReporter::Annotation::MacMemoryPressure
,
446 aCrashReporter
->AddAnnotation(
447 CrashReporter::Annotation::MacMemoryPressureNormalTime
, mNormalTimeStr
);
448 aCrashReporter
->AddAnnotation(
449 CrashReporter::Annotation::MacMemoryPressureWarningTime
, mWarningTimeStr
);
450 aCrashReporter
->AddAnnotation(
451 CrashReporter::Annotation::MacMemoryPressureCriticalTime
,
453 aCrashReporter
->AddAnnotation(
454 CrashReporter::Annotation::MacMemoryPressureSysctl
, mLevelSysctl
);
455 aCrashReporter
->AddAnnotation(
456 CrashReporter::Annotation::MacAvailableMemorySysctl
, mAvailMemSysctl
);
459 void nsAvailableMemoryWatcher::LowMemoryResponse() {
460 if (mResponseMask
& ResponseMask::eTabUnload
) {
461 MP_LOG("Attempting tab unload");
462 mTabUnloader
->UnloadTabAsync();
464 // Re-use OnUnloadAttemptCompleted() to issue the internal
465 // memory pressure event.
466 OnUnloadAttemptCompleted(NS_ERROR_NOT_AVAILABLE
);
471 nsAvailableMemoryWatcher::Notify(nsITimer
* aTimer
) {
472 MOZ_ASSERT(NS_IsMainThread());
473 MOZ_ASSERT(mLevel
>= mResponseLevel
);
478 // Override OnUnloadAttemptCompleted() so that we can issue Gecko memory
479 // pressure notifications only if eInternalMemoryPressure is set in
480 // mResponseMask. When called from the tab unloader, an |aResult| value of
481 // NS_OK indicates the tab unloader successfully unloaded a tab.
482 // NS_ERROR_NOT_AVAILABLE indicates the tab unloader did not unload any tabs.
484 nsAvailableMemoryWatcher::OnUnloadAttemptCompleted(nsresult aResult
) {
485 // On MacOS we don't access these members offthread; however we do on other
486 // OSes and so they are guarded by the mutex.
487 MutexAutoLock
lock(mMutex
);
489 // A tab was unloaded successfully.
491 MP_LOG("Tab unloaded");
492 ++mNumOfTabUnloading
;
495 // Either the tab unloader found no unloadable tabs OR we've been called
496 // locally to explicitly issue the internal memory pressure event because
497 // tab unloading is disabled in |mResponseMask|. In either case, attempt
498 // to reduce memory use using the internal memory pressure notification.
499 case NS_ERROR_NOT_AVAILABLE
:
500 if (mResponseMask
& ResponseMask::eInternalMemoryPressure
) {
501 ++mNumOfMemoryPressure
;
502 MP_LOG("Tab not unloaded");
503 MP_LOG("Issuing MemoryPressureState::LowMemory");
504 NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory
);
508 // There was a pending task to unload a tab.
513 MOZ_ASSERT_UNREACHABLE("Unexpected aResult");
520 nsAvailableMemoryWatcher::Observe(nsISupports
* aSubject
, const char* aTopic
,
521 const char16_t
* aData
) {
522 nsresult rv
= nsAvailableMemoryWatcherBase::Observe(aSubject
, aTopic
, aData
);
527 if (strcmp(aTopic
, "xpcom-shutdown") == 0) {
529 } else if (strcmp(aTopic
, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID
) == 0) {
535 void nsAvailableMemoryWatcher::OnShutdown() {
537 Preferences::RemoveObserver(this, kResponseMask
);
538 Preferences::RemoveObserver(this, kPollingIntervalMS
);
541 void nsAvailableMemoryWatcher::OnPrefChange() {
542 MP_LOG("OnPrefChange()");
543 // Handle the polling interval changing.
544 uint32_t pollingInterval
= Preferences::GetUint(kPollingIntervalMS
);
545 if (pollingInterval
!= mPollingInterval
) {
546 mPollingInterval
= std::clamp(pollingInterval
, kPollingIntervalMinMS
,
547 kPollingIntervalMaxMS
);
551 // Handle the response mask changing.
552 uint32_t responseMask
= Preferences::GetUint(kResponseMask
);
553 if (mResponseMask
!= responseMask
) {
554 mResponseMask
= std::min(responseMask
, kResponseMaskMax
);
556 // Do we need to turn on polling?
557 if (mResponseMask
&& (mLevel
>= mResponseLevel
) && !IsPolling()) {
561 // Do we need to turn off polling?
562 if (!mResponseMask
&& IsPolling()) {
567 // Handle the response level changing.
568 MacMemoryPressureLevel newResponseLevel
;
569 if (Preferences::GetBool(kResponseOnWarn
, kResponseLevelOnWarnDefault
)) {
570 newResponseLevel
= MacMemoryPressureLevel::Value::eWarning
;
572 newResponseLevel
= MacMemoryPressureLevel::Value::eCritical
;
574 if (newResponseLevel
== mResponseLevel
) {
578 // Do we need to turn on polling?
579 if (mResponseMask
&& (newResponseLevel
<= mLevel
)) {
580 UpdateLowMemoryTimeStamp();
585 // Do we need to turn off polling?
586 if (IsPolling() && (newResponseLevel
> mLevel
)) {
588 MutexAutoLock
lock(mMutex
);
589 RecordTelemetryEventOnHighMemory(lock
);
592 MP_LOG("Issuing MemoryPressureState::NoPressure");
593 NS_NotifyOfMemoryPressure(MemoryPressureState::NoPressure
);
595 mResponseLevel
= newResponseLevel
;
598 void nsAvailableMemoryWatcher::StartPolling() {
599 MOZ_ASSERT(NS_IsMainThread());
601 MP_LOG("Starting poller");
602 mTimer
= NS_NewTimer();
604 mTimer
->InitWithCallback(this, mPollingInterval
,
605 nsITimer::TYPE_REPEATING_SLACK
);
610 void nsAvailableMemoryWatcher::StopPolling() {
611 MOZ_ASSERT(NS_IsMainThread());
613 MP_LOG("Pausing poller");
619 void nsAvailableMemoryWatcher::RestartPolling() {
629 nsAvailableMemoryWatcher::GetName(nsACString
& aName
) {
630 aName
.AssignLiteral("nsAvailableMemoryWatcher");
634 } // namespace mozilla