summaryrefslogtreecommitdiffstats
path: root/xpcom/base/AvailableMemoryWatcherMac.cpp
blob: 452b32a14907ef4f3a2bafd84e1837af2cccd6eb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
/* -*- 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 <sys/sysctl.h>
#include <sys/types.h>
#include <time.h>

#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<ipc::CrashReporterHost>& 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::AnnotateCrashReport(aAnnotation, aString);
  }
  void AddParentAnnotation(CrashReporter::Annotation aAnnotation,
                           uint32_t aData) {
    CrashReporter::AnnotateCrashReport(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<nsITimer> 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::AnnotateCrashReport(
      CrashReporter::Annotation::MacMemoryPressure, mLevelStr);
  CrashReporter::AnnotateCrashReport(
      CrashReporter::Annotation::MacMemoryPressureNormalTime, mNormalTimeStr);
  CrashReporter::AnnotateCrashReport(
      CrashReporter::Annotation::MacMemoryPressureWarningTime, mWarningTimeStr);
  CrashReporter::AnnotateCrashReport(
      CrashReporter::Annotation::MacMemoryPressureCriticalTime,
      mCriticalTimeStr);
  CrashReporter::AnnotateCrashReport(
      CrashReporter::Annotation::MacMemoryPressureSysctl, mLevelSysctl);
  CrashReporter::AnnotateCrashReport(
      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<nsAvailableMemoryWatcherBase> 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<uint64_t>(timeChanged));

  nsAutoCString pressureLevelString;
  Maybe<CrashReporter::Annotation> 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)) {
    RecordTelemetryEventOnHighMemory();
    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<ipc::CrashReporterHost>& aCrashReporter) {
  aCrashReporter->AddAnnotation(CrashReporter::Annotation::MacMemoryPressure,
                                mLevelStr);
  aCrashReporter->AddAnnotation(
      CrashReporter::Annotation::MacMemoryPressureNormalTime, mNormalTimeStr);
  aCrashReporter->AddAnnotation(
      CrashReporter::Annotation::MacMemoryPressureWarningTime, mWarningTimeStr);
  aCrashReporter->AddAnnotation(
      CrashReporter::Annotation::MacMemoryPressureCriticalTime,
      mCriticalTimeStr);
  aCrashReporter->AddAnnotation(
      CrashReporter::Annotation::MacMemoryPressureSysctl, mLevelSysctl);
  aCrashReporter->AddAnnotation(
      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) {
  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)) {
    RecordTelemetryEventOnHighMemory();
    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