static _computeRecursiveAttributableURLs(task, parentURLs, allURLsInTree, priorTaskData) {
    const args = task.event.args;
    const argsData = {...(args.beginData || {}), ...(args.data || {})};
    const frame = argsData.frame || '';
    let frameURL = priorTaskData.frameURLsById.get(frame);
    const stackFrameURLs = (argsData.stackTrace || []).map(entry => entry.url);

    // If the frame was an `about:blank` style ad frame, the first real URL will be more relevant to the frame's URL.
    const potentialFrameURL = stackFrameURLs[0];
    if (frame && frameURL && frameURL.startsWith('about:') && potentialFrameURL) {
      priorTaskData.frameURLsById.set(frame, potentialFrameURL);
      frameURL = potentialFrameURL;
    }

    /** @type {Array<string|undefined>} */
    let taskURLs = [];
    switch (task.event.name) {
      /**
       * Some trace events reference a specific script URL that triggered them.
       * Use this URL as the higher precedence attributable URL.
       * @see https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/timeline/TimelineUIUtils.js?type=cs&q=_initEventStyles+-f:out+f:devtools&sq=package:chromium&g=0&l=678-744
       */
      case 'v8.compile':
      case 'EvaluateScript':
      case 'FunctionCall':
        taskURLs = [argsData.url, frameURL];
        break;
      case 'v8.compileModule':
        taskURLs = [task.event.args.fileName];
        break;
      case 'TimerFire': {
        /** @type {string} */
        // @ts-expect-error - timerId exists when name is TimerFire
        const timerId = task.event.args.data.timerId;
        const timerInstallerTaskNode = priorTaskData.timers.get(timerId);
        if (!timerInstallerTaskNode) break;
        taskURLs = timerInstallerTaskNode.attributableURLs;
        break;
      }
      case 'ParseHTML':
        taskURLs = [argsData.url, frameURL];
        break;
      case 'ParseAuthorStyleSheet':
        taskURLs = [argsData.styleSheetUrl, frameURL];
        break;
      case 'UpdateLayoutTree':
      case 'Layout':
      case 'Paint':
        // If we had a specific frame we were updating, just attribute it to that frame.
        if (frameURL) {
          taskURLs = [frameURL];
          break;
        }

        // Otherwise, sometimes Chrome will split layout into separate toplevel task after things have settled.
        // In this case we want to attribute the work to the prior task.
        // Inherit from previous task only if we don't have any task data set already.
        if (allURLsInTree.length) break;
        taskURLs = priorTaskData.lastTaskURLs;
        break;
      case 'XHRReadyStateChange':
      case 'XHRLoad': {
        // Inherit from task that issued the XHR
        const xhrUrl = argsData.url;
        const readyState = argsData.readyState;
        if (!xhrUrl || (typeof readyState === 'number' && readyState !== 4)) break;
        const xhrRequesterTaskNode = priorTaskData.xhrs.get(xhrUrl);
        if (!xhrRequesterTaskNode) break;
        taskURLs = xhrRequesterTaskNode.attributableURLs;
        break;
      }
      default:
        taskURLs = [];
        break;
    }

    /** @type {string[]} */
    const attributableURLs = Array.from(parentURLs);
    for (const url of [...taskURLs, ...stackFrameURLs]) {
      // Don't add empty URLs
      if (!url) continue;
      // Add unique URLs to our overall tree.
      if (!allURLsInTree.includes(url)) allURLsInTree.push(url);
      // Don't add consecutive, duplicate URLs
      if (attributableURLs[attributableURLs.length - 1] === url) continue;
      attributableURLs.push(url);
    }

    task.attributableURLs = attributableURLs;
    task.children.forEach(child =>
      MainThreadTasks._computeRecursiveAttributableURLs(
        child,
        attributableURLs,
        allURLsInTree,
        priorTaskData
      )
    );

    // After we've traversed the entire tree, set all the empty URLs to the set that we found in the task.
    // This attributes the overhead of browser task management to the scripts that created the work rather than
    // have it fall into the blackhole of "Other".
    if (!attributableURLs.length && !task.parent && allURLsInTree.length) {
      MainThreadTasks._setRecursiveEmptyAttributableURLs(task, allURLsInTree);
    }
  }
@param {TaskNode} task
@param {string[]} parentURLs
@param {string[]} allURLsInTree
@param {PriorTaskData} priorTaskData