--- 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,
};