• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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