Commit 5baa1f51 authored by Dmitry Skiba's avatar Dmitry Skiba Committed by Commit Bot

Improve memory pressure reporting on Android.

This CL overhauls the way memory pressure is sensed and reported for
the browser process. Main changes are:

* ActivityManager.getMyMemoryStat() is used to poll pressure.

* Pressure signals are throttled.

* CRITICAL->MODERATE pressure changes are always reported.

See MemoryPressureMonitor.java comments for details.

Bug: 813909
Change-Id: I6fb5395b175cf8ca4ac40fac021125dd9d7fbc9f
Reviewed-on: https://chromium-review.googlesource.com/953166
Commit-Queue: Dmitry Skiba <dskiba@chromium.org>
Reviewed-by: default avatarAlexei Svitkine <asvitkine@chromium.org>
Reviewed-by: default avatarYaron Friedman <yfriedman@chromium.org>
Reviewed-by: default avatarBrian White <bcwhite@chromium.org>
Reviewed-by: default avatarBo <boliu@chromium.org>
Reviewed-by: default avatarRichard Coles <torne@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Cr-Commit-Position: refs/heads/master@{#549818}
parent ff4cbba0
......@@ -95,7 +95,7 @@ int AwBrowserMainParts::PreCreateThreads() {
pak_file_path = pak_file_path.AppendASCII("resources.pak");
ui::LoadMainAndroidPackFile("assets/resources.pak", pak_file_path);
base::android::MemoryPressureListenerAndroid::RegisterSystemCallback(
base::android::MemoryPressureListenerAndroid::Initialize(
base::android::AttachCurrentThread());
breakpad::CrashDumpObserver::Create();
......
......@@ -17,8 +17,8 @@ import org.chromium.android_webview.AwSettings;
import org.chromium.android_webview.command_line.CommandLineUtil;
import org.chromium.base.Callback;
import org.chromium.base.MemoryPressureLevel;
import org.chromium.base.MemoryPressureListener;
import org.chromium.base.ThreadUtils;
import org.chromium.base.memory.MemoryPressureMonitor;
import java.util.List;
......@@ -71,7 +71,7 @@ public class SharedStatics {
public void freeMemoryForTests() {
if (ActivityManager.isRunningInTestHarness()) {
MemoryPressureListener.onMemoryPressure(MemoryPressureLevel.CRITICAL);
MemoryPressureMonitor.INSTANCE.notifyPressure(MemoryPressureLevel.CRITICAL);
}
}
......
......@@ -7,6 +7,7 @@ package org.chromium.android_webview;
import android.content.Context;
import android.content.SharedPreferences;
import org.chromium.base.memory.MemoryPressureMonitor;
import org.chromium.content.browser.ContentViewStatics;
/**
......@@ -32,6 +33,20 @@ public class AwBrowserContext {
mApplicationContext = applicationContext;
PlatformServiceBridge.getInstance().setSafeBrowsingHandler();
// Register MemoryPressureMonitor callbacks and make sure it polls only if there is at
// least one WebView around.
MemoryPressureMonitor.INSTANCE.registerComponentCallbacks();
AwContentsLifecycleNotifier.addObserver(new AwContentsLifecycleNotifier.Observer() {
@Override
public void onFirstWebViewCreated() {
MemoryPressureMonitor.INSTANCE.enablePolling();
}
@Override
public void onLastWebViewDestroyed() {
MemoryPressureMonitor.INSTANCE.disablePolling();
}
});
}
public AwGeolocationPermissions getGeolocationPermissions() {
......
......@@ -2740,6 +2740,8 @@ if (is_android) {
"android/java/src/org/chromium/base/process_launcher/ChildProcessService.java",
"android/java/src/org/chromium/base/process_launcher/ChildProcessServiceDelegate.java",
"android/java/src/org/chromium/base/process_launcher/FileDescriptorInfo.java",
"android/java/src/org/chromium/base/memory/MemoryPressureMonitor.java",
"android/java/src/org/chromium/base/memory/MemoryPressureCallback.java",
]
# New versions of BuildConfig.java and NativeLibraries.java
......@@ -2773,7 +2775,6 @@ if (is_android) {
"android/javatests/src/org/chromium/base/CommandLineInitUtilTest.java",
"android/javatests/src/org/chromium/base/CommandLineTest.java",
"android/javatests/src/org/chromium/base/EarlyTraceEventTest.java",
"android/javatests/src/org/chromium/base/MemoryPressureListenerTest.java",
# TODO(nona): move to Junit once that is built for Android N.
"android/javatests/src/org/chromium/base/LocaleUtilsTest.java",
......@@ -2888,6 +2889,7 @@ if (is_android) {
"android/junit/src/org/chromium/base/LogTest.java",
"android/junit/src/org/chromium/base/NonThreadSafeTest.java",
"android/junit/src/org/chromium/base/PromiseTest.java",
"android/junit/src/org/chromium/base/memory/MemoryPressureMonitorTest.java",
"android/junit/src/org/chromium/base/process_launcher/ChildConnectionAllocatorTest.java",
"android/junit/src/org/chromium/base/process_launcher/ChildProcessConnectionTest.java",
"test/android/junit/src/org/chromium/base/test/SetUpStatementTest.java",
......
......@@ -6,16 +6,23 @@ package org.chromium.base;
import android.app.Activity;
import android.content.ComponentCallbacks2;
import android.content.res.Configuration;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.memory.MemoryPressureCallback;
/**
* This is an internal implementation of the C++ counterpart.
* It registers a ComponentCallbacks2 with the system, and dispatches into
* native for levels that are considered actionable.
* This class is Java equivalent of base::MemoryPressureListener: it distributes pressure
* signals to callbacks.
*
* The class also serves as an entry point to the native side - once native code is ready,
* it adds native callback.
*
* notifyMemoryPressure() is called exclusively by MemoryPressureMonitor, which
* monitors and throttles pressure signals.
*
* NOTE: this class should only be used on UiThread as defined by ThreadUtils (which is
* Android main thread for Chrome, but can be some other thread for WebView).
*/
@MainDex
public class MemoryPressureListener {
......@@ -45,57 +52,41 @@ public class MemoryPressureListener {
private static final String ACTION_TRIM_MEMORY_MODERATE =
"org.chromium.base.ACTION_TRIM_MEMORY_MODERATE";
private static OnMemoryPressureCallbackForTesting sOnMemoryPressureCallbackForTesting;
private static final ObserverList<MemoryPressureCallback> sCallbacks = new ObserverList<>();
@VisibleForTesting
/**
* Called by the native side to add native callback.
*/
@CalledByNative
public static void registerSystemCallback() {
ContextUtils.getApplicationContext().registerComponentCallbacks(
new ComponentCallbacks2() {
@Override
public void onTrimMemory(int level) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE
|| level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
onMemoryPressure(MemoryPressureLevel.CRITICAL);
} else if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Don't notifiy on TRIM_MEMORY_UI_HIDDEN, since this class only
// dispatches actionable memory pressure signals to native.
onMemoryPressure(MemoryPressureLevel.MODERATE);
}
}
@Override
public void onLowMemory() {
onMemoryPressure(MemoryPressureLevel.CRITICAL);
}
@Override
public void onConfigurationChanged(Configuration configuration) {
}
});
private static void addNativeCallback() {
addCallback(MemoryPressureListener::nativeOnMemoryPressure);
}
/** Memory pressure callback that is called before calling native. Use for testing only.
/**
* Adds a memory pressure callback.
* Callback is only added once, regardless of the number of addCallback() calls.
* This method should be called only on ThreadUtils.UiThread.
*/
@VisibleForTesting
@FunctionalInterface
public interface OnMemoryPressureCallbackForTesting {
public void apply(@MemoryPressureLevel int pressure);
public static void addCallback(MemoryPressureCallback callback) {
sCallbacks.addObserver(callback);
}
@VisibleForTesting
public static void setOnMemoryPressureCallbackForTesting(
OnMemoryPressureCallbackForTesting callback) {
sOnMemoryPressureCallbackForTesting = callback;
/**
* Removes previously added memory pressure callback.
* This method should be called only on ThreadUtils.UiThread.
*/
public void removeCallback(MemoryPressureCallback callback) {
sCallbacks.removeObserver(callback);
}
/** Reports memory pressure.
/**
* Distributes |pressure| to all callbacks.
* This method should be called only on ThreadUtils.UiThread.
*/
public static void onMemoryPressure(@MemoryPressureLevel int pressure) {
if (sOnMemoryPressureCallbackForTesting != null) {
sOnMemoryPressureCallbackForTesting.apply(pressure);
public static void notifyMemoryPressure(@MemoryPressureLevel int pressure) {
for (MemoryPressureCallback callback : sCallbacks) {
callback.onPressure(pressure);
}
nativeOnMemoryPressure(pressure);
}
/**
......@@ -135,6 +126,5 @@ public class MemoryPressureListener {
activity.onTrimMemory(level);
}
// Don't call directly, always go through onMemoryPressure().
private static native void nativeOnMemoryPressure(@MemoryPressureLevel int pressure);
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 org.chromium.base.MemoryPressureLevel;
/**
* Memory pressure callback interface.
*/
@FunctionalInterface
public interface MemoryPressureCallback {
public void onPressure(@MemoryPressureLevel int pressure);
}
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base;
import android.app.Application;
import android.content.ComponentCallbacks2;
import android.support.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.test.BaseJUnit4ClassRunner;
/**
* Tests for {@link MemoryPressureListener}.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class MemoryPressureListenerTest {
/** Helper class to capture and validate OnMemoryPressureCallback calls.
*/
private static class MemoryPressureCallback {
private boolean mCalled;
private @MemoryPressureLevel int mPressure = MemoryPressureLevel.NONE;
public void install() {
MemoryPressureListener.setOnMemoryPressureCallbackForTesting(this::onMemoryPressure);
}
public void uninstall() {
MemoryPressureListener.setOnMemoryPressureCallbackForTesting(null);
}
private void onMemoryPressure(@MemoryPressureLevel int pressure) {
Assert.assertFalse("Should not be called twice", mCalled);
mCalled = true;
mPressure = pressure;
}
public void assertCalledWith(@MemoryPressureLevel int pressure) {
Assert.assertTrue(mCalled);
Assert.assertEquals(pressure, mPressure);
}
public void assertNotCalled() {
Assert.assertFalse(mCalled);
}
}
@Before
public void setUp() throws Exception {
LibraryLoader.get(LibraryProcessType.PROCESS_BROWSER).ensureInitialized();
MemoryPressureListener.registerSystemCallback();
}
@Test
@SmallTest
public void testLowMemoryTranslation() {
MemoryPressureCallback callback = new MemoryPressureCallback();
ThreadUtils.runOnUiThreadBlocking(() -> {
callback.install();
((Application) ContextUtils.getApplicationContext()).onLowMemory();
callback.uninstall();
});
callback.assertCalledWith(MemoryPressureLevel.CRITICAL);
}
@Test
@SmallTest
public void testTrimLevelTranslation() {
Integer[][] trimLevelToPressureMap = {
// Levels >= TRIM_MEMORY_COMPLETE map to CRITICAL.
{ComponentCallbacks2.TRIM_MEMORY_COMPLETE + 1, MemoryPressureLevel.CRITICAL},
{ComponentCallbacks2.TRIM_MEMORY_COMPLETE, MemoryPressureLevel.CRITICAL},
// TRIM_MEMORY_RUNNING_CRITICAL maps to CRITICAL.
{ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL, MemoryPressureLevel.CRITICAL},
// Levels < TRIM_MEMORY_COMPLETE && >= TRIM_MEMORY_BACKGROUND map to MODERATE.
{ComponentCallbacks2.TRIM_MEMORY_COMPLETE - 1, MemoryPressureLevel.MODERATE},
{ComponentCallbacks2.TRIM_MEMORY_BACKGROUND + 1, MemoryPressureLevel.MODERATE},
{ComponentCallbacks2.TRIM_MEMORY_BACKGROUND, MemoryPressureLevel.MODERATE},
// Other levels don't trigger memory pressure at all.
{ComponentCallbacks2.TRIM_MEMORY_BACKGROUND - 1, null},
{ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW, null},
{ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE, null},
{ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN, null}};
for (Integer[] trimLevelAndPressure : trimLevelToPressureMap) {
final int trimLevel = trimLevelAndPressure[0];
Integer pressure = trimLevelAndPressure[1];
MemoryPressureCallback callback = new MemoryPressureCallback();
ThreadUtils.runOnUiThreadBlocking(() -> {
callback.install();
((Application) ContextUtils.getApplicationContext()).onTrimMemory(trimLevel);
callback.uninstall();
});
if (pressure == null) {
callback.assertNotCalled();
} else {
callback.assertCalledWith(pressure);
}
}
}
}
......@@ -22,8 +22,8 @@ static void JNI_MemoryPressureListener_OnMemoryPressure(
namespace base {
namespace android {
void MemoryPressureListenerAndroid::RegisterSystemCallback(JNIEnv* env) {
Java_MemoryPressureListener_registerSystemCallback(env);
void MemoryPressureListenerAndroid::Initialize(JNIEnv* env) {
Java_MemoryPressureListener_addNativeCallback(env);
}
} // namespace android
......
......@@ -14,7 +14,7 @@ namespace android {
// Implements the C++ counter part of MemoryPressureListener.java
class BASE_EXPORT MemoryPressureListenerAndroid {
public:
static void RegisterSystemCallback(JNIEnv* env);
static void Initialize(JNIEnv* env);
// Called by JNI.
static void OnMemoryPressure(int memory_pressure_type);
......
......@@ -11,6 +11,7 @@ import android.content.Intent;
import android.os.Bundle;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.BuildConfig;
import org.chromium.base.CommandLineInitUtil;
......@@ -22,6 +23,7 @@ import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.annotations.MainDex;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.memory.MemoryPressureMonitor;
import org.chromium.base.multidex.ChromiumMultiDexInstaller;
import org.chromium.build.BuildHooks;
import org.chromium.build.BuildHooksAndroid;
......@@ -92,10 +94,21 @@ public class ChromeApplication extends Application {
// Only browser process requires custom resources.
BuildHooksAndroid.initCustomResources(this);
// Disable MemoryPressureMonitor polling when Chrome goes to the background.
ApplicationStatus.registerApplicationStateListener(newState -> {
if (newState == ApplicationState.HAS_RUNNING_ACTIVITIES) {
MemoryPressureMonitor.INSTANCE.enablePolling();
} else if (newState == ApplicationState.HAS_STOPPED_ACTIVITIES) {
MemoryPressureMonitor.INSTANCE.disablePolling();
}
});
// Not losing much to not cover the below conditional since it just has simple setters.
TraceEvent.end("ChromeApplication.attachBaseContext");
}
MemoryPressureMonitor.INSTANCE.registerComponentCallbacks();
if (!ContextUtils.isIsolatedProcess()) {
// Incremental install disables process isolation, so things in this block will actually
// be run for incremental apks, but not normal apks.
......
......@@ -8,7 +8,7 @@
#include "chrome/browser/lifetime/application_lifetime_android.h"
BrowserProcessPlatformPart::BrowserProcessPlatformPart() {
base::android::MemoryPressureListenerAndroid::RegisterSystemCallback(
base::android::MemoryPressureListenerAndroid::Initialize(
base::android::AttachCurrentThread());
}
......
......@@ -114,7 +114,7 @@ void JNI_ContentChildProcessServiceDelegate_InternalInitChildProcess(
gpu::ScopedSurfaceRequestConduit::SetInstance(
g_child_process_surface_manager.Pointer());
base::android::MemoryPressureListenerAndroid::RegisterSystemCallback(env);
base::android::MemoryPressureListenerAndroid::Initialize(env);
}
} // namespace
......
......@@ -1345,6 +1345,24 @@ http://cs/file:chrome/histograms.xml - but prefer this file for new entries.
</summary>
</histogram>
<histogram name="Android.MemoryPressureMonitor.GetMyMemoryState.Failed.Time"
units="microseconds">
<owner>dskiba@chromium.org</owner>
<summary>
The duration of each failed ActivityManager.getMyMemoryState() call made by
MemoryPressureMonitor.
</summary>
</histogram>
<histogram name="Android.MemoryPressureMonitor.GetMyMemoryState.Succeeded.Time"
units="microseconds">
<owner>dskiba@chromium.org</owner>
<summary>
The duration of each successful ActivityManager.getMyMemoryState() call made
by MemoryPressureMonitor.
</summary>
</histogram>
<histogram name="Android.ModerateBindingCount" units="bindings">
<owner>jaekyun@chromium.org</owner>
<summary>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment