tracer-frames.js (20513B)
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 const { 6 TRACER_FIELDS_INDEXES, 7 } = require("resource://devtools/server/actors/tracer.js"); 8 9 const lazy = {}; 10 ChromeUtils.defineESModuleGetters(lazy, { 11 BinarySearch: "resource://gre/modules/BinarySearch.sys.mjs", 12 }); 13 14 export const NO_SEARCH_VALUE = Symbol("no-search-value"); 15 16 function initialState(previousState = { searchValueOrGrip: NO_SEARCH_VALUE }) { 17 return { 18 // These fields are mutable as they are large arrays and UI will rerender based on their size 19 20 // The three next array are always of the same size. 21 // List of all trace resources, as defined by the server codebase (See the TracerActor) 22 mutableTraces: [], 23 // Array of arrays. This is of the same size as mutableTraces. 24 // Store the indexes within mutableTraces of each children matching the same index in mutableTraces. 25 mutableChildren: [], 26 // Indexes of parents within mutableTraces. 27 mutableParents: [], 28 29 // Frames are also a trace resources, but they are stored in a dedicated array. 30 mutableFrames: [], 31 32 // List of indexes within mutableTraces of top level trace, without any parent. 33 mutableTopTraces: [], 34 35 // Similar to mutableTopTraces except that filter out unwanted DOM Events. 36 mutableFilteredTopTraces: [], 37 38 // List of all trace resources indexes within mutableTraces which are about dom mutations 39 mutableMutationTraces: [], 40 41 // List of all traces matching the current search string 42 // (this isn't only top traces) 43 mutableMatchingTraces: [], 44 45 // If the user started searching for some value, it may be an invalid expression 46 // and the error related to this will be stored as a string in this attribute. 47 searchExceptionMessage: null, 48 49 // If a valid search has been requested, the actual value for this search is stored in this attribute. 50 // It can be either a primitive data type, or an object actor form (aka grip) 51 searchValueOrGrip: previousState.searchValueOrGrip, 52 53 // List of all event names which triggered some JavaScript code in the current tracer record. 54 mutableEventNames: new Set(), 55 56 // List of all possible DOM Events (similar to DOM Event panel) 57 // This is initialized once on debugger startup. 58 // This is a Map of category objects consumed by EventListeners React component, 59 // keyed by DOM Event name (string) communicated by the Tracer. 60 // DOM Event name can look like this: 61 // - global.click (click fired on window object) 62 // - node.mousemove (mousemove fired on a DOM Element) 63 // - xhr.error (error on an XMLHttpRequest object) 64 // - worker.error (error from a worker) 65 // - setTimeout (setTimeout function being called) 66 // - setTimeoutCallback (setTimeout callback being fired) 67 domEventInfoByTracerName: 68 previousState.domEventInfoByTracerName || new Map(), 69 70 // List of DOM Events "categories" currently available in the current traces 71 // Categories are objects consumed by the EventListener React component. 72 domEventCategories: [], 73 74 // List of DOM Events which should be show and be in `mutableFilteredTopTraces` 75 activeDomEvents: [], 76 77 // List of DOM Events names to be highlighted in the left timeline 78 highlightedDomEvents: [], 79 80 // Index of the currently selected trace within `mutableTraces`. 81 selectedTraceIndex: null, 82 83 // Updated alongside `selectedTraceIndex`, refer to the location of the selected trace. 84 selectedTraceLocation: null, 85 86 // Object like the one generated by `generateInlinePreview`, but for the currently selected trace 87 previews: null, 88 89 // Runtime versions to help show warning when there is a mismatch between frontend and backend versions 90 localPlatformVersion: null, 91 remotePlatformVersion: null, 92 93 // Is it currently recording trace *and* is collecting values 94 traceValues: false, 95 }; 96 } 97 98 // eslint-disable-next-line complexity 99 function update(state = initialState(), action) { 100 switch (action.type) { 101 case "SET_TRACE_SEARCH_EXCEPTION": { 102 return { 103 ...state, 104 searchExceptionMessage: action.errorMessage, 105 searchValueOrGrip: NO_SEARCH_VALUE, 106 mutableMatchingTraces: [], 107 }; 108 } 109 case "SET_TRACE_SEARCH_STRING": { 110 const { searchValueOrGrip } = action; 111 if (searchValueOrGrip === NO_SEARCH_VALUE) { 112 return { 113 ...state, 114 searchValueOrGrip, 115 searchExceptionMessage: null, 116 mutableMatchingTraces: [], 117 }; 118 } 119 const mutableMatchingTraces = []; 120 for (const trace of state.mutableTraces) { 121 const type = trace[TRACER_FIELDS_INDEXES.TYPE]; 122 if (type != "enter") { 123 continue; 124 } 125 if (isTraceMatchingSearch(trace, searchValueOrGrip)) { 126 mutableMatchingTraces.push(trace); 127 } 128 } 129 return { 130 ...state, 131 searchValueOrGrip, 132 mutableMatchingTraces, 133 searchExceptionMessage: null, 134 }; 135 } 136 case "TRACING_TOGGLED": { 137 if (action.enabled) { 138 state = initialState(state); 139 if (action.traceValues) { 140 state.traceValues = true; 141 } else { 142 state.searchValueOrGrip = NO_SEARCH_VALUE; 143 } 144 return state; 145 } 146 return state; 147 } 148 149 case "TRACING_CLEAR": { 150 return initialState(state); 151 } 152 153 case "ADD_TRACES": { 154 addTraces(state, action.traces); 155 return { ...state }; 156 } 157 158 case "SELECT_TRACE": { 159 const { traceIndex, location } = action; 160 if ( 161 traceIndex < 0 || 162 traceIndex >= state.mutableTraces.length || 163 traceIndex == state.selectedTraceIndex 164 ) { 165 return state; 166 } 167 168 const trace = state.mutableTraces[traceIndex]; 169 return { 170 ...state, 171 selectedTraceIndex: traceIndex, 172 selectedTraceLocation: location, 173 174 // Also compute the inline preview data when we select a trace 175 // and we have the values recording enabled. 176 previews: generatePreviewsForTrace(state, trace), 177 }; 178 } 179 180 case "SELECT_FRAME": 181 case "PAUSED": { 182 if (!state.previews && state.selectedTraceIndex == null) { 183 return state; 184 } 185 186 // Reset the selected trace and previews when we pause/step/select a frame in the scope panel, 187 // so that it is no longer highlighted, nor do we show inline variables. 188 return { 189 ...state, 190 selectedTraceIndex: null, 191 selectedTraceLocation: null, 192 previews: null, 193 }; 194 } 195 196 case "SET_SELECTED_LOCATION": { 197 // Traces are reference to the generated location only, so ignore any original source being selected 198 // and wait for SET_GENERATED_SELECTED_LOCATION instead. 199 if (action.location.source.isOriginal) { 200 return state; 201 } 202 203 // Ignore if the currently selected trace matches the new location. 204 if ( 205 state.selectedTrace && 206 locationMatchTrace(action.location, state.selectedTrace) 207 ) { 208 return state; 209 } 210 211 // Lookup for a trace matching the newly selected location 212 for (const trace of state.mutableTraces) { 213 if (locationMatchTrace(action.location, trace)) { 214 return { 215 ...state, 216 selectedTrace: trace, 217 }; 218 } 219 } 220 221 return { 222 ...state, 223 selectedTrace: null, 224 }; 225 } 226 227 case "SET_GENERATED_SELECTED_LOCATION": { 228 // When selecting an original location, we have to wait for the newly selected original location 229 // to be mapped to a generated location so that we can find a matching trace. 230 231 // Ignore if the currently selected trace matches the new location. 232 if ( 233 state.selectedTrace && 234 locationMatchTrace(action.generatedLocation, state.selectedTrace) 235 ) { 236 return state; 237 } 238 239 // Lookup for a trace matching the newly selected location 240 for (const trace of state.mutableTraces) { 241 if (locationMatchTrace(action.generatedLocation, trace)) { 242 return { 243 ...state, 244 selectedTrace: trace, 245 }; 246 } 247 } 248 249 return { 250 ...state, 251 selectedTrace: null, 252 }; 253 } 254 255 case "CLEAR_SELECTED_LOCATION": { 256 return { 257 ...state, 258 selectedTrace: null, 259 }; 260 } 261 case "SET_RUNTIME_VERSIONS": { 262 return { 263 ...state, 264 localPlatformVersion: action.localPlatformVersion, 265 remotePlatformVersion: action.remotePlatformVersion, 266 }; 267 } 268 269 case "RECEIVE_EVENT_LISTENER_TYPES": { 270 const domEventInfoByTracerName = new Map(); 271 for (const category of action.categories) { 272 for (const event of category.events) { 273 const value = { id: event.id, category, name: event.name }; 274 if (event.type == "event") { 275 for (const targetType of event.targetTypes) { 276 domEventInfoByTracerName.set( 277 `${targetType}.${event.eventType}`, 278 value 279 ); 280 } 281 } else { 282 domEventInfoByTracerName.set(event.notificationType, value); 283 } 284 } 285 } 286 return { ...state, domEventInfoByTracerName }; 287 } 288 289 case "UPDATE_EVENT_LISTENERS": { 290 // This action is also used for the DOM Event breakpoints panel 291 if (action.panelKey != "tracer") { 292 return state; 293 } 294 295 const { mutableTraces, mutableTopTraces } = state; 296 297 // If all the DOM events are shown, return the unfiltered list as-is. 298 if (action.active.length == state.mutableEventNames.size) { 299 return { 300 ...state, 301 mutableFilteredTopTraces: mutableTopTraces, 302 activeDomEvents: action.active, 303 }; 304 } 305 306 // Update `mutableFilteredTopTraces` by re-filtering all top traces from `mutableTopTraces` 307 // and considering the new list of DOM event names 308 const mutableFilteredTopTraces = []; 309 for (const traceIndex of mutableTopTraces) { 310 const trace = mutableTraces[traceIndex]; 311 const type = trace[TRACER_FIELDS_INDEXES.TYPE]; 312 if (type == "event") { 313 const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME]; 314 315 // Map JS Tracer event name into an Event Breakpoint's ID, as `action.active` is an array of such IDs. 316 // (from "node.click" to "event.mouse.click") 317 const id = 318 state.domEventInfoByTracerName.get(eventName)?.id || 319 `event.unclassified.${eventName}`; 320 321 if (action.active.includes(id)) { 322 mutableFilteredTopTraces.push(traceIndex); 323 } 324 } 325 } 326 return { 327 ...state, 328 mutableFilteredTopTraces, 329 activeDomEvents: action.active, 330 }; 331 } 332 333 case "HIGHLIGHT_EVENT_LISTENERS": { 334 // This action is also used for the DOM Event breakpoints panel 335 if (action.panelKey != "tracer") { 336 return state; 337 } 338 339 // Map ids (event.mouse.click) to event names (node.click) 340 const eventNames = []; 341 for (const [ 342 eventName, 343 { id }, 344 ] of state.domEventInfoByTracerName.entries()) { 345 if (action.eventIds.includes(id)) { 346 eventNames.push(eventName); 347 } 348 } 349 return { 350 ...state, 351 highlightedDomEvents: eventNames, 352 }; 353 } 354 355 case "SET_SELECTED_LOCACTION_TRACES": { 356 return { 357 ...state, 358 selectedLocationTraces: action.selectedLocationTraces, 359 }; 360 } 361 } 362 return state; 363 } 364 365 function addTraces(state, traces) { 366 const { 367 mutableTraces, 368 mutableMutationTraces, 369 mutableFrames, 370 mutableTopTraces, 371 mutableFilteredTopTraces, 372 mutableChildren, 373 mutableParents, 374 mutableMatchingTraces, 375 searchValueOrGrip, 376 } = state; 377 378 function matchParent(traceIndex, depth) { 379 // The very last element is the one matching traceIndex, 380 // so pick the one added just before. 381 // We consider that traces are reported by the server in the execution order. 382 let idx = mutableTraces.length - 2; 383 while (idx != null) { 384 const trace = mutableTraces[idx]; 385 if (!trace) { 386 break; 387 } 388 const currentDepth = trace[TRACER_FIELDS_INDEXES.DEPTH]; 389 if (currentDepth < depth) { 390 mutableChildren[idx].push(traceIndex); 391 mutableParents.push(idx); 392 return; 393 } 394 idx = mutableParents[idx]; 395 } 396 397 // If no parent was found, flag it as top level trace 398 mutableTopTraces.push(traceIndex); 399 mutableFilteredTopTraces.push(traceIndex); 400 mutableParents.push(null); 401 } 402 for (const traceResource of traces) { 403 // For now, only consider traces from the top level target/thread 404 if (!traceResource.targetFront.isTopLevel) { 405 continue; 406 } 407 408 const type = traceResource[TRACER_FIELDS_INDEXES.TYPE]; 409 410 switch (type) { 411 case "frame": { 412 // Store the object used by SmartTraces 413 mutableFrames.push({ 414 functionDisplayName: traceResource[TRACER_FIELDS_INDEXES.FRAME_NAME], 415 source: traceResource[TRACER_FIELDS_INDEXES.FRAME_URL], 416 sourceId: traceResource[TRACER_FIELDS_INDEXES.FRAME_SOURCEID], 417 line: traceResource[TRACER_FIELDS_INDEXES.FRAME_LINE], 418 column: traceResource[TRACER_FIELDS_INDEXES.FRAME_COLUMN], 419 }); 420 break; 421 } 422 423 case "enter": { 424 const traceIndex = mutableTraces.length; 425 mutableTraces.push(traceResource); 426 mutableChildren.push([]); 427 const depth = traceResource[TRACER_FIELDS_INDEXES.DEPTH]; 428 matchParent(traceIndex, depth); 429 430 if ( 431 searchValueOrGrip != NO_SEARCH_VALUE && 432 isTraceMatchingSearch(traceResource, searchValueOrGrip) 433 ) { 434 mutableMatchingTraces.push(traceResource); 435 } 436 break; 437 } 438 439 case "exit": { 440 // The sidebar doesn't use this information yet 441 break; 442 } 443 444 case "dom-mutation": { 445 const traceIndex = mutableTraces.length; 446 mutableTraces.push(traceResource); 447 mutableChildren.push([]); 448 mutableMutationTraces.push(traceIndex); 449 450 const depth = traceResource[TRACER_FIELDS_INDEXES.DEPTH]; 451 matchParent(traceIndex, depth); 452 break; 453 } 454 455 case "event": { 456 const traceIndex = mutableTraces.length; 457 mutableTraces.push(traceResource); 458 mutableChildren.push([]); 459 mutableParents.push(null); 460 mutableTopTraces.push(traceIndex); 461 462 const eventName = traceResource[TRACER_FIELDS_INDEXES.EVENT_NAME]; 463 registerDOMEvent(state, eventName); 464 465 // Map JS Tracer event name into an Event Breakpoint's ID, as `action.active` is an array of such IDs. 466 // (from "node.click" to "event.mouse.click") 467 const id = 468 state.domEventInfoByTracerName.get(eventName)?.id || 469 `event.unclassified.${eventName}`; 470 471 // Only register in the filtered list, if this event type isn't filtered out. 472 // (do this after `registerDOMEvent`, as that will populate `activeDomEvents` array. 473 if (state.activeDomEvents.includes(id)) { 474 mutableFilteredTopTraces.push(traceIndex); 475 } 476 break; 477 } 478 } 479 } 480 } 481 482 // EventListener's category for all events that are not breakable, and not returned by the thread actor, and not in `domEventInfoByTracerName`. 483 const UNCLASSIFIED_CATEGORY = { id: "unclassified", name: "Unclassified" }; 484 485 /** 486 * Register this possibly new event type in data set used to display EventListener React component. 487 * 488 * @param {object} state 489 * @param {string} eventName 490 */ 491 function registerDOMEvent(state, eventName) { 492 if (state.mutableEventNames.has(eventName)) { 493 return; 494 } 495 state.mutableEventNames.add(eventName); 496 497 // `domEventInfoByTracerName` is defined by the server and only register the events 498 // for which we can set breakpoints for. 499 // Fallback to a "unclassified" category for all these missing event types. 500 const { category, id, name } = state.domEventInfoByTracerName.get( 501 eventName 502 ) || { 503 category: UNCLASSIFIED_CATEGORY, 504 id: `event.unclassified.${eventName}`, 505 name: eventName, 506 }; 507 508 // By default, when we get a new event type, it is made visible 509 if (!state.activeDomEvents.includes(id)) { 510 state.activeDomEvents.push(id); 511 } 512 513 let newCategory = state.domEventCategories.find( 514 cat => cat.name == category.name 515 ); 516 if (!newCategory) { 517 // Create a new category with an empty event list 518 newCategory = { id: category.id, name: category.name, events: [] }; 519 state.domEventCategories = [...state.domEventCategories]; 520 addSortedCategoryOrEvent(state.domEventCategories, newCategory); 521 } 522 if (!newCategory.events.some(e => e.name == name)) { 523 // Register this new event in the category's event list 524 addSortedCategoryOrEvent(newCategory.events, { id, name }); 525 // Clone the root object to force a re-render of EventListeners React component 526 // Cloning newCategory(.events) wouldn't be enough as that's not returned by a mapStateToProps. 527 state.domEventCategories = [...state.domEventCategories]; 528 } 529 } 530 531 function addSortedCategoryOrEvent(array, newElement) { 532 const index = lazy.BinarySearch.insertionIndexOf( 533 function (a, b) { 534 // Both category and event are using `name` as display label 535 return a.name.localeCompare(b.name); 536 }, 537 array, 538 newElement 539 ); 540 array.splice(index, 0, newElement); 541 } 542 543 function locationMatchTrace(location, trace) { 544 return ( 545 trace.sourceId == location.sourceActor.id && 546 trace.lineNumber == location.line && 547 trace.columnNumber == location.column 548 ); 549 } 550 551 /** 552 * Reports if a given trace matches the current searched argument value. 553 * 554 * @param {object} trace 555 * The trace object communicated by the backend. 556 * @param {any primitive|ObjectActor's form} searchValueOrGrip 557 * Either a primitive value (string, number, boolean, …) to match directly, 558 * or, an object actor form where we could match the actor ID. 559 */ 560 function isTraceMatchingSearch(trace, searchValueOrGrip) { 561 const argumentValues = trace[TRACER_FIELDS_INDEXES.ENTER_ARGS]; 562 if (!argumentValues) { 563 return false; 564 } 565 if (searchValueOrGrip) { 566 const { actor } = searchValueOrGrip; 567 if (actor) { 568 return argumentValues.some(v => v.actor === searchValueOrGrip.actor); 569 } 570 } 571 // `null` and `undefined` aren't serialized as-is and have a special grip object 572 if (searchValueOrGrip === null) { 573 return argumentValues.some(v => v?.type == "null"); 574 } else if (searchValueOrGrip === undefined) { 575 return argumentValues.some(v => v?.type == "undefined"); 576 } 577 return argumentValues.some(v => v === searchValueOrGrip); 578 } 579 580 /** 581 * Generate the previews object consumed by InlinePreviews React component. 582 * 583 * @param {object} state 584 * @param {object} trace 585 * Trace reducer object. 586 * @return {object} 587 * Previews consumed by InlinePreviews. 588 */ 589 function generatePreviewsForTrace(state, trace) { 590 let previews = state.previews; 591 const argumentValues = trace[TRACER_FIELDS_INDEXES.ENTER_ARGS]; 592 const argumentNames = trace[TRACER_FIELDS_INDEXES.ENTER_ARG_NAMES]; 593 if (argumentNames && argumentValues) { 594 const frameIndex = trace[TRACER_FIELDS_INDEXES.FRAME_INDEX]; 595 const frame = state.mutableFrames[frameIndex]; 596 // CM6 are 1-based 597 const line = frame.line; 598 const column = frame.column; 599 600 const preview = []; 601 for (let i = 0; i < argumentNames.length; i++) { 602 const name = argumentNames[i]; 603 604 // Values are either primitives, or an Object Front 605 const objectGrip = argumentValues[i]?.getGrip 606 ? argumentValues[i]?.getGrip() 607 : argumentValues[i]; 608 609 preview.push({ 610 // All the argument will be show at the exact same spot. 611 // Ideally it would be nice to show them next to each argument, 612 // but the tracer currently expose the location of the first instruction 613 // in the function body. 614 line, 615 column, 616 617 // This attribute helps distinguish pause from trace previews 618 type: "trace", 619 name, 620 value: objectGrip, 621 }); 622 } 623 624 // This is the shape of data expected by InlinePreviews component 625 previews = { 626 [line]: preview, 627 }; 628 } 629 return previews; 630 } 631 632 export default update;