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