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.io.IOException;
24 import java.nio.file.Files;
25 import java.nio.file.Path;
26 import java.nio.file.Paths;
27 import java.util.Collection;
28 import java.util.concurrent.TimeUnit;
29 
30 /**
31  * Utility functions for calling Perfetto
32  */
33 public class PerfettoUtils implements TraceUtils.TraceEngine {
34 
35     static final String TAG = "Traceur";
36     public static final String NAME = "PERFETTO";
37 
38     private static final String OUTPUT_EXTENSION = "perfetto-trace";
39     private static final String TEMP_DIR= "/data/local/traces/";
40     private static final String TEMP_TRACE_LOCATION = "/data/local/traces/.trace-in-progress.trace";
41 
42     private static final String PERFETTO_TAG = "traceur";
43     private static final String MARKER = "PERFETTO_ARGUMENTS";
44     private static final int STARTUP_TIMEOUT_MS = 10000;
45     private static final long MEGABYTES_TO_BYTES = 1024L * 1024L;
46     private static final long MINUTES_TO_MILLISECONDS = 60L * 1000L;
47 
48     private static final String GFX_TAG = "gfx";
49     private static final String MEMORY_TAG = "memory";
50     private static final String POWER_TAG = "power";
51     private static final String SCHED_TAG = "sched";
52 
getName()53     public String getName() {
54         return NAME;
55     }
56 
getOutputExtension()57     public String getOutputExtension() {
58         return OUTPUT_EXTENSION;
59     }
60 
traceStart(Collection<String> tags, int bufferSizeKb, boolean apps, boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)61     public boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps,
62             boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb,
63             int maxLongTraceDurationMinutes) {
64         if (isTracingOn()) {
65             Log.e(TAG, "Attempting to start perfetto trace but trace is already in progress");
66             return false;
67         } else {
68             // Ensure the temporary trace file is cleared.
69             try {
70                 Files.deleteIfExists(Paths.get(TEMP_TRACE_LOCATION));
71             } catch (Exception e) {
72                 throw new RuntimeException(e);
73             }
74         }
75 
76         // The user chooses a per-CPU buffer size due to atrace limitations.
77         // So we use this to ensure that we reserve the correctly-sized buffer.
78         int numCpus = Runtime.getRuntime().availableProcessors();
79 
80         // Build the perfetto config that will be passed on the command line.
81         StringBuilder config = new StringBuilder()
82             .append("write_into_file: true\n")
83             // Ensure that we flush ftrace data every 30s even if cpus are idle.
84             .append("flush_period_ms: 30000\n");
85 
86             // If the user has flagged that in-progress trace sessions should be grabbed
87             // during bugreports, and BetterBug is present.
88             if (attachToBugreport) {
89                 config.append("bugreport_score: 500\n");
90             }
91 
92             // Indicates that perfetto should notify Traceur if the tracing session's status
93             // changes.
94             config.append("notify_traceur: true\n");
95 
96             if (longTrace) {
97                 if (maxLongTraceSizeMb != 0) {
98                     config.append("max_file_size_bytes: "
99                         + (maxLongTraceSizeMb * MEGABYTES_TO_BYTES) + "\n");
100                 }
101 
102                 if (maxLongTraceDurationMinutes != 0) {
103                     config.append("duration_ms: "
104                         + (maxLongTraceDurationMinutes * MINUTES_TO_MILLISECONDS)
105                         + "\n");
106                 }
107 
108                 // Default value for long traces to write to file.
109                 config.append("file_write_period_ms: 1000\n");
110             } else {
111                 // For short traces, we don't write to the file.
112                 // So, always use the maximum value here: 7 days.
113                 config.append("file_write_period_ms: 604800000\n");
114             }
115 
116         config.append("incremental_state_config {\n")
117             .append("  clear_period_ms: 15000\n")
118             .append("} \n")
119             // This is target_buffer: 0, which is used for ftrace and the ftrace-derived
120             // android.gpu.memory.
121             .append("buffers {\n")
122             .append("  size_kb: " + bufferSizeKb * numCpus + "\n")
123             .append("  fill_policy: RING_BUFFER\n")
124             .append("} \n")
125             // This is target_buffer: 1, which is used for additional data sources.
126             .append("buffers {\n")
127             .append("  size_kb: 2048\n")
128             .append("  fill_policy: RING_BUFFER\n")
129             .append("} \n")
130             .append("data_sources {\n")
131             .append("  config {\n")
132             .append("    name: \"linux.ftrace\"\n")
133             .append("    target_buffer: 0\n")
134             .append("    ftrace_config {\n")
135             .append("      symbolize_ksyms: true\n");
136 
137         for (String tag : tags) {
138             // Tags are expected to be only letters, numbers, and underscores.
139             String cleanTag = tag.replaceAll("[^a-zA-Z0-9_]", "");
140             if (!cleanTag.equals(tag)) {
141                 Log.w(TAG, "Attempting to use an invalid tag: " + tag);
142             }
143             config.append("      atrace_categories: \"" + cleanTag + "\"\n");
144         }
145 
146         if (apps) {
147             config.append("      atrace_apps: \"*\"\n");
148         }
149 
150         // Request a dense encoding of the common sched events (sched_switch, sched_waking).
151         if (tags.contains(SCHED_TAG)) {
152             config.append("      compact_sched {\n");
153             config.append("        enabled: true\n");
154             config.append("      }\n");
155         }
156 
157         // These parameters affect only the kernel trace buffer size and how
158         // frequently it gets moved into the userspace buffer defined above.
159         config.append("      buffer_size_kb: 8192\n")
160             .append("      drain_period_ms: 1000\n")
161             .append("    }\n")
162             .append("  }\n")
163             .append("}\n")
164             .append(" \n");
165 
166         // Captures initial counter values, updates are captured in ftrace.
167         if (tags.contains(MEMORY_TAG) || tags.contains(GFX_TAG)) {
168              config.append("data_sources: {\n")
169                 .append("  config { \n")
170                 .append("    name: \"android.gpu.memory\"\n")
171                 .append("    target_buffer: 0\n")
172                 .append("  }\n")
173                 .append("}\n");
174         }
175 
176         // For process association. If the memory tag is enabled,
177         // poll periodically instead of just once at the beginning.
178         config.append("data_sources {\n")
179             .append("  config {\n")
180             .append("    name: \"linux.process_stats\"\n")
181             .append("    target_buffer: 1\n");
182         if (tags.contains(MEMORY_TAG)) {
183             config.append("    process_stats_config {\n")
184                 .append("      proc_stats_poll_ms: 60000\n")
185                 .append("    }\n");
186         }
187         config.append("  }\n")
188             .append("} \n");
189 
190         if (tags.contains(POWER_TAG)) {
191             config.append("data_sources: {\n")
192                 .append("  config { \n")
193                 .append("    name: \"android.power\"\n")
194                 .append("    target_buffer: 1\n")
195                 .append("    android_power_config {\n");
196             if (longTrace) {
197                 config.append("      battery_poll_ms: 5000\n");
198             } else {
199                 config.append("      battery_poll_ms: 1000\n");
200             }
201             config.append("      collect_power_rails: true\n")
202                 .append("      battery_counters: BATTERY_COUNTER_CAPACITY_PERCENT\n")
203                 .append("      battery_counters: BATTERY_COUNTER_CHARGE\n")
204                 .append("      battery_counters: BATTERY_COUNTER_CURRENT\n")
205                 .append("    }\n")
206                 .append("  }\n")
207                 .append("}\n");
208         }
209 
210         if (tags.contains(MEMORY_TAG)) {
211             config.append("data_sources: {\n")
212                 .append("  config { \n")
213                 .append("    name: \"android.sys_stats\"\n")
214                 .append("    target_buffer: 1\n")
215                 .append("    sys_stats_config {\n")
216                 .append("      vmstat_period_ms: 1000\n")
217                 .append("    }\n")
218                 .append("  }\n")
219                 .append("}\n");
220         }
221 
222         if (tags.contains(GFX_TAG)) {
223           config.append("data_sources: {\n")
224               .append("  config { \n")
225               .append("    name: \"android.surfaceflinger.frametimeline\"\n")
226               .append("  }\n")
227               .append("}\n");
228         }
229 
230         String configString = config.toString();
231 
232         // If the here-doc ends early, within the config string, exit immediately.
233         // This should never happen.
234         if (configString.contains(MARKER)) {
235             throw new RuntimeException("The arguments to the Perfetto command are malformed.");
236         }
237 
238         String cmd = "perfetto --detach=" + PERFETTO_TAG
239             + " -o " + TEMP_TRACE_LOCATION
240             + " -c - --txt"
241             + " <<" + MARKER +"\n" + configString + "\n" + MARKER;
242 
243         Log.v(TAG, "Starting perfetto trace.");
244         try {
245             Process process = TraceUtils.exec(cmd, TEMP_DIR);
246 
247             // If we time out, ensure that the perfetto process is destroyed.
248             if (!process.waitFor(STARTUP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
249                 Log.e(TAG, "perfetto traceStart has timed out after "
250                     + STARTUP_TIMEOUT_MS + " ms.");
251                 process.destroyForcibly();
252                 return false;
253             }
254 
255             if (process.exitValue() != 0) {
256                 Log.e(TAG, "perfetto traceStart failed with: "
257                     + process.exitValue());
258                 return false;
259             }
260         } catch (Exception e) {
261             throw new RuntimeException(e);
262         }
263 
264         Log.v(TAG, "perfetto traceStart succeeded!");
265         return true;
266     }
267 
traceStop()268     public void traceStop() {
269         Log.v(TAG, "Stopping perfetto trace.");
270 
271         if (!isTracingOn()) {
272             Log.w(TAG, "No trace appears to be in progress. Stopping perfetto trace may not work.");
273         }
274 
275         String cmd = "perfetto --stop --attach=" + PERFETTO_TAG;
276         try {
277             Process process = TraceUtils.exec(cmd);
278             if (process.waitFor() != 0) {
279                 Log.e(TAG, "perfetto traceStop failed with: " + process.exitValue());
280             }
281         } catch (Exception e) {
282             throw new RuntimeException(e);
283         }
284     }
285 
traceDump(File outFile)286     public boolean traceDump(File outFile) {
287         traceStop();
288 
289         // Short-circuit if the file we're trying to dump to doesn't exist.
290         if (!Files.exists(Paths.get(TEMP_TRACE_LOCATION))) {
291             Log.e(TAG, "In-progress trace file doesn't exist, aborting trace dump.");
292             return false;
293         }
294 
295         Log.v(TAG, "Saving perfetto trace to " + outFile);
296 
297         try {
298             Os.rename(TEMP_TRACE_LOCATION, outFile.getCanonicalPath());
299         } catch (Exception e) {
300             throw new RuntimeException(e);
301         }
302 
303         outFile.setReadable(true, false); // (readable, ownerOnly)
304         outFile.setWritable(true, false); // (readable, ownerOnly)
305         return true;
306     }
307 
isTracingOn()308     public boolean isTracingOn() {
309         String cmd = "perfetto --is_detached=" + PERFETTO_TAG;
310 
311         try {
312             Process process = TraceUtils.exec(cmd);
313 
314             // 0 represents a detached process exists with this name
315             // 2 represents no detached process with this name
316             // 1 (or other error code) represents an error
317             int result = process.waitFor();
318             if (result == 0) {
319                 return true;
320             } else if (result == 2) {
321                 return false;
322             } else {
323                 throw new RuntimeException("Perfetto error: " + result);
324             }
325         } catch (Exception e) {
326             throw new RuntimeException(e);
327         }
328     }
329 }
330