• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
5 package org.chromium.content.browser;
6 
7 import android.content.BroadcastReceiver;
8 import android.content.Context;
9 import android.content.Intent;
10 import android.content.IntentFilter;
11 import android.os.Environment;
12 import android.text.TextUtils;
13 import android.util.Log;
14 import android.widget.Toast;
15 
16 import org.chromium.base.CalledByNative;
17 import org.chromium.base.JNINamespace;
18 import org.chromium.content.R;
19 
20 import java.io.File;
21 import java.text.SimpleDateFormat;
22 import java.util.Date;
23 import java.util.Locale;
24 import 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")
43 public 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 
TracingControllerAndroid(Context context)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      */
getBroadcastReceiver()81     public BroadcastReceiver getBroadcastReceiver() {
82         return mBroadcastReceiver;
83     }
84 
85     /**
86      * Get an IntentFilter for profiler intents.
87      */
getIntentFilter()88     public IntentFilter getIntentFilter() {
89         return mIntentFilter;
90     }
91 
92     /**
93      * Register a BroadcastReceiver in the given context.
94      */
registerReceiver(Context context)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      */
unregisterReceiver(Context context)103     public void unregisterReceiver(Context context) {
104         context.unregisterReceiver(getBroadcastReceiver());
105     }
106 
107     /**
108      * Returns true if we're currently profiling.
109      */
isTracing()110     public boolean isTracing() {
111         return mIsTracing;
112     }
113 
114     /**
115      * Returns the path of the current output file. Null if isTracing() false.
116      */
getOutputPath()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
generateTracingFilePath()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, boolean) with a new timestamped filename.
147      * @see #startTracing(String, boolean, String, boolean)
148      */
startTracing(boolean showToasts, String categories, boolean recordContinuously)149     public boolean startTracing(boolean showToasts, String categories,
150             boolean recordContinuously) {
151         mShowToasts = showToasts;
152 
153         String filePath = generateTracingFilePath();
154         if (filePath == null) {
155           logAndToastError(
156               mContext.getString(R.string.profiler_no_storage_toast));
157         }
158         return startTracing(filePath, showToasts, categories, recordContinuously);
159     }
160 
initializeNativeControllerIfNeeded()161     private void initializeNativeControllerIfNeeded() {
162         if (mNativeTracingControllerAndroid == 0) {
163             mNativeTracingControllerAndroid = nativeInit();
164         }
165     }
166 
167     /**
168      * Start profiling to the specified file. Returns true on success.
169      *
170      * Only one TracingControllerAndroid can be running at the same time. If another profiler
171      * is running when this method is called, it will be cancelled. If this
172      * profiler is already running, this method does nothing and returns false.
173      *
174      * @param filename The name of the file to output the profile data to.
175      * @param showToasts Whether or not we want to show toasts during this profiling session.
176      * When we are timing the profile run we might not want to incur extra draw overhead of showing
177      * notifications about the profiling system.
178      * @param categories Which categories to trace. See TracingControllerAndroid::BeginTracing()
179      * (in content/public/browser/trace_controller.h) for the format.
180      * @param recordContinuously Record until the user ends the trace. The trace buffer is fixed
181      * size and we use it as a ring buffer during recording.
182      */
startTracing(String filename, boolean showToasts, String categories, boolean recordContinuously)183     public boolean startTracing(String filename, boolean showToasts, String categories,
184             boolean recordContinuously) {
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                 recordContinuously)) {
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      */
stopTracing()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
onTracingStopped()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      */
getCategoryGroups()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
finalize()244     protected void finalize() {
245         if (mNativeTracingControllerAndroid != 0) {
246             nativeDestroy(mNativeTracingControllerAndroid);
247             mNativeTracingControllerAndroid = 0;
248         }
249     }
250 
logAndToastError(String str)251     private void logAndToastError(String str) {
252         Log.e(TAG, str);
253         if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show();
254     }
255 
256     // The |str| string needs to match the ones that adb_chrome_profiler looks for.
logForProfiler(String str)257     private void logForProfiler(String str) {
258         Log.i(TAG, str);
259     }
260 
showToast(String str)261     private void showToast(String str) {
262         if (mShowToasts) Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show();
263     }
264 
265     private static class TracingIntentFilter extends IntentFilter {
TracingIntentFilter(Context context)266         TracingIntentFilter(Context context) {
267             addAction(context.getPackageName() + "." + ACTION_START);
268             addAction(context.getPackageName() + "." + ACTION_STOP);
269             addAction(context.getPackageName() + "." + ACTION_LIST_CATEGORIES);
270         }
271     }
272 
273     class TracingBroadcastReceiver extends BroadcastReceiver {
274         @Override
onReceive(Context context, Intent intent)275         public void onReceive(Context context, Intent intent) {
276             if (intent.getAction().endsWith(ACTION_START)) {
277                 String categories = intent.getStringExtra(CATEGORIES_EXTRA);
278                 if (TextUtils.isEmpty(categories)) {
279                     categories = nativeGetDefaultCategories();
280                 } else {
281                     categories = categories.replaceFirst(
282                             DEFAULT_CHROME_CATEGORIES_PLACE_HOLDER, nativeGetDefaultCategories());
283                 }
284                 boolean recordContinuously =
285                         intent.getStringExtra(RECORD_CONTINUOUSLY_EXTRA) != null;
286                 String filename = intent.getStringExtra(FILE_EXTRA);
287                 if (filename != null) {
288                     startTracing(filename, true, categories, recordContinuously);
289                 } else {
290                     startTracing(true, categories, recordContinuously);
291                 }
292             } else if (intent.getAction().endsWith(ACTION_STOP)) {
293                 stopTracing();
294             } else if (intent.getAction().endsWith(ACTION_LIST_CATEGORIES)) {
295                 getCategoryGroups();
296             } else {
297                 Log.e(TAG, "Unexpected intent: " + intent);
298             }
299         }
300     }
301 
302     private long mNativeTracingControllerAndroid;
nativeInit()303     private native long nativeInit();
nativeDestroy(long nativeTracingControllerAndroid)304     private native void nativeDestroy(long nativeTracingControllerAndroid);
nativeStartTracing( long nativeTracingControllerAndroid, String categories, boolean recordContinuously)305     private native boolean nativeStartTracing(
306             long nativeTracingControllerAndroid, String categories, boolean recordContinuously);
nativeStopTracing(long nativeTracingControllerAndroid, String filename)307     private native void nativeStopTracing(long nativeTracingControllerAndroid, String filename);
nativeGetKnownCategoryGroupsAsync(long nativeTracingControllerAndroid)308     private native boolean nativeGetKnownCategoryGroupsAsync(long nativeTracingControllerAndroid);
nativeGetDefaultCategories()309     private native String nativeGetDefaultCategories();
310 }
311