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