1 /* 2 * Copyright (C) 2015 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 17 package com.android.traceur; 18 19 import android.system.Os; 20 import android.util.Log; 21 22 import java.io.File; 23 import java.nio.file.Files; 24 import java.nio.file.Paths; 25 import java.util.Collection; 26 import java.util.List; 27 import java.util.TreeMap; 28 import java.util.concurrent.TimeUnit; 29 30 import perfetto.protos.DataSourceDescriptorOuterClass.DataSourceDescriptor; 31 import perfetto.protos.FtraceDescriptorOuterClass.FtraceDescriptor.AtraceCategory; 32 import perfetto.protos.TracingServiceStateOuterClass.TracingServiceState; 33 import perfetto.protos.TracingServiceStateOuterClass.TracingServiceState.DataSource; 34 35 /** 36 * Utility functions for calling Perfetto 37 */ 38 public class PerfettoUtils implements TraceUtils.TraceEngine { 39 40 static final String TAG = "Traceur"; 41 public static final String NAME = "PERFETTO"; 42 43 private static final String OUTPUT_EXTENSION = "perfetto-trace"; 44 private static final String TEMP_DIR = "/data/local/traces/"; 45 private static final String TEMP_TRACE_LOCATION = "/data/local/traces/.trace-in-progress.trace"; 46 47 private static final String PERFETTO_TAG = "traceur"; 48 private static final String MARKER = "PERFETTO_ARGUMENTS"; 49 private static final int LIST_TIMEOUT_MS = 10000; 50 private static final int STARTUP_TIMEOUT_MS = 10000; 51 private static final int STOP_TIMEOUT_MS = 30000; 52 private static final long MEGABYTES_TO_BYTES = 1024L * 1024L; 53 private static final long MINUTES_TO_MILLISECONDS = 60L * 1000L; 54 55 // The total amount of memory allocated to the two target buffers will be divided according to a 56 // ratio of (BUFFER_SIZE_RATIO - 1) to 1. 57 private static final int BUFFER_SIZE_RATIO = 32; 58 59 // atrace trace categories that will result in added data sources in the Perfetto config. 60 private static final String CAMERA_TAG = "camera"; 61 private static final String GFX_TAG = "gfx"; 62 private static final String MEMORY_TAG = "memory"; 63 private static final String NETWORK_TAG = "network"; 64 private static final String POWER_TAG = "power"; 65 private static final String SCHED_TAG = "sched"; 66 private static final String WEBVIEW_TAG = "webview"; 67 68 // Custom trace categories. 69 private static final String SYS_STATS_TAG = "sys_stats"; 70 private static final String LOG_TAG = "logs"; 71 private static final String CPU_TAG = "cpu"; 72 getName()73 public String getName() { 74 return NAME; 75 } 76 getOutputExtension()77 public String getOutputExtension() { 78 return OUTPUT_EXTENSION; 79 } 80 traceStart(Collection<String> tags, int bufferSizeKb, boolean apps, boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)81 public boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps, 82 boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb, 83 int maxLongTraceDurationMinutes) { 84 if (isTracingOn()) { 85 Log.e(TAG, "Attempting to start perfetto trace but trace is already in progress"); 86 return false; 87 } else { 88 recoverExistingRecording(); 89 } 90 91 StringBuilder config = new StringBuilder(); 92 appendBaseConfigOptions(config, attachToBugreport, longTrace, maxLongTraceSizeMb, 93 maxLongTraceDurationMinutes); 94 95 // The user chooses a per-CPU buffer size due to atrace limitations. 96 // So we use this to ensure that we reserve the correctly-sized buffer. 97 int numCpus = Runtime.getRuntime().availableProcessors(); 98 99 // Allots 1 / BUFFER_SIZE_RATIO to the small buffer and the remainder to the large buffer. 100 int totalBufferSizeKb = numCpus * bufferSizeKb; 101 int targetBuffer1Kb = totalBufferSizeKb / BUFFER_SIZE_RATIO; 102 int targetBuffer0Kb = totalBufferSizeKb - targetBuffer1Kb; 103 104 // This is target_buffer: 0, which is used for ftrace and the ftrace-derived 105 // android.gpu.memory. 106 appendTraceBuffer(config, targetBuffer0Kb); 107 108 // This is target_buffer: 1, which is used for additional data sources. 109 appendTraceBuffer(config, targetBuffer1Kb); 110 111 appendFtraceConfig(config, tags, apps); 112 appendProcStatsConfig(config, tags, /* target_buffer = */ 1); 113 appendAdditionalDataSources(config, tags, longTrace, /* target_buffer = */ 1); 114 115 return startPerfettoWithConfig(config.toString()); 116 } 117 stackSampleStart(boolean attachToBugreport)118 public boolean stackSampleStart(boolean attachToBugreport) { 119 if (isTracingOn()) { 120 Log.e(TAG, "Attemping to start stack sampling but perfetto is already active"); 121 return false; 122 } else { 123 recoverExistingRecording(); 124 } 125 126 StringBuilder config = new StringBuilder(); 127 appendBaseConfigOptions(config, attachToBugreport, /* longTrace = */ false, 128 /* maxLongTraceSizeMb */ 0, /* maxLongTraceDurationMinutes = */ 0); 129 130 // Number of cores * 16MiB. 16MiB was chosen as it is the default for Traceur traces. 131 int targetBufferKb = Runtime.getRuntime().availableProcessors() * (16 * 1024); 132 appendTraceBuffer(config, targetBufferKb); 133 134 appendLinuxPerfConfig(config, /* target_buffer = */ 0); 135 appendProcStatsConfig(config, /* tags = */ null, /* target_buffer = */ 0); 136 137 return startPerfettoWithConfig(config.toString()); 138 } 139 traceStop()140 public void traceStop() { 141 Log.v(TAG, "Stopping perfetto trace."); 142 143 if (!isTracingOn()) { 144 Log.w(TAG, "No trace appears to be in progress. Stopping perfetto trace may not work."); 145 } 146 147 String cmd = "perfetto --stop --attach=" + PERFETTO_TAG; 148 try { 149 Process process = TraceUtils.execWithTimeout(cmd, null, STOP_TIMEOUT_MS); 150 if (process != null && process.exitValue() != 0) { 151 Log.e(TAG, "perfetto traceStop failed with: " + process.exitValue()); 152 } 153 } catch (Exception e) { 154 throw new RuntimeException(e); 155 } 156 } 157 traceDump(File outFile)158 public boolean traceDump(File outFile) { 159 traceStop(); 160 161 // Short-circuit if a trace was not stopped. 162 if (isTracingOn()) { 163 Log.e(TAG, "Trace was not stopped successfully, aborting trace dump."); 164 return false; 165 } 166 167 // Short-circuit if the file we're trying to dump to doesn't exist. 168 if (!Files.exists(Paths.get(TEMP_TRACE_LOCATION))) { 169 Log.e(TAG, "In-progress trace file doesn't exist, aborting trace dump."); 170 return false; 171 } 172 173 Log.v(TAG, "Saving perfetto trace to " + outFile); 174 175 try { 176 Os.rename(TEMP_TRACE_LOCATION, outFile.getCanonicalPath()); 177 } catch (Exception e) { 178 throw new RuntimeException(e); 179 } 180 181 outFile.setReadable(true, false); // (readable, ownerOnly) 182 outFile.setWritable(true, false); // (writable, ownerOnly) 183 return true; 184 } 185 isTracingOn()186 public boolean isTracingOn() { 187 String cmd = "perfetto --is_detached=" + PERFETTO_TAG; 188 189 try { 190 Process process = TraceUtils.exec(cmd); 191 192 // 0 represents a detached process exists with this name 193 // 2 represents no detached process with this name 194 // 1 (or other error code) represents an error 195 int result = process.waitFor(); 196 if (result == 0) { 197 return true; 198 } else if (result == 2) { 199 return false; 200 } else { 201 throw new RuntimeException("Perfetto error: " + result); 202 } 203 } catch (Exception e) { 204 throw new RuntimeException(e); 205 } 206 } 207 perfettoListCategories()208 public static TreeMap<String,String> perfettoListCategories() { 209 String cmd = "perfetto --query-raw"; 210 211 Log.v(TAG, "Listing tags: " + cmd); 212 try { 213 214 TreeMap<String, String> result = new TreeMap<>(); 215 216 // execWithTimeout() cannot be used because stdout must be consumed before the process 217 // is terminated. 218 Process perfetto = TraceUtils.exec(cmd, null, false); 219 TracingServiceState serviceState = 220 TracingServiceState.parseFrom(perfetto.getInputStream()); 221 222 // Destroy the perfetto process if it times out. 223 if (!perfetto.waitFor(LIST_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { 224 Log.e(TAG, "perfettoListCategories timed out after " + LIST_TIMEOUT_MS + " ms."); 225 perfetto.destroyForcibly(); 226 return result; 227 } 228 229 // The perfetto process completed and failed, but does not need to be destroyed. 230 if (perfetto.exitValue() != 0) { 231 Log.e(TAG, "perfettoListCategories failed with: " + perfetto.exitValue()); 232 } 233 234 List<AtraceCategory> categories = null; 235 236 for (DataSource dataSource : serviceState.getDataSourcesList()) { 237 DataSourceDescriptor dataSrcDescriptor = dataSource.getDsDescriptor(); 238 if (dataSrcDescriptor.getName().equals("linux.ftrace")){ 239 categories = dataSrcDescriptor.getFtraceDescriptor().getAtraceCategoriesList(); 240 break; 241 } 242 } 243 244 if (categories != null) { 245 for (AtraceCategory category : categories) { 246 result.put(category.getName(), category.getDescription()); 247 } 248 } 249 return result; 250 } catch (Exception e) { 251 throw new RuntimeException(e); 252 } 253 } 254 255 // Starts Perfetto with the provided config string. startPerfettoWithConfig(String config)256 private boolean startPerfettoWithConfig(String config) { 257 // If the here-doc ends early, within the config string, exit immediately. 258 // This should never happen. 259 if (config.contains(MARKER)) { 260 throw new RuntimeException("The arguments to the Perfetto command are malformed."); 261 } 262 263 String cmd = "perfetto --detach=" + PERFETTO_TAG 264 + " -o " + TEMP_TRACE_LOCATION 265 + " -c - --txt" 266 + " <<" + MARKER +"\n" + config + "\n" + MARKER; 267 268 Log.v(TAG, "Starting perfetto trace."); 269 try { 270 Process process = TraceUtils.execWithTimeout(cmd, TEMP_DIR, STARTUP_TIMEOUT_MS); 271 if (process == null) { 272 return false; 273 } else if (process.exitValue() != 0) { 274 Log.e(TAG, "perfetto trace start failed with: " + process.exitValue()); 275 return false; 276 } 277 } catch (Exception e) { 278 throw new RuntimeException(e); 279 } 280 281 Log.v(TAG, "perfetto traceStart succeeded!"); 282 return true; 283 } 284 285 // Saves an existing temporary recording under a "recovered" filename. recoverExistingRecording()286 private void recoverExistingRecording() { 287 File recoveredFile = TraceUtils.getOutputFile( 288 TraceUtils.getRecoveredFilename()); 289 if (!traceDump(recoveredFile)) { 290 Log.w(TAG, "Failed to recover in-progress trace."); 291 } 292 } 293 294 // Appends options that can be used in any of Traceur's Perfetto configs. appendBaseConfigOptions(StringBuilder config, boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)295 private void appendBaseConfigOptions(StringBuilder config, boolean attachToBugreport, 296 boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes) { 297 config.append("write_into_file: true\n"); 298 299 // Ensure that we flush ftrace every 30s even if cpus are idle. 300 config.append("flush_period_ms: 30000\n"); 301 302 // If the user has flagged that in-progress trace sessions should be grabbed during 303 // bugreports, and BetterBug is present. 304 if (attachToBugreport) { 305 config.append("bugreport_score: 500\n"); 306 } 307 308 // Indicates that Perfetto should notify Traceur if the tracing session's status changes. 309 config.append("notify_traceur: true\n"); 310 311 // Allow previous trace contents to be referenced instead of duplicating. 312 config.append("incremental_state_config {\n") 313 .append(" clear_period_ms: 15000\n") 314 .append("}\n"); 315 316 // Add long/short trace-specific options. 317 if (longTrace) { 318 if (maxLongTraceSizeMb != 0) { 319 config.append("max_file_size_bytes: " 320 + (maxLongTraceSizeMb * MEGABYTES_TO_BYTES) + "\n"); 321 } 322 if (maxLongTraceDurationMinutes != 0) { 323 config.append("duration_ms: " 324 + (maxLongTraceDurationMinutes * MINUTES_TO_MILLISECONDS) 325 + "\n"); 326 } 327 328 // Default value for long traces to write to file. 329 config.append("file_write_period_ms: 1000\n"); 330 } else { 331 // For short traces, we don't write to the file. 332 // So, always use the maximum value here: 7 days. 333 config.append("file_write_period_ms: 604800000\n"); 334 } 335 } 336 337 // Specifies an additional buffer of size bufferSizeKb. Data sources can reference specific 338 // buffers in the order that they are added by this method. appendTraceBuffer(StringBuilder config, int bufferSizeKb)339 private void appendTraceBuffer(StringBuilder config, int bufferSizeKb) { 340 config.append("buffers {\n") 341 .append(" size_kb: " + bufferSizeKb + "\n") 342 .append(" fill_policy: RING_BUFFER\n") 343 .append("}\n"); 344 } 345 346 // Appends ftrace-related data sources to buffer 0 (linux.ftrace, android.gpu.memory). appendFtraceConfig(StringBuilder config, Collection<String> tags, boolean apps)347 private void appendFtraceConfig(StringBuilder config, Collection<String> tags, boolean apps) { 348 config.append("data_sources {\n") 349 .append(" config {\n") 350 .append(" name: \"linux.ftrace\"\n") 351 .append(" target_buffer: 0\n") 352 .append(" ftrace_config {\n") 353 .append(" symbolize_ksyms: true\n"); 354 355 for (String tag : tags) { 356 // Tags are expected to be only letters, numbers, and underscores. 357 String cleanTag = tag.replaceAll("[^a-zA-Z0-9_]", ""); 358 if (!cleanTag.equals(tag)) { 359 Log.w(TAG, "Attempting to use an invalid tag: " + tag); 360 } 361 config.append(" atrace_categories: \"" + cleanTag + "\"\n"); 362 } 363 364 if (apps) { 365 config.append(" atrace_apps: \"*\"\n"); 366 } 367 368 // Request a dense encoding of the common sched events (sched_switch, sched_waking). 369 if (tags.contains(SCHED_TAG)) { 370 config.append(" compact_sched {\n"); 371 config.append(" enabled: true\n"); 372 config.append(" }\n"); 373 } 374 375 // These parameters affect only the kernel trace buffer size and how 376 // frequently it gets moved into the userspace buffer defined above. 377 config.append(" buffer_size_kb: 8192\n") 378 .append(" }\n") 379 .append(" }\n") 380 .append("}\n") 381 .append("\n"); 382 383 // Captures initial counter values, updates are captured in ftrace. 384 if (tags.contains(MEMORY_TAG) || tags.contains(GFX_TAG)) { 385 config.append("data_sources: {\n") 386 .append(" config { \n") 387 .append(" name: \"android.gpu.memory\"\n") 388 .append(" target_buffer: 0\n") 389 .append(" }\n") 390 .append("}\n"); 391 } 392 } 393 394 // Appends the linux.process_stats data source to the specified target buffer. appendProcStatsConfig(StringBuilder config, Collection<String> tags, int targetBuffer)395 private void appendProcStatsConfig(StringBuilder config, Collection<String> tags, 396 int targetBuffer) { 397 boolean tagsContainsMemory = (tags != null) ? tags.contains(MEMORY_TAG) : false; 398 // For process association. If the memory tag is enabled, poll periodically instead of just 399 // once at the beginning. 400 config.append("data_sources {\n") 401 .append(" config {\n") 402 .append(" name: \"linux.process_stats\"\n") 403 .append(" target_buffer: " + targetBuffer + "\n") 404 .append(" process_stats_config {\n"); 405 if (tagsContainsMemory) { 406 config.append(" proc_stats_poll_ms: 60000\n"); 407 } else { 408 config.append(" scan_all_processes_on_start: true\n"); 409 } 410 config.append(" }\n") 411 .append(" }\n") 412 .append("}\n"); 413 } 414 415 // Appends the callstack-sampling data source. Sampling frequency is measured in Hz. appendLinuxPerfConfig(StringBuilder config, int targetBuffer)416 private void appendLinuxPerfConfig(StringBuilder config, int targetBuffer) { 417 config.append("data_sources: {\n") 418 .append(" config {\n") 419 .append(" name: \"linux.perf\"\n") 420 .append(" target_buffer: " + targetBuffer + "\n") 421 .append(" perf_event_config {\n") 422 .append(" all_cpus: true\n") 423 .append(" sampling_frequency: 100\n") 424 .append(" }\n") 425 .append(" }\n") 426 .append("}\n"); 427 } 428 429 // Appends additional data sources to the specified extra buffer based on enabled trace tags. appendAdditionalDataSources(StringBuilder config, Collection<String> tags, boolean longTrace, int targetBuffer)430 private void appendAdditionalDataSources(StringBuilder config, Collection<String> tags, 431 boolean longTrace, int targetBuffer) { 432 if (tags.contains(POWER_TAG)) { 433 config.append("data_sources: {\n") 434 .append(" config { \n") 435 .append(" name: \"android.power\"\n") 436 .append(" target_buffer: " + targetBuffer + "\n") 437 .append(" android_power_config {\n"); 438 if (longTrace) { 439 config.append(" battery_poll_ms: 5000\n"); 440 } else { 441 config.append(" battery_poll_ms: 1000\n"); 442 } 443 config.append(" collect_power_rails: true\n") 444 .append(" battery_counters: BATTERY_COUNTER_CAPACITY_PERCENT\n") 445 .append(" battery_counters: BATTERY_COUNTER_CHARGE\n") 446 .append(" battery_counters: BATTERY_COUNTER_CURRENT\n") 447 .append(" }\n") 448 .append(" }\n") 449 .append("}\n"); 450 } 451 452 if (tags.contains(SYS_STATS_TAG)) { 453 config.append("data_sources: {\n") 454 .append(" config { \n") 455 .append(" name: \"linux.sys_stats\"\n") 456 .append(" target_buffer: " + targetBuffer + "\n") 457 .append(" sys_stats_config {\n") 458 .append(" meminfo_period_ms: 1000\n") 459 .append(" vmstat_period_ms: 1000\n") 460 .append(" }\n") 461 .append(" }\n") 462 .append("}\n"); 463 } 464 465 if (tags.contains(LOG_TAG)) { 466 config.append("data_sources: {\n") 467 .append(" config {\n") 468 .append(" name: \"android.log\"\n") 469 .append(" target_buffer: " + targetBuffer + "\n") 470 .append(" }\n") 471 .append("}\n"); 472 } 473 474 if (tags.contains(CPU_TAG)) { 475 appendLinuxPerfConfig(config, /* target_buffer = */ 1); 476 } 477 478 if (tags.contains(GFX_TAG)) { 479 config.append("data_sources: {\n") 480 .append(" config { \n") 481 .append(" name: \"android.surfaceflinger.frametimeline\"\n") 482 .append(" target_buffer: " + targetBuffer + "\n") 483 .append(" }\n") 484 .append("}\n"); 485 } 486 487 if (tags.contains(CAMERA_TAG)) { 488 config.append("data_sources: {\n") 489 .append(" config { \n") 490 .append(" name: \"android.hardware.camera\"\n") 491 .append(" target_buffer: " + targetBuffer + "\n") 492 .append(" }\n") 493 .append("}\n"); 494 } 495 496 if (tags.contains(NETWORK_TAG)) { 497 config.append("data_sources: {\n") 498 .append(" config { \n") 499 .append(" name: \"android.network_packets\"\n") 500 .append(" target_buffer: " + targetBuffer + "\n") 501 .append(" network_packet_trace_config {\n") 502 .append(" poll_ms: 250\n") 503 .append(" }\n") 504 .append(" }\n") 505 .append("}\n"); 506 // Include the packages_list data source so that we can map UIDs 507 // from Network Tracing to the corresponding package name. 508 config.append("data_sources: {\n") 509 .append(" config { \n") 510 .append(" name: \"android.packages_list\"\n") 511 .append(" target_buffer: " + targetBuffer + "\n") 512 .append(" }\n") 513 .append("}\n"); 514 } 515 516 // Also enable Chrome events when the WebView tag is enabled. 517 if (tags.contains(WEBVIEW_TAG)) { 518 String chromeTraceConfig = "{" + 519 "\\\"record_mode\\\":\\\"record-continuously\\\"," + 520 "\\\"included_categories\\\":[\\\"*\\\"]" + 521 "}"; 522 config.append("data_sources: {\n") 523 .append(" config {\n") 524 .append(" name: \"org.chromium.trace_event\"\n") 525 .append(" target_buffer: " + targetBuffer + "\n") 526 .append(" chrome_config {\n") 527 .append(" trace_config: \"" + chromeTraceConfig + "\"\n") 528 .append(" }\n") 529 .append(" }\n") 530 .append("}\n") 531 .append("data_sources: {\n") 532 .append(" config {\n") 533 .append(" name: \"org.chromium.trace_metadata\"\n") 534 .append(" target_buffer: " + targetBuffer + "\n") 535 .append(" chrome_config {\n") 536 .append(" trace_config: \"" + chromeTraceConfig + "\"\n") 537 .append(" }\n") 538 .append(" }\n") 539 .append("}\n"); 540 } 541 } 542 } 543