static extractChainsFromGraph(mainResource, graph) {
    /** @type {LH.Artifacts.CriticalRequestNode} */
    const rootNode = {};

    /**
     * @param {LH.Artifacts.NetworkRequest[]} path
     */
    function addChain(path) {
      let currentNode = rootNode;

      for (const record of path) {
        if (!currentNode[record.requestId]) {
          currentNode[record.requestId] = {
            request: record,
            children: {},
          };
        }

        currentNode = currentNode[record.requestId].children;
      }
    }

    // By default `traverse` will discover nodes in BFS-order regardless of dependencies, but
    // here we need traversal in a topological sort order. We'll visit a node only when its
    // dependencies have been met.
    const seenNodes = new Set();
    /** @param {LH.Gatherer.Simulation.GraphNode} node */
    function getNextNodes(node) {
      return node.getDependents().filter(n => n.getDependencies().every(d => seenNodes.has(d)));
    }

    graph.traverse((node, traversalPath) => {
      seenNodes.add(node);
      if (node.type !== 'network') return;
      if (!CriticalRequestChains.isCritical(node.request, mainResource)) return;

      const networkPath = traversalPath
        .filter(n => n.type === 'network')
        .reverse()
        .map(node => node.rawRequest);

      // Ignore if some ancestor is not a critical request.
      if (networkPath.some(r => !CriticalRequestChains.isCritical(r, mainResource))) return;

      // Ignore non-network things (like data urls).
      if (NetworkRequest.isNonNetworkRequest(node.request)) return;

      addChain(networkPath);
    }, getNextNodes);

    return rootNode;
  }
Create a tree of critical requests.
@param {LH.Artifacts.NetworkRequest} mainResource
@param {LH.Gatherer.Simulation.GraphNode} graph
@return {LH.Artifacts.CriticalRequestNode}