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 }