TracingControllerAndroid.java revision 010d83a9304c5a91596085d917d248abff47903a
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    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     * Generates a unique filename to be used for tracing in the Downloads directory.
118     */
119    @CalledByNative
120    private static String generateTracingFilePath() {
121        String state = Environment.getExternalStorageState();
122        if (!Environment.MEDIA_MOUNTED.equals(state)) {
123            return null;
124        }
125
126        // Generate a hopefully-unique filename using the UTC timestamp.
127        // (Not a huge problem if it isn't unique, we'll just append more data.)
128        SimpleDateFormat formatter = new SimpleDateFormat(
129                "yyyy-MM-dd-HHmmss", Locale.US);
130        formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
131        File dir = Environment.getExternalStoragePublicDirectory(
132                Environment.DIRECTORY_DOWNLOADS);
133        File file = new File(
134                dir, "chrome-profile-results-" + formatter.format(new Date()));
135        return file.getPath();
136    }
137
138    /**
139     * Start profiling to a new file in the Downloads directory.
140     *
141     * Calls #startTracing(String, boolean, String, boolean) with a new timestamped filename.
142     * @see #startTracing(String, boolean, String, boolean)
143     */
144    public boolean startTracing(boolean showToasts, String categories,
145            boolean recordContinuously) {
146        mShowToasts = showToasts;
147
148        String filePath = generateTracingFilePath();
149        if (filePath == null) {
150          logAndToastError(
151              mContext.getString(R.string.profiler_no_storage_toast));
152        }
153        return startTracing(filePath, showToasts, categories, recordContinuously);
154    }
155
156    private void initializeNativeControllerIfNeeded() {
157        if (mNativeTracingControllerAndroid == 0) {
158            mNativeTracingControllerAndroid = nativeInit();
159        }
160    }
161
162    /**
163     * Start profiling to the specified file. Returns true on success.
164     *
165     * Only one TracingControllerAndroid can be running at the same time. If another profiler
166     * is running when this method is called, it will be cancelled. If this
167     * profiler is already running, this method does nothing and returns false.
168     *
169     * @param filename The name of the file to output the profile data to.
170     * @param showToasts Whether or not we want to show toasts during this profiling session.
171     * When we are timing the profile run we might not want to incur extra draw overhead of showing
172     * notifications about the profiling system.
173     * @param categories Which categories to trace. See TracingControllerAndroid::BeginTracing()
174     * (in content/public/browser/trace_controller.h) for the format.
175     * @param recordContinuously Record until the user ends the trace. The trace buffer is fixed
176     * size and we use it as a ring buffer during recording.
177     */
178    public boolean startTracing(String filename, boolean showToasts, String categories,
179            boolean recordContinuously) {
180        mShowToasts = showToasts;
181        if (isTracing()) {
182            // Don't need a toast because this shouldn't happen via the UI.
183            Log.e(TAG, "Received startTracing, but we're already tracing");
184            return false;
185        }
186        // Lazy initialize the native side, to allow construction before the library is loaded.
187        initializeNativeControllerIfNeeded();
188        if (!nativeStartTracing(mNativeTracingControllerAndroid, categories,
189                recordContinuously)) {
190            logAndToastError(mContext.getString(R.string.profiler_error_toast));
191            return false;
192        }
193
194        logAndToastInfo(mContext.getString(R.string.profiler_started_toast) + ": " + categories);
195        mFilename = filename;
196        mIsTracing = true;
197        return true;
198    }
199
200    /**
201     * Stop profiling. This won't take effect until Chrome has flushed its file.
202     */
203    public void stopTracing() {
204        if (isTracing()) {
205            nativeStopTracing(mNativeTracingControllerAndroid, mFilename);
206        }
207    }
208
209    /**
210     * Called by native code when the profiler's output file is closed.
211     */
212    @CalledByNative
213    protected void onTracingStopped() {
214        if (!isTracing()) {
215            // Don't need a toast because this shouldn't happen via the UI.
216            Log.e(TAG, "Received onTracingStopped, but we aren't tracing");
217            return;
218        }
219
220        logAndToastInfo(
221                mContext.getString(R.string.profiler_stopped_toast, mFilename));
222        mIsTracing = false;
223        mFilename = null;
224    }
225
226    /**
227     * Get known category groups.
228     */
229    public void getCategoryGroups() {
230        // Lazy initialize the native side, to allow construction before the library is loaded.
231        initializeNativeControllerIfNeeded();
232        if (!nativeGetKnownCategoryGroupsAsync(mNativeTracingControllerAndroid)) {
233            Log.e(TAG, "Unable to fetch tracing record groups list.");
234        }
235    }
236
237    @Override
238    protected void finalize() {
239        if (mNativeTracingControllerAndroid != 0) {
240            nativeDestroy(mNativeTracingControllerAndroid);
241            mNativeTracingControllerAndroid = 0;
242        }
243    }
244
245    void logAndToastError(String str) {
246        Log.e(TAG, str);
247        if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show();
248    }
249
250    void logAndToastInfo(String str) {
251        Log.i(TAG, str);
252        if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show();
253    }
254
255    private static class TracingIntentFilter extends IntentFilter {
256        TracingIntentFilter(Context context) {
257            addAction(context.getPackageName() + "." + ACTION_START);
258            addAction(context.getPackageName() + "." + ACTION_STOP);
259            addAction(context.getPackageName() + "." + ACTION_LIST_CATEGORIES);
260        }
261    }
262
263    class TracingBroadcastReceiver extends BroadcastReceiver {
264        @Override
265        public void onReceive(Context context, Intent intent) {
266            if (intent.getAction().endsWith(ACTION_START)) {
267                String categories = intent.getStringExtra(CATEGORIES_EXTRA);
268                if (TextUtils.isEmpty(categories)) {
269                    categories = nativeGetDefaultCategories();
270                } else {
271                    categories = categories.replaceFirst(
272                            DEFAULT_CHROME_CATEGORIES_PLACE_HOLDER, nativeGetDefaultCategories());
273                }
274                boolean recordContinuously =
275                        intent.getStringExtra(RECORD_CONTINUOUSLY_EXTRA) != null;
276                String filename = intent.getStringExtra(FILE_EXTRA);
277                if (filename != null) {
278                    startTracing(filename, true, categories, recordContinuously);
279                } else {
280                    startTracing(true, categories, recordContinuously);
281                }
282            } else if (intent.getAction().endsWith(ACTION_STOP)) {
283                stopTracing();
284            } else if (intent.getAction().endsWith(ACTION_LIST_CATEGORIES)) {
285                getCategoryGroups();
286            } else {
287                Log.e(TAG, "Unexpected intent: " + intent);
288            }
289        }
290    }
291
292    private long mNativeTracingControllerAndroid;
293    private native long nativeInit();
294    private native void nativeDestroy(long nativeTracingControllerAndroid);
295    private native boolean nativeStartTracing(
296            long nativeTracingControllerAndroid, String categories, boolean recordContinuously);
297    private native void nativeStopTracing(long nativeTracingControllerAndroid, String filename);
298    private native boolean nativeGetKnownCategoryGroupsAsync(long nativeTracingControllerAndroid);
299    private native String nativeGetDefaultCategories();
300}
301