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.os.Build; 20 import android.os.FileUtils; 21 import android.util.Log; 22 23 import java.io.BufferedReader; 24 import java.io.File; 25 import java.io.IOException; 26 import java.io.InputStream; 27 import java.io.InputStreamReader; 28 import java.io.OutputStream; 29 import java.text.SimpleDateFormat; 30 import java.util.Arrays; 31 import java.util.Collection; 32 import java.util.Date; 33 import java.util.Locale; 34 import java.util.TreeMap; 35 import java.util.concurrent.ExecutorService; 36 import java.util.concurrent.Executors; 37 import java.util.concurrent.FutureTask; 38 import java.util.concurrent.TimeUnit; 39 40 /** 41 * Utility functions for tracing. 42 */ 43 public class TraceUtils { 44 45 static final String TAG = "Traceur"; 46 47 public static final String TRACE_DIRECTORY = "/data/local/traces/"; 48 49 private static TraceEngine mTraceEngine = new PerfettoUtils(); 50 51 private static final Runtime RUNTIME = Runtime.getRuntime(); 52 private static final int PROCESS_TIMEOUT_MS = 30000; // 30 seconds 53 54 enum RecordingType { 55 UNKNOWN, TRACE, STACK_SAMPLES 56 } 57 public interface TraceEngine { getName()58 public String getName(); getOutputExtension()59 public String getOutputExtension(); traceStart(Collection<String> tags, int bufferSizeKb, boolean apps, boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)60 public boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps, 61 boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb, 62 int maxLongTraceDurationMinutes); stackSampleStart(boolean attachToBugreport)63 public boolean stackSampleStart(boolean attachToBugreport); traceStop()64 public void traceStop(); traceDump(File outFile)65 public boolean traceDump(File outFile); isTracingOn()66 public boolean isTracingOn(); 67 } 68 currentTraceEngine()69 public static String currentTraceEngine() { 70 return mTraceEngine.getName(); 71 } 72 traceStart(Collection<String> tags, int bufferSizeKb, boolean apps, boolean longTrace, boolean attachToBugreport, int maxLongTraceSizeMb, int maxLongTraceDurationMinutes)73 public static boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps, 74 boolean longTrace, boolean attachToBugreport, int maxLongTraceSizeMb, 75 int maxLongTraceDurationMinutes) { 76 return mTraceEngine.traceStart(tags, bufferSizeKb, apps, 77 attachToBugreport, longTrace, maxLongTraceSizeMb, maxLongTraceDurationMinutes); 78 } 79 stackSampleStart(boolean attachToBugreport)80 public static boolean stackSampleStart(boolean attachToBugreport) { 81 return mTraceEngine.stackSampleStart(attachToBugreport); 82 } 83 traceStop()84 public static void traceStop() { 85 mTraceEngine.traceStop(); 86 } 87 traceDump(File outFile)88 public static boolean traceDump(File outFile) { 89 return mTraceEngine.traceDump(outFile); 90 } 91 isTracingOn()92 public static boolean isTracingOn() { 93 return mTraceEngine.isTracingOn(); 94 } 95 listCategories()96 public static TreeMap<String, String> listCategories() { 97 TreeMap<String, String> categories = PerfettoUtils.perfettoListCategories(); 98 categories.put("sys_stats", "meminfo and vmstats"); 99 categories.put("logs", "android logcat"); 100 categories.put("cpu", "callstack samples"); 101 return categories; 102 } 103 clearSavedTraces()104 public static void clearSavedTraces() { 105 String cmd = "rm -f " + TRACE_DIRECTORY + "trace-*.*trace " + 106 TRACE_DIRECTORY + "recovered-trace*.*trace " + 107 TRACE_DIRECTORY + "stack-samples*.*trace"; 108 109 Log.v(TAG, "Clearing trace directory: " + cmd); 110 try { 111 Process rm = exec(cmd); 112 113 if (rm.waitFor() != 0) { 114 Log.e(TAG, "clearSavedTraces failed with: " + rm.exitValue()); 115 } 116 } catch (Exception e) { 117 throw new RuntimeException(e); 118 } 119 } 120 exec(String cmd)121 public static Process exec(String cmd) throws IOException { 122 return exec(cmd, null); 123 } 124 exec(String cmd, String tmpdir)125 public static Process exec(String cmd, String tmpdir) throws IOException { 126 return exec(cmd, tmpdir, true); 127 } 128 exec(String cmd, String tmpdir, boolean logOutput)129 public static Process exec(String cmd, String tmpdir, boolean logOutput) throws IOException { 130 String[] cmdarray = {"sh", "-c", cmd}; 131 String[] envp = {"TMPDIR=" + tmpdir}; 132 envp = tmpdir == null ? null : envp; 133 134 Log.v(TAG, "exec: " + Arrays.toString(envp) + " " + Arrays.toString(cmdarray)); 135 136 Process process = RUNTIME.exec(cmdarray, envp); 137 new Logger("traceService:stderr", process.getErrorStream()); 138 if (logOutput) { 139 new Logger("traceService:stdout", process.getInputStream()); 140 } 141 142 return process; 143 } 144 145 // Returns the Process if the command terminated on time and null if not. execWithTimeout(String cmd, String tmpdir, long timeout)146 public static Process execWithTimeout(String cmd, String tmpdir, long timeout) 147 throws IOException { 148 Process process = exec(cmd, tmpdir, true); 149 try { 150 if (!process.waitFor(timeout, TimeUnit.MILLISECONDS)) { 151 Log.e(TAG, "Command '" + cmd + "' has timed out after " + timeout + " ms."); 152 process.destroyForcibly(); 153 // Return null to signal a timeout and that the Process was destroyed. 154 return null; 155 } 156 } catch (Exception e) { 157 throw new RuntimeException(e); 158 } 159 return process; 160 } 161 getOutputFilename(RecordingType type)162 public static String getOutputFilename(RecordingType type) { 163 String prefix; 164 switch (type) { 165 case TRACE: 166 prefix = "trace"; 167 break; 168 case STACK_SAMPLES: 169 prefix = "stack-samples"; 170 break; 171 case UNKNOWN: 172 default: 173 prefix = "recording"; 174 break; 175 } 176 String format = "yyyy-MM-dd-HH-mm-ss"; 177 String now = new SimpleDateFormat(format, Locale.US).format(new Date()); 178 return String.format("%s-%s-%s-%s.%s", prefix, Build.BOARD, Build.ID, now, 179 mTraceEngine.getOutputExtension()); 180 } 181 getRecoveredFilename()182 public static String getRecoveredFilename() { 183 // Knowing what the previous Traceur session was recording would require adding a 184 // recordingWasTrace parameter to TraceUtils.traceStart(). 185 return "recovered-" + getOutputFilename(RecordingType.UNKNOWN); 186 } 187 getOutputFile(String filename)188 public static File getOutputFile(String filename) { 189 return new File(TraceUtils.TRACE_DIRECTORY, filename); 190 } 191 cleanupOlderFiles(final int minCount, final long minAge)192 protected static void cleanupOlderFiles(final int minCount, final long minAge) { 193 FutureTask<Void> task = new FutureTask<Void>( 194 () -> { 195 try { 196 FileUtils.deleteOlderFiles(new File(TRACE_DIRECTORY), minCount, minAge); 197 } catch (RuntimeException e) { 198 Log.e(TAG, "Failed to delete older traces", e); 199 } 200 return null; 201 }); 202 ExecutorService executor = Executors.newSingleThreadExecutor(); 203 // execute() instead of submit() because we don't need the result. 204 executor.execute(task); 205 } 206 207 /** 208 * Streams data from an InputStream to an OutputStream 209 */ 210 static class Streamer { 211 private boolean mDone; 212 Streamer(final String tag, final InputStream in, final OutputStream out)213 Streamer(final String tag, final InputStream in, final OutputStream out) { 214 new Thread(tag) { 215 @Override 216 public void run() { 217 int read; 218 byte[] buf = new byte[2 << 10]; 219 try { 220 while ((read = in.read(buf)) != -1) { 221 out.write(buf, 0, read); 222 } 223 } catch (IOException e) { 224 Log.e(TAG, "Error while streaming " + tag); 225 } finally { 226 try { 227 out.close(); 228 } catch (IOException e) { 229 // Welp. 230 } 231 synchronized (Streamer.this) { 232 mDone = true; 233 Streamer.this.notify(); 234 } 235 } 236 } 237 }.start(); 238 } 239 isDone()240 synchronized boolean isDone() { 241 return mDone; 242 } 243 waitForDone()244 synchronized void waitForDone() { 245 while (!isDone()) { 246 try { 247 wait(); 248 } catch (InterruptedException e) { 249 Thread.currentThread().interrupt(); 250 } 251 } 252 } 253 } 254 255 /** 256 * Redirects an InputStream to logcat. 257 */ 258 private static class Logger { 259 Logger(final String tag, final InputStream in)260 Logger(final String tag, final InputStream in) { 261 new Thread(tag) { 262 @Override 263 public void run() { 264 String line; 265 BufferedReader r = new BufferedReader(new InputStreamReader(in)); 266 try { 267 while ((line = r.readLine()) != null) { 268 Log.e(TAG, tag + ": " + line); 269 } 270 } catch (IOException e) { 271 Log.e(TAG, "Error while streaming " + tag); 272 } finally { 273 try { 274 r.close(); 275 } catch (IOException e) { 276 // Welp. 277 } 278 } 279 } 280 }.start(); 281 } 282 } 283 } 284