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