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