Package com.google.speedtracer.client.model

Source Code of com.google.speedtracer.client.model.ChromeDebuggerDataInstance$Proxy$TypeTranslationVisitor

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

Related Classes of com.google.speedtracer.client.model.ChromeDebuggerDataInstance$Proxy$TypeTranslationVisitor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.