1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tests.applaunch;
17 
18 import static org.junit.Assert.assertNotNull;
19 
20 import android.accounts.Account;
21 import android.accounts.AccountManager;
22 import android.app.ActivityManager;
23 import android.app.ActivityManager.ProcessErrorStateInfo;
24 import android.app.IActivityManager;
25 import android.app.UiAutomation;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.PackageManager;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.content.pm.ResolveInfo;
31 import android.os.Bundle;
32 import android.os.ParcelFileDescriptor;
33 import android.os.RemoteException;
34 import android.os.SystemClock;
35 import android.os.UserHandle;
36 import android.support.test.uiautomator.UiDevice;
37 import android.test.InstrumentationTestCase;
38 import android.test.InstrumentationTestRunner;
39 import android.util.Log;
40 
41 import androidx.test.rule.logging.AtraceLogger;
42 
43 import java.io.BufferedReader;
44 import java.io.BufferedWriter;
45 import java.io.File;
46 import java.io.FileInputStream;
47 import java.io.FileOutputStream;
48 import java.io.FileWriter;
49 import java.io.IOException;
50 import java.io.InputStream;
51 import java.io.InputStreamReader;
52 import java.io.OutputStreamWriter;
53 import java.nio.file.Paths;
54 import java.time.format.DateTimeFormatter;
55 import java.time.ZonedDateTime;
56 import java.time.ZoneOffset;
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.HashMap;
60 import java.util.HashSet;
61 import java.util.LinkedHashMap;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Set;
65 
66 /**
67  * This test is intended to measure the time it takes for the apps to start.
68  * Names of the applications are passed in command line, and the
69  * test starts each application, and reports the start up time in milliseconds.
70  * The instrumentation expects the following key to be passed on the command line:
71  * apps - A list of applications to start and their corresponding result keys
72  * in the following format:
73  * -e apps <app name>^<result key>|<app name>^<result key>
74  */
75 @Deprecated
76 public class AppLaunch extends InstrumentationTestCase {
77 
78     private static final int JOIN_TIMEOUT = 10000;
79     private static final String TAG = AppLaunch.class.getSimpleName();
80 
81     // optional parameter: comma separated list of required account types before proceeding
82     // with the app launch
83     private static final String KEY_REQUIRED_ACCOUNTS = "required_accounts";
84     private static final String KEY_APPS = "apps";
85     private static final String KEY_IORAP_TRIAL_LAUNCH = "iorap_trial_launch";
86     private static final String KEY_IORAP_COMPILER_FILTERS = "iorap_compiler_filters";
87     private static final String KEY_TRIAL_LAUNCH = "trial_launch";
88     private static final String KEY_LAUNCH_ITERATIONS = "launch_iterations";
89     private static final String KEY_LAUNCH_ORDER = "launch_order";
90     private static final String KEY_DROP_CACHE = "drop_cache";
91     private static final String KEY_SIMPLEPERF_CMD = "simpleperf_cmd";
92     private static final String KEY_SIMPLEPERF_APP = "simpleperf_app";
93     private static final String KEY_CYCLE_CLEAN = "cycle_clean";
94     private static final String KEY_TRACE_ALL = "trace_all";
95     private static final String KEY_TRACE_ITERATIONS = "trace_iterations";
96     private static final String KEY_LAUNCH_DIRECTORY = "launch_directory";
97     private static final String KEY_TRACE_DIRECTORY = "trace_directory";
98     private static final String KEY_TRACE_CATEGORY = "trace_categories";
99     private static final String KEY_TRACE_BUFFERSIZE = "trace_bufferSize";
100     private static final String KEY_TRACE_DUMPINTERVAL = "tracedump_interval";
101     private static final String KEY_COMPILER_FILTERS = "compiler_filters";
102     private static final String KEY_FORCE_STOP_APP = "force_stop_app";
103     private static final String ENABLE_SCREEN_RECORDING = "enable_screen_recording";
104     private static final int MAX_RECORDING_PARTS = 5;
105     private static final long VIDEO_TAIL_BUFFER = 500;
106 
107     private static final String SIMPLEPERF_APP_CMD =
108             "simpleperf --log fatal stat --csv -e cpu-cycles,major-faults --app %s & %s";
109     private static final String WEARABLE_ACTION_GOOGLE =
110             "com.google.android.wearable.action.GOOGLE";
111     private static final int INITIAL_LAUNCH_IDLE_TIMEOUT = 5000; // 5s to allow app to idle
112     private static final int POST_LAUNCH_IDLE_TIMEOUT = 750; // 750ms idle for non initial launches
113     private static final int BEFORE_FORCE_STOP_SLEEP_TIMEOUT = 1000; // 1s before force stopping
114     private static final int BEFORE_KILL_APP_SLEEP_TIMEOUT = 1000; // 1s before killing
115     private static final int BETWEEN_LAUNCH_SLEEP_TIMEOUT = 3000; // 3s between launching apps
116     private static final int PROFILE_SAVE_SLEEP_TIMEOUT = 1000; // Allow 1s for the profile to save
117     private static final int IORAP_TRACE_DURATION_TIMEOUT = 7000; // Allow 7s for trace to complete.
118     private static final int IORAP_TRIAL_LAUNCH_ITERATIONS = 5;  // min 5 launches to merge traces.
119     private static final int IORAP_COMPILE_CMD_TIMEOUT = 60;  // in seconds: 1 minutes
120     private static final int IORAP_COMPILE_MIN_TRACES = 1;  // configure iorapd to need 1 trace.
121     private static final int IORAP_COMPILE_RETRIES = 3;  // retry compiler 3 times if it fails.
122     private static final String LAUNCH_SUB_DIRECTORY = "launch_logs";
123     private static final String LAUNCH_FILE = "applaunch.txt";
124     private static final String TRACE_SUB_DIRECTORY = "atrace_logs";
125     private static final String DEFAULT_TRACE_CATEGORIES =
126             "sched,freq,gfx,view,dalvik,webview,input,wm,disk,am,wm,binder_driver,hal,ss";
127     private static final String DEFAULT_TRACE_BUFFER_SIZE = "20000";
128     private static final String DEFAULT_TRACE_DUMP_INTERVAL = "10";
129     private static final String TRIAL_LAUNCH = "TRIAL_LAUNCH";
130     private static final String IORAP_TRIAL_LAUNCH = "IORAP_TRIAL_LAUNCH";
131     private static final String IORAP_TRIAL_LAUNCH_FIRST = "IORAP_TRIAL_LAUNCH_FIRST";
132     private static final String IORAP_TRIAL_LAUNCH_LAST = "IORAP_TRIAL_LAUNCH_LAST";
133     private static final String DELIMITER = ",";
134     private static final String DROP_CACHE_SCRIPT = "/data/local/tmp/dropCache.sh";
135     private static final String APP_LAUNCH_CMD = "am start -W -n";
136     private static final String SUCCESS_MESSAGE = "Status: ok";
137     private static final String TOTAL_TIME_MESSAGE = "TotalTime:";
138     private static final String COMPILE_SUCCESS = "Success";
139     private static final String LAUNCH_ITERATION = "LAUNCH_ITERATION-%d";
140     private static final String TRACE_ITERATION = "TRACE_ITERATION-%d";
141     private static final String LAUNCH_ITERATION_PREFIX = "LAUNCH_ITERATION";
142     private static final String TRACE_ITERATION_PREFIX = "TRACE_ITERATION";
143     private static final String LAUNCH_ORDER_CYCLIC = "cyclic";
144     private static final String LAUNCH_ORDER_SEQUENTIAL = "sequential";
145     private static final String COMPILE_CMD = "cmd package compile -f -m %s %s";
146     private static final String IORAP_COMPILE_CMD = "dumpsys iorapd --compile-package %s";
147     private static final String IORAP_MAINTENANCE_CMD =
148             "dumpsys iorapd --purge-package %s";
149     private static final String IORAP_DUMPSYS_CMD = "dumpsys iorapd";
150     private static final String SPEED_PROFILE_FILTER = "speed-profile";
151     private static final String VERIFY_FILTER = "verify";
152     private static final String LAUNCH_SCRIPT_NAME = "appLaunch";
153 
154     private Map<String, Intent> mNameToIntent;
155     private List<LaunchOrder> mLaunchOrderList = new ArrayList<LaunchOrder>();
156     private RecordingThread mCurrentThread;
157     private Map<String, String> mNameToResultKey;
158     private Map<String, Map<String, List<AppLaunchResult>>> mNameToLaunchTime;
159     private IActivityManager mAm;
160     private File launchSubDir = null;
161     private String mSimplePerfCmd = null;
162     private String mLaunchOrder = null;
163     private boolean mDropCache = false;
164     private int mLaunchIterations = 10;
165     private boolean mForceStopApp = true;
166     private boolean mEnableRecording = false;
167     private int mTraceLaunchCount = 0;
168     private String mTraceDirectoryStr = null;
169     private Bundle mResult = new Bundle();
170     private Set<String> mRequiredAccounts;
171     private boolean mTrialLaunch = false;
172     private boolean mIorapTrialLaunch = false;
173     private BufferedWriter mBufferedWriter = null;
174     private boolean mSimplePerfAppOnly = false;
175     private String[] mCompilerFilters = null;
176     private List<String> mIorapCompilerFilters = null;
177     private String mLastAppName = "";
178     private boolean mCycleCleanUp = false;
179     private boolean mTraceAll = false;
180     private boolean mIterationCycle = false;
181     private UiDevice mDevice;
182 
183     enum IorapStatus {
184         UNDEFINED,
185         ENABLED,
186         DISABLED
187     }
188     private IorapStatus mIorapStatus = IorapStatus.UNDEFINED;
189     private long mCycleTime = 0;
190     private StringBuilder mCycleTimes = new StringBuilder();
191 
192     @Override
setUp()193     protected void setUp() throws Exception {
194         super.setUp();
195         getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
196     }
197 
198     @Override
tearDown()199     protected void tearDown() throws Exception {
200         getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
201         super.tearDown();
202     }
203 
addLaunchResult(LaunchOrder launch, AppLaunchResult result)204     private void addLaunchResult(LaunchOrder launch, AppLaunchResult result) {
205         mNameToLaunchTime.get(launch.getApp()).get(launch.getCompilerFilter()).add(result);
206     }
207 
hasFailureOnFirstLaunch(LaunchOrder launch)208     private boolean hasFailureOnFirstLaunch(LaunchOrder launch) {
209         List<AppLaunchResult> results =
210             mNameToLaunchTime.get(launch.getApp()).get(launch.getCompilerFilter());
211         return (results.size() > 0) && (results.get(0).mLaunchTime < 0);
212     }
213 
testMeasureStartUpTime()214     public void testMeasureStartUpTime() throws RemoteException, NameNotFoundException,
215             IOException, InterruptedException {
216         InstrumentationTestRunner instrumentation =
217                 (InstrumentationTestRunner)getInstrumentation();
218         Bundle args = instrumentation.getArguments();
219         mAm = ActivityManager.getService();
220         String launchDirectory = args.getString(KEY_LAUNCH_DIRECTORY);
221 
222         createMappings();
223         parseArgs(args);
224         checkAccountSignIn();
225 
226         // Root directory for applaunch file to log the app launch output
227         // Will be useful in case of simpleperf command is used
228         File launchRootDir = null;
229         if (null != launchDirectory && !launchDirectory.isEmpty()) {
230             launchRootDir = new File(launchDirectory);
231             if (!launchRootDir.exists() && !launchRootDir.mkdirs()) {
232                 throw new IOException("Unable to create the destination directory "
233                     + launchRootDir + ". Try disabling selinux.");
234             }
235         }
236 
237         try {
238             launchSubDir = new File(launchRootDir, LAUNCH_SUB_DIRECTORY);
239 
240             if (!launchSubDir.exists() && !launchSubDir.mkdirs()) {
241                 throw new IOException("Unable to create the lauch file sub directory "
242                     + launchSubDir + ". Try disabling selinux.");
243             }
244             File file = new File(launchSubDir, LAUNCH_FILE);
245             FileOutputStream outputStream = new FileOutputStream(file);
246             mBufferedWriter = new BufferedWriter(new OutputStreamWriter(
247                     outputStream));
248 
249             // Root directory for trace file during the launches
250             File rootTrace = null;
251             File rootTraceSubDir = null;
252             int traceBufferSize = 0;
253             int traceDumpInterval = 0;
254             Set<String> traceCategoriesSet = null;
255             if (null != mTraceDirectoryStr && !mTraceDirectoryStr.isEmpty()) {
256                 rootTrace = new File(mTraceDirectoryStr);
257                 if (!rootTrace.exists() && !rootTrace.mkdirs()) {
258                     throw new IOException("Unable to create the trace directory");
259                 }
260                 rootTraceSubDir = new File(rootTrace, TRACE_SUB_DIRECTORY);
261                 if (!rootTraceSubDir.exists() && !rootTraceSubDir.mkdirs()) {
262                     throw new IOException("Unable to create the trace sub directory");
263                 }
264                 assertNotNull("Trace iteration parameter is mandatory",
265                         args.getString(KEY_TRACE_ITERATIONS));
266                 mTraceLaunchCount = Integer.parseInt(args.getString(KEY_TRACE_ITERATIONS));
267                 String traceCategoriesStr = args
268                         .getString(KEY_TRACE_CATEGORY, DEFAULT_TRACE_CATEGORIES);
269                 traceBufferSize = Integer.parseInt(args.getString(KEY_TRACE_BUFFERSIZE,
270                         DEFAULT_TRACE_BUFFER_SIZE));
271                 traceDumpInterval = Integer.parseInt(args.getString(KEY_TRACE_DUMPINTERVAL,
272                         DEFAULT_TRACE_DUMP_INTERVAL));
273                 traceCategoriesSet = new HashSet<String>();
274                 if (!traceCategoriesStr.isEmpty()) {
275                     String[] traceCategoriesSplit = traceCategoriesStr.split(DELIMITER);
276                     for (int i = 0; i < traceCategoriesSplit.length; i++) {
277                         traceCategoriesSet.add(traceCategoriesSplit[i]);
278                     }
279                 }
280             }
281 
282             // Get the app launch order based on launch order, trial launch,
283             // launch iterations and trace iterations
284             setLaunchOrder();
285 
286             for (LaunchOrder launch : mLaunchOrderList) {
287                 toggleIorapStatus(launch.getIorapEnabled());
288                 dropCache(/*override*/false);
289 
290                 Log.v(TAG, "Launch reason: " + launch.getLaunchReason());
291 
292                 // App launch times for trial launch will not be used for final
293                 // launch time calculations.
294                 if (launch.getLaunchReason().equals(TRIAL_LAUNCH)) {
295                     mIterationCycle = false;
296                     // In the "applaunch.txt" file, trail launches is referenced using
297                     // "TRIAL_LAUNCH"
298                     Intent startIntent = mNameToIntent.get(launch.getApp());
299                     if (startIntent == null) {
300                         Log.w(TAG, "App does not exist: " + launch.getApp());
301                         mResult.putString(mNameToResultKey.get(launch.getApp()),
302                             "App does not exist");
303                         continue;
304                     }
305                     String appPkgName = startIntent.getComponent().getPackageName();
306                     if (SPEED_PROFILE_FILTER.equals(launch.getCompilerFilter())) {
307                         assertTrue(String.format("Not able to compile the app : %s", appPkgName),
308                               compileApp(VERIFY_FILTER, appPkgName));
309                     } else if (launch.getCompilerFilter() != null) {
310                         assertTrue(String.format("Not able to compile the app : %s", appPkgName),
311                               compileApp(launch.getCompilerFilter(), appPkgName));
312                     }
313                     // We only need to run a trial for the speed-profile filter, but we always
314                     // run one for "applaunch.txt" consistency.
315                     AppLaunchResult launchResult =
316                         startApp(launch.getApp(), launch.getLaunchReason());
317                     if (launchResult.mLaunchTime < 0) {
318                         addLaunchResult(launch, new AppLaunchResult());
319                         // simply pass the app if launch isn't successful
320                         // error should have already been logged by startApp
321                         continue;
322                     }
323                     sleep(INITIAL_LAUNCH_IDLE_TIMEOUT);
324                     if (SPEED_PROFILE_FILTER.equals(launch.getCompilerFilter())) {
325                         // Send SIGUSR1 to force dumping a profile.
326                         String sendSignalCommand =
327                             String.format("killall -s SIGUSR1 %s", appPkgName);
328                         getInstrumentation().getUiAutomation().executeShellCommand(
329                             sendSignalCommand);
330                         // killall is async, wait one second to let the app save the profile.
331                         sleep(PROFILE_SAVE_SLEEP_TIMEOUT);
332                         assertTrue(String.format("Not able to compile the app : %s", appPkgName),
333                               compileApp(launch.getCompilerFilter(), appPkgName));
334                     }
335                 }
336                 else if (launch.getLaunchReason().startsWith(IORAP_TRIAL_LAUNCH)) {
337                     mIterationCycle = false;
338 
339                     // In the "applaunch.txt" file, iorap-trial launches is referenced using
340                     // "IORAP_TRIAL_LAUNCH" or "IORAP_TRIAL_LAUNCH_LAST"
341                     Intent startIntent = mNameToIntent.get(launch.getApp());
342                     if (startIntent == null) {
343                         Log.w(TAG, "App does not exist: " + launch.getApp());
344                         mResult.putString(mNameToResultKey.get(launch.getApp()),
345                             "App does not exist");
346                         continue;
347                     }
348                     String appPkgName = startIntent.getComponent().getPackageName();
349 
350                     if (launch.getLaunchReason().equals(IORAP_TRIAL_LAUNCH_FIRST)) {
351                         // delete any iorap-traces associated with this package.
352                         purgeIorapPackage(appPkgName);
353                     }
354                     dropCache(/*override*/true);  // iorap-trial runs must have drop cache.
355 
356                     AppLaunchResult launchResult =
357                         startApp(launch.getApp(), launch.getLaunchReason());
358                     if (launchResult.mLaunchTime < 0) {
359                         addLaunchResult(launch, new AppLaunchResult());
360                         // simply pass the app if launch isn't successful
361                         // error should have already been logged by startApp
362                         continue;
363                     }
364                     // wait for slightly more than 5s (iorapd.perfetto.trace_duration_ms) for the trace buffers to complete.
365                     sleep(IORAP_TRACE_DURATION_TIMEOUT);
366 
367                     if (launch.getLaunchReason().equals(IORAP_TRIAL_LAUNCH_LAST)) {
368                         // run the iorap compiler and wait for iorap to compile fully.
369                         // this throws an exception if it fails.
370                         compileAppForIorapWithRetries(appPkgName, IORAP_COMPILE_RETRIES);
371                     }
372                 }
373 
374                 // App launch times used for final calculation
375                 else if (launch.getLaunchReason().contains(LAUNCH_ITERATION_PREFIX)) {
376                     mIterationCycle = true;
377                     AppLaunchResult launchResults = null;
378                     if (hasFailureOnFirstLaunch(launch)) {
379                         // skip if the app has failures while launched first
380                         Log.w(TAG, "Has failures on first launch: " + launch.getApp());
381                         forceStopApp(launch.getApp());
382                         continue;
383                     }
384                     AtraceLogger atraceLogger = null;
385                     if (mTraceAll) {
386                         Log.i(TAG, "Started tracing " + launch.getApp());
387                         atraceLogger = AtraceLogger
388                                 .getAtraceLoggerInstance(getInstrumentation());
389                     }
390                     try {
391                         // Start the trace
392                         if (atraceLogger != null) {
393                             atraceLogger.atraceStart(traceCategoriesSet, traceBufferSize,
394                                     traceDumpInterval, rootTraceSubDir,
395                                     String.format("%s-%s-%s", launch.getApp(),
396                                             launch.getCompilerFilter(), launch.getLaunchReason()));
397                         }
398                         // In the "applaunch.txt" file app launches are referenced using
399                         // "LAUNCH_ITERATION - ITERATION NUM"
400                         launchResults = startApp(launch.getApp(), launch.getLaunchReason());
401                         if (launchResults.mLaunchTime < 0) {
402                             addLaunchResult(launch, new AppLaunchResult());
403                             // if it fails once, skip the rest of the launches
404                             continue;
405                         } else {
406                             mCycleTime += launchResults.mLaunchTime;
407                             addLaunchResult(launch, launchResults);
408                         }
409                         sleep(POST_LAUNCH_IDLE_TIMEOUT);
410                     } finally {
411                         // Stop the trace
412                         if (atraceLogger != null) {
413                             Log.i(TAG, "Stopped tracing " + launch.getApp());
414                             atraceLogger.atraceStop();
415                         }
416                     }
417 
418                 }
419 
420                 // App launch times for trace launch will not be used for final
421                 // launch time calculations.
422                 else if (launch.getLaunchReason().contains(TRACE_ITERATION_PREFIX)) {
423                     mIterationCycle = false;
424                     AtraceLogger atraceLogger = AtraceLogger
425                             .getAtraceLoggerInstance(getInstrumentation());
426                     // Start the trace
427                     try {
428                         atraceLogger.atraceStart(traceCategoriesSet, traceBufferSize,
429                                 traceDumpInterval, rootTraceSubDir,
430                                 String.format("%s-%s-%s", launch.getApp(),
431                                         launch.getCompilerFilter(), launch.getLaunchReason()));
432                         startApp(launch.getApp(), launch.getLaunchReason());
433                         sleep(POST_LAUNCH_IDLE_TIMEOUT);
434                     } finally {
435                         // Stop the trace
436                         atraceLogger.atraceStop();
437                     }
438                 }
439                 if(mForceStopApp) {
440                     sleep(BEFORE_FORCE_STOP_SLEEP_TIMEOUT);
441                     forceStopApp(launch.getApp());
442                     sleep(BEFORE_KILL_APP_SLEEP_TIMEOUT);
443                     // Close again for good measure (just in case).
444                     forceStopApp(launch.getApp());
445                     // Kill the backgrounded process in the case forceStopApp only sent it to
446                     // background.
447                     killBackgroundApp(launch.getApp());
448                 } else {
449                     startHomeIntent();
450                 }
451                 sleep(BETWEEN_LAUNCH_SLEEP_TIMEOUT);
452 
453                 // If cycle clean up is enabled and last app launched is
454                 // current app then the cycle is completed and eligible for
455                 // cleanup.
456                 if (LAUNCH_ORDER_CYCLIC.equalsIgnoreCase(mLaunchOrder) && mCycleCleanUp
457                         && launch.getApp().equalsIgnoreCase(mLastAppName)) {
458                     // Kill all the apps and drop all the cache
459                     cleanUpAfterCycle();
460                     if (mIterationCycle) {
461                         // Save the previous cycle time and reset the cycle time to 0
462                         mCycleTimes.append(String.format("%d,", mCycleTime));
463                         mCycleTime = 0;
464                     }
465                 }
466             }
467         } finally {
468             if (null != mBufferedWriter) {
469                 mBufferedWriter.close();
470             }
471         }
472 
473         if (mCycleTimes.length() != 0) {
474                 mResult.putString("Cycle_Times", mCycleTimes.toString());
475         }
476         for (String app : mNameToResultKey.keySet()) {
477             for (String compilerFilter : mCompilerFilters) {
478                 StringBuilder launchTimes = new StringBuilder();
479                 StringBuilder cpuCycles = new StringBuilder();
480                 StringBuilder majorFaults = new StringBuilder();
481                 for (AppLaunchResult result : mNameToLaunchTime.get(app).get(compilerFilter)) {
482                     launchTimes.append(result.mLaunchTime);
483                     launchTimes.append(",");
484                     if (mSimplePerfAppOnly) {
485                         cpuCycles.append(result.mCpuCycles);
486                         cpuCycles.append(",");
487                         majorFaults.append(result.mMajorFaults);
488                         majorFaults.append(",");
489                     }
490                 }
491                 String filterName = (compilerFilter == null) ? "" : ("-" + compilerFilter);
492                 mResult.putString(mNameToResultKey.get(app) + filterName, launchTimes.toString());
493                 if (mSimplePerfAppOnly) {
494                     mResult.putString(mNameToResultKey.get(app) + filterName + "-cpuCycles",
495                         cpuCycles.toString());
496                     mResult.putString(mNameToResultKey.get(app) + filterName + "-majorFaults",
497                         majorFaults.toString());
498                 }
499             }
500         }
501         instrumentation.sendStatus(0, mResult);
502     }
503 
504     /**
505      * Compile the app package using compilerFilter and return true or false
506      * based on status of the compilation command.
507      */
compileApp(String compilerFilter, String appPkgName)508     private boolean compileApp(String compilerFilter, String appPkgName) throws IOException {
509         try (ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
510                 executeShellCommand(String.format(COMPILE_CMD, compilerFilter, appPkgName));
511                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
512                         new FileInputStream(result.getFileDescriptor())))) {
513             String line;
514             while ((line = bufferedReader.readLine()) != null) {
515                 if (line.contains(COMPILE_SUCCESS)) {
516                     return true;
517                 }
518             }
519             return false;
520         }
521     }
522 
523     /**
524      * Compile the app package using compilerFilter,
525      * retrying if the compilation command fails in between.
526      */
compileAppForIorapWithRetries(String appPkgName, int retries)527     private void compileAppForIorapWithRetries(String appPkgName, int retries) throws IOException {
528         for (int i = 0; i < retries; ++i) {
529             if (compileAppForIorap(appPkgName)) {
530                 return;
531             }
532             sleep(1000);
533         }
534 
535         throw new IllegalStateException("compileAppForIorapWithRetries: timed out after "
536                 + retries + " retries");
537     }
538 
539     /**
540      * Compile the app package using compilerFilter and return true or false
541      * based on status of the compilation command.
542      */
compileAppForIorap(String appPkgName)543     private boolean compileAppForIorap(String appPkgName) throws IOException {
544         String logcatTimestamp = getTimeNowForLogcat();
545 
546         getInstrumentation().getUiAutomation().
547                 executeShellCommand(String.format(IORAP_COMPILE_CMD, appPkgName));
548 
549         int i = 0;
550         for (i = 0; i < IORAP_COMPILE_CMD_TIMEOUT; ++i) {
551             IorapCompilationStatus status = waitForIorapCompiled(appPkgName);
552             if (status == IorapCompilationStatus.COMPLETE) {
553                 Log.v(TAG, "compileAppForIorap: success");
554                 logDumpsysIorapd(appPkgName);
555                 break;
556             } else if (status == IorapCompilationStatus.INSUFFICIENT_TRACES) {
557                 Log.e(TAG, "compileAppForIorap: failed due to insufficient traces");
558                 logDumpsysIorapd(appPkgName);
559                 throw new IllegalStateException(
560                         "compileAppForIorap: failed due to insufficient traces");
561             } // else INCOMPLETE. keep asking iorapd if it's done yet.
562             sleep(1000);
563         }
564 
565         if (i == IORAP_COMPILE_CMD_TIMEOUT) {
566             Log.e(TAG, "compileAppForIorap: failed due to timeout");
567             logDumpsysIorapd(appPkgName);
568             return false;
569         }
570 
571         return true;
572     }
573 
574     /** Save the contents of $(adb shell dumpsys iorapd) to the launch_logs directory. */
logDumpsysIorapd(String packageName)575     private void logDumpsysIorapd(String packageName) throws IOException {
576         InstrumentationTestRunner instrumentation =
577                 (InstrumentationTestRunner)getInstrumentation();
578         Bundle args = instrumentation.getArguments();
579 
580         String launchDirectory = args.getString(KEY_LAUNCH_DIRECTORY);
581 
582         // Root directory for applaunch file to log the app launch output
583         // Will be useful in case of simpleperf command is used
584         File launchRootDir = null;
585         if (null != launchDirectory && !launchDirectory.isEmpty()) {
586             launchRootDir = new File(launchDirectory);
587             if (!launchRootDir.exists() && !launchRootDir.mkdirs()) {
588                 throw new IOException("Unable to create the destination directory "
589                     + launchRootDir + ". Try disabling selinux.");
590             }
591         } else {
592             Log.w(TAG, "logDumpsysIorapd: Missing launch-directory arg");
593             return;
594         }
595 
596         File launchSubDir = new File(launchRootDir, LAUNCH_SUB_DIRECTORY);
597 
598         if (!launchSubDir.exists() && !launchSubDir.mkdirs()) {
599             throw new IOException("Unable to create the lauch file sub directory "
600                 + launchSubDir + ". Try disabling selinux.");
601         }
602         String path = "iorapd_dumpsys_" + packageName + "_" + System.nanoTime() + ".txt";
603         File file = new File(launchSubDir, path);
604         try (FileOutputStream outputStream = new FileOutputStream(file);
605                 BufferedWriter writer = new BufferedWriter(
606                         new OutputStreamWriter(outputStream));
607                 ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
608                         executeShellCommand(IORAP_DUMPSYS_CMD);
609                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
610                         new FileInputStream(result.getFileDescriptor())))) {
611             String line;
612             while ((line = bufferedReader.readLine()) != null) {
613                 writer.write(line + "\n");
614             }
615         }
616 
617         Log.v(TAG, "logDumpsysIorapd: Saved to file: " + path);
618     }
619 
620     enum IorapCompilationStatus {
621         INCOMPLETE,
622         COMPLETE,
623         INSUFFICIENT_TRACES,
624     }
waitForIorapCompiled(String appPkgName)625     private IorapCompilationStatus waitForIorapCompiled(String appPkgName) throws IOException {
626         try (ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
627                 executeShellCommand(IORAP_DUMPSYS_CMD);
628                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
629                         new FileInputStream(result.getFileDescriptor())))) {
630             String line;
631             String prevLine = "";
632             while ((line = bufferedReader.readLine()) != null) {
633                 // Match the indented VersionedComponentName string.
634                 // "  com.google.android.deskclock/com.android.deskclock.DeskClock@62000712"
635                 // Note: spaces are meaningful here.
636                 if (prevLine.contains("  " + appPkgName) && prevLine.contains("@")) {
637                     // pre-requisite:
638                     // Compiled Status: Raw traces pending compilation (3)
639                     if (line.contains("Compiled Status: Usable compiled trace")) {
640                         return IorapCompilationStatus.COMPLETE;
641                     } else if (line.contains("Compiled Status: ") &&
642                             line.contains("more traces for compilation")) {
643                         //      Compiled Status: Need 1 more traces for compilation
644                         // No amount of waiting will help here because there were
645                         // insufficient traces made.
646                         return IorapCompilationStatus.INSUFFICIENT_TRACES;
647                     }
648                 }
649 
650                 prevLine = line;
651             }
652             return IorapCompilationStatus.INCOMPLETE;
653         }
654     }
655 
makeReasonForIorapTrialLaunch(int launchCount)656     private String makeReasonForIorapTrialLaunch(int launchCount) {
657         String reason = IORAP_TRIAL_LAUNCH;
658         if (launchCount == 0) {
659             reason = IORAP_TRIAL_LAUNCH_FIRST;
660         }
661         if (launchCount == IORAP_TRIAL_LAUNCH_ITERATIONS - 1) {
662             reason = IORAP_TRIAL_LAUNCH_LAST;
663         }
664         return reason;
665     }
666 
shouldIncludeIorap(String compilerFilter)667     private boolean shouldIncludeIorap(String compilerFilter) {
668         if (!mIorapTrialLaunch) {
669             return false;
670         }
671 
672         // No iorap compiler filters specified: treat all compiler filters as ok.
673         if (mIorapCompilerFilters == null) {
674             return true;
675         }
676 
677         // iorap compiler filters specified: the compilerFilter must be in the whitelist.
678         if (mIorapCompilerFilters.indexOf(compilerFilter) != -1) {
679             return true;
680         }
681 
682         return false;
683     }
684 
685     /**
686      * If launch order is "cyclic" then apps will be launched one after the
687      * other for each iteration count.
688      * If launch order is "sequential" then each app will be launched for given number
689      * iterations at once before launching the other apps.
690      */
setLaunchOrder()691     private void setLaunchOrder() {
692         if (LAUNCH_ORDER_CYCLIC.equalsIgnoreCase(mLaunchOrder)) {
693             for (String compilerFilter : mCompilerFilters) {
694                 if (mTrialLaunch) {
695                     for (String app : mNameToResultKey.keySet()) {
696                         mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, TRIAL_LAUNCH, /*iorapEnabled*/false));
697                     }
698                 }
699                 if (shouldIncludeIorap(compilerFilter)) {
700                     for (int launchCount = 0; launchCount < IORAP_TRIAL_LAUNCH_ITERATIONS; ++launchCount) {
701                         for (String app : mNameToResultKey.keySet()) {
702                             String reason = makeReasonForIorapTrialLaunch(launchCount);
703                             mLaunchOrderList.add(
704                                     new LaunchOrder(app, compilerFilter,
705                                             reason,
706                                             /*iorapEnabled*/true));
707                         }
708                     }
709                 }
710                 for (int launchCount = 0; launchCount < mLaunchIterations; launchCount++) {
711                     for (String app : mNameToResultKey.keySet()) {
712                         mLaunchOrderList.add(new LaunchOrder(app, compilerFilter,
713                                   String.format(LAUNCH_ITERATION, launchCount),
714                                         shouldIncludeIorap(compilerFilter)));
715                     }
716                 }
717                 if (mTraceDirectoryStr != null && !mTraceDirectoryStr.isEmpty()) {
718                     for (int traceCount = 0; traceCount < mTraceLaunchCount; traceCount++) {
719                         for (String app : mNameToResultKey.keySet()) {
720                             mLaunchOrderList.add(new LaunchOrder(app, compilerFilter,
721                                       String.format(TRACE_ITERATION, traceCount),
722                                             shouldIncludeIorap(compilerFilter)));
723                         }
724                     }
725                 }
726             }
727         } else if (LAUNCH_ORDER_SEQUENTIAL.equalsIgnoreCase(mLaunchOrder)) {
728             for (String compilerFilter : mCompilerFilters) {
729                 for (String app : mNameToResultKey.keySet()) {
730                     if (mTrialLaunch) {
731                         mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, TRIAL_LAUNCH, /*iorapEnabled*/false));
732                     }
733                     if (shouldIncludeIorap(compilerFilter)) {
734                         for (int launchCount = 0; launchCount < IORAP_TRIAL_LAUNCH_ITERATIONS; ++launchCount) {
735                             String reason = makeReasonForIorapTrialLaunch(launchCount);
736                             mLaunchOrderList.add(
737                                     new LaunchOrder(app, compilerFilter,
738                                             reason,
739                                             /*iorapEnabled*/true));
740                         }
741                     }
742                     for (int launchCount = 0; launchCount < mLaunchIterations; launchCount++) {
743                         mLaunchOrderList.add(new LaunchOrder(app, compilerFilter,
744                                 String.format(LAUNCH_ITERATION, launchCount),
745                                         shouldIncludeIorap(compilerFilter)));
746                     }
747                     if (mTraceDirectoryStr != null && !mTraceDirectoryStr.isEmpty()) {
748                         for (int traceCount = 0; traceCount < mTraceLaunchCount; traceCount++) {
749                             mLaunchOrderList.add(new LaunchOrder(app, compilerFilter,
750                                     String.format(TRACE_ITERATION, traceCount),
751                                             shouldIncludeIorap(compilerFilter)));
752                         }
753                     }
754                 }
755             }
756         } else {
757             assertTrue("Launch order is not valid parameter", false);
758         }
759     }
760 
dropCache(boolean override)761     private void dropCache(boolean override) {
762         if (mDropCache || override) {
763             assertNotNull("Issue in dropping the cache",
764                     getInstrumentation().getUiAutomation()
765                             .executeShellCommand(DROP_CACHE_SCRIPT));
766         }
767     }
768 
769     // [[ $(adb shell whoami) == "root" ]]
checkIfRoot()770     private boolean checkIfRoot() throws IOException {
771         String total = "";
772         try (ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
773                 executeShellCommand("whoami");
774                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
775                         new FileInputStream(result.getFileDescriptor())))) {
776             String line;
777             while ((line = bufferedReader.readLine()) != null) {
778                 total = total + line;
779             }
780         }
781         return total.contains("root");
782     }
783 
stopIorapd()784     private void stopIorapd() {
785         getInstrumentation().getUiAutomation()
786                 .executeShellCommand("stop iorapd");
787         sleep(100);  // give it extra time to fully stop.
788     }
789 
startIorapd()790     private void startIorapd() {
791         String logcatTimeNow = getTimeNowForLogcat();
792         Log.v(TAG, "startIorapd, logcat time: " + logcatTimeNow);
793 
794         getInstrumentation().getUiAutomation()
795                 .executeShellCommand("start iorapd");
796 
797         int maxAttempts = 100;
798         int attempt = 0;
799         do {
800             // Ensure that IorapForwardingService fully reconnects to iorapd before proceeding.
801             String needle = "Connected to iorapd native service";
802             String logcatLines = getLogcatSinceTime(logcatTimeNow);
803 
804             if (logcatLines.contains(needle)) {
805                 break;
806             }
807 
808             sleep(1000);
809             attempt++;
810         } while (attempt < maxAttempts);
811 
812         if (attempt == maxAttempts) {
813             Log.e(TAG, "Timed out after waiting for iorapd to start");
814         }
815         // Wait a little bit longer for iorapd to settle.
816         sleep(1000);
817     }
818 
819     // Delete all db rows and files associated with a package in iorapd.
820     // Effectively deletes any raw or compiled trace files, unoptimizing the package in iorap.
purgeIorapPackage(String packageName)821     private void purgeIorapPackage(String packageName) {
822         try {
823             if (!checkIfRoot()) {
824                 throw new AssertionError("must be root to toggle iorapd; try adb root?");
825             }
826         } catch (IOException e) {
827             throw new AssertionError(e);
828         }
829 
830         Log.v(TAG, "Purge iorap package: " + packageName);
831         getInstrumentation().getUiAutomation()
832                 .executeShellCommand(String.format(IORAP_MAINTENANCE_CMD, packageName));
833         Log.v(TAG, "Executed: " + String.format(IORAP_MAINTENANCE_CMD, packageName));
834     }
835 
executeShellCommandWithTempFile(String cmd)836     String executeShellCommandWithTempFile(String cmd) {
837         Log.v(TAG, "executeShellCommandWithTempFile, cmd: " + cmd);
838         try {
839             //File outputDir =
840             //       InstrumentationRegistry.getInstrumentation().getContext().getCacheDir();
841             File outputFile = File.createTempFile("exec_shell_command", ".sh");
842 
843             try {
844                 outputFile.setWritable(true);
845                 outputFile.setExecutable(true, /*ownersOnly*/false);
846 
847                 String scriptPath = outputFile.toString();
848 
849                 // If this works correctly, the next log-line will print 'Success'.
850                 try (BufferedWriter writer = new BufferedWriter(new FileWriter(scriptPath))) {
851                     writer.write(cmd);
852                 }
853 
854                 String resultString = "";
855                 try (ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
856                         executeShellCommand(scriptPath);
857                         BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
858                                 new FileInputStream(result.getFileDescriptor())))) {
859                     String line;
860                     while ((line = bufferedReader.readLine()) != null) {
861                         resultString += line + "\n";
862                     }
863                 }
864 
865                 return resultString;
866             } finally {
867                 outputFile.delete();
868             }
869         } catch (IOException e) {
870             throw new AssertionError("Failed to execute shell command: " + cmd, e);
871         }
872     }
873 
874     // Get the 'now' timestamp usable with $(adb logcat -v utc -T "time string")
getTimeNowForLogcat()875     String getTimeNowForLogcat() {
876         ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC);
877 
878         // YYYY-MM-DD hh:mm:ss.mmm
879         return utc.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
880     }
881 
getLogcatSinceTime(String logcatTime)882     String getLogcatSinceTime(String logcatTime) {
883         // The time has spaces in it but must be passed as a single arg.
884         // Therefore use a temp script file.
885         return executeShellCommandWithTempFile(
886                 String.format("logcat -d -v threadtime -v utc -T '%s'", logcatTime));
887     }
888 
889     /**
890      * Toggle iorapd-based readahead and trace-collection.
891      * If iorapd is already enabled and enable is true, does nothing.
892      * If iorapd is already disabled and enable is false, does nothing.
893      */
toggleIorapStatus(boolean enable)894     private void toggleIorapStatus(boolean enable) {
895         boolean currentlyEnabled = false;
896         Log.v(TAG, "toggleIorapStatus " + Boolean.toString(enable));
897 
898         // Do nothing if we are already enabled or disabled.
899         if (mIorapStatus == IorapStatus.ENABLED && enable) {
900             return;
901         } else if (mIorapStatus == IorapStatus.DISABLED && !enable) {
902             return;
903         }
904 
905         try {
906             if (!checkIfRoot()) {
907                 throw new AssertionError("must be root to toggle iorapd; try adb root?");
908             }
909         } catch (IOException e) {
910             throw new AssertionError(e);
911         }
912 
913         getInstrumentation().getUiAutomation()
914                 .executeShellCommand(String.format("setprop iorapd.perfetto.enable %b", enable));
915         getInstrumentation().getUiAutomation()
916                 .executeShellCommand(String.format("setprop iorapd.readahead.enable %b", enable));
917         getInstrumentation().getUiAutomation()
918                 .executeShellCommand(String.format(
919                         "setprop iorapd.maintenance.min_traces %d", IORAP_COMPILE_MIN_TRACES));
920         // this last command blocks until iorapd refreshes its system properties
921         getInstrumentation().getUiAutomation()
922                 .executeShellCommand(String.format("dumpsys iorapd --refresh-properties"));
923 
924         if (enable) {
925             mIorapStatus = IorapStatus.ENABLED;
926         } else {
927             mIorapStatus = IorapStatus.DISABLED;
928         }
929     }
930 
parseArgs(Bundle args)931     private void parseArgs(Bundle args) {
932         mNameToResultKey = new LinkedHashMap<String, String>();
933         mNameToLaunchTime = new HashMap<>();
934         String launchIterations = args.getString(KEY_LAUNCH_ITERATIONS);
935         if (launchIterations != null) {
936             mLaunchIterations = Integer.parseInt(launchIterations);
937         }
938         String forceStopApp = args.getString(KEY_FORCE_STOP_APP);
939 
940         if (forceStopApp != null) {
941             mForceStopApp = Boolean.parseBoolean(forceStopApp);
942         }
943 
944         String enableRecording = args.getString(ENABLE_SCREEN_RECORDING);
945 
946         if (enableRecording != null) {
947             mEnableRecording = Boolean.parseBoolean(enableRecording);
948         }
949         String appList = args.getString(KEY_APPS);
950         if (appList == null)
951             return;
952 
953         String appNames[] = appList.split("\\|");
954         for (String pair : appNames) {
955             String[] parts = pair.split("\\^");
956             if (parts.length != 2) {
957                 Log.e(TAG, "The apps key is incorrectly formatted");
958                 fail();
959             }
960 
961             mNameToResultKey.put(parts[0], parts[1]);
962             mNameToLaunchTime.put(parts[0], null);
963             mLastAppName = parts[0];
964         }
965         String requiredAccounts = args.getString(KEY_REQUIRED_ACCOUNTS);
966         if (requiredAccounts != null) {
967             mRequiredAccounts = new HashSet<String>();
968             for (String accountType : requiredAccounts.split(",")) {
969                 mRequiredAccounts.add(accountType);
970             }
971         }
972 
973         String compilerFilterList = args.getString(KEY_COMPILER_FILTERS);
974         if (compilerFilterList != null) {
975             // If a compiler filter is passed, we make a trial launch to force compilation
976             // of the apps.
977             mTrialLaunch = true;
978             mCompilerFilters = compilerFilterList.split("\\|");
979         } else {
980             // Just pass a null compiler filter to use the current state of the app.
981             mCompilerFilters = new String[1];
982         }
983 
984         String iorapCompilerFilterList = args.getString(KEY_IORAP_COMPILER_FILTERS);
985         if (iorapCompilerFilterList != null) {
986             // Passing in iorap compiler filters implies an iorap trial launch.
987             mIorapTrialLaunch = true;
988             mIorapCompilerFilters = Arrays.asList(iorapCompilerFilterList.split("\\|"));
989         }
990 
991         // Pre-populate the results map to avoid null checks.
992         for (String app : mNameToLaunchTime.keySet()) {
993             HashMap<String, List<AppLaunchResult>> map = new HashMap<>();
994             mNameToLaunchTime.put(app, map);
995             for (String compilerFilter : mCompilerFilters) {
996                 map.put(compilerFilter, new ArrayList<>());
997             }
998         }
999 
1000         mTraceDirectoryStr = args.getString(KEY_TRACE_DIRECTORY);
1001         mDropCache = Boolean.parseBoolean(args.getString(KEY_DROP_CACHE));
1002         mSimplePerfCmd = args.getString(KEY_SIMPLEPERF_CMD);
1003         mLaunchOrder = args.getString(KEY_LAUNCH_ORDER, LAUNCH_ORDER_CYCLIC);
1004         mSimplePerfAppOnly = Boolean.parseBoolean(args.getString(KEY_SIMPLEPERF_APP));
1005         mCycleCleanUp = Boolean.parseBoolean(args.getString(KEY_CYCLE_CLEAN));
1006         mTraceAll = Boolean.parseBoolean(args.getString(KEY_TRACE_ALL));
1007         mTrialLaunch = mTrialLaunch || Boolean.parseBoolean(args.getString(KEY_TRIAL_LAUNCH));
1008         mIorapTrialLaunch = mIorapTrialLaunch ||
1009                 Boolean.parseBoolean(args.getString(KEY_IORAP_TRIAL_LAUNCH));
1010 
1011         if (mSimplePerfCmd != null && mSimplePerfAppOnly) {
1012             Log.w(TAG, String.format("Passing both %s and %s is not supported, ignoring %s",
1013                 KEY_SIMPLEPERF_CMD, KEY_SIMPLEPERF_APP, KEY_SIMPLEPERF_CMD));
1014         }
1015     }
1016 
hasLeanback(Context context)1017     private boolean hasLeanback(Context context) {
1018         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
1019     }
1020 
createMappings()1021     private void createMappings() {
1022         mNameToIntent = new LinkedHashMap<String, Intent>();
1023 
1024         PackageManager pm = getInstrumentation().getContext()
1025                 .getPackageManager();
1026         Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
1027         intentToResolve.addCategory(hasLeanback(getInstrumentation().getContext()) ?
1028                 Intent.CATEGORY_LEANBACK_LAUNCHER :
1029                 Intent.CATEGORY_LAUNCHER);
1030         List<ResolveInfo> ris = pm.queryIntentActivities(intentToResolve, 0);
1031         resolveLoop(ris, intentToResolve, pm);
1032         // For Wear
1033         intentToResolve = new Intent(WEARABLE_ACTION_GOOGLE);
1034         ris = pm.queryIntentActivities(intentToResolve, 0);
1035         resolveLoop(ris, intentToResolve, pm);
1036     }
1037 
resolveLoop(List<ResolveInfo> ris, Intent intentToResolve, PackageManager pm)1038     private void resolveLoop(List<ResolveInfo> ris, Intent intentToResolve, PackageManager pm) {
1039         if (ris == null || ris.isEmpty()) {
1040             Log.i(TAG, "Could not find any apps");
1041         } else {
1042             for (ResolveInfo ri : ris) {
1043                 Intent startIntent = new Intent(intentToResolve);
1044                 startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
1045                         | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1046                 startIntent.setClassName(ri.activityInfo.packageName,
1047                         ri.activityInfo.name);
1048                 String appName = ri.loadLabel(pm).toString();
1049                 if (appName != null) {
1050                     // Support launching intent using package name or app name
1051                     mNameToIntent.put(ri.activityInfo.packageName, startIntent);
1052                     mNameToIntent.put(appName, startIntent);
1053                 }
1054             }
1055         }
1056     }
1057 
startApp(String appName, String launchReason)1058     private AppLaunchResult startApp(String appName, String launchReason)
1059             throws NameNotFoundException, RemoteException {
1060         Log.i(TAG, "Starting " + appName);
1061         if(mEnableRecording) {
1062             startRecording(appName, launchReason);
1063         }
1064 
1065         Intent startIntent = mNameToIntent.get(appName);
1066         if (startIntent == null) {
1067             Log.w(TAG, "App does not exist: " + appName);
1068             mResult.putString(mNameToResultKey.get(appName), "App does not exist");
1069             return new AppLaunchResult();
1070         }
1071         AppLaunchRunnable runnable = new AppLaunchRunnable(startIntent, launchReason);
1072         Thread t = new Thread(runnable);
1073         t.start();
1074         try {
1075             t.join(JOIN_TIMEOUT);
1076         } catch (InterruptedException e) {
1077             // ignore
1078         }
1079 
1080         if(mEnableRecording) {
1081             stopRecording();
1082         }
1083         return runnable.getResult();
1084     }
1085 
checkAccountSignIn()1086     private void checkAccountSignIn() {
1087         // ensure that the device has the required account types before starting test
1088         // e.g. device must have a valid Google account sign in to measure a meaningful launch time
1089         // for Gmail
1090         if (mRequiredAccounts == null || mRequiredAccounts.isEmpty()) {
1091             return;
1092         }
1093         final AccountManager am =
1094                 (AccountManager) getInstrumentation().getTargetContext().getSystemService(
1095                         Context.ACCOUNT_SERVICE);
1096         Account[] accounts = am.getAccounts();
1097         // use set here in case device has multiple accounts of the same type
1098         Set<String> foundAccounts = new HashSet<String>();
1099         for (Account account : accounts) {
1100             if (mRequiredAccounts.contains(account.type)) {
1101                 foundAccounts.add(account.type);
1102             }
1103         }
1104         // check if account type matches, if not, fail test with message on what account types
1105         // are missing
1106         if (mRequiredAccounts.size() != foundAccounts.size()) {
1107             mRequiredAccounts.removeAll(foundAccounts);
1108             StringBuilder sb = new StringBuilder("Device missing these accounts:");
1109             for (String account : mRequiredAccounts) {
1110                 sb.append(' ');
1111                 sb.append(account);
1112             }
1113             fail(sb.toString());
1114         }
1115     }
1116 
startHomeIntent()1117     private void startHomeIntent() {
1118         Intent homeIntent = new Intent(Intent.ACTION_MAIN);
1119         homeIntent.addCategory(Intent.CATEGORY_HOME);
1120         homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
1121                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1122         getInstrumentation().getContext().startActivity(homeIntent);
1123         sleep(POST_LAUNCH_IDLE_TIMEOUT);
1124     }
1125 
cleanUpAfterCycle()1126     private void cleanUpAfterCycle() {
1127         // Kill all the apps
1128         for (String appName : mNameToIntent.keySet()) {
1129             Log.w(TAG, String.format("killing %s", appName));
1130             forceStopApp(appName);
1131         }
1132         // Drop all the cache.
1133         assertNotNull("Issue in dropping the cache",
1134                 getInstrumentation().getUiAutomation()
1135                         .executeShellCommand(DROP_CACHE_SCRIPT));
1136     }
1137 
forceStopApp(String appName)1138     private void forceStopApp(String appName) {
1139         Intent startIntent = mNameToIntent.get(appName);
1140         if (startIntent != null) {
1141             String packageName = startIntent.getComponent().getPackageName();
1142             try {
1143                 mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT);
1144             } catch (RemoteException e) {
1145                 Log.w(TAG, "Error closing app", e);
1146             }
1147         }
1148     }
1149 
killBackgroundApp(String appName)1150     private void killBackgroundApp(String appName) {
1151         Intent startIntent = mNameToIntent.get(appName);
1152         if (startIntent != null) {
1153             String packageName = startIntent.getComponent().getPackageName();
1154             try {
1155                 mAm.killBackgroundProcesses(packageName, UserHandle.USER_CURRENT);
1156             } catch (RemoteException e) {
1157                 Log.w(TAG, "Error closing app", e);
1158             }
1159         }
1160     }
1161 
sleep(int time)1162     private void sleep(int time) {
1163         try {
1164             Thread.sleep(time);
1165         } catch (InterruptedException e) {
1166             // ignore
1167         }
1168     }
1169 
reportError(String appName, String processName)1170     private void reportError(String appName, String processName) {
1171         ActivityManager am = (ActivityManager) getInstrumentation()
1172                 .getContext().getSystemService(Context.ACTIVITY_SERVICE);
1173         List<ProcessErrorStateInfo> crashes = am.getProcessesInErrorState();
1174         if (crashes != null) {
1175             for (ProcessErrorStateInfo crash : crashes) {
1176                 if (!crash.processName.equals(processName))
1177                     continue;
1178 
1179                 Log.w(TAG, appName + " crashed: " + crash.shortMsg);
1180                 mResult.putString(mNameToResultKey.get(appName), crash.shortMsg);
1181                 return;
1182             }
1183         }
1184 
1185         mResult.putString(mNameToResultKey.get(appName),
1186                 "Crashed for unknown reason");
1187         Log.w(TAG, appName
1188                 + " not found in process list, most likely it is crashed");
1189     }
1190 
1191     private class LaunchOrder {
1192         private String mApp;
1193         private String mCompilerFilter;
1194         private String mLaunchReason;
1195         private boolean mIorapEnabled;
1196 
LaunchOrder(String app, String compilerFilter, String launchReason, boolean iorapEnabled)1197         LaunchOrder(String app, String compilerFilter, String launchReason, boolean iorapEnabled) {
1198             mApp = app;
1199             mCompilerFilter = compilerFilter;
1200             mLaunchReason = launchReason;
1201             mIorapEnabled = iorapEnabled;
1202         }
1203 
getApp()1204         public String getApp() {
1205             return mApp;
1206         }
1207 
setApp(String app)1208         public void setApp(String app) {
1209             mApp = app;
1210         }
1211 
getCompilerFilter()1212         public String getCompilerFilter() {
1213             return mCompilerFilter;
1214         }
1215 
getLaunchReason()1216         public String getLaunchReason() {
1217             return mLaunchReason;
1218         }
1219 
setLaunchReason(String launchReason)1220         public void setLaunchReason(String launchReason) {
1221             mLaunchReason = launchReason;
1222         }
1223 
setIorapEnabled(boolean iorapEnabled)1224         public void setIorapEnabled(boolean iorapEnabled) {
1225             mIorapEnabled = iorapEnabled;
1226         }
1227 
getIorapEnabled()1228         public boolean getIorapEnabled() {
1229             return mIorapEnabled;
1230         }
1231     }
1232 
1233     private class AppLaunchResult {
1234         long mLaunchTime;
1235         long mCpuCycles;
1236         long mMajorFaults;
1237 
AppLaunchResult()1238         AppLaunchResult() {
1239             mLaunchTime = -1L;
1240             mCpuCycles = -1L;
1241             mMajorFaults = -1L;
1242         }
1243 
AppLaunchResult(String launchTime, String cpuCycles, String majorFaults)1244         AppLaunchResult(String launchTime, String cpuCycles, String majorFaults) {
1245             try {
1246                 mLaunchTime = Long.parseLong(launchTime, 10);
1247                 mCpuCycles = Long.parseLong(cpuCycles, 10);
1248                 mMajorFaults = Long.parseLong(majorFaults, 10);
1249             } catch (NumberFormatException e) {
1250                 Log.e(TAG, "Error parsing result", e);
1251             }
1252         }
1253     }
1254 
1255     private class AppLaunchRunnable implements Runnable {
1256         private Intent mLaunchIntent;
1257         private AppLaunchResult mLaunchResult;
1258         private String mLaunchReason;
1259 
AppLaunchRunnable(Intent intent, String launchReason)1260         public AppLaunchRunnable(Intent intent, String launchReason) {
1261             mLaunchIntent = intent;
1262             mLaunchReason = launchReason;
1263             mLaunchResult = new AppLaunchResult();
1264         }
1265 
getResult()1266         public AppLaunchResult getResult() {
1267             return mLaunchResult;
1268         }
1269 
run()1270         public void run() {
1271             File launchFile = null;
1272             try {
1273                 String packageName = mLaunchIntent.getComponent().getPackageName();
1274                 String componentName = mLaunchIntent.getComponent().flattenToShortString();
1275                 if (mForceStopApp) {
1276                     mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT);
1277                 }
1278                 String launchCmd = String.format("%s %s", APP_LAUNCH_CMD, componentName);
1279                 if (mSimplePerfAppOnly) {
1280                     try {
1281                         // executeShellCommand cannot handle shell specific actions, like '&'.
1282                         // Therefore, we create a file containing the command and make that
1283                         // the command to launch.
1284                         launchFile = File.createTempFile(LAUNCH_SCRIPT_NAME, ".sh");
1285                         launchFile.setExecutable(true);
1286                         try (FileOutputStream stream = new FileOutputStream(launchFile);
1287                              BufferedWriter writer =
1288                                 new BufferedWriter(new OutputStreamWriter(stream))) {
1289                             String cmd = String.format(SIMPLEPERF_APP_CMD, packageName, launchCmd);
1290                             // In the file, we need to escape any "$".
1291                             cmd = cmd.replace("$", "\\$");
1292                             writer.write(cmd);
1293                         }
1294                         launchCmd = launchFile.getAbsolutePath();
1295                     } catch (IOException e) {
1296                         Log.w(TAG, "Error writing the launch command", e);
1297                         return;
1298                     }
1299                 } else if (null != mSimplePerfCmd) {
1300                     launchCmd = String.format("%s %s", mSimplePerfCmd, launchCmd);
1301                 }
1302                 Log.v(TAG, "Final launch cmd:" + launchCmd);
1303                 ParcelFileDescriptor parcelDesc = getInstrumentation().getUiAutomation()
1304                         .executeShellCommand(launchCmd);
1305                 mLaunchResult = parseLaunchTimeAndWrite(parcelDesc, String.format
1306                         ("App Launch :%s %s", componentName, mLaunchReason));
1307             } catch (RemoteException e) {
1308                 Log.w(TAG, "Error launching app", e);
1309             } finally {
1310                 if (launchFile != null) {
1311                     launchFile.delete();
1312                 }
1313             }
1314         }
1315 
1316         /**
1317          * Method to parse the launch time info and write the result to file
1318          *
1319          * @param parcelDesc
1320          * @return
1321          */
parseLaunchTimeAndWrite(ParcelFileDescriptor parcelDesc, String headerInfo)1322         private AppLaunchResult parseLaunchTimeAndWrite(ParcelFileDescriptor parcelDesc,
1323                 String headerInfo) {
1324             String launchTime = "-1";
1325             String cpuCycles = "-1";
1326             String majorFaults = "-1";
1327             boolean launchSuccess = false;
1328             try {
1329                 InputStream inputStream = new FileInputStream(parcelDesc.getFileDescriptor());
1330                 /* SAMPLE OUTPUT : Cold launch
1331                 Starting: Intent { cmp=com.google.android.calculator/com.android.calculator2.Calculator }
1332                 Status: ok
1333                 LaunchState: COLD
1334                 Activity: com.google.android.calculator/com.android.calculator2.Calculator
1335                 TotalTime: 357
1336                 WaitTime: 377
1337                 Complete*/
1338                 /* SAMPLE OUTPUT : Hot launch
1339                 Starting: Intent { cmp=com.google.android.calculator/com.android.calculator2.Calculator }
1340                 Warning: Activity not started, its current task has been brought to the front
1341                 Status: ok
1342                 LaunchState: HOT
1343                 Activity: com.google.android.calculator/com.android.calculator2.CalculatorGoogle
1344                 TotalTime: 60
1345                 WaitTime: 67
1346                 Complete*/
1347                 /* WITH SIMPLEPERF :
1348                 Performance counter statistics,
1349                 6595722690,cpu-cycles,4.511040,GHz,(100%),
1350                 0,major-faults,0.000,/sec,(100%),
1351                 Total test time,1.462129,seconds,*/
1352                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
1353                         inputStream));
1354                 String line;
1355                 mBufferedWriter.newLine();
1356                 mBufferedWriter.write(headerInfo);
1357                 mBufferedWriter.newLine();
1358                 while ((line = bufferedReader.readLine()) != null) {
1359                     mBufferedWriter.write(line);
1360                     mBufferedWriter.newLine();
1361                     if (line.startsWith(SUCCESS_MESSAGE)) {
1362                         launchSuccess = true;
1363                     }
1364                     if (!launchSuccess) {
1365                         continue;
1366                     }
1367                     // Parse TotalTime which is the launch time
1368                     if (line.startsWith(TOTAL_TIME_MESSAGE)) {
1369                         String launchSplit[] = line.split(":");
1370                         launchTime = launchSplit[1].trim();
1371                     }
1372 
1373                     if (mSimplePerfAppOnly) {
1374                         if (line.contains(",cpu-cycles,")) {
1375                             cpuCycles = line.split(",")[0].trim();
1376                         } else if (line.contains(",major-faults,")) {
1377                             majorFaults = line.split(",")[0].trim();
1378                         }
1379                     }
1380                 }
1381                 mBufferedWriter.flush();
1382                 inputStream.close();
1383             } catch (IOException e) {
1384                 Log.w(TAG, "Error parsing launch time and writing to file", e);
1385             }
1386             return new AppLaunchResult(launchTime, cpuCycles, majorFaults);
1387         }
1388 
1389     }
1390 
1391     /**
1392      * Start the screen recording while launching the app.
1393      *
1394      * @param appName
1395      * @param launchReason
1396      */
startRecording(String appName, String launchReason)1397     private void startRecording(String appName, String launchReason) {
1398         Log.v(TAG, "Started Recording");
1399         mCurrentThread = new RecordingThread("test-screen-record",
1400                 String.format("%s_%s", appName, launchReason));
1401         mCurrentThread.start();
1402     }
1403 
1404     /**
1405      * Stop already started screen recording.
1406      */
stopRecording()1407     private void stopRecording() {
1408         // Skip if not directory.
1409         if (launchSubDir == null) {
1410             return;
1411         }
1412 
1413         // Add some extra time to the video end.
1414         SystemClock.sleep(VIDEO_TAIL_BUFFER);
1415         // Ctrl + C all screen record processes.
1416         mCurrentThread.cancel();
1417         // Wait for the thread to completely die.
1418         try {
1419             mCurrentThread.join();
1420         } catch (InterruptedException ex) {
1421             Log.e(TAG, "Interrupted when joining the recording thread.", ex);
1422         }
1423         Log.v(TAG, "Stopped Recording");
1424     }
1425 
1426     /** Returns the recording's name for part {@code part} of launch description. */
getOutputFile(String description, int part)1427     private File getOutputFile(String description, int part) {
1428         // Omit the iteration number for the first iteration.
1429         final String fileName =
1430                 String.format(
1431                         "%s-video%s.mp4", description, part == 1 ? "" : part);
1432         return Paths.get(launchSubDir.getAbsolutePath(), description).toFile();
1433     }
1434 
1435 
1436     /**
1437      * Encapsulates the start and stop screen recording logic.
1438      * Copied from ScreenRecordCollector.
1439      */
1440     private class RecordingThread extends Thread {
1441         private final String mDescription;
1442         private final List<File> mRecordings;
1443 
1444         private boolean mContinue;
1445 
RecordingThread(String name, String description)1446         public RecordingThread(String name, String description) {
1447             super(name);
1448 
1449             mContinue = true;
1450             mRecordings = new ArrayList<>();
1451 
1452             assertNotNull("No test description provided for recording.", description);
1453             mDescription = description;
1454         }
1455 
1456         @Override
run()1457         public void run() {
1458             try {
1459                 // Start at i = 1 to encode parts as X.mp4, X2.mp4, X3.mp4, etc.
1460                 for (int i = 1; i <= MAX_RECORDING_PARTS && mContinue; i++) {
1461                     File output = getOutputFile(mDescription, i);
1462                     Log.d(
1463                             TAG,
1464                             String.format("Recording screen to %s", output.getAbsolutePath()));
1465                     mRecordings.add(output);
1466                     // Make sure not to block on this background command in the main thread so
1467                     // that the test continues to run, but block in this thread so it does not
1468                     // trigger a new screen recording session before the prior one completes.
1469                     getDevice().executeShellCommand(
1470                                     String.format("screenrecord %s", output.getAbsolutePath()));
1471                 }
1472             } catch (IOException e) {
1473                 throw new RuntimeException("Caught exception while screen recording.");
1474             }
1475         }
1476 
cancel()1477         public void cancel() {
1478             mContinue = false;
1479 
1480             // Identify the screenrecord PIDs and send SIGINT 2 (Ctrl + C) to each.
1481             try {
1482                 String[] pids = getDevice().executeShellCommand(
1483                         "pidof screenrecord").split(" ");
1484                 for (String pid : pids) {
1485                     // Avoid empty process ids, because of weird splitting behavior.
1486                     if (pid.isEmpty()) {
1487                         continue;
1488                     }
1489 
1490                     getDevice().executeShellCommand(
1491                             String.format("kill -2 %s", pid));
1492                     Log.d(
1493                             TAG,
1494                             String.format("Sent SIGINT 2 to screenrecord process (%s)", pid));
1495                 }
1496             } catch (IOException e) {
1497                 throw new RuntimeException("Failed to kill screen recording process.");
1498             }
1499         }
1500 
getRecordings()1501         public List<File> getRecordings() {
1502             return mRecordings;
1503         }
1504     }
1505 
getDevice()1506     public UiDevice getDevice() {
1507         if (mDevice == null) {
1508             mDevice = UiDevice.getInstance(getInstrumentation());
1509         }
1510         return mDevice;
1511     }
1512 }
1513