Some small tweaks to improve memory management.
We now allow processes that currently have stopping activities to
be managed as if they were done stopping, so that memory trimming
can be done before the process goes to the background. Hopefully
this will reduce cases where the processes goes to the background
and immediately gets killed, but wouldn't have had to be killed if
it had a chance to trim its memory.
Also change window memory trimming to always do the aggressive
trimming when memory is critical, even if not on a low-end device.
And tweak web view trimming to not trim for foreground UI events.
Change-Id: I241b3152b52d09757bd14a202477cf69c9b78786
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 7207e29..227900e 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -55,6 +55,7 @@
import android.util.AttributeSet;
import android.util.EventLog;
import android.util.Log;
+import android.util.Slog;
import android.util.SparseArray;
import android.view.ActionMode;
import android.view.ContextMenu;
@@ -642,6 +643,7 @@
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2 {
private static final String TAG = "Activity";
+ private static final boolean DEBUG_LIFECYCLE = false;
/** Standard activity result: operation canceled. */
public static final int RESULT_CANCELED = 0;
@@ -865,6 +867,7 @@
* @see #onPostCreate
*/
protected void onCreate(Bundle savedInstanceState) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);
if (mLastNonConfigurationInstances != null) {
mAllLoaderManagers = mLastNonConfigurationInstances.loaders;
}
@@ -1013,6 +1016,7 @@
* @see #onResume
*/
protected void onStart() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStart " + this);
mCalled = true;
if (!mLoadersStarted) {
@@ -1073,6 +1077,7 @@
* @see #onPause
*/
protected void onResume() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onResume " + this);
getApplication().dispatchActivityResumed(this);
mCalled = true;
}
@@ -1131,6 +1136,7 @@
final void performSaveInstanceState(Bundle outState) {
onSaveInstanceState(outState);
saveManagedDialogs(outState);
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onSaveInstanceState " + this + ": " + outState);
}
/**
@@ -1261,6 +1267,7 @@
* @see #onStop
*/
protected void onPause() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onPause " + this);
getApplication().dispatchActivityPaused(this);
mCalled = true;
}
@@ -1347,6 +1354,7 @@
* @see #onDestroy
*/
protected void onStop() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStop " + this);
if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false);
getApplication().dispatchActivityStopped(this);
mCalled = true;
@@ -1381,6 +1389,7 @@
* @see #isFinishing
*/
protected void onDestroy() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onDestroy " + this);
mCalled = true;
// dismiss any dialogs we are managing.
@@ -1432,6 +1441,7 @@
* @param newConfig The new device configuration.
*/
public void onConfigurationChanged(Configuration newConfig) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onConfigurationChanged " + this + ": " + newConfig);
mCalled = true;
mFragments.dispatchConfigurationChanged(newConfig);
@@ -1613,11 +1623,13 @@
}
public void onLowMemory() {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onLowMemory " + this);
mCalled = true;
mFragments.dispatchLowMemory();
}
public void onTrimMemory(int level) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "onTrimMemory " + this + ": " + level);
mCalled = true;
mFragments.dispatchTrimMemory(level);
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 98c4e10..1489b2c 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -138,6 +138,7 @@
private static final boolean DEBUG_BACKUP = true;
private static final boolean DEBUG_CONFIGURATION = false;
private static final boolean DEBUG_SERVICE = false;
+ private static final boolean DEBUG_MEMORY_TRIM = false;
private static final long MIN_TIME_BETWEEN_GCS = 5*1000;
private static final Pattern PATTERN_SEMICOLON = Pattern.compile(";");
private static final int SQLITE_MEM_RELEASED_EVENT_LOG_TAG = 75003;
@@ -2779,9 +2780,21 @@
performStopActivityInner(r, null, false, saveState);
}
- private static class StopInfo {
+ private static class StopInfo implements Runnable {
+ ActivityClientRecord activity;
+ Bundle state;
Bitmap thumbnail;
CharSequence description;
+
+ @Override public void run() {
+ // Tell activity manager we have been stopped.
+ try {
+ if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + activity);
+ ActivityManagerNative.getDefault().activityStopped(
+ activity.token, state, thumbnail, description);
+ } catch (RemoteException ex) {
+ }
+ }
}
private static final class ProviderRefCount {
@@ -2911,12 +2924,14 @@
QueuedWork.waitToFinish();
}
- // Tell activity manager we have been stopped.
- try {
- ActivityManagerNative.getDefault().activityStopped(
- r.token, r.state, info.thumbnail, info.description);
- } catch (RemoteException ex) {
- }
+ // Schedule the call to tell the activity manager we have
+ // stopped. We don't do this immediately, because we want to
+ // have a chance for any other pending work (in particular memory
+ // trim requests) to complete before you tell the activity
+ // manager to proceed and allow us to go fully into the background.
+ info.activity = r;
+ info.state = r.state;
+ mH.post(info);
}
final void performRestartActivity(IBinder token) {
@@ -3749,6 +3764,7 @@
}
final void handleTrimMemory(int level) {
+ if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Trimming memory to level: " + level);
WindowManagerImpl.getDefault().trimMemory(level);
ArrayList<ComponentCallbacks2> callbacks;
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index a45a87e..52bd860 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -432,23 +432,24 @@
*/
public void trimMemory(int level) {
if (HardwareRenderer.isAvailable()) {
- // On low and medium end gfx devices
- if (!ActivityManager.isHighEndGfx(getDefaultDisplay())) {
- if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
- // Destroy all hardware surfaces and resources associated to
- // known windows
- synchronized (this) {
- if (mViews == null) return;
- int count = mViews.length;
- for (int i = 0; i < count; i++) {
- mRoots[i].terminateHardwareResources();
- }
+ // On low-end gfx devices we trim when memory is moderate;
+ // on high-end devices we do this when low.
+ if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE
+ || (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE
+ && !ActivityManager.isHighEndGfx(getDefaultDisplay()))) {
+ // Destroy all hardware surfaces and resources associated to
+ // known windows
+ synchronized (this) {
+ if (mViews == null) return;
+ int count = mViews.length;
+ for (int i = 0; i < count; i++) {
+ mRoots[i].terminateHardwareResources();
}
- // Force a full memory flush
- HardwareRenderer.trimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE);
- mNeedsEglTerminate = true;
- return;
}
+ // Force a full memory flush
+ HardwareRenderer.trimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE);
+ mNeedsEglTerminate = true;
+ return;
}
HardwareRenderer.trimMemory(level);
}
diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java
index 8dc3b7e..28af7c1 100644
--- a/core/java/android/webkit/WebViewClassic.java
+++ b/core/java/android/webkit/WebViewClassic.java
@@ -886,7 +886,9 @@
// the existing GL resources for the html5 video will be destroyed
// at native side.
// Here we just need to clean up the Surface Texture which is static.
- HTML5VideoInline.cleanupSurfaceTexture();
+ if (level >= TRIM_MEMORY_UI_HIDDEN) {
+ HTML5VideoInline.cleanupSurfaceTexture();
+ }
WebViewClassic.nativeOnTrimMemory(level);
}
diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java
index 80e59cd..186460d 100644
--- a/services/java/com/android/server/am/ActivityManagerService.java
+++ b/services/java/com/android/server/am/ActivityManagerService.java
@@ -13444,7 +13444,7 @@
// an earlier hidden adjustment that isn't really for us... if
// so, use the new hidden adjustment.
if (!recursed && app.hidden) {
- app.curAdj = app.curRawAdj = hiddenAdj;
+ app.curAdj = app.curRawAdj = app.nonStoppingAdj = hiddenAdj;
}
return app.curRawAdj;
}
@@ -13468,7 +13468,7 @@
// below foreground, so it is not worth doing work for it.
app.adjType = "fixed";
app.adjSeq = mAdjSeq;
- app.curRawAdj = app.maxAdj;
+ app.curRawAdj = app.nonStoppingAdj = app.maxAdj;
app.foregroundActivities = false;
app.keeping = true;
app.curSchedGroup = Process.THREAD_GROUP_DEFAULT;
@@ -13545,6 +13545,8 @@
app.adjType = "bg-empty";
}
+ boolean hasStoppingActivities = false;
+
// Examine all activities if not already foreground.
if (!app.foregroundActivities && activitiesSize > 0) {
for (int j = 0; j < activitiesSize; j++) {
@@ -13559,15 +13561,20 @@
app.hidden = false;
app.foregroundActivities = true;
break;
- } else if (r.state == ActivityState.PAUSING || r.state == ActivityState.PAUSED
- || r.state == ActivityState.STOPPING) {
- // Only upgrade adjustment.
+ } else if (r.state == ActivityState.PAUSING || r.state == ActivityState.PAUSED) {
if (adj > ProcessList.PERCEPTIBLE_APP_ADJ) {
adj = ProcessList.PERCEPTIBLE_APP_ADJ;
- app.adjType = "stopping";
+ app.adjType = "pausing";
}
app.hidden = false;
app.foregroundActivities = true;
+ } else if (r.state == ActivityState.STOPPING) {
+ // We will apply the actual adjustment later, because
+ // we want to allow this process to immediately go through
+ // any memory trimming that is in effect.
+ app.hidden = false;
+ app.foregroundActivities = true;
+ hasStoppingActivities = true;
}
}
}
@@ -13625,7 +13632,7 @@
// this gives us a baseline and makes sure we don't get into an
// infinite recursion.
app.adjSeq = mAdjSeq;
- app.curRawAdj = adj;
+ app.curRawAdj = app.nonStoppingAdj = adj;
if (mBackupTarget != null && app == mBackupTarget.app) {
// If possible we want to avoid killing apps while they're being backed up
@@ -13882,6 +13889,28 @@
}
}
+ if (adj == ProcessList.SERVICE_ADJ) {
+ if (doingAll) {
+ app.serviceb = mNewNumServiceProcs > (mNumServiceProcs/3);
+ mNewNumServiceProcs++;
+ }
+ if (app.serviceb) {
+ adj = ProcessList.SERVICE_B_ADJ;
+ }
+ } else {
+ app.serviceb = false;
+ }
+
+ app.nonStoppingAdj = adj;
+
+ if (hasStoppingActivities) {
+ // Only upgrade adjustment.
+ if (adj > ProcessList.PERCEPTIBLE_APP_ADJ) {
+ adj = ProcessList.PERCEPTIBLE_APP_ADJ;
+ app.adjType = "stopping";
+ }
+ }
+
app.curRawAdj = adj;
//Slog.i(TAG, "OOM ADJ " + app + ": pid=" + app.pid +
@@ -13915,18 +13944,6 @@
}
}
- if (adj == ProcessList.SERVICE_ADJ) {
- if (doingAll) {
- app.serviceb = mNewNumServiceProcs > (mNumServiceProcs/3);
- mNewNumServiceProcs++;
- }
- if (app.serviceb) {
- adj = ProcessList.SERVICE_B_ADJ;
- }
- } else {
- app.serviceb = false;
- }
-
app.curAdj = adj;
app.curSchedGroup = schedGroup;
@@ -14138,7 +14155,7 @@
}
// If a process has held a wake lock for more
// than 50% of the time during this period,
- // that sounds pad. Kill!
+ // that sounds bad. Kill!
if (doWakeKills && realtimeSince > 0
&& ((wtimeUsed*100)/realtimeSince) >= 50) {
synchronized (stats) {
@@ -14186,23 +14203,6 @@
computeOomAdjLocked(app, hiddenAdj, TOP_APP, false, doingAll);
if (app.curRawAdj != app.setRawAdj) {
- if (false) {
- // Removing for now. Forcing GCs is not so useful anymore
- // with Dalvik, and the new memory level hint facility is
- // better for what we need to do these days.
- if (app.curRawAdj > ProcessList.FOREGROUND_APP_ADJ
- && app.setRawAdj <= ProcessList.FOREGROUND_APP_ADJ) {
- // If this app is transitioning from foreground to
- // non-foreground, have it do a gc.
- scheduleAppGcLocked(app);
- } else if (app.curRawAdj >= ProcessList.HIDDEN_APP_MIN_ADJ
- && app.setRawAdj < ProcessList.HIDDEN_APP_MIN_ADJ) {
- // Likewise do a gc when an app is moving in to the
- // background (such as a service stopping).
- scheduleAppGcLocked(app);
- }
- }
-
if (wasKeeping && !app.keeping) {
// This app is no longer something we want to keep. Note
// its current wake lock time to later know to kill it if
@@ -14319,6 +14319,7 @@
if (factor < 1) factor = 1;
int step = 0;
int numHidden = 0;
+ int numTrimming = 0;
// First update the OOM adjustment for each of the
// application processes based on their current state.
@@ -14363,6 +14364,11 @@
app.killedBackground = true;
Process.killProcessQuiet(app.pid);
}
+ if (app.nonStoppingAdj >= ProcessList.HOME_APP_ADJ
+ && app.nonStoppingAdj != ProcessList.SERVICE_B_ADJ
+ && !app.killedBackground) {
+ numTrimming++;
+ }
}
}
@@ -14376,7 +14382,7 @@
// memory they want.
if (numHidden <= (ProcessList.MAX_HIDDEN_APPS/2)) {
final int N = mLruProcesses.size();
- factor = numHidden/3;
+ factor = numTrimming/3;
int minFactor = 2;
if (mHomeProcess != null) minFactor++;
if (mPreviousProcess != null) minFactor++;
@@ -14393,8 +14399,8 @@
int curLevel = ComponentCallbacks2.TRIM_MEMORY_COMPLETE;
for (i=0; i<N; i++) {
ProcessRecord app = mLruProcesses.get(i);
- if (app.curAdj >= ProcessList.HOME_APP_ADJ
- && app.curAdj != ProcessList.SERVICE_B_ADJ
+ if (app.nonStoppingAdj >= ProcessList.HOME_APP_ADJ
+ && app.nonStoppingAdj != ProcessList.SERVICE_B_ADJ
&& !app.killedBackground) {
if (app.trimMemoryLevel < curLevel && app.thread != null) {
try {
@@ -14426,7 +14432,7 @@
break;
}
}
- } else if (app.curAdj == ProcessList.HEAVY_WEIGHT_APP_ADJ) {
+ } else if (app.nonStoppingAdj == ProcessList.HEAVY_WEIGHT_APP_ADJ) {
if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_BACKGROUND
&& app.thread != null) {
try {
@@ -14437,7 +14443,7 @@
}
app.trimMemoryLevel = ComponentCallbacks2.TRIM_MEMORY_BACKGROUND;
} else {
- if ((app.curAdj > ProcessList.VISIBLE_APP_ADJ || app.systemNoUi)
+ if ((app.nonStoppingAdj > ProcessList.VISIBLE_APP_ADJ || app.systemNoUi)
&& app.pendingUiClean) {
// If this application is now in the background and it
// had done UI, then give it the special trim level to
@@ -14464,7 +14470,7 @@
final int N = mLruProcesses.size();
for (i=0; i<N; i++) {
ProcessRecord app = mLruProcesses.get(i);
- if ((app.curAdj > ProcessList.VISIBLE_APP_ADJ || app.systemNoUi)
+ if ((app.nonStoppingAdj > ProcessList.VISIBLE_APP_ADJ || app.systemNoUi)
&& app.pendingUiClean) {
if (app.trimMemoryLevel < ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
&& app.thread != null) {
diff --git a/services/java/com/android/server/am/ActivityStack.java b/services/java/com/android/server/am/ActivityStack.java
index a01ed25..6596e1f 100644
--- a/services/java/com/android/server/am/ActivityStack.java
+++ b/services/java/com/android/server/am/ActivityStack.java
@@ -97,6 +97,11 @@
// next activity.
static final int PAUSE_TIMEOUT = 500;
+ // How long we wait for the activity to tell us it has stopped before
+ // giving up. This is a good amount of time because we really need this
+ // from the application in order to get its saved state.
+ static final int STOP_TIMEOUT = 10*1000;
+
// How long we can hold the sleep wake lock before giving up.
static final int SLEEP_TIMEOUT = 5*1000;
@@ -280,6 +285,7 @@
static final int DESTROY_TIMEOUT_MSG = ActivityManagerService.FIRST_ACTIVITY_STACK_MSG + 5;
static final int RESUME_TOP_ACTIVITY_MSG = ActivityManagerService.FIRST_ACTIVITY_STACK_MSG + 6;
static final int LAUNCH_TICK_MSG = ActivityManagerService.FIRST_ACTIVITY_STACK_MSG + 7;
+ static final int STOP_TIMEOUT_MSG = ActivityManagerService.FIRST_ACTIVITY_STACK_MSG + 8;
final Handler mHandler = new Handler() {
//public Handler() {
@@ -364,6 +370,17 @@
resumeTopActivityLocked(null);
}
} break;
+ case STOP_TIMEOUT_MSG: {
+ ActivityRecord r = (ActivityRecord)msg.obj;
+ // We don't at this point know if the activity is fullscreen,
+ // so we need to be conservative and assume it isn't.
+ Slog.w(TAG, "Activity stop timeout for " + r);
+ synchronized (mService) {
+ if (r.isInHistory()) {
+ activityStoppedLocked(r, null, null, null);
+ }
+ }
+ } break;
}
}
};
@@ -1000,31 +1017,38 @@
final void activityStoppedLocked(ActivityRecord r, Bundle icicle, Bitmap thumbnail,
CharSequence description) {
if (DEBUG_SAVED_STATE) Slog.i(TAG, "Saving icicle of " + r + ": " + icicle);
- r.icicle = icicle;
- r.haveState = true;
- r.updateThumbnail(thumbnail, description);
- r.stopped = true;
- if (DEBUG_STATES) Slog.v(TAG, "Moving to STOPPED: " + r + " (stop complete)");
- r.state = ActivityState.STOPPED;
- if (!r.finishing) {
- if (r.configDestroy) {
- destroyActivityLocked(r, true, false, "stop-config");
- resumeTopActivityLocked(null);
- } else {
- // Now that this process has stopped, we may want to consider
- // it to be the previous app to try to keep around in case
- // the user wants to return to it.
- ProcessRecord fgApp = null;
- if (mResumedActivity != null) {
- fgApp = mResumedActivity.app;
- } else if (mPausingActivity != null) {
- fgApp = mPausingActivity.app;
- }
- if (r.app != null && fgApp != null && r.app != fgApp
- && r.lastVisibleTime > mService.mPreviousProcessVisibleTime
- && r.app != mService.mHomeProcess) {
- mService.mPreviousProcess = r.app;
- mService.mPreviousProcessVisibleTime = r.lastVisibleTime;
+ if (icicle != null) {
+ // If icicle is null, this is happening due to a timeout, so we
+ // haven't really saved the state.
+ r.icicle = icicle;
+ r.haveState = true;
+ r.updateThumbnail(thumbnail, description);
+ }
+ if (!r.stopped) {
+ if (DEBUG_STATES) Slog.v(TAG, "Moving to STOPPED: " + r + " (stop complete)");
+ mHandler.removeMessages(STOP_TIMEOUT_MSG, r);
+ r.stopped = true;
+ r.state = ActivityState.STOPPED;
+ if (!r.finishing) {
+ if (r.configDestroy) {
+ destroyActivityLocked(r, true, false, "stop-config");
+ resumeTopActivityLocked(null);
+ } else {
+ // Now that this process has stopped, we may want to consider
+ // it to be the previous app to try to keep around in case
+ // the user wants to return to it.
+ ProcessRecord fgApp = null;
+ if (mResumedActivity != null) {
+ fgApp = mResumedActivity.app;
+ } else if (mPausingActivity != null) {
+ fgApp = mPausingActivity.app;
+ }
+ if (r.app != null && fgApp != null && r.app != fgApp
+ && r.lastVisibleTime > mService.mPreviousProcessVisibleTime
+ && r.app != mService.mHomeProcess) {
+ mService.mPreviousProcess = r.app;
+ mService.mPreviousProcessVisibleTime = r.lastVisibleTime;
+ }
}
}
}
@@ -3228,6 +3252,9 @@
if (mService.isSleeping()) {
r.setSleeping(true);
}
+ Message msg = mHandler.obtainMessage(STOP_TIMEOUT_MSG);
+ msg.obj = r;
+ mHandler.sendMessageDelayed(msg, STOP_TIMEOUT);
} catch (Exception e) {
// Maybe just ignore exceptions here... if the process
// has crashed, our death notification will clean things
@@ -3694,6 +3721,7 @@
// Get rid of any pending idle timeouts.
mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r);
+ mHandler.removeMessages(STOP_TIMEOUT_MSG, r);
mHandler.removeMessages(IDLE_TIMEOUT_MSG, r);
mHandler.removeMessages(DESTROY_TIMEOUT_MSG, r);
r.finishLaunchTickingLocked();
diff --git a/services/java/com/android/server/am/ProcessRecord.java b/services/java/com/android/server/am/ProcessRecord.java
index b64261d..4529ecc 100644
--- a/services/java/com/android/server/am/ProcessRecord.java
+++ b/services/java/com/android/server/am/ProcessRecord.java
@@ -63,6 +63,7 @@
int hiddenAdj; // If hidden, this is the adjustment to use
int curRawAdj; // Current OOM unlimited adjustment for this process
int setRawAdj; // Last set OOM unlimited adjustment for this process
+ int nonStoppingAdj; // Adjustment not counting any stopping activities
int curAdj; // Current OOM adjustment for this process
int setAdj; // Last set OOM adjustment for this process
int curSchedGroup; // Currently desired scheduling class
@@ -199,6 +200,7 @@
pw.print(" hidden="); pw.print(hiddenAdj);
pw.print(" curRaw="); pw.print(curRawAdj);
pw.print(" setRaw="); pw.print(setRawAdj);
+ pw.print(" nonStopping="); pw.print(nonStoppingAdj);
pw.print(" cur="); pw.print(curAdj);
pw.print(" set="); pw.println(setAdj);
pw.print(prefix); pw.print("curSchedGroup="); pw.print(curSchedGroup);