/* 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 . */ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import React, { Component, createFactory, } from "devtools/client/shared/vendor/react"; import { div, button, footer, } from "devtools/client/shared/vendor/react-dom-factories"; import SearchInput from "../shared/SearchInput"; import EventListeners from "../shared/EventListeners"; import { connect } from "devtools/client/shared/vendor/react-redux"; import { getSelectedTraceIndex, getFilteredTopTraces, getAllTraces, getTraceChildren, getTraceParents, getTraceFrames, getAllMutationTraces, getAllTraceCount, getIsCurrentlyTracing, getRuntimeVersions, getTraceHighlightedDomEvents, getTraceMatchingSearchTraces, getTraceMatchingSearchException, getTraceMatchingSearchValueOrGrip, getIsTracingValues, } from "../../selectors/index"; import { NO_SEARCH_VALUE } from "../../reducers/tracer-frames"; const { throttle } = require("resource://devtools/shared/throttle.js"); const VirtualizedTree = require("resource://devtools/client/shared/components/VirtualizedTree.js"); const FrameView = createFactory( require("resource://devtools/client/shared/components/Frame.js") ); const Reps = ChromeUtils.importESModule( "resource://devtools/client/shared/components/reps/index.mjs" ); const { REPS: { Rep }, MODE, } = Reps; const { TRACER_FIELDS_INDEXES, } = require("resource://devtools/server/actors/tracer.js"); const { HTMLTooltip, } = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js"); const { TabPanel, Tabs } = ChromeUtils.importESModule( "resource://devtools/client/shared/components/tabs/Tabs.mjs", { global: "current" } ); import actions from "../../actions/index"; const isMacOS = AppConstants.platform == "macosx"; const TREE_NODE_HEIGHT = 20; const DEBUG = false; export class Tracer extends Component { constructor(props) { super(props); this.state = { // List of expanded traces in the tree expanded: new Set(), // First visible trace's index. // Note that these two indexes aren't related to the VirtualizedTree viewport. // That's the possibly visible traces when scrolling top/bottom of the whole Tree. startIndex: 0, // Last visible trace's index. -1 is we should show all of them at the end. // As soon as we start scrolling via the left slider, new traces are added outside of the selected viewport. endIndex: -1, // Number of trace rendered in the timeline and the tree (considering the tree is expanded, less may be displayed based on collapsing) renderedTraceCount: 0, // Index of the currently selected tab (traces or events) selectedTabIndex: 0, }; this.onSliderClick = this.onSliderClick.bind(this); this.onSliderWheel = this.onSliderWheel.bind(this); this.resetZoom = this.resetZoom.bind(this); // Throttle requests to update the trace argument search query as it is a costly operation this.throttledUpdateSearch = throttle( this.throttledUpdateSearch.bind(this), 250 ); } UNSAFE_componentWillReceiveProps(nextProps) { const { traceParents } = this.props; if ( nextProps.selectedTraceIndex != this.props.selectedTraceIndex && nextProps.selectedTraceIndex != null ) { const { expanded } = this.state; let index = traceParents[nextProps.selectedTraceIndex]; while (index) { expanded.add(index); index = traceParents[index]; } this.setState({ expanded }); } // Force update the renderedTraceCount when we receive new traces if (nextProps.traceCount != this.props.traceCount) { if (nextProps.traceCount == 0) { // Reset the indexes when the view is cleared (i.e. when we just started recording a new trace) this.updateIndexes( { startIndex: 0, endIndex: -1, }, nextProps ); } else { this.updateIndexes( { startIndex: this.state.startIndex, endIndex: this.state.endIndex, }, nextProps ); } } } componentDidMount() { // Prevent toolbox zooming when using Ctrl+Wheel on the slider. // (for some reason... React doesn't seem to register the right "wheel", event listener via `onWheel`, // which is the one to cancel to prevent toolbox zooming code to work.) this.refs.timeline.onwheel = e => e.preventDefault(); if (!this.tooltip) { this.instantiateTooltip(); } // Force updating indexes when we navigate back to the tracer sidebar. // For example, when we navigated away to the source tree. if (!this.state.renderedTraceCount) { this.updateIndexes( { startIndex: this.state.startIndex, endIndex: this.state.endIndex, }, this.props ); } } instantiateTooltip() { this.tooltip = new HTMLTooltip(this.refs.timeline.ownerDocument, { className: "event-tooltip", type: "arrow", // Avoid consuming the first click on the anchored UI element in the slider consumeOutsideClicks: false, }); this.tooltip.setContentSize({ height: "auto" }); this.tooltip.startTogglingOnHover(this.refs.timeline, (target, tooltip) => { if (target.classList.contains("tracer-slider-event")) { const { traceIndex } = target.dataset; const trace = this.props.allTraces[traceIndex]; const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME]; const eventType = getEventClassNameFromTraceEventName(eventName); tooltip.panel.innerHTML = ""; const el = document.createElement("div"); el.classList.add("tracer-dom-event", eventType); el.textContent = `DOM | ${eventName}`; tooltip.panel.append( el, document.createElement("hr"), document.createTextNode( "Double click to focus on the executions related to this event." ) ); return true; } else if (target.classList.contains("tracer-slider-mutation")) { const { traceIndex } = target.dataset; const trace = this.props.allTraces[traceIndex]; const mutationType = trace[TRACER_FIELDS_INDEXES.DOM_MUTATION_TYPE]; tooltip.panel.innerHTML = ""; const el = document.createElement("div"); el.classList.add("tracer-dom-mutation"); el.textContent = `DOM Mutation | ${mutationType}`; tooltip.panel.append( el, document.createElement("hr"), document.createTextNode( "Click to find the call tree leading to this mutation." ) ); return true; } return false; }); } componentDidUpdate() { if (DEBUG) { dump( ` # start: ${this.state.startIndex} end: ${this.state.endIndex} rendered: ${this.state.renderedTraceCount} traceCount:${this.props.traceCount}\n` ); } } renderCallTree() { let { selectedTraceIndex, topTraces, allTraces, traceChildren, traceParents, } = this.props; // Print warning message when there is no trace recorded yet if (!allTraces.length) { if (!this.props.isTracing) { // We can't yet distinguish being completely off or pending for next interaction or load return div( { className: "tracer-message" }, "Tracer is off, or pending for next interaction/load." ); } return div( { className: "tracer-message" }, "Waiting for the first JavaScript executions" ); } // If there is some traces in allTraces but none in topTraces, // it means that they were all filtered out. if (!topTraces.length) { // Use distinct message when we are showing only a slice of the record, of the whole record if (this.state.renderedTraceCount != this.props.traceCount) { return div( { className: "tracer-message" }, "All traces have been filtered out, in the slice of the record" ); } return div( { className: "tracer-message" }, "All traces have been filtered out" ); } // Indexes are floating number, so convert them to a decimal number as indexes in the trace array let { startIndex, endIndex } = this.state; startIndex = Math.floor(startIndex); endIndex = Math.floor(endIndex); if (startIndex != 0 || endIndex != -1) { // When we start zooming, only consider traces whose top level frame // is in the zoomed section. // Lookup for the first top trace after the start index let topTracesStartIndex = 0; if (startIndex != 0) { topTracesStartIndex = -1; for (let i = 0; i < topTraces.length; i++) { const traceIndex = topTraces[i]; if (traceIndex >= startIndex) { topTracesStartIndex = i; break; } } } // Lookup for the first top trace from the end before the end index let topTracesEndIndex = topTraces.length; if (endIndex != -1) { for (let i = topTraces.length; i >= 0; i--) { const traceIndex = topTraces[i]; if (traceIndex <= endIndex) { topTracesEndIndex = i + 1; break; } } } if (topTracesStartIndex == -1) { // When none of the top traces are within the selected range, pick the start index of top trace. // This happens when we zoom on the last call tree at the end of the record. topTraces = [startIndex]; } else { topTraces = topTraces.slice(topTracesStartIndex, topTracesEndIndex); } // When the top trace isn't the top most one (`!0`) and isn't a top trace (`!topTraces[0]`), // We need to add the current start trace as a top trace, as well as all its following siblings // and the following siblings of parent traces recursively. // This help show partial call tree when scrolling/zooming with a partial view on a call stack. // // Note that for endIndex, the cut is being done in VirtualizedTree's getChildren function. if (startIndex != 0 && topTraces[0] != startIndex) { const results = []; results.push(startIndex); collectAllSiblings(traceParents, traceChildren, startIndex, results); topTraces.unshift(...results); } } return React.createElement(VirtualizedTree, { itemHeight: TREE_NODE_HEIGHT, autoExpandDepth: 1, getRoots() { return topTraces; }, getKey(traceIndex) { return `${traceIndex}`; }, getParent(traceIndex) { return traceParents[traceIndex]; }, getChildren(traceIndex) { // When we aren't displaying all children up to the end of the record, // we may need to remove children that are outside of the viewport. if (endIndex != -1) { return traceChildren[traceIndex].filter(index => { return index < endIndex; }); } return traceChildren[traceIndex]; }, isExpanded: traceIndex => { return this.state.expanded.has(traceIndex); }, onExpand: traceIndex => { const { expanded } = this.state; expanded.add(traceIndex); this.setState({ expanded }); }, onCollapse: traceIndex => { const { expanded } = this.state; expanded.delete(traceIndex); this.setState({ expanded }); }, focused: selectedTraceIndex, onFocus: traceIndex => { this.props.selectTrace(traceIndex); }, shown: selectedTraceIndex, renderItem: (traceIndex, _depth, isFocused, arrow, _isExpanded) => { const trace = allTraces[traceIndex]; const type = trace[TRACER_FIELDS_INDEXES.TYPE]; if (type == "event") { // Trace for DOM Events are always top level trace (and do not need margin/indent) const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME]; const eventType = getEventClassNameFromTraceEventName(eventName); return div( { className: "trace-line", }, arrow, div( { className: `tracer-dom-event ${eventType}${ selectedTraceIndex == traceIndex ? " selected" : "" }`, onDoubleClick: () => { this.focusOnTrace(traceIndex); }, }, `DOM | ${eventName}` ) ); } if (type == "dom-mutation") { // Trace for DOM Mutations are always a leaf and don't have children. const mutationType = trace[TRACER_FIELDS_INDEXES.DOM_MUTATION_TYPE]; return div( { className: `tracer-dom-mutation${ selectedTraceIndex == trace ? " selected" : "" }`, }, `DOM Mutation | ${mutationType}` ); } if (type == "exit") { return null; } let className = ""; if (selectedTraceIndex) { let idx = selectedTraceIndex; let onStack = false; while ((idx = traceParents[idx])) { if (idx == traceIndex) { onStack = true; break; } } if (onStack) { className += " onstack"; } } const frameIndex = trace[TRACER_FIELDS_INDEXES.FRAME_INDEX]; const frame = this.props.frames[frameIndex]; return div( { className: "trace-line", onDoubleClick: () => { this.focusOnTrace(traceIndex); }, }, arrow, FrameView({ className, showFunctionName: true, showAnonymousFunctionName: true, // Frame's savedFrameToLocation mess up with the frame object // by incrementing the column unexpectedly. frame: { ...frame, column: frame.column + 1 }, sourceMapURLService: window.sourceMapURLService, }) ); }, }); } onSliderClick(event) { const { top, height } = this.refs.sliceSlider.getBoundingClientRect(); const yInSlider = event.clientY - top; const mousePositionRatio = yInSlider / height; // Indexes and ratios are floating number whereas // we expect to pass an array index to `selectTrace`. const index = Math.round( this.state.startIndex + mousePositionRatio * this.state.renderedTraceCount ); const { traceParents } = this.props; const parentIndex = getTraceParentIndex(traceParents, index); // Ignore the click if we clicked on a filtered out / not-rendered trace. // `topTraces` contains the visible top-most parent trace indexes. if (!this.props.topTraces.includes(parentIndex)) { return; } this.props.selectTrace(index); } onSliderWheel(event) { const direction = event.deltaY > 0 ? 1 : -1; const scrolledDelta = Math.abs(event.deltaY) * 0.01; let { startIndex, endIndex } = this.state; if (isMacOS ? event.metaKey : event.ctrlKey) { // Handle zooming it/out as we are either using CtrlOrMeta+Wheel or zooming via the touchpad // Compute the ratio (a percentage) of the position where the mouse or touch started zooming from const { top, height } = this.refs.sliceSlider.getBoundingClientRect(); const yInSlider = event.clientY - top; const zoomOriginRatio = yInSlider / height; // Compute the number of indexes we should add or remove to both indexes const shift = Math.floor( Math.max(this.state.renderedTraceCount * scrolledDelta, 2) * direction ); // Use the origin ratio in order to try to zoom where the cursor is // and distribute the shift between start and end according to its position. startIndex -= shift * zoomOriginRatio; if (endIndex == -1) { endIndex = this.props.traceCount + shift * (1 - zoomOriginRatio); } else { endIndex += shift * (1 - zoomOriginRatio); } } else { // Handle scrolling up/down as We are doing a simple scroll via wheel or touchpad // Avoid scrolling if we already at top or bottomn if ( (direction < 0 && startIndex == 0) || (direction > 0 && endIndex == -1) ) { return; } // Compute the number of indexes we should add or remove to both indexes const shift = Math.max(1, this.state.renderedTraceCount * scrolledDelta) * direction; startIndex += shift; if (endIndex == -1) { endIndex = this.props.traceCount + shift; } else { endIndex += shift; } } // Normalize the computed indexes. // start can't be lower than zero startIndex = Math.max(0, startIndex); // start can't be greater than the trace count startIndex = Math.min(startIndex, this.props.traceCount - 1); if (endIndex != -1) { // end can't be lower than start + 1 endIndex = Math.max(startIndex + 1, endIndex); // end also can't be higher than the total number of traces if (endIndex >= this.props.traceCount) { // -1 means, there is no end filtering endIndex = -1; } } this.updateIndexes({ startIndex, endIndex, }); } updateIndexes({ startIndex, endIndex }, nextProps = this.props) { const renderedTraceCount = (endIndex == -1 ? nextProps.traceCount : endIndex) - startIndex; this.setState({ startIndex, endIndex, renderedTraceCount, }); if (this.tooltip) { this.tooltip.hide(); } } focusOnTrace(traceIndex) { // Force selecting the call traces panel this.setState({ selectedTabIndex: 0 }); const lastTraceIndex = findLastTraceIndex( this.props.traceChildren, traceIndex ); this.updateIndexes({ startIndex: traceIndex, endIndex: lastTraceIndex, }); } resetZoom() { this.updateIndexes({ startIndex: 0, endIndex: -1, }); } tracePositionInPercent(traceIndex) { return Math.round( ((traceIndex - this.state.startIndex) / this.state.renderedTraceCount) * 100 ); } renderMutationsInSlider() { const { mutationTraces, allTraces } = this.props; const { startIndex, endIndex } = this.state; const displayedMutationTraces = []; for (const traceIndex of mutationTraces) { if ( traceIndex >= startIndex && (endIndex == -1 || traceIndex <= endIndex) ) { displayedMutationTraces.push(traceIndex); } } return displayedMutationTraces.map(traceIndex => { const symbol = { add: "+", attributes: "=", remove: "-", }; const trace = allTraces[traceIndex]; const mutationType = trace[TRACER_FIELDS_INDEXES.DOM_MUTATION_TYPE]; return div( { className: `tracer-slider-mutation`, "data-trace-index": traceIndex, style: { top: `${this.tracePositionInPercent(traceIndex)}%`, }, onClick: event => { event.preventDefault(); event.stopPropagation(); this.props.selectTrace(traceIndex); }, }, symbol[mutationType] ); }); } renderEventsInSlider() { // When getting back to tracer sidebar after having moved to any other side panel, like source tree, // the timeline is null and would crash here. if (!this.refs.timeline) { return null; } const { topTraces, allTraces, traceChildren } = this.props; const { startIndex, endIndex } = this.state; // Compute only once the percentage value for 1px const onePixelPercent = 1 / this.refs.timeline.clientHeight; const displayedTraceEvents = []; for (const traceIndex of topTraces) { // Match the last event index in order to allow showing partial event // which may not be complete at the beginning of the record when we are zoomed. const lastTraceIndex = findLastTraceIndex(traceChildren, traceIndex); if ( lastTraceIndex >= startIndex && (endIndex == -1 || traceIndex <= endIndex) ) { displayedTraceEvents.push(traceIndex); } } return displayedTraceEvents.map(traceIndex => { const trace = allTraces[traceIndex]; if (trace[TRACER_FIELDS_INDEXES.TYPE] != "event") { return null; } const eventPositionInPercent = this.tracePositionInPercent(traceIndex); const lastTraceIndex = findLastTraceIndex(traceChildren, traceIndex); const eventHeightInPercentFloat = ((lastTraceIndex - traceIndex) / this.state.renderedTraceCount) * 100; const eventHeightInPercent = Math.round(eventHeightInPercentFloat); const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME]; const eventType = getEventClassNameFromTraceEventName(eventName); // Is it being highlighted when hovering a category of events or one specific event in the DOM events panel const highlighted = this.props.highlightedDomEvents.includes(eventName); // Give some hint to the CSS to know if the item is smaller than a pixel. // It will still be visible, but we can stop some expensive stylings. let sizeClass = ""; if (eventHeightInPercent < onePixelPercent) { sizeClass = "size-subpixel"; } return div({ className: `tracer-slider-event ${eventType}${ highlighted ? " highlighted" : "" } ${sizeClass}`, "data-trace-index": traceIndex, style: { top: `${eventPositionInPercent}%`, height: `${Math.max( Math.min(eventHeightInPercent, 100 - eventPositionInPercent), 1 )}%`, }, onClick: event => { event.preventDefault(); event.stopPropagation(); this.props.selectTrace(traceIndex); }, onDoubleClick: () => { this.focusOnTrace(traceIndex); }, }); }); } renderVerticalSliders() { if (!this.props.traceCount) { // Always return the top element so that componentDidMount can register its wheel listener return div({ className: "tracer-timeline hidden", ref: "timeline", onWheel: this.onSliderWheel, }); } const { selectedTraceIndex } = this.props; const { startIndex, endIndex } = this.state; let selectedHighlightHeight; if (selectedTraceIndex > startIndex + this.state.renderedTraceCount) { selectedHighlightHeight = 100; } else if (selectedTraceIndex < startIndex) { selectedHighlightHeight = 0; } else { selectedHighlightHeight = this.tracePositionInPercent(selectedTraceIndex); } const classnames = []; if (startIndex > 0) { classnames.push("cut-start"); } if (endIndex != -1) { classnames.push("cut-end"); } if (selectedTraceIndex) { if (selectedTraceIndex < startIndex) { classnames.push("selected-before"); } else if (endIndex != -1 && selectedTraceIndex > endIndex) { classnames.push("selected-after"); } } const isZoomed = this.state.renderedTraceCount != this.props.traceCount; return div( { className: "tracer-timeline", }, div( { className: `tracer-slider-box ${classnames.join(" ")}`, ref: "timeline", onWheel: this.onSliderWheel, }, div( { className: "tracer-slice-slider ", ref: "sliceSlider", onClick: this.onSliderClick, style: { "--slider-bar-progress": `${selectedHighlightHeight}%`, }, }, selectedTraceIndex ? div({ className: "tracer-slider-bar", }) : null, selectedTraceIndex && selectedTraceIndex >= startIndex && selectedTraceIndex <= startIndex + this.state.renderedTraceCount ? div({ className: "tracer-slider-position", }) : null, this.renderEventsInSlider(), this.renderMutationsInSlider() ) ), isZoomed ? button( { className: "tracer-reset-zoom", onClick: this.resetZoom, }, "Reset zoom" ) : null ); } searchInputOnChange = e => { const searchString = e.target.value; // Throttle the calls to searchTraceArgument as that a costly operation this.throttledUpdateSearch(searchString); }; throttledUpdateSearch(searchString) { this.props.searchTraceArguments(searchString); } /** * Select the next or previous trace according to the current search string * * @param {boolean} goForward * Select the next matching trace if true, * otherwise select the previous one. */ selectNextMatchingTrace = goForward => { const { tracesMatchingSearch, allTraces } = this.props; const selectedTrace = allTraces[this.props.selectedTraceIndex]; const currentIndexInMatchingArray = tracesMatchingSearch.indexOf(selectedTrace); let nextIndexInMatchingArray; if (goForward) { // If we aren't selecting any of the matching traces, or the last one, // select the first matching trace. if ( currentIndexInMatchingArray == -1 || currentIndexInMatchingArray == tracesMatchingSearch.length - 1 ) { nextIndexInMatchingArray = 0; } else { nextIndexInMatchingArray = currentIndexInMatchingArray + 1; } } else if ( currentIndexInMatchingArray == -1 || currentIndexInMatchingArray == 0 ) { nextIndexInMatchingArray = tracesMatchingSearch.length - 1; } else { nextIndexInMatchingArray = currentIndexInMatchingArray - 1; } // `selectTrace` expect a trace index (and not a trace object) const nextTraceIndex = allTraces.indexOf( tracesMatchingSearch[nextIndexInMatchingArray] ); this.props.selectTrace(nextTraceIndex); }; renderCallTreeSearchInput() { const { tracesMatchingSearch, searchExceptionMessage, searchValueOrGrip } = this.props; return [ React.createElement(SearchInput, { count: tracesMatchingSearch.length, placeholder: this.props.traceValues ? `Search for function call argument values ("foo", 42, $0, $("canvas"), …)` : "Enable tracing values to search for values", disabled: !this.props.traceValues, size: "small", showClose: false, onChange: this.searchInputOnChange, onKeyDown: e => { if (e.key == "Enter") { // Shift key will reverse the selection direction this.selectNextMatchingTrace(!e.shiftKey); } }, handlePrev: () => this.selectNextMatchingTrace(false), handleNext: () => this.selectNextMatchingTrace(true), }), // When this isn't a valid primitive type, we try to evaluate on the server // and show the exception, if one was thrown searchExceptionMessage ? div({ className: "search-exception" }, searchExceptionMessage) : null, // When we have a valid search string, either matching a primitive type or an object, // we display it here, alongside the number of matches this.props.traceValues && searchValueOrGrip != NO_SEARCH_VALUE ? div( { className: "search-value" }, "Searching for:", Rep({ object: searchValueOrGrip, mode: MODE.SHORT, onDOMNodeClick: () => this.props.openElementInInspector(searchValueOrGrip), onInspectIconClick: () => this.props.openElementInInspector(searchValueOrGrip), onDOMNodeMouseOver: () => this.props.highlightDomElement(searchValueOrGrip), onDOMNodeMouseOut: () => this.props.unHighlightDomElement(), }), ` (${tracesMatchingSearch.length} match(es))` ) : null, ]; } render() { const { runtimeVersions } = this.props; return div( { className: "tracer-container", style: { "--tree-node-height": `${TREE_NODE_HEIGHT}px`, }, }, div( { className: "tracer-toolbar" }, this.props.traceCount == 0 ? div( { className: "tracer-experimental-notice", }, "This panel is experimental. It may change, regress, be dropped or replaced." ) : null, runtimeVersions && runtimeVersions.localPlatformVersion != runtimeVersions.remotePlatformVersion ? div( { className: "tracer-runtime-version-mismatch", }, `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.` ) : null ), this.renderVerticalSliders(), React.createElement( Tabs, { activeTab: this.state.selectedTabIndex || 0, onAfterChange: index => { this.setState({ selectedTabIndex: index }); }, }, React.createElement( TabPanel, { id: "tracer-traces", title: "Call Traces", }, div( { className: "call-tree-container" }, ...this.renderCallTreeSearchInput(), this.renderCallTree() ) ), React.createElement( TabPanel, { id: "tracer-events", title: "DOM Events", }, div( { className: "event-listeners-container" }, React.createElement(EventListeners, { panelKey: "tracer", }), footer( null, `${ isMacOS ? "Cmd" : "Ctrl" } + Click to select only one category or event` ) ) ) ) ); } } /** * Walk through the call tree to find the very last children frame * and return its trace index. * * @param {object} traceChildren * The reducer data containing children trace indexes for all the traces. * @param {number} traceIndex */ function findLastTraceIndex(traceChildren, traceIndex) { const children = traceChildren[traceIndex]; if (!children.length) { return traceIndex; } return findLastTraceIndex(traceChildren, children.at(-1)); } /** * Store in the `results` attribute all following siblings for a given trace, * as well as for its parents, that, recursively up to the top traces. * * @param {object} traceParents * The reducer data containing parent trace index for all the traces. * @param {object} traceChildren * The reducer data containing children trace indexes for all the traces. * @param {number} traceIndex * @param {Array} results */ function collectAllSiblings(traceParents, traceChildren, traceIndex, results) { const parentIndex = traceParents[traceIndex]; if (parentIndex != null) { const parentChildren = traceChildren[parentIndex]; const indexInItsParent = parentChildren.indexOf(traceIndex); const siblingTraces = parentChildren.slice(indexInItsParent + 1); if (siblingTraces.length) { results.push(...siblingTraces); } collectAllSiblings(traceParents, traceChildren, parentIndex, results); } } /** * Given the TRACER_FIELDS_INDEXES.EVENT_NAME field of a trace, * return the classname to use for a given event trace. * * @param {string} eventName */ function getEventClassNameFromTraceEventName(eventName) { let eventType = "other"; // Bug 1916755 should be using DOM Event categories instead of having such a custom mapping if ( eventName.startsWith("global.mouse") || eventName.startsWith("global.click") || eventName.startsWith("node.mouse") || eventName.startsWith("node.click") ) { eventType = "mouse"; } else if ( eventName.startsWith("global.key") || eventName.startsWith("node.key") ) { eventType = "key"; } return eventType; } /** * Return the index of the top-most parent frame for a given trace index. * * @param {object} traceParents * The reducer data containing parent trace index for all the traces. * @param {number} traceIndex * @return {number} The top-most parent trace index */ function getTraceParentIndex(traceParents, index) { const parentIndex = traceParents[index]; if (parentIndex == undefined) { return index; } return getTraceParentIndex(traceParents, parentIndex); } const mapStateToProps = state => { return { isTracing: getIsCurrentlyTracing(state), topTraces: getFilteredTopTraces(state), allTraces: getAllTraces(state), traceChildren: getTraceChildren(state), traceParents: getTraceParents(state), frames: getTraceFrames(state), mutationTraces: getAllMutationTraces(state), traceCount: getAllTraceCount(state), selectedTraceIndex: getSelectedTraceIndex(state), runtimeVersions: getRuntimeVersions(state), highlightedDomEvents: getTraceHighlightedDomEvents(state), tracesMatchingSearch: getTraceMatchingSearchTraces(state), searchExceptionMessage: getTraceMatchingSearchException(state), searchValueOrGrip: getTraceMatchingSearchValueOrGrip(state), traceValues: getIsTracingValues(state), }; }; export default connect(mapStateToProps, { selectTrace: actions.selectTrace, searchTraceArguments: actions.searchTraceArguments, openElementInInspector: actions.openElementInInspectorCommand, highlightDomElement: actions.highlightDomElement, unHighlightDomElement: actions.unHighlightDomElement, })(Tracer);