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