tor-browser

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

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;