// 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.base.TraceEvent;
import org.chromium.content.R;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/**
* 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 long mNativeTracingControllerAndroid;
private native long nativeInit();
private native void nativeDestroy(long nativeTracingControllerAndroid);
private native boolean nativeStartTracing(long nativeTracingControllerAndroid, String filename,
String categories, boolean recordContinuously);
private native void nativeStopTracing(long nativeTracingControllerAndroid);
private native String nativeGetDefaultCategories();
}