Bug 1404917 - Fetch response content only on-demand. r=Honza draft
authorAlexandre Poirot <poirot.alex@gmail.com>
Thu, 05 Oct 2017 17:49:07 +0200
changeset 675597 a37ae0bc0fb58fccf8b926f73373b89b5064e841
parent 675596 e4f84da9e2db9e3dfda3605419f9b208af01edde
child 675598 bd62a4afa17f381d7dbe4625d9698639e15d6b79
child 675604 c7a59ff43a02fe442214471c2ff5e1cc4895c5f6
push id83180
push userbmo:poirot.alex@gmail.com
push dateThu, 05 Oct 2017 15:54:09 +0000
reviewersHonza
bugs1404917
milestone58.0a1
Bug 1404917 - Fetch response content only on-demand. r=Honza MozReview-Commit-ID: CtpJ8PKsCsm
devtools/client/netmonitor/src/components/response-panel.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/connector/index.js
devtools/client/netmonitor/src/request-list-context-menu.js
--- a/devtools/client/netmonitor/src/components/response-panel.js
+++ b/devtools/client/netmonitor/src/components/response-panel.js
@@ -8,16 +8,18 @@ const {
   createClass,
   createFactory,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 const { L10N } = require("../utils/l10n");
 const { formDataURI, getUrlBaseName } = require("../utils/request-utils");
 
+const { getResponseContent } = require("../connector/index");
+
 // Components
 const PropertiesView = createFactory(require("./properties-view"));
 
 const { div, img } = DOM;
 const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
 const JSON_FILTER_TEXT = L10N.getStr("jsonFilterText");
 const RESPONSE_IMG_NAME = L10N.getStr("netmonitor.response.name");
 const RESPONSE_IMG_DIMENSIONS = L10N.getStr("netmonitor.response.dimensions");
@@ -105,16 +107,30 @@ const ResponsePanel = createClass({
       }
 
       return result;
     }
 
     return null;
   },
 
+  // componentWillReceiveProps is the only method called when switching to the
+  // ResponsePanel *and* when being on it and switching between two requests.
+  componentWillReceiveProps(nextProps) {
+    // When switching to another request lazily fetch response content
+    // from the backend. The Response Panel will first be empty and then
+    // display the content.
+    if (!nextProps.request.responseContent ||
+        !nextProps.request.responseContent.content) {
+      // This method will set `props.request.responseContent.content`
+      // asynchronously and introduce another render.
+      getResponseContent(nextProps.request.id);
+    }
+  },
+
   render() {
     let { openLink, request } = this.props;
     let { responseContent, url } = request;
 
     if (!responseContent || typeof responseContent.content.text !== "string") {
       return null;
     }
 
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -288,11 +288,15 @@ class FirefoxConnector {
    * @param {string} sourceURL source url
    * @param {number} sourceLine source line number
    */
   viewSourceInDebugger(sourceURL, sourceLine) {
     if (this.toolbox) {
       this.toolbox.viewSourceInDebugger(sourceURL, sourceLine);
     }
   }
+
+  getResponseContent(request) {
+    return this.dataProvider.getResponseContent(request);
+  }
 }
 
 module.exports = new FirefoxConnector();
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -16,16 +16,18 @@ const { fetchHeaders, formDataURI } = re
 class FirefoxDataProvider {
   constructor({webConsoleClient, actions}) {
     // Options
     this.webConsoleClient = webConsoleClient;
     this.actions = actions;
 
     // Internal properties
     this.payloadQueue = [];
+    // Map of [request id => Promise], used by getResponseContent
+    this.pendingResponseContentRequests = new Map();
 
     // Public methods
     this.addRequest = this.addRequest.bind(this);
     this.updateRequest = this.updateRequest.bind(this);
 
     // Internals
     this.fetchImage = this.fetchImage.bind(this);
     this.fetchRequestHeaders = this.fetchRequestHeaders.bind(this);
@@ -43,17 +45,16 @@ class FirefoxDataProvider {
     this.onNetworkEvent = this.onNetworkEvent.bind(this);
     this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
     this.onRequestHeaders = this.onRequestHeaders.bind(this);
     this.onRequestCookies = this.onRequestCookies.bind(this);
     this.onRequestPostData = this.onRequestPostData.bind(this);
     this.onSecurityInfo = this.onSecurityInfo.bind(this);
     this.onResponseHeaders = this.onResponseHeaders.bind(this);
     this.onResponseCookies = this.onResponseCookies.bind(this);
-    this.onResponseContent = this.onResponseContent.bind(this);
     this.onEventTimings = this.onEventTimings.bind(this);
   }
 
   /**
    * Add a new network request to application state.
    *
    * @param {string} id request id
    * @param {object} data data payload will be added to application state
@@ -128,18 +129,22 @@ class FirefoxDataProvider {
       postDataObj,
       requestCookiesObj,
       responseCookiesObj
     );
 
     this.pushPayloadToQueue(id, payload);
 
     if (this.actions.updateRequest && this.isQueuePayloadReady(id)) {
-      await this.actions.updateRequest(id, this.getPayloadFromQueue(id).payload, true);
+      let payload = this.getPayloadFromQueue(id).payload;
+      await this.actions.updateRequest(id, payload, true);
+      return payload;
     }
+
+    return payload;
   }
 
   async fetchImage(mimeType, responseContent) {
     let payload = {};
     if (mimeType && responseContent && responseContent.content) {
       let { encoding, text } = responseContent.content;
       let response = await this.getLongString(text);
 
@@ -301,16 +306,49 @@ class FirefoxDataProvider {
    *         A promise that is resolved when the full string contents
    *         are available, or rejected if something goes wrong.
    */
   getLongString(stringGrip) {
     return this.webConsoleClient.getString(stringGrip);
   }
 
   /**
+   * Fetches the response content of a given request.
+   *
+   * @param {string} id request id
+   */
+  async getResponseContent(id) {
+    // Return the response if that's already fetched
+    let queuedPayload = this.getPayloadFromQueue(id);
+    if (queuedPayload && queuedPayload.payload.responseContent) {
+      return queuedPayload.payload.responseContent;
+    }
+    // Prevent fetching the response more than once at a time
+    // in case this method is called multiple times in a raw.
+    if (this.pendingResponseContentRequests.has(id)) {
+      return this.pendingResponseContentRequests.get(id);
+    }
+    let promise = this.webConsoleClient.getResponseContent(id)
+      .then(async response => {
+        // We have to pass mimeType in order to ensure calling fetchImage in updateRequest
+        // and have the LongString in `response.content.text` converted to text.
+        let payload = await this.updateRequest(id, {
+          responseContent: response,
+          mimeType: response.content.mimeType
+        });
+        this.pendingResponseContentRequests.delete(id);
+        emit(EVENTS.RECEIVED_RESPONSE_CONTENT, id);
+        return payload.responseContent;
+      });
+    this.pendingResponseContentRequests.set(id, promise);
+    emit(EVENTS.UPDATING_RESPONSE_CONTENT, id);
+    return promise;
+  }
+
+  /**
    * The "networkEvent" message type handler.
    *
    * @param {string} type message type
    * @param {object} networkInfo network request information
    */
   onNetworkEvent(type, networkInfo) {
     let {
       actor,
@@ -385,23 +423,21 @@ class FirefoxDataProvider {
           status: networkInfo.response.status,
           statusText: networkInfo.response.statusText,
           headersSize: networkInfo.response.headersSize
         }).then(() => {
           emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
         });
         break;
       case "responseContent":
-        this.webConsoleClient.getResponseContent(actor,
-          this.onResponseContent.bind(this, {
-            contentSize: networkInfo.response.bodySize,
-            transferredSize: networkInfo.response.transferredSize,
-            mimeType: networkInfo.response.content.mimeType
-          }));
-        emit(EVENTS.UPDATING_RESPONSE_CONTENT, actor);
+        this.updateRequest(actor, {
+          contentSize: networkInfo.response.bodySize,
+          transferredSize: networkInfo.response.transferredSize,
+          mimeType: networkInfo.response.content.mimeType
+        });
         break;
       case "eventTimings":
         this.updateRequest(actor, { totalTime: networkInfo.totalTime })
           .then(() => {
             this.webConsoleClient.getEventTimings(actor, this.onEventTimings);
             emit(EVENTS.UPDATING_EVENT_TIMINGS, actor);
           });
         break;
@@ -482,29 +518,16 @@ class FirefoxDataProvider {
     this.updateRequest(response.from, {
       responseCookies: response
     }).then(() => {
       emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
     });
   }
 
   /**
-   * Handles additional information received for a "responseContent" packet.
-   *
-   * @param {object} data the message received from the server event.
-   * @param {object} response the message received from the server.
-   */
-  onResponseContent(data, response) {
-    let payload = Object.assign({ responseContent: response }, data);
-    this.updateRequest(response.from, payload).then(() => {
-      emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
-    });
-  }
-
-  /**
    * Handles additional information received for a "eventTimings" packet.
    *
    * @param {object} response the message received from the server.
    */
   onEventTimings(response) {
     this.updateRequest(response.from, {
       eventTimings: response
     }).then(() => {
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -64,22 +64,27 @@ function setPreferences() {
 function triggerActivity() {
   return connector.triggerActivity(...arguments);
 }
 
 function viewSourceInDebugger() {
   return connector.viewSourceInDebugger(...arguments);
 }
 
+function getResponseContent() {
+  return connector.getResponseContent(...arguments);
+}
+
 module.exports = {
   onConnect,
   onChromeConnect,
   onFirefoxConnect,
   onDisconnect,
   getLongString,
   getNetworkRequest,
   getTabTarget,
   inspectRequest,
   sendHTTPRequest,
   setPreferences,
   triggerActivity,
   viewSourceInDebugger,
+  getResponseContent,
 };
--- a/devtools/client/netmonitor/src/request-list-context-menu.js
+++ b/devtools/client/netmonitor/src/request-list-context-menu.js
@@ -8,16 +8,17 @@ const Services = require("Services");
 const { Curl } = require("devtools/client/shared/curl");
 const { gDevTools } = require("devtools/client/framework/devtools");
 const { saveAs } = require("devtools/client/shared/file-saver");
 const { copyString } = require("devtools/shared/platform/clipboard");
 const { HarExporter } = require("./har/har-exporter");
 const {
   getLongString,
   getTabTarget,
+  getResponseContent,
 } = require("./connector/index");
 const {
   getSelectedRequest,
   getSortedRequests,
 } = require("./selectors/index");
 const { L10N } = require("./utils/l10n");
 const { showMenu } = require("devtools/client/netmonitor/src/utils/menu");
 const {
@@ -109,20 +110,17 @@ RequestListContextMenu.prototype = {
       visible: !!(selectedRequest && selectedRequest.responseHeaders),
       click: () => this.copyResponseHeaders(),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-response",
       label: L10N.getStr("netmonitor.context.copyResponse"),
       accesskey: L10N.getStr("netmonitor.context.copyResponse.accesskey"),
-      visible: !!(selectedRequest &&
-               selectedRequest.responseContent &&
-               selectedRequest.responseContent.content.text &&
-               selectedRequest.responseContent.content.text.length !== 0),
+      visible: !!selectedRequest,
       click: () => this.copyResponse(),
     });
 
     copySubmenu.push({
       id: "request-list-context-copy-image-as-data-uri",
       label: L10N.getStr("netmonitor.context.copyImageAsDataUri"),
       accesskey: L10N.getStr("netmonitor.context.copyImageAsDataUri.accesskey"),
       visible: !!(selectedRequest &&
@@ -333,25 +331,27 @@ RequestListContextMenu.prototype = {
       rawHeaders = rawHeaders.replace(/\r/g, "");
     }
     copyString(rawHeaders);
   },
 
   /**
    * Copy image as data uri.
    */
-  copyImageAsDataUri() {
+  async copyImageAsDataUri() {
+    await getResponseContent(this.selectedRequest.id);
     copyString(this.selectedRequest.responseContentDataUri);
   },
 
   /**
    * Save image as.
    */
-  saveImageAs() {
-    let { encoding, text } = this.selectedRequest.responseContent.content;
+  async saveImageAs() {
+    let responseContent = await getResponseContent(this.selectedRequest.id);
+    let { encoding, text } = responseContent.content;
     let fileName = getUrlBaseName(this.selectedRequest.url);
     let data;
     if (encoding === "base64") {
       let decoded = atob(text);
       data = new Uint8Array(decoded.length);
       for (let i = 0; i < decoded.length; ++i) {
         data[i] = decoded.charCodeAt(i);
       }
@@ -359,18 +359,19 @@ RequestListContextMenu.prototype = {
       data = text;
     }
     saveAs(new Blob([data]), fileName, document);
   },
 
   /**
    * Copy response data as a string.
    */
-  copyResponse() {
-    copyString(this.selectedRequest.responseContent.content.text);
+  async copyResponse() {
+    let responseContent = await getResponseContent(this.selectedRequest.id);
+    copyString(responseContent.content.text);
   },
 
   /**
    * Copy HAR from the network panel content to the clipboard.
    */
   copyAllAsHar() {
     return HarExporter.copy(this.getDefaultHarOptions());
   },