Skip to content

Commit

Permalink
core: import lantern from trace engine (#16092)
Browse files Browse the repository at this point in the history
  • Loading branch information
connorjclark committed Jul 1, 2024
1 parent 4d8a2f0 commit e0cdc93
Show file tree
Hide file tree
Showing 77 changed files with 74 additions and 8,077 deletions.
2 changes: 0 additions & 2 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,3 @@ third-party/**
**/*.d.cts

page-functions-test-case*out*.js
# TODO(15841): remove when importing Lantern from npm
core/lib/lantern/**/*.test.js
9 changes: 3 additions & 6 deletions core/audits/byte-efficiency/byte-efficiency-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import {LCPImageRecord} from '../../computed/lcp-image-record.js';

const str_ = i18n.createIcuMessageFn(import.meta.url, {});

/** @typedef {import('../../lib/lantern/simulation/Simulator.js').Simulator} Simulator */
/** @typedef {import('../../lib/lantern/BaseNode.js').Node<LH.Artifacts.NetworkRequest>} Node */

// Parameters for log-normal distribution scoring. These values were determined by fitting the
// log-normal cumulative distribution function curve to the former method of linear interpolation
// scoring between the control points {average = 300 ms, poor = 750 ms, zero = 5000 ms} using the
Expand Down Expand Up @@ -96,8 +93,8 @@ class ByteEfficiencyAudit extends Audit {
* Computes the estimated effect of all the byte savings on the provided graph.
*
* @param {Array<LH.Audit.ByteEfficiencyItem>} results The array of byte savings results per resource
* @param {Node} graph
* @param {Simulator} simulator
* @param {LH.Gatherer.Simulation.GraphNode} graph
* @param {LH.Gatherer.Simulation.Simulator} simulator
* @param {{label?: string, providedWastedBytesByUrl?: Map<string, number>}=} options
* @return {{savings: number, simulationBeforeChanges: LH.Gatherer.Simulation.Result, simulationAfterChanges: LH.Gatherer.Simulation.Result}}
*/
Expand Down Expand Up @@ -151,7 +148,7 @@ class ByteEfficiencyAudit extends Audit {

/**
* @param {ByteEfficiencyProduct} result
* @param {Simulator} simulator
* @param {LH.Gatherer.Simulation.Simulator} simulator
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationInput
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
Expand Down
26 changes: 10 additions & 16 deletions core/audits/byte-efficiency/render-blocking-resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,16 @@
* @fileoverview Audit a page to see if it does have resources that are blocking first paint
*/


import {Audit} from '../audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
import {BaseNode} from '../../lib/lantern/lantern.js';
import * as Lantern from '../../lib/lantern/lantern.js';
import {UnusedCSS} from '../../computed/unused-css.js';
import {NetworkRequest} from '../../lib/network-request.js';
import {LoadSimulator} from '../../computed/load-simulator.js';
import {FirstContentfulPaint} from '../../computed/metrics/first-contentful-paint.js';
import {LCPImageRecord} from '../../computed/lcp-image-record.js';
import {NavigationInsights} from '../../computed/navigation-insights.js';


/** @typedef {import('../../lib/lantern/simulation/Simulator.js').Simulator} Simulator */
/** @typedef {import('../../lib/lantern/BaseNode.js').Node<LH.Artifacts.NetworkRequest>} Node */
/** @typedef {import('../../lib/lantern/NetworkNode.js').NetworkNode<LH.Artifacts.NetworkRequest>} NetworkNode */

// Because of the way we detect blocking stylesheets, asynchronously loaded
// CSS with link[rel=preload] and an onload handler (see https://github.com/filamentgroup/loadCSS)
// can be falsely flagged as blocking. Therefore, ignore stylesheets that loaded fast enough
Expand All @@ -44,10 +38,10 @@ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
/**
* Given a simulation's nodeTimings, return an object with the nodes/timing keyed by network URL
* @param {LH.Gatherer.Simulation.Result['nodeTimings']} nodeTimings
* @return {Map<string, {node: Node, nodeTiming: LH.Gatherer.Simulation.NodeTiming}>}
* @return {Map<string, {node: LH.Gatherer.Simulation.GraphNode, nodeTiming: LH.Gatherer.Simulation.NodeTiming}>}
*/
function getNodesAndTimingByRequestId(nodeTimings) {
/** @type {Map<string, {node: Node, nodeTiming: LH.Gatherer.Simulation.NodeTiming}>} */
/** @type {Map<string, {node: LH.Gatherer.Simulation.GraphNode, nodeTiming: LH.Gatherer.Simulation.NodeTiming}>} */
const requestIdToNode = new Map();

for (const [node, nodeTiming] of nodeTimings) {
Expand All @@ -61,8 +55,8 @@ function getNodesAndTimingByRequestId(nodeTimings) {

/**
* Adjust the timing of a node and its dependencies to account for stack specific overrides.
* @param {Map<Node, LH.Gatherer.Simulation.NodeTiming>} adjustedNodeTimings
* @param {Node} node
* @param {Map<LH.Gatherer.Simulation.GraphNode, LH.Gatherer.Simulation.NodeTiming>} adjustedNodeTimings
* @param {LH.Gatherer.Simulation.GraphNode} node
* @param {LH.Artifacts.DetectedStack[]} Stacks
*/
function adjustNodeTimings(adjustedNodeTimings, node, Stacks) {
Expand All @@ -83,7 +77,7 @@ function adjustNodeTimings(adjustedNodeTimings, node, Stacks) {
* Any stack specific timing overrides should go in this function.
* @see https://github.com/GoogleChrome/lighthouse/issues/2832#issuecomment-591066081
*
* @param {Node} node
* @param {LH.Gatherer.Simulation.GraphNode} node
* @param {LH.Gatherer.Simulation.NodeTiming} nodeTiming
* @param {LH.Artifacts.DetectedStack[]} Stacks
*/
Expand All @@ -93,7 +87,7 @@ function computeStackSpecificTiming(node, nodeTiming, Stacks) {
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 2.1 seconds:
// https://github.com/ampproject/amphtml/blob/8e03ac2f315774070651584a7e046ff24212c9b1/src/font-stylesheet-timeout.js#L54-L59
// Any potential savings must only include time spent on AMP stylesheet nodes before 2.1 seconds.
if (node.type === BaseNode.TYPES.NETWORK &&
if (node.type === Lantern.Graph.BaseNode.types.NETWORK &&
node.request.resourceType === NetworkRequest.TYPES.Stylesheet &&
nodeTiming.endTime > 2100) {
stackSpecificTiming.endTime = Math.max(nodeTiming.startTime, 2100);
Expand Down Expand Up @@ -208,8 +202,8 @@ class RenderBlockingResources extends Audit {
* devs that they should be able to get to a reasonable first paint without JS, which is not a bad
* thing.
*
* @param {Simulator} simulator
* @param {Node} fcpGraph
* @param {LH.Gatherer.Simulation.Simulator} simulator
* @param {LH.Gatherer.Simulation.GraphNode} fcpGraph
* @param {Set<string>} deferredIds
* @param {Map<string, number>} wastedCssBytesByUrl
* @param {LH.Artifacts.DetectedStack[]} Stacks
Expand All @@ -225,7 +219,7 @@ class RenderBlockingResources extends Audit {

// If a node can be deferred, exclude it from the new FCP graph
const canDeferRequest = deferredIds.has(node.id);
if (node.type !== BaseNode.TYPES.NETWORK) return !canDeferRequest;
if (node.type !== Lantern.Graph.BaseNode.types.NETWORK) return !canDeferRequest;

const isStylesheet =
node.request.resourceType === NetworkRequest.TYPES.Stylesheet;
Expand Down
7 changes: 2 additions & 5 deletions core/audits/dobetterweb/uses-http2.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
* origin are over the http/2 protocol.
*/

/** @typedef {import('../../lib/lantern/simulation/Simulator.js').Simulator} Simulator */
/** @typedef {import('../../lib/lantern/BaseNode.js').Node<LH.Artifacts.NetworkRequest>} Node */

import {Audit} from '../audit.js';
import {EntityClassification} from '../../computed/entity-classification.js';
import UrlUtils from '../../lib/url-utils.js';
Expand Down Expand Up @@ -69,8 +66,8 @@ class UsesHTTP2Audit extends Audit {
* Computes the estimated effect of all results being converted to http/2 on the provided graph.
*
* @param {Array<{url: string}>} results
* @param {Node} graph
* @param {Simulator} simulator
* @param {LH.Gatherer.Simulation.GraphNode} graph
* @param {LH.Gatherer.Simulation.Simulator} simulator
* @param {{label?: string}=} options
* @return {{savings: number, simulationBefore: LH.Gatherer.Simulation.Result, simulationAfter: LH.Gatherer.Simulation.Result}}
*/
Expand Down
8 changes: 4 additions & 4 deletions core/audits/long-tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class LongTasks extends Audit {
* most time will be attributed to 'other' (the category of the top-level
* RunTask). See pruning in `PageDependencyGraph.linkCPUNodes`.
* @param {LH.Artifacts.TaskNode} task
* @param {Map<Lantern.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
* @param {Map<Lantern.Types.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
* @param {Map<TaskGroupIds, number>} [timeByTaskGroup]
* @return {{startTime: number, duration: number, timeByTaskGroup: Map<TaskGroupIds, number>}}
*/
Expand Down Expand Up @@ -117,7 +117,7 @@ class LongTasks extends Audit {
/**
* @param {Array<LH.Artifacts.TaskNode>} longTasks
* @param {Set<string>} jsUrls
* @param {Map<Lantern.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
* @param {Map<Lantern.Types.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
* @return {LH.Audit.Details.DebugData}
*/
static makeDebugData(longTasks, jsUrls, taskTimingsByEvent) {
Expand Down Expand Up @@ -155,7 +155,7 @@ class LongTasks extends Audit {
/**
* Get timing from task, overridden by taskTimingsByEvent if provided.
* @param {LH.Artifacts.TaskNode} task
* @param {Map<Lantern.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
* @param {Map<Lantern.Types.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
* @return {Timing}
*/
static getTiming(task, taskTimingsByEvent) {
Expand Down Expand Up @@ -185,7 +185,7 @@ class LongTasks extends Audit {
const metricComputationData = Audit.makeMetricComputationDataInput(artifacts, context);
const tbtResult = await TotalBlockingTime.request(metricComputationData, context);

/** @type {Map<Lantern.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} */
/** @type {Map<Lantern.Types.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} */
let taskTimingsByEvent;

if (settings.throttlingMethod === 'simulate') {
Expand Down
14 changes: 8 additions & 6 deletions core/audits/uses-rel-preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as Lantern from '../lib/lantern/lantern.js';
import UrlUtils from '../lib/url-utils.js';
import {NetworkRequest} from '../lib/network-request.js';
import {Audit} from './audit.js';
Expand Down Expand Up @@ -61,8 +62,8 @@ class UsesRelPreloadAudit extends Audit {
if (node.type !== 'network') return;
// Don't include the node itself or any CPU nodes in the initiatorPath
const path = traversalPath.slice(1).filter(initiator => initiator.type === 'network');
if (!UsesRelPreloadAudit.shouldPreloadRequest(node.rawRequest, mainResource, path)) return;
urls.add(node.rawRequest.url);
if (!UsesRelPreloadAudit.shouldPreloadRequest(node.request, mainResource, path)) return;
urls.add(node.request.url);
});

return urls;
Expand All @@ -75,6 +76,7 @@ class UsesRelPreloadAudit extends Audit {
* @return {Set<string>}
*/
static getURLsFailedToPreload(graph) {
// TODO: add `fromPrefetchCache` to Lantern.Types.NetworkRequest, then use node.request here instead of rawRequest.
/** @type {Array<LH.Artifacts.NetworkRequest>} */
const requests = [];
graph.traverse(node => node.type === 'network' && requests.push(node.rawRequest));
Expand Down Expand Up @@ -109,8 +111,8 @@ class UsesRelPreloadAudit extends Audit {
* Critical requests deeper than depth 2 are more likely to be a case-by-case basis such that it
* would be a little risky to recommend blindly.
*
* @param {LH.Artifacts.NetworkRequest} request
* @param {LH.Artifacts.NetworkRequest} mainResource
* @param {Lantern.Types.NetworkRequest} request
* @param {Lantern.Types.NetworkRequest} mainResource
* @param {Array<LH.Gatherer.Simulation.GraphNode>} initiatorPath
* @return {boolean}
*/
Expand Down Expand Up @@ -157,7 +159,7 @@ class UsesRelPreloadAudit extends Audit {

if (node.isMainDocument()) {
mainDocumentNode = node;
} else if (node.rawRequest && urls.has(node.rawRequest.url)) {
} else if (node.request && urls.has(node.request.url)) {
nodesToPreload.push(node);
}
});
Expand Down Expand Up @@ -189,7 +191,7 @@ class UsesRelPreloadAudit extends Audit {

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

if (!results.length) {
Expand Down
9 changes: 5 additions & 4 deletions core/computed/critical-request-chains.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as Lantern from '../lib/lantern/lantern.js';
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkRequest} from '../lib/network-request.js';
import {MainResource} from './main-resource.js';
Expand All @@ -14,8 +15,8 @@ class CriticalRequestChains {
* For now, we use network priorities as a proxy for "render-blocking"/critical-ness.
* It's imperfect, but there is not a higher-fidelity signal available yet.
* @see https://docs.google.com/document/d/1bCDuq9H1ih9iNjgzyAL0gpwNFiEP4TZS-YLRp_RuMlc
* @param {LH.Artifacts.NetworkRequest} request
* @param {LH.Artifacts.NetworkRequest} mainResource
* @param {Lantern.Types.NetworkRequest} request
* @param {Lantern.Types.NetworkRequest} mainResource
* @return {boolean}
*/
static isCritical(request, mainResource) {
Expand Down Expand Up @@ -105,7 +106,7 @@ class CriticalRequestChains {
graph.traverse((node, traversalPath) => {
seenNodes.add(node);
if (node.type !== 'network') return;
if (!CriticalRequestChains.isCritical(node.rawRequest, mainResource)) return;
if (!CriticalRequestChains.isCritical(node.request, mainResource)) return;

const networkPath = traversalPath
.filter(/** @return {n is LH.Gatherer.Simulation.GraphNetworkNode} */
Expand All @@ -117,7 +118,7 @@ class CriticalRequestChains {
if (networkPath.some(r => !CriticalRequestChains.isCritical(r, mainResource))) return;

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

addChain(networkPath);
}, getNextNodes);
Expand Down
2 changes: 1 addition & 1 deletion core/computed/document-urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class DocumentUrls {
if (!requestedUrl || !mainDocumentUrl) throw new Error('No main frame navigations found');

const initialRequest =
Lantern.Simulation.NetworkAnalyzer.findResourceForUrl(networkRecords, requestedUrl);
Lantern.Core.NetworkAnalyzer.findResourceForUrl(networkRecords, requestedUrl);
if (initialRequest?.redirects?.length) requestedUrl = initialRequest.redirects[0].url;

return {requestedUrl, mainDocumentUrl};
Expand Down
2 changes: 1 addition & 1 deletion core/computed/load-simulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class LoadSimulator {
/**
* @param {{devtoolsLog: LH.DevtoolsLog, settings: LH.Audit.Context['settings']}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<Lantern.Simulation.Simulator>}
* @return {Promise<LH.Gatherer.Simulation.Simulator>}
*/
static async compute_(data, context) {
const networkAnalysis = await NetworkAnalysis.request(data.devtoolsLog, context);
Expand Down
2 changes: 1 addition & 1 deletion core/computed/main-resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class MainResource {
// would have evicted the first request by the time `MainDocumentRequest` (a consumer
// of this computed artifact) attempts to fetch the contents, resulting in a protocol error.
const mainResource =
Lantern.Simulation.NetworkAnalyzer.findLastDocumentForUrl(records, mainDocumentUrl);
Lantern.Core.NetworkAnalyzer.findLastDocumentForUrl(records, mainDocumentUrl);
if (!mainResource) {
throw new Error('Unable to identify the main resource');
}
Expand Down
4 changes: 1 addition & 3 deletions core/computed/metrics/lantern-first-contentful-paint.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ import * as Lantern from '../../lib/lantern/lantern.js';
import {makeComputedArtifact} from '../computed-artifact.js';
import {getComputationDataParams, lanternErrorAdapter} from './lantern-metric.js';

/** @typedef {import('../../lib/lantern/Metric.js').Extras} Extras */

class LanternFirstContentfulPaint extends Lantern.Metrics.FirstContentfulPaint {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @param {Omit<Lantern.Metrics.Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async computeMetricWithGraphs(data, context, extras) {
Expand Down
4 changes: 1 addition & 3 deletions core/computed/metrics/lantern-interactive.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import {makeComputedArtifact} from '../computed-artifact.js';
import {LanternLargestContentfulPaint} from './lantern-largest-contentful-paint.js';
import {getComputationDataParams, lanternErrorAdapter} from './lantern-metric.js';

/** @typedef {import('../../lib/lantern/Metric.js').Extras} Extras */

class LanternInteractive extends Lantern.Metrics.Interactive {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @param {Omit<Lantern.Metrics.Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async computeMetricWithGraphs(data, context, extras) {
Expand Down
4 changes: 1 addition & 3 deletions core/computed/metrics/lantern-largest-contentful-paint.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import {makeComputedArtifact} from '../computed-artifact.js';
import {getComputationDataParams, lanternErrorAdapter} from './lantern-metric.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';

/** @typedef {import('../../lib/lantern/Metric.js').Extras} Extras */

class LanternLargestContentfulPaint extends Lantern.Metrics.LargestContentfulPaint {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @param {Omit<Lantern.Metrics.Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async computeMetricWithGraphs(data, context, extras) {
Expand Down
4 changes: 1 addition & 3 deletions core/computed/metrics/lantern-max-potential-fid.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import {makeComputedArtifact} from '../computed-artifact.js';
import {getComputationDataParams, lanternErrorAdapter} from './lantern-metric.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';

/** @typedef {import('../../lib/lantern/Metric.js').Extras} Extras */

class LanternMaxPotentialFID extends Lantern.Metrics.MaxPotentialFID {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @param {Omit<Lantern.Metrics.Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async computeMetricWithGraphs(data, context, extras) {
Expand Down
2 changes: 1 addition & 1 deletion core/computed/metrics/lantern-metric.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function getComputationDataParamsFromTrace(data, context) {
* @return {never}
*/
function lanternErrorAdapter(err) {
if (!(err instanceof Lantern.Error)) {
if (!(err instanceof Lantern.Core.LanternError)) {
throw err;
}

Expand Down
4 changes: 1 addition & 3 deletions core/computed/metrics/lantern-speed-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ import {getComputationDataParams, lanternErrorAdapter} from './lantern-metric.js
import {Speedline} from '../speedline.js';
import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';

/** @typedef {import('../../lib/lantern/Metric.js').Extras} Extras */

class LanternSpeedIndex extends Lantern.Metrics.SpeedIndex {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @param {Omit<Lantern.Metrics.Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async computeMetricWithGraphs(data, context, extras) {
Expand Down
4 changes: 1 addition & 3 deletions core/computed/metrics/lantern-total-blocking-time.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js';
import {LanternInteractive} from './lantern-interactive.js';
import {getComputationDataParams} from './lantern-metric.js';

/** @typedef {import('../../lib/lantern/Metric.js').Extras} Extras */

class LanternTotalBlockingTime extends Lantern.Metrics.TotalBlockingTime {
/**
* @param {LH.Artifacts.MetricComputationDataInput} data
* @param {LH.Artifacts.ComputedContext} context
* @param {Omit<Extras, 'optimistic'>=} extras
* @param {Omit<Lantern.Metrics.Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
static async computeMetricWithGraphs(data, context, extras) {
Expand Down
Loading

0 comments on commit e0cdc93

Please sign in to comment.