tor-browser

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

Tracer.js (34714B)


      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 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 import React, {
      7  Component,
      8  createFactory,
      9 } from "devtools/client/shared/vendor/react";
     10 import {
     11  div,
     12  button,
     13  footer,
     14 } from "devtools/client/shared/vendor/react-dom-factories";
     15 import SearchInput from "../shared/SearchInput";
     16 import EventListeners from "../shared/EventListeners";
     17 import { connect } from "devtools/client/shared/vendor/react-redux";
     18 import {
     19  getSelectedTraceIndex,
     20  getFilteredTopTraces,
     21  getAllTraces,
     22  getTraceChildren,
     23  getTraceParents,
     24  getTraceFrames,
     25  getAllMutationTraces,
     26  getAllTraceCount,
     27  getIsCurrentlyTracing,
     28  getRuntimeVersions,
     29  getTraceHighlightedDomEvents,
     30  getTraceMatchingSearchTraces,
     31  getTraceMatchingSearchException,
     32  getTraceMatchingSearchValueOrGrip,
     33  getIsTracingValues,
     34 } from "../../selectors/index";
     35 import { NO_SEARCH_VALUE } from "../../reducers/tracer-frames";
     36 
     37 const { throttle } = require("resource://devtools/shared/throttle.js");
     38 const VirtualizedTree = require("resource://devtools/client/shared/components/VirtualizedTree.js");
     39 const FrameView = createFactory(
     40  require("resource://devtools/client/shared/components/Frame.js")
     41 );
     42 const Reps = ChromeUtils.importESModule(
     43  "resource://devtools/client/shared/components/reps/index.mjs"
     44 );
     45 const {
     46  REPS: { Rep },
     47  MODE,
     48 } = Reps;
     49 const {
     50  TRACER_FIELDS_INDEXES,
     51 } = require("resource://devtools/server/actors/tracer.js");
     52 const {
     53  HTMLTooltip,
     54 } = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
     55 
     56 const { TabPanel, Tabs } = ChromeUtils.importESModule(
     57  "resource://devtools/client/shared/components/tabs/Tabs.mjs",
     58  { global: "current" }
     59 );
     60 
     61 import actions from "../../actions/index";
     62 
     63 const isMacOS = AppConstants.platform == "macosx";
     64 const TREE_NODE_HEIGHT = 20;
     65 const DEBUG = false;
     66 
     67 export class Tracer extends Component {
     68  constructor(props) {
     69    super(props);
     70 
     71    this.state = {
     72      // List of expanded traces in the tree
     73      expanded: new Set(),
     74 
     75      // First visible trace's index.
     76      // Note that these two indexes aren't related to the VirtualizedTree viewport.
     77      // That's the possibly visible traces when scrolling top/bottom of the whole Tree.
     78      startIndex: 0,
     79 
     80      // Last visible trace's index. -1 is we should show all of them at the end.
     81      // As soon as we start scrolling via the left slider, new traces are added outside of the selected viewport.
     82      endIndex: -1,
     83 
     84      // Number of trace rendered in the timeline and the tree (considering the tree is expanded, less may be displayed based on collapsing)
     85      renderedTraceCount: 0,
     86 
     87      // Index of the currently selected tab (traces or events)
     88      selectedTabIndex: 0,
     89    };
     90 
     91    this.onSliderClick = this.onSliderClick.bind(this);
     92    this.onSliderWheel = this.onSliderWheel.bind(this);
     93    this.resetZoom = this.resetZoom.bind(this);
     94 
     95    // Throttle requests to update the trace argument search query as it is a costly operation
     96    this.throttledUpdateSearch = throttle(
     97      this.throttledUpdateSearch.bind(this),
     98      250
     99    );
    100  }
    101 
    102  UNSAFE_componentWillReceiveProps(nextProps) {
    103    const { traceParents } = this.props;
    104    if (
    105      nextProps.selectedTraceIndex != this.props.selectedTraceIndex &&
    106      nextProps.selectedTraceIndex != null
    107    ) {
    108      const { expanded } = this.state;
    109      let index = traceParents[nextProps.selectedTraceIndex];
    110      while (index) {
    111        expanded.add(index);
    112        index = traceParents[index];
    113      }
    114      this.setState({ expanded });
    115    }
    116 
    117    // Force update the renderedTraceCount when we receive new traces
    118    if (nextProps.traceCount != this.props.traceCount) {
    119      if (nextProps.traceCount == 0) {
    120        // Reset the indexes when the view is cleared (i.e. when we just started recording a new trace)
    121        this.updateIndexes(
    122          {
    123            startIndex: 0,
    124            endIndex: -1,
    125          },
    126          nextProps
    127        );
    128      } else {
    129        this.updateIndexes(
    130          {
    131            startIndex: this.state.startIndex,
    132            endIndex: this.state.endIndex,
    133          },
    134          nextProps
    135        );
    136      }
    137    }
    138  }
    139 
    140  componentDidMount() {
    141    // Prevent toolbox zooming when using Ctrl+Wheel on the slider.
    142    // (for some reason... React doesn't seem to register the right "wheel", event listener via `onWheel`,
    143    //  which is the one to cancel to prevent toolbox zooming code to work.)
    144    this.refs.timeline.onwheel = e => e.preventDefault();
    145 
    146    if (!this.tooltip) {
    147      this.instantiateTooltip();
    148    }
    149 
    150    // Force updating indexes when we navigate back to the tracer sidebar.
    151    // For example, when we navigated away to the source tree.
    152    if (!this.state.renderedTraceCount) {
    153      this.updateIndexes(
    154        {
    155          startIndex: this.state.startIndex,
    156          endIndex: this.state.endIndex,
    157        },
    158        this.props
    159      );
    160    }
    161  }
    162 
    163  instantiateTooltip() {
    164    this.tooltip = new HTMLTooltip(this.refs.timeline.ownerDocument, {
    165      className: "event-tooltip",
    166      type: "arrow",
    167      // Avoid consuming the first click on the anchored UI element in the slider
    168      consumeOutsideClicks: false,
    169    });
    170    this.tooltip.setContentSize({ height: "auto" });
    171    this.tooltip.startTogglingOnHover(this.refs.timeline, (target, tooltip) => {
    172      if (target.classList.contains("tracer-slider-event")) {
    173        const { traceIndex } = target.dataset;
    174        const trace = this.props.allTraces[traceIndex];
    175        const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME];
    176        const eventType = getEventClassNameFromTraceEventName(eventName);
    177        tooltip.panel.innerHTML = "";
    178        const el = document.createElement("div");
    179        el.classList.add("tracer-dom-event", eventType);
    180        el.textContent = `DOM | ${eventName}`;
    181        tooltip.panel.append(
    182          el,
    183          document.createElement("hr"),
    184          document.createTextNode(
    185            "Double click to focus on the executions related to this event."
    186          )
    187        );
    188        return true;
    189      } else if (target.classList.contains("tracer-slider-mutation")) {
    190        const { traceIndex } = target.dataset;
    191        const trace = this.props.allTraces[traceIndex];
    192        const mutationType = trace[TRACER_FIELDS_INDEXES.DOM_MUTATION_TYPE];
    193        tooltip.panel.innerHTML = "";
    194        const el = document.createElement("div");
    195        el.classList.add("tracer-dom-mutation");
    196        el.textContent = `DOM Mutation | ${mutationType}`;
    197        tooltip.panel.append(
    198          el,
    199          document.createElement("hr"),
    200          document.createTextNode(
    201            "Click to find the call tree leading to this mutation."
    202          )
    203        );
    204        return true;
    205      }
    206      return false;
    207    });
    208  }
    209 
    210  componentDidUpdate() {
    211    if (DEBUG) {
    212      dump(
    213        ` # start: ${this.state.startIndex} end: ${this.state.endIndex} rendered: ${this.state.renderedTraceCount} traceCount:${this.props.traceCount}\n`
    214      );
    215    }
    216  }
    217 
    218  renderCallTree() {
    219    let {
    220      selectedTraceIndex,
    221      topTraces,
    222      allTraces,
    223      traceChildren,
    224      traceParents,
    225    } = this.props;
    226    // Print warning message when there is no trace recorded yet
    227    if (!allTraces.length) {
    228      if (!this.props.isTracing) {
    229        // We can't yet distinguish being completely off or pending for next interaction or load
    230        return div(
    231          { className: "tracer-message" },
    232          "Tracer is off, or pending for next interaction/load."
    233        );
    234      }
    235      return div(
    236        { className: "tracer-message" },
    237        "Waiting for the first JavaScript executions"
    238      );
    239    }
    240 
    241    // If there is some traces in allTraces but none in topTraces,
    242    // it means that they were all filtered out.
    243    if (!topTraces.length) {
    244      // Use distinct message when we are showing only a slice of the record, of the whole record
    245      if (this.state.renderedTraceCount != this.props.traceCount) {
    246        return div(
    247          { className: "tracer-message" },
    248          "All traces have been filtered out, in the slice of the record"
    249        );
    250      }
    251      return div(
    252        { className: "tracer-message" },
    253        "All traces have been filtered out"
    254      );
    255    }
    256 
    257    // Indexes are floating number, so convert them to a decimal number as indexes in the trace array
    258    let { startIndex, endIndex } = this.state;
    259    startIndex = Math.floor(startIndex);
    260    endIndex = Math.floor(endIndex);
    261 
    262    if (startIndex != 0 || endIndex != -1) {
    263      // When we start zooming, only consider traces whose top level frame
    264      // is in the zoomed section.
    265 
    266      // Lookup for the first top trace after the start index
    267      let topTracesStartIndex = 0;
    268      if (startIndex != 0) {
    269        topTracesStartIndex = -1;
    270        for (let i = 0; i < topTraces.length; i++) {
    271          const traceIndex = topTraces[i];
    272          if (traceIndex >= startIndex) {
    273            topTracesStartIndex = i;
    274            break;
    275          }
    276        }
    277      }
    278 
    279      // Lookup for the first top trace from the end before the end index
    280      let topTracesEndIndex = topTraces.length;
    281      if (endIndex != -1) {
    282        for (let i = topTraces.length; i >= 0; i--) {
    283          const traceIndex = topTraces[i];
    284          if (traceIndex <= endIndex) {
    285            topTracesEndIndex = i + 1;
    286            break;
    287          }
    288        }
    289      }
    290 
    291      if (topTracesStartIndex == -1) {
    292        // When none of the top traces are within the selected range, pick the start index of top trace.
    293        // This happens when we zoom on the last call tree at the end of the record.
    294        topTraces = [startIndex];
    295      } else {
    296        topTraces = topTraces.slice(topTracesStartIndex, topTracesEndIndex);
    297      }
    298 
    299      // When the top trace isn't the top most one (`!0`) and isn't a top trace (`!topTraces[0]`),
    300      // We need to add the current start trace as a top trace, as well as all its following siblings
    301      // and the following siblings of parent traces recursively.
    302      // This help show partial call tree when scrolling/zooming with a partial view on a call stack.
    303      //
    304      // Note that for endIndex, the cut is being done in VirtualizedTree's getChildren function.
    305      if (startIndex != 0 && topTraces[0] != startIndex) {
    306        const results = [];
    307        results.push(startIndex);
    308        collectAllSiblings(traceParents, traceChildren, startIndex, results);
    309        topTraces.unshift(...results);
    310      }
    311    }
    312 
    313    return React.createElement(VirtualizedTree, {
    314      itemHeight: TREE_NODE_HEIGHT,
    315      autoExpandDepth: 1,
    316      getRoots() {
    317        return topTraces;
    318      },
    319      getKey(traceIndex) {
    320        return `${traceIndex}`;
    321      },
    322      getParent(traceIndex) {
    323        return traceParents[traceIndex];
    324      },
    325      getChildren(traceIndex) {
    326        // When we aren't displaying all children up to the end of the record,
    327        // we may need to remove children that are outside of the viewport.
    328        if (endIndex != -1) {
    329          return traceChildren[traceIndex].filter(index => {
    330            return index < endIndex;
    331          });
    332        }
    333        return traceChildren[traceIndex];
    334      },
    335 
    336      isExpanded: traceIndex => {
    337        return this.state.expanded.has(traceIndex);
    338      },
    339      onExpand: traceIndex => {
    340        const { expanded } = this.state;
    341        expanded.add(traceIndex);
    342        this.setState({ expanded });
    343      },
    344      onCollapse: traceIndex => {
    345        const { expanded } = this.state;
    346        expanded.delete(traceIndex);
    347        this.setState({ expanded });
    348      },
    349 
    350      focused: selectedTraceIndex,
    351      onFocus: traceIndex => {
    352        this.props.selectTrace(traceIndex);
    353      },
    354 
    355      shown: selectedTraceIndex,
    356 
    357      renderItem: (traceIndex, _depth, isFocused, arrow, _isExpanded) => {
    358        const trace = allTraces[traceIndex];
    359        const type = trace[TRACER_FIELDS_INDEXES.TYPE];
    360 
    361        if (type == "event") {
    362          // Trace for DOM Events are always top level trace (and do not need margin/indent)
    363          const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME];
    364 
    365          const eventType = getEventClassNameFromTraceEventName(eventName);
    366          return div(
    367            {
    368              className: "trace-line",
    369            },
    370            arrow,
    371            div(
    372              {
    373                className: `tracer-dom-event ${eventType}${
    374                  selectedTraceIndex == traceIndex ? " selected" : ""
    375                }`,
    376 
    377                onDoubleClick: () => {
    378                  this.focusOnTrace(traceIndex);
    379                },
    380              },
    381              `DOM | ${eventName}`
    382            )
    383          );
    384        }
    385 
    386        if (type == "dom-mutation") {
    387          // Trace for DOM Mutations are always a leaf and don't have children.
    388          const mutationType = trace[TRACER_FIELDS_INDEXES.DOM_MUTATION_TYPE];
    389          return div(
    390            {
    391              className: `tracer-dom-mutation${
    392                selectedTraceIndex == trace ? " selected" : ""
    393              }`,
    394            },
    395            `DOM Mutation | ${mutationType}`
    396          );
    397        }
    398 
    399        if (type == "exit") {
    400          return null;
    401        }
    402 
    403        let className = "";
    404        if (selectedTraceIndex) {
    405          let idx = selectedTraceIndex;
    406          let onStack = false;
    407          while ((idx = traceParents[idx])) {
    408            if (idx == traceIndex) {
    409              onStack = true;
    410              break;
    411            }
    412          }
    413          if (onStack) {
    414            className += " onstack";
    415          }
    416        }
    417        const frameIndex = trace[TRACER_FIELDS_INDEXES.FRAME_INDEX];
    418        const frame = this.props.frames[frameIndex];
    419        return div(
    420          {
    421            className: "trace-line",
    422            onDoubleClick: () => {
    423              this.focusOnTrace(traceIndex);
    424            },
    425          },
    426          arrow,
    427          FrameView({
    428            className,
    429            showFunctionName: true,
    430            showAnonymousFunctionName: true,
    431            // Frame's savedFrameToLocation mess up with the frame object
    432            // by incrementing the column unexpectedly.
    433            frame: { ...frame, column: frame.column + 1 },
    434            sourceMapURLService: window.sourceMapURLService,
    435          })
    436        );
    437      },
    438    });
    439  }
    440 
    441  onSliderClick(event) {
    442    const { top, height } = this.refs.sliceSlider.getBoundingClientRect();
    443    const yInSlider = event.clientY - top;
    444    const mousePositionRatio = yInSlider / height;
    445 
    446    // Indexes and ratios are floating number whereas
    447    // we expect to pass an array index to `selectTrace`.
    448    const index = Math.round(
    449      this.state.startIndex + mousePositionRatio * this.state.renderedTraceCount
    450    );
    451 
    452    const { traceParents } = this.props;
    453    const parentIndex = getTraceParentIndex(traceParents, index);
    454    // Ignore the click if we clicked on a filtered out / not-rendered trace.
    455    // `topTraces` contains the visible top-most parent trace indexes.
    456    if (!this.props.topTraces.includes(parentIndex)) {
    457      return;
    458    }
    459 
    460    this.props.selectTrace(index);
    461  }
    462 
    463  onSliderWheel(event) {
    464    const direction = event.deltaY > 0 ? 1 : -1;
    465    const scrolledDelta = Math.abs(event.deltaY) * 0.01;
    466 
    467    let { startIndex, endIndex } = this.state;
    468 
    469    if (isMacOS ? event.metaKey : event.ctrlKey) {
    470      // Handle zooming it/out as we are either using CtrlOrMeta+Wheel or zooming via the touchpad
    471 
    472      // Compute the ratio (a percentage) of the position where the mouse or touch started zooming from
    473      const { top, height } = this.refs.sliceSlider.getBoundingClientRect();
    474      const yInSlider = event.clientY - top;
    475      const zoomOriginRatio = yInSlider / height;
    476 
    477      // Compute the number of indexes we should add or remove to both indexes
    478      const shift = Math.floor(
    479        Math.max(this.state.renderedTraceCount * scrolledDelta, 2) * direction
    480      );
    481 
    482      // Use the origin ratio in order to try to zoom where the cursor is
    483      // and distribute the shift between start and end according to its position.
    484      startIndex -= shift * zoomOriginRatio;
    485      if (endIndex == -1) {
    486        endIndex = this.props.traceCount + shift * (1 - zoomOriginRatio);
    487      } else {
    488        endIndex += shift * (1 - zoomOriginRatio);
    489      }
    490    } else {
    491      // Handle scrolling up/down as We are doing a simple scroll via wheel or touchpad
    492 
    493      // Avoid scrolling if we already at top or bottomn
    494      if (
    495        (direction < 0 && startIndex == 0) ||
    496        (direction > 0 && endIndex == -1)
    497      ) {
    498        return;
    499      }
    500 
    501      // Compute the number of indexes we should add or remove to both indexes
    502      const shift =
    503        Math.max(1, this.state.renderedTraceCount * scrolledDelta) * direction;
    504      startIndex += shift;
    505      if (endIndex == -1) {
    506        endIndex = this.props.traceCount + shift;
    507      } else {
    508        endIndex += shift;
    509      }
    510    }
    511 
    512    // Normalize the computed indexes.
    513    // start can't be lower than zero
    514    startIndex = Math.max(0, startIndex);
    515    // start can't be greater than the trace count
    516    startIndex = Math.min(startIndex, this.props.traceCount - 1);
    517 
    518    if (endIndex != -1) {
    519      // end can't be lower than start + 1
    520      endIndex = Math.max(startIndex + 1, endIndex);
    521      // end also can't be higher than the total number of traces
    522      if (endIndex >= this.props.traceCount) {
    523        // -1 means, there is no end filtering
    524        endIndex = -1;
    525      }
    526    }
    527 
    528    this.updateIndexes({
    529      startIndex,
    530      endIndex,
    531    });
    532  }
    533 
    534  updateIndexes({ startIndex, endIndex }, nextProps = this.props) {
    535    const renderedTraceCount =
    536      (endIndex == -1 ? nextProps.traceCount : endIndex) - startIndex;
    537    this.setState({
    538      startIndex,
    539      endIndex,
    540      renderedTraceCount,
    541    });
    542    if (this.tooltip) {
    543      this.tooltip.hide();
    544    }
    545  }
    546 
    547  focusOnTrace(traceIndex) {
    548    // Force selecting the call traces panel
    549    this.setState({ selectedTabIndex: 0 });
    550 
    551    const lastTraceIndex = findLastTraceIndex(
    552      this.props.traceChildren,
    553      traceIndex
    554    );
    555    this.updateIndexes({
    556      startIndex: traceIndex,
    557      endIndex: lastTraceIndex,
    558    });
    559  }
    560 
    561  resetZoom() {
    562    this.updateIndexes({
    563      startIndex: 0,
    564      endIndex: -1,
    565    });
    566  }
    567 
    568  tracePositionInPercent(traceIndex) {
    569    return Math.round(
    570      ((traceIndex - this.state.startIndex) / this.state.renderedTraceCount) *
    571        100
    572    );
    573  }
    574 
    575  renderMutationsInSlider() {
    576    const { mutationTraces, allTraces } = this.props;
    577    const { startIndex, endIndex } = this.state;
    578 
    579    const displayedMutationTraces = [];
    580    for (const traceIndex of mutationTraces) {
    581      if (
    582        traceIndex >= startIndex &&
    583        (endIndex == -1 || traceIndex <= endIndex)
    584      ) {
    585        displayedMutationTraces.push(traceIndex);
    586      }
    587    }
    588 
    589    return displayedMutationTraces.map(traceIndex => {
    590      const symbol = {
    591        add: "+",
    592        attributes: "=",
    593        remove: "-",
    594      };
    595      const trace = allTraces[traceIndex];
    596      const mutationType = trace[TRACER_FIELDS_INDEXES.DOM_MUTATION_TYPE];
    597      return div(
    598        {
    599          className: `tracer-slider-mutation`,
    600          "data-trace-index": traceIndex,
    601          style: {
    602            top: `${this.tracePositionInPercent(traceIndex)}%`,
    603          },
    604          onClick: event => {
    605            event.preventDefault();
    606            event.stopPropagation();
    607            this.props.selectTrace(traceIndex);
    608          },
    609        },
    610        symbol[mutationType]
    611      );
    612    });
    613  }
    614 
    615  renderEventsInSlider() {
    616    // When getting back to tracer sidebar after having moved to any other side panel, like source tree,
    617    // the timeline is null and would crash here.
    618    if (!this.refs.timeline) {
    619      return null;
    620    }
    621    const { topTraces, allTraces, traceChildren } = this.props;
    622    const { startIndex, endIndex } = this.state;
    623 
    624    // Compute only once the percentage value for 1px
    625    const onePixelPercent = 1 / this.refs.timeline.clientHeight;
    626 
    627    const displayedTraceEvents = [];
    628    for (const traceIndex of topTraces) {
    629      // Match the last event index in order to allow showing partial event
    630      // which may not be complete at the beginning of the record when we are zoomed.
    631      const lastTraceIndex = findLastTraceIndex(traceChildren, traceIndex);
    632      if (
    633        lastTraceIndex >= startIndex &&
    634        (endIndex == -1 || traceIndex <= endIndex)
    635      ) {
    636        displayedTraceEvents.push(traceIndex);
    637      }
    638    }
    639 
    640    return displayedTraceEvents.map(traceIndex => {
    641      const trace = allTraces[traceIndex];
    642      if (trace[TRACER_FIELDS_INDEXES.TYPE] != "event") {
    643        return null;
    644      }
    645 
    646      const eventPositionInPercent = this.tracePositionInPercent(traceIndex);
    647      const lastTraceIndex = findLastTraceIndex(traceChildren, traceIndex);
    648      const eventHeightInPercentFloat =
    649        ((lastTraceIndex - traceIndex) / this.state.renderedTraceCount) * 100;
    650      const eventHeightInPercent = Math.round(eventHeightInPercentFloat);
    651      const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME];
    652      const eventType = getEventClassNameFromTraceEventName(eventName);
    653 
    654      // Is it being highlighted when hovering a category of events or one specific event in the DOM events panel
    655      const highlighted = this.props.highlightedDomEvents.includes(eventName);
    656 
    657      // Give some hint to the CSS to know if the item is smaller than a pixel.
    658      // It will still be visible, but we can stop some expensive stylings.
    659      let sizeClass = "";
    660      if (eventHeightInPercent < onePixelPercent) {
    661        sizeClass = "size-subpixel";
    662      }
    663 
    664      return div({
    665        className: `tracer-slider-event ${eventType}${
    666          highlighted ? " highlighted" : ""
    667        } ${sizeClass}`,
    668        "data-trace-index": traceIndex,
    669        style: {
    670          top: `${eventPositionInPercent}%`,
    671          height: `${Math.max(
    672            Math.min(eventHeightInPercent, 100 - eventPositionInPercent),
    673            1
    674          )}%`,
    675        },
    676        onClick: event => {
    677          event.preventDefault();
    678          event.stopPropagation();
    679          this.props.selectTrace(traceIndex);
    680        },
    681        onDoubleClick: () => {
    682          this.focusOnTrace(traceIndex);
    683        },
    684      });
    685    });
    686  }
    687 
    688  renderVerticalSliders() {
    689    if (!this.props.traceCount) {
    690      // Always return the top element so that componentDidMount can register its wheel listener
    691      return div({
    692        className: "tracer-timeline hidden",
    693        ref: "timeline",
    694        onWheel: this.onSliderWheel,
    695      });
    696    }
    697 
    698    const { selectedTraceIndex } = this.props;
    699 
    700    const { startIndex, endIndex } = this.state;
    701 
    702    let selectedHighlightHeight;
    703    if (selectedTraceIndex > startIndex + this.state.renderedTraceCount) {
    704      selectedHighlightHeight = 100;
    705    } else if (selectedTraceIndex < startIndex) {
    706      selectedHighlightHeight = 0;
    707    } else {
    708      selectedHighlightHeight = this.tracePositionInPercent(selectedTraceIndex);
    709    }
    710 
    711    const classnames = [];
    712    if (startIndex > 0) {
    713      classnames.push("cut-start");
    714    }
    715    if (endIndex != -1) {
    716      classnames.push("cut-end");
    717    }
    718    if (selectedTraceIndex) {
    719      if (selectedTraceIndex < startIndex) {
    720        classnames.push("selected-before");
    721      } else if (endIndex != -1 && selectedTraceIndex > endIndex) {
    722        classnames.push("selected-after");
    723      }
    724    }
    725 
    726    const isZoomed = this.state.renderedTraceCount != this.props.traceCount;
    727    return div(
    728      {
    729        className: "tracer-timeline",
    730      },
    731      div(
    732        {
    733          className: `tracer-slider-box ${classnames.join(" ")}`,
    734          ref: "timeline",
    735          onWheel: this.onSliderWheel,
    736        },
    737        div(
    738          {
    739            className: "tracer-slice-slider ",
    740            ref: "sliceSlider",
    741            onClick: this.onSliderClick,
    742            style: {
    743              "--slider-bar-progress": `${selectedHighlightHeight}%`,
    744            },
    745          },
    746          selectedTraceIndex
    747            ? div({
    748                className: "tracer-slider-bar",
    749              })
    750            : null,
    751          selectedTraceIndex &&
    752            selectedTraceIndex >= startIndex &&
    753            selectedTraceIndex <= startIndex + this.state.renderedTraceCount
    754            ? div({
    755                className: "tracer-slider-position",
    756              })
    757            : null,
    758          this.renderEventsInSlider(),
    759          this.renderMutationsInSlider()
    760        )
    761      ),
    762      isZoomed
    763        ? button(
    764            {
    765              className: "tracer-reset-zoom",
    766              onClick: this.resetZoom,
    767            },
    768            "Reset zoom"
    769          )
    770        : null
    771    );
    772  }
    773 
    774  searchInputOnChange = e => {
    775    const searchString = e.target.value;
    776 
    777    // Throttle the calls to searchTraceArgument as that a costly operation
    778    this.throttledUpdateSearch(searchString);
    779  };
    780 
    781  throttledUpdateSearch(searchString) {
    782    this.props.searchTraceArguments(searchString);
    783  }
    784 
    785  /**
    786   * Select the next or previous trace according to the current search string
    787   *
    788   * @param {boolean} goForward
    789   *                  Select the next matching trace if true,
    790   *                  otherwise select the previous one.
    791   */
    792  selectNextMatchingTrace = goForward => {
    793    const { tracesMatchingSearch, allTraces } = this.props;
    794    const selectedTrace = allTraces[this.props.selectedTraceIndex];
    795    const currentIndexInMatchingArray =
    796      tracesMatchingSearch.indexOf(selectedTrace);
    797 
    798    let nextIndexInMatchingArray;
    799    if (goForward) {
    800      // If we aren't selecting any of the matching traces, or the last one,
    801      // select the first matching trace.
    802      if (
    803        currentIndexInMatchingArray == -1 ||
    804        currentIndexInMatchingArray == tracesMatchingSearch.length - 1
    805      ) {
    806        nextIndexInMatchingArray = 0;
    807      } else {
    808        nextIndexInMatchingArray = currentIndexInMatchingArray + 1;
    809      }
    810    } else if (
    811      currentIndexInMatchingArray == -1 ||
    812      currentIndexInMatchingArray == 0
    813    ) {
    814      nextIndexInMatchingArray = tracesMatchingSearch.length - 1;
    815    } else {
    816      nextIndexInMatchingArray = currentIndexInMatchingArray - 1;
    817    }
    818 
    819    // `selectTrace` expect a trace index (and not a trace object)
    820    const nextTraceIndex = allTraces.indexOf(
    821      tracesMatchingSearch[nextIndexInMatchingArray]
    822    );
    823 
    824    this.props.selectTrace(nextTraceIndex);
    825  };
    826 
    827  renderCallTreeSearchInput() {
    828    const { tracesMatchingSearch, searchExceptionMessage, searchValueOrGrip } =
    829      this.props;
    830    return [
    831      React.createElement(SearchInput, {
    832        count: tracesMatchingSearch.length,
    833 
    834        placeholder: this.props.traceValues
    835          ? `Search for function call argument values ("foo", 42, $0, $("canvas"), …)`
    836          : "Enable tracing values to search for values",
    837        disabled: !this.props.traceValues,
    838        size: "small",
    839        showClose: false,
    840        onChange: this.searchInputOnChange,
    841        onKeyDown: e => {
    842          if (e.key == "Enter") {
    843            // Shift key will reverse the selection direction
    844            this.selectNextMatchingTrace(!e.shiftKey);
    845          }
    846        },
    847        handlePrev: () => this.selectNextMatchingTrace(false),
    848        handleNext: () => this.selectNextMatchingTrace(true),
    849      }),
    850 
    851      // When this isn't a valid primitive type, we try to evaluate on the server
    852      // and show the exception, if one was thrown
    853      searchExceptionMessage
    854        ? div({ className: "search-exception" }, searchExceptionMessage)
    855        : null,
    856 
    857      // When we have a valid search string, either matching a primitive type or an object,
    858      // we display it here, alongside the number of matches
    859      this.props.traceValues && searchValueOrGrip != NO_SEARCH_VALUE
    860        ? div(
    861            { className: "search-value" },
    862            "Searching for:",
    863            Rep({
    864              object: searchValueOrGrip,
    865              mode: MODE.SHORT,
    866              onDOMNodeClick: () =>
    867                this.props.openElementInInspector(searchValueOrGrip),
    868              onInspectIconClick: () =>
    869                this.props.openElementInInspector(searchValueOrGrip),
    870              onDOMNodeMouseOver: () =>
    871                this.props.highlightDomElement(searchValueOrGrip),
    872              onDOMNodeMouseOut: () => this.props.unHighlightDomElement(),
    873            }),
    874            ` (${tracesMatchingSearch.length} match(es))`
    875          )
    876        : null,
    877    ];
    878  }
    879 
    880  render() {
    881    const { runtimeVersions } = this.props;
    882 
    883    return div(
    884      {
    885        className: "tracer-container",
    886        style: {
    887          "--tree-node-height": `${TREE_NODE_HEIGHT}px`,
    888        },
    889      },
    890      div(
    891        { className: "tracer-toolbar" },
    892        this.props.traceCount == 0
    893          ? div(
    894              {
    895                className: "tracer-experimental-notice",
    896              },
    897              "This panel is experimental. It may change, regress, be dropped or replaced."
    898            )
    899          : null,
    900        runtimeVersions &&
    901          runtimeVersions.localPlatformVersion !=
    902            runtimeVersions.remotePlatformVersion
    903          ? div(
    904              {
    905                className: "tracer-runtime-version-mismatch",
    906              },
    907              `Client and remote runtime have different versions (${runtimeVersions.localPlatformVersion} vs ${runtimeVersions.remotePlatformVersion}) . The Tracer may be broken because of protocol changes between these two versions. Please upgrade or downgrade one of the two to use the same major version.`
    908            )
    909          : null
    910      ),
    911      this.renderVerticalSliders(),
    912      React.createElement(
    913        Tabs,
    914        {
    915          activeTab: this.state.selectedTabIndex || 0,
    916          onAfterChange: index => {
    917            this.setState({ selectedTabIndex: index });
    918          },
    919        },
    920        React.createElement(
    921          TabPanel,
    922          {
    923            id: "tracer-traces",
    924            title: "Call Traces",
    925          },
    926          div(
    927            { className: "call-tree-container" },
    928            ...this.renderCallTreeSearchInput(),
    929            this.renderCallTree()
    930          )
    931        ),
    932        React.createElement(
    933          TabPanel,
    934          {
    935            id: "tracer-events",
    936            title: "DOM Events",
    937          },
    938          div(
    939            { className: "event-listeners-container" },
    940            React.createElement(EventListeners, {
    941              panelKey: "tracer",
    942            }),
    943            footer(
    944              null,
    945              `${
    946                isMacOS ? "Cmd" : "Ctrl"
    947              } + Click to select only one category or event`
    948            )
    949          )
    950        )
    951      )
    952    );
    953  }
    954 }
    955 
    956 /**
    957 * Walk through the call tree to find the very last children frame
    958 * and return its trace index.
    959 *
    960 * @param {object} traceChildren
    961 *                 The reducer data containing children trace indexes for all the traces.
    962 * @param {number} traceIndex
    963 */
    964 function findLastTraceIndex(traceChildren, traceIndex) {
    965  const children = traceChildren[traceIndex];
    966  if (!children.length) {
    967    return traceIndex;
    968  }
    969  return findLastTraceIndex(traceChildren, children.at(-1));
    970 }
    971 
    972 /**
    973 * Store in the `results` attribute all following siblings for a given trace,
    974 * as well as for its parents, that, recursively up to the top traces.
    975 *
    976 * @param {object} traceParents
    977 *                 The reducer data containing parent trace index for all the traces.
    978 * @param {object} traceChildren
    979 *                 The reducer data containing children trace indexes for all the traces.
    980 * @param {number} traceIndex
    981 * @param {Array} results
    982 */
    983 function collectAllSiblings(traceParents, traceChildren, traceIndex, results) {
    984  const parentIndex = traceParents[traceIndex];
    985  if (parentIndex != null) {
    986    const parentChildren = traceChildren[parentIndex];
    987    const indexInItsParent = parentChildren.indexOf(traceIndex);
    988    const siblingTraces = parentChildren.slice(indexInItsParent + 1);
    989    if (siblingTraces.length) {
    990      results.push(...siblingTraces);
    991    }
    992    collectAllSiblings(traceParents, traceChildren, parentIndex, results);
    993  }
    994 }
    995 
    996 /**
    997 * Given the TRACER_FIELDS_INDEXES.EVENT_NAME field of a trace,
    998 * return the classname to use for a given event trace.
    999 *
   1000 * @param {string} eventName
   1001 */
   1002 function getEventClassNameFromTraceEventName(eventName) {
   1003  let eventType = "other";
   1004  // Bug 1916755 should be using DOM Event categories instead of having such a custom mapping
   1005  if (
   1006    eventName.startsWith("global.mouse") ||
   1007    eventName.startsWith("global.click") ||
   1008    eventName.startsWith("node.mouse") ||
   1009    eventName.startsWith("node.click")
   1010  ) {
   1011    eventType = "mouse";
   1012  } else if (
   1013    eventName.startsWith("global.key") ||
   1014    eventName.startsWith("node.key")
   1015  ) {
   1016    eventType = "key";
   1017  }
   1018  return eventType;
   1019 }
   1020 
   1021 /**
   1022 * Return the index of the top-most parent frame for a given trace index.
   1023 *
   1024 * @param {object} traceParents
   1025 *                 The reducer data containing parent trace index for all the traces.
   1026 * @param {number} traceIndex
   1027 * @return {number} The top-most parent trace index
   1028 */
   1029 function getTraceParentIndex(traceParents, index) {
   1030  const parentIndex = traceParents[index];
   1031  if (parentIndex == undefined) {
   1032    return index;
   1033  }
   1034  return getTraceParentIndex(traceParents, parentIndex);
   1035 }
   1036 
   1037 const mapStateToProps = state => {
   1038  return {
   1039    isTracing: getIsCurrentlyTracing(state),
   1040    topTraces: getFilteredTopTraces(state),
   1041    allTraces: getAllTraces(state),
   1042    traceChildren: getTraceChildren(state),
   1043    traceParents: getTraceParents(state),
   1044    frames: getTraceFrames(state),
   1045    mutationTraces: getAllMutationTraces(state),
   1046    traceCount: getAllTraceCount(state),
   1047    selectedTraceIndex: getSelectedTraceIndex(state),
   1048    runtimeVersions: getRuntimeVersions(state),
   1049    highlightedDomEvents: getTraceHighlightedDomEvents(state),
   1050    tracesMatchingSearch: getTraceMatchingSearchTraces(state),
   1051    searchExceptionMessage: getTraceMatchingSearchException(state),
   1052    searchValueOrGrip: getTraceMatchingSearchValueOrGrip(state),
   1053    traceValues: getIsTracingValues(state),
   1054  };
   1055 };
   1056 
   1057 export default connect(mapStateToProps, {
   1058  selectTrace: actions.selectTrace,
   1059  searchTraceArguments: actions.searchTraceArguments,
   1060  openElementInInspector: actions.openElementInInspectorCommand,
   1061  highlightDomElement: actions.highlightDomElement,
   1062  unHighlightDomElement: actions.unHighlightDomElement,
   1063 })(Tracer);