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