static computeWasteWithGraph(urls, graph, simulator) {
    if (!urls.size) {
      return {wastedMs: 0, results: []};
    }

    // Preload changes the ordering of requests, simulate the original graph
    // to have a reasonable baseline for comparison.
    const simulationBeforeChanges = simulator.simulate(graph);
    const modifiedGraph = graph.cloneWithRelationships();

    /** @type {Array<LH.Gatherer.Simulation.GraphNetworkNode>} */
    const nodesToPreload = [];
    /** @type {LH.Gatherer.Simulation.GraphNode|null} */
    let mainDocumentNode = null;
    modifiedGraph.traverse(node => {
      if (node.type !== 'network') return;

      if (node.isMainDocument()) {
        mainDocumentNode = node;
      } else if (node.request && urls.has(node.request.url)) {
        nodesToPreload.push(node);
      }
    });

    if (!mainDocumentNode) {
      // Should always find the main document node
      throw new Error('Could not find main document node');
    }

    // Preload has the effect of moving the resource's only dependency to the main HTML document
    // Remove all dependencies of the nodes
    for (const node of nodesToPreload) {
      node.removeAllDependencies();
      node.addDependency(mainDocumentNode);
    }

    // Once we've modified the dependencies, simulate the new graph.
    const simulationAfterChanges = simulator.simulate(modifiedGraph);
    const originalNodesByRequest = Array.from(simulationBeforeChanges.nodeTimings.keys())
        // @ts-expect-error we don't care if all nodes without a request collect on `undefined`
        .reduce((map, node) => map.set(node.request, node), new Map());

    const results = [];
    for (const node of nodesToPreload) {
      const originalNode = originalNodesByRequest.get(node.request);
      const timingAfter = simulationAfterChanges.nodeTimings.get(node);
      const timingBefore = simulationBeforeChanges.nodeTimings.get(originalNode);
      if (!timingBefore || !timingAfter) throw new Error('Missing preload node');

      const wastedMs = Math.round(timingBefore.endTime - timingAfter.endTime);
      if (wastedMs < THRESHOLD_IN_MS) continue;
      results.push({url: node.request.url, wastedMs});
    }

    if (!results.length) {
      return {wastedMs: 0, results};
    }

    return {
      // Preload won't necessarily impact the deepest chain/overall time
      // We'll use the maximum endTime improvement for now
      wastedMs: Math.max(...results.map(item => item.wastedMs)),
      results,
    };
  }
Computes the estimated effect of preloading all the resources.
@param {Set<string>} urls The array of byte savings results per resource
@param {LH.Gatherer.Simulation.GraphNode} graph
@param {LH.Gatherer.Simulation.Simulator} simulator
@return {{wastedMs: number, results: Array<{url: string, wastedMs: number}>}}