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

[WIP] feat: base config v2 functionality #1880

Closed
wants to merge 9 commits into from
Closed

Conversation

patrickhulce
Copy link
Collaborator

@patrickhulce patrickhulce commented Mar 20, 2017

Implements the base functionality for the move to config v2 upon which all other features for the new format can be implemented.

design doc
related #1874 #1875 #1876 #1463 #1826

Missing because WIP (will be fixed before merge):

  • category description text
  • jsdocs
  • tests
  • consensus :)

Missing because MVP:

  • settings object
  • audit/gatherer options
  • customizable scoring functions (both proposed are already implemented just hardcoded atm)
  • handling of auditResults/artifacts/trace files, planned to be split from config entirely in v2

Current look:

image

Copy link
Member

@paulirish paulirish left a comment

Choose a reason for hiding this comment

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

can you also hook up --perf to select the perf aggregation? would love to see that.


EDIT: ah, this is dependent on the settings object. That's coming, I see. :)

}

// Perform a shallow clone so we can adjust gatherers and audits
configJson = Object.assign({}, configJson);
Copy link
Member

Choose a reason for hiding this comment

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

I think you'll need to do parse/stringify otherwise the original configJson object can be mutated, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I do shallow copies on any children I mutate and whenever this is exposed to outside for potential mutation it's parsed/stringified in asJson, this was mainly to preserve the ability to call from javascript with audits: {custom: {implementation: MyAuditClass}}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

or at least I try to do shallow copies everywhere, we'll see when I add tests :)

Copy link
Member

Choose a reason for hiding this comment

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

or at least I try to do shallow copies everywhere, we'll see when I add tests :)

test will be good :) assert.notDeepStrictEqual might be sufficient, but we could also do something with deep Object.freeze and see what throws (in strict mode)

Especially important for the browserified/lighthouse-as-a-module case since when loading the config json via require() any changes to the original object is persisted to the next use of it.

artifacts: runResults.artifacts,
runtimeConfig: Runner.getRuntimeConfig(opts.flags),
categories,
Copy link
Member

Choose a reason for hiding this comment

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

should we put this inside a report object on the result payload? or title it reportCategories to communicate clearly that it's just presentational?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I'm in favor of restructuring this object for v2, should probably add a section to the doc to discuss format

I was thinking something like the following?

{
  report: {
    meta: {version, time, url}, // stuff thats currently top-level
    config, // full config used (after extension, cli overrides, etc)
    score, // final overall score
    categories, // heirarchy of computed scores/results
    audits // individual results by audit id
  } // the stuff that gets written to report.json and whose contents can be used to render the HTML report without any additional information
  artifacts: {gatherers, trace, perfLog/devtoolsMessages} // used to quickly re-run analysis/perform later deep dives/debug/etc
}

Copy link
Member

Choose a reason for hiding this comment

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

agree that top level categories here is a bit confusing. Can we call it reportCategories until we settle on a result object shape?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

@paulirish paulirish mentioned this pull request Mar 21, 2017
12 tasks
}, []).sort((itemA, itemB) => itemA.order - itemB.order);
}

static getGathererImplementations(gatherersObject) {
Copy link
Member

Choose a reason for hiding this comment

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

this and getAuditImplementations appear to be the same. You leaving them WET for now expecting them to change in the future?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So they are, haha I'll DRY up. It was copypasta I expected to modify but didn't need to in the end


if (gathererIds.size !== usedGathererIds.size) {
const unused = _subtract(gathererIds, usedGathererIds);
log.warn('config', `Gatherers are unused: ${unused.join(', ')}`);
Copy link
Member

Choose a reason for hiding this comment

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

is this a bad thing? or just noting they are being culled from the working config based on the selection.

I'm fine with warn but the text could be more neutral to indicate its okay. (I'm expecting that this will log a lot when you do --perf)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Seemed like a good thing to warn about in case you misname or something, this function is likely to experience the most change when we add only/skip etc, so I'd say we can re-evaluate or just remove in the filtered case.

@paulirish
Copy link
Member

Super excited about this work. :)

On the whole I'm super excited about the new declarative config. The v2/config is a big upgrade over the current config.

Let's get some tests in place. IMO Category text can wait for another PR.

I spoke with @brendankenny and we're thinking of him enabling the v2 flag which enables the DOM based report renderer. This will start as an empty page and we'll build it out from there.

Copy link
Member

@brendankenny brendankenny left a comment

Choose a reason for hiding this comment

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

mostly higher level feedback, with a few style things.

A lot of really good work here, it's going to make the config a lot cleaner.

const defaultConfigJson = require(defaultConfigPath);

const _flatten = arr => [].concat(...arr);
const _subtract = (setA, setB) => Array.from(setA).filter(x => !setB.has(x));
Copy link
Member

Choose a reason for hiding this comment

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

maybe _difference? also should this return a Set as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure, and it did at first but it never gets used as a set, just as an array, I'll rename to _differenceAsArray

@@ -0,0 +1,308 @@
{
"version": 2,
Copy link
Member

Choose a reason for hiding this comment

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

share @ebidel's concern over the genericness of version. Personally I think configVersion works, but I also think we don't necessarily need anything here since we're planning on supporting just one type of config at a time, and we can just go with config verification with nice error messages instead (we could even include a check for "looks like you're using the old config format...here's a link").

Otherwise it's just a magic number users have to get right or lighthouse complains.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed we should have verification of the config format but we explicitly are attempting to support two config versions at once to start as we iterate, so I think the version has value as a clue of which path to head toward. A try until it works approach with validation and error messages greatly complicates the MVP. How about configVersion until it becomes the default and we nuke the field entirely and rely on validation only?

"styles": {},
"css-usage": {},
"all-event-listeners": {
"path": "dobetterweb/all-event-listeners"
Copy link
Member

Choose a reason for hiding this comment

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

as we were talking about, if some of the core gatherers can be referred to by their id they probably all should be. Having subdirectories is just an artifact of our repo organization process, not anything inherent to the gatherer

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure what you're trying to say here. IMO, this is actually a step in the direction of decoupling our arbitrary repo folder structure from magic happening in the config. What leads you to believe our repo organization process is now inherent to the gatherer now?

"theme-color-meta": {},
"content-width": {},
"deprecations": {},
"aria-allowed-attr": {
Copy link
Member

Choose a reason for hiding this comment

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

what does the object key ("aria-allowed-attr" here) do functionally?

On current master, aggregations refer to audit.name values, while this string would be used only to look up the path to that audit. For configv2 the path property here is used to find the file, so is the object key used for nothing or is it now what's referred to by the category's children?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For audits, as with each of these toplevel objects, the keys are the id (currently audit.meta.name) of the audit (rather than have it redundantly specified inside the object as well). There's really no need for audits to have category or name in their implementation now either.

"pwa": {
"name": "Progressive Web App",
"description": "",
"children": [
Copy link
Member

@brendankenny brendankenny Mar 22, 2017

Choose a reason for hiding this comment

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

children seems like the wrong relationship here. Was audits bad? It does make it clear that this is a set of audits... :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

children was used forseeing the whole subcategory discussion occurring and the likely scenario that we will need to add categories as children of other categories. Keeping it generic with children, IMO, leaves open the possibility without a breaking change. In the proposal, each child has a type: "audit"|"category"|"some-newfangled-thing" but since all we use are audits today it seemed unnecessary.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm in favor of keeping "audits" for now. I read this block as "this category contains/runs these audits". That naming is consistent with the list of "audits" above and is probably easier for contributors to make the linkage when adding new audits.

@@ -431,6 +431,11 @@ class GatherRunner {
static instantiateGatherers(passes, rootPath) {
return passes.map(pass => {
pass.gatherers = pass.gatherers.map(gatherer => {
if (gatherer.implementation) {
Copy link
Member

Choose a reason for hiding this comment

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

here (and in a few other places (report-generator, runner) where there's a special conditional branch) can you make sure to add some sort of // for config v2 so it's clear and easy to spot?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure

}, {});

const score = this._geometricMean([
{score: scoreByCategory.pwa, weight: 5},
Copy link
Member

Choose a reason for hiding this comment

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

hard coding category names and weights here seems like a mistake. Were you waiting to add a scoring property? Waiting is good, but can you just add weight to the category objects and skip this? (I'll comment in the design doc)

I also think using _geometricMean here is premature until we've had a real discussion on scoring, but happy to have it here for now :P

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I was waiting to introduce scoring methods and since nothing scoring wise has changed despite lots of talk the past few months I thought I'd poke the bear and see what came of it :P

Copy link
Contributor

Choose a reason for hiding this comment

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

Anything related to scoring does feel like a future PR. There's enough to bit off in here with all the other changes :)

artifacts: runResults.artifacts,
runtimeConfig: Runner.getRuntimeConfig(opts.flags),
categories,
Copy link
Member

Choose a reason for hiding this comment

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

agree that top level categories here is a bit confusing. Can we call it reportCategories until we settle on a result object shape?


"passes": {
"defaultPass": {
"order": 10,
Copy link
Member

Choose a reason for hiding this comment

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

can we kick order from the MVP so we can discuss it in a separate PR? I'm still not really a fan and with no motivating code that needs it yet there's no rush

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure

]);

// Extend only after our paths have been resolved
configJson = ConfigV2.extendIfNecessary(configJson, configPath);
Copy link
Member

Choose a reason for hiding this comment

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

I kind of want to punt extensions from the MVP as well, that's going to take some thought to get it right, but it looks like you're just matching current behavior here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it looks like you're just matching current behavior here?

The intent behind several of the config changes was specifically to enable clear and simple extension not needing logic or fancy behavior of any sort, so this is the final behavior as far as I'm concerned. That's what the structure of the new config enables :)

@brendankenny
Copy link
Member

I'd really like to clear up the audit/gatherer id/name/classname/key-in-various-objects distinctions a bit more.

  • What is the relationship between the keys in the gatherers top level object and the gatherers object in each pass? Do the pass gatherer keys reference the keys in the top level object?
  • Same thing for the audits top-level object keys vs the id values in each category's children array (I believe I asked this in a comment)
  • Is there a way to use a gatherer twice with different input? This is hard since we use artifacts[classname] currently to identify the output of each gatherer, so we may want to table this for now.
  • Is there a way to use an audit twice? Since we're letting audits take input with this new version this is going to be a great thing. Ideally I could do something like
"time-to-interactive-cold": {
  "path": "time-to-interactive",
  "inputTrace": "firstPass"
},
"time-to-interactive-warm": {
  "path": "time-to-interactive",
  "inputTrace": "laterPass"
}

(modulo better naming/clearer handling of path or whatever, etc)
and then being able to refer to time-to-interactive-cold and time-to-interactive-warm as different things in my categories. This would require using the keys as ids though, and not something on the audit itself.

I haven't fully thought that through yet, so there may be issues with that :)

So, basically, it would be sweet to simplify the id/key/path thing a bit more and figure out for sure how we're going to allow single audits multiple times (bonus points for gatherers multiple times, though that seems a lot messier).

@brendankenny
Copy link
Member

kind of shitty idea, everyone feel free to disagree: to iterate on some of this a bit more but not block the new report, we could make even more of an MVP by making the config v2 exactly the same as v1 except have report.categories instead of aggregations. I think everyone is already in agreement on the report.categories stuff here.

config.js probably wouldn't need to change much from master and the report generation/scoring could stay as it is in this PR. It's mostly v2/config.js that wouldn't be needed yet.

Just to be clear, this is all really good and would just get rolled over to the next change, I'm just thinking about this because I don't want to slow us way down by making us talk about all these things.

...? :)

@patrickhulce
Copy link
Collaborator Author

audit/gatherer id/name/classname/key-in-various-objects

I seem to have failed in config clarity given that I think the biggest win of config v2 is getting rid all of most of this nonsense and moving everything to IDs (apart from artifact name being class name...still need to hash that one out). The keys for all toplevel objects (gatherers, audits, passes) are IDs of that object. This ID should how everything else in the config refers to that particular object. Examples below.

What is the relationship between the keys in the gatherers top level object and the gatherers object in each pass? Do the pass gatherer keys reference the keys in the top level object?

The strings specified in a passes gatherers property are the IDs of gatherers. The long-term view was that these wouldn't need to be strings but potentially objects specifying a gatherer ID and potential options (for example collecting similar data across passes, or with slightly different settings).

Same thing for the audits top-level object keys vs the id values in each category's children array (I believe I asked this in a comment)

Responded in comment too, but same story. These strings are the IDs of the audits.

Is there a way to use a gatherer twice with different input? This is hard since we use artifacts[classname] currently to identify the output of each gatherer, so we may want to table this for now.

Correcto, as mentioned above still need to hash this out, but my big controversial proposal will be to start storing artifacts by gatherer ID, ignore class name completely, and move requiredArtifacts to an input property of an audit (with a defaults suggested by impl so config doesn't balloon with unnecessary wiring).....buuuuuuut one major change at a time :)

Is there a way to use an audit twice? Since we're letting audits take input with this new version this is going to be a great thing. Ideally I could do something like

Bingo :)

This would require using the keys as ids though, and not something on the audit itself.

yes! this is exactly what I'm trying to move to.

@patrickhulce
Copy link
Collaborator Author

making the config v2 exactly the same as v1 except have report.categories instead of aggregations

😢 😢

Seriously though, IMO there's no need to have these set in stone until we actually flip the switch and kill config v1 making v2 the default. If we have buyer's remorse about passes as object/whatever, I don't see why we couldn't change it all around tomorrow (though I obviously hope major points could be ironed out in doc stage 😄 )

static resolvePaths(object, configPath, searchPaths = []) {
object = Object.assign({}, object);
Object.keys(object).forEach(key => {
// We don't need to resolve objects that already have
Copy link
Contributor

Choose a reason for hiding this comment

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

already have....?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed

return object;
}

static objectToList(object) {
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a reason to make this a static? Can it be a helper like _differenceAsArray?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

mainly for testing since it did do fancier things like ordering, but that's been nixed for a future PR and didn't seem worth moving

{
"configVersion": 2,

"gatherers": {
Copy link
Contributor

Choose a reason for hiding this comment

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

what is the top-level gatherers for? list all possible gatherers and allow options?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep :) lists all gatherers with their ids and paths (if different from id) plus allowing options and multiple version of the same implementation later on down the road

},

"audits": {
"is-on-https": {},
Copy link
Contributor

Choose a reason for hiding this comment

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

shall we alphabetize all of these listings?

"pwa": {
"name": "Progressive Web App",
"description": "",
"children": [
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm in favor of keeping "audits" for now. I read this block as "this category contains/runs these audits". That naming is consistent with the list of "audits" above and is probably easier for contributors to make the linkage when adding new audits.

}
},

"report": {
Copy link
Contributor

Choose a reason for hiding this comment

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

I still thinks it's worth exploring modularity but am alright with adding a report section.

IOW,report could be it's own file (config/report.json). In that way, we keep configuration separate from output generation. As as an example, I could see users wanting to use our settings.json with their own report.json to create a custom report. They wouldn't have to change a mega file. That said, maybe your extension model smooths over that nasty....?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm definitely not opposed to separating the presentation config from the runtime config at some point, easy extension eases the immediate pain IMO.

},
"performance": {
"name": "Performance",
"description": "",
Copy link
Contributor

Choose a reason for hiding this comment

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

Might as well port over the descriptions from v1 while we're here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

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.

4 participants