blob: dffce1acdec34845070e6b87f4c5bf83b131d13b [file] [log] [blame]
khmel@google.com017a9782018-09-28 11:32:01 -07001/*
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 */
16package android.gameperformance;
17
18import java.io.BufferedReader;
19import java.io.FileReader;
20import java.io.IOException;
21import java.util.ArrayList;
22import java.util.Arrays;
23import java.util.Collections;
24import java.util.Comparator;
25import java.util.HashMap;
26import java.util.List;
27import java.util.Map;
28import java.util.Set;
29import java.util.StringTokenizer;
30import java.util.TreeMap;
31import 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 */
63public 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
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 */
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
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
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
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 */
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 */
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 */
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 */
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 */
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
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.
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
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
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
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}