Start process of next activity with top priority in advance

In common cases, to resume the next activity we need to wait for the
current one to be paused. Since starting a process for activity is
asynchronous, if we already know the process of next activity has not
started yet, we can start the process earlier so the time waiting for
the pause to complete can be saved.

Also if the launching activity is going to be the top app, we can set
the top schedule group right after its process is started so the start
time before actually launching the activity can be improved.

Although before the current activity is paused, the next top activity
may still change and results some empty processes. That should not be
a common case and the process is still useful when going back the stack,
and the empty background processes are easier to be reclaimed.

Bug: 123043091
Test: AppLaunchTest
Test: Launch calculator from launcher, the event log am_proc_start will
      show "pre-top-activity".
Test: Cold launch a top activity, the system log should not show
      "not expected top priority".
Test: Use startActivities to start serveral activities in a sequence.
      Check "adb shell cat /proc/$pid/task/$pid/cgroup" for each process.
      Only the last one has top-app, others are background.

Change-Id: I9601b66e7cc0855fd7c2b573ded31fcf8d0711ae
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8915b9a..66ab511 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4735,7 +4735,7 @@
         EventLog.writeEvent(EventLogTags.AM_PROC_BOUND, app.userId, app.pid, app.processName);
 
         app.curAdj = app.setAdj = app.verifiedAdj = ProcessList.INVALID_ADJ;
-        app.setCurrentSchedulingGroup(app.setSchedGroup = ProcessList.SCHED_GROUP_DEFAULT);
+        mOomAdjuster.setAttachingSchedGroupLocked(app);
         app.forcingToImportant = null;
         updateProcessForegroundLocked(app, false, 0, false);
         app.hasShownUi = false;
@@ -18294,16 +18294,19 @@
         }
 
         @Override
-        public void startProcess(String processName, ApplicationInfo info,
-                boolean knownToBeDead, String hostingType, ComponentName hostingName) {
+        public void startProcess(String processName, ApplicationInfo info, boolean knownToBeDead,
+                boolean isTop, String hostingType, ComponentName hostingName) {
             try {
                 if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                     Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "startProcess:"
                             + processName);
                 }
                 synchronized (ActivityManagerService.this) {
+                    // If the process is known as top app, set a hint so when the process is
+                    // started, the top priority can be applied immediately to avoid cpu being
+                    // preempted by other processes before attaching the process of top app.
                     startProcessLocked(processName, info, knownToBeDead, 0 /* intentFlags */,
-                            new HostingRecord(hostingType, hostingName),
+                            new HostingRecord(hostingType, hostingName, isTop),
                             false /* allowWhileBooting */, false /* isolated */,
                             true /* keepIfLarge */);
                 }
diff --git a/services/core/java/com/android/server/am/HostingRecord.java b/services/core/java/com/android/server/am/HostingRecord.java
index 784dde1..6bb5def 100644
--- a/services/core/java/com/android/server/am/HostingRecord.java
+++ b/services/core/java/com/android/server/am/HostingRecord.java
@@ -41,6 +41,8 @@
  * {@link android.content.Context#BIND_EXTERNAL_SERVICE} service. In that case, the packageName
  * and uid in the ApplicationInfo will be set to those of the caller, not of the defining package.
  *
+ * {@code mIsTopApp} will be passed to {@link android.os.Process#start}. So Zygote will initialize
+ * the process with high priority.
  */
 
 public final class HostingRecord {
@@ -53,15 +55,22 @@
     private final int mHostingZygote;
     private final String mDefiningPackageName;
     private final int mDefiningUid;
+    private final boolean mIsTopApp;
 
     public HostingRecord(String hostingType) {
-        this(hostingType, null, REGULAR_ZYGOTE, null, -1);
+        this(hostingType, null /* hostingName */, REGULAR_ZYGOTE, null /* definingPackageName */,
+                -1 /* mDefiningUid */, false /* isTopApp */);
     }
 
     public HostingRecord(String hostingType, ComponentName hostingName) {
         this(hostingType, hostingName, REGULAR_ZYGOTE);
     }
 
+    public HostingRecord(String hostingType, ComponentName hostingName, boolean isTopApp) {
+        this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE,
+                null /* definingPackageName */, -1 /* mDefiningUid */, isTopApp /* isTopApp */);
+    }
+
     public HostingRecord(String hostingType, String hostingName) {
         this(hostingType, hostingName, REGULAR_ZYGOTE);
     }
@@ -71,16 +80,18 @@
     }
 
     private HostingRecord(String hostingType, String hostingName, int hostingZygote) {
-        this(hostingType, hostingName, hostingZygote, null, -1);
+        this(hostingType, hostingName, hostingZygote, null /* definingPackageName */,
+                -1 /* mDefiningUid */, false /* isTopApp */);
     }
 
     private HostingRecord(String hostingType, String hostingName, int hostingZygote,
-            String definingPackageName, int definingUid) {
+            String definingPackageName, int definingUid, boolean isTopApp) {
         mHostingType = hostingType;
         mHostingName = hostingName;
         mHostingZygote = hostingZygote;
         mDefiningPackageName = definingPackageName;
         mDefiningUid = definingUid;
+        mIsTopApp = isTopApp;
     }
 
     public String getType() {
@@ -91,6 +102,10 @@
         return mHostingName;
     }
 
+    public boolean isTopApp() {
+        return mIsTopApp;
+    }
+
     /**
      * Returns the UID of the package defining the component we want to start. Only valid
      * when {@link #usesAppZygote()} returns true.
@@ -130,7 +145,7 @@
     public static HostingRecord byAppZygote(ComponentName hostingName, String definingPackageName,
             int definingUid) {
         return new HostingRecord("", hostingName.toShortString(), APP_ZYGOTE,
-                definingPackageName, definingUid);
+                definingPackageName, definingUid, false /* isTopApp */);
     }
 
     /**
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 7abfcea..b930e17 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -177,9 +177,12 @@
         adjusterThread.start();
         Process.setThreadGroupAndCpuset(adjusterThread.getThreadId(), THREAD_GROUP_TOP_APP);
         mProcessGroupHandler = new Handler(adjusterThread.getLooper(), msg -> {
-            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setProcessGroup");
             final int pid = msg.arg1;
             final int group = msg.arg2;
+            if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setProcessGroup "
+                        + msg.obj + " to " + group);
+            }
             try {
                 setProcessGroup(pid, group);
             } catch (Exception e) {
@@ -1757,7 +1760,7 @@
                         break;
                 }
                 mProcessGroupHandler.sendMessage(mProcessGroupHandler.obtainMessage(
-                        0 /* unused */, app.pid, processGroup));
+                        0 /* unused */, app.pid, processGroup, app.processName));
                 try {
                     if (curSchedGroup == ProcessList.SCHED_GROUP_TOP_APP) {
                         // do nothing if we already switched to RT
@@ -1948,6 +1951,38 @@
         return success;
     }
 
+    @GuardedBy("mService")
+    void setAttachingSchedGroupLocked(ProcessRecord app) {
+        int initialSchedGroup = ProcessList.SCHED_GROUP_DEFAULT;
+        // If the process has been marked as foreground via Zygote.START_FLAG_USE_TOP_APP_PRIORITY,
+        // then verify that the top priority is actually is applied.
+        if (app.hasForegroundActivities()) {
+            String fallbackReason = null;
+            try {
+                // The priority must be the same as how does {@link #applyOomAdjLocked} set for
+                // {@link ProcessList.SCHED_GROUP_TOP_APP}. We don't check render thread because it
+                // is not ready when attaching.
+                if (Process.getProcessGroup(app.pid) == THREAD_GROUP_TOP_APP) {
+                    app.getWindowProcessController().onTopProcChanged();
+                    setThreadPriority(app.pid, TOP_APP_PRIORITY_BOOST);
+                } else {
+                    fallbackReason = "not expected top priority";
+                }
+            } catch (Exception e) {
+                fallbackReason = e.toString();
+            }
+            if (fallbackReason == null) {
+                initialSchedGroup = ProcessList.SCHED_GROUP_TOP_APP;
+            } else {
+                // The real scheduling group will depend on if there is any component of the process
+                // did something during attaching.
+                Slog.w(TAG, "Fallback pre-set sched group to default: " + fallbackReason);
+            }
+        }
+
+        app.setCurrentSchedulingGroup(app.setSchedGroup = initialSchedGroup);
+    }
+
     // ONLY used for unit testing in OomAdjusterTests.java
     @VisibleForTesting
     void maybeUpdateUsageStats(ProcessRecord app, long nowElapsed) {
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index d64a2c2..11f0e3d 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -1803,6 +1803,14 @@
             Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " +
                     app.processName);
             checkSlow(startTime, "startProcess: asking zygote to start proc");
+            final boolean isTopApp = hostingRecord.isTopApp();
+            if (isTopApp) {
+                // Use has-foreground-activities as a temporary hint so the current scheduling
+                // group won't be lost when the process is attaching. The actual state will be
+                // refreshed when computing oom-adj.
+                app.setHasForegroundActivities(true);
+            }
+
             final Process.ProcessStartResult startResult;
             if (hostingRecord.usesWebviewZygote()) {
                 startResult = startWebView(entryPoint,
@@ -1817,13 +1825,13 @@
                         app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                         app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                         app.info.dataDir, null, app.info.packageName,
-                        /*useUsapPool=*/ false,
+                        /*useUsapPool=*/ false, isTopApp,
                         new String[] {PROC_START_SEQ_IDENT + app.startSeq});
             } else {
                 startResult = Process.start(entryPoint,
                         app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                         app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
-                        app.info.dataDir, invokeWith, app.info.packageName,
+                        app.info.dataDir, invokeWith, app.info.packageName, isTopApp,
                         new String[] {PROC_START_SEQ_IDENT + app.startSeq});
             }
             checkSlow(startTime, "startProcess: returned from zygote!");
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 1344727..d306b85 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3549,7 +3549,7 @@
         mStackSupervisor.scheduleRestartTimeout(this);
     }
 
-    private boolean isProcessRunning() {
+    boolean isProcessRunning() {
         WindowProcessController proc = app;
         if (proc == null) {
             proc = mAtmService.mProcessNames.get(processName, info.applicationInfo.uid);
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 74c3069..156fb98 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -2743,7 +2743,7 @@
         final boolean resumeWhilePausing = (next.info.flags & FLAG_RESUME_WHILE_PAUSING) != 0
                 && !lastResumedCanPip;
 
-        boolean pausing = getDisplay().pauseBackStacks(userLeaving, next, false);
+        boolean pausing = display.pauseBackStacks(userLeaving, next, false);
         if (mResumedActivity != null) {
             if (DEBUG_STATES) Slog.d(TAG_STATES,
                     "resumeTopActivityLocked: Pausing " + mResumedActivity);
@@ -2759,6 +2759,13 @@
             if (next.attachedToProcess()) {
                 next.app.updateProcessInfo(false /* updateServiceConnectionActivities */,
                         true /* activityChange */, false /* updateOomAdj */);
+            } else if (!next.isProcessRunning()) {
+                // Since the start-process is asynchronous, if we already know the process of next
+                // activity isn't running, we can start the process earlier to save the time to wait
+                // for the current activity to be paused.
+                final boolean isTop = this == display.getFocusedStack();
+                mService.startProcessAsync(next, false /* knownToBeDead */, isTop,
+                        isTop ? "pre-top-activity" : "pre-activity");
             }
             if (lastResumed != null) {
                 lastResumed.setWillCloseOrEnterPip(true);
@@ -2827,7 +2834,7 @@
         // that the previous one will be hidden soon.  This way it can know
         // to ignore it when computing the desired screen orientation.
         boolean anim = true;
-        final DisplayContent dc = getDisplay().mDisplayContent;
+        final DisplayContent dc = display.mDisplayContent;
         if (prev != null) {
             if (prev.finishing) {
                 if (DEBUG_TRANSITION) Slog.v(TAG_TRANSITION,
@@ -2983,7 +2990,7 @@
                 next.clearOptionsLocked();
                 transaction.setLifecycleStateRequest(
                         ResumeActivityItem.obtain(next.app.getReportedProcState(),
-                                getDisplay().mDisplayContent.isNextTransitionForward()));
+                                dc.isNextTransitionForward()));
                 mService.getLifecycleManager().scheduleTransaction(transaction);
 
                 if (DEBUG_STATES) Slog.d(TAG_STATES, "resumeTopActivityLocked: Resumed "
diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
index c992a69..94800fc 100644
--- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
@@ -980,20 +980,8 @@
             r.notifyUnknownVisibilityLaunched();
         }
 
-        try {
-            if (Trace.isTagEnabled(TRACE_TAG_ACTIVITY_MANAGER)) {
-                Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "dispatchingStartProcess:"
-                        + r.processName);
-            }
-            // Post message to start process to avoid possible deadlock of calling into AMS with the
-            // ATMS lock held.
-            final Message msg = PooledLambda.obtainMessage(
-                    ActivityManagerInternal::startProcess, mService.mAmInternal, r.processName,
-                    r.info.applicationInfo, knownToBeDead, "activity", r.intent.getComponent());
-            mService.mH.sendMessage(msg);
-        } finally {
-            Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
-        }
+        final boolean isTop = andResume && r.isTopRunningActivity();
+        mService.startProcessAsync(r, knownToBeDead, isTop, isTop ? "top-activity" : "activity");
     }
 
     boolean checkStartAnyActivityPermission(Intent intent, ActivityInfo aInfo, String resultWho,
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index f76d0eb..d4b49aa 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5631,6 +5631,24 @@
         mH.sendMessage(m);
     }
 
+    void startProcessAsync(ActivityRecord activity, boolean knownToBeDead, boolean isTop,
+            String hostingType) {
+        try {
+            if (Trace.isTagEnabled(TRACE_TAG_ACTIVITY_MANAGER)) {
+                Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "dispatchingStartProcess:"
+                        + activity.processName);
+            }
+            // Post message to start process to avoid possible deadlock of calling into AMS with the
+            // ATMS lock held.
+            final Message m = PooledLambda.obtainMessage(ActivityManagerInternal::startProcess,
+                    mAmInternal, activity.processName, activity.info.applicationInfo, knownToBeDead,
+                    isTop, hostingType, activity.intent.getComponent());
+            mH.sendMessage(m);
+        } finally {
+            Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
+        }
+    }
+
     void setBooting(boolean booting) {
         mAmInternal.setBooting(booting);
     }