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.OptionClass;
20 import com.android.tradefed.device.DeviceNotAvailableException;
21 import com.android.tradefed.invoker.TestInformation;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.result.ITestInvocationListener;
24 import com.android.tradefed.result.TestDescription;
25 import com.android.tradefed.util.FileUtil;
26 import com.android.tradefed.util.proto.TfMetricProtoUtil;
27 
28 import com.google.common.collect.ImmutableMap;
29 import com.google.common.collect.ImmutableMultimap;
30 
31 import org.json.JSONArray;
32 import org.json.JSONException;
33 import org.json.JSONObject;
34 import org.xmlpull.v1.XmlPullParser;
35 import org.xmlpull.v1.XmlPullParserException;
36 import org.xmlpull.v1.XmlPullParserFactory;
37 
38 import java.io.ByteArrayInputStream;
39 import java.io.File;
40 import java.io.IOException;
41 import java.util.ArrayList;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47 
48 /**
49  * This test invocation runs android.hardware.camera2.cts.PerformanceTest - Camera2 API use case
50  * performance KPIs (Key Performance Indicator), such as camera open time, session creation time,
51  * shutter lag etc. The KPI data will be parsed and reported.
52  */
53 @OptionClass(alias = "camera-framework")
54 public class CameraPerformanceTest extends CameraTestBase {
55 
56     private static final String TEST_CAMERA_LAUNCH = "testCameraLaunch";
57     private static final String TEST_SINGLE_CAPTURE = "testSingleCapture";
58     private static final String TEST_REPROCESSING_LATENCY = "testReprocessingLatency";
59     private static final String TEST_REPROCESSING_THROUGHPUT = "testReprocessingThroughput";
60 
61     // KPIs to be reported. The key is test methods and the value is KPIs in the method.
62     private final ImmutableMultimap<String, String> mReportingKpis =
63             new ImmutableMultimap.Builder<String, String>()
64                     .put(TEST_CAMERA_LAUNCH, "Camera launch time")
65                     .put(TEST_CAMERA_LAUNCH, "Camera start preview time")
66                     .put(TEST_SINGLE_CAPTURE, "Camera capture result latency")
67                     .put(TEST_REPROCESSING_LATENCY, "YUV reprocessing shot to shot latency")
68                     .put(TEST_REPROCESSING_LATENCY, "opaque reprocessing shot to shot latency")
69                     .put(TEST_REPROCESSING_THROUGHPUT, "YUV reprocessing capture latency")
70                     .put(TEST_REPROCESSING_THROUGHPUT, "opaque reprocessing capture latency")
71                     .build();
72 
73     // JSON format keymap, key is test method name and the value is stream name in Json file
74     private static final ImmutableMap<String, String> METHOD_JSON_KEY_MAP =
75             new ImmutableMap.Builder<String, String>()
76                     .put(TEST_CAMERA_LAUNCH, "test_camera_launch")
77                     .put(TEST_SINGLE_CAPTURE, "test_single_capture")
78                     .put(TEST_REPROCESSING_LATENCY, "test_reprocessing_latency")
79                     .put(TEST_REPROCESSING_THROUGHPUT, "test_reprocessing_throughput")
80                     .build();
81 
getAverage(List<E> list)82     private <E extends Number> double getAverage(List<E> list) {
83         double sum = 0;
84         int size = list.size();
85         for (E num : list) {
86             sum += num.doubleValue();
87         }
88         if (size == 0) {
89             return 0.0;
90         }
91         return (sum / size);
92     }
93 
CameraPerformanceTest()94     public CameraPerformanceTest() {
95         // Set up the default test info. But this is subject to be overwritten by options passed
96         // from commands.
97         setTestPackage("android.camera.cts");
98         setTestClass("android.hardware.camera2.cts.PerformanceTest");
99         setTestRunner("androidx.test.runner.AndroidJUnitRunner");
100         setRuKey("camera_framework_performance");
101         setTestTimeoutMs(10 * 60 * 1000); // 10 mins
102         setIsolatedStorageFlag(false);
103     }
104 
105     /** {@inheritDoc} */
106     @Override
run(TestInformation testInfo, ITestInvocationListener listener)107     public void run(TestInformation testInfo, ITestInvocationListener listener)
108             throws DeviceNotAvailableException {
109         runInstrumentationTest(testInfo, listener, new CollectingListener(listener));
110     }
111 
112     /**
113      * A listener to collect the output from test run and fatal errors
114      */
115     private class CollectingListener extends CameraTestMetricsCollectionListener.DefaultCollectingListener {
116 
CollectingListener(ITestInvocationListener listener)117         public CollectingListener(ITestInvocationListener listener) {
118             super(listener);
119         }
120 
121         @Override
handleMetricsOnTestEnded(TestDescription test, Map<String, String> testMetrics)122         public void handleMetricsOnTestEnded(TestDescription test, Map<String, String> testMetrics) {
123             // Pass the test name for a key in the aggregated metrics, because
124             // it is used to generate the key of the final metrics to post at the end of test run.
125             for (Map.Entry<String, String> metric : testMetrics.entrySet()) {
126                 getAggregatedMetrics().put(test.getTestName(), metric.getValue());
127             }
128         }
129 
130         @Override
handleTestRunEnded( ITestInvocationListener listener, long elapsedTime, Map<String, String> runMetrics)131         public void handleTestRunEnded(
132                 ITestInvocationListener listener,
133                 long elapsedTime,
134                 Map<String, String> runMetrics) {
135             // Report metrics at the end of test run.
136             Map<String, String> result = parseResult(getAggregatedMetrics());
137             listener.testRunEnded(getTestDurationMs(), TfMetricProtoUtil.upgradeConvert(result));
138         }
139     }
140 
141     /**
142      * Parse Camera Performance KPIs results and then put them all together to post the final
143      * report.
144      *
145      * @return a {@link HashMap} that contains pairs of kpiName and kpiValue
146      */
parseResult(Map<String, String> metrics)147     private Map<String, String> parseResult(Map<String, String> metrics) {
148 
149         // if json report exists, return the parse results
150         CtsJsonResultParser ctsJsonResultParser = new CtsJsonResultParser();
151 
152         if (ctsJsonResultParser.isJsonFileExist()) {
153             return ctsJsonResultParser.parse();
154         }
155 
156         Map<String, String> resultsAll = new HashMap<String, String>();
157 
158         CtsResultParserBase parser;
159         for (Map.Entry<String, String> metric : metrics.entrySet()) {
160             String testMethod = metric.getKey();
161             String testResult = metric.getValue();
162             CLog.d("test name %s", testMethod);
163             CLog.d("test result %s", testResult);
164             // Probe which result parser should be used.
165             if (shouldUseCtsXmlResultParser(testResult)) {
166                 parser = new CtsXmlResultParser();
167             } else {
168                 parser = new CtsDelimitedResultParser();
169             }
170 
171             // Get pairs of { KPI name, KPI value } from stdout that each test outputs.
172             // Assuming that a device has both the front and back cameras, parser will return
173             // 2 KPIs in HashMap. For an example of testCameraLaunch,
174             //   {
175             //     ("Camera 0 Camera launch time", "379.2"),
176             //     ("Camera 1 Camera launch time", "272.8"),
177             //   }
178             Map<String, String> testKpis = parser.parse(testResult, testMethod);
179             for (String k : testKpis.keySet()) {
180                 if (resultsAll.containsKey(k)) {
181                     throw new RuntimeException(
182                             String.format("KPI name (%s) conflicts with the existing names.", k));
183                 }
184             }
185             parser.clear();
186 
187             // Put each result together to post the final result
188             resultsAll.putAll(testKpis);
189         }
190         return resultsAll;
191     }
192 
shouldUseCtsXmlResultParser(String result)193     public boolean shouldUseCtsXmlResultParser(String result) {
194         final String XML_DECLARATION = "<?xml";
195         return (result.startsWith(XML_DECLARATION)
196                 || result.startsWith(XML_DECLARATION.toUpperCase()));
197     }
198 
199     /** Data class of CTS test results for Camera framework performance test */
200     public static class CtsMetric {
201         String testMethod;  // "testSingleCapture"
202         String source;      // "android.hardware.camera2.cts.PerformanceTest#testSingleCapture:327"
203         // or "testSingleCapture" (just test method name)
204         String message; // "Camera 0: Camera capture latency"
205         String type; // "lower_better"
206         String unit;        // "ms"
207         String value;       // "691.0" (is an average of 736.0 688.0 679.0 667.0 686.0)
208         String schemaKey;   // RU schema key = message (+ testMethodName if needed), derived
209 
210         // eg. "android.hardware.camera2.cts.PerformanceTest#testSingleCapture:327"
211         public static final Pattern SOURCE_REGEX =
212                 Pattern.compile("^(?<package>[a-zA-Z\\d\\._$]+)#(?<method>[a-zA-Z\\d_$]+)(:\\d+)?");
213         // eg. "Camera 0: Camera capture latency"
214         public static final Pattern MESSAGE_REGEX =
215                 Pattern.compile("^Camera\\s+(?<cameraId>\\d+):\\s+(?<kpiName>.*)");
216 
CtsMetric( String testMethod, String source, String message, String type, String unit, String value)217         CtsMetric(
218                 String testMethod,
219                 String source,
220                 String message,
221                 String type,
222                 String unit,
223                 String value) {
224             this.testMethod = testMethod;
225             this.source = source;
226             this.message = message;
227             this.type = type;
228             this.unit = unit;
229             this.value = value;
230             this.schemaKey = getRuSchemaKeyName(message);
231         }
232 
matches(String testMethod, String kpiName)233         public boolean matches(String testMethod, String kpiName) {
234             return (this.testMethod.equals(testMethod) && this.message.endsWith(kpiName));
235         }
236 
getRuSchemaKeyName(String message)237         public String getRuSchemaKeyName(String message) {
238             // Note 1: The key shouldn't contain ":" for side by side report.
239             String schemaKey = message.replace(":", "");
240             // Note 2: Two tests testReprocessingLatency & testReprocessingThroughput have the
241             // same metric names to report results. To make the report key name distinct,
242             // the test name is added as prefix for these tests for them.
243             final String[] TEST_NAMES_AS_PREFIX = {
244                 "testReprocessingLatency", "testReprocessingThroughput"
245             };
246             for (String testName : TEST_NAMES_AS_PREFIX) {
247                 if (testMethod.endsWith(testName)) {
248                     schemaKey = String.format("%s_%s", testName, schemaKey);
249                     break;
250                 }
251             }
252             return schemaKey;
253         }
254 
getTestMethodNameInSource(String source)255         public String getTestMethodNameInSource(String source) {
256             Matcher m = SOURCE_REGEX.matcher(source);
257             if (!m.matches()) {
258                 return source;
259             }
260             return m.group("method");
261         }
262     }
263 
264     /**
265      * Base class of CTS test result parser. This is inherited to two derived parsers,
266      * {@link CtsDelimitedResultParser} for legacy delimiter separated format and
267      * {@link CtsXmlResultParser} for XML typed format introduced since NYC.
268      */
269     public abstract class CtsResultParserBase {
270 
271         protected CtsMetric mSummary;
272         protected List<CtsMetric> mDetails = new ArrayList<>();
273 
274         /**
275          * Parse Camera Performance KPIs result first, then leave the only KPIs that matter.
276          *
277          * @param result String to be parsed
278          * @param testMethod test method name used to leave the only metric that matters
279          * @return a {@link HashMap} that contains kpiName and kpiValue
280          */
parse(String result, String testMethod)281         public abstract Map<String, String> parse(String result, String testMethod);
282 
filter(List<CtsMetric> metrics, String testMethod)283         protected Map<String, String> filter(List<CtsMetric> metrics, String testMethod) {
284             Map<String, String> filtered = new HashMap<String, String>();
285             for (CtsMetric metric : metrics) {
286                 for (String kpiName : mReportingKpis.get(testMethod)) {
287                     // Post the data only when it matches with the given methods and KPI names.
288                     if (metric.matches(testMethod, kpiName)) {
289                         filtered.put(metric.schemaKey, metric.value);
290                     }
291                 }
292             }
293             return filtered;
294         }
295 
setSummary(CtsMetric summary)296         protected void setSummary(CtsMetric summary) {
297             mSummary = summary;
298         }
299 
addDetail(CtsMetric detail)300         protected void addDetail(CtsMetric detail) {
301             mDetails.add(detail);
302         }
303 
getDetails()304         protected List<CtsMetric> getDetails() {
305             return mDetails;
306         }
307 
clear()308         void clear() {
309             mSummary = null;
310             mDetails.clear();
311         }
312     }
313 
314     /**
315      * Parses the camera performance test generated by the underlying instrumentation test and
316      * returns it to test runner for later reporting.
317      *
318      * <p>TODO(liuyg): Rename this class to not reference CTS.
319      *
320      * <p>Format: (summary message)| |(type)|(unit)|(value) ++++
321      * (source)|(message)|(type)|(unit)|(value)... +++ ...
322      *
323      * <p>Example: Camera launch average time for Camera 1| |lower_better|ms|586.6++++
324      * android.hardware.camera2.cts.PerformanceTest#testCameraLaunch:171| Camera 0: Camera open
325      * time|lower_better|ms|74.0 100.0 70.0 67.0 82.0 +++
326      * android.hardware.camera2.cts.PerformanceTest#testCameraLaunch:171| Camera 0: Camera configure
327      * stream time|lower_better|ms|9.0 5.0 5.0 8.0 5.0 ...
328      *
329      * <p>See also com.android.cts.util.ReportLog for the format detail.
330      */
331     public class CtsDelimitedResultParser extends CtsResultParserBase {
332         private static final String LOG_SEPARATOR = "\\+\\+\\+";
333         private static final String SUMMARY_SEPARATOR = "\\+\\+\\+\\+";
334         private final Pattern mSummaryRegex =
335                 Pattern.compile(
336                         "^(?<message>[^|]+)\\| \\|(?<type>[^|]+)\\|(?<unit>[^|]+)\\|(?<value>[0-9 .]+)");
337         private final Pattern mDetailRegex =
338                 Pattern.compile(
339                         "^(?<source>[^|]+)\\|(?<message>[^|]+)\\|(?<type>[^|]+)\\|(?<unit>[^|]+)\\|"
340                                 + "(?<values>[0-9 .]+)");
341 
342         @Override
parse(String result, String testMethod)343         public Map<String, String> parse(String result, String testMethod) {
344             parseToCtsMetrics(result, testMethod);
345             parseToCtsMetrics(result, testMethod);
346             return filter(getDetails(), testMethod);
347         }
348 
parseToCtsMetrics(String result, String testMethod)349         void parseToCtsMetrics(String result, String testMethod) {
350             // Split summary and KPIs from stdout passes as parameter.
351             String[] output = result.split(SUMMARY_SEPARATOR);
352             if (output.length != 2) {
353                 throw new RuntimeException("Value not in the correct format");
354             }
355             Matcher summaryMatcher = mSummaryRegex.matcher(output[0].trim());
356 
357             // Parse summary.
358             // Example: "Camera launch average time for Camera 1| |lower_better|ms|586.6++++"
359             if (summaryMatcher.matches()) {
360                 setSummary(
361                         new CtsMetric(
362                                 testMethod,
363                                 null,
364                                 summaryMatcher.group("message"),
365                                 summaryMatcher.group("type"),
366                                 summaryMatcher.group("unit"),
367                                 summaryMatcher.group("value")));
368             } else {
369                 // Fall through since the summary is not posted as results.
370                 CLog.w("Summary not in the correct format");
371             }
372 
373             // Parse KPIs.
374             // Example: "android.hardware.camera2.cts.PerformanceTest#testCameraLaunch:171|Camera 0:
375             // Camera open time|lower_better|ms|74.0 100.0 70.0 67.0 82.0 +++"
376             String[] details = output[1].split(LOG_SEPARATOR);
377             for (String detail : details) {
378                 Matcher detailMatcher = mDetailRegex.matcher(detail.trim());
379                 if (detailMatcher.matches()) {
380                     // get average of kpi values
381                     List<Double> values = new ArrayList<>();
382                     for (String value : detailMatcher.group("values").split("\\s+")) {
383                         values.add(Double.parseDouble(value));
384                     }
385                     String kpiValue = String.format("%.1f", getAverage(values));
386                     addDetail(
387                             new CtsMetric(
388                                     testMethod,
389                                     detailMatcher.group("source"),
390                                     detailMatcher.group("message"),
391                                     detailMatcher.group("type"),
392                                     detailMatcher.group("unit"),
393                                     kpiValue));
394                 } else {
395                     throw new RuntimeException("KPI not in the correct format");
396                 }
397             }
398         }
399     }
400 
401     /**
402      * Parses the CTS test results in a XML format introduced since NYC.
403      * Format:
404      *   <Summary>
405      *       <Metric source="android.hardware.camera2.cts.PerformanceTest#testSingleCapture:327"
406      *               message="Camera capture result average latency for all cameras "
407      *               score_type="lower_better"
408      *               score_unit="ms"
409      *           <Value>353.9</Value>
410      *       </Metric>
411      *   </Summary>
412      *   <Detail>
413      *       <Metric source="android.hardware.camera2.cts.PerformanceTest#testSingleCapture:303"
414      *               message="Camera 0: Camera capture latency"
415      *               score_type="lower_better"
416      *               score_unit="ms">
417      *           <Value>335.0</Value>
418      *           <Value>302.0</Value>
419      *           <Value>316.0</Value>
420      *       </Metric>
421      *   </Detail>
422      *  }
423      * See also com.android.compatibility.common.util.ReportLog for the format detail.
424      */
425     public class CtsXmlResultParser extends CtsResultParserBase {
426         private static final String ENCODING = "UTF-8";
427         // XML constants
428         private static final String DETAIL_TAG = "Detail";
429         private static final String METRIC_TAG = "Metric";
430         private static final String MESSAGE_ATTR = "message";
431         private static final String SCORETYPE_ATTR = "score_type";
432         private static final String SCOREUNIT_ATTR = "score_unit";
433         private static final String SOURCE_ATTR = "source";
434         private static final String SUMMARY_TAG = "Summary";
435         private static final String VALUE_TAG = "Value";
436         private String mTestMethod;
437 
438         @Override
parse(String result, String testMethod)439         public Map<String, String> parse(String result, String testMethod) {
440             try {
441                 mTestMethod = testMethod;
442                 XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
443                 XmlPullParser parser = factory.newPullParser();
444                 parser.setInput(new ByteArrayInputStream(result.getBytes(ENCODING)), ENCODING);
445                 parser.nextTag();
446                 parse(parser);
447                 return filter(getDetails(), testMethod);
448             } catch (XmlPullParserException | IOException e) {
449                 throw new RuntimeException("Failed to parse results in XML.", e);
450             }
451         }
452 
453         /**
454          * Parses a {@link CtsMetric} from the given XML parser.
455          *
456          * @param parser
457          * @throws IOException
458          * @throws XmlPullParserException
459          */
parse(XmlPullParser parser)460         private void parse(XmlPullParser parser) throws XmlPullParserException, IOException {
461             parser.require(XmlPullParser.START_TAG, null, SUMMARY_TAG);
462             parser.nextTag();
463             setSummary(parseToCtsMetrics(parser));
464             parser.nextTag();
465             parser.require(XmlPullParser.END_TAG, null, SUMMARY_TAG);
466             parser.nextTag();
467             if (parser.getName().equals(DETAIL_TAG)) {
468                 while (parser.nextTag() == XmlPullParser.START_TAG) {
469                     addDetail(parseToCtsMetrics(parser));
470                 }
471                 parser.require(XmlPullParser.END_TAG, null, DETAIL_TAG);
472             }
473         }
474 
parseToCtsMetrics(XmlPullParser parser)475         CtsMetric parseToCtsMetrics(XmlPullParser parser)
476                 throws IOException, XmlPullParserException {
477             parser.require(XmlPullParser.START_TAG, null, METRIC_TAG);
478             String source = parser.getAttributeValue(null, SOURCE_ATTR);
479             String message = parser.getAttributeValue(null, MESSAGE_ATTR);
480             String type = parser.getAttributeValue(null, SCORETYPE_ATTR);
481             String unit = parser.getAttributeValue(null, SCOREUNIT_ATTR);
482             List<Double> values = new ArrayList<>();
483             while (parser.nextTag() == XmlPullParser.START_TAG) {
484                 parser.require(XmlPullParser.START_TAG, null, VALUE_TAG);
485                 values.add(Double.parseDouble(parser.nextText()));
486                 parser.require(XmlPullParser.END_TAG, null, VALUE_TAG);
487             }
488             String kpiValue = String.format("%.1f", getAverage(values));
489             parser.require(XmlPullParser.END_TAG, null, METRIC_TAG);
490             return new CtsMetric(mTestMethod, source, message, type, unit, kpiValue);
491         }
492     }
493 
494     /*
495      * Parse the Json report from the Json String
496      * "test_single_capture":
497      * {"camera_id":"0","camera_capture_latency":[264.0,229.0,229.0,237.0,234.0],
498      * "camera_capture_result_latency":[230.0,197.0,196.0,204.0,202.0]},"
499      * "test_reprocessing_latency":
500      * {"camera_id":"0","format":35,"reprocess_type":"YUV reprocessing",
501      * "capture_message":"shot to shot latency","latency":[102.0,101.0,99.0,99.0,100.0,101.0],
502      * "camera_reprocessing_shot_to_shot_average_latency":100.33333333333333},
503      *
504      * TODO: move this to a seperate class
505      */
506     public class CtsJsonResultParser {
507 
508         // report json file set in
509         // cts/tools/cts-tradefed/res/config/cts-preconditions.xml
510         private static final String JSON_RESULT_FILE =
511                 "/sdcard/report-log-files/CtsCameraTestCases.reportlog.json";
512         private static final String CAMERA_ID_KEY = "camera_id";
513         private static final String REPROCESS_TYPE_KEY = "reprocess_type";
514         private static final String CAPTURE_MESSAGE_KEY = "capture_message";
515         private static final String LATENCY_KEY = "latency";
516 
parse()517         public Map<String, String> parse() {
518 
519             Map<String, String> metrics = new HashMap<>();
520 
521             String jsonString = getFormatedJsonReportFromFile();
522             if (null == jsonString) {
523                 throw new RuntimeException("Get null json report string.");
524             }
525 
526             Map<String, List<Double>> metricsData = new HashMap<>();
527 
528             try {
529                 JSONObject jsonObject = new JSONObject(jsonString);
530 
531                 for (String testMethod : METHOD_JSON_KEY_MAP.keySet()) {
532 
533                     JSONArray jsonArray =
534                             (JSONArray) jsonObject.get(METHOD_JSON_KEY_MAP.get(testMethod));
535 
536                     switch (testMethod) {
537                         case TEST_REPROCESSING_THROUGHPUT:
538                         case TEST_REPROCESSING_LATENCY:
539                             for (int i = 0; i < jsonArray.length(); i++) {
540                                 JSONObject element = jsonArray.getJSONObject(i);
541 
542                                 // create a kpiKey from camera id,
543                                 // reprocess type and capture message
544                                 String cameraId = element.getString(CAMERA_ID_KEY);
545                                 String reprocessType = element.getString(REPROCESS_TYPE_KEY);
546                                 String captureMessage = element.getString(CAPTURE_MESSAGE_KEY);
547                                 String kpiKey =
548                                         String.format(
549                                                 "%s_Camera %s %s %s",
550                                                 testMethod,
551                                                 cameraId,
552                                                 reprocessType,
553                                                 captureMessage);
554 
555                                 // read the data array from json object
556                                 JSONArray jsonDataArray = element.getJSONArray(LATENCY_KEY);
557                                 if (!metricsData.containsKey(kpiKey)) {
558                                     List<Double> list = new ArrayList<>();
559                                     metricsData.put(kpiKey, list);
560                                 }
561                                 for (int j = 0; j < jsonDataArray.length(); j++) {
562                                     metricsData.get(kpiKey).add(jsonDataArray.getDouble(j));
563                                 }
564                             }
565                             break;
566                         case TEST_SINGLE_CAPTURE:
567                         case TEST_CAMERA_LAUNCH:
568                             for (int i = 0; i < jsonArray.length(); i++) {
569                                 JSONObject element = jsonArray.getJSONObject(i);
570 
571                                 String cameraid = element.getString(CAMERA_ID_KEY);
572                                 for (String kpiName : mReportingKpis.get(testMethod)) {
573 
574                                     // the json key is all lower case
575                                     String jsonKey = kpiName.toLowerCase().replace(" ", "_");
576                                     String kpiKey =
577                                             String.format("Camera %s %s", cameraid, kpiName);
578                                     if (!metricsData.containsKey(kpiKey)) {
579                                         List<Double> list = new ArrayList<>();
580                                         metricsData.put(kpiKey, list);
581                                     }
582                                     JSONArray jsonDataArray = element.getJSONArray(jsonKey);
583                                     for (int j = 0; j < jsonDataArray.length(); j++) {
584                                         metricsData.get(kpiKey).add(jsonDataArray.getDouble(j));
585                                     }
586                                 }
587                             }
588                             break;
589                         default:
590                             break;
591                     }
592                 }
593             } catch (JSONException e) {
594                 CLog.w("JSONException: %s in string %s", e.getMessage(), jsonString);
595             }
596 
597             // take the average of all data for reporting
598             for (String kpiKey : metricsData.keySet()) {
599                 String kpiValue = String.format("%.1f", getAverage(metricsData.get(kpiKey)));
600                 metrics.put(kpiKey, kpiValue);
601             }
602             return metrics;
603         }
604 
isJsonFileExist()605         public boolean isJsonFileExist() {
606             try {
607                 return getDevice().doesFileExist(JSON_RESULT_FILE);
608             } catch (DeviceNotAvailableException e) {
609                 throw new RuntimeException("Failed to check json report file on device.", e);
610             }
611         }
612 
613         /*
614          * read json report file on the device
615          */
getFormatedJsonReportFromFile()616         private String getFormatedJsonReportFromFile() {
617             String jsonString = null;
618             try {
619                 // pull the json report file from device
620                 File outputFile = FileUtil.createTempFile("json", ".txt");
621                 getDevice().pullFile(JSON_RESULT_FILE, outputFile);
622                 jsonString = reformatJsonString(FileUtil.readStringFromFile(outputFile));
623             } catch (IOException e) {
624                 CLog.w("Couldn't parse the output json log file: ", e);
625             } catch (DeviceNotAvailableException e) {
626                 CLog.w("Could not pull file: %s, error: %s", JSON_RESULT_FILE, e);
627             }
628             return jsonString;
629         }
630 
631         // Reformat the json file to remove duplicate keys
reformatJsonString(String jsonString)632         private String reformatJsonString(String jsonString) {
633 
634             final String TEST_METRICS_PATTERN = "\\\"([a-z0-9_]*)\\\":(\\{[^{}]*\\})";
635             StringBuilder newJsonBuilder = new StringBuilder();
636             // Create map of stream names and json objects.
637             HashMap<String, List<String>> jsonMap = new HashMap<>();
638             Pattern p = Pattern.compile(TEST_METRICS_PATTERN);
639             Matcher m = p.matcher(jsonString);
640             while (m.find()) {
641                 String key = m.group(1);
642                 String value = m.group(2);
643                 if (!jsonMap.containsKey(key)) {
644                     jsonMap.put(key, new ArrayList<String>());
645                 }
646                 jsonMap.get(key).add(value);
647             }
648             // Rewrite json string as arrays.
649             newJsonBuilder.append("{");
650             boolean firstLine = true;
651             for (String key : jsonMap.keySet()) {
652                 if (!firstLine) {
653                     newJsonBuilder.append(",");
654                 } else {
655                     firstLine = false;
656                 }
657                 newJsonBuilder.append("\"").append(key).append("\":[");
658                 boolean firstValue = true;
659                 for (String stream : jsonMap.get(key)) {
660                     if (!firstValue) {
661                         newJsonBuilder.append(",");
662                     } else {
663                         firstValue = false;
664                     }
665                     newJsonBuilder.append(stream);
666                 }
667                 newJsonBuilder.append("]");
668             }
669             newJsonBuilder.append("}");
670             return newJsonBuilder.toString();
671         }
672     }
673 }
674