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 = 3;  // min 3 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                         continue;
381                     }
382                     AtraceLogger atraceLogger = null;
383                     if (mTraceAll) {
384                         Log.i(TAG, "Started tracing " + launch.getApp());
385                         atraceLogger = AtraceLogger
386                                 .getAtraceLoggerInstance(getInstrumentation());
387                     }
388                     try {
389                         // Start the trace
390                         if (atraceLogger != null) {
391                             atraceLogger.atraceStart(traceCategoriesSet, traceBufferSize,
392                                     traceDumpInterval, rootTraceSubDir,
393                                     String.format("%s-%s-%s", launch.getApp(),
394                                             launch.getCompilerFilter(), launch.getLaunchReason()));
395                         }
396                         // In the "applaunch.txt" file app launches are referenced using
397                         // "LAUNCH_ITERATION - ITERATION NUM"
398                         launchResults = startApp(launch.getApp(), launch.getLaunchReason());
399                         if (launchResults.mLaunchTime < 0) {
400                             addLaunchResult(launch, new AppLaunchResult());
401                             // if it fails once, skip the rest of the launches
402                             continue;
403                         } else {
404                             mCycleTime += launchResults.mLaunchTime;
405                             addLaunchResult(launch, launchResults);
406                         }
407                         sleep(POST_LAUNCH_IDLE_TIMEOUT);
408                     } finally {
409                         // Stop the trace
410                         if (atraceLogger != null) {
411                             Log.i(TAG, "Stopped tracing " + launch.getApp());
412                             atraceLogger.atraceStop();
413                         }
414                     }
415 
416                 }
417 
418                 // App launch times for trace launch will not be used for final
419                 // launch time calculations.
420                 else if (launch.getLaunchReason().contains(TRACE_ITERATION_PREFIX)) {
421                     mIterationCycle = false;
422                     AtraceLogger atraceLogger = AtraceLogger
423                             .getAtraceLoggerInstance(getInstrumentation());
424                     // Start the trace
425                     try {
426                         atraceLogger.atraceStart(traceCategoriesSet, traceBufferSize,
427                                 traceDumpInterval, rootTraceSubDir,
428                                 String.format("%s-%s-%s", launch.getApp(),
429                                         launch.getCompilerFilter(), launch.getLaunchReason()));
430                         startApp(launch.getApp(), launch.getLaunchReason());
431                         sleep(POST_LAUNCH_IDLE_TIMEOUT);
432                     } finally {
433                         // Stop the trace
434                         atraceLogger.atraceStop();
435                     }
436                 }
437                 if(mForceStopApp) {
438                     sleep(BEFORE_FORCE_STOP_SLEEP_TIMEOUT);
439                     forceStopApp(launch.getApp());
440                     sleep(BEFORE_KILL_APP_SLEEP_TIMEOUT);
441                     // Close again for good measure (just in case).
442                     forceStopApp(launch.getApp());
443                     // Kill the backgrounded process in the case forceStopApp only sent it to
444                     // background.
445                     killBackgroundApp(launch.getApp());
446                 } else {
447                     startHomeIntent();
448                 }
449                 sleep(BETWEEN_LAUNCH_SLEEP_TIMEOUT);
450 
451                 // If cycle clean up is enabled and last app launched is
452                 // current app then the cycle is completed and eligible for
453                 // cleanup.
454                 if (LAUNCH_ORDER_CYCLIC.equalsIgnoreCase(mLaunchOrder) && mCycleCleanUp
455                         && launch.getApp().equalsIgnoreCase(mLastAppName)) {
456                     // Kill all the apps and drop all the cache
457                     cleanUpAfterCycle();
458                     if (mIterationCycle) {
459                         // Save the previous cycle time and reset the cycle time to 0
460                         mCycleTimes.append(String.format("%d,", mCycleTime));
461                         mCycleTime = 0;
462                     }
463                 }
464             }
465         } finally {
466             if (null != mBufferedWriter) {
467                 mBufferedWriter.close();
468             }
469         }
470 
471         if (mCycleTimes.length() != 0) {
472                 mResult.putString("Cycle_Times", mCycleTimes.toString());
473         }
474         for (String app : mNameToResultKey.keySet()) {
475             for (String compilerFilter : mCompilerFilters) {
476                 StringBuilder launchTimes = new StringBuilder();
477                 StringBuilder cpuCycles = new StringBuilder();
478                 StringBuilder majorFaults = new StringBuilder();
479                 for (AppLaunchResult result : mNameToLaunchTime.get(app).get(compilerFilter)) {
480                     launchTimes.append(result.mLaunchTime);
481                     launchTimes.append(",");
482                     if (mSimplePerfAppOnly) {
483                         cpuCycles.append(result.mCpuCycles);
484                         cpuCycles.append(",");
485                         majorFaults.append(result.mMajorFaults);
486                         majorFaults.append(",");
487                     }
488                 }
489                 String filterName = (compilerFilter == null) ? "" : ("-" + compilerFilter);
490                 mResult.putString(mNameToResultKey.get(app) + filterName, launchTimes.toString());
491                 if (mSimplePerfAppOnly) {
492                     mResult.putString(mNameToResultKey.get(app) + filterName + "-cpuCycles",
493                         cpuCycles.toString());
494                     mResult.putString(mNameToResultKey.get(app) + filterName + "-majorFaults",
495                         majorFaults.toString());
496                 }
497             }
498         }
499         instrumentation.sendStatus(0, mResult);
500     }
501 
502     /**
503      * Compile the app package using compilerFilter and return true or false
504      * based on status of the compilation command.
505      */
compileApp(String compilerFilter, String appPkgName)506     private boolean compileApp(String compilerFilter, String appPkgName) throws IOException {
507         try (ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
508                 executeShellCommand(String.format(COMPILE_CMD, compilerFilter, appPkgName));
509                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
510                         new FileInputStream(result.getFileDescriptor())))) {
511             String line;
512             while ((line = bufferedReader.readLine()) != null) {
513                 if (line.contains(COMPILE_SUCCESS)) {
514                     return true;
515                 }
516             }
517             return false;
518         }
519     }
520 
521     /**
522      * Compile the app package using compilerFilter,
523      * retrying if the compilation command fails in between.
524      */
compileAppForIorapWithRetries(String appPkgName, int retries)525     private void compileAppForIorapWithRetries(String appPkgName, int retries) throws IOException {
526         for (int i = 0; i < retries; ++i) {
527             if (compileAppForIorap(appPkgName)) {
528                 return;
529             }
530             sleep(1000);
531         }
532 
533         throw new IllegalStateException("compileAppForIorapWithRetries: timed out after "
534                 + retries + " retries");
535     }
536 
537     /**
538      * Compile the app package using compilerFilter and return true or false
539      * based on status of the compilation command.
540      */
compileAppForIorap(String appPkgName)541     private boolean compileAppForIorap(String appPkgName) throws IOException {
542         String logcatTimestamp = getTimeNowForLogcat();
543 
544         getInstrumentation().getUiAutomation().
545                 executeShellCommand(String.format(IORAP_COMPILE_CMD, appPkgName));
546 
547         int i = 0;
548         for (i = 0; i < IORAP_COMPILE_CMD_TIMEOUT; ++i) {
549             IorapCompilationStatus status = waitForIorapCompiled(appPkgName);
550             if (status == IorapCompilationStatus.COMPLETE) {
551                 Log.v(TAG, "compileAppForIorap: success");
552                 logDumpsysIorapd(appPkgName);
553                 break;
554             } else if (status == IorapCompilationStatus.INSUFFICIENT_TRACES) {
555                 Log.e(TAG, "compileAppForIorap: failed due to insufficient traces");
556                 logDumpsysIorapd(appPkgName);
557                 throw new IllegalStateException(
558                         "compileAppForIorap: failed due to insufficient traces");
559             } // else INCOMPLETE. keep asking iorapd if it's done yet.
560             sleep(1000);
561         }
562 
563         if (i == IORAP_COMPILE_CMD_TIMEOUT) {
564             Log.e(TAG, "compileAppForIorap: failed due to timeout");
565             logDumpsysIorapd(appPkgName);
566             return false;
567         }
568 
569         return true;
570     }
571 
572     /** Save the contents of $(adb shell dumpsys iorapd) to the launch_logs directory. */
logDumpsysIorapd(String packageName)573     private void logDumpsysIorapd(String packageName) throws IOException {
574         InstrumentationTestRunner instrumentation =
575                 (InstrumentationTestRunner)getInstrumentation();
576         Bundle args = instrumentation.getArguments();
577 
578         String launchDirectory = args.getString(KEY_LAUNCH_DIRECTORY);
579 
580         // Root directory for applaunch file to log the app launch output
581         // Will be useful in case of simpleperf command is used
582         File launchRootDir = null;
583         if (null != launchDirectory && !launchDirectory.isEmpty()) {
584             launchRootDir = new File(launchDirectory);
585             if (!launchRootDir.exists() && !launchRootDir.mkdirs()) {
586                 throw new IOException("Unable to create the destination directory "
587                     + launchRootDir + ". Try disabling selinux.");
588             }
589         } else {
590             Log.w(TAG, "logDumpsysIorapd: Missing launch-directory arg");
591             return;
592         }
593 
594         File launchSubDir = new File(launchRootDir, LAUNCH_SUB_DIRECTORY);
595 
596         if (!launchSubDir.exists() && !launchSubDir.mkdirs()) {
597             throw new IOException("Unable to create the lauch file sub directory "
598                 + launchSubDir + ". Try disabling selinux.");
599         }
600         String path = "iorapd_dumpsys_" + packageName + "_" + System.nanoTime() + ".txt";
601         File file = new File(launchSubDir, path);
602         try (FileOutputStream outputStream = new FileOutputStream(file);
603                 BufferedWriter writer = new BufferedWriter(
604                         new OutputStreamWriter(outputStream));
605                 ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
606                         executeShellCommand(IORAP_DUMPSYS_CMD);
607                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
608                         new FileInputStream(result.getFileDescriptor())))) {
609             String line;
610             while ((line = bufferedReader.readLine()) != null) {
611                 writer.write(line + "\n");
612             }
613         }
614 
615         Log.v(TAG, "logDumpsysIorapd: Saved to file: " + path);
616     }
617 
618     enum IorapCompilationStatus {
619         INCOMPLETE,
620         COMPLETE,
621         INSUFFICIENT_TRACES,
622     }
waitForIorapCompiled(String appPkgName)623     private IorapCompilationStatus waitForIorapCompiled(String appPkgName) throws IOException {
624         try (ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
625                 executeShellCommand(IORAP_DUMPSYS_CMD);
626                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
627                         new FileInputStream(result.getFileDescriptor())))) {
628             String line;
629             String prevLine = "";
630             while ((line = bufferedReader.readLine()) != null) {
631                 // Match the indented VersionedComponentName string.
632                 // "  com.google.android.deskclock/com.android.deskclock.DeskClock@62000712"
633                 // Note: spaces are meaningful here.
634                 if (prevLine.contains("  " + appPkgName) && prevLine.contains("@")) {
635                     // pre-requisite:
636                     // Compiled Status: Raw traces pending compilation (3)
637                     if (line.contains("Compiled Status: Usable compiled trace")) {
638                         return IorapCompilationStatus.COMPLETE;
639                     } else if (line.contains("Compiled Status: ") &&
640                             line.contains("more traces for compilation")) {
641                         //      Compiled Status: Need 1 more traces for compilation
642                         // No amount of waiting will help here because there were
643                         // insufficient traces made.
644                         return IorapCompilationStatus.INSUFFICIENT_TRACES;
645                     }
646                 }
647 
648                 prevLine = line;
649             }
650             return IorapCompilationStatus.INCOMPLETE;
651         }
652     }
653 
makeReasonForIorapTrialLaunch(int launchCount)654     private String makeReasonForIorapTrialLaunch(int launchCount) {
655         String reason = IORAP_TRIAL_LAUNCH;
656         if (launchCount == 0) {
657             reason = IORAP_TRIAL_LAUNCH_FIRST;
658         }
659         if (launchCount == IORAP_TRIAL_LAUNCH_ITERATIONS - 1) {
660             reason = IORAP_TRIAL_LAUNCH_LAST;
661         }
662         return reason;
663     }
664 
shouldIncludeIorap(String compilerFilter)665     private boolean shouldIncludeIorap(String compilerFilter) {
666         if (!mIorapTrialLaunch) {
667             return false;
668         }
669 
670         // No iorap compiler filters specified: treat all compiler filters as ok.
671         if (mIorapCompilerFilters == null) {
672             return true;
673         }
674 
675         // iorap compiler filters specified: the compilerFilter must be in the whitelist.
676         if (mIorapCompilerFilters.indexOf(compilerFilter) != -1) {
677             return true;
678         }
679 
680         return false;
681     }
682 
683     /**
684      * If launch order is "cyclic" then apps will be launched one after the
685      * other for each iteration count.
686      * If launch order is "sequential" then each app will be launched for given number
687      * iterations at once before launching the other apps.
688      */
setLaunchOrder()689     private void setLaunchOrder() {
690         if (LAUNCH_ORDER_CYCLIC.equalsIgnoreCase(mLaunchOrder)) {
691             for (String compilerFilter : mCompilerFilters) {
692                 if (mTrialLaunch) {
693                     for (String app : mNameToResultKey.keySet()) {
694                         mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, TRIAL_LAUNCH, /*iorapEnabled*/false));
695                     }
696                 }
697                 if (shouldIncludeIorap(compilerFilter)) {
698                     for (int launchCount = 0; launchCount < IORAP_TRIAL_LAUNCH_ITERATIONS; ++launchCount) {
699                         for (String app : mNameToResultKey.keySet()) {
700                             String reason = makeReasonForIorapTrialLaunch(launchCount);
701                             mLaunchOrderList.add(
702                                     new LaunchOrder(app, compilerFilter,
703                                             reason,
704                                             /*iorapEnabled*/true));
705                         }
706                     }
707                 }
708                 for (int launchCount = 0; launchCount < mLaunchIterations; launchCount++) {
709                     for (String app : mNameToResultKey.keySet()) {
710                         mLaunchOrderList.add(new LaunchOrder(app, compilerFilter,
711                                   String.format(LAUNCH_ITERATION, launchCount),
712                                         shouldIncludeIorap(compilerFilter)));
713                     }
714                 }
715                 if (mTraceDirectoryStr != null && !mTraceDirectoryStr.isEmpty()) {
716                     for (int traceCount = 0; traceCount < mTraceLaunchCount; traceCount++) {
717                         for (String app : mNameToResultKey.keySet()) {
718                             mLaunchOrderList.add(new LaunchOrder(app, compilerFilter,
719                                       String.format(TRACE_ITERATION, traceCount),
720                                             shouldIncludeIorap(compilerFilter)));
721                         }
722                     }
723                 }
724             }
725         } else if (LAUNCH_ORDER_SEQUENTIAL.equalsIgnoreCase(mLaunchOrder)) {
726             for (String compilerFilter : mCompilerFilters) {
727                 for (String app : mNameToResultKey.keySet()) {
728                     if (mTrialLaunch) {
729                         mLaunchOrderList.add(new LaunchOrder(app, compilerFilter, TRIAL_LAUNCH, /*iorapEnabled*/false));
730                     }
731                     if (shouldIncludeIorap(compilerFilter)) {
732                         for (int launchCount = 0; launchCount < IORAP_TRIAL_LAUNCH_ITERATIONS; ++launchCount) {
733                             String reason = makeReasonForIorapTrialLaunch(launchCount);
734                             mLaunchOrderList.add(
735                                     new LaunchOrder(app, compilerFilter,
736                                             reason,
737                                             /*iorapEnabled*/true));
738                         }
739                     }
740                     for (int launchCount = 0; launchCount < mLaunchIterations; launchCount++) {
741                         mLaunchOrderList.add(new LaunchOrder(app, compilerFilter,
742                                 String.format(LAUNCH_ITERATION, launchCount),
743                                         shouldIncludeIorap(compilerFilter)));
744                     }
745                     if (mTraceDirectoryStr != null && !mTraceDirectoryStr.isEmpty()) {
746                         for (int traceCount = 0; traceCount < mTraceLaunchCount; traceCount++) {
747                             mLaunchOrderList.add(new LaunchOrder(app, compilerFilter,
748                                     String.format(TRACE_ITERATION, traceCount),
749                                             shouldIncludeIorap(compilerFilter)));
750                         }
751                     }
752                 }
753             }
754         } else {
755             assertTrue("Launch order is not valid parameter", false);
756         }
757     }
758 
dropCache(boolean override)759     private void dropCache(boolean override) {
760         if (mDropCache || override) {
761             assertNotNull("Issue in dropping the cache",
762                     getInstrumentation().getUiAutomation()
763                             .executeShellCommand(DROP_CACHE_SCRIPT));
764         }
765     }
766 
767     // [[ $(adb shell whoami) == "root" ]]
checkIfRoot()768     private boolean checkIfRoot() throws IOException {
769         String total = "";
770         try (ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
771                 executeShellCommand("whoami");
772                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
773                         new FileInputStream(result.getFileDescriptor())))) {
774             String line;
775             while ((line = bufferedReader.readLine()) != null) {
776                 total = total + line;
777             }
778         }
779         return total.contains("root");
780     }
781 
stopIorapd()782     private void stopIorapd() {
783         getInstrumentation().getUiAutomation()
784                 .executeShellCommand("stop iorapd");
785         sleep(100);  // give it extra time to fully stop.
786     }
787 
startIorapd()788     private void startIorapd() {
789         String logcatTimeNow = getTimeNowForLogcat();
790         Log.v(TAG, "startIorapd, logcat time: " + logcatTimeNow);
791 
792         getInstrumentation().getUiAutomation()
793                 .executeShellCommand("start iorapd");
794 
795         int maxAttempts = 100;
796         int attempt = 0;
797         do {
798             // Ensure that IorapForwardingService fully reconnects to iorapd before proceeding.
799             String needle = "Connected to iorapd native service";
800             String logcatLines = getLogcatSinceTime(logcatTimeNow);
801 
802             if (logcatLines.contains(needle)) {
803                 break;
804             }
805 
806             sleep(1000);
807             attempt++;
808         } while (attempt < maxAttempts);
809 
810         if (attempt == maxAttempts) {
811             Log.e(TAG, "Timed out after waiting for iorapd to start");
812         }
813         // Wait a little bit longer for iorapd to settle.
814         sleep(1000);
815     }
816 
817     // Delete all db rows and files associated with a package in iorapd.
818     // Effectively deletes any raw or compiled trace files, unoptimizing the package in iorap.
purgeIorapPackage(String packageName)819     private void purgeIorapPackage(String packageName) {
820         try {
821             if (!checkIfRoot()) {
822                 throw new AssertionError("must be root to toggle iorapd; try adb root?");
823             }
824         } catch (IOException e) {
825             throw new AssertionError(e);
826         }
827 
828         Log.v(TAG, "Purge iorap package: " + packageName);
829         getInstrumentation().getUiAutomation()
830                 .executeShellCommand(String.format(IORAP_MAINTENANCE_CMD, packageName));
831         Log.v(TAG, "Executed: " + String.format(IORAP_MAINTENANCE_CMD, packageName));
832     }
833 
executeShellCommandWithTempFile(String cmd)834     String executeShellCommandWithTempFile(String cmd) {
835         Log.v(TAG, "executeShellCommandWithTempFile, cmd: " + cmd);
836         try {
837             //File outputDir =
838             //       InstrumentationRegistry.getInstrumentation().getContext().getCacheDir();
839             File outputFile = File.createTempFile("exec_shell_command", ".sh");
840 
841             try {
842                 outputFile.setWritable(true);
843                 outputFile.setExecutable(true, /*ownersOnly*/false);
844 
845                 String scriptPath = outputFile.toString();
846 
847                 // If this works correctly, the next log-line will print 'Success'.
848                 try (BufferedWriter writer = new BufferedWriter(new FileWriter(scriptPath))) {
849                     writer.write(cmd);
850                 }
851 
852                 String resultString = "";
853                 try (ParcelFileDescriptor result = getInstrumentation().getUiAutomation().
854                         executeShellCommand(scriptPath);
855                         BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
856                                 new FileInputStream(result.getFileDescriptor())))) {
857                     String line;
858                     while ((line = bufferedReader.readLine()) != null) {
859                         resultString += line + "\n";
860                     }
861                 }
862 
863                 return resultString;
864             } finally {
865                 outputFile.delete();
866             }
867         } catch (IOException e) {
868             throw new AssertionError("Failed to execute shell command: " + cmd, e);
869         }
870     }
871 
872     // Get the 'now' timestamp usable with $(adb logcat -v utc -T "time string")
getTimeNowForLogcat()873     String getTimeNowForLogcat() {
874         ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC);
875 
876         // YYYY-MM-DD hh:mm:ss.mmm
877         return utc.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
878     }
879 
getLogcatSinceTime(String logcatTime)880     String getLogcatSinceTime(String logcatTime) {
881         // The time has spaces in it but must be passed as a single arg.
882         // Therefore use a temp script file.
883         return executeShellCommandWithTempFile(
884                 String.format("logcat -d -v threadtime -v utc -T '%s'", logcatTime));
885     }
886 
887     /**
888      * Toggle iorapd-based readahead and trace-collection.
889      * If iorapd is already enabled and enable is true, does nothing.
890      * If iorapd is already disabled and enable is false, does nothing.
891      */
toggleIorapStatus(boolean enable)892     private void toggleIorapStatus(boolean enable) {
893         boolean currentlyEnabled = false;
894         Log.v(TAG, "toggleIorapStatus " + Boolean.toString(enable));
895 
896         // Do nothing if we are already enabled or disabled.
897         if (mIorapStatus == IorapStatus.ENABLED && enable) {
898             return;
899         } else if (mIorapStatus == IorapStatus.DISABLED && !enable) {
900             return;
901         }
902 
903         try {
904             if (!checkIfRoot()) {
905                 throw new AssertionError("must be root to toggle iorapd; try adb root?");
906             }
907         } catch (IOException e) {
908             throw new AssertionError(e);
909         }
910 
911         getInstrumentation().getUiAutomation()
912                 .executeShellCommand(String.format("setprop iorapd.perfetto.enable %b", enable));
913         getInstrumentation().getUiAutomation()
914                 .executeShellCommand(String.format("setprop iorapd.readahead.enable %b", enable));
915         getInstrumentation().getUiAutomation()
916                 .executeShellCommand(String.format(
917                         "setprop iorapd.maintenance.min_traces %d", IORAP_COMPILE_MIN_TRACES));
918         // this last command blocks until iorapd refreshes its system properties
919         getInstrumentation().getUiAutomation()
920                 .executeShellCommand(String.format("dumpsys iorapd --refresh-properties"));
921 
922         if (enable) {
923             mIorapStatus = IorapStatus.ENABLED;
924         } else {
925             mIorapStatus = IorapStatus.DISABLED;
926         }
927     }
928 
parseArgs(Bundle args)929     private void parseArgs(Bundle args) {
930         mNameToResultKey = new LinkedHashMap<String, String>();
931         mNameToLaunchTime = new HashMap<>();
932         String launchIterations = args.getString(KEY_LAUNCH_ITERATIONS);
933         if (launchIterations != null) {
934             mLaunchIterations = Integer.parseInt(launchIterations);
935         }
936         String forceStopApp = args.getString(KEY_FORCE_STOP_APP);
937 
938         if (forceStopApp != null) {
939             mForceStopApp = Boolean.parseBoolean(forceStopApp);
940         }
941 
942         String enableRecording = args.getString(ENABLE_SCREEN_RECORDING);
943 
944         if (enableRecording != null) {
945             mEnableRecording = Boolean.parseBoolean(enableRecording);
946         }
947         String appList = args.getString(KEY_APPS);
948         if (appList == null)
949             return;
950 
951         String appNames[] = appList.split("\\|");
952         for (String pair : appNames) {
953             String[] parts = pair.split("\\^");
954             if (parts.length != 2) {
955                 Log.e(TAG, "The apps key is incorrectly formatted");
956                 fail();
957             }
958 
959             mNameToResultKey.put(parts[0], parts[1]);
960             mNameToLaunchTime.put(parts[0], null);
961             mLastAppName = parts[0];
962         }
963         String requiredAccounts = args.getString(KEY_REQUIRED_ACCOUNTS);
964         if (requiredAccounts != null) {
965             mRequiredAccounts = new HashSet<String>();
966             for (String accountType : requiredAccounts.split(",")) {
967                 mRequiredAccounts.add(accountType);
968             }
969         }
970 
971         String compilerFilterList = args.getString(KEY_COMPILER_FILTERS);
972         if (compilerFilterList != null) {
973             // If a compiler filter is passed, we make a trial launch to force compilation
974             // of the apps.
975             mTrialLaunch = true;
976             mCompilerFilters = compilerFilterList.split("\\|");
977         } else {
978             // Just pass a null compiler filter to use the current state of the app.
979             mCompilerFilters = new String[1];
980         }
981 
982         String iorapCompilerFilterList = args.getString(KEY_IORAP_COMPILER_FILTERS);
983         if (iorapCompilerFilterList != null) {
984             // Passing in iorap compiler filters implies an iorap trial launch.
985             mIorapTrialLaunch = true;
986             mIorapCompilerFilters = Arrays.asList(iorapCompilerFilterList.split("\\|"));
987         }
988 
989         // Pre-populate the results map to avoid null checks.
990         for (String app : mNameToLaunchTime.keySet()) {
991             HashMap<String, List<AppLaunchResult>> map = new HashMap<>();
992             mNameToLaunchTime.put(app, map);
993             for (String compilerFilter : mCompilerFilters) {
994                 map.put(compilerFilter, new ArrayList<>());
995             }
996         }
997 
998         mTraceDirectoryStr = args.getString(KEY_TRACE_DIRECTORY);
999         mDropCache = Boolean.parseBoolean(args.getString(KEY_DROP_CACHE));
1000         mSimplePerfCmd = args.getString(KEY_SIMPLEPERF_CMD);
1001         mLaunchOrder = args.getString(KEY_LAUNCH_ORDER, LAUNCH_ORDER_CYCLIC);
1002         mSimplePerfAppOnly = Boolean.parseBoolean(args.getString(KEY_SIMPLEPERF_APP));
1003         mCycleCleanUp = Boolean.parseBoolean(args.getString(KEY_CYCLE_CLEAN));
1004         mTraceAll = Boolean.parseBoolean(args.getString(KEY_TRACE_ALL));
1005         mTrialLaunch = mTrialLaunch || Boolean.parseBoolean(args.getString(KEY_TRIAL_LAUNCH));
1006         mIorapTrialLaunch = mIorapTrialLaunch ||
1007                 Boolean.parseBoolean(args.getString(KEY_IORAP_TRIAL_LAUNCH));
1008 
1009         if (mSimplePerfCmd != null && mSimplePerfAppOnly) {
1010             Log.w(TAG, String.format("Passing both %s and %s is not supported, ignoring %s",
1011                 KEY_SIMPLEPERF_CMD, KEY_SIMPLEPERF_APP, KEY_SIMPLEPERF_CMD));
1012         }
1013     }
1014 
hasLeanback(Context context)1015     private boolean hasLeanback(Context context) {
1016         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
1017     }
1018 
createMappings()1019     private void createMappings() {
1020         mNameToIntent = new LinkedHashMap<String, Intent>();
1021 
1022         PackageManager pm = getInstrumentation().getContext()
1023                 .getPackageManager();
1024         Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
1025         intentToResolve.addCategory(hasLeanback(getInstrumentation().getContext()) ?
1026                 Intent.CATEGORY_LEANBACK_LAUNCHER :
1027                 Intent.CATEGORY_LAUNCHER);
1028         List<ResolveInfo> ris = pm.queryIntentActivities(intentToResolve, 0);
1029         resolveLoop(ris, intentToResolve, pm);
1030         // For Wear
1031         intentToResolve = new Intent(WEARABLE_ACTION_GOOGLE);
1032         ris = pm.queryIntentActivities(intentToResolve, 0);
1033         resolveLoop(ris, intentToResolve, pm);
1034     }
1035 
resolveLoop(List<ResolveInfo> ris, Intent intentToResolve, PackageManager pm)1036     private void resolveLoop(List<ResolveInfo> ris, Intent intentToResolve, PackageManager pm) {
1037         if (ris == null || ris.isEmpty()) {
1038             Log.i(TAG, "Could not find any apps");
1039         } else {
1040             for (ResolveInfo ri : ris) {
1041                 Intent startIntent = new Intent(intentToResolve);
1042                 startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
1043                         | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1044                 startIntent.setClassName(ri.activityInfo.packageName,
1045                         ri.activityInfo.name);
1046                 String appName = ri.loadLabel(pm).toString();
1047                 if (appName != null) {
1048                     // Support launching intent using package name or app name
1049                     mNameToIntent.put(ri.activityInfo.packageName, startIntent);
1050                     mNameToIntent.put(appName, startIntent);
1051                 }
1052             }
1053         }
1054     }
1055 
startApp(String appName, String launchReason)1056     private AppLaunchResult startApp(String appName, String launchReason)
1057             throws NameNotFoundException, RemoteException {
1058         Log.i(TAG, "Starting " + appName);
1059         if(mEnableRecording) {
1060             startRecording(appName, launchReason);
1061         }
1062 
1063         Intent startIntent = mNameToIntent.get(appName);
1064         if (startIntent == null) {
1065             Log.w(TAG, "App does not exist: " + appName);
1066             mResult.putString(mNameToResultKey.get(appName), "App does not exist");
1067             return new AppLaunchResult();
1068         }
1069         AppLaunchRunnable runnable = new AppLaunchRunnable(startIntent, launchReason);
1070         Thread t = new Thread(runnable);
1071         t.start();
1072         try {
1073             t.join(JOIN_TIMEOUT);
1074         } catch (InterruptedException e) {
1075             // ignore
1076         }
1077 
1078         if(mEnableRecording) {
1079             stopRecording();
1080         }
1081         return runnable.getResult();
1082     }
1083 
checkAccountSignIn()1084     private void checkAccountSignIn() {
1085         // ensure that the device has the required account types before starting test
1086         // e.g. device must have a valid Google account sign in to measure a meaningful launch time
1087         // for Gmail
1088         if (mRequiredAccounts == null || mRequiredAccounts.isEmpty()) {
1089             return;
1090         }
1091         final AccountManager am =
1092                 (AccountManager) getInstrumentation().getTargetContext().getSystemService(
1093                         Context.ACCOUNT_SERVICE);
1094         Account[] accounts = am.getAccounts();
1095         // use set here in case device has multiple accounts of the same type
1096         Set<String> foundAccounts = new HashSet<String>();
1097         for (Account account : accounts) {
1098             if (mRequiredAccounts.contains(account.type)) {
1099                 foundAccounts.add(account.type);
1100             }
1101         }
1102         // check if account type matches, if not, fail test with message on what account types
1103         // are missing
1104         if (mRequiredAccounts.size() != foundAccounts.size()) {
1105             mRequiredAccounts.removeAll(foundAccounts);
1106             StringBuilder sb = new StringBuilder("Device missing these accounts:");
1107             for (String account : mRequiredAccounts) {
1108                 sb.append(' ');
1109                 sb.append(account);
1110             }
1111             fail(sb.toString());
1112         }
1113     }
1114 
startHomeIntent()1115     private void startHomeIntent() {
1116         Intent homeIntent = new Intent(Intent.ACTION_MAIN);
1117         homeIntent.addCategory(Intent.CATEGORY_HOME);
1118         homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
1119                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1120         getInstrumentation().getContext().startActivity(homeIntent);
1121         sleep(POST_LAUNCH_IDLE_TIMEOUT);
1122     }
1123 
cleanUpAfterCycle()1124     private void cleanUpAfterCycle() {
1125         // Kill all the apps
1126         for (String appName : mNameToIntent.keySet()) {
1127             Log.w(TAG, String.format("killing %s", appName));
1128             forceStopApp(appName);
1129         }
1130         // Drop all the cache.
1131         assertNotNull("Issue in dropping the cache",
1132                 getInstrumentation().getUiAutomation()
1133                         .executeShellCommand(DROP_CACHE_SCRIPT));
1134     }
1135 
forceStopApp(String appName)1136     private void forceStopApp(String appName) {
1137         Intent startIntent = mNameToIntent.get(appName);
1138         if (startIntent != null) {
1139             String packageName = startIntent.getComponent().getPackageName();
1140             try {
1141                 mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT);
1142             } catch (RemoteException e) {
1143                 Log.w(TAG, "Error closing app", e);
1144             }
1145         }
1146     }
1147 
killBackgroundApp(String appName)1148     private void killBackgroundApp(String appName) {
1149         Intent startIntent = mNameToIntent.get(appName);
1150         if (startIntent != null) {
1151             String packageName = startIntent.getComponent().getPackageName();
1152             try {
1153                 mAm.killBackgroundProcesses(packageName, UserHandle.USER_CURRENT);
1154             } catch (RemoteException e) {
1155                 Log.w(TAG, "Error closing app", e);
1156             }
1157         }
1158     }
1159 
sleep(int time)1160     private void sleep(int time) {
1161         try {
1162             Thread.sleep(time);
1163         } catch (InterruptedException e) {
1164             // ignore
1165         }
1166     }
1167 
reportError(String appName, String processName)1168     private void reportError(String appName, String processName) {
1169         ActivityManager am = (ActivityManager) getInstrumentation()
1170                 .getContext().getSystemService(Context.ACTIVITY_SERVICE);
1171         List<ProcessErrorStateInfo> crashes = am.getProcessesInErrorState();
1172         if (crashes != null) {
1173             for (ProcessErrorStateInfo crash : crashes) {
1174                 if (!crash.processName.equals(processName))
1175                     continue;
1176 
1177                 Log.w(TAG, appName + " crashed: " + crash.shortMsg);
1178                 mResult.putString(mNameToResultKey.get(appName), crash.shortMsg);
1179                 return;
1180             }
1181         }
1182 
1183         mResult.putString(mNameToResultKey.get(appName),
1184                 "Crashed for unknown reason");
1185         Log.w(TAG, appName
1186                 + " not found in process list, most likely it is crashed");
1187     }
1188 
1189     private class LaunchOrder {
1190         private String mApp;
1191         private String mCompilerFilter;
1192         private String mLaunchReason;
1193         private boolean mIorapEnabled;
1194 
LaunchOrder(String app, String compilerFilter, String launchReason, boolean iorapEnabled)1195         LaunchOrder(String app, String compilerFilter, String launchReason, boolean iorapEnabled) {
1196             mApp = app;
1197             mCompilerFilter = compilerFilter;
1198             mLaunchReason = launchReason;
1199             mIorapEnabled = iorapEnabled;
1200         }
1201 
getApp()1202         public String getApp() {
1203             return mApp;
1204         }
1205 
setApp(String app)1206         public void setApp(String app) {
1207             mApp = app;
1208         }
1209 
getCompilerFilter()1210         public String getCompilerFilter() {
1211             return mCompilerFilter;
1212         }
1213 
getLaunchReason()1214         public String getLaunchReason() {
1215             return mLaunchReason;
1216         }
1217 
setLaunchReason(String launchReason)1218         public void setLaunchReason(String launchReason) {
1219             mLaunchReason = launchReason;
1220         }
1221 
setIorapEnabled(boolean iorapEnabled)1222         public void setIorapEnabled(boolean iorapEnabled) {
1223             mIorapEnabled = iorapEnabled;
1224         }
1225 
getIorapEnabled()1226         public boolean getIorapEnabled() {
1227             return mIorapEnabled;
1228         }
1229     }
1230 
1231     private class AppLaunchResult {
1232         long mLaunchTime;
1233         long mCpuCycles;
1234         long mMajorFaults;
1235 
AppLaunchResult()1236         AppLaunchResult() {
1237             mLaunchTime = -1L;
1238             mCpuCycles = -1L;
1239             mMajorFaults = -1L;
1240         }
1241 
AppLaunchResult(String launchTime, String cpuCycles, String majorFaults)1242         AppLaunchResult(String launchTime, String cpuCycles, String majorFaults) {
1243             try {
1244                 mLaunchTime = Long.parseLong(launchTime, 10);
1245                 mCpuCycles = Long.parseLong(cpuCycles, 10);
1246                 mMajorFaults = Long.parseLong(majorFaults, 10);
1247             } catch (NumberFormatException e) {
1248                 Log.e(TAG, "Error parsing result", e);
1249             }
1250         }
1251     }
1252 
1253     private class AppLaunchRunnable implements Runnable {
1254         private Intent mLaunchIntent;
1255         private AppLaunchResult mLaunchResult;
1256         private String mLaunchReason;
1257 
AppLaunchRunnable(Intent intent, String launchReason)1258         public AppLaunchRunnable(Intent intent, String launchReason) {
1259             mLaunchIntent = intent;
1260             mLaunchReason = launchReason;
1261             mLaunchResult = new AppLaunchResult();
1262         }
1263 
getResult()1264         public AppLaunchResult getResult() {
1265             return mLaunchResult;
1266         }
1267 
run()1268         public void run() {
1269             File launchFile = null;
1270             try {
1271                 String packageName = mLaunchIntent.getComponent().getPackageName();
1272                 String componentName = mLaunchIntent.getComponent().flattenToShortString();
1273                 if (mForceStopApp) {
1274                     mAm.forceStopPackage(packageName, UserHandle.USER_CURRENT);
1275                 }
1276                 String launchCmd = String.format("%s %s", APP_LAUNCH_CMD, componentName);
1277                 if (mSimplePerfAppOnly) {
1278                     try {
1279                         // executeShellCommand cannot handle shell specific actions, like '&'.
1280                         // Therefore, we create a file containing the command and make that
1281                         // the command to launch.
1282                         launchFile = File.createTempFile(LAUNCH_SCRIPT_NAME, ".sh");
1283                         launchFile.setExecutable(true);
1284                         try (FileOutputStream stream = new FileOutputStream(launchFile);
1285                              BufferedWriter writer =
1286                                 new BufferedWriter(new OutputStreamWriter(stream))) {
1287                             String cmd = String.format(SIMPLEPERF_APP_CMD, packageName, launchCmd);
1288                             // In the file, we need to escape any "$".
1289                             cmd = cmd.replace("$", "\\$");
1290                             writer.write(cmd);
1291                         }
1292                         launchCmd = launchFile.getAbsolutePath();
1293                     } catch (IOException e) {
1294                         Log.w(TAG, "Error writing the launch command", e);
1295                         return;
1296                     }
1297                 } else if (null != mSimplePerfCmd) {
1298                     launchCmd = String.format("%s %s", mSimplePerfCmd, launchCmd);
1299                 }
1300                 Log.v(TAG, "Final launch cmd:" + launchCmd);
1301                 ParcelFileDescriptor parcelDesc = getInstrumentation().getUiAutomation()
1302                         .executeShellCommand(launchCmd);
1303                 mLaunchResult = parseLaunchTimeAndWrite(parcelDesc, String.format
1304                         ("App Launch :%s %s", componentName, mLaunchReason));
1305             } catch (RemoteException e) {
1306                 Log.w(TAG, "Error launching app", e);
1307             } finally {
1308                 if (launchFile != null) {
1309                     launchFile.delete();
1310                 }
1311             }
1312         }
1313 
1314         /**
1315          * Method to parse the launch time info and write the result to file
1316          *
1317          * @param parcelDesc
1318          * @return
1319          */
parseLaunchTimeAndWrite(ParcelFileDescriptor parcelDesc, String headerInfo)1320         private AppLaunchResult parseLaunchTimeAndWrite(ParcelFileDescriptor parcelDesc,
1321                 String headerInfo) {
1322             String launchTime = "-1";
1323             String cpuCycles = "-1";
1324             String majorFaults = "-1";
1325             boolean launchSuccess = false;
1326             try {
1327                 InputStream inputStream = new FileInputStream(parcelDesc.getFileDescriptor());
1328                 /* SAMPLE OUTPUT : Cold launch
1329                 Starting: Intent { cmp=com.google.android.calculator/com.android.calculator2.Calculator }
1330                 Status: ok
1331                 LaunchState: COLD
1332                 Activity: com.google.android.calculator/com.android.calculator2.Calculator
1333                 TotalTime: 357
1334                 WaitTime: 377
1335                 Complete*/
1336                 /* SAMPLE OUTPUT : Hot launch
1337                 Starting: Intent { cmp=com.google.android.calculator/com.android.calculator2.Calculator }
1338                 Warning: Activity not started, its current task has been brought to the front
1339                 Status: ok
1340                 LaunchState: HOT
1341                 Activity: com.google.android.calculator/com.android.calculator2.CalculatorGoogle
1342                 TotalTime: 60
1343                 WaitTime: 67
1344                 Complete*/
1345                 /* WITH SIMPLEPERF :
1346                 Performance counter statistics,
1347                 6595722690,cpu-cycles,4.511040,GHz,(100%),
1348                 0,major-faults,0.000,/sec,(100%),
1349                 Total test time,1.462129,seconds,*/
1350                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
1351                         inputStream));
1352                 String line;
1353                 mBufferedWriter.newLine();
1354                 mBufferedWriter.write(headerInfo);
1355                 mBufferedWriter.newLine();
1356                 while ((line = bufferedReader.readLine()) != null) {
1357                     mBufferedWriter.write(line);
1358                     mBufferedWriter.newLine();
1359                     if (line.startsWith(SUCCESS_MESSAGE)) {
1360                         launchSuccess = true;
1361                     }
1362                     if (!launchSuccess) {
1363                         continue;
1364                     }
1365                     // Parse TotalTime which is the launch time
1366                     if (line.startsWith(TOTAL_TIME_MESSAGE)) {
1367                         String launchSplit[] = line.split(":");
1368                         launchTime = launchSplit[1].trim();
1369                     }
1370 
1371                     if (mSimplePerfAppOnly) {
1372                         if (line.contains(",cpu-cycles,")) {
1373                             cpuCycles = line.split(",")[0].trim();
1374                         } else if (line.contains(",major-faults,")) {
1375                             majorFaults = line.split(",")[0].trim();
1376                         }
1377                     }
1378                 }
1379                 mBufferedWriter.flush();
1380                 inputStream.close();
1381             } catch (IOException e) {
1382                 Log.w(TAG, "Error parsing launch time and writing to file", e);
1383             }
1384             return new AppLaunchResult(launchTime, cpuCycles, majorFaults);
1385         }
1386 
1387     }
1388 
1389     /**
1390      * Start the screen recording while launching the app.
1391      *
1392      * @param appName
1393      * @param launchReason
1394      */
startRecording(String appName, String launchReason)1395     private void startRecording(String appName, String launchReason) {
1396         Log.v(TAG, "Started Recording");
1397         mCurrentThread = new RecordingThread("test-screen-record",
1398                 String.format("%s_%s", appName, launchReason));
1399         mCurrentThread.start();
1400     }
1401 
1402     /**
1403      * Stop already started screen recording.
1404      */
stopRecording()1405     private void stopRecording() {
1406         // Skip if not directory.
1407         if (launchSubDir == null) {
1408             return;
1409         }
1410 
1411         // Add some extra time to the video end.
1412         SystemClock.sleep(VIDEO_TAIL_BUFFER);
1413         // Ctrl + C all screen record processes.
1414         mCurrentThread.cancel();
1415         // Wait for the thread to completely die.
1416         try {
1417             mCurrentThread.join();
1418         } catch (InterruptedException ex) {
1419             Log.e(TAG, "Interrupted when joining the recording thread.", ex);
1420         }
1421         Log.v(TAG, "Stopped Recording");
1422     }
1423 
1424     /** Returns the recording's name for part {@code part} of launch description. */
getOutputFile(String description, int part)1425     private File getOutputFile(String description, int part) {
1426         // Omit the iteration number for the first iteration.
1427         final String fileName =
1428                 String.format(
1429                         "%s-video%s.mp4", description, part == 1 ? "" : part);
1430         return Paths.get(launchSubDir.getAbsolutePath(), description).toFile();
1431     }
1432 
1433 
1434     /**
1435      * Encapsulates the start and stop screen recording logic.
1436      * Copied from ScreenRecordCollector.
1437      */
1438     private class RecordingThread extends Thread {
1439         private final String mDescription;
1440         private final List<File> mRecordings;
1441 
1442         private boolean mContinue;
1443 
RecordingThread(String name, String description)1444         public RecordingThread(String name, String description) {
1445             super(name);
1446 
1447             mContinue = true;
1448             mRecordings = new ArrayList<>();
1449 
1450             assertNotNull("No test description provided for recording.", description);
1451             mDescription = description;
1452         }
1453 
1454         @Override
run()1455         public void run() {
1456             try {
1457                 // Start at i = 1 to encode parts as X.mp4, X2.mp4, X3.mp4, etc.
1458                 for (int i = 1; i <= MAX_RECORDING_PARTS && mContinue; i++) {
1459                     File output = getOutputFile(mDescription, i);
1460                     Log.d(
1461                             TAG,
1462                             String.format("Recording screen to %s", output.getAbsolutePath()));
1463                     mRecordings.add(output);
1464                     // Make sure not to block on this background command in the main thread so
1465                     // that the test continues to run, but block in this thread so it does not
1466                     // trigger a new screen recording session before the prior one completes.
1467                     getDevice().executeShellCommand(
1468                                     String.format("screenrecord %s", output.getAbsolutePath()));
1469                 }
1470             } catch (IOException e) {
1471                 throw new RuntimeException("Caught exception while screen recording.");
1472             }
1473         }
1474 
cancel()1475         public void cancel() {
1476             mContinue = false;
1477 
1478             // Identify the screenrecord PIDs and send SIGINT 2 (Ctrl + C) to each.
1479             try {
1480                 String[] pids = getDevice().executeShellCommand(
1481                         "pidof screenrecord").split(" ");
1482                 for (String pid : pids) {
1483                     // Avoid empty process ids, because of weird splitting behavior.
1484                     if (pid.isEmpty()) {
1485                         continue;
1486                     }
1487 
1488                     getDevice().executeShellCommand(
1489                             String.format("kill -2 %s", pid));
1490                     Log.d(
1491                             TAG,
1492                             String.format("Sent SIGINT 2 to screenrecord process (%s)", pid));
1493                 }
1494             } catch (IOException e) {
1495                 throw new RuntimeException("Failed to kill screen recording process.");
1496             }
1497         }
1498 
getRecordings()1499         public List<File> getRecordings() {
1500             return mRecordings;
1501         }
1502     }
1503 
getDevice()1504     public UiDevice getDevice() {
1505         if (mDevice == null) {
1506             mDevice = UiDevice.getInstance(getInstrumentation());
1507         }
1508         return mDevice;
1509     }
1510 }
1511