| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.content.browser; |
| |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.os.Environment; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.widget.Toast; |
| |
| import org.chromium.base.CalledByNative; |
| import org.chromium.base.JNINamespace; |
| import org.chromium.content.app.LibraryLoader; |
| import org.chromium.content.common.TraceEvent; |
| import org.chromium.content.R; |
| |
| import java.io.File; |
| import java.util.Date; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| import java.text.SimpleDateFormat; |
| |
| /** |
| * Controller for Chrome's tracing feature. |
| * |
| * We don't have any UI per se. Just call startTracing() to start and |
| * stopTracing() to stop. We'll report progress to the user with Toasts. |
| * |
| * If the host application registers this class's BroadcastReceiver, you can |
| * also start and stop the tracer with a broadcast intent, as follows: |
| * <ul> |
| * <li>To start tracing: am broadcast -a org.chromium.content_shell_apk.GPU_PROFILER_START |
| * <li>Add "-e file /foo/bar/xyzzy" to log trace data to a specific file. |
| * <li>To stop tracing: am broadcast -a org.chromium.content_shell_apk.GPU_PROFILER_STOP |
| * </ul> |
| * Note that the name of these intents change depending on which application |
| * is being traced, but the general form is [app package name].GPU_PROFILER_{START,STOP}. |
| */ |
| @JNINamespace("content") |
| public class TracingControllerAndroid { |
| |
| private static final String TAG = "TracingControllerAndroid"; |
| |
| private static final String ACTION_START = "GPU_PROFILER_START"; |
| private static final String ACTION_STOP = "GPU_PROFILER_STOP"; |
| private static final String FILE_EXTRA = "file"; |
| private static final String CATEGORIES_EXTRA = "categories"; |
| private static final String RECORD_CONTINUOUSLY_EXTRA = "continuous"; |
| private static final String DEFAULT_CHROME_CATEGORIES_PLACE_HOLDER = |
| "_DEFAULT_CHROME_CATEGORIES"; |
| |
| private final Context mContext; |
| private final TracingBroadcastReceiver mBroadcastReceiver; |
| private final TracingIntentFilter mIntentFilter; |
| private boolean mIsTracing; |
| |
| // We might not want to always show toasts when we start the profiler, especially if |
| // showing the toast impacts performance. This gives us the chance to disable them. |
| private boolean mShowToasts = true; |
| |
| private String mFilename; |
| |
| public TracingControllerAndroid(Context context) { |
| mContext = context; |
| mBroadcastReceiver = new TracingBroadcastReceiver(); |
| mIntentFilter = new TracingIntentFilter(context); |
| } |
| |
| /** |
| * Get a BroadcastReceiver that can handle profiler intents. |
| */ |
| public BroadcastReceiver getBroadcastReceiver() { |
| return mBroadcastReceiver; |
| } |
| |
| /** |
| * Get an IntentFilter for profiler intents. |
| */ |
| public IntentFilter getIntentFilter() { |
| return mIntentFilter; |
| } |
| |
| /** |
| * Register a BroadcastReceiver in the given context. |
| */ |
| public void registerReceiver(Context context) { |
| context.registerReceiver(getBroadcastReceiver(), getIntentFilter()); |
| } |
| |
| /** |
| * Unregister the GPU BroadcastReceiver in the given context. |
| * @param context |
| */ |
| public void unregisterReceiver(Context context) { |
| context.unregisterReceiver(getBroadcastReceiver()); |
| } |
| |
| /** |
| * Returns true if we're currently profiling. |
| */ |
| public boolean isTracing() { |
| return mIsTracing; |
| } |
| |
| /** |
| * Returns the path of the current output file. Null if isTracing() false. |
| */ |
| public String getOutputPath() { |
| return mFilename; |
| } |
| |
| /** |
| * Start profiling to a new file in the Downloads directory. |
| * |
| * Calls #startTracing(String, boolean, String, boolean) with a new timestamped filename. |
| * @see #startTracing(String, boolean, String, boolean) |
| */ |
| public boolean startTracing(boolean showToasts, String categories, |
| boolean recordContinuously) { |
| mShowToasts = showToasts; |
| String state = Environment.getExternalStorageState(); |
| if (!Environment.MEDIA_MOUNTED.equals(state)) { |
| logAndToastError( |
| mContext.getString(R.string.profiler_no_storage_toast)); |
| return false; |
| } |
| |
| // Generate a hopefully-unique filename using the UTC timestamp. |
| // (Not a huge problem if it isn't unique, we'll just append more data.) |
| SimpleDateFormat formatter = new SimpleDateFormat( |
| "yyyy-MM-dd-HHmmss", Locale.US); |
| formatter.setTimeZone(TimeZone.getTimeZone("UTC")); |
| File dir = Environment.getExternalStoragePublicDirectory( |
| Environment.DIRECTORY_DOWNLOADS); |
| File file = new File( |
| dir, "chrome-profile-results-" + formatter.format(new Date())); |
| |
| return startTracing(file.getPath(), showToasts, categories, recordContinuously); |
| } |
| |
| /** |
| * Start profiling to the specified file. Returns true on success. |
| * |
| * Only one TracingControllerAndroid can be running at the same time. If another profiler |
| * is running when this method is called, it will be cancelled. If this |
| * profiler is already running, this method does nothing and returns false. |
| * |
| * @param filename The name of the file to output the profile data to. |
| * @param showToasts Whether or not we want to show toasts during this profiling session. |
| * When we are timing the profile run we might not want to incur extra draw overhead of showing |
| * notifications about the profiling system. |
| * @param categories Which categories to trace. See TracingControllerAndroid::BeginTracing() |
| * (in content/public/browser/trace_controller.h) for the format. |
| * @param recordContinuously Record until the user ends the trace. The trace buffer is fixed |
| * size and we use it as a ring buffer during recording. |
| */ |
| public boolean startTracing(String filename, boolean showToasts, String categories, |
| boolean recordContinuously) { |
| mShowToasts = showToasts; |
| if (isTracing()) { |
| // Don't need a toast because this shouldn't happen via the UI. |
| Log.e(TAG, "Received startTracing, but we're already tracing"); |
| return false; |
| } |
| // Lazy initialize the native side, to allow construction before the library is loaded. |
| if (mNativeTracingControllerAndroid == 0) { |
| mNativeTracingControllerAndroid = nativeInit(); |
| } |
| if (!nativeStartTracing(mNativeTracingControllerAndroid, filename, categories, |
| recordContinuously)) { |
| logAndToastError(mContext.getString(R.string.profiler_error_toast)); |
| return false; |
| } |
| |
| logAndToastInfo(mContext.getString(R.string.profiler_started_toast) + ": " + categories); |
| TraceEvent.setEnabledToMatchNative(); |
| mFilename = filename; |
| mIsTracing = true; |
| return true; |
| } |
| |
| /** |
| * Stop profiling. This won't take effect until Chrome has flushed its file. |
| */ |
| public void stopTracing() { |
| if (isTracing()) { |
| nativeStopTracing(mNativeTracingControllerAndroid); |
| } |
| } |
| |
| /** |
| * Called by native code when the profiler's output file is closed. |
| */ |
| @CalledByNative |
| protected void onTracingStopped() { |
| if (!isTracing()) { |
| // Don't need a toast because this shouldn't happen via the UI. |
| Log.e(TAG, "Received onTracingStopped, but we aren't tracing"); |
| return; |
| } |
| |
| logAndToastInfo( |
| mContext.getString(R.string.profiler_stopped_toast, mFilename)); |
| TraceEvent.setEnabledToMatchNative(); |
| mIsTracing = false; |
| mFilename = null; |
| } |
| |
| @Override |
| protected void finalize() { |
| if (mNativeTracingControllerAndroid != 0) { |
| nativeDestroy(mNativeTracingControllerAndroid); |
| mNativeTracingControllerAndroid = 0; |
| } |
| } |
| |
| void logAndToastError(String str) { |
| Log.e(TAG, str); |
| if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); |
| } |
| |
| void logAndToastInfo(String str) { |
| Log.i(TAG, str); |
| if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show(); |
| } |
| |
| private static class TracingIntentFilter extends IntentFilter { |
| TracingIntentFilter(Context context) { |
| addAction(context.getPackageName() + "." + ACTION_START); |
| addAction(context.getPackageName() + "." + ACTION_STOP); |
| } |
| } |
| |
| class TracingBroadcastReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (intent.getAction().endsWith(ACTION_START)) { |
| String categories = intent.getStringExtra(CATEGORIES_EXTRA); |
| if (TextUtils.isEmpty(categories)) { |
| categories = nativeGetDefaultCategories(); |
| } else { |
| categories = categories.replaceFirst( |
| DEFAULT_CHROME_CATEGORIES_PLACE_HOLDER, nativeGetDefaultCategories()); |
| } |
| boolean recordContinuously = |
| intent.getStringExtra(RECORD_CONTINUOUSLY_EXTRA) != null; |
| String filename = intent.getStringExtra(FILE_EXTRA); |
| if (filename != null) { |
| startTracing(filename, true, categories, recordContinuously); |
| } else { |
| startTracing(true, categories, recordContinuously); |
| } |
| } else if (intent.getAction().endsWith(ACTION_STOP)) { |
| stopTracing(); |
| } else { |
| Log.e(TAG, "Unexpected intent: " + intent); |
| } |
| } |
| } |
| |
| private int mNativeTracingControllerAndroid; |
| private native int nativeInit(); |
| private native void nativeDestroy(int nativeTracingControllerAndroid); |
| private native boolean nativeStartTracing(int nativeTracingControllerAndroid, String filename, |
| String categories, boolean recordContinuously); |
| private native void nativeStopTracing(int nativeTracingControllerAndroid); |
| private native String nativeGetDefaultCategories(); |
| } |