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