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