1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.content.browser; 6 7 import android.content.BroadcastReceiver; 8 import android.content.Context; 9 import android.content.Intent; 10 import android.content.IntentFilter; 11 import android.os.Environment; 12 import android.text.TextUtils; 13 import android.util.Log; 14 import android.widget.Toast; 15 16 import org.chromium.base.CalledByNative; 17 import org.chromium.base.JNINamespace; 18 import org.chromium.content.R; 19 20 import java.io.File; 21 import java.text.SimpleDateFormat; 22 import java.util.Date; 23 import java.util.Locale; 24 import java.util.TimeZone; 25 26 /** 27 * Controller for Chrome's tracing feature. 28 * 29 * We don't have any UI per se. Just call startTracing() to start and 30 * stopTracing() to stop. We'll report progress to the user with Toasts. 31 * 32 * If the host application registers this class's BroadcastReceiver, you can 33 * also start and stop the tracer with a broadcast intent, as follows: 34 * <ul> 35 * <li>To start tracing: am broadcast -a org.chromium.content_shell_apk.GPU_PROFILER_START 36 * <li>Add "-e file /foo/bar/xyzzy" to log trace data to a specific file. 37 * <li>To stop tracing: am broadcast -a org.chromium.content_shell_apk.GPU_PROFILER_STOP 38 * </ul> 39 * Note that the name of these intents change depending on which application 40 * is being traced, but the general form is [app package name].GPU_PROFILER_{START,STOP}. 41 */ 42 @JNINamespace("content") 43 public class TracingControllerAndroid { 44 45 private static final String TAG = "TracingControllerAndroid"; 46 47 private static final String ACTION_START = "GPU_PROFILER_START"; 48 private static final String ACTION_STOP = "GPU_PROFILER_STOP"; 49 private static final String ACTION_LIST_CATEGORIES = "GPU_PROFILER_LIST_CATEGORIES"; 50 private static final String FILE_EXTRA = "file"; 51 private static final String CATEGORIES_EXTRA = "categories"; 52 private static final String RECORD_CONTINUOUSLY_EXTRA = "continuous"; 53 private static final String DEFAULT_CHROME_CATEGORIES_PLACE_HOLDER = 54 "_DEFAULT_CHROME_CATEGORIES"; 55 56 // These strings must match the ones expected by adb_profile_chrome. 57 private static final String PROFILER_STARTED_FMT = "Profiler started: %s"; 58 private static final String PROFILER_FINISHED_FMT = 59 "Profiler finished. Results are in %s."; 60 61 private final Context mContext; 62 private final TracingBroadcastReceiver mBroadcastReceiver; 63 private final TracingIntentFilter mIntentFilter; 64 private boolean mIsTracing; 65 66 // We might not want to always show toasts when we start the profiler, especially if 67 // showing the toast impacts performance. This gives us the chance to disable them. 68 private boolean mShowToasts = true; 69 70 private String mFilename; 71 TracingControllerAndroid(Context context)72 public TracingControllerAndroid(Context context) { 73 mContext = context; 74 mBroadcastReceiver = new TracingBroadcastReceiver(); 75 mIntentFilter = new TracingIntentFilter(context); 76 } 77 78 /** 79 * Get a BroadcastReceiver that can handle profiler intents. 80 */ getBroadcastReceiver()81 public BroadcastReceiver getBroadcastReceiver() { 82 return mBroadcastReceiver; 83 } 84 85 /** 86 * Get an IntentFilter for profiler intents. 87 */ getIntentFilter()88 public IntentFilter getIntentFilter() { 89 return mIntentFilter; 90 } 91 92 /** 93 * Register a BroadcastReceiver in the given context. 94 */ registerReceiver(Context context)95 public void registerReceiver(Context context) { 96 context.registerReceiver(getBroadcastReceiver(), getIntentFilter()); 97 } 98 99 /** 100 * Unregister the GPU BroadcastReceiver in the given context. 101 * @param context 102 */ unregisterReceiver(Context context)103 public void unregisterReceiver(Context context) { 104 context.unregisterReceiver(getBroadcastReceiver()); 105 } 106 107 /** 108 * Returns true if we're currently profiling. 109 */ isTracing()110 public boolean isTracing() { 111 return mIsTracing; 112 } 113 114 /** 115 * Returns the path of the current output file. Null if isTracing() false. 116 */ getOutputPath()117 public String getOutputPath() { 118 return mFilename; 119 } 120 121 /** 122 * Generates a unique filename to be used for tracing in the Downloads directory. 123 */ 124 @CalledByNative generateTracingFilePath()125 private static String generateTracingFilePath() { 126 String state = Environment.getExternalStorageState(); 127 if (!Environment.MEDIA_MOUNTED.equals(state)) { 128 return null; 129 } 130 131 // Generate a hopefully-unique filename using the UTC timestamp. 132 // (Not a huge problem if it isn't unique, we'll just append more data.) 133 SimpleDateFormat formatter = new SimpleDateFormat( 134 "yyyy-MM-dd-HHmmss", Locale.US); 135 formatter.setTimeZone(TimeZone.getTimeZone("UTC")); 136 File dir = Environment.getExternalStoragePublicDirectory( 137 Environment.DIRECTORY_DOWNLOADS); 138 File file = new File( 139 dir, "chrome-profile-results-" + formatter.format(new Date())); 140 return file.getPath(); 141 } 142 143 /** 144 * Start profiling to a new file in the Downloads directory. 145 * 146 * Calls #startTracing(String, boolean, String, boolean) with a new timestamped filename. 147 * @see #startTracing(String, boolean, String, boolean) 148 */ startTracing(boolean showToasts, String categories, boolean recordContinuously)149 public boolean startTracing(boolean showToasts, String categories, 150 boolean recordContinuously) { 151 mShowToasts = showToasts; 152 153 String filePath = generateTracingFilePath(); 154 if (filePath == null) { 155 logAndToastError( 156 mContext.getString(R.string.profiler_no_storage_toast)); 157 } 158 return startTracing(filePath, showToasts, categories, recordContinuously); 159 } 160 initializeNativeControllerIfNeeded()161 private void initializeNativeControllerIfNeeded() { 162 if (mNativeTracingControllerAndroid == 0) { 163 mNativeTracingControllerAndroid = nativeInit(); 164 } 165 } 166 167 /** 168 * Start profiling to the specified file. Returns true on success. 169 * 170 * Only one TracingControllerAndroid can be running at the same time. If another profiler 171 * is running when this method is called, it will be cancelled. If this 172 * profiler is already running, this method does nothing and returns false. 173 * 174 * @param filename The name of the file to output the profile data to. 175 * @param showToasts Whether or not we want to show toasts during this profiling session. 176 * When we are timing the profile run we might not want to incur extra draw overhead of showing 177 * notifications about the profiling system. 178 * @param categories Which categories to trace. See TracingControllerAndroid::BeginTracing() 179 * (in content/public/browser/trace_controller.h) for the format. 180 * @param recordContinuously Record until the user ends the trace. The trace buffer is fixed 181 * size and we use it as a ring buffer during recording. 182 */ startTracing(String filename, boolean showToasts, String categories, boolean recordContinuously)183 public boolean startTracing(String filename, boolean showToasts, String categories, 184 boolean recordContinuously) { 185 mShowToasts = showToasts; 186 if (isTracing()) { 187 // Don't need a toast because this shouldn't happen via the UI. 188 Log.e(TAG, "Received startTracing, but we're already tracing"); 189 return false; 190 } 191 // Lazy initialize the native side, to allow construction before the library is loaded. 192 initializeNativeControllerIfNeeded(); 193 if (!nativeStartTracing(mNativeTracingControllerAndroid, categories, 194 recordContinuously)) { 195 logAndToastError(mContext.getString(R.string.profiler_error_toast)); 196 return false; 197 } 198 199 logForProfiler(String.format(PROFILER_STARTED_FMT, categories)); 200 showToast(mContext.getString(R.string.profiler_started_toast) + ": " + categories); 201 mFilename = filename; 202 mIsTracing = true; 203 return true; 204 } 205 206 /** 207 * Stop profiling. This won't take effect until Chrome has flushed its file. 208 */ stopTracing()209 public void stopTracing() { 210 if (isTracing()) { 211 nativeStopTracing(mNativeTracingControllerAndroid, mFilename); 212 } 213 } 214 215 /** 216 * Called by native code when the profiler's output file is closed. 217 */ 218 @CalledByNative onTracingStopped()219 protected void onTracingStopped() { 220 if (!isTracing()) { 221 // Don't need a toast because this shouldn't happen via the UI. 222 Log.e(TAG, "Received onTracingStopped, but we aren't tracing"); 223 return; 224 } 225 226 logForProfiler(String.format(PROFILER_FINISHED_FMT, mFilename)); 227 showToast(mContext.getString(R.string.profiler_stopped_toast, mFilename)); 228 mIsTracing = false; 229 mFilename = null; 230 } 231 232 /** 233 * Get known category groups. 234 */ getCategoryGroups()235 public void getCategoryGroups() { 236 // Lazy initialize the native side, to allow construction before the library is loaded. 237 initializeNativeControllerIfNeeded(); 238 if (!nativeGetKnownCategoryGroupsAsync(mNativeTracingControllerAndroid)) { 239 Log.e(TAG, "Unable to fetch tracing record groups list."); 240 } 241 } 242 243 @Override finalize()244 protected void finalize() { 245 if (mNativeTracingControllerAndroid != 0) { 246 nativeDestroy(mNativeTracingControllerAndroid); 247 mNativeTracingControllerAndroid = 0; 248 } 249 } 250 logAndToastError(String str)251 private void logAndToastError(String str) { 252 Log.e(TAG, str); 253 if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); 254 } 255 256 // The |str| string needs to match the ones that adb_chrome_profiler looks for. logForProfiler(String str)257 private void logForProfiler(String str) { 258 Log.i(TAG, str); 259 } 260 showToast(String str)261 private void showToast(String str) { 262 if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); 263 } 264 265 private static class TracingIntentFilter extends IntentFilter { TracingIntentFilter(Context context)266 TracingIntentFilter(Context context) { 267 addAction(context.getPackageName() + "." + ACTION_START); 268 addAction(context.getPackageName() + "." + ACTION_STOP); 269 addAction(context.getPackageName() + "." + ACTION_LIST_CATEGORIES); 270 } 271 } 272 273 class TracingBroadcastReceiver extends BroadcastReceiver { 274 @Override onReceive(Context context, Intent intent)275 public void onReceive(Context context, Intent intent) { 276 if (intent.getAction().endsWith(ACTION_START)) { 277 String categories = intent.getStringExtra(CATEGORIES_EXTRA); 278 if (TextUtils.isEmpty(categories)) { 279 categories = nativeGetDefaultCategories(); 280 } else { 281 categories = categories.replaceFirst( 282 DEFAULT_CHROME_CATEGORIES_PLACE_HOLDER, nativeGetDefaultCategories()); 283 } 284 boolean recordContinuously = 285 intent.getStringExtra(RECORD_CONTINUOUSLY_EXTRA) != null; 286 String filename = intent.getStringExtra(FILE_EXTRA); 287 if (filename != null) { 288 startTracing(filename, true, categories, recordContinuously); 289 } else { 290 startTracing(true, categories, recordContinuously); 291 } 292 } else if (intent.getAction().endsWith(ACTION_STOP)) { 293 stopTracing(); 294 } else if (intent.getAction().endsWith(ACTION_LIST_CATEGORIES)) { 295 getCategoryGroups(); 296 } else { 297 Log.e(TAG, "Unexpected intent: " + intent); 298 } 299 } 300 } 301 302 private long mNativeTracingControllerAndroid; nativeInit()303 private native long nativeInit(); nativeDestroy(long nativeTracingControllerAndroid)304 private native void nativeDestroy(long nativeTracingControllerAndroid); nativeStartTracing( long nativeTracingControllerAndroid, String categories, boolean recordContinuously)305 private native boolean nativeStartTracing( 306 long nativeTracingControllerAndroid, String categories, boolean recordContinuously); nativeStopTracing(long nativeTracingControllerAndroid, String filename)307 private native void nativeStopTracing(long nativeTracingControllerAndroid, String filename); nativeGetKnownCategoryGroupsAsync(long nativeTracingControllerAndroid)308 private native boolean nativeGetKnownCategoryGroupsAsync(long nativeTracingControllerAndroid); nativeGetDefaultCategories()309 private native String nativeGetDefaultCategories(); 310 } 311