TracingControllerAndroid.java revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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, String) with a new timestamped filename. 147 * @see #startTracing(String, boolean, String, String) 148 */ 149 public boolean startTracing(boolean showToasts, String categories, String traceOptions) { 150 mShowToasts = showToasts; 151 152 String filePath = generateTracingFilePath(); 153 if (filePath == null) { 154 logAndToastError( 155 mContext.getString(R.string.profiler_no_storage_toast)); 156 } 157 return startTracing(filePath, showToasts, categories, traceOptions); 158 } 159 160 private void initializeNativeControllerIfNeeded() { 161 if (mNativeTracingControllerAndroid == 0) { 162 mNativeTracingControllerAndroid = nativeInit(); 163 } 164 } 165 166 /** 167 * Start profiling to the specified file. Returns true on success. 168 * 169 * Only one TracingControllerAndroid can be running at the same time. If another profiler 170 * is running when this method is called, it will be cancelled. If this 171 * profiler is already running, this method does nothing and returns false. 172 * 173 * @param filename The name of the file to output the profile data to. 174 * @param showToasts Whether or not we want to show toasts during this profiling session. 175 * When we are timing the profile run we might not want to incur extra draw overhead of showing 176 * notifications about the profiling system. 177 * @param categories Which categories to trace. See TracingControllerAndroid::BeginTracing() 178 * (in content/public/browser/trace_controller.h) for the format. 179 * @param traceOptions Which trace options to use. See 180 * TraceOptions::TraceOptions(const std::string& options_string) 181 * (in base/debug/trace_event_impl.h) for the format. 182 */ 183 public boolean startTracing(String filename, boolean showToasts, String categories, 184 String traceOptions) { 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 traceOptions.toString())) { 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 String traceOptions = 294 intent.getStringExtra(RECORD_CONTINUOUSLY_EXTRA) == null ? 295 "record-until-full" : "record-continuously"; 296 String filename = intent.getStringExtra(FILE_EXTRA); 297 if (filename != null) { 298 startTracing(filename, true, categories, traceOptions); 299 } else { 300 startTracing(true, categories, traceOptions); 301 } 302 } else if (intent.getAction().endsWith(ACTION_STOP)) { 303 stopTracing(); 304 } else if (intent.getAction().endsWith(ACTION_LIST_CATEGORIES)) { 305 getCategoryGroups(); 306 } else { 307 Log.e(TAG, "Unexpected intent: " + intent); 308 } 309 } 310 } 311 312 private long mNativeTracingControllerAndroid; 313 private native long nativeInit(); 314 private native void nativeDestroy(long nativeTracingControllerAndroid); 315 private native boolean nativeStartTracing( 316 long nativeTracingControllerAndroid, String categories, String traceOptions); 317 private native void nativeStopTracing(long nativeTracingControllerAndroid, String filename); 318 private native boolean nativeGetKnownCategoryGroupsAsync(long nativeTracingControllerAndroid); 319 private native String nativeGetDefaultCategories(); 320} 321