com.google.speedtracer.client.BackgroundPage.java Source code

Java tutorial

Introduction

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

Source

/*
 * Copyright 2009 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;

import com.google.gwt.chrome.crx.client.Chrome;
import com.google.gwt.chrome.crx.client.Extension;
import com.google.gwt.chrome.crx.client.Icon;
import com.google.gwt.chrome.crx.client.Port;
import com.google.gwt.chrome.crx.client.Tabs;
import com.google.gwt.chrome.crx.client.Tabs.OnTabCallback;
import com.google.gwt.chrome.crx.client.Tabs.Tab;
import com.google.gwt.chrome.crx.client.Windows;
import com.google.gwt.chrome.crx.client.Windows.OnWindowCallback;
import com.google.gwt.chrome.crx.client.Windows.Window;
import com.google.gwt.chrome.crx.client.events.BrowserActionEvent;
import com.google.gwt.chrome.crx.client.events.ConnectEvent;
import com.google.gwt.chrome.crx.client.events.ConnectExternalEvent;
import com.google.gwt.chrome.crx.client.events.MessageEvent;
import com.google.gwt.chrome.crx.client.events.RequestEvent;
import com.google.gwt.chrome.crx.client.events.RequestExternalEvent;
import com.google.gwt.chrome.crx.client.events.SendResponse;
import com.google.gwt.chrome.crx.client.events.Sender;
import com.google.gwt.chrome.crx.client.events.TabUpdatedEvent;
import com.google.gwt.chrome.crx.client.events.TabUpdatedEvent.ChangeInfo;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.coreext.client.DataBag;
import com.google.gwt.events.client.Event;
import com.google.gwt.events.client.EventListener;
import com.google.speedtracer.client.WindowChannel.Client;
import com.google.speedtracer.client.WindowChannel.Request;
import com.google.speedtracer.client.WindowChannel.Server;
import com.google.speedtracer.client.WindowChannel.ServerListener;
import com.google.speedtracer.client.messages.EventRecordMessage;
import com.google.speedtracer.client.messages.InitializeMonitorMessage;
import com.google.speedtracer.client.messages.PageEventMessage;
import com.google.speedtracer.client.messages.RecordingDataMessage;
import com.google.speedtracer.client.messages.RequestInitializationMessage;
import com.google.speedtracer.client.messages.ResendProfilingOptions;
import com.google.speedtracer.client.messages.ResetBaseTimeMessage;
import com.google.speedtracer.client.model.DataInstance;
import com.google.speedtracer.client.model.ChromeDebuggerDataInstance;
import com.google.speedtracer.client.model.ChromeDebuggerDataInstance.Proxy;
import com.google.speedtracer.client.model.ExternalExtensionDataInstance;
import com.google.speedtracer.client.model.ExternalExtensionDataInstance.ConnectRequest;
import com.google.speedtracer.client.model.LoadFileDataInstance;
import com.google.speedtracer.client.model.TabDescription;
import com.google.speedtracer.client.model.VersionedRecordConverter;
import com.google.speedtracer.client.util.dom.WindowExt;

import java.util.HashMap;

/**
 * The Chrome extension background page script.
 */
@Extension.ManifestInfo(name = "Speed Tracer (by Google)", description = "Get insight into the performance of your web applications.", version = ClientConfig.VERSION, permissions = {
        "tabs", "http://*/*", "https://*/*", "debugger" }, icons = { "resources/icon16.png", "resources/icon32.png",
                "resources/icon48.png", "resources/icon128.png" }, publicKey = "")
public abstract class BackgroundPage extends Extension {

    /**
     * Listener that does the bidding of external extensions driving Speed Tracer.
     */
    class ExternalExtensionListener {
        public void onBrowserConnected(int browserId) {
            browserConnectionMap.put(browserId, new BrowserConnectionState());
        }

        public void onBrowserDisconnected(int browserId) {
            browserConnectionMap.remove(browserId);
        }

        public void onTabMonitorStarted(int browserId, TabDescription tab, DataInstance dataInstance) {
            BrowserConnectionState browserConnection = browserConnectionMap.get(browserId);
            assert (browserConnection != null);

            TabModel tabModel = getOrCreateTabModel(browserConnection, tab.getId());
            tabModel.dataInstance = dataInstance;
            tabModel.tabDescription = tab;
            openMonitor(browserId, tab.getId(), tabModel);
        }
    }

    /**
     * Callback function for notifying the Content Script that the Monitor has
     * been opened after an AutoOpen request came in.
     */
    private static class AutoOpenSpeedTracerCallback extends SendResponse {
        @SuppressWarnings("all")
        protected AutoOpenSpeedTracerCallback() {
        }

        public final native void monitorOpened() /*-{      
                                                 this({ready: true});
                                                 }-*/;
    }

    /**
     * Simple data structure class to maintain information about the connection
     * states for a connected browser and its tabs.
     */
    private class BrowserConnectionState {
        private final HashMap<Integer, TabModel> tabMap = new HashMap<Integer, TabModel>();

        BrowserConnectionState() {
        }
    }

    /**
     * Listener that is responsible for handling clicks on the
     * MonitorTabPageAction in the chrome omnibox and will open the monitor UI if
     * it isn't already open.
     */
    private class MonitorTabClickListener implements BrowserActionEvent.Listener {
        public void onClicked(Tab tab) {
            BrowserConnectionState browserConnection = browserConnectionMap.get(CHROME_BROWSER_ID);

            int tabId = tab.getId();
            String tabUrl = tab.getUrl();

            // Verify that it is not a click on the browser action button in a
            // Monitor window. If it is, early out.
            String urlNoParams = tabUrl.split("\\?")[0];
            if (urlNoParams.equals(Chrome.getExtension().getUrl(MONITOR_RESOURCE_PATH))) {
                return;
            }

            TabModel tabModel = getOrCreateTabModel(browserConnection, tabId);

            // Update the URL if we have a tabDescription already.
            if (tabModel.tabDescription != null) {
                tabModel.tabDescription.updateUrl(tabUrl);
            } else {
                tabModel.tabDescription = TabDescription.create(tabId, tab.getTitle(), tabUrl);
            }

            // We want to either open the monitor or resume monitoring.
            if (tabModel.currentIcon == browserAction.mtIcon()) {
                if (tabModel.dataInstance == null) {
                    tabModel.dataInstance = ChromeDebuggerDataInstance.create(tabId);
                }

                if (tabModel.monitorClosed) {
                    // Open the Monitor UI.
                    openMonitor(CHROME_BROWSER_ID, tabId, tabModel);
                } else {
                    // If this is the case then restart monitoring instead of starting
                    // over.
                    tabModel.dataInstance.resumeMonitoring();
                    setBrowserActionIcon(tabId, browserAction.mtIconActive(), tabModel);
                    tabModel.channel.sendMessage(RecordingDataMessage.TYPE, RecordingDataMessage.create(true));
                    // We need to ensure that the profiling options are in synch in
                    // the browser with the current state reflected in the UI.
                    tabModel.channel.sendMessage(ResendProfilingOptions.TYPE, ResendProfilingOptions.create());
                }

                return;
            }

            // If the icon is the record button, then we should already have an open
            // monitor, and we should start monitoring.
            if (tabModel.currentIcon == browserAction.mtIconActive()) {
                tabModel.dataInstance.stopMonitoring();
                setBrowserActionIcon(tabId, browserAction.mtIcon(), tabModel);
                tabModel.channel.sendMessage(RecordingDataMessage.TYPE, RecordingDataMessage.create(false));
            }
        }
    }

    /**
     * Simple wrapper that hold on to a reference for the DataInstance and most
     * recent TabDescription object for a tab.
     */
    private static class TabModel {
        WindowChannel.Client channel = null;
        Icon currentIcon;
        DataInstance dataInstance;
        boolean monitorClosed = true;
        TabDescription tabDescription = null;
        AutoOpenSpeedTracerCallback monitorOpenedCallback = null;

        TabModel(Icon icon) {
            this.currentIcon = icon;
        }
    }

    private static final int CHROME_BROWSER_ID = 0;

    private static final int FILE_BROWSER_ID = 0x7FFFFFFF;

    private static final String MONITOR_RESOURCE_PATH = "monitor.html";

    private final MonitorTabBrowserAction browserAction = GWT.create(MonitorTabBrowserAction.class);

    private final HashMap<Integer, BrowserConnectionState> browserConnectionMap = new HashMap<Integer, BrowserConnectionState>();

    private final MonitorTabClickListener monitorTabClickListener = new MonitorTabClickListener();

    /**
     * Our entry point function. All things start here.
     */
    @Override
    public void onBackgroundPageLoad() {
        // Chrome is "connected". Insert an entry for it.
        browserConnectionMap.put(CHROME_BROWSER_ID, new BrowserConnectionState());

        GWT.create(DataLoader.class);

        initialize();

        // Register page action and browser action listeners.
        browserAction.addListener(monitorTabClickListener);

        listenForTabEvents();
        listenForContentScripts();
        listenForExternalExtensions(new ExternalExtensionListener());
    }

    /**
     * Helper function that loads data from a file. This should only get called
     * when the port name is either {@link DataLoader.DATA_LOAD} or
     * {@link DataLoader.RAW_DATA_LOAD}.
     */
    private void doDataLoad(final Port port) {
        BrowserConnectionState browserConn = browserConnectionMap.get(FILE_BROWSER_ID);
        if (browserConn == null) {
            browserConn = new BrowserConnectionState();
            browserConnectionMap.put(FILE_BROWSER_ID, browserConn);
        }

        // In situation where we open a file in a tab that was previously
        // used to open a file... we dont care. Overwrite it.
        final TabModel tabModel = new TabModel(browserAction.mtIcon());
        int tabId = port.getSender().getTab().getId();

        if (port.getName().equals(DataLoader.DATA_LOAD)) {
            final LoadFileDataInstance dataInstance = LoadFileDataInstance.create(port);
            tabModel.dataInstance = dataInstance;
            browserConn.tabMap.put(tabId, tabModel);

            // Connect the datainstance to receive data from the data_loader.
            port.getOnMessageEvent().addListener(new MessageEvent.Listener() {
                VersionedRecordConverter converter;
                boolean receivedFirstMessage;

                public void onMessage(MessageEvent.Message message) {
                    if (!receivedFirstMessage) {
                        receivedFirstMessage = true;
                        dataInstance.onTimelineProfilerStarted();
                    }
                    EventRecordMessage eventRecordMessage = message.cast();
                    if (!getVersion().equals(eventRecordMessage.getVersion())) {
                        if (converter == null) {
                            converter = VersionedRecordConverter.create(eventRecordMessage.getVersion());
                        }
                        converter.convert(dataInstance, eventRecordMessage.getEventRecord());
                        return;
                    }
                    dataInstance.onEventRecord(eventRecordMessage.getEventRecord());
                }
            });
        } else {
            // We are dealing with RAW data (untransformed inspector data) that still
            // needs conversion.
            final Proxy proxy = new Proxy(tabId) {
                @Override
                protected void connectToDataSource() {
                    // Tell the data_loader content script to start sending.
                    port.postMessage(LoadFileDataInstance.createAck());
                }
            };

            // Connect the DataInstance to receive data from the data_loader
            port.getOnMessageEvent().addListener(new MessageEvent.Listener() {
                boolean receivedFirstMessage;

                public void onMessage(MessageEvent.Message message) {
                    if (!receivedFirstMessage) {
                        receivedFirstMessage = true;
                        tabModel.dataInstance.onTimelineProfilerStarted();
                    }
                    PageEventMessage pageEventMessage = message.cast();
                    // We don't support versioning for RAW data since it would mean
                    // maintaining support for multiple Chrome versions. We assume
                    // that RAW data should always be the same format as the current
                    // Chrome build.
                    proxy.dispatchDebuggerEventRecord(pageEventMessage.getDebuggerRecord());
                }
            });

            tabModel.dataInstance = ChromeDebuggerDataInstance.create(proxy);
            browserConn.tabMap.put(tabId, tabModel);
        }

        tabModel.tabDescription = TabDescription.create(tabId, port.getSender().getTab().getTitle(),
                port.getSender().getTab().getUrl());
        openMonitor(FILE_BROWSER_ID, tabId, tabModel);
    }

    /**
     * Returns a TabModel for a specified tab in a BrowserConnectionState object,
     * or creates one if one is not found. It also initialized the TabModel with
     * the specified DataInstance.
     */
    private TabModel getOrCreateTabModel(BrowserConnectionState browserConnection, int tabId) {
        TabModel tabModel = browserConnection.tabMap.get(tabId);
        if (tabModel == null) {
            tabModel = new TabModel(browserAction.mtIcon());
            browserConnection.tabMap.put(tabId, tabModel);
        }
        return tabModel;
    }

    /**
     * Temporary method until we figure out what updates to give to topspin to
     * make it document aware.
     * 
     * TODO(jaimeyap): Make Topspin document aware.
     */
    private native WindowExt getWindow() /*-{
                                         return window;
                                         }-*/;

    /**
     * Injects the plugin and calls Load(). Also starts our
     * {@link WindowChannel.Server} for communicating and initializing instances
     * of our Monitor UI.
     */
    private void initialize() {
        Server.listen(getWindow(), Monitor.CHANNEL_NAME, new ServerListener() {
            public void onClientChannelRequested(Request request) {
                request.accept(new WindowChannel.Listener() {
                    public void onChannelClosed(Client channel) {
                    }

                    public void onChannelConnected(Client channel) {
                    }

                    public void onMessage(final Client channel, int type, WindowChannel.Message data) {
                        switch (type) {
                        case RequestInitializationMessage.TYPE:
                            doRequestInitialization(channel, data);
                            break;
                        case RecordingDataMessage.TYPE:
                            doRecordingData(channel, data);
                            break;
                        case ResetBaseTimeMessage.TYPE:
                            doResetBaseTime(data);
                            break;
                        default:
                            assert false : "Unhandled Message" + type;
                        }
                    }

                    private void doRecordingData(Client channel, WindowChannel.Message data) {
                        final RecordingDataMessage recordingDataMessage = data.cast();
                        int tabId = recordingDataMessage.getTabId();
                        int browserId = recordingDataMessage.getBrowserId();
                        TabModel tabModel = browserConnectionMap.get(browserId).tabMap.get(tabId);
                        Icon pageActionIcon;
                        if (recordingDataMessage.isRecording()) {
                            tabModel.dataInstance.resumeMonitoring();
                            pageActionIcon = browserAction.mtIconActive();
                            // We need to ensure that the profiling options are in synch in
                            // the browser with the current state reflected in the UI.
                            channel.sendMessage(ResendProfilingOptions.TYPE, ResendProfilingOptions.create());
                        } else {
                            tabModel.dataInstance.stopMonitoring();
                            pageActionIcon = browserAction.mtIcon();
                        }
                        if (browserId == CHROME_BROWSER_ID) {
                            // Update the page action icon.
                            setBrowserActionIcon(tabId, pageActionIcon, tabModel);
                        }
                    }

                    /**
                     * Called by the monitor's onModuleLoad when it is requesting
                     * initialization from the background page. It is essentially asking
                     * for us to give it a DataInstance and TabDescription.
                     */
                    private void doRequestInitialization(final Client channel, WindowChannel.Message data) {
                        final RequestInitializationMessage request = data.cast();
                        final int tabId = request.getTabId();
                        final int browserId = request.getBrowserId();
                        final BrowserConnectionState browserConnection = browserConnectionMap.get(browserId);
                        assert (browserConnection != null);

                        // Extract the relevant DataInstance and TabDescription
                        // that we have stashed.
                        final TabModel tabModel = browserConnection.tabMap.get(tabId);
                        // Store a reference to the channel in case we want to
                        // send messages later.
                        tabModel.channel = channel;

                        // We are talking to another browser. Go ahead and initialize
                        // the window since the interaction was started externally.
                        assert (tabModel.tabDescription != null);
                        assert (tabModel.dataInstance != null);

                        final InitializeMonitorMessage initializeMessage = InitializeMonitorMessage
                                .create(tabModel.tabDescription, tabModel.dataInstance, getVersion());
                        channel.sendMessage(InitializeMonitorMessage.TYPE, initializeMessage);

                        // If we are talking to chrome, update the pageAction Icon and
                        // wait for a second click to initialize the monitor.
                        if (browserId == CHROME_BROWSER_ID) {
                            // We now change the page action icon. This signals that the
                            // next time it is clicked, we should initialize.
                            setBrowserActionIcon(tabId, browserAction.mtIconActive(), tabModel);
                        }

                        // Hook unload so we can close down and keep track of monitor
                        // state.
                        request.getMonitorWindow().addUnloadListener(new EventListener() {
                            public void handleEvent(Event event) {
                                TabModel tabModel = browserConnection.tabMap.get(tabId);
                                channel.close();
                                tabModel.channel = null;
                                tabModel.monitorClosed = true;
                                tabModel.dataInstance.unload();
                                tabModel.dataInstance = null;
                                setBrowserActionIcon(tabId, browserAction.mtIcon(), tabModel);
                                browserConnection.tabMap.remove(tabModel);
                            }
                        });

                        if (tabModel.monitorOpenedCallback != null) {
                            tabModel.monitorOpenedCallback.monitorOpened();
                            tabModel.monitorOpenedCallback = null;
                        }
                    }

                    private void doResetBaseTime(WindowChannel.Message data) {
                        final ResetBaseTimeMessage request = data.cast();
                        final int tabId = request.getTabId();
                        final int browserId = request.getBrowserId();
                        final BrowserConnectionState browserConnection = browserConnectionMap.get(browserId);
                        final TabModel tabModel = browserConnection.tabMap.get(tabId);
                        DataInstance dataInstance = tabModel.dataInstance;
                        dataInstance.setBaseTime(-1);
                    }

                });
            }

        });
    }

    private void listenForContentScripts() {
        // A content script connects to us when we want to load data.
        Chrome.getExtension().getOnConnectEvent().addListener(new ConnectEvent.Listener() {
            public void onConnect(final Port port) {
                String portName = port.getName();
                if (portName.equals(DataLoader.DATA_LOAD) || portName.equals(DataLoader.RAW_DATA_LOAD)) {
                    // We are loading data.
                    doDataLoad(port);
                }
            }
        });

        // A content script can message us if it detects that we should auto open
        // Speed Tracer for a trampoline file.
        Chrome.getExtension().getOnRequestEvent().addListener(new RequestEvent.Listener() {
            public void onRequest(JavaScriptObject request, Sender sender, SendResponse sendResponse) {
                if (DataBag.getBooleanProperty(request, "autoOpen")) {
                    // Open Speed Tracer.
                    Tab tab = sender.getTab();
                    monitorTabClickListener.onClicked(tab);
                    // The Monitor coming alive and calling back should be
                    // asynchronous. We should be able to stick in the SendResponse
                    // callback in before the Monitor calls back, and then notify the
                    // content script after we know the monitor is opened and ready.
                    BrowserConnectionState browserConnection = browserConnectionMap.get(CHROME_BROWSER_ID);
                    browserConnection.tabMap.get(tab.getId()).monitorOpenedCallback = sendResponse.cast();
                }
            }
        });
    }

    private void listenForExternalExtensions(final ExternalExtensionListener exListener) {
        // External extensions can also be used as data sources. Hook this up.
        Chrome.getExtension().getOnRequestExternalEvent().addListener(new RequestExternalEvent.Listener() {
            public void onRequestExternal(JavaScriptObject request, Sender sender, SendResponse sendResponse) {
                // Ensure the extension attempting to connect is not blacklisted.
                if (!ExternalExtensionDataInstance.isBlackListed(sender.getId())) {
                    final ConnectRequest connectRequest = request.cast();
                    final int browserId = connectRequest.getBrowserId();

                    BrowserConnectionState connection = browserConnectionMap.get(browserId);

                    if (connection == null) {
                        // If this is the first opened connection for this browser type,
                        // then we provision an entry for it in the browser map.
                        exListener.onBrowserConnected(browserId);
                    }

                    final int tabId = connectRequest.getTabId();
                    final String portName = ExternalExtensionDataInstance.SPEED_TRACER_EXTERNAL_PORT + browserId
                            + "-" + tabId;

                    // So we will now begin listening for connections on a dedicated
                    // port name for this browser/tab combo.
                    Chrome.getExtension().getOnConnectExternalEvent()
                            .addListener(new ConnectExternalEvent.Listener() {
                                public void onConnectExternal(Port port) {
                                    if (portName.equals(port.getName())) {
                                        // Provision a DataInstance and a TabDescription.
                                        DataInstance dataInstance = ExternalExtensionDataInstance.create(port);
                                        TabDescription tabDescription = TabDescription.create(tabId,
                                                connectRequest.getTitle(), connectRequest.getUrl());

                                        // Now remember the DataInstance and TabDescription, and
                                        // open a Monitor.
                                        exListener.onTabMonitorStarted(browserId, tabDescription, dataInstance);
                                    }
                                }
                            });

                    // Send a response that tells the external extension what port
                    // name to connect to.
                    sendResponse.invoke(ExternalExtensionDataInstance.createResponse(portName));
                }
            }
        });
    }

    private void listenForTabEvents() {
        // We need to keep the browser action icon consistent.
        Tabs.getOnUpdatedEvent().addListener(new TabUpdatedEvent.Listener() {
            public void onTabUpdated(int tabId, ChangeInfo changeInfo, Tab tab) {
                if (changeInfo.getStatus().equals(ChangeInfo.STATUS_LOADING)) {
                    TabModel tabModel = browserConnectionMap.get(CHROME_BROWSER_ID).tabMap.get(tabId);
                    if (tabModel != null) {
                        // We want the icon to remain what it was before the page
                        // transition.
                        setBrowserActionIcon(tabId, tabModel.currentIcon, tabModel);
                    }
                }
            }
        });
    }

    /**
     * Opens the monitor UI for a given tab, iff it is not already open.
     */
    private void openMonitor(final int browserId, final int tabId, final TabModel tabModel) {
        assert (tabModel != null);

        Windows.create(MONITOR_RESOURCE_PATH + "?tabId=" + tabId + "&browserId=" + Integer.toString(browserId), 0,
                0, 850, 700, new OnWindowCallback() {
                    public void onWindow(Window window) {
                        tabModel.monitorClosed = false;
                        // The Tab containing the Monitor UI should not have a valid browser
                        // action button.
                        Tabs.getSelected(window.getId(), new OnTabCallback() {
                            public void onTab(Tab tab) {
                                setBrowserActionIcon(tab.getId(), browserAction.mtIconDisabled(), null);
                            }
                        });
                    }
                });
    }

    private void setBrowserActionIcon(int tabId, Icon icon, TabModel tabModel) {
        String title = "";
        if (icon == browserAction.mtIcon()) {
            title = "Monitor Tab";
        }
        if (icon == browserAction.mtIconActive()) {
            title = "Stop Monitoring";
        }

        browserAction.setIcon(tabId, icon);
        browserAction.setTitle(tabId, title);
        if (tabModel != null) {
            tabModel.currentIcon = icon;
        }
    }
}