Bug 1416201 - HTTP request stack trace should be lazy loaded;r=rickychien draft
authorJan Odvarko <odvarko@gmail.com>
Thu, 23 Nov 2017 13:35:04 +0100
changeset 702617 400e5d48848b087f8965652df6b9c37686fb4b40
parent 702588 b6bed1b710c3e22cab49f22f1b5f44d80286bcb9
child 702618 7284ef615b184ec80ce724fab2f62bdaf9173518
push id90556
push userjodvarko@mozilla.com
push dateThu, 23 Nov 2017 13:15:34 +0000
reviewersrickychien
bugs1416201
milestone59.0a1
Bug 1416201 - HTTP request stack trace should be lazy loaded;r=rickychien MozReview-Commit-ID: 5SWLLcNqORz
devtools/client/netmonitor/src/components/StackTracePanel.js
devtools/client/netmonitor/src/components/TabboxPanel.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/constants.js
devtools/server/actors/webconsole.js
devtools/shared/webconsole/client.js
--- a/devtools/client/netmonitor/src/components/StackTracePanel.js
+++ b/devtools/client/netmonitor/src/components/StackTracePanel.js
@@ -1,51 +1,91 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { createFactory } = require("devtools/client/shared/vendor/react");
+const { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const { div } = dom;
 
 // Components
 const StackTrace = createFactory(require("devtools/client/shared/components/StackTrace"));
 
 /**
  * This component represents a side panel responsible for
  * rendering stack-trace info for selected request.
  */
-function StackTracePanel({
-  connector,
-  openLink,
-  request,
-  sourceMapService,
-}) {
-  let { stacktrace } = request.cause;
+class StackTracePanel extends Component {
+  static get propTypes() {
+    return {
+      connector: PropTypes.object.isRequired,
+      request: PropTypes.object.isRequired,
+      sourceMapService: PropTypes.object,
+      openLink: PropTypes.func,
+    };
+  }
+
+  /**
+   * `componentDidMount` is called when opening the StackTracePanel
+   * for the first time
+   */
+  componentDidMount() {
+    this.maybeFetchStackTrace(this.props);
+  }
+
+  /**
+   * `componentWillReceiveProps` is the only method called when
+   * switching between two requests while this panel is displayed.
+   */
+  componentWillReceiveProps(nextProps) {
+    this.maybeFetchStackTrace(nextProps);
+  }
 
-  return (
-    div({ className: "panel-container" },
-      StackTrace({
-        stacktrace,
-        onViewSourceInDebugger: ({ url, line }) => {
-          return connector.viewSourceInDebugger(url, line);
-        },
-        sourceMapService,
-        openLink,
-      }),
-    )
-  );
+  /**
+   * When switching to another request, lazily fetch stack-trace
+   * from the backend. This Panel will first be empty and then
+   * display the content.
+   */
+  maybeFetchStackTrace(props) {
+    // Fetch stack trace only if it's available and not yet
+    // on the client.
+    if (!props.request.stacktrace &&
+      props.request.cause.stacktraceAvailable) {
+      // This method will set `props.request.stacktrace`
+      // asynchronously and force another render.
+      props.connector.requestData(props.request.id, "stackTrace");
+    }
+  }
+
+  // Rendering
+
+  render() {
+    let {
+      connector,
+      openLink,
+      request,
+      sourceMapService,
+    } = this.props;
+
+    let {
+      stacktrace = []
+    } = request;
+
+    return (
+      div({ className: "panel-container" },
+        StackTrace({
+          stacktrace,
+          onViewSourceInDebugger: ({ url, line }) => {
+            return connector.viewSourceInDebugger(url, line);
+          },
+          sourceMapService,
+          openLink,
+        }),
+      )
+    );
+  }
 }
 
-StackTracePanel.displayName = "StackTracePanel";
-
-StackTracePanel.propTypes = {
-  connector: PropTypes.object.isRequired,
-  request: PropTypes.object.isRequired,
-  sourceMapService: PropTypes.object,
-  openLink: PropTypes.func,
-};
-
 module.exports = StackTracePanel;
--- a/devtools/client/netmonitor/src/components/TabboxPanel.js
+++ b/devtools/client/netmonitor/src/components/TabboxPanel.js
@@ -79,17 +79,17 @@ function TabboxPanel({
         ResponsePanel({ request, openLink, connector }),
       ),
       TabPanel({
         id: PANELS.TIMINGS,
         title: TIMINGS_TITLE,
       },
         TimingsPanel({ request }),
       ),
-      request.cause && request.cause.stacktrace && request.cause.stacktrace.length > 0 &&
+      request.cause && request.cause.stacktraceAvailable &&
       TabPanel({
         id: PANELS.STACK_TRACE,
         title: STACK_TRACE_TITLE,
       },
         StackTracePanel({ connector, openLink, request, sourceMapService }),
       ),
       request.securityState && request.securityState !== "insecure" &&
       TabPanel({
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -61,16 +61,22 @@ class FirefoxDataProvider {
     if (this.actions.addRequest) {
       await this.actions.addRequest(id, {
         // Convert the received date/time string to a unix timestamp.
         startedMillis: Date.parse(startedDateTime),
         method,
         url,
         isXHR,
         cause,
+
+        // Compatibility code to support Firefox 58 and earlier that always
+        // send stack-trace immediately on networkEvent message.
+        // FF59+ supports fetching the traces lazily via requestData.
+        stacktrace: cause.stacktrace,
+
         fromCache,
         fromServiceWorker},
         true,
       );
     }
 
     emit(EVENTS.REQUEST_ADDED, id);
   }
@@ -656,16 +662,29 @@ class FirefoxDataProvider {
    */
   onEventTimings(response) {
     return this.updateRequest(response.from, {
       eventTimings: response
     }).then(() => {
       emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
     });
   }
+
+  /**
+   * Handles information received for a "stackTrace" packet.
+   *
+   * @param {object} response the message received from the server.
+   */
+  onStackTrace(response) {
+    return this.updateRequest(response.from, {
+      stacktrace: response.stacktrace
+    }).then(() => {
+      emit(EVENTS.RECEIVED_EVENT_STACKTRACE, response.from);
+    });
+  }
 }
 
 /**
  * Guard 'emit' to avoid exception in non-window environment.
  */
 function emit(type, data) {
   if (typeof window != "undefined") {
     window.emit(type, data);
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -88,16 +88,19 @@ const EVENTS = {
   UPDATING_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdating:EventTimings",
   RECEIVED_EVENT_TIMINGS: "NetMonitor:NetworkEventUpdated:EventTimings",
 
   // When response content begins, updates and finishes receiving.
   STARTED_RECEIVING_RESPONSE: "NetMonitor:NetworkEventUpdating:ResponseStart",
   UPDATING_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdating:ResponseContent",
   RECEIVED_RESPONSE_CONTENT: "NetMonitor:NetworkEventUpdated:ResponseContent",
 
+  // When stack-trace finishes receiving.
+  RECEIVED_EVENT_STACKTRACE: "NetMonitor:NetworkEventUpdated:StackTrace",
+
   // Fired once the connection is established
   CONNECTED: "connected",
 
   // When request payload (HTTP details data) are fetched from the backend.
   PAYLOAD_READY: "NetMonitor:PayloadReady",
 };
 
 const UPDATE_PROPS = [
@@ -121,16 +124,17 @@ const UPDATE_PROPS = [
   "requestHeadersFromUploadStream",
   "requestCookies",
   "requestPostData",
   "responseHeaders",
   "responseCookies",
   "responseContent",
   "responseContentAvailable",
   "formDataSections",
+  "stacktrace",
 ];
 
 const PANELS = {
   COOKIES: "cookies",
   HEADERS: "headers",
   PARAMS: "params",
   RESPONSE: "response",
   SECURITY: "security",
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1931,16 +1931,17 @@ function NetworkEventActor(webConsoleAct
 
   this._response = {
     headers: [],
     cookies: [],
     content: {},
   };
 
   this._timings = {};
+  this._stackTrace = {};
 
   // Keep track of LongStringActors owned by this NetworkEventActor.
   this._longStringActors = new Set();
 }
 
 NetworkEventActor.prototype =
 {
   _request: null,
@@ -2003,16 +2004,24 @@ NetworkEventActor.prototype =
    */
   init: function (networkEvent) {
     this._startedDateTime = networkEvent.startedDateTime;
     this._isXHR = networkEvent.isXHR;
     this._cause = networkEvent.cause;
     this._fromCache = networkEvent.fromCache;
     this._fromServiceWorker = networkEvent.fromServiceWorker;
 
+    // Stack trace info isn't sent automatically. The client
+    // needs to request it explicitly using getStackTrace
+    // packet.
+    this._stackTrace = networkEvent.cause.stacktrace;
+    delete networkEvent.cause.stacktrace;
+    networkEvent.cause.stacktraceAvailable =
+      !!(this._stackTrace && this._stackTrace.length);
+
     for (let prop of ["method", "url", "httpVersion", "headersSize"]) {
       this._request[prop] = networkEvent[prop];
     }
 
     this._discardRequestBody = networkEvent.discardRequestBody;
     this._discardResponseBody = networkEvent.discardResponseBody;
     this._private = networkEvent.private;
   },
@@ -2124,16 +2133,29 @@ NetworkEventActor.prototype =
     return {
       from: this.actorID,
       timings: this._timings,
       totalTime: this._totalTime,
       offsets: this._offsets
     };
   },
 
+  /**
+   * The "getStackTrace" packet type handler.
+   *
+   * @return object
+   *         The response packet - stack trace.
+   */
+  onGetStackTrace: function () {
+    return {
+      from: this.actorID,
+      stacktrace: this._stackTrace,
+    };
+  },
+
   /** ****************************************************************
    * Listeners for new network event data coming from NetworkMonitor.
    ******************************************************************/
 
   /**
    * Add network request headers.
    *
    * @param array headers
@@ -2372,9 +2394,10 @@ NetworkEventActor.prototype.requestTypes
   "getRequestHeaders": NetworkEventActor.prototype.onGetRequestHeaders,
   "getRequestCookies": NetworkEventActor.prototype.onGetRequestCookies,
   "getRequestPostData": NetworkEventActor.prototype.onGetRequestPostData,
   "getResponseHeaders": NetworkEventActor.prototype.onGetResponseHeaders,
   "getResponseCookies": NetworkEventActor.prototype.onGetResponseCookies,
   "getResponseContent": NetworkEventActor.prototype.onGetResponseContent,
   "getEventTimings": NetworkEventActor.prototype.onGetEventTimings,
   "getSecurityInfo": NetworkEventActor.prototype.onGetSecurityInfo,
+  "getStackTrace": NetworkEventActor.prototype.onGetStackTrace,
 };
--- a/devtools/shared/webconsole/client.js
+++ b/devtools/shared/webconsole/client.js
@@ -562,16 +562,34 @@ WebConsoleClient.prototype = {
     let packet = {
       to: actor,
       type: "getSecurityInfo",
     };
     return this._client.request(packet, onResponse);
   },
 
   /**
+   * Retrieve the stack-trace information for the given NetworkEventActor.
+   *
+   * @param string actor
+   *        The NetworkEventActor ID.
+   * @param function onResponse
+   *        The function invoked when the stack-trace is received.
+   * @return request
+   *         Request object that implements both Promise and EventEmitter interfaces
+   */
+  getStackTrace: function (actor, onResponse) {
+    let packet = {
+      to: actor,
+      type: "getStackTrace",
+    };
+    return this._client.request(packet, onResponse);
+  },
+
+  /**
    * Send a HTTP request with the given data.
    *
    * @param string data
    *        The details of the HTTP request.
    * @param function onResponse
    *        The function invoked when the response is received.
    * @return request
    *         Request object that implements both Promise and EventEmitter interfaces