Skip to content

Commit

Permalink
core: move simulator creation and network analysis to lib/lantern (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
connorjclark authored May 15, 2024
1 parent 24b46f1 commit a2968de
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 96 deletions.
53 changes: 1 addition & 52 deletions core/computed/load-simulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as Lantern from '../lib/lantern/types/lantern.js';
import {makeComputedArtifact} from './computed-artifact.js';
import * as constants from '../config/constants.js';
import {Simulator} from '../lib/lantern/simulator/simulator.js';
import {NetworkAnalysis} from './network-analysis.js';

Expand All @@ -17,57 +15,8 @@ class LoadSimulator {
* @return {Promise<Simulator>}
*/
static async compute_(data, context) {
const {throttlingMethod, throttling, precomputedLanternData} = data.settings;
const networkAnalysis = await NetworkAnalysis.request(data.devtoolsLog, context);

/** @type {Lantern.Simulation.Options} */
const options = {
additionalRttByOrigin: networkAnalysis.additionalRttByOrigin,
serverResponseTimeByOrigin: networkAnalysis.serverResponseTimeByOrigin,
observedThroughput: networkAnalysis.throughput,
};

// If we have precomputed lantern data, overwrite our observed estimates and use precomputed instead
// for increased stability.
if (precomputedLanternData) {
options.additionalRttByOrigin = new Map(Object.entries(
precomputedLanternData.additionalRttByOrigin));
options.serverResponseTimeByOrigin = new Map(Object.entries(
precomputedLanternData.serverResponseTimeByOrigin));
}

switch (throttlingMethod) {
case 'provided':
options.rtt = networkAnalysis.rtt;
options.throughput = networkAnalysis.throughput;
options.cpuSlowdownMultiplier = 1;
options.layoutTaskMultiplier = 1;
break;
case 'devtools':
if (throttling) {
options.rtt =
throttling.requestLatencyMs / constants.throttling.DEVTOOLS_RTT_ADJUSTMENT_FACTOR;
options.throughput =
throttling.downloadThroughputKbps * 1024 /
constants.throttling.DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR;
}

options.cpuSlowdownMultiplier = 1;
options.layoutTaskMultiplier = 1;
break;
case 'simulate':
if (throttling) {
options.rtt = throttling.rttMs;
options.throughput = throttling.throughputKbps * 1024;
options.cpuSlowdownMultiplier = throttling.cpuSlowdownMultiplier;
}
break;
default:
// intentionally fallback to simulator defaults
break;
}

return new Simulator(options);
return Simulator.createSimulator({...data.settings, networkAnalysis});
}

/**
Expand Down
42 changes: 1 addition & 41 deletions core/computed/network-analysis.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,14 @@ import {NetworkAnalyzer} from '../lib/lantern/simulator/network-analyzer.js';
import {NetworkRecords} from './network-records.js';

class NetworkAnalysis {
/**
* @param {Array<LH.Artifacts.NetworkRequest>} records
* @return {LH.Util.StrictOmit<LH.Artifacts.NetworkAnalysis, 'throughput'>}
*/
static computeRTTAndServerResponseTime(records) {
// First pass compute the estimated observed RTT to each origin's servers.
/** @type {Map<string, number>} */
const rttByOrigin = new Map();
for (const [origin, summary] of NetworkAnalyzer.estimateRTTByOrigin(records).entries()) {
rttByOrigin.set(origin, summary.min);
}

// We'll use the minimum RTT as the assumed connection latency since we care about how much addt'l
// latency each origin introduces as Lantern will be simulating with its own connection latency.
const minimumRtt = Math.min(...Array.from(rttByOrigin.values()));
// We'll use the observed RTT information to help estimate the server response time
const responseTimeSummaries = NetworkAnalyzer.estimateServerResponseTimeByOrigin(records, {
rttByOrigin,
});

/** @type {Map<string, number>} */
const additionalRttByOrigin = new Map();
/** @type {Map<string, number>} */
const serverResponseTimeByOrigin = new Map();
for (const [origin, summary] of responseTimeSummaries.entries()) {
// Not all origins have usable timing data, we'll default to using no additional latency.
const rttForOrigin = rttByOrigin.get(origin) || minimumRtt;
additionalRttByOrigin.set(origin, rttForOrigin - minimumRtt);
serverResponseTimeByOrigin.set(origin, summary.median);
}

return {
rtt: minimumRtt,
additionalRttByOrigin,
serverResponseTimeByOrigin,
};
}

/**
* @param {LH.DevtoolsLog} devtoolsLog
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.NetworkAnalysis>}
*/
static async compute_(devtoolsLog, context) {
const records = await NetworkRecords.request(devtoolsLog, context);
const throughput = NetworkAnalyzer.estimateThroughput(records);
const rttAndServerResponseTime = NetworkAnalysis.computeRTTAndServerResponseTime(records);
return {throughput, ...rttAndServerResponseTime};
return NetworkAnalyzer.analyze(records);
}
}

Expand Down
55 changes: 52 additions & 3 deletions core/lib/lantern/simulator/network-analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,16 +431,16 @@ class NetworkAnalyzer {
* Excludes data URI, failed or otherwise incomplete, and cached requests.
* Returns Infinity if there were no analyzable network records.
*
* @param {Array<Lantern.NetworkRequest>} networkRecords
* @param {Lantern.NetworkRequest[]} records
* @return {number}
*/
static estimateThroughput(networkRecords) {
static estimateThroughput(records) {
let totalBytes = 0;

// We will measure throughput by summing the total bytes downloaded by the total time spent
// downloading those bytes. We slice up all the network records into start/end boundaries, so
// it's easier to deal with the gaps in downloading.
const timeBoundaries = networkRecords.reduce((boundaries, record) => {
const timeBoundaries = records.reduce((boundaries, record) => {
const scheme = record.parsedURL?.scheme;
// Requests whose bodies didn't come over the network or didn't completely finish will mess
// with the computation, just skip over them.
Expand Down Expand Up @@ -483,6 +483,55 @@ class NetworkAnalyzer {
return totalBytes * 8 / totalDuration;
}

/**
* @param {Lantern.NetworkRequest[]} records
*/
static computeRTTAndServerResponseTime(records) {
// First pass compute the estimated observed RTT to each origin's servers.
/** @type {Map<string, number>} */
const rttByOrigin = new Map();
for (const [origin, summary] of NetworkAnalyzer.estimateRTTByOrigin(records).entries()) {
rttByOrigin.set(origin, summary.min);
}

// We'll use the minimum RTT as the assumed connection latency since we care about how much addt'l
// latency each origin introduces as Lantern will be simulating with its own connection latency.
const minimumRtt = Math.min(...Array.from(rttByOrigin.values()));
// We'll use the observed RTT information to help estimate the server response time
const responseTimeSummaries = NetworkAnalyzer.estimateServerResponseTimeByOrigin(records, {
rttByOrigin,
});

/** @type {Map<string, number>} */
const additionalRttByOrigin = new Map();
/** @type {Map<string, number>} */
const serverResponseTimeByOrigin = new Map();
for (const [origin, summary] of responseTimeSummaries.entries()) {
// Not all origins have usable timing data, we'll default to using no additional latency.
const rttForOrigin = rttByOrigin.get(origin) || minimumRtt;
additionalRttByOrigin.set(origin, rttForOrigin - minimumRtt);
serverResponseTimeByOrigin.set(origin, summary.median);
}

return {
rtt: minimumRtt,
additionalRttByOrigin,
serverResponseTimeByOrigin,
};
}

/**
* @param {Lantern.NetworkRequest[]} records
* @return {Lantern.Simulation.Settings['networkAnalysis']}
*/
static analyze(records) {
const throughput = NetworkAnalyzer.estimateThroughput(records);
return {
throughput,
...NetworkAnalyzer.computeRTTAndServerResponseTime(records),
};
}

/**
* @template {Lantern.NetworkRequest} T
* @param {Array<T>} records
Expand Down
56 changes: 56 additions & 0 deletions core/lib/lantern/simulator/simulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,62 @@ const ALL_SIMULATION_NODE_TIMINGS = new Map();
* @template [T=any]
*/
class Simulator {
/**
* @param {Lantern.Simulation.Settings} settings
*/
static async createSimulator(settings) {
const {throttlingMethod, throttling, precomputedLanternData, networkAnalysis} = settings;

/** @type {Lantern.Simulation.Options} */
const options = {
additionalRttByOrigin: networkAnalysis.additionalRttByOrigin,
serverResponseTimeByOrigin: networkAnalysis.serverResponseTimeByOrigin,
observedThroughput: networkAnalysis.throughput,
};

// If we have precomputed lantern data, overwrite our observed estimates and use precomputed instead
// for increased stability.
if (precomputedLanternData) {
options.additionalRttByOrigin = new Map(Object.entries(
precomputedLanternData.additionalRttByOrigin));
options.serverResponseTimeByOrigin = new Map(Object.entries(
precomputedLanternData.serverResponseTimeByOrigin));
}

switch (throttlingMethod) {
case 'provided':
options.rtt = networkAnalysis.rtt;
options.throughput = networkAnalysis.throughput;
options.cpuSlowdownMultiplier = 1;
options.layoutTaskMultiplier = 1;
break;
case 'devtools':
if (throttling) {
options.rtt =
throttling.requestLatencyMs / constants.throttling.DEVTOOLS_RTT_ADJUSTMENT_FACTOR;
options.throughput =
throttling.downloadThroughputKbps * 1024 /
constants.throttling.DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR;
}

options.cpuSlowdownMultiplier = 1;
options.layoutTaskMultiplier = 1;
break;
case 'simulate':
if (throttling) {
options.rtt = throttling.rttMs;
options.throughput = throttling.throughputKbps * 1024;
options.cpuSlowdownMultiplier = throttling.cpuSlowdownMultiplier;
}
break;
default:
// intentionally fallback to simulator defaults
break;
}

return new Simulator(options);
}

/**
* @param {Lantern.Simulation.Options} [options]
*/
Expand Down
38 changes: 38 additions & 0 deletions core/lib/lantern/types/lantern.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,44 @@ export namespace Simulation {
pessimistic: number;
}

/** Simulation settings that control the amount of network & cpu throttling in the run. */
interface ThrottlingSettings {
/** The round trip time in milliseconds. */
rttMs?: number;
/** The network throughput in kilobits per second. */
throughputKbps?: number;
// devtools settings
/** The network request latency in milliseconds. */
requestLatencyMs?: number;
/** The network download throughput in kilobits per second. */
downloadThroughputKbps?: number;
/** The network upload throughput in kilobits per second. */
uploadThroughputKbps?: number;
// used by both
/** The amount of slowdown applied to the cpu (1/<cpuSlowdownMultiplier>). */
cpuSlowdownMultiplier?: number
}

interface PrecomputedLanternData {
additionalRttByOrigin: {[origin: string]: number};
serverResponseTimeByOrigin: {[origin: string]: number};
}

interface Settings {
networkAnalysis: {
rtt: number;
additionalRttByOrigin: Map<string, number>;
serverResponseTimeByOrigin: Map<string, number>;
throughput: number;
};
/** The method used to throttle the network. */
throttlingMethod: 'devtools'|'simulate'|'provided';
/** The throttling config settings. */
throttling: Required<ThrottlingSettings>;
/** Precomputed lantern estimates to use instead of observed analysis. */
precomputedLanternData?: PrecomputedLanternData | null;
}

interface Options {
rtt?: number;
throughput?: number;
Expand Down
18 changes: 18 additions & 0 deletions core/test/lib/lantern/simulator/network-analyzer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {NetworkRecords} from '../../../../computed/network-records.js';
import {readJson} from '../../../test-utils.js';
import {NetworkRequest} from '../../../../lib/network-request.js';

// TODO(15841): use new traces
const devtoolsLog = readJson('../../../fixtures/traces/progressive-app-m60.devtools.log.json', import.meta);
const devtoolsLogWithRedirect = readJson('../../../fixtures/artifacts/redirect/devtoolslog.json', import.meta);

Expand Down Expand Up @@ -438,6 +439,23 @@ describe('DependencyGraph/Simulator/NetworkAnalyzer', () => {
});
});

describe('#computeRTTAndServerResponseTime', () => {
it('should work', async () => {
const records = await NetworkRecords.request(devtoolsLog, {computedCache: new Map()});
const result = await NetworkAnalyzer.computeRTTAndServerResponseTime(records);

expect(Math.round(result.rtt)).toEqual(3);
expect(result.additionalRttByOrigin).toMatchInlineSnapshot(`
Map {
"https://pwa.rocks" => 0.3960000176447025,
"https://www.googletagmanager.com" => 0,
"https://www.google-analytics.com" => 1.0450000117997007,
"__SUMMARY__" => 0,
}
`);
});
});

describe('#findMainDocument', () => {
it('should find the main document', async () => {
const records = await NetworkRecords.request(devtoolsLog, {computedCache: new Map()});
Expand Down

0 comments on commit a2968de

Please sign in to comment.