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 
17 package com.android.server.wm.flicker;
18 
19 import static com.android.server.wm.flicker.monitor.TransitionMonitor.OUTPUT_DIR;
20 
21 import android.util.Log;
22 
23 import androidx.annotation.NonNull;
24 import androidx.annotation.Nullable;
25 import androidx.annotation.VisibleForTesting;
26 import androidx.test.InstrumentationRegistry;
27 
28 import com.android.server.wm.flicker.monitor.TransitionMonitor;
29 import com.android.server.wm.flicker.monitor.LayersTraceMonitor;
30 import com.android.server.wm.flicker.monitor.ScreenRecorder;
31 import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor;
32 import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor;
33 
34 import java.nio.file.Files;
35 import java.nio.file.Path;
36 import java.nio.file.Paths;
37 import java.util.ArrayList;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.Locale;
41 
42 /**
43  * Builds and runs UI transitions capturing test artifacts.
44  *
45  * <p>User can compose a transition from simpler steps, specifying setup and teardown steps. During
46  * a transition, Layers trace, WindowManager trace, screen recordings and window animation frame
47  * stats can be captured.
48  *
49  * <pre>
50  * Transition builder options:
51  *  {@link TransitionBuilder#run(Runnable)} run transition under test. Monitors will be started
52  *  before the transition and stopped after the transition is completed.
53  *  {@link TransitionBuilder#repeat(int)} repeat transitions under test multiple times recording
54  *  result for each run.
55  *  {@link TransitionBuilder#withTag(String)} specify a string identifier used to prefix logs and
56  *  artifacts generated.
57  *  {@link TransitionBuilder#runBeforeAll(Runnable)} run setup transitions once before all other
58  *  transition are run to set up an initial state on device.
59  *  {@link TransitionBuilder#runBefore(Runnable)} run setup transitions before each test transition
60  *  run.
61  *  {@link TransitionBuilder#runAfter(Runnable)} run teardown transitions after each test
62  *  transition.
63  *  {@link TransitionBuilder#runAfter(Runnable)} run teardown transitions once after all
64  *  other transition  are run.
65  *  {@link TransitionBuilder#includeJankyRuns()} disables {@link WindowAnimationFrameStatsMonitor}
66  *  to monitor janky frames. If janky frames are detected, then the test run is skipped. This
67  *  monitor is enabled by default.
68  *  {@link TransitionBuilder#skipLayersTrace()} disables {@link LayersTraceMonitor} used to
69  *  capture Layers trace during a transition. This monitor is enabled by default.
70  *  {@link TransitionBuilder#skipWindowManagerTrace()} disables {@link WindowManagerTraceMonitor}
71  *  used to capture WindowManager trace during a transition. This monitor is enabled by
72  *  default.
73  *  {@link TransitionBuilder#recordAllRuns()} records the screen contents and saves it to a file.
74  *  All the runs including setup and teardown transitions are included in the recording. This
75  *  monitor is used for debugging purposes.
76  *  {@link TransitionBuilder#recordEachRun()} records the screen contents during test transitions
77  *  and saves it to a file for each run. This monitor is used for debugging purposes.
78  *
79  * Example transition to capture WindowManager and Layers trace when opening a test app:
80  * {@code
81  * TransitionRunner.newBuilder()
82  *      .withTag("OpenTestAppFast")
83  *      .runBeforeAll(UiAutomationLib::wakeUp)
84  *      .runBeforeAll(UiAutomationLib::UnlockDevice)
85  *      .runBeforeAll(UiAutomationLib::openTestApp)
86  *      .runBefore(UiAutomationLib::closeTestApp)
87  *      .run(UiAutomationLib::openTestApp)
88  *      .runAfterAll(UiAutomationLib::closeTestApp)
89  *      .repeat(5)
90  *      .build()
91  *      .run();
92  * }
93  * </pre>
94  */
95 public class TransitionRunner {
96     private static final String TAG = "FLICKER";
97     private final ScreenRecorder mScreenRecorder;
98     private final WindowManagerTraceMonitor mWmTraceMonitor;
99     private final LayersTraceMonitor mLayersTraceMonitor;
100     private final WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
101 
102     private final List<TransitionMonitor> mAllRunsMonitors;
103     private final List<TransitionMonitor> mPerRunMonitors;
104     private final List<Runnable> mBeforeAlls;
105     private final List<Runnable> mBefores;
106     private final List<Runnable> mTransitions;
107     private final List<Runnable> mAfters;
108     private final List<Runnable> mAfterAlls;
109 
110     private final int mIterations;
111     private final String mTestTag;
112 
113     @Nullable private List<TransitionResult> mResults = null;
114 
TransitionRunner(TransitionBuilder builder)115     private TransitionRunner(TransitionBuilder builder) {
116         mScreenRecorder = builder.mScreenRecorder;
117         mWmTraceMonitor = builder.mWmTraceMonitor;
118         mLayersTraceMonitor = builder.mLayersTraceMonitor;
119         mFrameStatsMonitor = builder.mFrameStatsMonitor;
120 
121         mAllRunsMonitors = builder.mAllRunsMonitors;
122         mPerRunMonitors = builder.mPerRunMonitors;
123         mBeforeAlls = builder.mBeforeAlls;
124         mBefores = builder.mBefores;
125         mTransitions = builder.mTransitions;
126         mAfters = builder.mAfters;
127         mAfterAlls = builder.mAfterAlls;
128 
129         mIterations = builder.mIterations;
130         mTestTag = builder.mTestTag;
131     }
132 
newBuilder(String outputDir)133     public static TransitionBuilder newBuilder(String outputDir) {
134         return new TransitionBuilder(Paths.get(outputDir));
135     }
136 
newBuilder()137     public static TransitionBuilder newBuilder() {
138         return new TransitionBuilder();
139     }
140 
141     /**
142      * Runs the composed transition and calls monitors at the appropriate stages. If jank monitor is
143      * enabled, transitions with jank are skipped.
144      *
145      * @return itself
146      */
run()147     public TransitionRunner run() {
148         mResults = new ArrayList<>();
149         mAllRunsMonitors.forEach(TransitionMonitor::start);
150         mBeforeAlls.forEach(Runnable::run);
151         for (int iteration = 0; iteration < mIterations; iteration++) {
152             mBefores.forEach(Runnable::run);
153             mPerRunMonitors.forEach(TransitionMonitor::start);
154             mTransitions.forEach(Runnable::run);
155             mPerRunMonitors.forEach(TransitionMonitor::stop);
156             mAfters.forEach(Runnable::run);
157             if (runJankFree() && mFrameStatsMonitor.jankyFramesDetected()) {
158                 String msg =
159                         String.format(
160                                 Locale.getDefault(),
161                                 "Skipping iteration %d/%d for test %s due to jank. %s",
162                                 iteration,
163                                 mIterations - 1,
164                                 mTestTag,
165                                 mFrameStatsMonitor.toString());
166                 Log.e(TAG, msg);
167                 continue;
168             }
169             mResults.add(saveResult(iteration));
170         }
171         mAfterAlls.forEach(Runnable::run);
172         mAllRunsMonitors.forEach(
173                 monitor -> {
174                     monitor.stop();
175                     monitor.save(mTestTag);
176                 });
177         return this;
178     }
179 
180     /**
181      * Returns a list of transition results.
182      *
183      * @return list of transition results.
184      */
getResults()185     public List<TransitionResult> getResults() {
186         if (mResults == null) {
187             throw new IllegalStateException("Results do not exist!");
188         }
189         return mResults;
190     }
191 
192     /**
193      * Deletes all transition results that are not marked for saving.
194      *
195      * @return list of transition results.
196      */
deleteResults()197     public void deleteResults() {
198         if (mResults == null) {
199             return;
200         }
201         mResults.stream().filter(TransitionResult::canDelete).forEach(TransitionResult::delete);
202         mResults = null;
203     }
204 
205     /**
206      * Saves monitor results to file.
207      *
208      * @return object containing paths to test artifacts
209      */
saveResult(int iteration)210     private TransitionResult saveResult(int iteration) {
211         Path windowTrace = null;
212         String windowTraceChecksum = "";
213         Path layerTrace = null;
214         String layerTraceChecksum = "";
215         Path screenCaptureVideo = null;
216         String screenCaptureVideoChecksum = "";
217 
218         if (mPerRunMonitors.contains(mWmTraceMonitor)) {
219             windowTrace = mWmTraceMonitor.save(mTestTag, iteration);
220             windowTraceChecksum = mWmTraceMonitor.getChecksum();
221         }
222         if (mPerRunMonitors.contains(mLayersTraceMonitor)) {
223             layerTrace = mLayersTraceMonitor.save(mTestTag, iteration);
224             layerTraceChecksum = mLayersTraceMonitor.getChecksum();
225         }
226         if (mPerRunMonitors.contains(mScreenRecorder)) {
227             screenCaptureVideo = mScreenRecorder.save(mTestTag, iteration);
228             screenCaptureVideoChecksum = mScreenRecorder.getChecksum();
229         }
230         return new TransitionResult(
231                 layerTrace,
232                 layerTraceChecksum,
233                 windowTrace,
234                 windowTraceChecksum,
235                 screenCaptureVideo,
236                 screenCaptureVideoChecksum);
237     }
238 
runJankFree()239     private boolean runJankFree() {
240         return mPerRunMonitors.contains(mFrameStatsMonitor);
241     }
242 
getTestTag()243     public String getTestTag() {
244         return mTestTag;
245     }
246 
247     /** Stores paths to all test artifacts. */
248     @VisibleForTesting
249     public static class TransitionResult {
250         @Nullable private final Path layersTrace;
251         private final String layersTraceChecksum;
252         @Nullable private final Path windowManagerTrace;
253         private final String windowManagerTraceChecksum;
254         @Nullable private final Path screenCaptureVideo;
255         private final String screenCaptureVideoChecksum;
256         private boolean flaggedForSaving = true;
257 
TransitionResult( @ullable Path layersTrace, String layersTraceChecksum, @Nullable Path windowManagerTrace, String windowManagerTraceChecksum, @Nullable Path screenCaptureVideo, String screenCaptureVideoChecksum)258         public TransitionResult(
259                 @Nullable Path layersTrace,
260                 String layersTraceChecksum,
261                 @Nullable Path windowManagerTrace,
262                 String windowManagerTraceChecksum,
263                 @Nullable Path screenCaptureVideo,
264                 String screenCaptureVideoChecksum) {
265             this.layersTrace = layersTrace;
266             this.layersTraceChecksum = layersTraceChecksum;
267             this.windowManagerTrace = windowManagerTrace;
268             this.windowManagerTraceChecksum = windowManagerTraceChecksum;
269             this.screenCaptureVideo = screenCaptureVideo;
270             this.screenCaptureVideoChecksum = screenCaptureVideoChecksum;
271         }
272 
flagForSaving()273         public void flagForSaving() {
274             flaggedForSaving = true;
275         }
276 
canDelete()277         public boolean canDelete() {
278             return !flaggedForSaving;
279         }
280 
layersTraceExists()281         public boolean layersTraceExists() {
282             return layersTrace != null && layersTrace.toFile().exists();
283         }
284 
getLayersTrace()285         public byte[] getLayersTrace() {
286             try {
287                 return Files.readAllBytes(this.layersTrace);
288             } catch (Exception e) {
289                 throw new RuntimeException(e);
290             }
291         }
292 
getLayersTracePath()293         public Path getLayersTracePath() {
294             return layersTrace;
295         }
296 
getLayersTraceChecksum()297         public String getLayersTraceChecksum() {
298             return layersTraceChecksum;
299         }
300 
windowManagerTraceExists()301         public boolean windowManagerTraceExists() {
302             return windowManagerTrace != null && windowManagerTrace.toFile().exists();
303         }
304 
getWindowManagerTrace()305         public byte[] getWindowManagerTrace() {
306             try {
307                 return Files.readAllBytes(this.windowManagerTrace);
308             } catch (Exception e) {
309                 throw new RuntimeException(e);
310             }
311         }
312 
getWindowManagerTracePath()313         public Path getWindowManagerTracePath() {
314             return windowManagerTrace;
315         }
316 
getWindowManagerTraceChecksum()317         public String getWindowManagerTraceChecksum() {
318             return windowManagerTraceChecksum;
319         }
320 
screenCaptureVideoExists()321         public boolean screenCaptureVideoExists() {
322             return screenCaptureVideo != null && screenCaptureVideo.toFile().exists();
323         }
324 
screenCaptureVideoPath()325         public Path screenCaptureVideoPath() {
326             return screenCaptureVideo;
327         }
328 
getScreenCaptureVideoChecksum()329         public String getScreenCaptureVideoChecksum() {
330             return screenCaptureVideoChecksum;
331         }
332 
delete()333         public void delete() {
334             if (layersTraceExists()) layersTrace.toFile().delete();
335             if (windowManagerTraceExists()) windowManagerTrace.toFile().delete();
336             if (screenCaptureVideoExists()) screenCaptureVideo.toFile().delete();
337         }
338     }
339 
340     /** Builds a {@link TransitionRunner} instance. */
341     public static class TransitionBuilder {
342         private ScreenRecorder mScreenRecorder;
343         private WindowManagerTraceMonitor mWmTraceMonitor;
344         private LayersTraceMonitor mLayersTraceMonitor;
345         private WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
346 
347         private List<TransitionMonitor> mAllRunsMonitors = new LinkedList<>();
348         private List<TransitionMonitor> mPerRunMonitors = new LinkedList<>();
349         private List<Runnable> mBeforeAlls = new LinkedList<>();
350         private List<Runnable> mBefores = new LinkedList<>();
351         private List<Runnable> mTransitions = new LinkedList<>();
352         private List<Runnable> mAfters = new LinkedList<>();
353         private List<Runnable> mAfterAlls = new LinkedList<>();
354 
355         private boolean mRunJankFree = true;
356         private boolean mCaptureWindowManagerTrace = true;
357         private boolean mCaptureLayersTrace = true;
358         private boolean mRecordEachRun = false;
359         private int mIterations = 1;
360         private String mTestTag = "";
361 
362         private boolean mRecordAllRuns = false;
363 
TransitionBuilder(@onNull Path outputDir)364         private TransitionBuilder(@NonNull Path outputDir) {
365             mScreenRecorder = new ScreenRecorder();
366             mWmTraceMonitor = new WindowManagerTraceMonitor(outputDir);
367             mLayersTraceMonitor = new LayersTraceMonitor(outputDir);
368             mFrameStatsMonitor =
369                     new WindowAnimationFrameStatsMonitor(
370                             InstrumentationRegistry.getInstrumentation());
371         }
372 
TransitionBuilder()373         public TransitionBuilder() {
374             this(OUTPUT_DIR);
375         }
376 
build()377         public TransitionRunner build() {
378             if (mCaptureWindowManagerTrace) {
379                 mPerRunMonitors.add(mWmTraceMonitor);
380             }
381 
382             if (mCaptureLayersTrace) {
383                 mPerRunMonitors.add(mLayersTraceMonitor);
384             }
385 
386             if (mRunJankFree) {
387                 mPerRunMonitors.add(mFrameStatsMonitor);
388             }
389 
390             if (mRecordAllRuns) {
391                 mAllRunsMonitors.add(mScreenRecorder);
392             }
393 
394             if (mRecordEachRun) {
395                 mPerRunMonitors.add(mScreenRecorder);
396             }
397 
398             return new TransitionRunner(this);
399         }
400 
runBeforeAll(Runnable runnable)401         public TransitionBuilder runBeforeAll(Runnable runnable) {
402             mBeforeAlls.add(runnable);
403             return this;
404         }
405 
runBefore(Runnable runnable)406         public TransitionBuilder runBefore(Runnable runnable) {
407             mBefores.add(runnable);
408             return this;
409         }
410 
run(Runnable runnable)411         public TransitionBuilder run(Runnable runnable) {
412             mTransitions.add(runnable);
413             return this;
414         }
415 
runAfter(Runnable runnable)416         public TransitionBuilder runAfter(Runnable runnable) {
417             mAfters.add(runnable);
418             return this;
419         }
420 
runAfterAll(Runnable runnable)421         public TransitionBuilder runAfterAll(Runnable runnable) {
422             mAfterAlls.add(runnable);
423             return this;
424         }
425 
repeat(int iterations)426         public TransitionBuilder repeat(int iterations) {
427             mIterations = iterations;
428             return this;
429         }
430 
skipWindowManagerTrace()431         public TransitionBuilder skipWindowManagerTrace() {
432             mCaptureWindowManagerTrace = false;
433             return this;
434         }
435 
skipLayersTrace()436         public TransitionBuilder skipLayersTrace() {
437             mCaptureLayersTrace = false;
438             return this;
439         }
440 
includeJankyRuns()441         public TransitionBuilder includeJankyRuns() {
442             mRunJankFree = false;
443             return this;
444         }
445 
recordEachRun()446         public TransitionBuilder recordEachRun() {
447             if (mRecordAllRuns) {
448                 throw new IllegalArgumentException("Invalid option with recordAllRuns");
449             }
450             mRecordEachRun = true;
451             return this;
452         }
453 
recordAllRuns()454         public TransitionBuilder recordAllRuns() {
455             if (mRecordEachRun) {
456                 throw new IllegalArgumentException("Invalid option with recordEachRun");
457             }
458             mRecordAllRuns = true;
459             return this;
460         }
461 
withTag(String testTag)462         public TransitionBuilder withTag(String testTag) {
463             if (testTag.contains(" ")) {
464                 throw new IllegalArgumentException(
465                         "The test tag can not contain spaces since it "
466                                 + "is a part of the file name");
467             }
468             mTestTag = testTag;
469             return this;
470         }
471     }
472 }
473