/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.speedtracer.client.model;
import com.google.gwt.chrome.crx.client.Chrome;
import com.google.gwt.chrome.crx.client.Debugger;
import com.google.gwt.chrome.crx.client.Debugger.AttachCallback;
import com.google.gwt.chrome.crx.client.events.DebuggerEvent;
import com.google.gwt.chrome.crx.client.events.DebuggerEvent.Debuggee;
import com.google.gwt.chrome.crx.client.events.DebuggerEvent.RawDebuggerEventRecord;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.coreext.client.DataBag;
import com.google.gwt.coreext.client.JSOArray;
import com.google.gwt.coreext.client.JsIntegerMap;
import com.google.speedtracer.client.model.UiEvent.LeafFirstTraversalVoid;
import com.google.speedtracer.shared.EventRecordType;
/**
* This class is used in Chrome when we are getting data from the chrome
* debugger API. Its job is to receive data from the devtools API, ensure that
* the data in properly transformed into a consumable form, and to invoke
* callbacks passed in from the UI. We use this overlay type as the object we
* pass to the Monitor UI.
*
* See documentaion at: <a href=
* "http://code.google.com/chrome/extensions/trunk/debugger.html"
* >chrome.experimental.debugger</a>
*/
public class ChromeDebuggerDataInstance extends DataInstance {;
/**
* Maps the Chrome specific types to Speedtracer event types.
*/
private final static class TypeTranslationMap extends JavaScriptObject {
protected TypeTranslationMap() {
}
static TypeTranslationMap create() {
final TypeTranslationMap map = JavaScriptObject.createObject().cast();
// The weird end of line comments below keep the auto-formatter from
// from reformatting these lines.
return map //
.add("Program", EventRecordType.PROGRAM_EVENT)
.add("EventDispatch", EventRecordType.DOM_EVENT) //
.add("Layout", EventRecordType.LAYOUT_EVENT) //
.add("RecalculateStyles", EventRecordType.RECALC_STYLE_EVENT) //
.add("Paint", EventRecordType.PAINT_EVENT) //
.add("ParseHTML", EventRecordType.PARSE_HTML_EVENT) //
.add("TimerInstall", EventRecordType.TIMER_INSTALLED) //
.add("TimerRemove", EventRecordType.TIMER_CLEARED) //
.add("TimerFire", EventRecordType.TIMER_FIRED) //
.add("XHRReadyStateChange", EventRecordType.XHR_READY_STATE_CHANGE) //
.add("XHRLoad", EventRecordType.XHR_LOAD) //
.add("EvaluateScript", EventRecordType.EVAL_SCRIPT_EVENT) //
// MarkTimeline has been deprecated for TimeStamp, this can be
// removed soon.
.add("MarkTimeline", EventRecordType.LOG_MESSAGE_EVENT) //
.add("TimeStamp", EventRecordType.LOG_MESSAGE_EVENT) //
.add("ScheduleResourceRequest", EventRecordType.SCHEDULE_RESOURCE_REQUEST) //
.add("ResourceSendRequest", EventRecordType.RESOURCE_SEND_REQUEST) //
.add("ResourceReceiveResponse", EventRecordType.RESOURCE_RECEIVE_RESPONSE) //
.add("ResourceReceivedData", EventRecordType.RESOURCE_DATA_RECEIVED) //
.add("ResourceFinish", EventRecordType.RESOURCE_FINISH) //
.add("FunctionCall", EventRecordType.JAVASCRIPT_EXECUTION) //
.add("GCEvent", EventRecordType.GC_EVENT) //
.add("MarkDOMContent", EventRecordType.DOM_CONTENT_LOADED) //
.add("MarkLoad", EventRecordType.LOAD_EVENT);
}
native TypeTranslationMap add(String name, int id) /*-{
this[name] = id;
return this;
}-*/;
native int get(String typeName) /*-{
var type = this[typeName];
if (type === undefined) {
// When new types come in, deal with it here.
// console.log(typeName);
return @com.google.speedtracer.client.model.EventRecord::INVALID_TYPE
} else {
return type;
}
}-*/;
}
/**
* Proxy class that normalizes data coming in from the debugger API into a
* digestable form, and then forwards it on to the ChromeDebuggerDataInstance.
*/
public static class Proxy implements DataProxy {
private class TimeNormalizingVisitor implements LeafFirstTraversalVoid {
public void visit(UiEvent event) {
assert getBaseTime() >= 0 : "baseTime should already be set.";
event.<UnNormalizedEventRecord> cast().convertToEventRecord(getBaseTime());
}
}
private static class TypeTranslationVisitor implements LeafFirstTraversalVoid {
private final TypeTranslationMap map = TypeTranslationMap.create();
private native static void updateType(UiEvent event, int type) /*-{
event.type = type;
}-*/;
public void visit(UiEvent event) {
updateType(event, map.get(DataBag.getStringProperty(event, "type")));
}
}
private double baseTime;
private ChromeDebuggerDataInstance dataInstance;
private final Dispatcher dispatcher;
private JSOArray<UnNormalizedEventRecord> pendingRecords = JSOArray.create();
private final int tabId;
private final TimeNormalizingVisitor timeNormalizingVisitor = new TimeNormalizingVisitor();
private final TypeTranslationVisitor typeTranslationVistior = new TypeTranslationVisitor();
private double lastStartTime;
boolean profilingStarted = false;
public Proxy(int tabId) {
this.baseTime = -1;
this.tabId = tabId;
this.dispatcher = Dispatcher.create(this);
lastStartTime = -1;
}
// This is used only for loading from a saved file. We exploit the fact that the debugger
// records include the method inside themselves redundantly to learn the method at dispatch
// time. Normally, when records come out of the Debugger API, they already give us the method so
// we need not pull it out of the record.
public final void dispatchDebuggerEventRecord(RawDebuggerEventRecord body) {
dispatchDebuggerEventRecord(body.getMethod(), body);
}
private final void dispatchDebuggerEventRecord(String method, RawDebuggerEventRecord body) {
dispatcher.invoke(method, body);
}
public double getBaseTime() {
return baseTime;
}
public void load(DataInstance dataInstance) {
this.dataInstance = dataInstance.cast();
connectToDataSource();
}
public void resumeMonitoring() {
connectToDataSource();
}
public void setBaseTime(double baseTime) {
this.baseTime = baseTime;
}
public void setProfilingOptions(boolean enableStackTraces, boolean enableCpuProfiling) {
// No op. Stack traces are already on.
// TODO(jaimeyap): One day... turn on CPU profiling!
}
public void stopMonitoring() {
profilingStarted = false;
stopTimeline(tabId);
stopNetwork(tabId);
stopPage(tabId);
Debugger.detach(tabId);
}
public void unload() {
// reset the base time.
this.baseTime = -1;
stopMonitoring();
}
protected void connectToDataSource() {
try {
final Proxy me = this;
Debugger.attach(tabId, new AttachCallback() {
public void onAttach() {
startTimeline(tabId, me);
startNetwork(tabId, me);
startPage(tabId, me);
}
});
} catch (JavaScriptException ex) {
Chrome.getExtension().getBackgroundPage().getConsole().log(
"Error attaching to Debugger: " + ex);
// ignore
}
}
/**
* Establishes a base time if it has not been set and dispatches the event
* to the {@link DataInstance}.
*
* @param record the already normalized record to dispatch
*/
private void forwardToDataInstance(EventRecord record) {
assert (!Double.isNaN(record.getTime())) : "Time was not normalized!";
// TODO(jaimeyap/knorton): Remove this hack.
// Workaround for http://code.google.com/p/speedtracer/issues/detail?id=29
// WebKit sometimes delivers timeline records with a negative timestamp.
// We simply discard these until this issue can be resolved upstream.
if (record.getTime() < 0) {
return;
}
dataInstance.onEventRecord(record);
}
/**
* Establishes a base time if it has not been set and dispatches the event
* to the {@link DataInstance}.
*
* This method will normalizes times for any record passed in.
*
* @param record the record to dispatch
*/
private void normalizeAndDispatchEventRecord(UnNormalizedEventRecord record) {
if (getBaseTime() < 0) {
sendPendingRecordsAndSetBaseTime(record);
}
assert (getBaseTime() >= 0) : "Base Time is still not set";
// Run a visitor to normalize the times for this tree.
record.<UiEvent> cast().apply(timeNormalizingVisitor);
forwardToDataInstance(record);
}
/**
* Normalizes the inputed time to be relative to the base time, and converts
* the units of the inputed time to milliseconds from seconds.
*/
private double normalizeNetworkTime(double seconds) {
assert getBaseTime() >= 0 : "NormalizeTime called before a base time was established.";
double millis = seconds * 1000;
return millis - getBaseTime();
}
@SuppressWarnings("unused")
private void onNetworkResponseReceived(NetworkResponseReceivedEvent.Data data) {
// Normalize the detailed timing request time.
DetailedResponseTiming timing = data.getResponse().getDetailedTiming();
if (timing != null) {
timing.setRequestTime(normalizeNetworkTime(timing.getRequestTime()));
}
onNetworkResourceMessage(EventRecordType.NETWORK_RESPONSE_RECEIVED, data);
}
@SuppressWarnings("unused")
private void onPageFrameNavigated(JavaScriptObject body) {
FrameNavigation navigation = body.cast();
if (!navigation.isSubFrame()) {
normalizeAndDispatchEventRecord(TabChangeEvent.createUnNormalized(lastStartTime, navigation.getUrl()));
}
}
private void onNetworkResourceMessage(int messageType, NetworkEvent.Data data) {
if (getBaseTime() < 0) {
// We only allow proper timeline agent records to set base time.
return;
}
forwardToDataInstance(NetworkEvent.create(messageType,
normalizeNetworkTime(data.getTimeStamp()), data));
}
@SuppressWarnings("unused")
private void onNetworkDataReceived(NetworkDataReceivedEvent.Data data) {
onNetworkResourceMessage(EventRecordType.NETWORK_DATA_RECEIVED, data);
}
@SuppressWarnings("unused")
private void onNetworkLoadingFinished(NetworkDataReceivedEvent.Data data) {
onNetworkResourceMessage(EventRecordType.NETWORK_LOADING_FINISHED, data);
}
private void onTimelineProfilerStarted() {
if (profilingStarted) {
return;
}
profilingStarted = true;
dataInstance.onTimelineProfilerStarted();
}
@SuppressWarnings("unused")
private void onTimelineRecord(UnNormalizedEventRecord record) {
assert (dataInstance != null) : "Someone called invoke that wasn't our connect call!";
// When this visitor is applied, the appropriate speed tracer type
// is set. An issue occurs if a record comes through and is pushed
// onto pending and then sent back to onTimelineRecord. Therefore,
// any saved records should be sent directly to sendRecord()
record.<UiEvent> cast().apply(typeTranslationVistior);
// As of Chrome 22, we now have this silly top level event that is wrapping what
// used to be top level events.
if (record.getType() == EventRecordType.PROGRAM_EVENT) {
final JSOArray<UiEvent> children = record.<UiEvent> cast().getChildren();
for (int i=0,n=children.size();i<n;i++) {
sendRecord(children.get(i).<UnNormalizedEventRecord> cast());
}
return;
}
if(record.getType() == ResourceWillSendEvent.TYPE) {
// We do not want to immediately assume that a resource start is
// eligible to establish the base time.
// If the start actually happened as a child of some event trace, then
// using this to establish base time could lead to negative times for
// events since all network resource events are short circuited.
// We buffer it for now and wait for an event that is not a Resource
// Start to make the decision as to what should be the base time.
if (getBaseTime() < 0) {
pendingRecords.push(record);
return;
}
}
sendRecord(record);
}
/**
* Processes event records.
* Page transition events are processesd
* @param record
*/
private void sendRecord(UnNormalizedEventRecord record) {
lastStartTime = record.getStartTime();
// Normalize and send to the dataInstance.
normalizeAndDispatchEventRecord(record);
}
@SuppressWarnings("unused")
private void onNetworkRequestWillBeSent(NetworkRequestWillBeSentEvent.Data data) {
onNetworkResourceMessage(EventRecordType.NETWORK_REQUEST_WILL_BE_SENT, data);
}
/**
* Clears the record buffer and establishes a baseTime.
*
* @param triggerRecord the first record that is not a Resource Start.
*/
private void sendPendingRecordsAndSetBaseTime(UnNormalizedEventRecord triggerRecord) {
assert (getBaseTime() < 0) : "Emptying record buffer after establishing a base time.";
double baseTimeStamp = triggerRecord.getStartTime();
if (pendingRecords.size() > 0) {
// Normalize base time using either the event that triggered the check,
// or the first event that we buffered.
UnNormalizedEventRecord firstStart = pendingRecords.get(0).cast();
if (firstStart.getStartTime() < baseTimeStamp) {
baseTimeStamp = firstStart.getStartTime();
}
}
setBaseTime(baseTimeStamp);
// Now that we have set the base time, we can replay the buffered Record
// Starts since they did come in first, and they in fact still need to
// go through normalization and through the page transition logic.
for (int i = 0, n = pendingRecords.size(); i < n; i++) {
sendRecord(pendingRecords.get(i));
}
// Nuke the pending records list.
pendingRecords = JSOArray.create();
}
}
/**
* Represents a Page.frameNavigation event
* #TODO (sarahgsmith) consider sending this event with an isTabChange
* flag instead of using TabChangeEvent
*/
private final static class FrameNavigation extends JavaScriptObject {
protected FrameNavigation() {}
public native String getUrl() /*-{
return this.url;
}-*/;
public native String getId() /*-{
return this.id;
}-*/;
/**
* HACK (jaimeyap)!
* Top level frame navigations have no name field set. Let's exploit that to
* avoid iframe cheese.
*/
public native boolean isSubFrame() /*-{
return this.hasOwnProperty("name");
}-*/;
}
/**
* Overlay type for our dispatcher used by {@link Proxy}.
*/
private static final class Dispatcher extends JavaScriptObject {
/**
* Simple routing dispatcher used by the DevToolsDataProxy to quickly route.
*/
static native Dispatcher create(Proxy delegate) /*-{
var dispatcher = {};
dispatcher['Page.frameNavigated'] = function(body) {
delegate.
@com.google.speedtracer.client.model.ChromeDebuggerDataInstance.Proxy::onPageFrameNavigated(Lcom/google/gwt/core/client/JavaScriptObject;)
(body.frame);
};
// Events generated by the Timeline profiler.
dispatcher['Timeline.eventRecorded'] = function(body) {
delegate.
@com.google.speedtracer.client.model.ChromeDebuggerDataInstance.Proxy::onTimelineRecord(Lcom/google/speedtracer/client/model/UnNormalizedEventRecord;)
(body.record);
};
// Network resource events.
dispatcher['Network.requestWillBeSent'] = function(body) {
delegate.
@com.google.speedtracer.client.model.ChromeDebuggerDataInstance.Proxy::onNetworkRequestWillBeSent(Lcom/google/speedtracer/client/model/NetworkRequestWillBeSentEvent$Data;)
(body);
};
dispatcher['Network.responseReceived'] = function(body) {
delegate.
@com.google.speedtracer.client.model.ChromeDebuggerDataInstance.Proxy::onNetworkResponseReceived(Lcom/google/speedtracer/client/model/NetworkResponseReceivedEvent$Data;)
(body);
};
dispatcher['Network.dataReceived'] = function(body) {
delegate.
@com.google.speedtracer.client.model.ChromeDebuggerDataInstance.Proxy::onNetworkDataReceived(Lcom/google/speedtracer/client/model/NetworkDataReceivedEvent$Data;)
(body);
};
dispatcher['Network.loadingFinished'] = function(body) {
delegate.
@com.google.speedtracer.client.model.ChromeDebuggerDataInstance.Proxy::onNetworkLoadingFinished(Lcom/google/speedtracer/client/model/NetworkDataReceivedEvent$Data;)
(body);
};
return dispatcher;
}-*/;
protected Dispatcher() {
}
native void invoke(String method, JavaScriptObject payload) /*-{
if (this[method]) {
this[method](payload);
} else {
// For debugging breakages.
// console.log("No such method! -> " + method);
}
}-*/;
}
/**
* Constructs and returns a {@link ChromeDebuggerDataInstance} which is used to receive Timeline and
* Network events from the debugger API.
*
* @param tabId the tab that we want to connect to.
* @return a newly wired up {@link ChromeDebuggerDataInstance}.
*/
public static ChromeDebuggerDataInstance create(int tabId) {
Proxy proxy = new Proxy(tabId);
ensureRecordRouter();
recordRouter.put(tabId, proxy);
return DataInstance.create(proxy).cast();
}
/**
* Constructs and returns a {@link ChromeDebuggerDataInstance} which is used to
* receive events over the extensions-devtools API.
*
* @param proxy an externally supplied proxy to act as the record
* transformation layer
* @return a newly wired up {@link ChromeDebuggerDataInstance}.
*/
public static ChromeDebuggerDataInstance create(Proxy proxy) {
ensureRecordRouter();
recordRouter.put(proxy.tabId, proxy);
return DataInstance.create(proxy).cast();
}
protected ChromeDebuggerDataInstance() {
}
/**
* Chrome debugger API has a single output for all records. We assume that there is only one
* Proxy mapped to a given tab ID. We use this to route messages to the appropriate Proxy.
*/
private static JsIntegerMap<Proxy> recordRouter;
private static void ensureRecordRouter() {
if (recordRouter == null) {
recordRouter = JsIntegerMap.create();
Debugger.getEvent().addListener(new DebuggerEvent.Listener() {
public void onEvent(Debuggee source, String method, RawDebuggerEventRecord params) {
Proxy proxy = recordRouter.get(source.getTabId());
if (proxy == null) {
// Some other extension must be debugging this.
return;
}
// Dispatch the event to this guy.
proxy.dispatchDebuggerEventRecord(method, params);
}
});
}
}
private static void startTimeline(final int tabId, final Proxy proxy) {
Debugger.sendRequest(tabId, "Timeline.start", createStartTimelineParams(tabId),
new Debugger.SendRequestCallback() {
public void onResponse(JavaScriptObject result) {
if (result == null) {
// Starting failed.
Chrome.getExtension().getBackgroundPage().getConsole().log(
"Error starting timeline for tab: " + tabId);
}
proxy.onTimelineProfilerStarted();
}
});
}
private static void stopTimeline(int tabId) {
Debugger.sendCommand(tabId, "Timeline.stop");
}
private static native JavaScriptObject createStartTimelineParams(int tabId) /*-{
return {
"id": tabId + (new Date().getTime()),
"method": "Timeline.start",
"params": {
"maxCallStackDepth": 5
}
}
}-*/;
private static void startNetwork(final int tabId, final Proxy proxy) {
Debugger.sendRequest(tabId, "Network.enable", createStartNetworkParams(tabId),
new Debugger.SendRequestCallback() {
public void onResponse(JavaScriptObject result) {
if (result == null) {
// Starting failed.
Chrome.getExtension().getBackgroundPage().getConsole().log(
"Error starting network tracing for tab: " + tabId);
}
proxy.onTimelineProfilerStarted();
}
});
}
private static void stopNetwork(int tabId) {
Debugger.sendCommand(tabId, "Network.disable");
}
private static native JavaScriptObject createStartNetworkParams(int tabId) /*-{
return {
"id": tabId + (new Date().getTime()),
"method": "Network.enable"
}
}-*/;
private static void startPage(final int tabId, final Proxy proxy) {
Debugger.sendRequest(tabId, "Page.enable", createStartPageParams(tabId),
new Debugger.SendRequestCallback() {
public void onResponse(JavaScriptObject result) {
if (result == null) {
// Starting failed.
Chrome.getExtension().getBackgroundPage().getConsole().log(
"Error starting page events for tab: " + tabId);
}
proxy.onTimelineProfilerStarted();
}
});
}
private static void stopPage(int tabId) {
Debugger.sendCommand(tabId, "Page.disable");
}
private static native JavaScriptObject createStartPageParams(int tabId) /*-{
return {
"id": tabId + (new Date().getTime()),
"method": "Network.enable"
}
}-*/;
}