Repository: GoogleChrome/lighthouse
Pull request #12728 (2021-06-30 19:20:38)

core(fr): add timespan support to css-usage
Some slight restructuring was required because the original gatherer only calculated the rule usage at a single point in time (at the snapshot or end of navigation).

Ref #11313



Patch:
diff --git a/lighthouse-core/gather/gatherers/css-usage.js b/lighthouse-core/gather/gatherers/css-usage.js
index c83a0d8b0..06c9b4111 100644
--- a/lighthouse-core/gather/gatherers/css-usage.js
+++ b/lighthouse-core/gather/gatherers/css-usage.js
@@ -11,38 +11,83 @@ const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
  * @fileoverview Tracks unused CSS rules.
  */
 class CSSUsage extends FRGatherer {
+  constructor() {
+    super();
+    /** @type {Array<LH.Crdp.CSS.StyleSheetAddedEvent>} */
+    this._stylesheets = [];
+    /** @param {LH.Crdp.CSS.StyleSheetAddedEvent} sheet */
+    this._onStylesheetAdded = sheet => this._stylesheets.push(sheet);
+    /**
+     * Initialize as undefined so we can assert results are fetched.
+     * @type {LH.Crdp.CSS.RuleUsage[]|undefined}
+     */
+    this._ruleUsage = undefined;
+  }
+
   /** @type {LH.Gatherer.GathererMeta} */
   meta = {
-    // TODO(FR-COMPAT): Add support for timespan.
-    supportedModes: ['snapshot', 'navigation'],
+    supportedModes: ['snapshot', 'timespan', 'navigation'],
   };
 
   /**
    * @param {LH.Gatherer.FRTransitionalContext} context
-   * @return {Promise<LH.Artifacts['CSSUsage']>}
    */
-  async getArtifact(context) {
+  async startCSSUsageTracking(context) {
     const session = context.driver.defaultSession;
-    const executionContext = context.driver.executionContext;
-
-    /** @type {Array<LH.Crdp.CSS.StyleSheetAddedEvent>} */
-    const stylesheets = [];
-    /** @param {LH.Crdp.CSS.StyleSheetAddedEvent} sheet */
-    const onStylesheetAdded = sheet => stylesheets.push(sheet);
-    session.on('CSS.styleSheetAdded', onStylesheetAdded);
+    session.on('CSS.styleSheetAdded', this._onStylesheetAdded);
 
     await session.sendCommand('DOM.enable');
     await session.sendCommand('CSS.enable');
     await session.sendCommand('CSS.startRuleUsageTracking');
+  }
+
+  /**
+   * @param {LH.Gatherer.FRTransitionalContext} context
+   */
+  async startInstrumentation(context) {
+    if (context.gatherMode !== 'timespan') return;
+    await this.startCSSUsageTracking(context);
+  }
+
+  /**
+   * @param {LH.Gatherer.FRTransitionalContext} context
+   */
+  async stopCSSUsageTracking(context) {
+    const session = context.driver.defaultSession;
+    const coverageResponse = await session.sendCommand('CSS.stopRuleUsageTracking');
+    this._ruleUsage = coverageResponse.ruleUsage;
+    session.off('CSS.styleSheetAdded', this._onStylesheetAdded);
+  }
 
-    // Force style to recompute.
-    // Doesn't appear to be necessary in newer versions of Chrome.
-    await executionContext.evaluateAsync('getComputedStyle(document.body)');
+  /**
+   * @param {LH.Gatherer.FRTransitionalContext} context
+   */
+  async stopInstrumentation(context) {
+    if (context.gatherMode !== 'timespan') return;
+    await this.stopCSSUsageTracking(context);
+  }
 
-    session.off('CSS.styleSheetAdded', onStylesheetAdded);
+  /**
+   * @param {LH.Gatherer.FRTransitionalContext} context
+   * @return {Promise<LH.Artifacts['CSSUsage']>}
+   */
+  async getArtifact(context) {
+    const session = context.driver.defaultSession;
+    const executionContext = context.driver.executionContext;
+
+    // TODO(FR-COMPAT): Do this only for snapshot once legacy runner is deprecated.
+    if (context.gatherMode !== 'timespan') {
+      await this.startCSSUsageTracking(context);
+
+      // Force style to recompute.
+      // Doesn't appear to be necessary in newer versions of Chrome.
+      await executionContext.evaluateAsync('getComputedStyle(document.body)');
+
+      await this.stopCSSUsageTracking(context);
+    }
 
     // Fetch style sheet content in parallel.
-    const promises = stylesheets.map(sheet => {
+    const promises = this._stylesheets.map(sheet => {
       const styleSheetId = sheet.header.styleSheetId;
       return session.sendCommand('CSS.getStyleSheetText', {styleSheetId}).then(content => {
         return {
@@ -53,15 +98,17 @@ class CSSUsage extends FRGatherer {
     });
     const styleSheetInfo = await Promise.all(promises);
 
-    const ruleUsageResponse = await session.sendCommand('CSS.stopRuleUsageTracking');
     await session.sendCommand('CSS.disable');
     await session.sendCommand('DOM.disable');
 
     const dedupedStylesheets = new Map(styleSheetInfo.map(sheet => {
       return [sheet.content, sheet];
     }));
+
+    if (!this._ruleUsage) throw new Error('Issue collecting rule usages');
+
     return {
-      rules: ruleUsageResponse.ruleUsage,
+      rules: this._ruleUsage,
       stylesheets: Array.from(dedupedStylesheets.values()),
     };
   }


Test patch:
diff --git a/lighthouse-core/test/fraggle-rock/api-test-pptr.js b/lighthouse-core/test/fraggle-rock/api-test-pptr.js
index e1ea99ef1..696febf8d 100644
--- a/lighthouse-core/test/fraggle-rock/api-test-pptr.js
+++ b/lighthouse-core/test/fraggle-rock/api-test-pptr.js
@@ -130,9 +130,9 @@ describe('Fraggle Rock API', () => {
         notApplicableAudits,
       } = getAuditsBreakdown(lhr);
       // TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
-      expect(auditResults.length).toMatchInlineSnapshot(`56`);
+      expect(auditResults.length).toMatchInlineSnapshot(`59`);
 
-      expect(notApplicableAudits.length).toMatchInlineSnapshot(`6`);
+      expect(notApplicableAudits.length).toMatchInlineSnapshot(`7`);
       expect(notApplicableAudits.map(audit => audit.id)).not.toContain('server-response-time');
 
       expect(erroredAudits).toHaveLength(0);
@@ -172,13 +172,13 @@ describe('Fraggle Rock API', () => {
       if (!result) throw new Error('Lighthouse failed to produce a result');
 
       const {auditResults, erroredAudits, notApplicableAudits} = getAuditsBreakdown(result.lhr);
-      expect(auditResults.length).toMatchInlineSnapshot(`56`);
+      expect(auditResults.length).toMatchInlineSnapshot(`59`);
 
-      expect(notApplicableAudits.length).toMatchInlineSnapshot(`8`);
+      expect(notApplicableAudits.length).toMatchInlineSnapshot(`9`);
       expect(notApplicableAudits.map(audit => audit.id)).toContain('server-response-time');
 
       // TODO(FR-COMPAT): Reduce this number by handling the error, making N/A, or removing timespan support.
-      expect(erroredAudits.length).toMatchInlineSnapshot(`12`);
+      expect(erroredAudits.length).toMatchInlineSnapshot(`14`);
     });
   });