Skip to content

Commit

Permalink
core(fr): separate phase from gatherMode
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce committed Apr 27, 2021
1 parent d19b646 commit 9e7c178
Show file tree
Hide file tree
Showing 33 changed files with 337 additions and 336 deletions.
42 changes: 22 additions & 20 deletions lighthouse-core/fraggle-rock/gather/base-gatherer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,39 +20,42 @@ class FRGatherer {
meta = {supportedModes: []}

/**
* Method to start observing a page before a navigation.
* Method to start observing a page for an arbitrary period of time.
* @param {LH.Gatherer.FRTransitionalContext} passContext
* @return {Promise<void>|void}
*/
beforeNavigation(passContext) { }
startInstrumentation(passContext) { }

/**
* Method to start observing a page for an arbitrary period of time.
* Method to start observing a page when the measurements are very sensitive and
* should observe as little Lighthouse-induced work as possible.
* @param {LH.Gatherer.FRTransitionalContext} passContext
* @return {Promise<void>|void}
*/
beforeTimespan(passContext) { }
startSensitiveInstrumentation(passContext) { }

/**
* Method to end observing a page after an arbitrary period of time and return the results.
* Method to stop observing a page when the measurements are very sensitive and
* should observe as little Lighthouse-induced work as possible.
*
* @param {LH.Gatherer.FRTransitionalContext} passContext
* @return {LH.Gatherer.PhaseResult}
* @return {Promise<void>|void}
*/
afterTimespan(passContext) { }
stopSensitiveInstrumentation(passContext) { }

/**
* Method to end observing a page after a navigation and return the results.
* Method to end observing a page after an arbitrary period of time.
* @param {LH.Gatherer.FRTransitionalContext} passContext
* @return {LH.Gatherer.PhaseResult}
* @return {Promise<void>|void}
*/
afterNavigation(passContext) { }
stopInstrumentation(passContext) { }

/**
* Method to gather results about a page in a particular state.
* Method to gather results about a page.
* @param {LH.Gatherer.FRTransitionalContext} passContext
* @return {LH.Gatherer.PhaseResult}
*/
snapshot(passContext) { }
getArtifact(passContext) { }

/**
* Legacy property used to define the artifact ID. In Fraggle Rock, the artifact ID lives on the config.
Expand All @@ -64,12 +67,13 @@ class FRGatherer {
}

/**
* Legacy method. Called before navigation to target url, roughly corresponds to `beforeTimespan`.
* Legacy method. Called before navigation to target url, roughly corresponds to `startInstrumentation`.
* @param {LH.Gatherer.PassContext} passContext
* @return {Promise<LH.Gatherer.PhaseResultNonPromise>}
*/
async beforePass(passContext) {
await this.beforeTimespan({...passContext, dependencies: {}});
await this.startInstrumentation({...passContext, dependencies: {}});
await this.startSensitiveInstrumentation({...passContext, dependencies: {}});
}

/**
Expand All @@ -80,17 +84,15 @@ class FRGatherer {
pass(passContext) { }

/**
* Legacy method. Roughly corresponds to `afterTimespan` or `snapshot` depending on type of gatherer.
* Legacy method. Roughly corresponds to `stopInstrumentation` or `getArtifact` depending on type of gatherer.
* @param {LH.Gatherer.PassContext} passContext
* @param {LH.Gatherer.LoadData} loadData
* @return {Promise<LH.Gatherer.PhaseResultNonPromise>}
*/
async afterPass(passContext, loadData) {
if (this.meta.supportedModes.includes('timespan')) {
return this.afterTimespan({...passContext, dependencies: {}});
}

return this.snapshot({...passContext, dependencies: {}});
await this.stopSensitiveInstrumentation({...passContext, dependencies: {}});
await this.stopInstrumentation({...passContext, dependencies: {}});
return this.getArtifact({...passContext, dependencies: {}});
}
}

Expand Down
132 changes: 17 additions & 115 deletions lighthouse-core/fraggle-rock/gather/navigation-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

const Driver = require('./driver.js');
const Runner = require('../../runner.js');
const {collectArtifactDependencies} = require('./runner-helpers.js');
const {
getEmptyArtifactState,
collectPhaseArtifacts,
awaitArtifacts,
} = require('./runner-helpers.js');
const {defaultNavigationConfig} = require('../../config/constants.js');
const {initializeConfig} = require('../config/config.js');
const {getBaseArtifacts} = require('./base-artifacts.js');
Expand All @@ -19,16 +23,6 @@ const {getBaseArtifacts} = require('./base-artifacts.js');
* @property {string} requestedUrl
*/

/** @typedef {Record<string, Promise<any>>} IntermediateArtifacts */

/**
* @typedef CollectPhaseArtifactOptions
* @property {NavigationContext} navigationContext
* @property {ArtifactState} artifacts
* @property {keyof Omit<LH.Gatherer.FRGathererInstance, 'name'|'meta'>} phase
*/

/** @typedef {Record<CollectPhaseArtifactOptions['phase'], IntermediateArtifacts>} ArtifactState */

/**
* @param {{driver: Driver, config: LH.Config.FRConfig, requestedUrl: string}} args
Expand Down Expand Up @@ -56,63 +50,6 @@ async function _setupNavigation({driver, navigation}) {
// TODO(FR-COMPAT): setup network conditions (throttling & cache state)
}

/** @type {Set<CollectPhaseArtifactOptions['phase']>} */
const phasesRequiringDependencies = new Set(['afterTimespan', 'afterNavigation', 'snapshot']);
/** @type {Record<CollectPhaseArtifactOptions['phase'], LH.Gatherer.GatherMode>} */
const phaseToGatherMode = {
beforeNavigation: 'navigation',
beforeTimespan: 'timespan',
afterTimespan: 'timespan',
afterNavigation: 'navigation',
snapshot: 'snapshot',
};
/** @type {Record<CollectPhaseArtifactOptions['phase'], CollectPhaseArtifactOptions['phase'] | undefined>} */
const phaseToPriorPhase = {
beforeNavigation: undefined,
beforeTimespan: undefined,
afterTimespan: 'beforeTimespan',
afterNavigation: 'beforeNavigation',
snapshot: undefined,
};

/**
* Runs the gatherer methods for a particular navigation phase (beforeTimespan/afterNavigation/etc).
* All gatherer method return values are stored on the artifact state object, organized by phase.
* This method collects required dependencies, runs the applicable gatherer methods, and saves the
* result on the artifact state object that was passed as part of `options`.
*
* @param {CollectPhaseArtifactOptions} options
*/
async function _collectPhaseArtifacts({navigationContext, artifacts, phase}) {
const gatherMode = phaseToGatherMode[phase];
const priorPhase = phaseToPriorPhase[phase];
const priorPhaseArtifacts = (priorPhase && artifacts[priorPhase]) || {};

for (const artifactDefn of navigationContext.navigation.artifacts) {
const gatherer = artifactDefn.gatherer.instance;
if (!gatherer.meta.supportedModes.includes(gatherMode)) continue;

const priorArtifactPromise = priorPhaseArtifacts[artifactDefn.id] || Promise.resolve();
const artifactPromise = priorArtifactPromise.then(async () => {
const dependencies = phasesRequiringDependencies.has(phase)
? await collectArtifactDependencies(artifactDefn, await _mergeArtifacts(artifacts))
: {};

return gatherer[phase]({
url: await navigationContext.driver.url(),
driver: navigationContext.driver,
gatherMode: 'navigation',
dependencies,
});
});

// Do not set the artifact promise if the result was `undefined`.
const result = await artifactPromise.catch(err => err);
if (result === undefined) continue;
artifacts[phase][artifactDefn.id] = artifactPromise;
}
}

/**
* @param {NavigationContext} navigationContext
*/
Expand All @@ -125,62 +62,28 @@ async function _navigate(navigationContext) {
// TODO(FR-COMPAT): capture page load errors
}

/**
* Merges artifact in Lighthouse order of specificity.
* If a gatherer method returns `undefined`, the artifact is skipped for that phase (treated as not set).
*
* - Navigation artifacts are the most specific. These win over anything.
* - Snapshot artifacts win out next as they have access to all available information.
* - Timespan artifacts win when nothing else is defined.
*
* @param {ArtifactState} artifactState
* @return {Promise<Partial<LH.GathererArtifacts>>}
*/
async function _mergeArtifacts(artifactState) {
/** @type {IntermediateArtifacts} */
const artifacts = {};

const artifactResultsInIncreasingPriority = [
artifactState.afterTimespan,
artifactState.snapshot,
artifactState.afterNavigation,
];

for (const artifactResults of artifactResultsInIncreasingPriority) {
for (const [id, promise] of Object.entries(artifactResults)) {
const artifact = await promise.catch(err => err);
if (artifact === undefined) continue;
artifacts[id] = artifact;
}
}

return artifacts;
}

/**
* @param {NavigationContext} navigationContext
*/
async function _navigation(navigationContext) {
/** @type {ArtifactState} */
const artifactState = {
beforeNavigation: {},
beforeTimespan: {},
afterTimespan: {},
afterNavigation: {},
snapshot: {},
const artifactState = getEmptyArtifactState();
const options = {
gatherMode: /** @type {'navigation'} */ ('navigation'),
driver: navigationContext.driver,
artifactDefinitions: navigationContext.navigation.artifacts,
artifactState,
};

const options = {navigationContext, artifacts: artifactState};

await _setupNavigation(navigationContext);
await _collectPhaseArtifacts({phase: 'beforeNavigation', ...options});
await _collectPhaseArtifacts({phase: 'beforeTimespan', ...options});
await collectPhaseArtifacts({phase: 'startInstrumentation', ...options});
await collectPhaseArtifacts({phase: 'startSensitiveInstrumentation', ...options});
await _navigate(navigationContext);
await _collectPhaseArtifacts({phase: 'afterTimespan', ...options});
await _collectPhaseArtifacts({phase: 'afterNavigation', ...options});
await _collectPhaseArtifacts({phase: 'snapshot', ...options});
await collectPhaseArtifacts({phase: 'stopSensitiveInstrumentation', ...options});
await collectPhaseArtifacts({phase: 'stopInstrumentation', ...options});
await collectPhaseArtifacts({phase: 'getArtifact', ...options});

const artifacts = await _mergeArtifacts(artifactState);
const artifacts = await awaitArtifacts(artifactState);
return {artifacts};
}

Expand Down Expand Up @@ -242,7 +145,6 @@ module.exports = {
navigation,
_setup,
_setupNavigation,
_collectPhaseArtifacts,
_navigate,
_navigation,
_navigations,
Expand Down
98 changes: 97 additions & 1 deletion lighthouse-core/fraggle-rock/gather/runner-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@
*/
'use strict';

/**
* @typedef CollectPhaseArtifactOptions
* @property {import('./driver.js')} driver
* @property {Array<LH.Config.ArtifactDefn>} artifactDefinitions
* @property {ArtifactState} artifactState
* @property {LH.Gatherer.FRGatherPhase} phase
* @property {LH.Gatherer.GatherMode} gatherMode
*/

/** @typedef {Record<string, Promise<any>>} IntermediateArtifacts */

/** @typedef {Record<CollectPhaseArtifactOptions['phase'], IntermediateArtifacts>} ArtifactState */

/**
*
* @param {{id: string}} dependency
Expand All @@ -14,6 +27,65 @@ function createDependencyError(dependency, error) {
return new Error(`Dependency "${dependency.id}" failed with exception: ${error.message}`);
}

/** @return {ArtifactState} */
function getEmptyArtifactState() {
return {
startInstrumentation: {},
startSensitiveInstrumentation: {},
stopSensitiveInstrumentation: {},
stopInstrumentation: {},
getArtifact: {},
};
}


// We make this an explicit record instead of array, so it's easily type checked.
/** @type {Record<CollectPhaseArtifactOptions['phase'], CollectPhaseArtifactOptions['phase'] | undefined>} */
const phaseToPriorPhase = {
startInstrumentation: undefined,
startSensitiveInstrumentation: 'startInstrumentation',
stopSensitiveInstrumentation: 'startSensitiveInstrumentation',
stopInstrumentation: 'stopSensitiveInstrumentation',
getArtifact: 'stopInstrumentation',
};

/**
* Runs the gatherer methods for a particular navigation phase (startInstrumentation/getArtifact/etc).
* All gatherer method return values are stored on the artifact state object, organized by phase.
* This method collects required dependencies, runs the applicable gatherer methods, and saves the
* result on the artifact state object that was passed as part of `options`.
*
* @param {CollectPhaseArtifactOptions} options
*/
async function collectPhaseArtifacts(options) {
const {driver, artifactDefinitions, artifactState, phase, gatherMode} = options;
const priorPhase = phaseToPriorPhase[phase];
const priorPhaseArtifacts = (priorPhase && artifactState[priorPhase]) || {};

for (const artifactDefn of artifactDefinitions) {
const gatherer = artifactDefn.gatherer.instance;

const priorArtifactPromise = priorPhaseArtifacts[artifactDefn.id] || Promise.resolve();
const artifactPromise = priorArtifactPromise.then(async () => {
const dependencies = phase === 'getArtifact'
? await collectArtifactDependencies(artifactDefn, artifactState.getArtifact)
: {};

return gatherer[phase]({
url: await driver.url(),
gatherMode,
driver,
dependencies,
});
});

// Do not set the artifact promise if the result was `undefined`.
const result = await artifactPromise.catch(err => err);
if (result === undefined) continue;
artifactState[phase][artifactDefn.id] = artifactPromise;
}
}

/**
* @param {LH.Config.ArtifactDefn} artifact
* @param {Record<string, LH.Gatherer.PhaseResult>} artifactsById
Expand Down Expand Up @@ -41,4 +113,28 @@ async function collectArtifactDependencies(artifact, artifactsById) {
return Object.fromEntries(await Promise.all(dependencyPromises));
}

module.exports = {collectArtifactDependencies};
/**
* Awaits the result of artifact, catching errors to set the artifact to an error instead.
*
* @param {ArtifactState} artifactState
* @return {Promise<Partial<LH.GathererArtifacts>>}
*/
async function awaitArtifacts(artifactState) {
/** @type {IntermediateArtifacts} */
const artifacts = {};

for (const [id, promise] of Object.entries(artifactState.getArtifact)) {
const artifact = await promise.catch(err => err);
if (artifact === undefined) continue;
artifacts[id] = artifact;
}

return artifacts;
}

module.exports = {
getEmptyArtifactState,
awaitArtifacts,
collectPhaseArtifacts,
collectArtifactDependencies,
};
Loading

0 comments on commit 9e7c178

Please sign in to comment.