Bug 1425273 - eventTimings should be loaded lazily r?ochameau draft
authorRicky Chien <ricky060709@gmail.com>
Thu, 14 Dec 2017 11:41:17 -0600
changeset 720829 416322a1417506b49fa305b7bb5044af63a49649
parent 720581 8e33bdf820dcc2ecd6f326eb83c39fc5e4769dcf
child 720830 9535514fc775e42e31683e8dcd3a05a0e06918f8
push id95660
push userbmo:rchien@mozilla.com
push dateTue, 16 Jan 2018 10:30:53 +0000
reviewersochameau
bugs1425273
milestone59.0a1
Bug 1425273 - eventTimings should be loaded lazily r?ochameau MozReview-Commit-ID: DWITxNNGKHj
devtools/client/netmonitor/src/components/RequestListColumnWaterfall.js
devtools/client/netmonitor/src/components/RequestListHeader.js
devtools/client/netmonitor/src/components/RequestListItem.js
devtools/client/netmonitor/src/components/StatisticsPanel.js
devtools/client/netmonitor/src/components/TabboxPanel.js
devtools/client/netmonitor/src/components/TimingsPanel.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/constants.js
devtools/client/netmonitor/src/har/har-builder.js
devtools/client/netmonitor/src/selectors/ui.js
--- a/devtools/client/netmonitor/src/components/RequestListColumnWaterfall.js
+++ b/devtools/client/netmonitor/src/components/RequestListColumnWaterfall.js
@@ -2,130 +2,148 @@
  * 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 { Component } = 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 { L10N } = require("../utils/l10n");
-const { propertiesEqual } = require("../utils/request-utils");
+const {
+  fetchNetworkUpdatePacket,
+  propertiesEqual,
+} = require("../utils/request-utils");
 
 const { div } = dom;
 
 const UPDATED_WATERFALL_PROPS = [
   "eventTimings",
   "fromCache",
   "fromServiceWorker",
   "totalTime",
 ];
 // List of properties of the timing info we want to create boxes for
 const TIMING_KEYS = ["blocked", "dns", "connect", "ssl", "send", "wait", "receive"];
 
 class RequestListColumnWaterfall extends Component {
   static get propTypes() {
     return {
+      connector: PropTypes.object.isRequired,
       firstRequestStartedMillis: PropTypes.number.isRequired,
       item: PropTypes.object.isRequired,
       onWaterfallMouseDown: PropTypes.func.isRequired,
     };
   }
 
+  componentDidMount() {
+    let { connector, item } = this.props;
+    fetchNetworkUpdatePacket(connector.requestData, item, ["eventTimings"]);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    let { connector, item } = nextProps;
+    fetchNetworkUpdatePacket(connector.requestData, item, ["eventTimings"]);
+  }
+
   shouldComponentUpdate(nextProps) {
     return !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item) ||
       this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis;
   }
 
+  timingTooltip() {
+    let { eventTimings, fromCache, fromServiceWorker, totalTime } = this.props.item;
+    let tooltip = [];
+
+    if (fromCache || fromServiceWorker) {
+      return tooltip;
+    }
+
+    if (eventTimings) {
+      for (let key of TIMING_KEYS) {
+        let width = eventTimings.timings[key];
+
+        if (width > 0) {
+          tooltip.push(L10N.getFormatStr("netmonitor.waterfall.tooltip." + key, width));
+        }
+      }
+    }
+
+    if (typeof totalTime === "number") {
+      tooltip.push(L10N.getFormatStr("netmonitor.waterfall.tooltip.total", totalTime));
+    }
+
+    return tooltip.join(L10N.getStr("netmonitor.waterfall.tooltip.separator"));
+  }
+
+  timingBoxes() {
+    let { eventTimings, fromCache, fromServiceWorker, totalTime } = this.props.item;
+    let boxes = [];
+
+    if (fromCache || fromServiceWorker) {
+      return boxes;
+    }
+
+    if (eventTimings) {
+      // Add a set of boxes representing timing information.
+      for (let key of TIMING_KEYS) {
+        let width = eventTimings.timings[key];
+
+        // Don't render anything if it surely won't be visible.
+        // One millisecond == one unscaled pixel.
+        if (width > 0) {
+          boxes.push(
+            div({
+              key,
+              className: `requests-list-timings-box ${key}`,
+              style: { width },
+            })
+          );
+        }
+      }
+    }
+
+    if (typeof totalTime === "number") {
+      let title = L10N.getFormatStr("networkMenu.totalMS", totalTime);
+      boxes.push(
+        div({
+          key: "total",
+          className: "requests-list-timings-total",
+          title,
+        }, title)
+      );
+    }
+
+    return boxes;
+  }
+
   render() {
-    let { firstRequestStartedMillis, item, onWaterfallMouseDown } = this.props;
-    const boxes = timingBoxes(item);
+    let {
+      firstRequestStartedMillis,
+      item,
+      onWaterfallMouseDown,
+    } = this.props;
 
     return (
       div({
         className: "requests-list-column requests-list-waterfall",
-        onMouseOver: function ({target}) {
+        onMouseOver: ({ target }) => {
           if (!target.title) {
-            target.title = timingTooltip(item);
+            target.title = this.timingTooltip();
           }
         }
       },
         div({
           className: "requests-list-timings",
           style: {
             paddingInlineStart: `${item.startedMillis - firstRequestStartedMillis}px`,
           },
           onMouseDown: onWaterfallMouseDown,
         },
-          boxes,
+          this.timingBoxes(),
         )
       )
     );
   }
 }
 
-function timingTooltip(item) {
-  let { eventTimings, fromCache, fromServiceWorker, totalTime } = item;
-  let tooltip = [];
-
-  if (fromCache || fromServiceWorker) {
-    return tooltip;
-  }
-
-  if (eventTimings) {
-    for (let key of TIMING_KEYS) {
-      let width = eventTimings.timings[key];
-
-      if (width > 0) {
-        tooltip.push(L10N.getFormatStr("netmonitor.waterfall.tooltip." + key, width));
-      }
-    }
-  }
-
-  if (typeof totalTime === "number") {
-    tooltip.push(L10N.getFormatStr("netmonitor.waterfall.tooltip.total", totalTime));
-  }
-
-  return tooltip.join(L10N.getStr("netmonitor.waterfall.tooltip.separator"));
-}
-
-function timingBoxes(item) {
-  let { eventTimings, fromCache, fromServiceWorker, totalTime } = item;
-  let boxes = [];
-
-  if (fromCache || fromServiceWorker) {
-    return boxes;
-  }
-
-  if (eventTimings) {
-    // Add a set of boxes representing timing information.
-    for (let key of TIMING_KEYS) {
-      let width = eventTimings.timings[key];
-
-      // Don't render anything if it surely won't be visible.
-      // One millisecond == one unscaled pixel.
-      if (width > 0) {
-        boxes.push(
-          div({
-            key,
-            className: `requests-list-timings-box ${key}`,
-            style: { width },
-          })
-        );
-      }
-    }
-  }
-
-  if (typeof totalTime === "number") {
-    let title = L10N.getFormatStr("networkMenu.totalMS", totalTime);
-    boxes.push(
-      div({
-        key: "total",
-        className: "requests-list-timings-total",
-        title,
-      }, title)
-    );
-  }
-
-  return boxes;
-}
-
 module.exports = RequestListColumnWaterfall;
--- a/devtools/client/netmonitor/src/components/RequestListHeader.js
+++ b/devtools/client/netmonitor/src/components/RequestListHeader.js
@@ -39,16 +39,18 @@ class RequestListHeader extends Componen
     };
   }
 
   constructor(props) {
     super(props);
     this.onContextMenu = this.onContextMenu.bind(this);
     this.drawBackground = this.drawBackground.bind(this);
     this.resizeWaterfall = this.resizeWaterfall.bind(this);
+    this.waterfallDivisionLabels = this.waterfallDivisionLabels.bind(this);
+    this.waterfallLabel = this.waterfallLabel.bind(this);
   }
 
   componentWillMount() {
     const { resetColumns, toggleColumn } = this.props;
     this.contextMenu = new RequestListHeaderContextMenu({
       resetColumns,
       toggleColumn,
     });
@@ -93,16 +95,79 @@ class RequestListHeader extends Componen
       // Measure its width and update the 'waterfallWidth' property in the store.
       // The 'waterfallWidth' will be further updated on every window resize.
       window.cancelIdleCallback(this._resizeTimerId);
       this._resizeTimerId = window.requestIdleCallback(() =>
         this.props.resizeWaterfall(waterfallHeader.getBoundingClientRect().width));
     }
   }
 
+  /**
+   * Build the waterfall header - timing tick marks with the right spacing
+   */
+  waterfallDivisionLabels(waterfallWidth, scale) {
+    let labels = [];
+
+    // Build new millisecond tick labels...
+    let timingStep = REQUESTS_WATERFALL.HEADER_TICKS_MULTIPLE;
+    let scaledStep = scale * timingStep;
+
+    // Ignore any divisions that would end up being too close to each other.
+    while (scaledStep < REQUESTS_WATERFALL.HEADER_TICKS_SPACING_MIN) {
+      scaledStep *= 2;
+    }
+
+    // Insert one label for each division on the current scale.
+    for (let x = 0; x < waterfallWidth; x += scaledStep) {
+      let millisecondTime = x / scale;
+      let divisionScale = "millisecond";
+
+      // If the division is greater than 1 minute.
+      if (millisecondTime > 60000) {
+        divisionScale = "minute";
+      } else if (millisecondTime > 1000) {
+        // If the division is greater than 1 second.
+        divisionScale = "second";
+      }
+
+      let width = (x + scaledStep | 0) - (x | 0);
+      // Adjust the first marker for the borders
+      if (x == 0) {
+        width -= 2;
+      }
+      // Last marker doesn't need a width specified at all
+      if (x + scaledStep >= waterfallWidth) {
+        width = undefined;
+      }
+
+      labels.push(div(
+        {
+          key: labels.length,
+          className: "requests-list-timings-division",
+          "data-division-scale": divisionScale,
+          style: { width }
+        },
+        getFormattedTime(millisecondTime)
+      ));
+    }
+
+    return labels;
+  }
+
+  waterfallLabel(waterfallWidth, scale, label) {
+    let className = "button-text requests-list-waterfall-label-wrapper";
+
+    if (waterfallWidth !== null && scale !== null) {
+      label = this.waterfallDivisionLabels(waterfallWidth, scale);
+      className += " requests-list-waterfall-visible";
+    }
+
+    return div({ className }, label);
+  }
+
   render() {
     let { columns, scale, sort, sortBy, waterfallWidth } = this.props;
 
     return (
       div({ className: "devtools-toolbar requests-list-headers-wrapper" },
         div({
           className: "devtools-toolbar requests-list-headers",
           onContextMenu: this.onContextMenu
@@ -134,92 +199,29 @@ class RequestListHeader extends Componen
                 button({
                   id: `requests-list-${name}-button`,
                   className: `requests-list-header-button`,
                   "data-sorted": sorted,
                   title: sortedTitle ? `${label} (${sortedTitle})` : label,
                   onClick: () => sortBy(name),
                 },
                   name === "waterfall"
-                    ? WaterfallLabel(waterfallWidth, scale, label)
+                    ? this.waterfallLabel(waterfallWidth, scale, label)
                     : div({ className: "button-text" }, label),
                   div({ className: "button-icon" })
                 )
               )
             );
           })
         )
       )
     );
   }
 }
 
-/**
- * Build the waterfall header - timing tick marks with the right spacing
- */
-function waterfallDivisionLabels(waterfallWidth, scale) {
-  let labels = [];
-
-  // Build new millisecond tick labels...
-  let timingStep = REQUESTS_WATERFALL.HEADER_TICKS_MULTIPLE;
-  let scaledStep = scale * timingStep;
-
-  // Ignore any divisions that would end up being too close to each other.
-  while (scaledStep < REQUESTS_WATERFALL.HEADER_TICKS_SPACING_MIN) {
-    scaledStep *= 2;
-  }
-
-  // Insert one label for each division on the current scale.
-  for (let x = 0; x < waterfallWidth; x += scaledStep) {
-    let millisecondTime = x / scale;
-    let divisionScale = "millisecond";
-
-    // If the division is greater than 1 minute.
-    if (millisecondTime > 60000) {
-      divisionScale = "minute";
-    } else if (millisecondTime > 1000) {
-      // If the division is greater than 1 second.
-      divisionScale = "second";
-    }
-
-    let width = (x + scaledStep | 0) - (x | 0);
-    // Adjust the first marker for the borders
-    if (x == 0) {
-      width -= 2;
-    }
-    // Last marker doesn't need a width specified at all
-    if (x + scaledStep >= waterfallWidth) {
-      width = undefined;
-    }
-
-    labels.push(div(
-      {
-        key: labels.length,
-        className: "requests-list-timings-division",
-        "data-division-scale": divisionScale,
-        style: { width }
-      },
-      getFormattedTime(millisecondTime)
-    ));
-  }
-
-  return labels;
-}
-
-function WaterfallLabel(waterfallWidth, scale, label) {
-  let className = "button-text requests-list-waterfall-label-wrapper";
-
-  if (waterfallWidth !== null && scale !== null) {
-    label = waterfallDivisionLabels(waterfallWidth, scale);
-    className += " requests-list-waterfall-visible";
-  }
-
-  return div({ className }, label);
-}
-
 module.exports = connect(
   (state) => ({
     columns: state.ui.columns,
     firstRequestStartedMillis: state.requests.firstStartedMillis,
     scale: getWaterfallScale(state),
     sort: state.sort,
     timingMarkers: state.timingMarkers,
     waterfallWidth: state.ui.waterfallWidth,
--- a/devtools/client/netmonitor/src/components/RequestListItem.js
+++ b/devtools/client/netmonitor/src/components/RequestListItem.js
@@ -249,17 +249,20 @@ class RequestListItem extends Component 
           RequestListColumnEndTime({ item, firstRequestStartedMillis }),
         columns.responseTime &&
           RequestListColumnResponseTime({ item, firstRequestStartedMillis }),
         columns.duration && RequestListColumnDuration({ item }),
         columns.latency && RequestListColumnLatency({ item }),
         ...RESPONSE_HEADERS.filter(header => columns[header]).map(
           header => RequestListColumnResponseHeader({ item, header }),
         ),
-        columns.waterfall &&
-          RequestListColumnWaterfall({ item, firstRequestStartedMillis,
-                                       onWaterfallMouseDown }),
+        columns.waterfall && RequestListColumnWaterfall({
+          connector,
+          firstRequestStartedMillis,
+          item,
+          onWaterfallMouseDown,
+        }),
       )
     );
   }
 }
 
 module.exports = RequestListItem;
--- a/devtools/client/netmonitor/src/components/StatisticsPanel.js
+++ b/devtools/client/netmonitor/src/components/StatisticsPanel.js
@@ -62,24 +62,30 @@ class StatisticsPanel extends Component 
 
   componentWillMount() {
     this.mdnLinkContainerNodes = new Map();
   }
 
   componentDidMount() {
     let { requests, connector } = this.props;
     requests.forEach((request) => {
-      fetchNetworkUpdatePacket(connector.requestData, request, ["responseHeaders"]);
+      fetchNetworkUpdatePacket(connector.requestData, request, [
+        "eventTimings",
+        "responseHeaders",
+      ]);
     });
   }
 
   componentWillReceiveProps(nextProps) {
     let { requests, connector } = nextProps;
     requests.forEach((request) => {
-      fetchNetworkUpdatePacket(connector.requestData, request, ["responseHeaders"]);
+      fetchNetworkUpdatePacket(connector.requestData, request, [
+        "eventTimings",
+        "responseHeaders",
+      ]);
     });
   }
 
   componentDidUpdate(prevProps) {
     MediaQueryList.addListener(this.onLayoutChange);
 
     const { requests } = this.props;
     let ready = requests && requests.length && requests.every((req) =>
--- a/devtools/client/netmonitor/src/components/TabboxPanel.js
+++ b/devtools/client/netmonitor/src/components/TabboxPanel.js
@@ -85,17 +85,20 @@ function TabboxPanel({
         title: RESPONSE_TITLE,
       },
         ResponsePanel({ request, openLink, connector }),
       ),
       TabPanel({
         id: PANELS.TIMINGS,
         title: TIMINGS_TITLE,
       },
-        TimingsPanel({ request }),
+        TimingsPanel({
+          connector,
+          request,
+        }),
       ),
       request.cause && request.cause.stacktraceAvailable &&
       TabPanel({
         id: PANELS.STACK_TRACE,
         title: STACK_TRACE_TITLE,
       },
         StackTracePanel({ connector, openLink, request, sourceMapService }),
       ),
--- a/devtools/client/netmonitor/src/components/TimingsPanel.js
+++ b/devtools/client/netmonitor/src/components/TimingsPanel.js
@@ -1,80 +1,96 @@
 /* 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 { Component } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const { L10N } = require("../utils/l10n");
 const { getNetMonitorTimingsURL } = require("../utils/mdn-utils");
+const { fetchNetworkUpdatePacket } = require("../utils/request-utils");
 
 // Components
 const MDNLink = require("./MdnLink");
 
 const { div, span } = dom;
 const types = ["blocked", "dns", "connect", "ssl", "send", "wait", "receive"];
 const TIMINGS_END_PADDING = "80px";
 
 /*
  * Timings panel component
  * Display timeline bars that shows the total wait time for various stages
  */
-function TimingsPanel({ request }) {
-  if (!request.eventTimings) {
-    return null;
+class TimingsPanel extends Component {
+  static get propTypes() {
+    return {
+      connector: PropTypes.object.isRequired,
+      request: PropTypes.object.isRequired,
+    };
+  }
+
+  componentDidMount() {
+    let { connector, request } = this.props;
+    fetchNetworkUpdatePacket(connector.requestData, request, ["eventTimings"]);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    let { connector, request } = nextProps;
+    fetchNetworkUpdatePacket(connector.requestData, request, ["eventTimings"]);
   }
 
-  const { timings, totalTime, offsets } = request.eventTimings;
-  const timelines = types.map((type, idx) => {
-    // Determine the relative offset for each timings box. For example, the
-    // offset of third timings box will be 0 + blocked offset + dns offset
+  render() {
+    const { eventTimings } = this.props.request;
+    if (!eventTimings) {
+      return null;
+    }
 
-    const offsetScale = offsets[type] / totalTime || 0;
-    const timelineScale = timings[type] / totalTime || 0;
+    const { timings, totalTime, offsets } = eventTimings;
+    const timelines = types.map((type, idx) => {
+      // Determine the relative offset for each timings box. For example, the
+      // offset of third timings box will be 0 + blocked offset + dns offset
+
+      const offsetScale = offsets[type] / totalTime || 0;
+      const timelineScale = timings[type] / totalTime || 0;
 
-    return div({
-      key: type,
-      id: `timings-summary-${type}`,
-      className: "tabpanel-summary-container timings-container",
-    },
-      span({ className: "tabpanel-summary-label timings-label" },
-        L10N.getStr(`netmonitor.timings.${type}`)
-      ),
-      div({ className: "requests-list-timings-container" },
-        span({
-          className: "requests-list-timings-offset",
-          style: {
-            width: `calc(${offsetScale} * (100% - ${TIMINGS_END_PADDING})`,
-          },
+      return div({
+        key: type,
+        id: `timings-summary-${type}`,
+        className: "tabpanel-summary-container timings-container",
+      },
+        span({ className: "tabpanel-summary-label timings-label" },
+          L10N.getStr(`netmonitor.timings.${type}`)
+        ),
+        div({ className: "requests-list-timings-container" },
+          span({
+            className: "requests-list-timings-offset",
+            style: {
+              width: `calc(${offsetScale} * (100% - ${TIMINGS_END_PADDING})`,
+            },
+          }),
+          span({
+            className: `requests-list-timings-box ${type}`,
+            style: {
+              width: `calc(${timelineScale} * (100% - ${TIMINGS_END_PADDING}))`,
+            },
+          }),
+          span({ className: "requests-list-timings-total" },
+            L10N.getFormatStr("networkMenu.totalMS", timings[type])
+          )
+        ),
+      );
+    });
+
+    return (
+      div({ className: "panel-container" },
+        timelines,
+        MDNLink({
+          url: getNetMonitorTimingsURL(),
         }),
-        span({
-          className: `requests-list-timings-box ${type}`,
-          style: {
-            width: `calc(${timelineScale} * (100% - ${TIMINGS_END_PADDING}))`,
-          },
-        }),
-        span({ className: "requests-list-timings-total" },
-          L10N.getFormatStr("networkMenu.totalMS", timings[type])
-        )
-      ),
+      )
     );
-  });
-
-  return (
-    div({ className: "panel-container" },
-      timelines,
-      MDNLink({
-        url: getNetMonitorTimingsURL(),
-      }),
-    )
-  );
+  }
 }
 
-TimingsPanel.displayName = "TimingsPanel";
-
-TimingsPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
 module.exports = TimingsPanel;
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -308,17 +308,17 @@ class FirefoxDataProvider {
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param {string} type message type
    * @param {object} packet the message received from the server.
    * @param {object} networkInfo the network request information.
    */
-  async onNetworkEventUpdate(type, data) {
+  onNetworkEventUpdate(type, data) {
     let { packet, networkInfo } = data;
     let { actor } = networkInfo;
     let { updateType } = packet;
 
     switch (updateType) {
       case "securityInfo":
         this.pushRequestToQueue(actor, { securityState: networkInfo.securityInfo });
         break;
@@ -340,20 +340,19 @@ class FirefoxDataProvider {
           mimeType: networkInfo.response.content.mimeType,
         });
         break;
       case "eventTimings":
         // Total time doesn't have to be always set e.g. net provider enhancer
         // in Console panel is using this method to fetch data when network log
         // is expanded. So, make sure to not push undefined into the payload queue
         // (it could overwrite an existing value).
-        if (typeof networkInfo.totalTime != "undefined") {
+        if (typeof networkInfo.totalTime !== "undefined") {
           this.pushRequestToQueue(actor, { totalTime: networkInfo.totalTime });
         }
-        await this._requestData(actor, updateType);
         break;
     }
 
     // This available field helps knowing when/if updateType property is arrived
     // and can be requested via `requestData`
     this.pushRequestToQueue(actor, { [`${updateType}Available`]: true });
 
     this.onPayloadDataReceived(actor);
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -114,16 +114,17 @@ const UPDATE_PROPS = [
   "securityState",
   "securityInfo",
   "securityInfoAvailable",
   "mimeType",
   "contentSize",
   "transferredSize",
   "totalTime",
   "eventTimings",
+  "eventTimingsAvailable",
   "headersSize",
   "customQueryValue",
   "requestHeaders",
   "requestHeadersAvailable",
   "requestHeadersFromUploadStream",
   "requestCookies",
   "requestCookiesAvailable",
   "requestPostData",
--- a/devtools/client/netmonitor/src/har/har-builder.js
+++ b/devtools/client/netmonitor/src/har/har-builder.js
@@ -115,20 +115,25 @@ HarBuilder.prototype = {
   buildEntry: async function (log, file) {
     let page = this.getPage(log, file);
 
     let entry = {};
     entry.pageref = page.id;
     entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
     entry.time = file.endedMillis - file.startedMillis;
 
+    let eventTimings = file.eventTimings;
+    if (!eventTimings && this._options.requestData) {
+      eventTimings = await this._options.requestData(file.id, "eventTimings");
+    }
+
     entry.request = await this.buildRequest(file);
     entry.response = await this.buildResponse(file);
     entry.cache = this.buildCache(file);
-    entry.timings = file.eventTimings ? file.eventTimings.timings : {};
+    entry.timings = eventTimings ? eventTimings.timings : {};
 
     if (file.remoteAddress) {
       entry.serverIPAddress = file.remoteAddress;
     }
 
     if (file.remotePort) {
       entry.connection = file.remotePort + "";
     }
--- a/devtools/client/netmonitor/src/selectors/ui.js
+++ b/devtools/client/netmonitor/src/selectors/ui.js
@@ -1,34 +1,38 @@
 /* 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 { createSelector } = require("devtools/client/shared/vendor/reselect");
 const { REQUESTS_WATERFALL } = require("../constants");
 const { getDisplayedRequests } = require("./requests");
 
+const EPSILON = 0.001;
+
 function isNetworkDetailsToggleButtonDisabled(state) {
   return getDisplayedRequests(state).length == 0;
 }
 
-const EPSILON = 0.001;
+const getWaterfallScale = createSelector(
+  state => state.requests,
+  state => state.timingMarkers,
+  state => state.ui,
+  (requests, timingMarkers, ui) => {
+    if (requests.firstStartedMillis === +Infinity || ui.waterfallWidth === null) {
+      return null;
+    }
 
-function getWaterfallScale(state) {
-  const { requests, timingMarkers, ui } = state;
-
-  if (requests.firstStartedMillis === +Infinity || ui.waterfallWidth === null) {
-    return null;
+    const lastEventMillis = Math.max(requests.lastEndedMillis,
+                                     timingMarkers.firstDocumentDOMContentLoadedTimestamp,
+                                     timingMarkers.firstDocumentLoadTimestamp);
+    const longestWidth = lastEventMillis - requests.firstStartedMillis;
+    return Math.min(Math.max(
+      (ui.waterfallWidth - REQUESTS_WATERFALL.LABEL_WIDTH) / longestWidth, EPSILON), 1);
   }
-
-  const lastEventMillis = Math.max(requests.lastEndedMillis,
-                                   timingMarkers.firstDocumentDOMContentLoadedTimestamp,
-                                   timingMarkers.firstDocumentLoadTimestamp);
-  const longestWidth = lastEventMillis - requests.firstStartedMillis;
-  return Math.min(Math.max(
-    (ui.waterfallWidth - REQUESTS_WATERFALL.LABEL_WIDTH) / longestWidth, EPSILON), 1);
-}
+);
 
 module.exports = {
   isNetworkDetailsToggleButtonDisabled,
   getWaterfallScale,
 };