1 // Copyright 2022 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base.memory; 6 7 import androidx.annotation.VisibleForTesting; 8 9 import org.chromium.base.ApplicationState; 10 import org.chromium.base.ApplicationStatus; 11 import org.chromium.base.BaseFeatureMap; 12 import org.chromium.base.BaseFeatures; 13 import org.chromium.base.MemoryPressureLevel; 14 import org.chromium.base.MemoryPressureListener; 15 import org.chromium.base.ThreadUtils; 16 import org.chromium.base.TimeUtils; 17 import org.chromium.base.metrics.RecordHistogram; 18 19 /** 20 * This class is similar in principle to MemoryPurgeManager in blink, but on the browser process 21 * side. It triggers a critical memory pressure notification once the application has been in the 22 * background for more than a few minutes. 23 * 24 * UI thread only. 25 */ 26 public class MemoryPurgeManager implements ApplicationStatus.ApplicationStateListener { 27 private boolean mStarted; 28 private long mLastBackgroundPeriodStart = NEVER; 29 private boolean mDelayedPurgeTaskPending; 30 private boolean mHasBeenInForeground; 31 32 // Arbitrary delay, a few minutes is what is used for background renderer purge, and 5 minutes 33 // for freezing. 34 // TODO(crbug.com/1356242): Should ideally be tuned according to the distribution of background 35 // time residency. 36 @VisibleForTesting static final long PURGE_DELAY_MS = 4 * 60 * 1000; 37 private static final long NEVER = -1; 38 39 @VisibleForTesting 40 static final String BACKGROUND_DURATION_HISTOGRAM_NAME = 41 "Android.ApplicationState.TimeInBackgroundBeforeForegroundedAgain"; 42 43 private static final MemoryPurgeManager sInstance = new MemoryPurgeManager(); 44 45 @VisibleForTesting MemoryPurgeManager()46 MemoryPurgeManager() {} 47 getInstance()48 public static MemoryPurgeManager getInstance() { 49 return sInstance; 50 } 51 52 /** 53 * Start the background memory purge, if enabled. May be called several times. 54 * 55 * This attempts to trigger a critical memory pressure notification after 4 continuous minutes 56 * in background. 57 */ start()58 public void start() { 59 ThreadUtils.assertOnUiThread(); 60 if (mStarted) return; 61 mStarted = true; 62 if (!BaseFeatureMap.isEnabled(BaseFeatures.BROWSER_PROCESS_MEMORY_PURGE)) return; 63 64 ApplicationStatus.registerApplicationStateListener(this); 65 66 // We may already be in background, capture the initial state. 67 onApplicationStateChange(getApplicationState()); 68 } 69 70 @Override onApplicationStateChange(int state)71 public void onApplicationStateChange(int state) { 72 switch (state) { 73 case ApplicationState.UNKNOWN: 74 case ApplicationState.HAS_RUNNING_ACTIVITIES: 75 case ApplicationState.HAS_PAUSED_ACTIVITIES: 76 if (mLastBackgroundPeriodStart != NEVER && mHasBeenInForeground) { 77 long durationInBackgroundMs = 78 TimeUtils.elapsedRealtimeMillis() - mLastBackgroundPeriodStart; 79 RecordHistogram.recordLongTimesHistogram( 80 BACKGROUND_DURATION_HISTOGRAM_NAME, durationInBackgroundMs); 81 } 82 mHasBeenInForeground = true; 83 mLastBackgroundPeriodStart = NEVER; 84 break; 85 case ApplicationState.HAS_STOPPED_ACTIVITIES: 86 if (mLastBackgroundPeriodStart == NEVER) { 87 mLastBackgroundPeriodStart = TimeUtils.elapsedRealtimeMillis(); 88 maybePostDelayedPurgingTask(PURGE_DELAY_MS); 89 } 90 break; 91 case ApplicationState.HAS_DESTROYED_ACTIVITIES: 92 // Ignored on purpose: the initial state of a process which never had any activity 93 // is HAS_DESTROYED_ACTIVITIES, and we don't want to trigger in this case. 94 break; 95 } 96 } 97 delayedPurge()98 private void delayedPurge() { 99 // Came back to foreground in the meantime, do not repost a task, this will happen next time 100 // we go to background. 101 if (mLastBackgroundPeriodStart == NEVER) return; 102 103 assert mLastBackgroundPeriodStart < TimeUtils.elapsedRealtimeMillis(); 104 long inBackgroundFor = TimeUtils.elapsedRealtimeMillis() - mLastBackgroundPeriodStart; 105 if (inBackgroundFor < PURGE_DELAY_MS) { 106 maybePostDelayedPurgingTask(PURGE_DELAY_MS - inBackgroundFor); 107 return; 108 } 109 110 notifyMemoryPressure(); 111 } 112 113 protected void notifyMemoryPressure() { 114 MemoryPressureListener.notifyMemoryPressure(MemoryPressureLevel.CRITICAL); 115 } 116 117 protected int getApplicationState() { 118 return ApplicationStatus.getStateForApplication(); 119 } 120 121 private void maybePostDelayedPurgingTask(long delayMillis) { 122 ThreadUtils.assertOnUiThread(); 123 if (mDelayedPurgeTaskPending) return; 124 125 ThreadUtils.postOnUiThreadDelayed( 126 () -> { 127 mDelayedPurgeTaskPending = false; 128 delayedPurge(); 129 }, 130 delayMillis); 131 mDelayedPurgeTaskPending = true; 132 } 133 } 134