Skip to content

Commit 4143aac

Browse files
patrickhulcebrendankenny
authored andcommitted
feat(computed-artifact): support arbitrarily many inputs (#2705)
1 parent 648cce6 commit 4143aac

File tree

2 files changed

+133
-9
lines changed

2 files changed

+133
-9
lines changed

lighthouse-core/gather/computed/computed-artifact.js

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ class ComputedArtifact {
1717
this._allComputedArtifacts = allComputedArtifacts;
1818
}
1919

20+
get requiredNumberOfArtifacts() {
21+
return 1;
22+
}
23+
2024
/* eslint-disable no-unused-vars */
2125

2226
/**
@@ -31,21 +35,103 @@ class ComputedArtifact {
3135
throw new Error('compute_() not implemented for computed artifact ' + this.name);
3236
}
3337

38+
/**
39+
* This is a helper function for performing cache operations and is responsible for maintaing the
40+
* internal cache structure. This function accepts a path of artifacts, used to find the correct
41+
* nested cache object, and an operation to perform on that cache with the final key.
42+
*
43+
* The cache is structured with the first argument occupying the keys of the toplevel cache that point
44+
* to the Map of keys for the second argument and so forth until the 2nd to last argument's Map points
45+
* to result values rather than further nesting. In the simplest case of a single argument, there
46+
* is no nesting and the keys point directly to result values.
47+
*
48+
* Map(
49+
* argument1A -> Map(
50+
* argument2A -> result1A2A
51+
* argument2B -> result1A2B
52+
* )
53+
* argument1B -> Map(
54+
* argument2A -> result1B2A
55+
* )
56+
* )
57+
*
58+
* @param {!Array<*>} artifacts
59+
* @param {function(!Map, *)} cacheOperation
60+
*/
61+
_performCacheOperation(artifacts, cacheOperation) {
62+
artifacts = artifacts.slice();
63+
64+
let cache = this._cache;
65+
while (artifacts.length > 1) {
66+
const nextKey = artifacts.shift();
67+
if (cache.has(nextKey)) {
68+
cache = cache.get(nextKey);
69+
} else {
70+
const nextCache = new Map();
71+
cache.set(nextKey, nextCache);
72+
cache = nextCache;
73+
}
74+
}
75+
76+
return cacheOperation(cache, artifacts.shift());
77+
}
78+
79+
/**
80+
* Performs a cache.has operation, see _performCacheOperation for more.
81+
* @param {!Array<*>} artifacts
82+
* @return {boolean}
83+
*/
84+
_cacheHas(artifacts) {
85+
return this._performCacheOperation(artifacts, (cache, key) => cache.has(key));
86+
}
87+
88+
/**
89+
* Performs a cache.get operation, see _performCacheOperation for more.
90+
* @param {!Array<*>} artifacts
91+
* @return {*}
92+
*/
93+
_cacheGet(artifacts) {
94+
return this._performCacheOperation(artifacts, (cache, key) => cache.get(key));
95+
}
96+
97+
/**
98+
* Performs a cache.set operation, see _performCacheOperation for more.
99+
* @param {!Array<*>} artifacts
100+
* @param {*} result
101+
*/
102+
_cacheSet(artifacts, result) {
103+
return this._performCacheOperation(artifacts, (cache, key) => cache.set(key, result));
104+
}
105+
106+
/**
107+
* Asserts that the length of the array is the same as the number of inputs the class expects
108+
* @param {!Array<*>} artifacts
109+
*/
110+
_assertCorrectNumberOfArtifacts(artifacts) {
111+
const actual = artifacts.length;
112+
const expected = this.requiredNumberOfArtifacts;
113+
if (actual !== expected) {
114+
const className = this.constructor.name;
115+
throw new Error(`${className} requires ${expected} artifacts but ${actual} were given`);
116+
}
117+
}
118+
34119
/* eslint-enable no-unused-vars */
35120

36121
/**
37122
* Request a computed artifact, caching the result on the input artifact.
38-
* @param {*} artifact
123+
* @param {...*} artifacts
39124
* @return {!Promise<*>}
40125
*/
41-
request(artifact) {
42-
if (this._cache.has(artifact)) {
43-
return Promise.resolve(this._cache.get(artifact));
126+
request(...artifacts) {
127+
this._assertCorrectNumberOfArtifacts(artifacts);
128+
if (this._cacheHas(artifacts)) {
129+
return Promise.resolve(this._cacheGet(artifacts));
44130
}
45131

46132
const artifactPromise = Promise.resolve()
47-
.then(_ => this.compute_(artifact, this._allComputedArtifacts));
48-
this._cache.set(artifact, artifactPromise);
133+
.then(_ => this.compute_(...artifacts, this._allComputedArtifacts));
134+
this._cacheSet(artifacts, artifactPromise);
49135

50136
return artifactPromise;
51137
}

lighthouse-core/test/gather/computed/computed-artifact-test.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,41 @@ const assert = require('assert');
1212
const ComputedArtifact = require('../../../gather/computed/computed-artifact');
1313

1414
class TestComputedArtifact extends ComputedArtifact {
15-
constructor() {
16-
super();
15+
constructor(...args) {
16+
super(...args);
1717

18+
this.lastArguments = [];
1819
this.computeCounter = 0;
1920
}
2021

2122
get name() {
2223
return 'TestComputedArtifact';
2324
}
2425

25-
compute_(_) {
26+
compute_(...args) {
27+
this.lastArguments = args;
2628
return this.computeCounter++;
2729
}
2830
}
2931

32+
class MultipleInputArtifact extends TestComputedArtifact {
33+
get requiredNumberOfArtifacts() {
34+
return 2;
35+
}
36+
}
37+
3038
describe('ComputedArtifact base class', () => {
39+
it('tests correct number of inputs', () => {
40+
const singleInputArtifact = new TestComputedArtifact();
41+
const multiInputArtifact = new MultipleInputArtifact();
42+
43+
return Promise.resolve()
44+
.then(_ => singleInputArtifact.request(1))
45+
.then(_ => multiInputArtifact.request(1, 2))
46+
.then(_ => assert.throws(() => singleInputArtifact.request(1, 2)))
47+
.then(_ => assert.throws(() => multiInputArtifact.request(1)));
48+
});
49+
3150
it('caches computed artifacts', () => {
3251
const testComputedArtifact = new TestComputedArtifact();
3352

@@ -44,4 +63,23 @@ describe('ComputedArtifact base class', () => {
4463
assert.equal(result, 1);
4564
});
4665
});
66+
67+
it('caches multiple input arguments', () => {
68+
const mockComputed = {computed: true};
69+
const computedArtifact = new MultipleInputArtifact(mockComputed);
70+
71+
const obj0 = {value: 1};
72+
const obj1 = {value: 2};
73+
const obj2 = {value: 3};
74+
75+
return computedArtifact.request(obj0, obj1)
76+
.then(result => assert.equal(result, 0))
77+
.then(_ => assert.deepEqual(computedArtifact.lastArguments, [obj0, obj1, mockComputed]))
78+
.then(_ => computedArtifact.request(obj1, obj2))
79+
.then(result => assert.equal(result, 1))
80+
.then(_ => assert.deepEqual(computedArtifact.lastArguments, [obj1, obj2, mockComputed]))
81+
.then(_ => computedArtifact.request(obj0, obj1))
82+
.then(result => assert.equal(result, 0))
83+
.then(_ => assert.deepEqual(computedArtifact.lastArguments, [obj1, obj2, mockComputed]));
84+
});
4785
});

0 commit comments

Comments
 (0)