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.app.ActivityManager;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.os.Build;
23 import android.os.FileUtils;
24 import android.text.format.DateUtils;
25 import android.util.Log;
26 
27 import java.io.BufferedReader;
28 import java.io.File;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.InputStreamReader;
32 import java.io.OutputStream;
33 import java.text.SimpleDateFormat;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.Date;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Optional;
42 import java.util.Set;
43 import java.util.TreeMap;
44 import java.util.concurrent.ExecutorService;
45 import java.util.concurrent.Executors;
46 import java.util.concurrent.FutureTask;
47 import java.util.concurrent.TimeUnit;
48 import java.util.stream.Collectors;
49 
50 import perfetto.protos.TraceConfigOuterClass.TraceConfig;
51 
52 /**
53  * Utility functions for tracing.
54  */
55 public class TraceUtils {
56 
57     static final String TAG = "Traceur";
58 
59     public static final String TRACE_DIRECTORY = "/data/local/traces/";
60 
61     private static PerfettoUtils mTraceEngine = new PerfettoUtils();
62 
63     private static final Runtime RUNTIME = Runtime.getRuntime();
64 
65     // The number of files to keep when clearing old traces.
66     private static final int MIN_KEEP_COUNT = 0;
67 
68     // The age that old traces should be cleared at.
69     private static final long MIN_KEEP_AGE = 4 * DateUtils.WEEK_IN_MILLIS;
70 
71     public enum RecordingType {
72         UNKNOWN, TRACE, STACK_SAMPLES, HEAP_DUMP
73     }
74 
75     public enum PresetTraceType {
76         UNSET, PERFORMANCE, BATTERY, THERMAL, UI
77     }
78 
presetTraceStart(Context context, PresetTraceType type)79     public static boolean presetTraceStart(Context context, PresetTraceType type) {
80         Set<String> tags;
81         PresetTraceConfigs.TraceOptions options;
82         Log.v(TAG, "Using preset of type " + type.toString());
83         switch (type) {
84             case PERFORMANCE:
85                 tags = PresetTraceConfigs.getPerformanceTags();
86                 options = PresetTraceConfigs.getPerformanceOptions();
87                 break;
88             case BATTERY:
89                 tags = PresetTraceConfigs.getBatteryTags();
90                 options = PresetTraceConfigs.getBatteryOptions();
91                 break;
92             case THERMAL:
93                 tags = PresetTraceConfigs.getThermalTags();
94                 options = PresetTraceConfigs.getThermalOptions();
95                 break;
96             case UI:
97                 tags = PresetTraceConfigs.getUiTags();
98                 options = PresetTraceConfigs.getUiOptions();
99                 break;
100             case UNSET:
101             default:
102                 tags = PresetTraceConfigs.getDefaultTags();
103                 options = PresetTraceConfigs.getDefaultOptions();
104         }
105         return traceStart(context, tags, options.bufferSizeKb, options.winscope,
106             options.apps, /* options.longTrace --> b/343538743 */ false, options.attachToBugreport,
107             options.maxLongTraceSizeMb, options.maxLongTraceDurationMinutes);
108     }
109 
traceStart(Context context, TraceConfig config, boolean winscope)110     public static boolean traceStart(Context context, TraceConfig config, boolean winscope) {
111         // 'winscope' isn't passed to traceStart because the TraceConfig should specify any
112         // winscope-related data sources to be recorded using Perfetto. Winscope data that isn't yet
113         // available in Perfetto is captured using WinscopeUtils instead.
114         if (!mTraceEngine.traceStart(config)) {
115             return false;
116         }
117         WinscopeUtils.traceStart(context, winscope);
118         return true;
119     }
120 
traceStart(Context context, Collection<String> tags, int bufferSizeKb, boolean winscope, boolean apps, boolean longTrace, boolean attachToBugreport, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)121     public static boolean traceStart(Context context, Collection<String> tags,
122             int bufferSizeKb, boolean winscope, boolean apps, boolean longTrace,
123             boolean attachToBugreport, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes) {
124         if (!mTraceEngine.traceStart(tags, bufferSizeKb, winscope, apps, longTrace,
125                 attachToBugreport, maxLongTraceSizeMb, maxLongTraceDurationMinutes)) {
126             return false;
127         }
128         WinscopeUtils.traceStart(context, winscope);
129         return true;
130     }
131 
stackSampleStart(boolean attachToBugreport)132     public static boolean stackSampleStart(boolean attachToBugreport) {
133         return mTraceEngine.stackSampleStart(attachToBugreport);
134     }
135 
heapDumpStart(Collection<String> processes, boolean continuousDump, int dumpIntervalSeconds, boolean attachToBugreport)136     public static boolean heapDumpStart(Collection<String> processes, boolean continuousDump,
137             int dumpIntervalSeconds, boolean attachToBugreport) {
138         return mTraceEngine.heapDumpStart(processes, continuousDump, dumpIntervalSeconds,
139                 attachToBugreport);
140     }
141 
traceStop(Context context)142     public static void traceStop(Context context) {
143         mTraceEngine.traceStop();
144         WinscopeUtils.traceStop(context);
145     }
146 
traceDump(Context context, String outFilename)147     public static Optional<List<File>> traceDump(Context context, String outFilename) {
148         File outFile = TraceUtils.getOutputFile(outFilename);
149         if (!mTraceEngine.traceDump(outFile)) {
150             return Optional.empty();
151         }
152 
153         List<File> outFiles = new ArrayList();
154         outFiles.add(outFile);
155 
156         List<File> outLegacyWinscopeFiles = WinscopeUtils.traceDump(context, outFilename);
157         outFiles.addAll(outLegacyWinscopeFiles);
158 
159         return Optional.of(outFiles);
160     }
161 
isTracingOn()162     public static boolean isTracingOn() {
163         return mTraceEngine.isTracingOn();
164     }
165 
listCategories()166     public static TreeMap<String, String> listCategories() {
167         TreeMap<String, String> categories = PerfettoUtils.perfettoListCategories();
168         categories.put("sys_stats", "meminfo, psi, and vmstats");
169         categories.put("logs", "android logcat");
170         categories.put("cpu", "callstack samples");
171         return categories;
172     }
173 
clearSavedTraces()174     public static void clearSavedTraces() {
175         String cmd = "rm -f " + TRACE_DIRECTORY + "trace-*.*trace " +
176                 TRACE_DIRECTORY + "recovered-trace*.*trace " +
177                 TRACE_DIRECTORY + "stack-samples*.*trace " +
178                 TRACE_DIRECTORY + "heap-dump*.*trace";
179 
180         Log.v(TAG, "Clearing trace directory: " + cmd);
181         try {
182             Process rm = exec(cmd);
183 
184             if (rm.waitFor() != 0) {
185                 Log.e(TAG, "clearSavedTraces failed with: " + rm.exitValue());
186             }
187         } catch (Exception e) {
188             throw new RuntimeException(e);
189         }
190     }
191 
exec(String cmd)192     public static Process exec(String cmd) throws IOException {
193         return exec(cmd, null);
194     }
195 
exec(String cmd, String tmpdir)196     public static Process exec(String cmd, String tmpdir) throws IOException {
197         return exec(cmd, tmpdir, true);
198     }
199 
exec(String cmd, String tmpdir, boolean logOutput)200     public static Process exec(String cmd, String tmpdir, boolean logOutput) throws IOException {
201         String[] cmdarray = {"sh", "-c", cmd};
202         String[] envp = {"TMPDIR=" + tmpdir};
203         envp = tmpdir == null ? null : envp;
204 
205         Log.v(TAG, "exec: " + Arrays.toString(envp) + " " + Arrays.toString(cmdarray));
206 
207         Process process = RUNTIME.exec(cmdarray, envp);
208         new Logger("traceService:stderr", process.getErrorStream());
209         if (logOutput) {
210             new Logger("traceService:stdout", process.getInputStream());
211         }
212 
213         return process;
214     }
215 
execWithTimeout(String cmd, String tmpdir, long timeout)216     public static Process execWithTimeout(String cmd, String tmpdir, long timeout)
217             throws IOException {
218         return execWithTimeout(cmd, tmpdir, timeout, null);
219     }
220 
221     // Returns the Process if the command terminated on time and null if not.
execWithTimeout(String cmd, String tmpdir, long timeout, byte[] input)222     public static Process execWithTimeout(String cmd, String tmpdir, long timeout, byte[] input)
223             throws IOException {
224         Process process = exec(cmd, tmpdir, true);
225         try {
226             if (input != null) {
227                 OutputStream os = process.getOutputStream();
228                 os.write(input);
229                 os.flush();
230                 os.close();
231             }
232             if (!process.waitFor(timeout, TimeUnit.MILLISECONDS)) {
233                 Log.e(TAG, "Command '" + cmd + "' has timed out after " + timeout + " ms.");
234                 process.destroyForcibly();
235                 // Return null to signal a timeout and that the Process was destroyed.
236                 return null;
237             }
238         } catch (Exception e) {
239             throw new RuntimeException(e);
240         }
241         return process;
242     }
243 
getOutputFilename(RecordingType type)244     public static String getOutputFilename(RecordingType type) {
245         String prefix;
246         switch (type) {
247             case TRACE:
248                 prefix = "trace";
249                 break;
250             case STACK_SAMPLES:
251                 prefix = "stack-samples";
252                 break;
253             case HEAP_DUMP:
254                 prefix = "heap-dump";
255                 break;
256             case UNKNOWN:
257             default:
258                 prefix = "recording";
259                 break;
260         }
261         String format = "yyyy-MM-dd-HH-mm-ss";
262         String now = new SimpleDateFormat(format, Locale.US).format(new Date());
263         return String.format("%s-%s-%s-%s.%s", prefix, Build.BOARD, Build.ID, now,
264             mTraceEngine.getOutputExtension());
265     }
266 
getRecoveredFilename()267     public static String getRecoveredFilename() {
268         // Knowing what the previous Traceur session was recording would require adding a
269         // recordingWasTrace parameter to TraceUtils.traceStart().
270         return "recovered-" + getOutputFilename(RecordingType.UNKNOWN);
271     }
272 
getOutputFile(String filename)273     public static File getOutputFile(String filename) {
274         return new File(TraceUtils.TRACE_DIRECTORY, filename);
275     }
276 
cleanupOlderFiles()277     protected static void cleanupOlderFiles() {
278         FutureTask<Void> task = new FutureTask<Void>(
279                 () -> {
280                     try {
281                         FileUtils.deleteOlderFiles(new File(TRACE_DIRECTORY),
282                                 MIN_KEEP_COUNT, MIN_KEEP_AGE);
283                     } catch (RuntimeException e) {
284                         Log.e(TAG, "Failed to delete older traces", e);
285                     }
286                     return null;
287                 });
288         ExecutorService executor = Executors.newSingleThreadExecutor();
289         // execute() instead of submit() because we don't need the result.
290         executor.execute(task);
291     }
292 
getRunningAppProcesses(Context context)293     static Set<String> getRunningAppProcesses(Context context) {
294         ActivityManager am = context.getSystemService(ActivityManager.class);
295         List<ActivityManager.RunningAppProcessInfo> processes =
296                 am.getRunningAppProcesses();
297         // AM will return null instead of an empty list if no apps are found.
298         if (processes == null) {
299             return Collections.emptySet();
300         }
301 
302         Set<String> processNames = processes.stream()
303                 .map(process -> process.processName)
304                 .collect(Collectors.toSet());
305 
306         return processNames;
307     }
308 
309     /**
310      * Streams data from an InputStream to an OutputStream
311      */
312     static class Streamer {
313         private boolean mDone;
314 
Streamer(final String tag, final InputStream in, final OutputStream out)315         Streamer(final String tag, final InputStream in, final OutputStream out) {
316             new Thread(tag) {
317                 @Override
318                 public void run() {
319                     int read;
320                     byte[] buf = new byte[2 << 10];
321                     try {
322                         while ((read = in.read(buf)) != -1) {
323                             out.write(buf, 0, read);
324                         }
325                     } catch (IOException e) {
326                         Log.e(TAG, "Error while streaming " + tag);
327                     } finally {
328                         try {
329                             out.close();
330                         } catch (IOException e) {
331                             // Welp.
332                         }
333                         synchronized (Streamer.this) {
334                             mDone = true;
335                             Streamer.this.notify();
336                         }
337                     }
338                 }
339             }.start();
340         }
341 
isDone()342         synchronized boolean isDone() {
343             return mDone;
344         }
345 
waitForDone()346         synchronized void waitForDone() {
347             while (!isDone()) {
348                 try {
349                     wait();
350                 } catch (InterruptedException e) {
351                     Thread.currentThread().interrupt();
352                 }
353             }
354         }
355     }
356 
357     /**
358      * Redirects an InputStream to logcat.
359      */
360     private static class Logger {
361 
Logger(final String tag, final InputStream in)362         Logger(final String tag, final InputStream in) {
363             new Thread(tag) {
364                 @Override
365                 public void run() {
366                     String line;
367                     BufferedReader r = new BufferedReader(new InputStreamReader(in));
368                     try {
369                         while ((line = r.readLine()) != null) {
370                             Log.e(TAG, tag + ": " + line);
371                         }
372                     } catch (IOException e) {
373                         Log.e(TAG, "Error while streaming " + tag);
374                     } finally {
375                         try {
376                             r.close();
377                         } catch (IOException e) {
378                             // Welp.
379                         }
380                     }
381                 }
382             }.start();
383         }
384     }
385 }
386