TracingControllerAndroid.java revision 010d83a9304c5a91596085d917d248abff47903a
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 private final Context mContext; 57 private final TracingBroadcastReceiver mBroadcastReceiver; 58 private final TracingIntentFilter mIntentFilter; 59 private boolean mIsTracing; 60 61 // We might not want to always show toasts when we start the profiler, especially if 62 // showing the toast impacts performance. This gives us the chance to disable them. 63 private boolean mShowToasts = true; 64 65 private String mFilename; 66 67 public TracingControllerAndroid(Context context) { 68 mContext = context; 69 mBroadcastReceiver = new TracingBroadcastReceiver(); 70 mIntentFilter = new TracingIntentFilter(context); 71 } 72 73 /** 74 * Get a BroadcastReceiver that can handle profiler intents. 75 */ 76 public BroadcastReceiver getBroadcastReceiver() { 77 return mBroadcastReceiver; 78 } 79 80 /** 81 * Get an IntentFilter for profiler intents. 82 */ 83 public IntentFilter getIntentFilter() { 84 return mIntentFilter; 85 } 86 87 /** 88 * Register a BroadcastReceiver in the given context. 89 */ 90 public void registerReceiver(Context context) { 91 context.registerReceiver(getBroadcastReceiver(), getIntentFilter()); 92 } 93 94 /** 95 * Unregister the GPU BroadcastReceiver in the given context. 96 * @param context 97 */ 98 public void unregisterReceiver(Context context) { 99 context.unregisterReceiver(getBroadcastReceiver()); 100 } 101 102 /** 103 * Returns true if we're currently profiling. 104 */ 105 public boolean isTracing() { 106 return mIsTracing; 107 } 108 109 /** 110 * Returns the path of the current output file. Null if isTracing() false. 111 */ 112 public String getOutputPath() { 113 return mFilename; 114 } 115 116 /** 117 * Generates a unique filename to be used for tracing in the Downloads directory. 118 */ 119 @CalledByNative 120 private static String generateTracingFilePath() { 121 String state = Environment.getExternalStorageState(); 122 if (!Environment.MEDIA_MOUNTED.equals(state)) { 123 return null; 124 } 125 126 // Generate a hopefully-unique filename using the UTC timestamp. 127 // (Not a huge problem if it isn't unique, we'll just append more data.) 128 SimpleDateFormat formatter = new SimpleDateFormat( 129 "yyyy-MM-dd-HHmmss", Locale.US); 130 formatter.setTimeZone(TimeZone.getTimeZone("UTC")); 131 File dir = Environment.getExternalStoragePublicDirectory( 132 Environment.DIRECTORY_DOWNLOADS); 133 File file = new File( 134 dir, "chrome-profile-results-" + formatter.format(new Date())); 135 return file.getPath(); 136 } 137 138 /** 139 * Start profiling to a new file in the Downloads directory. 140 * 141 * Calls #startTracing(String, boolean, String, boolean) with a new timestamped filename. 142 * @see #startTracing(String, boolean, String, boolean) 143 */ 144 public boolean startTracing(boolean showToasts, String categories, 145 boolean recordContinuously) { 146 mShowToasts = showToasts; 147 148 String filePath = generateTracingFilePath(); 149 if (filePath == null) { 150 logAndToastError( 151 mContext.getString(R.string.profiler_no_storage_toast)); 152 } 153 return startTracing(filePath, showToasts, categories, recordContinuously); 154 } 155 156 private void initializeNativeControllerIfNeeded() { 157 if (mNativeTracingControllerAndroid == 0) { 158 mNativeTracingControllerAndroid = nativeInit(); 159 } 160 } 161 162 /** 163 * Start profiling to the specified file. Returns true on success. 164 * 165 * Only one TracingControllerAndroid can be running at the same time. If another profiler 166 * is running when this method is called, it will be cancelled. If this 167 * profiler is already running, this method does nothing and returns false. 168 * 169 * @param filename The name of the file to output the profile data to. 170 * @param showToasts Whether or not we want to show toasts during this profiling session. 171 * When we are timing the profile run we might not want to incur extra draw overhead of showing 172 * notifications about the profiling system. 173 * @param categories Which categories to trace. See TracingControllerAndroid::BeginTracing() 174 * (in content/public/browser/trace_controller.h) for the format. 175 * @param recordContinuously Record until the user ends the trace. The trace buffer is fixed 176 * size and we use it as a ring buffer during recording. 177 */ 178 public boolean startTracing(String filename, boolean showToasts, String categories, 179 boolean recordContinuously) { 180 mShowToasts = showToasts; 181 if (isTracing()) { 182 // Don't need a toast because this shouldn't happen via the UI. 183 Log.e(TAG, "Received startTracing, but we're already tracing"); 184 return false; 185 } 186 // Lazy initialize the native side, to allow construction before the library is loaded. 187 initializeNativeControllerIfNeeded(); 188 if (!nativeStartTracing(mNativeTracingControllerAndroid, categories, 189 recordContinuously)) { 190 logAndToastError(mContext.getString(R.string.profiler_error_toast)); 191 return false; 192 } 193 194 logAndToastInfo(mContext.getString(R.string.profiler_started_toast) + ": " + categories); 195 mFilename = filename; 196 mIsTracing = true; 197 return true; 198 } 199 200 /** 201 * Stop profiling. This won't take effect until Chrome has flushed its file. 202 */ 203 public void stopTracing() { 204 if (isTracing()) { 205 nativeStopTracing(mNativeTracingControllerAndroid, mFilename); 206 } 207 } 208 209 /** 210 * Called by native code when the profiler's output file is closed. 211 */ 212 @CalledByNative 213 protected void onTracingStopped() { 214 if (!isTracing()) { 215 // Don't need a toast because this shouldn't happen via the UI. 216 Log.e(TAG, "Received onTracingStopped, but we aren't tracing"); 217 return; 218 } 219 220 logAndToastInfo( 221 mContext.getString(R.string.profiler_stopped_toast, mFilename)); 222 mIsTracing = false; 223 mFilename = null; 224 } 225 226 /** 227 * Get known category groups. 228 */ 229 public void getCategoryGroups() { 230 // Lazy initialize the native side, to allow construction before the library is loaded. 231 initializeNativeControllerIfNeeded(); 232 if (!nativeGetKnownCategoryGroupsAsync(mNativeTracingControllerAndroid)) { 233 Log.e(TAG, "Unable to fetch tracing record groups list."); 234 } 235 } 236 237 @Override 238 protected void finalize() { 239 if (mNativeTracingControllerAndroid != 0) { 240 nativeDestroy(mNativeTracingControllerAndroid); 241 mNativeTracingControllerAndroid = 0; 242 } 243 } 244 245 void logAndToastError(String str) { 246 Log.e(TAG, str); 247 if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); 248 } 249 250 void logAndToastInfo(String str) { 251 Log.i(TAG, str); 252 if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); 253 } 254 255 private static class TracingIntentFilter extends IntentFilter { 256 TracingIntentFilter(Context context) { 257 addAction(context.getPackageName() + "." + ACTION_START); 258 addAction(context.getPackageName() + "." + ACTION_STOP); 259 addAction(context.getPackageName() + "." + ACTION_LIST_CATEGORIES); 260 } 261 } 262 263 class TracingBroadcastReceiver extends BroadcastReceiver { 264 @Override 265 public void onReceive(Context context, Intent intent) { 266 if (intent.getAction().endsWith(ACTION_START)) { 267 String categories = intent.getStringExtra(CATEGORIES_EXTRA); 268 if (TextUtils.isEmpty(categories)) { 269 categories = nativeGetDefaultCategories(); 270 } else { 271 categories = categories.replaceFirst( 272 DEFAULT_CHROME_CATEGORIES_PLACE_HOLDER, nativeGetDefaultCategories()); 273 } 274 boolean recordContinuously = 275 intent.getStringExtra(RECORD_CONTINUOUSLY_EXTRA) != null; 276 String filename = intent.getStringExtra(FILE_EXTRA); 277 if (filename != null) { 278 startTracing(filename, true, categories, recordContinuously); 279 } else { 280 startTracing(true, categories, recordContinuously); 281 } 282 } else if (intent.getAction().endsWith(ACTION_STOP)) { 283 stopTracing(); 284 } else if (intent.getAction().endsWith(ACTION_LIST_CATEGORIES)) { 285 getCategoryGroups(); 286 } else { 287 Log.e(TAG, "Unexpected intent: " + intent); 288 } 289 } 290 } 291 292 private long mNativeTracingControllerAndroid; 293 private native long nativeInit(); 294 private native void nativeDestroy(long nativeTracingControllerAndroid); 295 private native boolean nativeStartTracing( 296 long nativeTracingControllerAndroid, String categories, boolean recordContinuously); 297 private native void nativeStopTracing(long nativeTracingControllerAndroid, String filename); 298 private native boolean nativeGetKnownCategoryGroupsAsync(long nativeTracingControllerAndroid); 299 private native String nativeGetDefaultCategories(); 300} 301