• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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