com.google.speedtracer.client.model.ChromeDebuggerDataInstance.java Source code

Java tutorial

Introduction

Here is the source code for com.google.speedtracer.client.model.ChromeDebuggerDataInstance.java

Source

/*
 * 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"
                                                                            }
                                                                            }-*/;
}