Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: initial refactor of computedArtifact import/caching #5907

Merged
merged 5 commits into from Sep 21, 2018

Conversation

brendankenny
Copy link
Member

Proof of concept of moving computed artifacts off of the artifacts object passed into audits and just use regular require('./path/to/computed-artifact.js') to use them.

Starts with a really simple computed artifact, manifest-values.js, which doesn't need to request any computed artifacts itself, nor does any computed artifact request it, but it is non-trivial and required in four audits. The computed artifact cache is now part of LH.Audit.Context, and audits need to pass in the cache rather than it being created and bound behind the scenes.

Nice thing about this approach is non-converted computed artifacts can continue to function as-is and we can convert them one by one (or in bunches) rather than in one massive, unreadable PR.

There are a few things we lose without the automatically created methods, but I think we'll have a huge step up in being able to analyze dependencies, trace logic through, etc. The fact that this was so difficult to untangle just for a super-simple computedArtifact shows the downsides of the earlier magic experience. Using computed artifacts (or moving off them) in an audit is now as easy as pulling in any other library module.

This was part of #5008 under "Nice to have". I swear we had a discussion about this in a set of comments somewhere, but the earliest reference I could find was #4961 (review), so maybe it was in person.

@brendankenny
Copy link
Member Author

whoops, wanted it to be the cache passed in, not the whole context. Just sec :)

@brendankenny
Copy link
Member Author

Done.

Only ergonomic downside is that, since these are essentially single function calls with no state (that isn't cacheable and so needs to be passed in), they really cry out to be static. However, typescript doesn't support polymorphic this on static methods, so we can't have both automatic caching and type checking (and completion) based on whatever the computed artifact class needs as input/gives as output.

We really want type checking on those, but I think we also want automatic caching. Repeating the boilerplate everywhere would be dumb, and it's really easy to get slightly wrong and not notice it's recomputing some expensive calculation (a bug that did this was the original impetus for automatic caching - #675).

So, as a result, you have to do const result = new ComputedArtifact().request(cache, input);. Not ideal, but not that bad, and if typescript ever fixes microsoft/TypeScript#5863 and these are still around, some future Lighthouse person can do a quick fix for them all :)

@patrickhulce
Copy link
Collaborator

@brendankenny WDYT about something like this instead?

@brendankenny
Copy link
Member Author

WDYT about something like this instead?

well I spent the last day learning everything about the limits of polymorphic this in typescript to make my way work, but I think I like this one better :)

I'll try it out.

@brendankenny
Copy link
Member Author

@paulirish how do you feel about this format

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this a lot. My only gripe right now is the churn to all of our context passing.

Just thinking out loud here, what are the primary benefits of attaching the cache to context? If each computed artifact just kept its cache in its request method closure, would that be enough?

Might clean up a lot of the changes we'd have to do to migrate. If we're worried about having to introduce it later, maybe we could ensure all computed artifacts take an inputs object as first argument (like it works now) and an optional context second for future proofing?

return 'ManifestValues';
}

class ManifestValues {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe someone once told me that he thought it was silly we made classes with all static members instead of plain objects ;)

FWIW I kinda like them as classes for the flashy require name consistency, but I was mostly persuaded back then for plain objects, thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe someone once told me that he thought it was silly we made classes with all static members instead of plain objects ;)

haha, I didn't even think about it as I changed things. There used to be some tsc benefit due to polymorphic this (but not an issue with this implementation) and then from augmenting things in javascript passed out on module.exports (but that's all fixed in 3.1).

I'll admit to stockholm syndrome in that I don't see it as ugly and unnecessary any more, but I also don't have any problem with a plain object. The main thing might be the name property, which I think we still want, whether explicit or from just the StaticClass.name default from declaring a class.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true true, this WFM :)

@brendankenny
Copy link
Member Author

Just thinking out loud here, what are the primary benefits of attaching the cache to context? If each computed artifact just kept its cache in its request method closure, would that be enough?

I don't think we can do that because of GC. It would be node-module-only (I believe all the other clients destroy the world after every run), but since we use artifacts as the keys, artifacts will keep accumulating from each run.

Good use for WeakMap, but we don't always have unique parent objects for sets of artifacts, so, I don't think that will work. I can't think of another approach around that.

FWIW, I don't think the churn will end up being too bad outside of tests. Basically add a context param to each audit and pass it to each computed artifact request. We also don't have that many computed artifacts when it comes down to it.

Tests need more changes since we make so many calls, but I could reduce the noise there (e.g. just pass in {computedCache: new Map()} rather than doing a getMockContext() call).

@patrickhulce
Copy link
Collaborator

I don't think we can do that because of GC. It would be node-module-only (I believe all the other clients destroy the world after every run), but since we use artifacts as the keys, artifacts will keep accumulating from each run.

Yeah the clearing mechanism would be ugly at best, if you foresee little pain outside tests, then this makes sense to me 👍

Copy link
Collaborator

@patrickhulce patrickhulce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exciting 🚗ahead! woohoo!

@@ -269,8 +275,7 @@ class Runner {
const auditOptions = Object.assign({}, audit.defaultOptions, auditDefn.options);
const auditContext = {
options: auditOptions,
settings,
LighthouseRunWarnings: runWarnings,
...auditsContext,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont love these two diff objs with such similar names
how about sharedAuditsContext for this one?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// Computed artifacts switching to the new system.
'new-computed-artifact.js',
'manifest-values.js',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fn is for the gulpfile. wont browserify already discover this manifest-values module without being added manually?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the diff obscures it, but this is the list of filenamesToSkip :)

/** @type {Array<string>} */
const failures = [];

return artifacts.requestManifestValues(artifacts.Manifest).then(manifestValues => {
SplashScreen.assessManifest(manifestValues, failures);
const manifestValues = await ManifestValues.request(context, artifacts.Manifest);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll refrain from going promises->async/await in future changes to cut down the diffs as well :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants