<file_sep>/**
 * @license Copyright 2017 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */
'use strict';

const Audit = require('./audit');
const WebInspector = require('../lib/web-inspector');
const Util = require('../report/v2/renderer/util');
const {groupIdToName, taskToGroup} = require('../lib/task-groups');
const THRESHOLD_IN_MS = 10;

class BootupTime extends Audit {
  /**
   * @return {!AuditMeta}
   */
  static get meta() {
    return {
      category: 'Performance',
      name: 'bootup-time',
      description: 'JavaScript boot-up time',
      failureDescription: 'JavaScript boot-up time is too high',
      scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
      helpText: 'Consider reducing the time spent parsing, compiling, and executing JS. ' +
        'You may find delivering smaller JS payloads helps with this. [Learn ' +
        'more](https://developers.google.com/web/tools/lighthouse/audits/bootup).',
      requiredArtifacts: ['traces'],
    };
  }

  /**
   * @return {LH.Audit.ScoreOptions}
   */
  static get defaultOptions() {
    return {
      // see https://www.desmos.com/calculator/rkphawothk
      // <500ms ~= 100, >2s is yellow, >3.5s is red
      scorePODR: 600,
      scoreMedian: 3500,
    };
  }

  /**
   * @param {DevtoolsTimelineModel} timelineModel
   * @return {!Map<string, Number>}
   */
  static getExecutionTimingsByURL(timelineModel) {
    const bottomUpByURL = timelineModel.bottomUpGroupBy('URL');
    const result = new Map();

    bottomUpByURL.children.forEach((perUrlNode, url) => {
      // when url is "" or about:blank, we skip it
      if (!url || url === 'about:blank') {
        return;
      }

      const taskGroups = {};
      perUrlNode.children.forEach((perTaskPerUrlNode) => {
        // eventStyle() returns a string like 'Evaluate Script'
        const task = WebInspector.TimelineUIUtils.eventStyle(perTaskPerUrlNode.event);
        // Resolve which taskGroup we're using
        const groupName = taskToGroup[task.title] || groupIdToName.other;
        const groupTotal = taskGroups[groupName] || 0;
        taskGroups[groupName] = groupTotal + (perTaskPerUrlNode.selfTime || 0);
      });
      result.set(url, taskGroups);
    });

    return result;
  }

  /**
   * @param {!Artifacts} artifacts
   * @param {LH.Audit.Context} context
   * @return {!AuditResult}
   */
  static audit(artifacts, context) {
    const trace = artifacts.traces[BootupTime.DEFAULT_PASS];
    return artifacts.requestDevtoolsTimelineModel(trace).then(devtoolsTimelineModel => {
      const executionTimings = BootupTime.getExecutionTimingsByURL(devtoolsTimelineModel);
      let totalBootupTime = 0;
      const extendedInfo = {};

      const headings = [
        {key: 'url', itemType: 'url', text: 'URL'},
        {key: 'scripting', itemType: 'text', text: groupIdToName.scripting},
        {key: 'scriptParseCompile', itemType: 'text', text: groupIdToName.scriptParseCompile},
      ];

      // map data in correct format to create a table
      const results = Array.from(executionTimings)
        .map(([url, groups]) => {
          // Add up the totalBootupTime for all the taskGroups
          totalBootupTime += Object.keys(groups).reduce((sum, name) => sum += groups[name], 0);
          extendedInfo[url] = groups;

          const scriptingTotal = groups[groupIdToName.scripting] || 0;
          const parseCompileTotal = groups[groupIdToName.scriptParseCompile] || 0;
          return {
            url: url,
            sum: scriptingTotal + parseCompileTotal,
            // Only reveal the javascript task costs
            // Later we can account for forced layout costs, etc.
            scripting: Util.formatMilliseconds(scriptingTotal, 1),
            scriptParseCompile: Util.formatMilliseconds(parseCompileTotal, 1),
          };
        })
        .filter(result => result.sum >= THRESHOLD_IN_MS)
        .sort((a, b) => b.sum - a.sum);

      const summary = {wastedMs: totalBootupTime};
      const details = BootupTime.makeTableDetails(headings, results, summary);

      const score = Audit.computeLogNormalScore(
        totalBootupTime,
        context.options.scorePODR,
        context.options.scoreMedian
      );

      return {
        score,
        rawValue: totalBootupTime,
        displayValue: Util.formatMilliseconds(totalBootupTime),
        details,
        extendedInfo: {
          value: extendedInfo,
        },
      };
    });
  }
}

module.exports = BootupTime;
<file_sep>/**
 * @license Copyright 2016 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */

/**
 * @fileoverview
 *   Identifies stylesheets, HTML Imports, and scripts that potentially block
 *   the first paint of the page by running several scripts in the page context.
 *   Candidate blocking tags are collected by querying for all script tags in
 *   the head of the page and all link tags that are either matching media
 *   stylesheets or non-async HTML imports. These are then compared to the
 *   network requests to ensure they were initiated by the parser and not
 *   injected with script. To avoid false positives from strategies like
 *   (http://filamentgroup.github.io/loadCSS/test/preload.html), a separate
 *   script is run to flag all links that at one point were rel=preload.
 */

'use strict';

const Gatherer = require('../gatherer');

/* global document,window,HTMLLinkElement */

/* istanbul ignore next */
function installMediaListener() {
  window.___linkMediaChanges = [];
  Object.defineProperty(HTMLLinkElement.prototype, 'media', {
    set: function(val) {
      window.___linkMediaChanges.push({
        href: this.href,
        media: val,
        msSinceHTMLEnd: Date.now() - window.performance.timing.responseEnd,
        matches: window.matchMedia(val).matches,
      });

      return this.setAttribute('media', val);
    },
  });
}

/* istanbul ignore next */
function collectTagsThatBlockFirstPaint() {
  return new Promise((resolve, reject) => {
    try {
      const tagList = [...document.querySelectorAll('link, head script[src]')]
        .filter(tag => {
          if (tag.tagName === 'SCRIPT') {
            return (
              !tag.hasAttribute('async') &&
              !tag.hasAttribute('defer') &&
              !/^data:/.test(tag.src) &&
              tag.getAttribute('type') !== 'module'
            );
          }

          // Filter stylesheet/HTML imports that block rendering.
          // https://www.igvita.com/2012/06/14/debunking-responsive-css-performance-myths/
          // https://www.w3.org/TR/html-imports/#dfn-import-async-attribute
          const blockingStylesheet =
            tag.rel === 'stylesheet' && window.matchMedia(tag.media).matches && !tag.disabled;
          const blockingImport = tag.rel === 'import' && !tag.hasAttribute('async');
          return blockingStylesheet || blockingImport;
        })
        .map(tag => {
          return {
            tagName: tag.tagName,
            url: tag.tagName === 'LINK' ? tag.href : tag.src,
            src: tag.src,
            href: tag.href,
            rel: tag.rel,
            media: tag.media,
            disabled: tag.disabled,
            mediaChanges: window.___linkMediaChanges.filter(item => item.href === tag.href),
          };
        });
      resolve(tagList);
    } catch (e) {
      const friendly = 'Unable to gather Scripts/Stylesheets/HTML Imports on the page';
      reject(new Error(`${friendly}: ${e.message}`));
    }
  });
}

function filteredAndIndexedByUrl(networkRecords) {
  return networkRecords.reduce((prev, record) => {
    if (!record.finished) {
      return prev;
    }

    const isParserGenerated = record._initiator.type === 'parser';
    // A stylesheet only blocks script if it was initiated by the parser
    // https://html.spec.whatwg.org/multipage/semantics.html#interactions-of-styling-and-scripting
    const isParserScriptOrStyle = /(css|script)/.test(record._mimeType) && isParserGenerated;
    const isFailedRequest = record._failed;
    const isHtml = record._mimeType && record._mimeType.includes('html');

    // Filter stylesheet, javascript, and html import mimetypes.
    // Include 404 scripts/links generated by the parser because they are likely blocking.
    if (isHtml || isParserScriptOrStyle || (isFailedRequest && isParserGenerated)) {
      prev[record._url] = {
        isLinkPreload: record.isLinkPreload,
        transferSize: record._transferSize,
        startTime: record._startTime,
        endTime: record._endTime,
      };
    }

    return prev;
  }, {});
}

class TagsBlockingFirstPaint extends Gatherer {
  constructor() {
    super();
    this._filteredAndIndexedByUrl = filteredAndIndexedByUrl;
  }

  static findBlockingTags(driver, networkRecords) {
    const scriptSrc = `(${collectTagsThatBlockFirstPaint.toString()}())`;
    const firstRequestEndTime = networkRecords.reduce(
      (min, record) => Math.min(min, record._endTime),
      Infinity
    );
    return driver.evaluateAsync(scriptSrc).then(tags => {
      const requests = filteredAndIndexedByUrl(networkRecords);

      return tags.reduce((prev, tag) => {
        const request = requests[tag.url];
        if (request && !request.isLinkPreload) {
          // Even if the request was initially blocking or appeared to be blocking once the
          // page was loaded, the media attribute could have been changed during load, capping the
          // amount of time it was render blocking. See https://github.com/GoogleChrome/lighthouse/issues/2832.
          const timesResourceBecameNonBlocking = (tag.mediaChanges || [])
            .filter(change => !change.matches)
            .map(change => change.msSinceHTMLEnd);
          const earliestNonBlockingTime = Math.min(...timesResourceBecameNonBlocking);
          const lastTimeResourceWasBlocking = Math.max(
            request.startTime,
            firstRequestEndTime + earliestNonBlockingTime / 1000
          );

          prev.push({
            tag,
            transferSize: request.transferSize || 0,
            startTime: request.startTime,
            endTime: Math.min(request.endTime, lastTimeResourceWasBlocking),
          });

          // Prevent duplicates from showing up again
          requests[tag.url] = null;
        }

        return prev;
      }, []);
    });
  }

  /**
   * @param {!Object} context
   */
  beforePass(context) {
    return context.driver.evaluteScriptOnNewDocument(`(${installMediaListener.toString()})()`);
  }

  /**
   * @param {!Object} context
   * @param {{networkRecords: !Array<!NetworkRecord>}} tracingData
   * @return {!Array<{tag: string, transferSize: number, startTime: number, endTime: number}>}
   */
  afterPass(context, tracingData) {
    return TagsBlockingFirstPaint.findBlockingTags(context.driver, tracingData.networkRecords);
  }
}

module.exports = TagsBlockingFirstPaint;
<file_sep>/**
 * @license Copyright 2018 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */
'use strict';

const Audit = require('./audit');

class Metrics extends Audit {
  /**
   * @return {!AuditMeta}
   */
  static get meta() {
    return {
      name: 'metrics',
      informative: true,
      description: 'Metrics',
      helpText: 'Collects all available metrics.',
      requiredArtifacts: ['traces', 'devtoolsLogs'],
    };
  }

  /**
   * @param {!Artifacts} artifacts
   * @return {!AuditResult}
   */
  static async audit(artifacts, context) {
    const trace = artifacts.traces[Audit.DEFAULT_PASS];
    const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
    const metricComputationData = {trace, devtoolsLog, settings: context.settings};

    const traceOfTab = await artifacts.requestTraceOfTab(trace);
    const firstContentfulPaint = await artifacts.requestFirstContentfulPaint(metricComputationData);
    const firstMeaningfulPaint = await artifacts.requestFirstMeaningfulPaint(metricComputationData);
    const firstCPUIdle = await artifacts.requestFirstCPUIdle(metricComputationData);
    const timeToInteractive = await artifacts.requestConsistentlyInteractive(metricComputationData);
    const metrics = [];

    // Include the simulated/observed performance metrics
    const metricsMap = {
      firstContentfulPaint,
      firstMeaningfulPaint,
      firstCPUIdle,
      timeToInteractive,
    };
    for (const [metricName, values] of Object.entries(metricsMap)) {
      metrics.push(Object.assign({metricName}, values));
    }

    // Include all timestamps of interest from trace of tab
    for (const [traceEventName, timing] of Object.entries(traceOfTab.timings)) {
      const uppercased = traceEventName.slice(0, 1).toUpperCase() + traceEventName.slice(1);
      const metricName = `trace${uppercased}`;
      const timestamp = traceOfTab.timestamps[traceEventName];
      metrics.push({metricName, timing, timestamp});
    }

    const headings = [
      {key: 'metricName', itemType: 'text', text: 'Name'},
      {key: 'timing', itemType: 'ms', granularity: 10, text: 'Value (ms)'},
    ];

    const tableDetails = Audit.makeTableDetails(headings, metrics);

    return {
      score: 1,
      rawValue: timeToInteractive.timing,
      details: tableDetails,
    };
  }
}

module.exports = Metrics;
<file_sep>/**
 * @license Copyright 2017 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */
// @ts-nocheck
'use strict';

const Gatherer = require('./gatherer');
const Sentry = require('../../lib/sentry');
const fontFaceDescriptors = [
  'display',
  'family',
  'featureSettings',
  'stretch',
  'style',
  'unicodeRange',
  'variant',
  'weight',
];

/* eslint-env browser*/
/**
 * Collect applied webfont data from `document.fonts`
 * @param {string[]}
 * @return {{}}
 */
/* istanbul ignore next */
function getAllLoadedFonts(descriptors) {
  const getFont = fontFace => {
    const fontRule = {};
    descriptors.forEach(descriptor => {
      fontRule[descriptor] = fontFace[descriptor];
    });

    return fontRule;
  };

  return document.fonts.ready.then(() => {
    return Array.from(document.fonts).filter(fontFace => fontFace.status === 'loaded')
      .map(getFont);
  });
}

/**
 * Collect authored webfont data from the `CSSFontFaceRule`s present in document.styleSheets
 * @return {{}}
 */
/* istanbul ignore next */
function getFontFaceFromStylesheets() {
  /**
   * Get full data about each CSSFontFaceRule within a styleSheet object
   * @param {StyleSheet} stylesheet
   * @return {{}}
   */
  function getSheetsFontFaces(stylesheet) {
    const fontUrlRegex = 'url\\((?:")([^"]+)(?:"|\')\\)';
    const fontFaceRules = [];
    if (stylesheet.cssRules) {
      for (const rule of stylesheet.cssRules) {
        if (rule instanceof CSSFontFaceRule) {
          const fontsObject = {
            display: rule.style.fontDisplay || 'auto',
            family: rule.style.fontFamily.replace(/"|'/g, ''),
            stretch: rule.style.fontStretch || 'normal',
            style: rule.style.fontStyle || 'normal',
            weight: rule.style.fontWeight || 'normal',
            variant: rule.style.fontVariant || 'normal',
            unicodeRange: rule.style.unicodeRange || 'U+0-10FFFF',
            featureSettings: rule.style.featureSettings || 'normal',
            src: [],
          };

          if (rule.style.src) {
            const matches = rule.style.src.match(new RegExp(fontUrlRegex, 'g'));
            if (matches) {
              fontsObject.src = matches.map(match => {
                const res = new RegExp(fontUrlRegex).exec(match);
                return new URL(res[1], location.href).href;
              });
            }
          }

          fontFaceRules.push(fontsObject);
        }
      }
    }

    return fontFaceRules;
  }

  /**
   * Provided a <link rel=stylesheet> element, it attempts to reload the asset with CORS headers.
   * Without CORS headers, a cross-origin stylesheet will have node.styleSheet.cssRules === null.
   * @param {Element} oldNode
   * @return {<!Promise>}
   */
  function loadStylesheetWithCORS(oldNode) {
    const newNode = oldNode.cloneNode(true);

    return new Promise(resolve => {
      newNode.addEventListener('load', function onload() {
        newNode.removeEventListener('load', onload);
        resolve(getFontFaceFromStylesheets());
      });
      newNode.crossOrigin = 'anonymous';
      oldNode.parentNode.insertBefore(newNode, oldNode);
      oldNode.remove();
    });
  }

  const promises = [];
  // Get all loaded stylesheets
  for (const stylesheet of document.styleSheets) {
    try {
      // Cross-origin stylesheets don't expose cssRules by default. We reload them w/ CORS headers.
      if (stylesheet.cssRules === null && stylesheet.href && stylesheet.ownerNode &&
        !stylesheet.ownerNode.crossOrigin) {
        promises.push(loadStylesheetWithCORS(stylesheet.ownerNode));
      } else {
        promises.push(Promise.resolve(getSheetsFontFaces(stylesheet)));
      }
    } catch (err) {
      promises.push({err: {message: err.message, stack: err.stack}});
    }
  }
  // Flatten results
  return Promise.all(promises).then(fontFaces => [].concat(...fontFaces));
}
/* eslint-env node */

class Fonts extends Gatherer {
  _findSameFontFamily(fontFace, fontFacesList) {
    return fontFacesList.find(fontItem => {
      return !fontFaceDescriptors.find(descriptor => {
        return fontFace[descriptor] !== fontItem[descriptor];
      });
    });
  }

  afterPass({driver}) {
    const args = JSON.stringify(fontFaceDescriptors);
    return Promise.all(
      [
        driver.evaluateAsync(`(${getAllLoadedFonts.toString()})(${args})`),
        driver.evaluateAsync(`(${getFontFaceFromStylesheets.toString()})()`),
      ]
    ).then(([loadedFonts, fontFaces]) => {
      return loadedFonts.map(fontFace => {
        if (fontFace.err) {
          const err = new Error(fontFace.err.message);
          err.stack = fontFace.err.stack;
          Sentry.captureException(err, {tags: {gatherer: 'Fonts'}, level: 'warning'});
          return null;
        }

        const fontFaceItem = this._findSameFontFamily(fontFace, fontFaces);
        fontFace.src = (fontFaceItem && fontFaceItem.src) || [];

        return fontFace;
      }).filter(Boolean);
    });
  }
}

module.exports = Fonts;
<file_sep>/**
 * @license Copyright 2018 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */
'use strict';

const Gatherer = require('../gatherer');

/* global fetch, URL, location */

/* istanbul ignore next */
function getRobotsTxtContent() {
  return fetch(new URL('/robots.txt', location.href))
    .then(response => {
      if (!response.ok) {
        return {status: response.status, content: null};
      }

      return response.text()
        .then(content => ({status: response.status, content}));
    })
    .catch(_ => ({status: null, content: null}));
}


class RobotsTxt extends Gatherer {
  /**
   * @param {{driver: !Driver}} options Run options
   * @return {!Promise<!{code: number, content: string}>}
   */
  afterPass(options) {
    return options.driver.evaluateAsync(`(${getRobotsTxtContent.toString()}())`);
  }
}

module.exports = RobotsTxt;
<file_sep>/**
 * @license Copyright 2018 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */

import * as parseManifest from '../lighthouse-core/lib/manifest-parser.js';

declare global {
  module LH {
    export interface Artifacts {
      // Created by by gather-runner
      fetchedAt: string;
      LighthouseRunWarnings: string[];
      UserAgent: string;
      traces: {[passName: string]: Trace};
      devtoolsLogs: {[passName: string]: DevtoolsLog};
      settings: Config.Settings;

      // Remaining are provided by gatherers
      Accessibility: Artifacts.Accessibility;
      CacheContents: string[];
      ChromeConsoleMessages: Crdp.Log.EntryAddedEvent[];
      CSSUsage: {rules: Crdp.CSS.RuleUsage[], stylesheets: Artifacts.CSSStyleSheetInfo[]};
      HTMLWithoutJavaScript: {value: string};
      HTTPRedirect: {value: boolean};
      JsUsageArtifact: Crdp.Profiler.ScriptCoverage[];
      Manifest: ReturnType<typeof parseManifest> | null;
      Offline: number;
      RobotsTxt: {status: number|null, content: string|null};
      RuntimeExceptions: Crdp.Runtime.ExceptionThrownEvent[];
      Scripts: Record<string, string>;
      ServiceWorker: {versions: Crdp.ServiceWorker.ServiceWorkerVersion[]};
      ThemeColor: string|null;
      URL: {initialUrl: string, finalUrl: string};
      Viewport: string|null;
      ViewportDimensions: Artifacts.ViewportDimensions;

      // TODO(bckenny): remove this for real computed artifacts approach
      requestTraceOfTab(trace: Trace): Promise<Artifacts.TraceOfTab>
    }

    module Artifacts {
      export interface Accessibility {
        violations: {
          id: string;
          nodes: {
            path: string;
            snippet: string | null;
            target: string[];
          }[];
        }[];
        notApplicable: {
          id: string
        }[];
      }

      export interface CSSStyleSheetInfo {
        header: Crdp.CSS.CSSStyleSheetHeader;
        content: string;
      }

      export interface ViewportDimensions {
        innerWidth: number;
        innerHeight: number;
        outerWidth: number;
        outerHeight: number;
        devicePixelRatio: number;
      }

      export interface MetricComputationDataInput {
        devtoolsLog: DevtoolsLog;
        trace: Trace;
        settings: Config.Settings;
      }

      export interface MetricComputationData extends MetricComputationDataInput {
        networkRecords: Array<WebInspector.NetworkRequest>;
        traceOfTab: TraceOfTab;
      }

      export interface Metric {
        timing: number;
        timestamp: number;
      }

      export interface LanternMetric {
        timing: number;
        optimisticEstimate: Gatherer.Simulation.Result
        pessimisticEstimate: Gatherer.Simulation.Result;
        optimisticGraph: Gatherer.Simulation.GraphNode;
        pessimisticGraph: Gatherer.Simulation.GraphNode;
      }

      export interface TraceTimes {
        navigationStart: number;
        firstPaint: number;
        firstContentfulPaint: number;
        firstMeaningfulPaint: number;
        traceEnd: number;
        onLoad: number;
        domContentLoaded: number;
      }

      export interface TraceOfTab {
        timings: TraceTimes;
        timestamps: TraceTimes;
        processEvents: Array<TraceEvent>;
        mainThreadEvents: Array<TraceEvent>;
        startedInPageEvt: TraceEvent;
        navigationStartEvt: TraceEvent;
        firstPaintEvt: TraceEvent;
        firstContentfulPaintEvt: TraceEvent;
        firstMeaningfulPaintEvt: TraceEvent;
        onLoadEvt: TraceEvent;
        fmpFellBack: boolean;
      }
    }
  }
}

// empty export to keep file a module
export {}
