1 /*
2  * Copyright (C) 2018 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 android.gameperformance;
17 
18 import java.io.BufferedReader;
19 import java.io.FileReader;
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collections;
24 import java.util.Comparator;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.StringTokenizer;
30 import java.util.TreeMap;
31 import java.util.TreeSet;
32 
33 /**
34  * Utility class that performs analysis of atrace logs. This is implemented without Android
35  * dependencies and therefore can be used in stand-alone mode.
36  * Idea of this is to track atrace gfx event from graphics buffer producer/consumer.
37  * We analyze here from surfaceflinger
38  *   queueBuffer - event when buffer was queued.
39  *   acquireBuffer - event when buffer was requested for composition.
40  *   releaseBuffer - even when buffer was released after composition.
41  * This also track events, issued locally
42  *   localPostBuffer - event when buffer was posted from the local app.
43  *
44  *  queueBuffer, acquireBuffer, releaseBuffer is accompanied with buffer name so we
45  *  can track life-cycle of particular buffer.
46  *  We don't have such information for localPostBuffer, however we can track next queueBuffer
47  *  from surfaceflinger corresponds to previous localPostBuffer.
48  *
49  *  Following results are calculated:
50  *    post_time_[min/max/avr]_mcs - time for localPostBuffer duration.
51  *    ready_time_[min/max/avr]_mcs - time from localPostBuffer to when buffer was acquired by
52  *                                   surfaceflinger.
53  *    latency_[min/max/avr]_mcs - time from localPostBuffer to when buffer was released by
54  *                                 surfaceflinger.
55  *    missed_frame_percents - percentage of missed frames (frames that do not have right sequence
56  *                            of events).
57  *
58  * Following is example of atrace logs from different platforms
59  *            <...>-5237  (-----) [000] ...1   228.380392: tracing_mark_write: B|11|SurfaceView - android.gameperformance/android.gameperformance.GamePerformanceActivity#0: 2
60  *   surfaceflinger-5855  ( 5855) [001] ...1   169.627364: tracing_mark_write: B|24|acquireBuffer
61  *   HwBinder:617_2-652   (  617) [002] d..1 360262.921756: sde_evtlog: 617|sde_encoder_virt_atomic_check:855|19|0|0|0|0|0|0|0|0|0|0|0|0|0|0
62  */
63 public class GraphicBufferMetrics {
64     private final static String TAG = "GraphicBufferMetrics";
65 
66     private final static String KEY_POST_TIME = "post_time";
67     private final static String KEY_READY_TIME = "ready_time";
68     private final static String KEY_LATENCY = "latency";
69     private final static String SUFFIX_MIN = "min";
70     private final static String SUFFIX_MAX = "max";
71     private final static String SUFFIX_MEDIAN = "median";
72     private final static String KEY_MISSED_FRAME_RATE = "missed_frame_percents";
73 
74     private final static int EVENT_POST_BUFFER = 0;
75     private final static int EVENT_QUEUE_BUFFER = 1;
76     private final static int EVENT_ACQUIRE_BUFFER = 2;
77     private final static int EVENT_RELEASE_BUFFER = 3;
78 
79     // atrace prints this line. Used as a marker to make sure that we can parse its output.
80     private final static String ATRACE_HEADER =
81             "#           TASK-PID    TGID   CPU#  ||||    TIMESTAMP  FUNCTION";
82 
83     private final static String[] KNOWN_PHRASES = new String[] {
84             "capturing trace... done", "TRACE:"};
85     private final static List<String> KNWON_PHRASES_LIST = Arrays.asList(KNOWN_PHRASES);
86 
87     // Type of the atrace event we can parse and analyze.
88     private final static String FUNCTION_TRACING_MARK_WRITE = "tracing_mark_write";
89 
90     // Trace event we can ignore. It contains current timestamp information for the atrace output.
91     private final static String TRACE_EVENT_CLOCK_SYNC = "trace_event_clock_sync:";
92 
93     // Threshold we consider test passes successfully. If we cannot collect enough amount of frames
94     // let fail the test. 50 is calculated 10 frames per second running for five seconds.
95     private final static int MINIMAL_SAMPLE_CNT_TO_PASS = 50;
96 
97     /**
98      * Raw event in atrace. Stored hierarchically.
99      */
100     private static class RawEvent {
101         // Parent of this event or null for the root holder.
102         public final RawEvent mParent;
103         // Time of the event in mcs.
104         public final long mTime;
105         // Duration of the event in mcs.
106         public long mDuration;
107         // Name/body of the event.
108         public final String mName;
109         // Children events.
110         public final List<RawEvent> mChildren;
111 
RawEvent(RawEvent parent, long time, String name)112         public RawEvent(RawEvent parent, long time, String name) {
113             mParent = parent;
114             mTime = time;
115             mName = name;
116             mDuration = -1;
117             mChildren = new ArrayList<>();
118         }
119 
120         /**
121          * Recursively finds child events.
122          * @param path specify path to events. For example a/b. That means to find child with name
123          *             'a' of the current event and in this child find child with name 'b'. Path
124          *             can consist from only one segment and that means we analyze only children of
125          *             the current event.
126          * @param collector to collect found events.
127          */
findEvents(String path, List<RawEvent> collector)128         public void findEvents(String path, List<RawEvent> collector) {
129             final int separator = path.indexOf('/');
130             final String current = separator > 0 ? path.substring(0, separator) : path;
131             final String nextPath = separator > 0 ? path.substring(separator + 1) : null;
132             for (RawEvent child : mChildren) {
133                 if (child.mName.equals(current)) {
134                     if (nextPath != null) {
135                         child.findEvents(nextPath, collector);
136                     } else {
137                         collector.add(child);
138                     }
139                 }
140             }
141         }
142 
dump(String prefix)143         public void dump(String prefix) {
144             System.err.print(prefix);
145             System.err.println(mTime + "[" + mDuration + "] " + mName);
146             for (RawEvent e : mChildren) {
147                 e.dump(prefix + "  ");
148             }
149         }
150     }
151 
152     /**
153      * Describes graphic buffer event. local post, queued, acquired, released.
154      */
155     private static class BufferEvent {
156         public final int mType;
157         public final long mTime;
158         public final long mDuration;
159         public final String mBufferId;
160 
BufferEvent(int type, long time, long duration, String bufferId)161         public BufferEvent(int type, long time, long duration, String bufferId) {
162             mType = type;
163             mTime = time;
164             mDuration = duration;
165             mBufferId = bufferId;
166         }
167 
168         @Override
toString()169         public String toString() {
170             return "Type: " + mType + ". Time: " + mTime +
171                     "[" + mDuration + "]. Buffer: " + mBufferId + ".";
172         }
173     }
174 
175     /**
176      * Returns true if given char is digit.
177      */
isDigitChar(char c)178     private static boolean isDigitChar(char c) {
179         return (c >= '0') && (c <= '9');
180     }
181 
182     /**
183      * Returns true if given char is digit or '.'.
184      */
isDoubleDigitChar(char c)185     private static boolean isDoubleDigitChar(char c) {
186         return (c == '.') || isDigitChar(c);
187     }
188 
189     /**
190      * Convert timestamp string that represents double value in seconds to long time that represents
191      * timestamp in microseconds.
192      */
getTimeStamp(String timeStampStr)193     private static long getTimeStamp(String timeStampStr) {
194         return (long)(1000000.0 * Double.parseDouble(timeStampStr));
195     }
196 
197     /**
198      * Reads atrace log and build event model. Result is a map, where key specifies event for the
199      * particular thread. Value is the synthetic root RawEvent that holds all events for the
200      * thread. Events are stored hierarchically.
201      */
buildEventModel(String fileName)202     private static Map<Integer, RawEvent> buildEventModel(String fileName) throws IOException {
203         Map<Integer, RawEvent> result = new HashMap<>();
204 
205         BufferedReader bufferedReader = null;
206         String line = "";
207         boolean headerDetected = false;
208         try {
209             bufferedReader = new BufferedReader(new FileReader(fileName));
210             while ((line = bufferedReader.readLine()) != null) {
211                 // Make sure we find comment that describes output format we can with.
212                 headerDetected |= line.equals(ATRACE_HEADER);
213                 // Skip comments.
214                 if (line.startsWith("#")) {
215                     continue;
216                 }
217                 // Skip known service output
218                 if (KNWON_PHRASES_LIST.contains(line)) {
219                     continue;
220                 }
221 
222                 if (!headerDetected) {
223                     // We don't know the format of this line.
224                     throw new IllegalStateException("Header was not detected");
225                 }
226 
227                 // TASK-PID in header exists at position 12. PID position 17 should contains first
228                 // digit of thread id after the '-'.
229                 if (!isDigitChar(line.charAt(17)) || line.charAt(16) != '-') {
230                     throw new IllegalStateException("Failed to parse thread id: " + line);
231                 }
232                 int rightIndex = line.indexOf(' ', 17);
233                 final String threadIdStr = line.substring(17, rightIndex);
234                 final int threadId = Integer.parseInt(threadIdStr);
235 
236                 // TIMESTAMP in header exists at position 45
237                 // This position should point in the middle of timestamp which is ended by ':'.
238                 int leftIndex = 45;
239                 while (isDoubleDigitChar(line.charAt(leftIndex))) {
240                     --leftIndex;
241                 }
242                 rightIndex = line.indexOf(':', 45);
243 
244                 final String timeStampString = line.substring(leftIndex + 1, rightIndex);
245                 final long timeStampMcs = getTimeStamp(timeStampString);
246 
247                 // Find function name, pointed by FUNCTION. Long timestamp can shift if position
248                 // so use end of timestamp to find the function which is ended by ':'.
249                 leftIndex = rightIndex + 1;
250                 while (Character.isWhitespace(line.charAt(leftIndex))) {
251                     ++leftIndex;
252                 }
253                 rightIndex = line.indexOf(':', leftIndex);
254                 final String function = line.substring(leftIndex, rightIndex);
255 
256                 if (!function.equals(FUNCTION_TRACING_MARK_WRITE)) {
257                     continue;
258                 }
259 
260                 // Rest of the line is event body.
261                 leftIndex = rightIndex + 1;
262                 while (Character.isWhitespace(line.charAt(leftIndex))) {
263                     ++leftIndex;
264                 }
265 
266                 final String event = line.substring(leftIndex);
267                 if (event.startsWith(TRACE_EVENT_CLOCK_SYNC)) {
268                     continue;
269                 }
270 
271                 // Parse event, for example:
272                 // B|615|SurfaceView - android.gameperformance.GamePerformanceActivity#0: 1
273                 // E|615
274                 // C|11253|operation id|2
275                 StringTokenizer eventTokenizer = new StringTokenizer(event, "|");
276                 final String eventType = eventTokenizer.nextToken();
277 
278                 // Attach root on demand.
279                 if (!result.containsKey(threadId)) {
280                     result.put(threadId, new RawEvent(null /* parent */,
281                                                       timeStampMcs,
282                                                       "#ROOT_" + threadId));
283                 }
284 
285                 switch (eventType) {
286                 case "B": {
287                         // Log entry starts.
288                         eventTokenizer.nextToken(); // PID
289                         String eventText = eventTokenizer.nextToken();
290                         while (eventTokenizer.hasMoreTokens()) {
291                             eventText += " ";
292                             eventText += eventTokenizer.nextToken();
293                         }
294                         RawEvent parent = result.get(threadId);
295                         RawEvent current = new RawEvent(parent, timeStampMcs, eventText);
296                         parent.mChildren.add(current);
297                         result.put(threadId, current);
298                     }
299                     break;
300                 case "E": {
301                         // Log entry ends.
302                         RawEvent current = result.get(threadId);
303                         current.mDuration = timeStampMcs - current.mTime;
304                         if (current.mParent == null) {
305                             // Detect a tail of the previous call. Remove last child element if it
306                             // exists once it does not belong to the root.
307                             if (!current.mChildren.isEmpty()) {
308                                 current.mChildren.remove(current.mChildren.size() -1);
309                             }
310                         } else {
311                             result.put(threadId, current.mParent);
312                         }
313                     }
314                     break;
315                 case "C":
316                     // Counter, ignore
317                     break;
318                 default:
319                     throw new IllegalStateException(
320                             "Unrecognized trace: " + line + " # " + eventType + " # " + event);
321                 }
322             }
323 
324             // Detect incomplete events and detach from the root.
325             Set<Integer> threadIds = new TreeSet<>();
326             threadIds.addAll(result.keySet());
327             for (int threadId : threadIds) {
328                 RawEvent root = result.get(threadId);
329                 if (root.mParent == null) {
330                     // Last trace was closed.
331                     continue;
332                 }
333                 // Find the root.
334                 while (root.mParent != null) {
335                     root = root.mParent;
336                 }
337                 // Discard latest incomplete element.
338                 root.mChildren.remove(root.mChildren.size() - 1);
339                 result.put(threadId, root);
340             }
341         } catch (Exception e) {
342             throw new IOException("Failed to process " + line, e);
343         } finally {
344             Utils.closeQuietly(bufferedReader);
345         }
346 
347         return result;
348     }
349 
350     /**
351      * Processes provided atrace log and calculates graphics buffer metrics.
352      * @param fileName name of atrace log file.
353      * @param testTag tag to separate results for the different passes.
354      */
processGraphicBufferResult( String fileName, String testTag)355     public static Map<String, Double> processGraphicBufferResult(
356             String fileName, String testTag) throws IOException {
357         final Map<Integer, RawEvent> model = buildEventModel(fileName);
358 
359         List<RawEvent> collectorPostBuffer = new ArrayList<>();
360         List<RawEvent> collectorQueueBuffer = new ArrayList<>();
361         List<RawEvent> collectorReleaseBuffer = new ArrayList<>();
362         List<RawEvent> collectorAcquireBuffer = new ArrayList<>();
363 
364         // Collect required events.
365         for (RawEvent root : model.values()) {
366             // Surface view
367             root.findEvents("localPostBuffer", collectorPostBuffer);
368             // OpengGL view
369             root.findEvents("eglSwapBuffersWithDamageKHR", collectorPostBuffer);
370 
371             root.findEvents("queueBuffer", collectorQueueBuffer);
372             root.findEvents("onMessageReceived/handleMessageInvalidate/latchBuffer/" +
373                     "updateTexImage/acquireBuffer",
374                     collectorAcquireBuffer);
375             // PI stack
376             root.findEvents(
377                     "onMessageReceived/handleMessageRefresh/postComposition/releaseBuffer",
378                     collectorReleaseBuffer);
379             // NYC stack
380             root.findEvents(
381                     "onMessageReceived/handleMessageRefresh/releaseBuffer",
382                     collectorReleaseBuffer);
383         }
384 
385         // Convert raw event to buffer events.
386         List<BufferEvent> bufferEvents = new ArrayList<>();
387         for (RawEvent event : collectorPostBuffer) {
388             bufferEvents.add(
389                     new BufferEvent(EVENT_POST_BUFFER, event.mTime, event.mDuration, null));
390         }
391         toBufferEvents(EVENT_QUEUE_BUFFER, collectorQueueBuffer, bufferEvents);
392         toBufferEvents(EVENT_ACQUIRE_BUFFER, collectorAcquireBuffer, bufferEvents);
393         toBufferEvents(EVENT_RELEASE_BUFFER, collectorReleaseBuffer, bufferEvents);
394 
395         // Sort events based on time. These events are originally taken from different threads so
396         // order is not always preserved.
397         Collections.sort(bufferEvents, new Comparator<BufferEvent>() {
398             @Override
399             public int compare(BufferEvent o1, BufferEvent o2) {
400                 if (o1.mTime < o2.mTime) {
401                     return -1;
402                 } if (o1.mTime > o2.mTime) {
403                     return +1;
404                 } else {
405                     return 0;
406                 }
407             }
408         });
409 
410         // Collect samples.
411         List<Long> postTimes = new ArrayList<>();
412         List<Long> readyTimes = new ArrayList<>();
413         List<Long> latencyTimes = new ArrayList<>();
414         long missedCnt = 0;
415 
416         for (int i = 0; i < bufferEvents.size(); ++i) {
417             if (bufferEvents.get(i).mType != EVENT_POST_BUFFER) {
418                 continue;
419             }
420             final int queueIndex = findNextOfType(bufferEvents, i + 1, EVENT_QUEUE_BUFFER);
421             if (queueIndex < 0) {
422                 break;
423             }
424             final int acquireIndex = findNextOfBufferId(bufferEvents, queueIndex + 1,
425                     bufferEvents.get(queueIndex).mBufferId);
426             if (acquireIndex < 0) {
427                 break;
428             }
429             if (bufferEvents.get(acquireIndex).mType != EVENT_ACQUIRE_BUFFER) {
430                 // Was not actually presented.
431                 ++missedCnt;
432                 continue;
433             }
434             final int releaseIndex = findNextOfBufferId(bufferEvents, acquireIndex + 1,
435                     bufferEvents.get(queueIndex).mBufferId);
436             if (releaseIndex < 0) {
437                 break;
438             }
439             if (bufferEvents.get(releaseIndex).mType != EVENT_RELEASE_BUFFER) {
440                 // Was not actually presented.
441                 ++missedCnt;
442                 continue;
443             }
444 
445             postTimes.add(bufferEvents.get(i).mDuration);
446             readyTimes.add(
447                     bufferEvents.get(acquireIndex).mTime - bufferEvents.get(i).mTime);
448             latencyTimes.add(
449                     bufferEvents.get(releaseIndex).mTime - bufferEvents.get(i).mTime);
450         }
451 
452         if (postTimes.size() < MINIMAL_SAMPLE_CNT_TO_PASS) {
453             throw new IllegalStateException("Too few sample cnt: " + postTimes.size() +". " +
454                     MINIMAL_SAMPLE_CNT_TO_PASS + " is required.");
455         }
456 
457         Map<String, Double> status = new TreeMap<>();
458         addResults(status, testTag, KEY_POST_TIME, postTimes);
459         addResults(status, testTag, KEY_READY_TIME, readyTimes);
460         addResults(status, testTag, KEY_LATENCY, latencyTimes);
461         status.put(testTag + "_" + KEY_MISSED_FRAME_RATE,
462                 100.0 * missedCnt / (missedCnt + postTimes.size()));
463         return status;
464     }
465 
addResults( Map<String, Double> status, String tag, String key, List<Long> times)466     private static void addResults(
467             Map<String, Double> status, String tag, String key, List<Long> times) {
468         Collections.sort(times);
469         long min = times.get(0);
470         long max = times.get(0);
471         for (long time : times) {
472             min = Math.min(min, time);
473             max = Math.max(max, time);
474         }
475         status.put(tag + "_" + key + "_" + SUFFIX_MIN, (double)min);
476         status.put(tag + "_" + key + "_" + SUFFIX_MAX, (double)max);
477         status.put(tag + "_" + key + "_" + SUFFIX_MEDIAN, (double)times.get(times.size() / 2));
478     }
479 
480     // Helper to convert surface flinger events to buffer events.
toBufferEvents( int type, List<RawEvent> rawEvents, List<BufferEvent> bufferEvents)481     private static void toBufferEvents(
482             int type, List<RawEvent> rawEvents, List<BufferEvent> bufferEvents) {
483         for (RawEvent event : rawEvents) {
484             if (event.mChildren.isEmpty()) {
485                 throw new IllegalStateException("Buffer name is expected");
486             }
487             final String bufferName = event.mChildren.get(0).mName;
488             if (bufferName.startsWith("SurfaceView - android.gameperformance")) {
489                 bufferEvents.add(
490                         new BufferEvent(type, event.mTime, event.mDuration, bufferName));
491             }
492         }
493     }
494 
findNextOfType(List<BufferEvent> events, int startIndex, int type)495     private static int findNextOfType(List<BufferEvent> events, int startIndex, int type) {
496         for (int i = startIndex; i < events.size(); ++i) {
497             if (events.get(i).mType == type) {
498                 return i;
499             }
500         }
501         return -1;
502     }
503 
findNextOfBufferId( List<BufferEvent> events, int startIndex, String bufferId)504     private static int findNextOfBufferId(
505             List<BufferEvent> events, int startIndex, String bufferId) {
506         for (int i = startIndex; i < events.size(); ++i) {
507             if (bufferId.equals(events.get(i).mBufferId)) {
508                 return i;
509             }
510         }
511         return -1;
512     }
513 
main(String[] args)514     public static void main(String[] args) {
515         if (args.length != 1) {
516             System.err.println("Usage: " + TAG + " atrace.log");
517             return;
518         }
519 
520         try {
521             System.out.println("Results:");
522             for (Map.Entry<?, ?> entry :
523                 processGraphicBufferResult(args[0], "default").entrySet()) {
524                 System.out.println("    " + entry.getKey() + " = " + entry.getValue());
525             }
526         } catch (IOException e) {
527             e.printStackTrace();
528         }
529     }
530 }
531