-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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(perf): add timing instrumentation to measure runtime perf #3745
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awesome awesome stuff 💣 ⏲ 💥
lighthouse-core/gather/driver.js
Outdated
@@ -97,7 +97,9 @@ class Driver { | |||
* @return {!Promise<null>} | |||
*/ | |||
connect() { | |||
return this._connection.connect(); | |||
const status = {str: 'Connecting to browser', id: 'connect'}; | |||
log.log('status', status.str, status.id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log.time
?
lighthouse-core/config/config.js
Outdated
@@ -269,6 +269,7 @@ class Config { | |||
* @param {string=} configPath The absolute path to the config file, if there is one. | |||
*/ | |||
constructor(configJSON, configPath) { | |||
log.marky.mark('config-create'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd prefer log.time
here :)
lighthouse-logger/index.js
Outdated
@@ -104,6 +106,16 @@ class Log { | |||
Log._logToStdErr(`${prefix}:${level || ''}`, [method, snippet]); | |||
} | |||
|
|||
static time({str, id, args=[]}, level='log') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how do you feel about msg
instead of str
?
lighthouse-core/index.js
Outdated
lighthouseResults.timing.total = endTime - startTime; | ||
lighthouseResults.timing.total = totalEntry.duration; | ||
|
||
fs.writeFileSync('generateTrace.html', ` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like it needs fixin :)
lighthouse-core/index.js
Outdated
@@ -35,15 +37,25 @@ module.exports = function(url, flags = {}, configJSON) { | |||
// Use ConfigParser to generate a valid config file | |||
const config = new Config(configJSON, flags.configPath); | |||
|
|||
log.marky.mark('ConnectionSetup'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log.time?
lighthouse-core/runner.js
Outdated
@@ -25,13 +25,15 @@ class Runner { | |||
|
|||
// List of top-level warnings for this Lighthouse run. | |||
const lighthouseRunWarnings = []; | |||
log.marky.mark('runner.run'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log.time?
lighthouse-core/runner.js
Outdated
@@ -151,6 +160,12 @@ class Runner { | |||
score = report.score; | |||
} | |||
|
|||
log.timeEnd(status); | |||
const timings = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: let's just keep as timing
|
||
const jsonStr = ` | ||
{ "traceEvents": [ | ||
${events.map(evt => JSON.stringify(evt)).join(',\n')} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
haha how about JSON.stringify({traceEvents: events}, null, 2)
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
alright alright
updated. |
@@ -234,13 +252,20 @@ class GatherRunner { | |||
.then(_ => recordTrace && driver.beginTrace(options.flags)) | |||
// Navigate. | |||
.then(_ => GatherRunner.loadPage(driver, options)) | |||
.then(_ => log.log('statusEnd', status)); | |||
.then(_ => log.timeEnd(status, 'log')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the level
is log? that's weird haha
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shrug. i suppose its equivalent level is actually "info"/"informational", but that's otherwise hidden as an implementation detail of lighthouse-logger.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
impl looks good to me!
It might be worth noodling on the timing IDs so a hierarchy is a little more self-evident.
a sample of what this might look like with namespaces colon/hyphen concatenated?
lh:init:config:create
lh:gather:beforePass:Styles
lh:audit:unused-css-rules
lh
|- init
|- config
|- connect
|- runner
|- gathering
|- auditing
|- gather
|- setup
|- beforePass:<gatherer name>
|- load
|- pass:<gatherer name>
|- afterPass:<gatherer name>
|- <gatherer name> *not part of trace, just timing object*
|- audit
|- <audit name>
|- report
|- ...
|- cleanup
lighthouse-logger/package.json
Outdated
@@ -3,6 +3,7 @@ | |||
"version": "1.0.1", | |||
"license": "Apache-2.0", | |||
"dependencies": { | |||
"debug": "^2.6.8" | |||
"debug": "^2.6.8", | |||
"marky": "^1.2.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lighthouse depends on lighthouse-logger explicitly so we don't need to update root package.json too or should we add in root too?
|
updated based on feedback. example generated trace: example.timing.json.zip |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm!
still somewhat nervous about the marky dep being in logger, we'll need to publish logger and bump our dep on it before publishing next LH I guess?
@@ -0,0 +1,134 @@ | |||
/** | |||
* @license Copyright 2016 Google Inc. All Rights Reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2017?
lighthouse-core/config/config.js
Outdated
@@ -269,6 +269,8 @@ class Config { | |||
* @param {string=} configPath The absolute path to the config file, if there is one. | |||
*/ | |||
constructor(configJSON, configPath) { | |||
const status = {msg: 'Create config', id: 'config-create'}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lh:init:config maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is looking good.
I'm still wondering about nesting, though. It seems like that's ok (vs overlapping)? e.g. a gatherer time is inside gatherRunner is inside runner is inside total
If that's ok, should we rely more on nesting for naming context rather than do it manually? e.g. a gatherer doesn't need all of lh:gather:pass:gather-name
because it's already nested in those things.
lighthouse-logger/index.js
Outdated
@@ -207,5 +219,6 @@ class Log { | |||
} | |||
|
|||
Log.events = new Emitter(); | |||
Log.marky = marky; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since marky
is a singleton which will persist between runs, maybe we should wrap in a method on Log
to also clear? like
static getTimeEntries() {
const entries = marky.getEntries();
marky.clear();
return entries;
}
lighthouse-core/runner.js
Outdated
@@ -71,6 +74,8 @@ class Runner { | |||
if (validPassesAndAudits) { | |||
// Set up the driver and run gatherers. | |||
opts.driver = opts.driverMock || new Driver(connection); | |||
run = run.then(_ => log.timeEnd(runnerStatus)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't this need to end in all execution paths?
const path = require('path'); | ||
|
||
/** | ||
* Technically, it's fine for usertiming measures to overlap, however non-async events make |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand overlapping vs async events here. Seems like they are separate concerns?
traceFilePath = path.resolve(process.cwd(), 'run-timing.trace.json'); | ||
} | ||
fs.writeFileSync(traceFilePath, jsonStr, 'utf8'); | ||
process.stdout.write(` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason not to use console.log
? (with eslint line disable)
*/ | ||
function saveTraceFromCLI() { | ||
const printErrorAndQuit = msg => { | ||
process.stderr.write(`ERROR: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
console.error
?
* Takes filename of LHR object. The primary entrypoint on CLI | ||
*/ | ||
function saveTraceFromCLI() { | ||
const printErrorAndQuit = msg => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pull out as a top level function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should smoke tests assert at least some timing stuff exists? Or one of the runner/index tests that runs (but no gathering) has all the timings that make sense? It seems like it could be easy to lose a time
or a timeEnd
or misplace in a promise chain or something, and not notice it happened
@@ -0,0 +1,134 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
needs tests :)
Since we're taking a closer look at the LHR these days.. here's what the timing object looks as of this branch: Looking at it with today's eyes, I have two proposals:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⏱ 📏 :its_happening:
Last bit, otherwise LGTM!
lighthouse-core/runner.js
Outdated
|
||
// Summarize all the timings and drop onto the LHR | ||
try { | ||
log.timeEnd(runnerStatus); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
marky looks like it will only throw if runnerStatus.id
hadn't been started, but that looks like that isn't possible here. Still need the try/catch?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it was happening in a -GA
run?
@@ -0,0 +1,57 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should really organize scripts/
at some point, but not necessary in this PR :)
|
||
const fs = require('fs'); | ||
const path = require('path'); | ||
const {createTraceString} = require('../lib/timing-trace-saver'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this and timing-trace-saver.js
just be the same file? (we can do in a follow up)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it was at one point, but i separated it to write tests.
i mean i guess we could have lighthouse-core/test/scripts/...
but it seemed weird. :)
// Any Timing entries in saved artifacts will have a different timeOrigin than the auditing phase | ||
// The `gather` prop is read later in generate-timing-trace and they're added to a separate track of trace events | ||
artifacts.Timing.forEach(entry => (entry.gather = true)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this happen in gather-runner.js
? After line 399
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only want to break the trace into two tracks if they're done at different times. (Classic -G then -A workflow)
So that's why we assign here to force a diff track.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thx patrick very much for pushing this!
|
||
const fs = require('fs'); | ||
const path = require('path'); | ||
const {createTraceString} = require('../lib/timing-trace-saver'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it was at one point, but i separated it to write tests.
i mean i guess we could have lighthouse-core/test/scripts/...
but it seemed weird. :)
lighthouse-core/runner.js
Outdated
|
||
// Summarize all the timings and drop onto the LHR | ||
try { | ||
log.timeEnd(runnerStatus); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it was happening in a -GA
run?
no problemo @paulirish I'll let you finish up :) |
Co-Authored-By: paulirish <paul.irish@gmail.com>
RE: Marky is stubbing a global "performance" variable: One glaring difference ... performance is defined (browser)
not defined (node)
|
lgtm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
last things, then LGTM ⏱⏲⌚️:)
package.json
Outdated
@@ -53,6 +53,8 @@ | |||
"fast": "yarn start --disable-device-emulation --throttlingMethod=provided", | |||
"deploy-viewer": "cd lighthouse-viewer && gulp deploy", | |||
"bundlesize": "bundlesize", | |||
"plots-smoke": "bash plots/test/smoke.sh", | |||
"timing": "node lighthouse-core/scripts/generate-timing-trace.js", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we call this something more? timing-trace
?
lighthouse-core/runner.js
Outdated
@@ -119,6 +121,11 @@ class Runner { | |||
categories = ReportScoring.scoreAllCategories(runOpts.config.categories, resultsById); | |||
} | |||
|
|||
log.timeEnd(resultsStatus); | |||
|
|||
// Summarize all the timings and drop onto the LHR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should drop this comment now
...timingEntriesFromArtifacts, | ||
...timingEntriesFromRunner, | ||
].map(entry => /** @type {[string, PerformanceEntry]} */ ([entry.name, entry])); | ||
const timingEntries = Array.from(new Map(timingEntriesKeyValues).values()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be good to get a comment in here about why we're doing this/need to do this. Basically this handles both the auditMode
case where gatherer entries need to be merged in and the gather/audit case where timingEntriesFromRunner
contains all entries from this run, including those also in timingEntriesFromArtifacts
typings/artifacts.d.ts
Outdated
@@ -304,6 +306,16 @@ declare global { | |||
}[]; | |||
} | |||
|
|||
export interface MeasureEntry { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should maybe make this
export interface MeasureEntry extends PerformanceEntry {
/** Whether timing entry was collected during artifact gathering. */
gather?: boolean;
}
to make the intended interchangeability clearer
@brendankenny done |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
waiting for the green, but LGTM!
@paulirish we should probably add a "how to profile" section to CONTRIBUTING or wherever, but can do that at a later time :)
:rip_van_winkle: |
this merge deserves its own partay |
This work fixes #2304 (and is mentioned in #3719)
We make use of
marky
which generates user timing entries for our instrumentation. We then use a variant oftdresser/performance-observer-tracing
to create a trace from those measures.usage:
Since
lighthouse-logger
gets some changes you'll need tonpm link
it to absorb them.Here's the trace in the screenshot f you want to poke around it: flipkart.json.run-timing.trace.json.txt