1 /* 2 * Copyright (C) 2018 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 android.device.collectors; 17 18 import android.content.Context; 19 import android.device.collectors.annotations.OptionClass; 20 import android.os.Bundle; 21 import android.os.PowerManager; 22 import android.os.PowerManager.WakeLock; 23 import android.util.Log; 24 import androidx.annotation.VisibleForTesting; 25 import com.android.helpers.PerfettoHelper; 26 27 import java.io.IOException; 28 import java.nio.file.Path; 29 import java.nio.file.Paths; 30 import java.util.HashMap; 31 import java.util.Map; 32 import java.util.UUID; 33 import java.util.function.Supplier; 34 import org.junit.runner.Description; 35 import org.junit.runner.Result; 36 import org.junit.runner.notification.Failure; 37 38 /** 39 * A {@link PerfettoListener} that captures the perfetto trace during each test method 40 * and save the perfetto trace files under 41 * <root_folder>/<test_display_name>/PerfettoListener/<test_display_name>-<invocation_count>.pb 42 */ 43 @OptionClass(alias = "perfetto-collector") 44 public class PerfettoListener extends BaseMetricListener { 45 46 // Default perfetto config file name. 47 private static final String DEFAULT_CONFIG_FILE = "trace_config.pb"; 48 // Default wait time before stopping the perfetto trace. 49 private static final String DEFAULT_WAIT_TIME_MSECS = "3000"; 50 // Default output folder to store the perfetto output traces. 51 private static final String DEFAULT_OUTPUT_ROOT = "/sdcard/test_results"; 52 // Argument to indicate the perfetto config file is text proto file. 53 public static final String PERFETTO_CONFIG_TEXT_PROTO = "perfetto_config_text_proto"; 54 // Argument to get custom config file name for collecting the trace. 55 private static final String PERFETTO_CONFIG_FILE_ARG = "perfetto_config_file"; 56 // Argument to get custom time in millisecs to wait before dumping the trace. 57 // This has to be atleast the dump interval time set in the trace config file 58 // or greater than that. Otherwise we will miss trace information from the test. 59 private static final String PERFETTO_WAIT_TIME_ARG = "perfetto_wait_time_ms"; 60 // Destination directory to save the trace results. 61 private static final String TEST_OUTPUT_ROOT = "test_output_root"; 62 // Perfetto file path key. 63 private static final String PERFETTO_FILE_PATH = "perfetto_file_path"; 64 // Collect per run if it is set to true otherwise collect per test. 65 public static final String COLLECT_PER_RUN = "per_run"; 66 public static final String PERFETTO_PREFIX = "perfetto_"; 67 // Skip failure metrics collection if this flag is set to true. 68 public static final String SKIP_TEST_FAILURE_METRICS = "skip_test_failure_metrics"; 69 70 private final WakeLockContext mWakeLockContext; 71 private final Supplier<WakeLock> mWakelockSupplier; 72 private final WakeLockAcquirer mWakeLockAcquirer; 73 private final WakeLockReleaser mWakeLockReleaser; 74 75 // Trace config file name to use while collecting the trace which is defaulted to 76 // trace_config.pb. It can be changed via the perfetto_config_file arg. 77 private String mConfigFileName; 78 // Wait time can be customized based on the dump interval set in the trace config. 79 private long mWaitTimeInMs; 80 // Perfetto traces collected during the test will be saved under this root folder. 81 private String mTestOutputRoot; 82 // Store the method name and invocation count to create unique file name for each trace. 83 private Map<String, Integer> mTestIdInvocationCount = new HashMap<>(); 84 private boolean mPerfettoStartSuccess = false; 85 private boolean mIsConfigTextProto = false; 86 private boolean mIsCollectPerRun; 87 private boolean mSkipTestFailureMetrics; 88 private boolean mIsTestFailed = false; 89 90 private PerfettoHelper mPerfettoHelper = new PerfettoHelper(); 91 92 // For USB disconnected cases you may want this option to be true. This option makes sure 93 // the device does not go to sleep while collecting. 94 @VisibleForTesting 95 static final String HOLD_WAKELOCK_WHILE_COLLECTING = "hold_wakelock_while_collecting"; 96 97 private boolean mHoldWakelockWhileCollecting; 98 PerfettoListener()99 public PerfettoListener() { 100 super(); 101 mWakeLockContext = this::runWithWakeLock; 102 mWakelockSupplier = this::getWakeLock; 103 mWakeLockAcquirer = this::acquireWakelock; 104 mWakeLockReleaser = this::releaseWakelock; 105 } 106 107 /** 108 * Constructor to simulate receiving the instrumentation arguments. Should not be used except 109 * for testing. 110 */ 111 @VisibleForTesting PerfettoListener( Bundle args, PerfettoHelper helper, Map invocationMap, WakeLockContext wakeLockContext, Supplier<WakeLock> wakelockSupplier, WakeLockAcquirer wakeLockAcquirer, WakeLockReleaser wakeLockReleaser)112 PerfettoListener( 113 Bundle args, 114 PerfettoHelper helper, 115 Map invocationMap, 116 WakeLockContext wakeLockContext, 117 Supplier<WakeLock> wakelockSupplier, 118 WakeLockAcquirer wakeLockAcquirer, 119 WakeLockReleaser wakeLockReleaser) { 120 super(args); 121 mPerfettoHelper = helper; 122 mTestIdInvocationCount = invocationMap; 123 mWakeLockContext = wakeLockContext; 124 mWakeLockAcquirer = wakeLockAcquirer; 125 mWakeLockReleaser = wakeLockReleaser; 126 mWakelockSupplier = wakelockSupplier; 127 } 128 129 @Override onTestRunStart(DataRecord runData, Description description)130 public void onTestRunStart(DataRecord runData, Description description) { 131 Bundle args = getArgsBundle(); 132 133 // Whether to collect the for the entire test run or per test. 134 mIsCollectPerRun = Boolean.parseBoolean(args.getString(COLLECT_PER_RUN)); 135 136 // Whether the config is text proto or not. By default set to false. 137 mIsConfigTextProto = Boolean.parseBoolean(args.getString(PERFETTO_CONFIG_TEXT_PROTO)); 138 139 // Perfetto config file has to be under /data/misc/perfetto-traces/ 140 // defaulted to trace_config.pb is perfetto_config_file is not passed. 141 mConfigFileName = args.getString(PERFETTO_CONFIG_FILE_ARG, DEFAULT_CONFIG_FILE); 142 143 144 // Whether to hold wakelocks on all Prefetto tracing functions. You may want to enable 145 // this if your device is not USB connected. This option prevents the device from 146 // going into suspend mode while this listener is running intensive tasks. 147 mHoldWakelockWhileCollecting = 148 Boolean.parseBoolean(args.getString(HOLD_WAKELOCK_WHILE_COLLECTING)); 149 150 // Wait time before stopping the perfetto trace collection after the test 151 // is completed. Defaulted to 3000 msecs if perfetto_wait_time_ms is not passed. 152 // TODO: b/118122395 for parsing failures. 153 mWaitTimeInMs = 154 Long.parseLong(args.getString(PERFETTO_WAIT_TIME_ARG, DEFAULT_WAIT_TIME_MSECS)); 155 156 // Destination folder in the device to save all the trace files. 157 // Defaulted to /sdcard/test_results if test_output_root is not passed. 158 mTestOutputRoot = args.getString(TEST_OUTPUT_ROOT, DEFAULT_OUTPUT_ROOT); 159 160 // By default this flag is set to false to collect the metrics on test failure. 161 mSkipTestFailureMetrics = "true".equals(args.getString(SKIP_TEST_FAILURE_METRICS)); 162 163 if (!mIsCollectPerRun) { 164 return; 165 } 166 167 Runnable task = 168 () -> { 169 Log.i(getTag(), "Starting perfetto before test run started."); 170 startPerfettoTracing(); 171 }; 172 173 if (mHoldWakelockWhileCollecting) { 174 Log.d(getTag(), "Holding a wakelock at onTestRunSTart."); 175 mWakeLockContext.run(task); 176 } else { 177 task.run(); 178 } 179 } 180 181 @Override onTestStart(DataRecord testData, Description description)182 public void onTestStart(DataRecord testData, Description description) { 183 mIsTestFailed = false; 184 if (mIsCollectPerRun) { 185 return; 186 } 187 188 Runnable task = 189 () -> { 190 mTestIdInvocationCount.compute( 191 getTestFileName(description), 192 (key, value) -> (value == null) ? 1 : value + 1); 193 Log.i(getTag(), "Starting perfetto before test started."); 194 startPerfettoTracing(); 195 }; 196 197 if (mHoldWakelockWhileCollecting) { 198 Log.d(getTag(), "Holding a wakelock at onTestStart."); 199 mWakeLockContext.run(task); 200 } else { 201 task.run(); 202 } 203 } 204 205 @Override onTestFail(DataRecord testData, Description description, Failure failure)206 public void onTestFail(DataRecord testData, Description description, Failure failure) { 207 mIsTestFailed = true; 208 } 209 210 @Override onTestEnd(DataRecord testData, Description description)211 public void onTestEnd(DataRecord testData, Description description) { 212 if (mIsCollectPerRun) { 213 return; 214 } 215 216 if (!mPerfettoStartSuccess) { 217 Log.i( 218 getTag(), 219 "Skipping perfetto stop attempt onTestEnd because perfetto did not " 220 + "start successfully."); 221 return; 222 } 223 224 Runnable task = null; 225 if (mSkipTestFailureMetrics && mIsTestFailed) { 226 Log.i(getTag(), "Skipping the metric collection due to test failure."); 227 // Stop the existing perfetto trace collection. 228 try { 229 if (!mPerfettoHelper.stopPerfetto()) { 230 Log.e(getTag(), "Failed to stop the perfetto process."); 231 } 232 } catch (IOException e) { 233 Log.e(getTag(), "Failed to stop the perfetto.", e); 234 } 235 } else { 236 task = 237 () -> { 238 Log.i(getTag(), "Stopping perfetto after test ended."); 239 // Construct test output directory in the below format 240 // <root_folder>/<test_name>/PerfettoListener/<test_name>-<count>.pb 241 Path path = 242 Paths.get( 243 mTestOutputRoot, 244 getTestFileName(description), 245 this.getClass().getSimpleName(), 246 String.format( 247 "%s%s-%d.pb", 248 PERFETTO_PREFIX, 249 getTestFileName(description), 250 mTestIdInvocationCount.get( 251 getTestFileName(description)))); 252 stopPerfettoTracing(path, testData); 253 }; 254 if (mHoldWakelockWhileCollecting) { 255 Log.d(getTag(), "Holding a wakelock at onTestEnd."); 256 mWakeLockContext.run(task); 257 } else { 258 task.run(); 259 } 260 } 261 } 262 263 @Override onTestRunEnd(DataRecord runData, Result result)264 public void onTestRunEnd(DataRecord runData, Result result) { 265 if (!mIsCollectPerRun) { 266 return; 267 } 268 if (!mPerfettoStartSuccess) { 269 Log.i( 270 getTag(), 271 "Skipping perfetto stop attempt because perfetto did not " 272 + "start successfully."); 273 return; 274 } 275 276 Runnable task = 277 () -> { 278 Log.i(getTag(), "Stopping perfetto after test run ended."); 279 // Construct test output directory in the below format 280 // <root_folder>/PerfettoListener/<randomUUID>.pb 281 Path path = 282 Paths.get( 283 mTestOutputRoot, 284 this.getClass().getSimpleName(), 285 String.format( 286 "%s%d.pb", 287 PERFETTO_PREFIX, UUID.randomUUID().hashCode())); 288 stopPerfettoTracing(path, runData); 289 }; 290 291 if (mHoldWakelockWhileCollecting) { 292 Log.d(getTag(), "Holding a wakelock at onTestRunEnd."); 293 mWakeLockContext.run(task); 294 } else { 295 task.run(); 296 } 297 } 298 299 @VisibleForTesting runWithWakeLock(Runnable runnable)300 void runWithWakeLock(Runnable runnable) { 301 WakeLock wakelock = null; 302 try { 303 wakelock = mWakelockSupplier.get(); 304 mWakeLockAcquirer.acquire(wakelock); 305 runnable.run(); 306 } finally { 307 mWakeLockReleaser.release(wakelock); 308 } 309 } 310 311 interface WakeLockContext { run(Runnable runnable)312 void run(Runnable runnable); 313 } 314 315 interface WakeLockAcquirer { acquire(WakeLock wakelock)316 void acquire(WakeLock wakelock); 317 } 318 319 interface WakeLockReleaser { release(WakeLock wakelock)320 void release(WakeLock wakelock); 321 } 322 323 @VisibleForTesting acquireWakelock(WakeLock wakelock)324 public void acquireWakelock(WakeLock wakelock) { 325 if (wakelock != null) { 326 Log.d(getTag(), "wakelock.isHeld: " + wakelock.isHeld()); 327 Log.d(getTag(), "acquiring wakelock."); 328 wakelock.acquire(); 329 Log.d(getTag(), "wakelock acquired."); 330 Log.d(getTag(), "wakelock.isHeld: " + wakelock.isHeld()); 331 } 332 } 333 334 @VisibleForTesting releaseWakelock(WakeLock wakelock)335 public void releaseWakelock(WakeLock wakelock) { 336 if (wakelock != null) { 337 Log.d(getTag(), "wakelock.isHeld: " + wakelock.isHeld()); 338 Log.d(getTag(), "releasing wakelock."); 339 wakelock.release(); 340 Log.d(getTag(), "wakelock released."); 341 Log.d(getTag(), "wakelock.isHeld: " + wakelock.isHeld()); 342 } 343 } 344 getWakeLock()345 private WakeLock getWakeLock() { 346 PowerManager pm = 347 (PowerManager) 348 getInstrumentation().getContext().getSystemService(Context.POWER_SERVICE); 349 350 return pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, PerfettoListener.class.getName()); 351 } 352 353 /** 354 * Start perfetto tracing using the given config file. 355 */ startPerfettoTracing()356 private void startPerfettoTracing() { 357 mPerfettoStartSuccess = mPerfettoHelper.startCollecting(mConfigFileName, 358 mIsConfigTextProto); 359 if (!mPerfettoStartSuccess) { 360 Log.e(getTag(), "Perfetto did not start successfully."); 361 } 362 } 363 364 /** 365 * Stop perfetto tracing and dumping the collected trace file in given path and updating the 366 * record with the path to the trace file. 367 */ stopPerfettoTracing(Path path, DataRecord record)368 private void stopPerfettoTracing(Path path, DataRecord record) { 369 if (!mPerfettoHelper.stopCollecting(mWaitTimeInMs, path.toString())) { 370 Log.e(getTag(), "Failed to collect the perfetto output."); 371 } else { 372 record.addStringMetric(PERFETTO_FILE_PATH, path.toString()); 373 } 374 } 375 376 /** 377 * Returns the packagename.classname_methodname which has no special characters and used to 378 * create file names. 379 */ getTestFileName(Description description)380 public static String getTestFileName(Description description) { 381 return String.format("%s_%s", description.getClassName(), description.getMethodName()); 382 } 383 } 384