e405922df3b2ee1bc72a73527653f26e9e06280b
[platform/framework/web/crosswalk.git] / src / content / public / android / java / src / org / chromium / content / browser / TracingControllerAndroid.java
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.base.TraceEvent;
19 import org.chromium.content.R;
20
21 import java.io.File;
22 import java.text.SimpleDateFormat;
23 import java.util.Date;
24 import java.util.Locale;
25 import 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")
44 public 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 }