TracingControllerAndroid.java revision 116680a4aac90f2aa7413d9095a592090648e557
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 5package org.chromium.content.browser; 6 7import android.content.BroadcastReceiver; 8import android.content.Context; 9import android.content.Intent; 10import android.content.IntentFilter; 11import android.os.Environment; 12import android.text.TextUtils; 13import android.util.Log; 14import android.widget.Toast; 15 16import org.chromium.base.CalledByNative; 17import org.chromium.base.JNINamespace; 18import org.chromium.content.R; 19 20import java.io.File; 21import java.text.SimpleDateFormat; 22import java.util.Date; 23import java.util.Locale; 24import 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") 43public 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 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 */ 81 public BroadcastReceiver getBroadcastReceiver() { 82 return mBroadcastReceiver; 83 } 84 85 /** 86 * Get an IntentFilter for profiler intents. 87 */ 88 public IntentFilter getIntentFilter() { 89 return mIntentFilter; 90 } 91 92 /** 93 * Register a BroadcastReceiver in the given context. 94 */ 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 */ 103 public void unregisterReceiver(Context context) { 104 context.unregisterReceiver(getBroadcastReceiver()); 105 } 106 107 /** 108 * Returns true if we're currently profiling. 109 */ 110 public boolean isTracing() { 111 return mIsTracing; 112 } 113 114 /** 115 * Returns the path of the current output file. Null if isTracing() false. 116 */ 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 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 */ 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 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 */ 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 */ 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 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 */ 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 244 protected void finalize() { 245 // Ensure that destroy() was called. 246 assert mNativeTracingControllerAndroid == 0; 247 } 248 249 /** 250 * Clean up the C++ side of this class. 251 * After the call, this class instance shouldn't be used. 252 */ 253 public void destroy() { 254 if (mNativeTracingControllerAndroid != 0) { 255 nativeDestroy(mNativeTracingControllerAndroid); 256 mNativeTracingControllerAndroid = 0; 257 } 258 } 259 260 private void logAndToastError(String str) { 261 Log.e(TAG, str); 262 if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); 263 } 264 265 // The |str| string needs to match the ones that adb_chrome_profiler looks for. 266 private void logForProfiler(String str) { 267 Log.i(TAG, str); 268 } 269 270 private void showToast(String str) { 271 if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); 272 } 273 274 private static class TracingIntentFilter extends IntentFilter { 275 TracingIntentFilter(Context context) { 276 addAction(context.getPackageName() + "." + ACTION_START); 277 addAction(context.getPackageName() + "." + ACTION_STOP); 278 addAction(context.getPackageName() + "." + ACTION_LIST_CATEGORIES); 279 } 280 } 281 282 class TracingBroadcastReceiver extends BroadcastReceiver { 283 @Override 284 public void onReceive(Context context, Intent intent) { 285 if (intent.getAction().endsWith(ACTION_START)) { 286 String categories = intent.getStringExtra(CATEGORIES_EXTRA); 287 if (TextUtils.isEmpty(categories)) { 288 categories = nativeGetDefaultCategories(); 289 } else { 290 categories = categories.replaceFirst( 291 DEFAULT_CHROME_CATEGORIES_PLACE_HOLDER, nativeGetDefaultCategories()); 292 } 293 boolean recordContinuously = 294 intent.getStringExtra(RECORD_CONTINUOUSLY_EXTRA) != null; 295 String filename = intent.getStringExtra(FILE_EXTRA); 296 if (filename != null) { 297 startTracing(filename, true, categories, recordContinuously); 298 } else { 299 startTracing(true, categories, recordContinuously); 300 } 301 } else if (intent.getAction().endsWith(ACTION_STOP)) { 302 stopTracing(); 303 } else if (intent.getAction().endsWith(ACTION_LIST_CATEGORIES)) { 304 getCategoryGroups(); 305 } else { 306 Log.e(TAG, "Unexpected intent: " + intent); 307 } 308 } 309 } 310 311 private long mNativeTracingControllerAndroid; 312 private native long nativeInit(); 313 private native void nativeDestroy(long nativeTracingControllerAndroid); 314 private native boolean nativeStartTracing( 315 long nativeTracingControllerAndroid, String categories, boolean recordContinuously); 316 private native void nativeStopTracing(long nativeTracingControllerAndroid, String filename); 317 private native boolean nativeGetKnownCategoryGroupsAsync(long nativeTracingControllerAndroid); 318 private native String nativeGetDefaultCategories(); 319} 320