tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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);