blob: d640bf7b683c23fcd1223e62887750e675970a5a [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.memory;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.BaseFeatureMap;
import org.chromium.base.BaseFeatures;
import org.chromium.base.MemoryPressureLevel;
import org.chromium.base.MemoryPressureListener;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TimeUtils;
import org.chromium.base.metrics.RecordHistogram;
/**
* This class is similar in principle to MemoryPurgeManager in blink, but on the browser process
* side. It triggers a critical memory pressure notification once the application has been in the
* background for more than a few minutes.
*
* UI thread only.
*/
public class MemoryPurgeManager implements ApplicationStatus.ApplicationStateListener {
private boolean mStarted;
private long mLastBackgroundPeriodStart = NEVER;
private boolean mDelayedPurgeTaskPending;
private boolean mHasBeenInForeground;
// Arbitrary delay, a few minutes is what is used for background renderer purge, and 5 minutes
// for freezing.
// TODO(crbug.com/1356242): Should ideally be tuned according to the distribution of background
// time residency.
@VisibleForTesting static final long PURGE_DELAY_MS = 4 * 60 * 1000;
private static final long NEVER = -1;
@VisibleForTesting
static final String BACKGROUND_DURATION_HISTOGRAM_NAME =
"Android.ApplicationState.TimeInBackgroundBeforeForegroundedAgain";
private static final MemoryPurgeManager sInstance = new MemoryPurgeManager();
@VisibleForTesting
MemoryPurgeManager() {}
public static MemoryPurgeManager getInstance() {
return sInstance;
}
/**
* Start the background memory purge, if enabled. May be called several times.
*
* This attempts to trigger a critical memory pressure notification after 4 continuous minutes
* in background.
*/
public void start() {
ThreadUtils.assertOnUiThread();
if (mStarted) return;
mStarted = true;
if (!BaseFeatureMap.isEnabled(BaseFeatures.BROWSER_PROCESS_MEMORY_PURGE)) return;
ApplicationStatus.registerApplicationStateListener(this);
// We may already be in background, capture the initial state.
onApplicationStateChange(getApplicationState());
}
@Override
public void onApplicationStateChange(int state) {
switch (state) {
case ApplicationState.UNKNOWN:
case ApplicationState.HAS_RUNNING_ACTIVITIES:
case ApplicationState.HAS_PAUSED_ACTIVITIES:
if (mLastBackgroundPeriodStart != NEVER && mHasBeenInForeground) {
long durationInBackgroundMs =
TimeUtils.elapsedRealtimeMillis() - mLastBackgroundPeriodStart;
RecordHistogram.recordLongTimesHistogram(
BACKGROUND_DURATION_HISTOGRAM_NAME, durationInBackgroundMs);
}
mHasBeenInForeground = true;
mLastBackgroundPeriodStart = NEVER;
break;
case ApplicationState.HAS_STOPPED_ACTIVITIES:
if (mLastBackgroundPeriodStart == NEVER) {
mLastBackgroundPeriodStart = TimeUtils.elapsedRealtimeMillis();
maybePostDelayedPurgingTask(PURGE_DELAY_MS);
}
break;
case ApplicationState.HAS_DESTROYED_ACTIVITIES:
// Ignored on purpose: the initial state of a process which never had any activity
// is HAS_DESTROYED_ACTIVITIES, and we don't want to trigger in this case.
break;
}
}
private void delayedPurge() {
// Came back to foreground in the meantime, do not repost a task, this will happen next time
// we go to background.
if (mLastBackgroundPeriodStart == NEVER) return;
assert mLastBackgroundPeriodStart < TimeUtils.elapsedRealtimeMillis();
long inBackgroundFor = TimeUtils.elapsedRealtimeMillis() - mLastBackgroundPeriodStart;
if (inBackgroundFor < PURGE_DELAY_MS) {
maybePostDelayedPurgingTask(PURGE_DELAY_MS - inBackgroundFor);
return;
}
notifyMemoryPressure();
}
protected void notifyMemoryPressure() {
MemoryPressureListener.notifyMemoryPressure(MemoryPressureLevel.CRITICAL);
}
protected int getApplicationState() {
return ApplicationStatus.getStateForApplication();
}
private void maybePostDelayedPurgingTask(long delayMillis) {
ThreadUtils.assertOnUiThread();
if (mDelayedPurgeTaskPending) return;
ThreadUtils.postOnUiThreadDelayed(
() -> {
mDelayedPurgeTaskPending = false;
delayedPurge();
},
delayMillis);
mDelayedPurgeTaskPending = true;
}
}