1 /* 2 * Copyright (C) 2019 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.helpers; 17 18 import static com.android.helpers.MetricUtility.constructKey; 19 20 import android.util.Log; 21 22 import androidx.annotation.VisibleForTesting; 23 import androidx.test.InstrumentationRegistry; 24 import androidx.test.uiautomator.UiDevice; 25 26 import java.util.HashMap; 27 import java.util.Map; 28 import java.util.regex.Matcher; 29 import java.util.regex.Pattern; 30 import java.util.stream.Stream; 31 32 /** 33 * An {@link ICollectorHelper} for collecting SurfaceFlinger time stats. 34 * 35 * <p>This parses the output of {@code dumpsys SurfaceFlinger --timestats} and returns a collection 36 * of both global metrics and metrics tracked for each layer. 37 */ 38 public class SfStatsCollectionHelper implements ICollectorHelper<Double> { 39 40 private static final String LOG_TAG = SfStatsCollectionHelper.class.getSimpleName(); 41 42 private static final Pattern KEY_VALUE_PATTERN = 43 Pattern.compile("^(\\w+)\\s+=\\s+(\\d+\\.?\\d*|.*).*"); 44 private static final Pattern HISTOGRAM_PATTERN = 45 Pattern.compile("([^\\n]+)\\n((\\d+ms=\\d+\\s+)+)"); 46 47 private static final String FRAME_DURATION_KEY = "frameDuration histogram is as below:"; 48 private static final String RENDER_ENGINE_KEY = "renderEngineTiming histogram is as below:"; 49 50 @VisibleForTesting static final String SFSTATS_METRICS_PREFIX = "SFSTATS"; 51 52 @VisibleForTesting static final String SFSTATS_COMMAND = "dumpsys SurfaceFlinger --timestats "; 53 54 @VisibleForTesting 55 static final String SFSTATS_COMMAND_ENABLE_AND_CLEAR = SFSTATS_COMMAND + "-enable -clear"; 56 57 @VisibleForTesting static final String SFSTATS_COMMAND_DUMP = SFSTATS_COMMAND + "-dump"; 58 59 @VisibleForTesting 60 static final String SFSTATS_COMMAND_DISABLE_AND_CLEAR = SFSTATS_COMMAND + "-disable -clear"; 61 62 private UiDevice mDevice; 63 parseStatsValue(String v)64 private Double parseStatsValue(String v) { 65 try { 66 return Double.parseDouble(v); 67 } catch (NumberFormatException e) { 68 Log.e(LOG_TAG, "Encountered exception parsing value: " + v, e); 69 return -1.0; 70 } 71 } 72 73 @Override startCollecting()74 public boolean startCollecting() { 75 try { 76 getDevice().executeShellCommand(SFSTATS_COMMAND_ENABLE_AND_CLEAR); 77 } catch (Exception e) { 78 Log.e(LOG_TAG, "Encountered exception enabling dumpsys SurfaceFlinger.", e); 79 throw new RuntimeException(e); 80 } 81 return true; 82 } 83 84 @Override getMetrics()85 public Map<String, Double> getMetrics() { 86 Map<String, Double> results = new HashMap<>(); 87 String output; 88 try { 89 output = getDevice().executeShellCommand(SFSTATS_COMMAND_DUMP); 90 } catch (Exception e) { 91 Log.e(LOG_TAG, "Encountered exception calling dumpsys SurfaceFlinger.", e); 92 throw new RuntimeException(e); 93 } 94 String[] blocks = output.split("\n\n"); 95 96 HashMap<String, String> globalPairs = getStatPairs(blocks[0]); 97 Map<String, Histogram> histogramPairs = getHistogramPairs(blocks[0]); 98 99 for (String key : globalPairs.keySet()) { 100 String metricKey = constructKey(SFSTATS_METRICS_PREFIX, "GLOBAL", key.toUpperCase()); 101 results.put(metricKey, parseStatsValue(globalPairs.get(key))); 102 } 103 104 if (histogramPairs.containsKey(FRAME_DURATION_KEY)) { 105 results.put( 106 constructKey(SFSTATS_METRICS_PREFIX, "GLOBAL", "FRAME_CPU_DURATION_AVG"), 107 histogramPairs.get(FRAME_DURATION_KEY).mean()); 108 } 109 110 if (histogramPairs.containsKey(RENDER_ENGINE_KEY)) { 111 results.put( 112 constructKey(SFSTATS_METRICS_PREFIX, "GLOBAL", "RENDER_ENGINE_DURATION_AVG"), 113 histogramPairs.get(RENDER_ENGINE_KEY).mean()); 114 } 115 116 for (int i = 1; i < blocks.length; i++) { 117 HashMap<String, String> layerPairs = getStatPairs(blocks[i]); 118 String layerName = layerPairs.get("layerName"); 119 String totalFrames = layerPairs.get("totalFrames"); 120 String droppedFrames = layerPairs.get("droppedFrames"); 121 String averageFPS = layerPairs.get("averageFPS"); 122 123 if (totalFrames != null) { 124 results.put( 125 constructKey(SFSTATS_METRICS_PREFIX, layerName, "TOTAL_FRAMES"), 126 parseStatsValue(totalFrames)); 127 } else { 128 Log.i(LOG_TAG, "totalFrames not found for layer name: " + layerName); 129 } 130 131 if (droppedFrames != null) { 132 results.put( 133 constructKey(SFSTATS_METRICS_PREFIX, layerName, "DROPPED_FRAMES"), 134 parseStatsValue(droppedFrames)); 135 } else { 136 Log.i(LOG_TAG, "droppedFrames not found for layer name: " + layerName); 137 } 138 139 if (averageFPS != null) { 140 results.put( 141 constructKey(SFSTATS_METRICS_PREFIX, layerName, "AVERAGE_FPS"), 142 parseStatsValue(averageFPS)); 143 } else { 144 Log.i(LOG_TAG, "averageFPS not found for layer name: " + layerName); 145 } 146 } 147 148 return results; 149 } 150 151 @Override stopCollecting()152 public boolean stopCollecting() { 153 try { 154 getDevice().executeShellCommand(SFSTATS_COMMAND_DISABLE_AND_CLEAR); 155 } catch (Exception e) { 156 Log.e(LOG_TAG, "Encountered exception disabling dumpsys SurfaceFlinger.", e); 157 throw new RuntimeException(e); 158 } 159 return true; 160 } 161 162 /** Returns the {@link UiDevice} under test. */ 163 @VisibleForTesting getDevice()164 protected UiDevice getDevice() { 165 if (mDevice == null) { 166 mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 167 } 168 return mDevice; 169 } 170 171 /** 172 * Returns a map of key-value pairs for every line of timestats for each layer handled by 173 * SurfaceFlinger as well as some global SurfaceFlinger stats. An output line like {@code 174 * totalFrames = 42} would get parsed and be accessable as {@code pairs.get("totalFrames") => 175 * "42"} 176 */ getStatPairs(String block)177 private HashMap<String, String> getStatPairs(String block) { 178 HashMap<String, String> pairs = new HashMap<>(); 179 String[] lines = block.split("\n"); 180 for (int j = 0; j < lines.length; j++) { 181 Matcher keyValueMatcher = KEY_VALUE_PATTERN.matcher(lines[j].trim()); 182 if (keyValueMatcher.matches()) { 183 pairs.put(keyValueMatcher.group(1), keyValueMatcher.group(2)); 184 } 185 } 186 return pairs; 187 } 188 189 /** 190 * Returns a map of {@link Histogram} instances emitted by SurfaceFlinger stats. 191 * 192 * <p>Input must be of the format defined by the {@link HISTOGRAM_PATTERN} regex. Example input 193 * may include: 194 * 195 * <pre>{@code 196 * Sample key: 197 * 0ms=0 1ms=1 2ms=4 3ms=9 4ms=16 198 * }</pre> 199 * 200 * <p>The corresponding output would include "Sample key:" as the key for a {@link Histogram} 201 * instance constructed from the string {@code 0ms=0 1ms=1 2ms=4 3ms=9 4ms=16}. 202 */ getHistogramPairs(String block)203 private Map<String, Histogram> getHistogramPairs(String block) { 204 Map<String, Histogram> pairs = new HashMap<>(); 205 Matcher histogramMatcher = HISTOGRAM_PATTERN.matcher(block); 206 while (histogramMatcher.find()) { 207 String key = histogramMatcher.group(1); 208 String histogramString = histogramMatcher.group(2); 209 Histogram histogram = new Histogram(); 210 Stream.of(histogramString.split("\\s+")) 211 .forEach( 212 bucket -> 213 histogram.put( 214 Integer.valueOf( 215 bucket.substring(0, bucket.indexOf("ms"))), 216 Long.valueOf( 217 bucket.substring(bucket.indexOf("=") + 1)))); 218 pairs.put(key, histogram); 219 } 220 return pairs; 221 } 222 223 /** Representation for a statistical histogram */ 224 private static final class Histogram { 225 private final Map<Integer, Long> internalMap; 226 227 /** Constructs a histogram instance. */ Histogram()228 Histogram() { 229 internalMap = new HashMap<>(); 230 } 231 232 /** 233 * Puts a (key, value) pair in the histogram. 234 * 235 * <p>The key would represent the particular bucket that the value is inserted into. 236 */ put(Integer key, Long value)237 Histogram put(Integer key, Long value) { 238 internalMap.put(key, value); 239 return this; 240 } 241 242 /** 243 * Computes the mean of the histogram 244 * 245 * @return 0 if the histogram is empty, the true mean otherwise. 246 */ mean()247 double mean() { 248 long count = internalMap.values().stream().mapToLong(v -> v).sum(); 249 if (count <= 0) { 250 return 0.0; 251 } 252 long numerator = 253 internalMap 254 .entrySet() 255 .stream() 256 .mapToLong(entry -> entry.getKey() * entry.getValue()) 257 .sum(); 258 return (double) numerator / count; 259 } 260 261 @Override equals(Object obj)262 public boolean equals(Object obj) { 263 if (!(obj instanceof Histogram)) { 264 return false; 265 } 266 267 Histogram other = (Histogram) obj; 268 269 return internalMap.equals(other.internalMap); 270 } 271 272 @Override hashCode()273 public int hashCode() { 274 return internalMap.hashCode(); 275 } 276 } 277 } 278