1 /*
2  * Copyright (C) 2015 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.media.tests;
18 
19 import com.android.tradefed.config.Option;
20 import com.android.tradefed.config.OptionClass;
21 import com.android.tradefed.device.DeviceNotAvailableException;
22 import com.android.tradefed.invoker.TestInformation;
23 import com.android.tradefed.log.LogUtil.CLog;
24 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
25 import com.android.tradefed.result.ITestInvocationListener;
26 import com.android.tradefed.result.TestDescription;
27 import com.android.tradefed.targetprep.BuildError;
28 import com.android.tradefed.targetprep.ITargetPreparer;
29 import com.android.tradefed.targetprep.TargetSetupError;
30 import com.android.tradefed.targetprep.TemperatureThrottlingWaiter;
31 import com.android.tradefed.util.MultiMap;
32 import com.android.tradefed.util.proto.TfMetricProtoUtil;
33 
34 import org.junit.Assert;
35 
36 import java.util.HashMap;
37 import java.util.Map;
38 import java.util.Set;
39 import java.util.regex.Matcher;
40 import java.util.regex.Pattern;
41 
42 /**
43  * Camera app startup test
44  *
45  * Runs CameraActivityTest to measure Camera startup time and reports the metrics.
46  */
47 @OptionClass(alias = "camera-startup")
48 public class CameraStartupTest extends CameraTestBase {
49 
50     private static final Pattern STATS_REGEX = Pattern.compile(
51         "^(?<coldStartup>[0-9.]+)\\|(?<warmStartup>[0-9.]+)\\|(?<values>[0-9 .-]+)");
52     private static final String PREFIX_COLD_STARTUP = "Cold";
53     // all metrics are expected to be less than 10 mins and greater than 0.
54     private static final int METRICS_MAX_THRESHOLD_MS = 10 * 60 * 1000;
55     private static final int METRICS_MIN_THRESHOLD_MS = 0;
56     private static final String INVALID_VALUE = "-1";
57 
58     @Option(name="num-test-runs", description="The number of test runs. A instrumentation "
59             + "test will be repeatedly executed. Then it posts the average of test results.")
60     private int mNumTestRuns = 1;
61 
62     @Option(name="delay-between-test-runs", description="Time delay between multiple test runs, "
63             + "in msecs. Used to wait for device to cool down. "
64             + "Note that this will be ignored when TemperatureThrottlingWaiter is configured.")
65     private long mDelayBetweenTestRunsMs = 120 * 1000;  // 2 minutes
66 
67     private MultiMap<String, String> mMultipleRunMetrics = new MultiMap<String, String>();
68     private Map<String, String> mAverageMultipleRunMetrics = new HashMap<String, String>();
69     private long mTestRunsDurationMs = 0;
70 
CameraStartupTest()71     public CameraStartupTest() {
72         setTestPackage("com.google.android.camera");
73         setTestClass("com.android.camera.latency.CameraStartupTest");
74         setTestRunner("android.test.InstrumentationTestRunner");
75         setRuKey("CameraAppStartup");
76         setTestTimeoutMs(60 * 60 * 1000);   // 1 hour
77     }
78 
79     /** {@inheritDoc} */
80     @Override
run(TestInformation testInfo, ITestInvocationListener listener)81     public void run(TestInformation testInfo, ITestInvocationListener listener)
82             throws DeviceNotAvailableException {
83         runMultipleInstrumentationTests(testInfo, listener, mNumTestRuns);
84     }
85 
runMultipleInstrumentationTests( TestInformation testInfo, ITestInvocationListener listener, int numTestRuns)86     private void runMultipleInstrumentationTests(
87             TestInformation testInfo, ITestInvocationListener listener, int numTestRuns)
88             throws DeviceNotAvailableException {
89         Assert.assertTrue(numTestRuns > 0);
90 
91         mTestRunsDurationMs = 0;
92         for (int i = 0; i < numTestRuns; ++i) {
93             CLog.v("Running multiple instrumentation tests... [%d/%d]", i + 1, numTestRuns);
94             CollectingListener singleRunListener = new CollectingListener(listener);
95             runInstrumentationTest(testInfo, listener, singleRunListener);
96             mTestRunsDurationMs += getTestDurationMs();
97 
98             if (singleRunListener.hasFailedTests() ||
99                     singleRunListener.hasTestRunFatalError()) {
100                 exitTestRunsOnError(listener, singleRunListener.getErrorMessage());
101                 return;
102             }
103             if (i + 1 < numTestRuns) {  // Skipping preparation on the last run
104                 postSetupTestRun(testInfo);
105             }
106         }
107 
108         // Post the average of metrics collected in multiple instrumentation test runs.
109         postMultipleRunMetrics(listener);
110         CLog.v("multiple instrumentation tests end");
111     }
112 
exitTestRunsOnError(ITestInvocationListener listener, String errorMessage)113     private void exitTestRunsOnError(ITestInvocationListener listener, String errorMessage) {
114         CLog.e("The instrumentation result not found. Test runs may have failed due to exceptions."
115                 + " Test results will not be posted. errorMsg: %s", errorMessage);
116         listener.testRunFailed(errorMessage);
117         listener.testRunEnded(mTestRunsDurationMs, new HashMap<String, Metric>());
118     }
119 
postMultipleRunMetrics(ITestInvocationListener listener)120     private void postMultipleRunMetrics(ITestInvocationListener listener) {
121         listener.testRunEnded(mTestRunsDurationMs,
122                 TfMetricProtoUtil.upgradeConvert(getAverageMultipleRunMetrics()));
123     }
124 
postSetupTestRun(TestInformation testInfo)125     private void postSetupTestRun(TestInformation testInfo) throws DeviceNotAvailableException {
126         // Reboot for a cold start up of Camera application
127         CLog.d("Cold start: Rebooting...");
128         getDevice().reboot();
129 
130         // Wait for device to cool down to target temperature
131         // Use TemperatureThrottlingWaiter if configured, otherwise just wait for
132         // a specific amount of time.
133         CLog.d("Cold start: Waiting for device to cool down...");
134         boolean usedTemperatureThrottlingWaiter = false;
135         for (ITargetPreparer preparer : mConfiguration.getTargetPreparers()) {
136             if (preparer instanceof TemperatureThrottlingWaiter) {
137                 usedTemperatureThrottlingWaiter = true;
138                 try {
139                     preparer.setUp(testInfo);
140                 } catch (TargetSetupError e) {
141                     CLog.w("No-op even when temperature is still high after wait timeout. "
142                         + "error: %s", e.getMessage());
143                 } catch (BuildError e) {
144                     // This should not happen.
145                 }
146             }
147         }
148         if (!usedTemperatureThrottlingWaiter) {
149             getRunUtil().sleep(mDelayBetweenTestRunsMs);
150         }
151         CLog.d("Device gets prepared for the next test run.");
152     }
153 
154     // Call this function once at the end to get the average.
getAverageMultipleRunMetrics()155     private Map<String, String> getAverageMultipleRunMetrics() {
156         Assert.assertTrue(mMultipleRunMetrics.size() > 0);
157 
158         Set<String> keys = mMultipleRunMetrics.keySet();
159         mAverageMultipleRunMetrics.clear();
160         for (String key : keys) {
161             int sum = 0;
162             int size = 0;
163             boolean isInvalid = false;
164             for (String valueString : mMultipleRunMetrics.get(key)) {
165                 int value = Integer.parseInt(valueString);
166                 // If value is out of valid range, skip posting the result associated with the key
167                 if (value > METRICS_MAX_THRESHOLD_MS || value < METRICS_MIN_THRESHOLD_MS) {
168                     isInvalid = true;
169                     break;
170                 }
171                 sum += value;
172                 ++size;
173             }
174 
175             String valueString = INVALID_VALUE;
176             if (isInvalid) {
177                 CLog.w("Value is out of valid range. Key: %s ", key);
178             } else {
179                 valueString = String.format("%d", (sum / size));
180             }
181             mAverageMultipleRunMetrics.put(key, valueString);
182         }
183         return mAverageMultipleRunMetrics;
184     }
185 
186     /**
187      * A listener to collect the output from test run and fatal errors
188      */
189     private class CollectingListener extends CameraTestMetricsCollectionListener.DefaultCollectingListener {
190 
CollectingListener(ITestInvocationListener listener)191         public CollectingListener(ITestInvocationListener listener) {
192             super(listener);
193         }
194 
195         @Override
handleMetricsOnTestEnded(TestDescription test, Map<String, String> testMetrics)196         public void handleMetricsOnTestEnded(TestDescription test, Map<String, String> testMetrics) {
197             // Test metrics accumulated will be posted at the end of test run.
198             getAggregatedMetrics().putAll(parseResults(testMetrics));
199         }
200 
201         @Override
handleTestRunEnded(ITestInvocationListener listener, long elapsedTime, Map<String, String> runMetrics)202         public void handleTestRunEnded(ITestInvocationListener listener, long elapsedTime,
203                 Map<String, String> runMetrics) {
204             // Do not post aggregated metrics from a single run to a dashboard. Instead, it needs
205             // to collect all metrics from multiple test runs.
206             mMultipleRunMetrics.putAll(getAggregatedMetrics());
207         }
208 
parseResults(Map<String, String> testMetrics)209         public Map<String, String> parseResults(Map<String, String> testMetrics) {
210             // Parse activity time stats from the instrumentation result.
211             // Format : <metric_key>=<cold_startup>|<average_of_warm_startups>|<all_startups>
212             // Example:
213             // VideoStartupTimeMs=1098|1184.6|1098 1222 ... 788
214             // VideoOnCreateTimeMs=138|103.3|138 114 ... 114
215             // VideoOnResumeTimeMs=39|40.4|39 36 ... 41
216             // VideoFirstPreviewFrameTimeMs=0|0.0|0 0 ... 0
217             // CameraStartupTimeMs=2388|1045.4|2388 1109 ... 746
218             // CameraOnCreateTimeMs=574|122.7|574 124 ... 109
219             // CameraOnResumeTimeMs=610|504.6|610 543 ... 278
220             // CameraFirstPreviewFrameTimeMs=0|0.0|0 0 ... 0
221             //
222             // Then report only the first two startup time of cold startup and average warm startup.
223             Map<String, String> parsed = new HashMap<String, String>();
224             for (Map.Entry<String, String> metric : testMetrics.entrySet()) {
225                 Matcher matcher = STATS_REGEX.matcher(metric.getValue());
226                 String keyName = metric.getKey();
227                 String coldStartupValue = INVALID_VALUE;
228                 String warmStartupValue = INVALID_VALUE;
229                 if (matcher.matches()) {
230                     coldStartupValue = matcher.group("coldStartup");
231                     warmStartupValue = matcher.group("warmStartup");
232                 }
233                 parsed.put(PREFIX_COLD_STARTUP + keyName, coldStartupValue);
234                 parsed.put(keyName, warmStartupValue);
235             }
236             return parsed;
237         }
238     }
239 }
240