static async fetchFailingNodeSourceRules(session, failingNodes) {
    const nodesToAnalyze = failingNodes
      .sort((a, b) => b.textLength - a.textLength)
      .slice(0, MAX_NODES_SOURCE_RULE_FETCHED);

    // DOM.getDocument is necessary for pushNodesByBackendIdsToFrontend to properly retrieve nodeIds if the `DOM` domain was enabled before this gatherer, invoke it to be safe.
    await session.sendCommand('DOM.getDocument', {depth: -1, pierce: true});

    const {nodeIds} = await session.sendCommand('DOM.pushNodesByBackendIdsToFrontend', {
      backendNodeIds: nodesToAnalyze.map(node => node.parentNode.backendNodeId),
    });

    const analysisPromises = nodesToAnalyze
      .map(async (failingNode, i) => {
        failingNode.nodeId = nodeIds[i];
        try {
          const cssRule = await fetchSourceRule(session, nodeIds[i]);
          failingNode.cssRule = cssRule;
        } catch (err) {
          // The node was deleted. We don't need to distinguish between lack-of-rule
          // due to a deleted node vs due to failed attribution, so just set to undefined.
          failingNode.cssRule = undefined;
        }
        return failingNode;
      });

    const analyzedFailingNodesData = await Promise.all(analysisPromises);

    const analyzedFailingTextLength = analyzedFailingNodesData.reduce(
      (sum, {textLength}) => (sum += textLength),
      0
    );

    return {analyzedFailingNodesData, analyzedFailingTextLength};
  }
@param {LH.Gatherer.ProtocolSession} session
@param {Array<NodeFontData>} failingNodes