summaryrefslogtreecommitdiffstats
path: root/widget/gonk/GonkMemoryPressureMonitoring.cpp
blob: 0fafb37cf5d3717d77ee879bcbd12e17121c81a1 (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
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=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 <android/log.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/sysinfo.h>

#include "GonkMemoryPressureMonitoring.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/FileUtils.h"
#include "mozilla/Monitor.h"
#include "mozilla/Preferences.h"
#include "mozilla/ProcessPriorityManager.h"
#include "mozilla/Services.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsMemoryPressure.h"
#include "nsPrintfCString.h"
#include "nsThreadUtils.h"

#define LOG(args...)  \
  __android_log_print(ANDROID_LOG_INFO, "GonkMemoryPressure" , ## args)

using namespace mozilla;

namespace {

/**
 * MemoryPressureWatcher watches sysfs from its own thread to notice when the
 * system is under memory pressure.  When we observe memory pressure, we use
 * MemoryPressureRunnable to notify observers that they should release memory.
 *
 * When the system is under memory pressure, we don't want to constantly fire
 * memory-pressure events.  So instead, we try to detect when sysfs indicates
 * that we're no longer under memory pressure, and only then start firing events
 * again.
 *
 * (This is a bit problematic because we can't poll() to detect when we're no
 * longer under memory pressure; instead we have to periodically read the sysfs
 * node.  If we remain under memory pressure for a long time, this means we'll
 * continue waking up to read from the node for a long time, potentially wasting
 * battery life.  Hopefully we don't hit this case in practice!  We write to
 * logcat each time we go around this loop so it's at least noticable.)
 *
 * Shutting down safely is a bit of a chore.  XPCOM won't shut down until all
 * threads exit, so we need to exit the Run() method below on shutdown.  But our
 * thread might be blocked in one of two situations: We might be poll()'ing the
 * sysfs node waiting for memory pressure to occur, or we might be asleep
 * waiting to read() the sysfs node to see if we're no longer under memory
 * pressure.
 *
 * To let us wake up from the poll(), we poll() not just the sysfs node but also
 * a pipe, which we write to on shutdown.  To let us wake up from sleeping
 * between read()s, we sleep by Wait()'ing on a monitor, which we notify on
 * shutdown.
 */
class MemoryPressureWatcher final
  : public nsIRunnable
  , public nsIObserver
{
public:
  MemoryPressureWatcher()
    : mMonitor("MemoryPressureWatcher")
    , mLowMemTriggerKB(0)
    , mPageSize(0)
    , mShuttingDown(false)
    , mTriggerFd(-1)
    , mShutdownPipeRead(-1)
    , mShutdownPipeWrite(-1)
  {
  }

  NS_DECL_THREADSAFE_ISUPPORTS

  nsresult Init()
  {
    nsCOMPtr<nsIObserverService> os = services::GetObserverService();
    NS_ENSURE_STATE(os);

    // The observer service holds us alive.
    os->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, /* ownsWeak */ false);

    // Initialize the internal state
    mPageSize = sysconf(_SC_PAGESIZE);
    ReadPrefs();
    nsresult rv = OpenFiles();
    NS_ENSURE_SUCCESS(rv, rv);
    SetLowMemTrigger(mSoftLowMemTriggerKB);

    return NS_OK;
  }

  NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
                     const char16_t* aData)
  {
    MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0);
    LOG("Observed XPCOM shutdown.");

    MonitorAutoLock lock(mMonitor);
    mShuttingDown = true;
    mMonitor.Notify();

    int rv;
    do {
      // Write something to the pipe; doesn't matter what.
      uint32_t dummy = 0;
      rv = write(mShutdownPipeWrite, &dummy, sizeof(dummy));
    } while(rv == -1 && errno == EINTR);

    return NS_OK;
  }

  NS_IMETHOD Run() override
  {
    MOZ_ASSERT(!NS_IsMainThread());

    int triggerResetTimeout = -1;
    bool memoryPressure;
    nsresult rv = CheckForMemoryPressure(&memoryPressure);
    NS_ENSURE_SUCCESS(rv, rv);

    while (true) {
      // Wait for a notification on mTriggerFd or for data to be written to
      // mShutdownPipeWrite.  (poll(mTriggerFd, POLLPRI) blocks until we're
      // under memory pressure or until we time out, the time out is used
      // to adjust the trigger level after a memory pressure event.)
      struct pollfd pollfds[2];
      pollfds[0].fd = mTriggerFd;
      pollfds[0].events = POLLPRI;
      pollfds[1].fd = mShutdownPipeRead;
      pollfds[1].events = POLLIN;

      int pollRv = MOZ_TEMP_FAILURE_RETRY(
        poll(pollfds, ArrayLength(pollfds), triggerResetTimeout)
      );

      if (pollRv == 0) {
        // Timed out, adjust the trigger and update the timeout.
        triggerResetTimeout = AdjustTrigger(triggerResetTimeout);
        continue;
      }

      if (pollfds[1].revents) {
        // Something was written to our shutdown pipe; we're outta here.
        LOG("shutting down (1)");
        return NS_OK;
      }

      // If pollfds[1] isn't happening, pollfds[0] ought to be!
      if (!(pollfds[0].revents & POLLPRI)) {
        LOG("Unexpected revents value after poll(): %d. "
            "Shutting down GonkMemoryPressureMonitoring.", pollfds[0].revents);
        return NS_ERROR_FAILURE;
      }

      // POLLPRI on mTriggerFd indicates that we're in a low-memory situation.
      // We could read lowMemFd to double-check, but we've observed that the
      // read sometimes completes after the memory-pressure event is over, so
      // let's just believe the result of poll().
      rv = DispatchMemoryPressure(MemPressure_New);
      NS_ENSURE_SUCCESS(rv, rv);

      // Move to the hard level if we're on the soft one.
      if (mLowMemTriggerKB > mHardLowMemTriggerKB) {
        SetLowMemTrigger(mHardLowMemTriggerKB);
      }

      // Manually check mTriggerFd until we observe that memory pressure is
      // over.  We won't fire any more low-memory events until we observe that
      // we're no longer under pressure. Instead, we fire low-memory-ongoing
      // events, which cause processes to keep flushing caches but will not
      // trigger expensive GCs and other attempts to save memory that are
      // likely futile at this point.
      do {
        {
          MonitorAutoLock lock(mMonitor);

          // We need to check mShuttingDown before we wait here, in order to
          // catch a shutdown signal sent after we poll()'ed mShutdownPipeRead
          // above but before we started waiting on the monitor.  But we don't
          // need to check after we wait, because we'll either do another
          // iteration of this inner loop, in which case we'll check
          // mShuttingDown, or we'll exit this loop and do another iteration
          // of the outer loop, in which case we'll check the shutdown pipe.
          if (mShuttingDown) {
            LOG("shutting down (2)");
            return NS_OK;
          }
          mMonitor.Wait(PR_MillisecondsToInterval(mPollMS));
        }

        LOG("Checking to see if memory pressure is over.");
        rv = CheckForMemoryPressure(&memoryPressure);
        NS_ENSURE_SUCCESS(rv, rv);

        if (memoryPressure) {
          rv = DispatchMemoryPressure(MemPressure_Ongoing);
          NS_ENSURE_SUCCESS(rv, rv);
          continue;
        }
      } while (false);

      if (XRE_IsParentProcess()) {
        // The main process will try to adjust the trigger.
        triggerResetTimeout = mPollMS * 2;
      }

      LOG("Memory pressure is over.");
    }

    return NS_OK;
  }

protected:
  ~MemoryPressureWatcher() {}

private:
  void ReadPrefs() {
    // While we're under memory pressure, we periodically read()
    // notify_trigger_active to try and see when we're no longer under memory
    // pressure.  mPollMS indicates how many milliseconds we wait between those
    // read()s.
    Preferences::AddUintVarCache(&mPollMS,
      "gonk.systemMemoryPressureRecoveryPollMS", /* default */ 5000);

    // We have two values for the notify trigger, a soft one which is triggered
    // before we start killing background applications and an hard one which is
    // after we've killed background applications but before we start killing
    // foreground ones.
    Preferences::AddUintVarCache(&mSoftLowMemTriggerKB,
      "gonk.notifySoftLowMemUnderKB", /* default */ 43008);
    Preferences::AddUintVarCache(&mHardLowMemTriggerKB,
      "gonk.notifyHardLowMemUnderKB", /* default */ 14336);
  }

  nsresult OpenFiles() {
    mTriggerFd = open("/sys/kernel/mm/lowmemkiller/notify_trigger_active",
                      O_RDONLY | O_CLOEXEC);
    NS_ENSURE_STATE(mTriggerFd != -1);

    int pipes[2];
    NS_ENSURE_STATE(!pipe(pipes));
    mShutdownPipeRead = pipes[0];
    mShutdownPipeWrite = pipes[1];
    return NS_OK;
  }

  /**
   * Set the low memory trigger to the specified value, this can be done by
   * the main process alone.
   */
  void SetLowMemTrigger(uint32_t aValue) {
    if (XRE_IsParentProcess()) {
      nsPrintfCString str("%ld", (aValue * 1024) / mPageSize);
      if (WriteSysFile("/sys/module/lowmemorykiller/parameters/notify_trigger",
                       str.get())) {
        mLowMemTriggerKB = aValue;
      }
    }
  }

  /**
   * Read from the trigger file descriptor and determine whether we're
   * currently under memory pressure.
   *
   * We don't expect this method to block.
   */
  nsresult CheckForMemoryPressure(bool* aOut)
  {
    *aOut = false;

    lseek(mTriggerFd, 0, SEEK_SET);

    char buf[2];
    int nread = MOZ_TEMP_FAILURE_RETRY(read(mTriggerFd, buf, sizeof(buf)));
    NS_ENSURE_STATE(nread == 2);

    // The notify_trigger_active sysfs node should contain either "0\n" or
    // "1\n".  The latter indicates memory pressure.
    *aOut = (buf[0] == '1');
    return NS_OK;
  }

  int AdjustTrigger(int timeout)
  {
    if (!XRE_IsParentProcess()) {
      return -1; // Only the main process can adjust the trigger.
    }

    struct sysinfo info;
    int rv = sysinfo(&info);
    if (rv < 0) {
      return -1; // Without system information we're blind, bail out.
    }

    size_t freeMemory = (info.freeram * info.mem_unit) / 1024;

    if (freeMemory > mSoftLowMemTriggerKB) {
      SetLowMemTrigger(mSoftLowMemTriggerKB);
      return -1; // Trigger adjusted, wait indefinitely.
    }

    // Wait again but double the duration, max once per day.
    return std::min(86400000, timeout * 2);
  }

  /**
   * Dispatch the specified memory pressure event unless a high-priority
   * process is present. If a high-priority process is present then it's likely
   * responding to an urgent event (an incoming call or message for example) so
   * avoid wasting CPU time responding to low-memory events.
   */
  nsresult DispatchMemoryPressure(MemoryPressureState state)
  {
    if (ProcessPriorityManager::AnyProcessHasHighPriority()) {
      return NS_OK;
    }

    return NS_DispatchMemoryPressure(state);
  }

  Monitor mMonitor;
  uint32_t mPollMS; // Ongoing pressure poll delay
  uint32_t mSoftLowMemTriggerKB; // Soft memory pressure level
  uint32_t mHardLowMemTriggerKB; // Hard memory pressure level
  uint32_t mLowMemTriggerKB; // Current value of the trigger
  size_t mPageSize;
  bool mShuttingDown;

  ScopedClose mTriggerFd;
  ScopedClose mShutdownPipeRead;
  ScopedClose mShutdownPipeWrite;
};

NS_IMPL_ISUPPORTS(MemoryPressureWatcher, nsIRunnable, nsIObserver);

} // namespace

namespace mozilla {

void
InitGonkMemoryPressureMonitoring()
{
  // memoryPressureWatcher is held alive by the observer service.
  RefPtr<MemoryPressureWatcher> memoryPressureWatcher =
    new MemoryPressureWatcher();
  NS_ENSURE_SUCCESS_VOID(memoryPressureWatcher->Init());

  nsCOMPtr<nsIThread> thread;
  NS_NewNamedThread("MemoryPressure", getter_AddRefs(thread),
                    memoryPressureWatcher);
}

} // namespace mozilla