• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 com.android.catbox.util;
17 
18 import com.android.annotations.VisibleForTesting;
19 
20 import com.android.ddmlib.Log.LogLevel;
21 
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
24 import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
25 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
26 import com.android.tradefed.result.TestDescription;
27 import com.android.tradefed.util.proto.TfMetricProtoUtil;
28 
29 import com.google.common.base.Joiner;
30 import com.google.common.collect.ArrayListMultimap;
31 import com.google.common.math.Quantiles;
32 
33 import java.util.Arrays;
34 import java.util.Collection;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.LinkedHashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Map.Entry;
42 import java.util.Set;
43 import java.util.stream.Collectors;
44 
45 /**
46  * Contains common utility methods for storing the test metrics and aggregating the metrics in
47  * similar tests.
48  */
49 public class TestMetricsUtil {
50 
51     private static final String TEST_HEADER_SEPARATOR = "\n\n";
52     private static final String METRIC_SEPARATOR = "\n";
53     private static final String METRIC_KEY_VALUE_SEPARATOR = ":";
54     private static final String STATS_KEY_MIN = "min";
55     private static final String STATS_KEY_MAX = "max";
56     private static final String STATS_KEY_MEAN = "mean";
57     private static final String STATS_KEY_VAR = "var";
58     private static final String STATS_KEY_STDEV = "stdev";
59     private static final String STATS_KEY_MEDIAN = "median";
60     private static final String STATS_KEY_TOTAL = "total";
61     private static final String STATS_KEY_COUNT = "metric-count";
62     private static final String STATS_KEY_PERCENTILE_PREFIX = "p";
63     private static final String STATS_KEY_SEPARATOR = "-";
64     private static final Joiner CLASS_METHOD_JOINER = Joiner.on("#").skipNulls();
65 
66     // Used to separate the package name from the iteration number. Default is set to "$".
67     private String mTestIterationSeparator = "$";
68 
69     // Percentiles to include when calculating the aggregates.
70     private Set<Integer> mActualPercentiles = new HashSet<>();
71 
72     // Store the test metrics for aggregation at the end of test run.
73     // Outer map key is the test id and inner map key is the metric key name.
74     private Map<String, ArrayListMultimap<String, Metric>> mStoredTestMetrics =
75             new HashMap<String, ArrayListMultimap<String, Metric>>();
76 
77     /**
78      * Used for storing the individual test metrics and use it for aggregation.
79      */
storeTestMetrics(TestDescription testDescription, Map<String, Metric> testMetrics)80     public void storeTestMetrics(TestDescription testDescription, Map<String, Metric> testMetrics) {
81         if (testMetrics == null) {
82             return;
83         }
84 
85         // Group test cases which differs only by the iteration separator or test the same name.
86         String className = testDescription.getClassName();
87         String testName = testDescription.getTestName();
88 
89         // Check if the class name has an iteration separator.
90         if(className.contains(mTestIterationSeparator)) {
91             className = className.substring(0, className.indexOf(mTestIterationSeparator));
92         }
93         // Also check if the test(method) name has an iteration separator. See - http://b/342206870
94         if(testName.contains(mTestIterationSeparator)) {
95             testName = testName.substring(0, testName.indexOf(mTestIterationSeparator));
96         }
97         String newTestId = CLASS_METHOD_JOINER.join(className, testName);
98 
99         if (!mStoredTestMetrics.containsKey(newTestId)) {
100             mStoredTestMetrics.put(newTestId, ArrayListMultimap.create());
101         }
102         ArrayListMultimap<String, Metric> storedMetricsForThisTest = mStoredTestMetrics
103                 .get(newTestId);
104 
105         // Store only raw metrics
106         HashMap<String, Metric> rawMetrics = getRawMetricsOnly(testMetrics);
107         for (Map.Entry<String, Metric> entry : rawMetrics.entrySet()) {
108             String key = entry.getKey();
109             // In case of Multi User tests, explicitly filter out the method name(that also includes the iteration separator)
110             if (key.contains("#")) {
111                 key = key.substring(0, key.indexOf("#"));
112             }
113             storedMetricsForThisTest.put(key, entry.getValue());
114         }
115     }
116 
117     /**
118      * Aggregate the metrics collected from multiple iterations of the test and
119      * return aggregated metrics.
120      */
getAggregatedStoredTestMetrics()121     public Map<String, Map<String, String>> getAggregatedStoredTestMetrics() {
122         Map<String, Map<String, String>> aggregatedStoredMetrics =
123                 new HashMap<String, Map<String, String>>();
124         for (String testName : mStoredTestMetrics.keySet()) {
125             ArrayListMultimap<String, Metric> currentTest = mStoredTestMetrics.get(testName);
126 
127             Map<String, Metric> aggregateMetrics = new LinkedHashMap<String, Metric>();
128             for (String metricKey : currentTest.keySet()) {
129                 List<Metric> metrics = currentTest.get(metricKey);
130                 List<Measurements> measures = metrics.stream().map(Metric::getMeasurements)
131                         .collect(Collectors.toList());
132                 // Parse metrics into a list of SingleString values, concatenating lists in the process
133                 List<String> rawValues = measures.stream()
134                         .map(Measurements::getSingleString)
135                         .map(
136                                 m -> {
137                                     // Split results; also deals with the case of empty results
138                                     // in a certain run
139                                     List<String> splitVals = Arrays.asList(m.split(",", 0));
140                                     if (splitVals.size() == 1 && splitVals.get(0).isEmpty()) {
141                                         return Collections.<String>emptyList();
142                                     }
143                                     return splitVals;
144                                 })
145                         .flatMap(Collection::stream)
146                         .map(String::trim)
147                         .collect(Collectors.toList());
148                 // Do not report empty metrics
149                 if (rawValues.isEmpty()) {
150                     continue;
151                 }
152                 if (isAllDoubleValues(rawValues)) {
153                     buildStats(metricKey, rawValues, aggregateMetrics);
154                 }
155             }
156             Map<String, String> compatibleTestMetrics = TfMetricProtoUtil
157                     .compatibleConvert(aggregateMetrics);
158             aggregatedStoredMetrics.put(testName, compatibleTestMetrics);
159         }
160         return aggregatedStoredMetrics;
161     }
162 
163     /** Set percentiles */
setPercentiles(Set<Integer> percentiles)164     public void setPercentiles(Set<Integer> percentiles) {
165         mActualPercentiles = percentiles;
166     }
167 
168     /** Set iteration separator */
setIterationSeparator(String separator)169     public void setIterationSeparator(String separator) {
170         mTestIterationSeparator = separator;
171     }
172 
173     @VisibleForTesting
getStoredTestMetric()174     public Map<String, ArrayListMultimap<String, Metric>> getStoredTestMetric() {
175         return mStoredTestMetrics;
176     }
177 
178     /**
179      * Return true is all the values can be parsed to double value.
180      * Otherwise return false.
181      */
isAllDoubleValues(List<String> rawValues)182     public static boolean isAllDoubleValues(List<String> rawValues) {
183         return rawValues
184                 .stream()
185                 .allMatch(
186                         val -> {
187                             try {
188                                 Double.parseDouble(val);
189                                 return true;
190                             } catch (NumberFormatException e) {
191                                 return false;
192                             }
193                         });
194     }
195 
196     /**
197      * Compute the stats from the give list of values.
198      */
199     public static Map<String, Double> getStats(Collection<Double> values,
200             Set<Integer> percentiles) {
201         Map<String, Double> stats = new LinkedHashMap<>();
202         double sum = values.stream().mapToDouble(Double::doubleValue).sum();
203         double count = values.size();
204         // The orElse situation should never happen.
205         double mean = values.stream()
206                 .mapToDouble(Double::doubleValue)
207                 .average()
208                 .orElseThrow(IllegalStateException::new);
209         double variance = values.stream().reduce(0.0, (a, b) -> a + Math.pow(b - mean, 2) / count);
210         // Calculate percentiles. 50 th percentile will be used as medain.
211         Set<Integer> updatedPercentile = new HashSet<>(percentiles);
212         updatedPercentile.add(50);
213         Map<Integer, Double> percentileStat = Quantiles.percentiles().indexes(updatedPercentile)
214                 .compute(values);
215         double median = percentileStat.get(50);
216 
217         stats.put(STATS_KEY_MIN, Collections.min(values));
218         stats.put(STATS_KEY_MAX, Collections.max(values));
219         stats.put(STATS_KEY_MEAN, mean);
220         stats.put(STATS_KEY_VAR, variance);
221         stats.put(STATS_KEY_STDEV, Math.sqrt(variance));
222         stats.put(STATS_KEY_MEDIAN, median);
223         stats.put(STATS_KEY_TOTAL, sum);
224         stats.put(STATS_KEY_COUNT, count);
225         percentileStat
226                 .entrySet()
227                 .stream()
228                 .forEach(
229                         e -> {
230                             // If the percentile is 50, only include it if the user asks for it
231                             // explicitly.
232                             if (e.getKey() != 50 || percentiles.contains(50)) {
233                                 stats.put(
234                                         STATS_KEY_PERCENTILE_PREFIX + e.getKey().toString(),
235                                         e.getValue());
236                             }
237                         });
238         return stats;
239     }
240 
241     /**
242      * Build stats for the given set of values and build the metrics using the metric key
243      * and stats name and update the results in aggregated metrics.
244      */
245     private void buildStats(String metricKey, List<String> values,
246             Map<String, Metric> aggregateMetrics) {
247         List<Double> doubleValues = values.stream().map(Double::parseDouble)
248                 .collect(Collectors.toList());
249         Map<String, Double> stats = getStats(doubleValues, mActualPercentiles);
250         for (String statKey : stats.keySet()) {
251             Metric.Builder metricBuilder = Metric.newBuilder();
252             metricBuilder
253                     .getMeasurementsBuilder()
254                     .setSingleString(String.format("%2.2f", stats.get(statKey)));
255             aggregateMetrics.put(
256                     String.join(STATS_KEY_SEPARATOR, metricKey, statKey),
257                     metricBuilder.build());
258         }
259     }
260 
261     /**
262      * Get only raw values for processing.
263      */
264     private HashMap<String, Metric> getRawMetricsOnly(Map<String, Metric> metrics) {
265         HashMap<String, Metric> rawMetrics = new HashMap<>();
266         for (Entry<String, Metric> entry : metrics.entrySet()) {
267             if (DataType.RAW.equals(entry.getValue().getType())) {
268                 rawMetrics.put(entry.getKey(), entry.getValue());
269             }
270         }
271         return rawMetrics;
272     }
273 }