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