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 private static final String CAMERA_TAG = "camera"; 56 private static final String GFX_TAG = "gfx"; 57 private static final String MEMORY_TAG = "memory"; 58 private static final String POWER_TAG = "power"; 59 private static final String SCHED_TAG = "sched"; 60 private static final String WEBVIEW_TAG = "webview"; 61 getName()62 public String getName() { 63 return NAME; 64 } 65 getOutputExtension()66 public String getOutputExtension() { 67 return OUTPUT_EXTENSION; 68 } 69 traceStart(Collection<String> tags, int bufferSizeKb, boolean apps, boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)70 public boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps, 71 boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb, 72 int maxLongTraceDurationMinutes) { 73 if (isTracingOn()) { 74 Log.e(TAG, "Attempting to start perfetto trace but trace is already in progress"); 75 return false; 76 } else { 77 // Ensure the temporary trace file is cleared. 78 try { 79 Files.deleteIfExists(Paths.get(TEMP_TRACE_LOCATION)); 80 } catch (Exception e) { 81 throw new RuntimeException(e); 82 } 83 } 84 85 // The user chooses a per-CPU buffer size due to atrace limitations. 86 // So we use this to ensure that we reserve the correctly-sized buffer. 87 int numCpus = Runtime.getRuntime().availableProcessors(); 88 89 // Build the perfetto config that will be passed on the command line. 90 StringBuilder config = new StringBuilder() 91 .append("write_into_file: true\n") 92 // Ensure that we flush ftrace data every 30s even if cpus are idle. 93 .append("flush_period_ms: 30000\n"); 94 95 // If the user has flagged that in-progress trace sessions should be grabbed 96 // during bugreports, and BetterBug is present. 97 if (attachToBugreport) { 98 config.append("bugreport_score: 500\n"); 99 } 100 101 // Indicates that perfetto should notify Traceur if the tracing session's status 102 // changes. 103 config.append("notify_traceur: true\n"); 104 105 if (longTrace) { 106 if (maxLongTraceSizeMb != 0) { 107 config.append("max_file_size_bytes: " 108 + (maxLongTraceSizeMb * MEGABYTES_TO_BYTES) + "\n"); 109 } 110 111 if (maxLongTraceDurationMinutes != 0) { 112 config.append("duration_ms: " 113 + (maxLongTraceDurationMinutes * MINUTES_TO_MILLISECONDS) 114 + "\n"); 115 } 116 117 // Default value for long traces to write to file. 118 config.append("file_write_period_ms: 1000\n"); 119 } else { 120 // For short traces, we don't write to the file. 121 // So, always use the maximum value here: 7 days. 122 config.append("file_write_period_ms: 604800000\n"); 123 } 124 125 config.append("incremental_state_config {\n") 126 .append(" clear_period_ms: 15000\n") 127 .append("} \n") 128 // This is target_buffer: 0, which is used for ftrace and the ftrace-derived 129 // android.gpu.memory. 130 .append("buffers {\n") 131 .append(" size_kb: " + bufferSizeKb * numCpus + "\n") 132 .append(" fill_policy: RING_BUFFER\n") 133 .append("} \n") 134 // This is target_buffer: 1, which is used for additional data sources. 135 .append("buffers {\n") 136 .append(" size_kb: 2048\n") 137 .append(" fill_policy: RING_BUFFER\n") 138 .append("} \n") 139 .append("data_sources {\n") 140 .append(" config {\n") 141 .append(" name: \"linux.ftrace\"\n") 142 .append(" target_buffer: 0\n") 143 .append(" ftrace_config {\n") 144 .append(" symbolize_ksyms: true\n"); 145 146 for (String tag : tags) { 147 // Tags are expected to be only letters, numbers, and underscores. 148 String cleanTag = tag.replaceAll("[^a-zA-Z0-9_]", ""); 149 if (!cleanTag.equals(tag)) { 150 Log.w(TAG, "Attempting to use an invalid tag: " + tag); 151 } 152 config.append(" atrace_categories: \"" + cleanTag + "\"\n"); 153 } 154 155 if (apps) { 156 config.append(" atrace_apps: \"*\"\n"); 157 } 158 159 // Request a dense encoding of the common sched events (sched_switch, sched_waking). 160 if (tags.contains(SCHED_TAG)) { 161 config.append(" compact_sched {\n"); 162 config.append(" enabled: true\n"); 163 config.append(" }\n"); 164 } 165 166 // These parameters affect only the kernel trace buffer size and how 167 // frequently it gets moved into the userspace buffer defined above. 168 config.append(" buffer_size_kb: 8192\n") 169 .append(" drain_period_ms: 1000\n") 170 .append(" }\n") 171 .append(" }\n") 172 .append("}\n") 173 .append(" \n"); 174 175 // Captures initial counter values, updates are captured in ftrace. 176 if (tags.contains(MEMORY_TAG) || tags.contains(GFX_TAG)) { 177 config.append("data_sources: {\n") 178 .append(" config { \n") 179 .append(" name: \"android.gpu.memory\"\n") 180 .append(" target_buffer: 0\n") 181 .append(" }\n") 182 .append("}\n"); 183 } 184 185 // For process association. If the memory tag is enabled, 186 // poll periodically instead of just once at the beginning. 187 config.append("data_sources {\n") 188 .append(" config {\n") 189 .append(" name: \"linux.process_stats\"\n") 190 .append(" target_buffer: 1\n"); 191 if (tags.contains(MEMORY_TAG)) { 192 config.append(" process_stats_config {\n") 193 .append(" proc_stats_poll_ms: 60000\n") 194 .append(" }\n"); 195 } 196 config.append(" }\n") 197 .append("} \n"); 198 199 if (tags.contains(POWER_TAG)) { 200 config.append("data_sources: {\n") 201 .append(" config { \n") 202 .append(" name: \"android.power\"\n") 203 .append(" target_buffer: 1\n") 204 .append(" android_power_config {\n"); 205 if (longTrace) { 206 config.append(" battery_poll_ms: 5000\n"); 207 } else { 208 config.append(" battery_poll_ms: 1000\n"); 209 } 210 config.append(" collect_power_rails: true\n") 211 .append(" battery_counters: BATTERY_COUNTER_CAPACITY_PERCENT\n") 212 .append(" battery_counters: BATTERY_COUNTER_CHARGE\n") 213 .append(" battery_counters: BATTERY_COUNTER_CURRENT\n") 214 .append(" }\n") 215 .append(" }\n") 216 .append("}\n"); 217 } 218 219 if (tags.contains(MEMORY_TAG)) { 220 config.append("data_sources: {\n") 221 .append(" config { \n") 222 .append(" name: \"android.sys_stats\"\n") 223 .append(" target_buffer: 1\n") 224 .append(" sys_stats_config {\n") 225 .append(" vmstat_period_ms: 1000\n") 226 .append(" }\n") 227 .append(" }\n") 228 .append("}\n"); 229 } 230 231 if (tags.contains(GFX_TAG)) { 232 config.append("data_sources: {\n") 233 .append(" config { \n") 234 .append(" name: \"android.surfaceflinger.frametimeline\"\n") 235 .append(" }\n") 236 .append("}\n"); 237 } 238 239 if (tags.contains(CAMERA_TAG)) { 240 config.append("data_sources: {\n") 241 .append(" config { \n") 242 .append(" name: \"android.hardware.camera\"\n") 243 .append(" target_buffer: 1\n") 244 .append(" }\n") 245 .append("}\n"); 246 } 247 248 // Also enable Chrome events when the WebView tag is enabled. 249 if (tags.contains(WEBVIEW_TAG)) { 250 String chromeTraceConfig = "{" + 251 "\\\"record_mode\\\":\\\"record-continuously\\\"," + 252 "\\\"included_categories\\\":[\\\"*\\\"]" + 253 "}"; 254 config.append("data_sources: {\n") 255 .append(" config {\n") 256 .append(" name: \"org.chromium.trace_event\"\n") 257 .append(" chrome_config {\n") 258 .append(" trace_config: \"" + chromeTraceConfig + "\"\n") 259 .append(" }\n") 260 .append(" }\n") 261 .append("}\n") 262 .append("data_sources: {\n") 263 .append(" config {\n") 264 .append(" name: \"org.chromium.trace_metadata\"\n") 265 .append(" chrome_config {\n") 266 .append(" trace_config: \"" + chromeTraceConfig + "\"\n") 267 .append(" }\n") 268 .append(" }\n") 269 .append("}\n"); 270 } 271 272 String configString = config.toString(); 273 274 // If the here-doc ends early, within the config string, exit immediately. 275 // This should never happen. 276 if (configString.contains(MARKER)) { 277 throw new RuntimeException("The arguments to the Perfetto command are malformed."); 278 } 279 280 String cmd = "perfetto --detach=" + PERFETTO_TAG 281 + " -o " + TEMP_TRACE_LOCATION 282 + " -c - --txt" 283 + " <<" + MARKER +"\n" + configString + "\n" + MARKER; 284 285 Log.v(TAG, "Starting perfetto trace."); 286 try { 287 Process process = TraceUtils.execWithTimeout(cmd, TEMP_DIR, STARTUP_TIMEOUT_MS); 288 if (process == null) { 289 return false; 290 } else if (process.exitValue() != 0) { 291 Log.e(TAG, "perfetto traceStart failed with: " + process.exitValue()); 292 return false; 293 } 294 } catch (Exception e) { 295 throw new RuntimeException(e); 296 } 297 298 Log.v(TAG, "perfetto traceStart succeeded!"); 299 return true; 300 } 301 traceStop()302 public void traceStop() { 303 Log.v(TAG, "Stopping perfetto trace."); 304 305 if (!isTracingOn()) { 306 Log.w(TAG, "No trace appears to be in progress. Stopping perfetto trace may not work."); 307 } 308 309 String cmd = "perfetto --stop --attach=" + PERFETTO_TAG; 310 try { 311 Process process = TraceUtils.execWithTimeout(cmd, null, STOP_TIMEOUT_MS); 312 if (process != null && process.exitValue() != 0) { 313 Log.e(TAG, "perfetto traceStop failed with: " + process.exitValue()); 314 } 315 } catch (Exception e) { 316 throw new RuntimeException(e); 317 } 318 } 319 traceDump(File outFile)320 public boolean traceDump(File outFile) { 321 traceStop(); 322 323 // Short-circuit if a trace was not stopped. 324 if (isTracingOn()) { 325 Log.e(TAG, "Trace was not stopped successfully, aborting trace dump."); 326 return false; 327 } 328 329 // Short-circuit if the file we're trying to dump to doesn't exist. 330 if (!Files.exists(Paths.get(TEMP_TRACE_LOCATION))) { 331 Log.e(TAG, "In-progress trace file doesn't exist, aborting trace dump."); 332 return false; 333 } 334 335 Log.v(TAG, "Saving perfetto trace to " + outFile); 336 337 try { 338 Os.rename(TEMP_TRACE_LOCATION, outFile.getCanonicalPath()); 339 } catch (Exception e) { 340 throw new RuntimeException(e); 341 } 342 343 outFile.setReadable(true, false); // (readable, ownerOnly) 344 outFile.setWritable(true, false); // (readable, ownerOnly) 345 return true; 346 } 347 isTracingOn()348 public boolean isTracingOn() { 349 String cmd = "perfetto --is_detached=" + PERFETTO_TAG; 350 351 try { 352 Process process = TraceUtils.exec(cmd); 353 354 // 0 represents a detached process exists with this name 355 // 2 represents no detached process with this name 356 // 1 (or other error code) represents an error 357 int result = process.waitFor(); 358 if (result == 0) { 359 return true; 360 } else if (result == 2) { 361 return false; 362 } else { 363 throw new RuntimeException("Perfetto error: " + result); 364 } 365 } catch (Exception e) { 366 throw new RuntimeException(e); 367 } 368 } 369 perfettoListCategories()370 public static TreeMap<String,String> perfettoListCategories() { 371 String cmd = "perfetto --query-raw"; 372 373 Log.v(TAG, "Listing tags: " + cmd); 374 try { 375 376 TreeMap<String, String> result = new TreeMap<>(); 377 378 // execWithTimeout() cannot be used because stdout must be consumed before the process 379 // is terminated. 380 Process perfetto = TraceUtils.exec(cmd, null, false); 381 TracingServiceState serviceState = 382 TracingServiceState.parseFrom(perfetto.getInputStream()); 383 384 // Destroy the perfetto process if it times out. 385 if (!perfetto.waitFor(LIST_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { 386 Log.e(TAG, "perfettoListCategories timed out after " + LIST_TIMEOUT_MS + " ms."); 387 perfetto.destroyForcibly(); 388 return result; 389 } 390 391 // The perfetto process completed and failed, but does not need to be destroyed. 392 if (perfetto.exitValue() != 0) { 393 Log.e(TAG, "perfettoListCategories failed with: " + perfetto.exitValue()); 394 } 395 396 List<AtraceCategory> categories = null; 397 398 for (DataSource dataSource : serviceState.getDataSourcesList()) { 399 DataSourceDescriptor dataSrcDescriptor = dataSource.getDsDescriptor(); 400 if (dataSrcDescriptor.getName().equals("linux.ftrace")){ 401 categories = dataSrcDescriptor.getFtraceDescriptor().getAtraceCategoriesList(); 402 break; 403 } 404 } 405 406 if (categories != null) { 407 for (AtraceCategory category : categories) { 408 result.put(category.getName(), category.getDescription()); 409 } 410 } 411 return result; 412 } catch (Exception e) { 413 throw new RuntimeException(e); 414 } 415 } 416 } 417