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 import org.chromium.content.common.TraceEvent; 20 21 import java.io.File; 22 import java.text.SimpleDateFormat; 23 import java.util.Date; 24 import java.util.Locale; 25 import java.util.TimeZone; 26 27 /** 28 * Controller for Chrome's tracing feature. 29 * 30 * We don't have any UI per se. Just call startTracing() to start and 31 * stopTracing() to stop. We'll report progress to the user with Toasts. 32 * 33 * If the host application registers this class's BroadcastReceiver, you can 34 * also start and stop the tracer with a broadcast intent, as follows: 35 * <ul> 36 * <li>To start tracing: am broadcast -a org.chromium.content_shell_apk.GPU_PROFILER_START 37 * <li>Add "-e file /foo/bar/xyzzy" to log trace data to a specific file. 38 * <li>To stop tracing: am broadcast -a org.chromium.content_shell_apk.GPU_PROFILER_STOP 39 * </ul> 40 * Note that the name of these intents change depending on which application 41 * is being traced, but the general form is [app package name].GPU_PROFILER_{START,STOP}. 42 */ 43 @JNINamespace("content") 44 public class TracingControllerAndroid { 45 46 private static final String TAG = "TracingControllerAndroid"; 47 48 private static final String ACTION_START = "GPU_PROFILER_START"; 49 private static final String ACTION_STOP = "GPU_PROFILER_STOP"; 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 TracingControllerAndroid(Context context)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 */ getBroadcastReceiver()76 public BroadcastReceiver getBroadcastReceiver() { 77 return mBroadcastReceiver; 78 } 79 80 /** 81 * Get an IntentFilter for profiler intents. 82 */ getIntentFilter()83 public IntentFilter getIntentFilter() { 84 return mIntentFilter; 85 } 86 87 /** 88 * Register a BroadcastReceiver in the given context. 89 */ registerReceiver(Context context)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 */ unregisterReceiver(Context context)98 public void unregisterReceiver(Context context) { 99 context.unregisterReceiver(getBroadcastReceiver()); 100 } 101 102 /** 103 * Returns true if we're currently profiling. 104 */ isTracing()105 public boolean isTracing() { 106 return mIsTracing; 107 } 108 109 /** 110 * Returns the path of the current output file. Null if isTracing() false. 111 */ getOutputPath()112 public String getOutputPath() { 113 return mFilename; 114 } 115 116 /** 117 * Start profiling to a new file in the Downloads directory. 118 * 119 * Calls #startTracing(String, boolean, String, boolean) with a new timestamped filename. 120 * @see #startTracing(String, boolean, String, boolean) 121 */ startTracing(boolean showToasts, String categories, boolean recordContinuously)122 public boolean startTracing(boolean showToasts, String categories, 123 boolean recordContinuously) { 124 mShowToasts = showToasts; 125 String state = Environment.getExternalStorageState(); 126 if (!Environment.MEDIA_MOUNTED.equals(state)) { 127 logAndToastError( 128 mContext.getString(R.string.profiler_no_storage_toast)); 129 return false; 130 } 131 132 // Generate a hopefully-unique filename using the UTC timestamp. 133 // (Not a huge problem if it isn't unique, we'll just append more data.) 134 SimpleDateFormat formatter = new SimpleDateFormat( 135 "yyyy-MM-dd-HHmmss", Locale.US); 136 formatter.setTimeZone(TimeZone.getTimeZone("UTC")); 137 File dir = Environment.getExternalStoragePublicDirectory( 138 Environment.DIRECTORY_DOWNLOADS); 139 File file = new File( 140 dir, "chrome-profile-results-" + formatter.format(new Date())); 141 142 return startTracing(file.getPath(), showToasts, categories, recordContinuously); 143 } 144 145 /** 146 * Start profiling to the specified file. Returns true on success. 147 * 148 * Only one TracingControllerAndroid can be running at the same time. If another profiler 149 * is running when this method is called, it will be cancelled. If this 150 * profiler is already running, this method does nothing and returns false. 151 * 152 * @param filename The name of the file to output the profile data to. 153 * @param showToasts Whether or not we want to show toasts during this profiling session. 154 * When we are timing the profile run we might not want to incur extra draw overhead of showing 155 * notifications about the profiling system. 156 * @param categories Which categories to trace. See TracingControllerAndroid::BeginTracing() 157 * (in content/public/browser/trace_controller.h) for the format. 158 * @param recordContinuously Record until the user ends the trace. The trace buffer is fixed 159 * size and we use it as a ring buffer during recording. 160 */ startTracing(String filename, boolean showToasts, String categories, boolean recordContinuously)161 public boolean startTracing(String filename, boolean showToasts, String categories, 162 boolean recordContinuously) { 163 mShowToasts = showToasts; 164 if (isTracing()) { 165 // Don't need a toast because this shouldn't happen via the UI. 166 Log.e(TAG, "Received startTracing, but we're already tracing"); 167 return false; 168 } 169 // Lazy initialize the native side, to allow construction before the library is loaded. 170 if (mNativeTracingControllerAndroid == 0) { 171 mNativeTracingControllerAndroid = nativeInit(); 172 } 173 if (!nativeStartTracing(mNativeTracingControllerAndroid, filename, categories, 174 recordContinuously)) { 175 logAndToastError(mContext.getString(R.string.profiler_error_toast)); 176 return false; 177 } 178 179 logAndToastInfo(mContext.getString(R.string.profiler_started_toast) + ": " + categories); 180 TraceEvent.setEnabledToMatchNative(); 181 mFilename = filename; 182 mIsTracing = true; 183 return true; 184 } 185 186 /** 187 * Stop profiling. This won't take effect until Chrome has flushed its file. 188 */ stopTracing()189 public void stopTracing() { 190 if (isTracing()) { 191 nativeStopTracing(mNativeTracingControllerAndroid); 192 } 193 } 194 195 /** 196 * Called by native code when the profiler's output file is closed. 197 */ 198 @CalledByNative onTracingStopped()199 protected void onTracingStopped() { 200 if (!isTracing()) { 201 // Don't need a toast because this shouldn't happen via the UI. 202 Log.e(TAG, "Received onTracingStopped, but we aren't tracing"); 203 return; 204 } 205 206 logAndToastInfo( 207 mContext.getString(R.string.profiler_stopped_toast, mFilename)); 208 TraceEvent.setEnabledToMatchNative(); 209 mIsTracing = false; 210 mFilename = null; 211 } 212 213 @Override finalize()214 protected void finalize() { 215 if (mNativeTracingControllerAndroid != 0) { 216 nativeDestroy(mNativeTracingControllerAndroid); 217 mNativeTracingControllerAndroid = 0; 218 } 219 } 220 logAndToastError(String str)221 void logAndToastError(String str) { 222 Log.e(TAG, str); 223 if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); 224 } 225 logAndToastInfo(String str)226 void logAndToastInfo(String str) { 227 Log.i(TAG, str); 228 if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); 229 } 230 231 private static class TracingIntentFilter extends IntentFilter { TracingIntentFilter(Context context)232 TracingIntentFilter(Context context) { 233 addAction(context.getPackageName() + "." + ACTION_START); 234 addAction(context.getPackageName() + "." + ACTION_STOP); 235 } 236 } 237 238 class TracingBroadcastReceiver extends BroadcastReceiver { 239 @Override onReceive(Context context, Intent intent)240 public void onReceive(Context context, Intent intent) { 241 if (intent.getAction().endsWith(ACTION_START)) { 242 String categories = intent.getStringExtra(CATEGORIES_EXTRA); 243 if (TextUtils.isEmpty(categories)) { 244 categories = nativeGetDefaultCategories(); 245 } else { 246 categories = categories.replaceFirst( 247 DEFAULT_CHROME_CATEGORIES_PLACE_HOLDER, nativeGetDefaultCategories()); 248 } 249 boolean recordContinuously = 250 intent.getStringExtra(RECORD_CONTINUOUSLY_EXTRA) != null; 251 String filename = intent.getStringExtra(FILE_EXTRA); 252 if (filename != null) { 253 startTracing(filename, true, categories, recordContinuously); 254 } else { 255 startTracing(true, categories, recordContinuously); 256 } 257 } else if (intent.getAction().endsWith(ACTION_STOP)) { 258 stopTracing(); 259 } else { 260 Log.e(TAG, "Unexpected intent: " + intent); 261 } 262 } 263 } 264 265 private long mNativeTracingControllerAndroid; nativeInit()266 private native long nativeInit(); nativeDestroy(long nativeTracingControllerAndroid)267 private native void nativeDestroy(long nativeTracingControllerAndroid); nativeStartTracing(long nativeTracingControllerAndroid, String filename, String categories, boolean recordContinuously)268 private native boolean nativeStartTracing(long nativeTracingControllerAndroid, String filename, 269 String categories, boolean recordContinuously); nativeStopTracing(long nativeTracingControllerAndroid)270 private native void nativeStopTracing(long nativeTracingControllerAndroid); nativeGetDefaultCategories()271 private native String nativeGetDefaultCategories(); 272 } 273