TimingsPanel.js (8220B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 connect, 9 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 10 const { 11 Component, 12 } = require("resource://devtools/client/shared/vendor/react.mjs"); 13 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 14 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 15 const { 16 L10N, 17 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 18 const { 19 getNetMonitorTimingsURL, 20 } = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js"); 21 const { 22 fetchNetworkUpdatePacket, 23 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 24 const { 25 getFormattedTime, 26 } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); 27 const { 28 TIMING_KEYS, 29 } = require("resource://devtools/client/netmonitor/src/constants.js"); 30 31 // Components 32 const MDNLink = require("resource://devtools/client/shared/components/MdnLink.js"); 33 34 const { div, span } = dom; 35 36 /** 37 * Timings panel component 38 * Display timeline bars that shows the total wait time for various stages 39 */ 40 class TimingsPanel extends Component { 41 static get propTypes() { 42 return { 43 connector: PropTypes.object.isRequired, 44 request: PropTypes.object.isRequired, 45 firstRequestStartedMs: PropTypes.number, 46 }; 47 } 48 49 componentDidMount() { 50 const { connector, request } = this.props; 51 fetchNetworkUpdatePacket(connector.requestData, request, ["eventTimings"]); 52 } 53 54 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 55 UNSAFE_componentWillReceiveProps(nextProps) { 56 const { connector, request } = nextProps; 57 fetchNetworkUpdatePacket(connector.requestData, request, ["eventTimings"]); 58 } 59 60 renderServiceWorkerTimings() { 61 const { serviceWorkerTimings } = this.props.request.eventTimings; 62 63 if (!serviceWorkerTimings) { 64 return null; 65 } 66 67 const totalTime = Object.values(serviceWorkerTimings).reduce( 68 (acc, value) => acc + value, 69 0 70 ); 71 72 let offset = 0; 73 let preValue = 0; 74 75 return div( 76 {}, 77 div( 78 { className: "label-separator" }, 79 L10N.getStr("netmonitor.timings.serviceWorkerTiming") 80 ), 81 Object.entries(serviceWorkerTimings).map(([key, value]) => { 82 if (preValue > 0) { 83 offset += preValue / totalTime; 84 } 85 preValue = value; 86 return div( 87 { 88 key, 89 className: 90 "tabpanel-summary-container timings-container service-worker", 91 }, 92 span( 93 { className: "tabpanel-summary-label timings-label" }, 94 L10N.getStr(`netmonitor.timings.${key}`) 95 ), 96 div( 97 { className: "requests-list-timings-container" }, 98 span({ 99 className: `requests-list-timings-box serviceworker-timings-color-${key.replace( 100 "ServiceWorker", 101 "" 102 )}`, 103 style: { 104 "--current-timing-offset": offset > 0 ? offset : 0, 105 "--current-timing-width": value / totalTime, 106 }, 107 }), 108 span( 109 { className: "requests-list-timings-total" }, 110 getFormattedTime(value) 111 ) 112 ) 113 ); 114 }) 115 ); 116 } 117 118 renderServerTimings() { 119 const { serverTimings, totalTime } = this.props.request.eventTimings; 120 121 if (!serverTimings?.length) { 122 return null; 123 } 124 125 return div( 126 {}, 127 div( 128 { className: "label-separator" }, 129 L10N.getStr("netmonitor.timings.serverTiming") 130 ), 131 ...serverTimings.map(({ name, duration, description }, index) => { 132 const color = name === "total" ? "total" : (index % 3) + 1; 133 134 return div( 135 { 136 key: index, 137 className: "tabpanel-summary-container timings-container server", 138 }, 139 span( 140 { className: "tabpanel-summary-label timings-label" }, 141 description || name 142 ), 143 div( 144 { className: "requests-list-timings-container" }, 145 span({ 146 className: `requests-list-timings-box server-timings-color-${color}`, 147 style: { 148 "--current-timing-offset": (totalTime - duration) / totalTime, 149 "--current-timing-width": duration / totalTime, 150 }, 151 }), 152 span( 153 { className: "requests-list-timings-total" }, 154 getFormattedTime(duration) 155 ) 156 ) 157 ); 158 }) 159 ); 160 } 161 162 render() { 163 const { eventTimings, totalTime, startedMs } = this.props.request; 164 const { firstRequestStartedMs } = this.props; 165 166 if (!eventTimings) { 167 return div( 168 { 169 className: 170 "tabpanel-summary-container timings-container empty-notice", 171 }, 172 L10N.getStr("netmonitor.timings.noTimings") 173 ); 174 } 175 176 const { timings, offsets } = eventTimings; 177 let queuedAt, startedAt, downloadedAt; 178 const isFirstRequestStartedAvailable = firstRequestStartedMs !== null; 179 180 if (isFirstRequestStartedAvailable) { 181 queuedAt = startedMs - firstRequestStartedMs; 182 startedAt = queuedAt + timings.blocked; 183 downloadedAt = queuedAt + totalTime; 184 } 185 186 const timelines = TIMING_KEYS.map((type, idx) => { 187 // Determine the relative offset for each timings box. For example, the 188 // offset of third timings box will be 0 + blocked offset + dns offset 189 // If offsets sent from the backend aren't available calculate it 190 // from the timing info. 191 const offset = offsets 192 ? offsets[type] 193 : TIMING_KEYS.slice(0, idx).reduce( 194 (acc, cur) => acc + timings[cur] || 0, 195 0 196 ); 197 198 const offsetScale = offset / totalTime || 0; 199 const timelineScale = timings[type] / totalTime || 0; 200 201 return div( 202 { 203 key: type, 204 id: `timings-summary-${type}`, 205 className: "tabpanel-summary-container timings-container request", 206 }, 207 span( 208 { className: "tabpanel-summary-label timings-label" }, 209 L10N.getStr(`netmonitor.timings.${type}`) 210 ), 211 div( 212 { className: "requests-list-timings-container" }, 213 span({ 214 className: `requests-list-timings-box ${type}`, 215 style: { 216 "--current-timing-offset": offsetScale, 217 "--current-timing-width": timelineScale, 218 }, 219 }), 220 span( 221 { className: "requests-list-timings-total" }, 222 getFormattedTime(timings[type]) 223 ) 224 ) 225 ); 226 }); 227 228 return div( 229 { className: "panel-container" }, 230 isFirstRequestStartedAvailable && 231 div( 232 { className: "timings-overview" }, 233 span( 234 { className: "timings-overview-item" }, 235 L10N.getFormatStr( 236 "netmonitor.timings.queuedAt", 237 getFormattedTime(queuedAt) 238 ) 239 ), 240 span( 241 { className: "timings-overview-item" }, 242 L10N.getFormatStr( 243 "netmonitor.timings.startedAt", 244 getFormattedTime(startedAt) 245 ) 246 ), 247 span( 248 { className: "timings-overview-item" }, 249 L10N.getFormatStr( 250 "netmonitor.timings.downloadedAt", 251 getFormattedTime(downloadedAt) 252 ) 253 ) 254 ), 255 div( 256 { className: "label-separator" }, 257 L10N.getStr("netmonitor.timings.requestTiming") 258 ), 259 timelines, 260 this.renderServiceWorkerTimings(), 261 this.renderServerTimings(), 262 MDNLink({ 263 url: getNetMonitorTimingsURL(), 264 title: L10N.getStr("netmonitor.timings.learnMore"), 265 }) 266 ); 267 } 268 } 269 270 module.exports = connect(state => ({ 271 firstRequestStartedMs: state.requests ? state.requests.firstStartedMs : null, 272 }))(TimingsPanel);