StatisticsPanel.js (9347B)
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 ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.mjs"); 8 const { 9 Component, 10 createFactory, 11 } = require("resource://devtools/client/shared/vendor/react.mjs"); 12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 13 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 14 const { 15 connect, 16 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 17 const { Chart } = require("resource://devtools/client/shared/widgets/Chart.js"); 18 const { PluralForm } = require("resource://devtools/shared/plural-form.js"); 19 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); 20 const { 21 getSizeWithDecimals, 22 getTimeWithDecimals, 23 } = require("resource://devtools/client/netmonitor/src/utils/format-utils.js"); 24 const { 25 L10N, 26 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 27 const { 28 getPerformanceAnalysisURL, 29 } = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js"); 30 const { 31 fetchNetworkUpdatePacket, 32 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 33 const { 34 getStatisticsData, 35 } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); 36 37 // Components 38 const MDNLink = createFactory( 39 require("resource://devtools/client/shared/components/MdnLink.js") 40 ); 41 42 const { button, div } = dom; 43 const MediaQueryList = window.matchMedia("(min-width: 700px)"); 44 45 const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200; 46 const BACK_BUTTON = L10N.getStr("netmonitor.backButton"); 47 const CHARTS_CACHE_ENABLED = L10N.getStr("charts.cacheEnabled"); 48 const CHARTS_CACHE_DISABLED = L10N.getStr("charts.cacheDisabled"); 49 const CHARTS_LEARN_MORE = L10N.getStr("charts.learnMore"); 50 51 /* 52 * Statistics panel component 53 * Performance analysis tool which shows you how long the browser takes to 54 * download the different parts of your site. 55 */ 56 class StatisticsPanel extends Component { 57 static get propTypes() { 58 return { 59 connector: PropTypes.object.isRequired, 60 closeStatistics: PropTypes.func.isRequired, 61 enableRequestFilterTypeOnly: PropTypes.func.isRequired, 62 hasLoad: PropTypes.bool, 63 requests: PropTypes.array, 64 statisticsData: PropTypes.object, 65 }; 66 } 67 68 constructor(props) { 69 super(props); 70 71 this.state = { 72 isVerticalSplitter: MediaQueryList.matches, 73 }; 74 75 this.createMDNLink = this.createMDNLink.bind(this); 76 this.unmountMDNLinkContainers = this.unmountMDNLinkContainers.bind(this); 77 this.createChart = this.createChart.bind(this); 78 this.onLayoutChange = this.onLayoutChange.bind(this); 79 } 80 81 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 82 UNSAFE_componentWillMount() { 83 this.mdnLinkContainerNodes = new Map(); 84 } 85 86 componentDidMount() { 87 const { requests, connector } = this.props; 88 requests.forEach(request => { 89 fetchNetworkUpdatePacket(connector.requestData, request, [ 90 "eventTimings", 91 "responseHeaders", 92 ]); 93 }); 94 } 95 96 // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507 97 UNSAFE_componentWillReceiveProps(nextProps) { 98 const { requests, connector } = nextProps; 99 requests.forEach(request => { 100 fetchNetworkUpdatePacket(connector.requestData, request, [ 101 "eventTimings", 102 "responseHeaders", 103 ]); 104 }); 105 } 106 107 componentDidUpdate() { 108 MediaQueryList.addListener(this.onLayoutChange); 109 110 const { hasLoad, requests, statisticsData } = this.props; 111 112 // Display statistics about all requests for which we received enough data, 113 // as soon as the page is considered to be loaded 114 const ready = requests.length && hasLoad; 115 116 this.createChart({ 117 id: "primedCacheChart", 118 title: CHARTS_CACHE_ENABLED, 119 data: ready ? statisticsData.primedCacheData : null, 120 }); 121 122 this.createChart({ 123 id: "emptyCacheChart", 124 title: CHARTS_CACHE_DISABLED, 125 data: ready ? statisticsData.emptyCacheData : null, 126 }); 127 128 this.createMDNLink("primedCacheChart", getPerformanceAnalysisURL()); 129 this.createMDNLink("emptyCacheChart", getPerformanceAnalysisURL()); 130 } 131 132 componentWillUnmount() { 133 MediaQueryList.removeListener(this.onLayoutChange); 134 this.unmountMDNLinkContainers(); 135 } 136 137 createMDNLink(chartId, url) { 138 if (this.mdnLinkContainerNodes.has(chartId)) { 139 ReactDOM.unmountComponentAtNode(this.mdnLinkContainerNodes.get(chartId)); 140 } 141 142 // MDNLink is a React component but Chart isn't. To get the link 143 // into the chart we mount a new ReactDOM at the appropriate 144 // location after the chart has been created. 145 const title = this.refs[chartId].querySelector(".table-chart-title"); 146 const containerNode = document.createElement("span"); 147 title.appendChild(containerNode); 148 149 ReactDOM.render( 150 MDNLink({ 151 url, 152 title: CHARTS_LEARN_MORE, 153 }), 154 containerNode 155 ); 156 this.mdnLinkContainerNodes.set(chartId, containerNode); 157 } 158 159 unmountMDNLinkContainers() { 160 for (const [, node] of this.mdnLinkContainerNodes) { 161 ReactDOM.unmountComponentAtNode(node); 162 } 163 } 164 165 createChart({ id, title, data }) { 166 // Create a new chart. 167 const chart = Chart.PieTable(document, { 168 diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER, 169 title, 170 header: { 171 count: L10N.getStr("charts.requestsNumber"), 172 label: L10N.getStr("charts.type"), 173 size: L10N.getStr("charts.size"), 174 transferredSize: L10N.getStr("charts.transferred"), 175 time: L10N.getStr("charts.time"), 176 nonBlockingTime: L10N.getStr("charts.nonBlockingTime"), 177 }, 178 data, 179 strings: { 180 size: value => 181 L10N.getFormatStr( 182 "charts.size.kB", 183 getSizeWithDecimals(value / 1000) 184 ), 185 transferredSize: value => 186 L10N.getFormatStr( 187 "charts.transferredSize.kB", 188 getSizeWithDecimals(value / 1000) 189 ), 190 time: value => 191 L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)), 192 nonBlockingTime: value => 193 L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)), 194 }, 195 totals: { 196 cached: total => L10N.getFormatStr("charts.totalCached", total), 197 count: total => L10N.getFormatStr("charts.totalCount", total), 198 size: total => 199 L10N.getFormatStr( 200 "charts.totalSize.kB", 201 getSizeWithDecimals(total / 1000) 202 ), 203 transferredSize: total => 204 L10N.getFormatStr( 205 "charts.totalTransferredSize.kB", 206 getSizeWithDecimals(total / 1000) 207 ), 208 time: total => { 209 const seconds = total / 1000; 210 const string = getTimeWithDecimals(seconds); 211 return PluralForm.get( 212 seconds, 213 L10N.getStr("charts.totalSeconds") 214 ).replace("#1", string); 215 }, 216 nonBlockingTime: total => { 217 const seconds = total / 1000; 218 const string = getTimeWithDecimals(seconds); 219 return PluralForm.get( 220 seconds, 221 L10N.getStr("charts.totalSecondsNonBlocking") 222 ).replace("#1", string); 223 }, 224 }, 225 sorted: true, 226 }); 227 228 chart.on("click", ({ label }) => { 229 // Reset FilterButtons and enable one filter exclusively 230 this.props.closeStatistics(); 231 this.props.enableRequestFilterTypeOnly(label); 232 }); 233 234 const container = this.refs[id]; 235 236 // Update the charts of the specified type. 237 container.replaceChildren(chart.node); 238 } 239 240 onLayoutChange() { 241 this.setState({ 242 isVerticalSplitter: MediaQueryList.matches, 243 }); 244 } 245 246 render() { 247 const { closeStatistics } = this.props; 248 249 const directionSplitter = this.state.isVerticalSplitter 250 ? "devtools-side-splitter" 251 : "devtools-horizontal-splitter"; 252 253 return div( 254 { className: "statistics-panel" }, 255 button( 256 { 257 className: "back-button devtools-button", 258 "data-text-only": "true", 259 title: BACK_BUTTON, 260 onClick: closeStatistics, 261 }, 262 BACK_BUTTON 263 ), 264 div( 265 { className: "charts-container" }, 266 div({ 267 ref: "primedCacheChart", 268 className: "charts primed-cache-chart", 269 }), 270 div({ className: ["splitter", directionSplitter].join(" ") }), 271 div({ ref: "emptyCacheChart", className: "charts empty-cache-chart" }) 272 ) 273 ); 274 } 275 } 276 277 module.exports = connect( 278 state => ({ 279 // `firstDocumentLoadTimestamp` is set on timing markers when we receive 280 // DOCUMENT_EVENT's dom-complete, which is equivalent to page `load` event. 281 hasLoad: state.timingMarkers.firstDocumentLoadTimestamp != -1, 282 requests: [...state.requests.requests], 283 statisticsData: getStatisticsData(state), 284 }), 285 (dispatch, props) => ({ 286 closeStatistics: () => 287 dispatch(Actions.openStatistics(props.connector, false)), 288 enableRequestFilterTypeOnly: label => 289 dispatch(Actions.enableRequestFilterTypeOnly(label)), 290 }) 291 )(StatisticsPanel);