TracingControllerAndroid.java revision f2477e01787aa58f445919b809d89e252beef54f
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;
19import org.chromium.content.common.TraceEvent;
20
21import java.io.File;
22import java.text.SimpleDateFormat;
23import java.util.Date;
24import java.util.Locale;
25import 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")
44public 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
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     * 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     */
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     */
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     */
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
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
214    protected void finalize() {
215        if (mNativeTracingControllerAndroid != 0) {
216            nativeDestroy(mNativeTracingControllerAndroid);
217            mNativeTracingControllerAndroid = 0;
218        }
219    }
220
221    void logAndToastError(String str) {
222        Log.e(TAG, str);
223        if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show();
224    }
225
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 {
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
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;
266    private native long nativeInit();
267    private native void nativeDestroy(long nativeTracingControllerAndroid);
268    private native boolean nativeStartTracing(long nativeTracingControllerAndroid, String filename,
269            String categories, boolean recordContinuously);
270    private native void nativeStopTracing(long nativeTracingControllerAndroid);
271    private native String nativeGetDefaultCategories();
272}
273