/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include #include "AvailableMemoryWatcher.h" #include "Logging.h" #include "mozilla/Preferences.h" #include "nsICrashReporter.h" #include "nsISupports.h" #include "nsITimer.h" #include "nsMemoryPressure.h" #include "nsPrintfCString.h" #define MP_LOG(...) MOZ_LOG(gMPLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) static mozilla::LazyLogModule gMPLog("MemoryPressure"); namespace mozilla { /* * The Mac AvailableMemoryWatcher works as follows. When the OS memory pressure * level changes on macOS, nsAvailableMemoryWatcher::OnMemoryPressureChanged() * is called with the new memory pressure level. The level is represented in * Gecko by a MacMemoryPressureLevel instance and represents the states of * normal, warning, or critical which correspond to the native levels. When the * browser launches, the initial level is determined using a sysctl. Which * actions are taken in the browser in response to memory pressure, and the * level (warning or critical) which trigger the reponse is configurable with * prefs to make it easier to perform experiments to study how the response * affects the user experience. * * By default, the browser responds by attempting to reduce memory use when the * OS transitions to the critical level and while it stays in the critical * level. i.e., "critical" OS memory pressure is the default threshold for the * low memory response. Setting pref "browser.lowMemoryResponseOnWarn" to true * changes the memory response to occur at the "warning" level which is less * severe than "critical". When entering the critical level, we begin polling * the memory pressure level every 'n' milliseconds (specified via the pref * "browser.lowMemoryPollingIntervalMS"). Each time the poller wakes up and * finds the OS still under memory pressure, the low memory response is * executed. * * By default, the memory pressure response is, in order, to * 1) call nsITabUnloader::UnloadTabAsync(), * 2) if no tabs could be unloaded, issue a Gecko * MemoryPressureState::LowMemory notification. * The response can be changed via the pref "browser.lowMemoryResponseMask" to * limit the actions to only tab unloading or Gecko memory pressure * notifications. * * Polling occurs on the main thread because, at each polling interval, we * call into the tab unloader which requires being on the main thread. * Polling only occurs while under OS memory pressure at the critical (by * default) level. */ class nsAvailableMemoryWatcher final : public nsITimerCallback, public nsINamed, public nsAvailableMemoryWatcherBase { public: NS_DECL_ISUPPORTS_INHERITED NS_DECL_NSIOBSERVER NS_DECL_NSITIMERCALLBACK NS_DECL_NSINAMED nsAvailableMemoryWatcher(); nsresult Init() override; void OnMemoryPressureChanged(MacMemoryPressureLevel aLevel) override; void AddChildAnnotations( const UniquePtr& aCrashReporter) override; private: ~nsAvailableMemoryWatcher(){}; void OnMemoryPressureChangedInternal(MacMemoryPressureLevel aNewLevel, bool aIsInitialLevel); // Override OnUnloadAttemptCompleted() so that we can control whether // or not a Gecko memory-pressure event is sent after a tab unload attempt. // This method is called externally by the tab unloader after a tab unload // attempt. It is used internally when tab unloading is disabled in // mResponseMask. nsresult OnUnloadAttemptCompleted(nsresult aResult) override; void OnShutdown(); void OnPrefChange(); void InitParentAnnotations(); void UpdateParentAnnotations(); void AddParentAnnotation(CrashReporter::Annotation aAnnotation, nsAutoCString aString) { CrashReporter::RecordAnnotationNSCString(aAnnotation, aString); } void AddParentAnnotation(CrashReporter::Annotation aAnnotation, uint32_t aData) { CrashReporter::RecordAnnotationU32(aAnnotation, aData); } void LowMemoryResponse(); void StartPolling(); void StopPolling(); void RestartPolling(); inline bool IsPolling() { return mTimer; } void ReadSysctls(); // This enum represents the allowed values for the pref that controls // the low memory response - "browser.lowMemoryResponseMask". Specifically, // whether or not we unload tabs and/or issue the Gecko "memory-pressure" // internal notification. For tab unloading, the pref // "browser.tabs.unloadOnLowMemory" must also be set. enum ResponseMask { eNone = 0x0, eTabUnload = 0x1, eInternalMemoryPressure = 0x2, eAll = 0x3, }; static constexpr char kResponseMask[] = "browser.lowMemoryResponseMask"; static const uint32_t kResponseMaskDefault; static const uint32_t kResponseMaskMax; // Pref for controlling how often we wake up during an OS memory pressure // time period. At each wakeup, we unload tabs and issue the Gecko // "memory-pressure" internal notification. When not under OS memory pressure, // polling is disabled. static constexpr char kPollingIntervalMS[] = "browser.lowMemoryPollingIntervalMS"; static const uint32_t kPollingIntervalMaxMS; static const uint32_t kPollingIntervalMinMS; static const uint32_t kPollingIntervalDefaultMS; static constexpr char kResponseOnWarn[] = "browser.lowMemoryResponseOnWarn"; static const bool kResponseLevelOnWarnDefault = false; // Init has been called. bool mInitialized; // The memory pressure reported to the application by macOS. MacMemoryPressureLevel mLevel; // The OS memory pressure level that triggers the response. MacMemoryPressureLevel mResponseLevel; // The value of the kern.memorystatus_vm_pressure_level sysctl. The OS // notifies the application when the memory pressure level changes, // but the sysctl value can be read at any time. Unofficially, the sysctl // value corresponds to the OS memory pressure level with 4=>critical, // 2=>warning, and 1=>normal (values from kernel event.h file). uint32_t mLevelSysctl; static const int kSysctlLevelNormal = 0x1; static const int kSysctlLevelWarning = 0x2; static const int kSysctlLevelCritical = 0x4; // The value of the kern.memorystatus_level sysctl. Unofficially, // this is the percentage of available memory. (Also readable // via the undocumented memorystatus_get_level syscall.) int mAvailMemSysctl; // The string representation of `mLevel`. i.e., normal, warning, or critical. // Set to "unset" until a memory pressure change is reported to the process // by the OS. nsAutoCString mLevelStr; // Timestamps for memory pressure level changes. Specifically, the Unix // time in string form. Saved as Unix time to allow comparisons with // the crash time. nsAutoCString mNormalTimeStr; nsAutoCString mWarningTimeStr; nsAutoCString mCriticalTimeStr; nsCOMPtr mTimer; // non-null indicates the timer is active // Saved pref values. uint32_t mPollingInterval; uint32_t mResponseMask; }; const uint32_t nsAvailableMemoryWatcher::kResponseMaskDefault = ResponseMask::eAll; const uint32_t nsAvailableMemoryWatcher::kResponseMaskMax = ResponseMask::eAll; // 10 seconds const uint32_t nsAvailableMemoryWatcher::kPollingIntervalDefaultMS = 10'000; // 10 minutes const uint32_t nsAvailableMemoryWatcher::kPollingIntervalMaxMS = 600'000; // 100 milliseconds const uint32_t nsAvailableMemoryWatcher::kPollingIntervalMinMS = 100; NS_IMPL_ISUPPORTS_INHERITED(nsAvailableMemoryWatcher, nsAvailableMemoryWatcherBase, nsIObserver, nsITimerCallback, nsINamed) nsAvailableMemoryWatcher::nsAvailableMemoryWatcher() : mInitialized(false), mLevel(MacMemoryPressureLevel::Value::eUnset), mResponseLevel(MacMemoryPressureLevel::Value::eCritical), mLevelSysctl(0xFFFFFFFF), mAvailMemSysctl(-1), mLevelStr("Unset"), mNormalTimeStr("Unset"), mWarningTimeStr("Unset"), mCriticalTimeStr("Unset"), mPollingInterval(0), mResponseMask(ResponseMask::eAll) {} nsresult nsAvailableMemoryWatcher::Init() { nsresult rv = nsAvailableMemoryWatcherBase::Init(); if (NS_FAILED(rv)) { return rv; } // Users of nsAvailableMemoryWatcher should use // nsAvailableMemoryWatcherBase::GetSingleton() and not call Init directly. MOZ_ASSERT(!mInitialized); if (mInitialized) { return NS_ERROR_ALREADY_INITIALIZED; } // Read polling frequency pref mPollingInterval = Preferences::GetUint(kPollingIntervalMS, kPollingIntervalDefaultMS); mPollingInterval = std::clamp(mPollingInterval, kPollingIntervalMinMS, kPollingIntervalMaxMS); // Read response bitmask pref which (along with the main tab unloading // preference) controls whether or not tab unloading and Gecko (internal) // memory pressure notifications will be sent. The main tab unloading // preference must also be enabled for tab unloading to occur. mResponseMask = Preferences::GetUint(kResponseMask, kResponseMaskDefault); if (mResponseMask > kResponseMaskMax) { mResponseMask = kResponseMaskMax; } // Read response level pref if (Preferences::GetBool(kResponseOnWarn, kResponseLevelOnWarnDefault)) { mResponseLevel = MacMemoryPressureLevel::Value::eWarning; } else { mResponseLevel = MacMemoryPressureLevel::Value::eCritical; } ReadSysctls(); MP_LOG("Initial memory pressure sysctl: %d", mLevelSysctl); MP_LOG("Initial available memory sysctl: %d", mAvailMemSysctl); // Set the initial state of all annotations for parent crash reports. // Content process crash reports are set when a crash occurs and // AddChildAnnotations() is called. CrashReporter::RecordAnnotationNSCString( CrashReporter::Annotation::MacMemoryPressure, mLevelStr); CrashReporter::RecordAnnotationNSCString( CrashReporter::Annotation::MacMemoryPressureNormalTime, mNormalTimeStr); CrashReporter::RecordAnnotationNSCString( CrashReporter::Annotation::MacMemoryPressureWarningTime, mWarningTimeStr); CrashReporter::RecordAnnotationNSCString( CrashReporter::Annotation::MacMemoryPressureCriticalTime, mCriticalTimeStr); CrashReporter::RecordAnnotationU32( CrashReporter::Annotation::MacMemoryPressureSysctl, mLevelSysctl); CrashReporter::RecordAnnotationU32( CrashReporter::Annotation::MacAvailableMemorySysctl, mAvailMemSysctl); // To support running experiments, handle pref // changes without requiring a browser restart. rv = Preferences::AddStrongObserver(this, kResponseMask); if (NS_FAILED(rv)) { NS_WARNING( nsPrintfCString("Failed to add %s observer", kResponseMask).get()); } rv = Preferences::AddStrongObserver(this, kPollingIntervalMS); if (NS_FAILED(rv)) { NS_WARNING( nsPrintfCString("Failed to add %s observer", kPollingIntervalMS).get()); } rv = Preferences::AddStrongObserver(this, kResponseOnWarn); if (NS_FAILED(rv)) { NS_WARNING( nsPrintfCString("Failed to add %s observer", kResponseOnWarn).get()); } // Use the memory pressure sysctl to initialize our memory pressure state. MacMemoryPressureLevel initialLevel; switch (mLevelSysctl) { case kSysctlLevelNormal: initialLevel = MacMemoryPressureLevel::Value::eNormal; break; case kSysctlLevelWarning: initialLevel = MacMemoryPressureLevel::Value::eWarning; break; case kSysctlLevelCritical: initialLevel = MacMemoryPressureLevel::Value::eCritical; break; default: initialLevel = MacMemoryPressureLevel::Value::eUnexpected; } OnMemoryPressureChangedInternal(initialLevel, /* aIsInitialLevel */ true); mInitialized = true; return NS_OK; } already_AddRefed CreateAvailableMemoryWatcher() { // Users of nsAvailableMemoryWatcher should use // nsAvailableMemoryWatcherBase::GetSingleton(). RefPtr watcher(new nsAvailableMemoryWatcher()); watcher->Init(); return watcher.forget(); } // Update the memory pressure level, level change timestamps, and sysctl // level crash report annotations. void nsAvailableMemoryWatcher::UpdateParentAnnotations() { // Generate a string representation of the current Unix time. time_t timeChanged = time(NULL); nsAutoCString timeChangedString; timeChangedString = nsPrintfCString("%" PRIu64, static_cast(timeChanged)); nsAutoCString pressureLevelString; Maybe pressureLevelKey; switch (mLevel.GetValue()) { case MacMemoryPressureLevel::Value::eNormal: mNormalTimeStr = timeChangedString; pressureLevelString = "Normal"; pressureLevelKey.emplace( CrashReporter::Annotation::MacMemoryPressureNormalTime); break; case MacMemoryPressureLevel::Value::eWarning: mWarningTimeStr = timeChangedString; pressureLevelString = "Warning"; pressureLevelKey.emplace( CrashReporter::Annotation::MacMemoryPressureWarningTime); break; case MacMemoryPressureLevel::Value::eCritical: mCriticalTimeStr = timeChangedString; pressureLevelString = "Critical"; pressureLevelKey.emplace( CrashReporter::Annotation::MacMemoryPressureCriticalTime); break; default: pressureLevelString = "Unexpected"; break; } // Save the current memory pressure level. AddParentAnnotation(CrashReporter::Annotation::MacMemoryPressure, pressureLevelString); // Save the time we transitioned to the current memory pressure level. if (pressureLevelKey.isSome()) { AddParentAnnotation(pressureLevelKey.value(), timeChangedString); } AddParentAnnotation(CrashReporter::Annotation::MacMemoryPressureSysctl, mLevelSysctl); AddParentAnnotation(CrashReporter::Annotation::MacAvailableMemorySysctl, mAvailMemSysctl); } void nsAvailableMemoryWatcher::ReadSysctls() { // Pressure level uint32_t level; size_t size = sizeof(level); if (sysctlbyname("kern.memorystatus_vm_pressure_level", &level, &size, NULL, 0) == -1) { MP_LOG("Failure reading memory pressure sysctl"); } mLevelSysctl = level; // Available memory percent int availPercent; size = sizeof(availPercent); if (sysctlbyname("kern.memorystatus_level", &availPercent, &size, NULL, 0) == -1) { MP_LOG("Failure reading available memory level"); } mAvailMemSysctl = availPercent; } /* virtual */ void nsAvailableMemoryWatcher::OnMemoryPressureChanged( MacMemoryPressureLevel aNewLevel) { MOZ_ASSERT(mInitialized); OnMemoryPressureChangedInternal(aNewLevel, /* aIsInitialLevel */ false); } void nsAvailableMemoryWatcher::OnMemoryPressureChangedInternal( MacMemoryPressureLevel aNewLevel, bool aIsInitialLevel) { MOZ_ASSERT(mInitialized || aIsInitialLevel); MP_LOG("MemoryPressureChange: existing level: %s, new level: %s", mLevel.ToString(), aNewLevel.ToString()); // If 'aNewLevel' is not one of normal, warning, or critical, ASSERT // here so we can debug this scenario. For non-debug builds, ignore // the unexpected value which will be logged in crash reports. MOZ_ASSERT(aNewLevel.IsNormal() || aNewLevel.IsWarningOrAbove()); if (mLevel == aNewLevel) { return; } // Start the memory pressure response if the new level is high enough // and the existing level was not. if ((mLevel < mResponseLevel) && (aNewLevel >= mResponseLevel)) { UpdateLowMemoryTimeStamp(); LowMemoryResponse(); if (mResponseMask) { StartPolling(); } } // End the memory pressure reponse if the new level is not high enough. if ((mLevel >= mResponseLevel) && (aNewLevel < mResponseLevel)) { { MutexAutoLock lock(mMutex); RecordTelemetryEventOnHighMemory(lock); } StopPolling(); MP_LOG("Issuing MemoryPressureState::NoPressure"); NS_NotifyOfMemoryPressure(MemoryPressureState::NoPressure); } mLevel = aNewLevel; if (!aIsInitialLevel) { // Sysctls are already read by ::Init(). ReadSysctls(); MP_LOG("level sysctl: %d, available memory: %d percent", mLevelSysctl, mAvailMemSysctl); } UpdateParentAnnotations(); } /* virtual */ // Add all annotations to the provided crash reporter instance. void nsAvailableMemoryWatcher::AddChildAnnotations( const UniquePtr& aCrashReporter) { aCrashReporter->AddAnnotationNSCString( CrashReporter::Annotation::MacMemoryPressure, mLevelStr); aCrashReporter->AddAnnotationNSCString( CrashReporter::Annotation::MacMemoryPressureNormalTime, mNormalTimeStr); aCrashReporter->AddAnnotationNSCString( CrashReporter::Annotation::MacMemoryPressureWarningTime, mWarningTimeStr); aCrashReporter->AddAnnotationNSCString( CrashReporter::Annotation::MacMemoryPressureCriticalTime, mCriticalTimeStr); aCrashReporter->AddAnnotationU32( CrashReporter::Annotation::MacMemoryPressureSysctl, mLevelSysctl); aCrashReporter->AddAnnotationU32( CrashReporter::Annotation::MacAvailableMemorySysctl, mAvailMemSysctl); } void nsAvailableMemoryWatcher::LowMemoryResponse() { if (mResponseMask & ResponseMask::eTabUnload) { MP_LOG("Attempting tab unload"); mTabUnloader->UnloadTabAsync(); } else { // Re-use OnUnloadAttemptCompleted() to issue the internal // memory pressure event. OnUnloadAttemptCompleted(NS_ERROR_NOT_AVAILABLE); } } NS_IMETHODIMP nsAvailableMemoryWatcher::Notify(nsITimer* aTimer) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mLevel >= mResponseLevel); LowMemoryResponse(); return NS_OK; } // Override OnUnloadAttemptCompleted() so that we can issue Gecko memory // pressure notifications only if eInternalMemoryPressure is set in // mResponseMask. When called from the tab unloader, an |aResult| value of // NS_OK indicates the tab unloader successfully unloaded a tab. // NS_ERROR_NOT_AVAILABLE indicates the tab unloader did not unload any tabs. NS_IMETHODIMP nsAvailableMemoryWatcher::OnUnloadAttemptCompleted(nsresult aResult) { // On MacOS we don't access these members offthread; however we do on other // OSes and so they are guarded by the mutex. MutexAutoLock lock(mMutex); switch (aResult) { // A tab was unloaded successfully. case NS_OK: MP_LOG("Tab unloaded"); ++mNumOfTabUnloading; break; // Either the tab unloader found no unloadable tabs OR we've been called // locally to explicitly issue the internal memory pressure event because // tab unloading is disabled in |mResponseMask|. In either case, attempt // to reduce memory use using the internal memory pressure notification. case NS_ERROR_NOT_AVAILABLE: if (mResponseMask & ResponseMask::eInternalMemoryPressure) { ++mNumOfMemoryPressure; MP_LOG("Tab not unloaded"); MP_LOG("Issuing MemoryPressureState::LowMemory"); NS_NotifyOfEventualMemoryPressure(MemoryPressureState::LowMemory); } break; // There was a pending task to unload a tab. case NS_ERROR_ABORT: break; default: MOZ_ASSERT_UNREACHABLE("Unexpected aResult"); break; } return NS_OK; } NS_IMETHODIMP nsAvailableMemoryWatcher::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { nsresult rv = nsAvailableMemoryWatcherBase::Observe(aSubject, aTopic, aData); if (NS_FAILED(rv)) { return rv; } if (strcmp(aTopic, "xpcom-shutdown") == 0) { OnShutdown(); } else if (strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) { OnPrefChange(); } return NS_OK; } void nsAvailableMemoryWatcher::OnShutdown() { StopPolling(); Preferences::RemoveObserver(this, kResponseMask); Preferences::RemoveObserver(this, kPollingIntervalMS); } void nsAvailableMemoryWatcher::OnPrefChange() { MP_LOG("OnPrefChange()"); // Handle the polling interval changing. uint32_t pollingInterval = Preferences::GetUint(kPollingIntervalMS); if (pollingInterval != mPollingInterval) { mPollingInterval = std::clamp(pollingInterval, kPollingIntervalMinMS, kPollingIntervalMaxMS); RestartPolling(); } // Handle the response mask changing. uint32_t responseMask = Preferences::GetUint(kResponseMask); if (mResponseMask != responseMask) { mResponseMask = std::min(responseMask, kResponseMaskMax); // Do we need to turn on polling? if (mResponseMask && (mLevel >= mResponseLevel) && !IsPolling()) { StartPolling(); } // Do we need to turn off polling? if (!mResponseMask && IsPolling()) { StopPolling(); } } // Handle the response level changing. MacMemoryPressureLevel newResponseLevel; if (Preferences::GetBool(kResponseOnWarn, kResponseLevelOnWarnDefault)) { newResponseLevel = MacMemoryPressureLevel::Value::eWarning; } else { newResponseLevel = MacMemoryPressureLevel::Value::eCritical; } if (newResponseLevel == mResponseLevel) { return; } // Do we need to turn on polling? if (mResponseMask && (newResponseLevel <= mLevel)) { UpdateLowMemoryTimeStamp(); LowMemoryResponse(); StartPolling(); } // Do we need to turn off polling? if (IsPolling() && (newResponseLevel > mLevel)) { { MutexAutoLock lock(mMutex); RecordTelemetryEventOnHighMemory(lock); } StopPolling(); MP_LOG("Issuing MemoryPressureState::NoPressure"); NS_NotifyOfMemoryPressure(MemoryPressureState::NoPressure); } mResponseLevel = newResponseLevel; } void nsAvailableMemoryWatcher::StartPolling() { MOZ_ASSERT(NS_IsMainThread()); if (!mTimer) { MP_LOG("Starting poller"); mTimer = NS_NewTimer(); if (mTimer) { mTimer->InitWithCallback(this, mPollingInterval, nsITimer::TYPE_REPEATING_SLACK); } } } void nsAvailableMemoryWatcher::StopPolling() { MOZ_ASSERT(NS_IsMainThread()); if (mTimer) { MP_LOG("Pausing poller"); mTimer->Cancel(); mTimer = nullptr; } } void nsAvailableMemoryWatcher::RestartPolling() { if (IsPolling()) { StopPolling(); StartPolling(); } else { MOZ_ASSERT(!mTimer); } } NS_IMETHODIMP nsAvailableMemoryWatcher::GetName(nsACString& aName) { aName.AssignLiteral("nsAvailableMemoryWatcher"); return NS_OK; } } // namespace mozilla