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