-
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
Traces & Timespans: A Preview #12595
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.
Flushing some initial thoughts
} catch (err) { | ||
// SpeedIndex can't be computed on every timespan. If it errored, just mark as notApplicable. | ||
if (artifacts.GatherContext.gatherMode === 'timespan') return {notApplicable: true, score: 1}; | ||
throw err; | ||
} |
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 mentioned this in the UX sync already, but I'm concerned that SI doesn't make sense in timespan because expected UI events which visually update the page would punish the score.
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.
yes definitely! speed index only makes sense for the narrow SPA navigation use case of timespans. we can work on refining how to treat SPA navigations once more of this has taken shape. largely just a proof of concept that we can compute these sorts of metrics on the trace in different modes
interactiveTimeMs | ||
), | ||
}; | ||
if (TraceOfTab.isNavigation(data.traceOfTab)) { |
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 normalizing TBT at some point be a good idea? Longer timespans are generally going to have a higher TBT.
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.
for sure, I expect before we launch MVP we'll use a normalized version of TBT (similar to CLS windows) for timespans
* @return {traceOfTab is LH.Artifacts.NavigationTraceOfTab} | ||
*/ | ||
static isNavigation(traceOfTab) { | ||
return Boolean(traceOfTab.firstContentfulPaintEvt); |
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.
Is this our only requirement for a navigation trace?
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.
no the real PR will have more requirements (likely checking the use of our custom timespan marker), just needed something quickly to show the concept of detecting the trace type for use in audits
interactiveTimeMs | ||
), | ||
}; | ||
if (TraceOfTab.isNavigation(data.traceOfTab)) { |
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 was expecting this to split on gatherMode === 'navigation'
and then use TraceOfTab.assertIsNavigation
. Is this so we can still get TBT in navigation mode if FCP is not present in the trace?
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.
good question, so isNavigation
is meant to effectively capture gatherMode === 'navigation'
. I like your suggestion of two separate methods that explicitly divide the two concepts and asserts that they match 👍
if ( | ||
!firstContentfulPaintEvt || | ||
firstContentfulPaintTiming === undefined || | ||
firstContentfulPaintTs === undefined || | ||
// FCP-AF will only be undefined if FCP is also undefined. | ||
// These conditions are for enforcing types and should never actually trigger. | ||
!firstContentfulPaintAllFramesEvt || | ||
firstContentfulPaintAllFramesTiming === undefined || | ||
firstContentfulPaintAllFramesTs === undefined | ||
) { | ||
throw new LHError(LHError.errors.NO_FCP); | ||
} |
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.
Glad to see this go
LGTM to start flushing this out |
@connorjclark @paulirish @brendankenny any major showstoppers for you? I'll plan to start breaking this up on Monday if not. |
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.
@connorjclark @paulirish @brendankenny any major showstoppers for you? I'll plan to start breaking this up on Monday if not.
I think this looks good.
As I mentioned earlier, I'd like for us to also start looking for larger structural changes we could make to either avoid shipping complexity down to leaf files or at least to help keep things simple for inspection, e.g. I don't have to travel far code-wise to figure out what code path a particular line is on and the invariants I can assume there because I could only have gotten here by one or two paths.
return NetworkRecords.request(devtoolsLog, context) | ||
.then(networkRecords => | ||
Promise.all([ | ||
this.audit_(artifacts, networkRecords, context), | ||
PageDependencyGraph.request({trace, devtoolsLog}, context), | ||
LoadSimulator.request(simulatorOptions, context), | ||
PageDependencyGraph.request({trace, devtoolsLog}, context).catch(err => { |
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.
as a style thing, it would be nice to put a lot of this stuff closer to top level where appropriate so you don't have to dig to see where mode differentiation is happening
@@ -133,6 +133,7 @@ class RenderBlockingResources extends Audit { | |||
const traceOfTab = await TraceOfTab.request(trace, context); | |||
const simulator = await LoadSimulator.request(simulatorData, context); | |||
const wastedCssBytes = await RenderBlockingResources.computeWastedCSSBytes(artifacts, context); | |||
TraceOfTab.assertIsNavigation(traceOfTab); |
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.
part of why we made FCP required was because (at the time) everything had to check if FCP was actually in the trace to make sure we weren't auditing nonsense. We've mitigated that in some other ways since, but I worry that we'd be regressing that by going back to assertions in every audit. In an ideal world everything works only if it's being done correctly. What if an audit forgets to call this but only implicitly relies on FCP being defined? Would we notice the somewhat nonsense numbers coming out? (this kind of gets back to my worry about test coverage * audits * mode).
You're in the thick of this so you'd know the feasibility better than me just handwaving here, but what if we changed a little bit more about the model here? e.g. rather than conditionals in trace-of-tab/trace-processor we split trace-processor so we have the thing everyone uses to identify the main process, frame tree, etc. then navigation auditing can pull in the timing/timestamp stuff from a separate module with that as input if/when needed? Almost all of that isn't needed for timespans, and when we get to the point where timespans can have an lcp as well, I'm ok with using a different model than coming in on a giant timestamps
object :)
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.
What if an audit forgets to call this but only implicitly relies on FCP being defined?
I'm not sure this is solvable with any method :/ If there is an implicit relationship that FCP is defined but the audit never references it, then that could always be forgotten with supportedMode
/dedicated types or classes/favorite method X, right? Or am I misunderstanding that specific concern?
For the case where we're relying on FCP existing because the audit uses it, that's exactly what the types are doing for us here.
rather than conditionals in trace-of-tab/trace-processor we split trace-processor so we have the thing everyone uses to identify the main process, frame tree, etc. then navigation auditing can pull in the timing/timestamp stuff from a separate module with that as input if/when needed?
A total fork of trace-processor is certainly possible, but I'm a little fuzzy on the primary benefit. Is the goal for the type signature of trace output in timespan audits to not have a key at all for firstContentfulPaint
instead of an optional one?
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.
A total fork of trace-processor is certainly possible, but I'm a little fuzzy on the primary benefit. Is the goal for the type signature of trace output in timespan audits to not have a key at all for
firstContentfulPaint
instead of an optional one?
splitting it up, not forking, but yes :) timespan's portion of the trace-processor's output is a subset of what navigations need, so lets just only fetch timings/timestamps when they're needed, rather than always fetching them and asserting they're defined when needed.
@@ -272,6 +273,8 @@ class RenderBlockingResources extends Audit { | |||
* @return {Promise<LH.Audit.Product>} | |||
*/ | |||
static async audit(artifacts, context) { | |||
if (artifacts.GatherContext.gatherMode !== 'navigation') return {score: 0, notApplicable: 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.
in an ideal world it seems like we wouldn't have to remember to do this either, but I don't know a way around it other than audit annotations (a la meta.supportedModes
) and have audit-runner
take care of the check, but I don't know if that's any better. Curious about your thoughts on it
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.
adding meta.supportedModes
to audits is one option, and might be the most straightforward to avoid a ton of notApplicable
audits. my primary hesitation is keeping up with the potential mismatch with gatherers and the common case not deviating from the inferred. perhaps it could just be optional though?
@adamraine thoughts?
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 like the idea of having it as an optinal dependency. I was imagining the supported modes would be inferred by the available artifacts. Maybe we could use meta.applicableModes
or something to avoid mismatch.
displayValue: str_(i18n.UIStrings.seconds, {timeInMs: metricResult.timing}), | ||
}; | ||
} catch (err) { | ||
// SpeedIndex can't be computed on every timespan. If it errored, just mark as notApplicable. |
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.
is there more to this? no screenshots in the trace for some timespans? seems kind of loosey goosey for a metric :)
// TODO(FR-COMPAT): read additional trace categories from overall settings? | ||
// TODO(FR-COMPAT): check if CSS/DOM domains have been enabled in another session and warn? | ||
await driver.defaultSession.sendCommand('Page.enable'); | ||
await driver.defaultSession.sendCommand('Tracing.start', { | ||
categories: Trace.getDefaultTraceCategories().join(','), | ||
options: 'sampling-frequency=10000', // 1000 is default and too slow. | ||
}); | ||
|
||
// Only inject the trace processor timespan marker, for timespan traces. | ||
if (gatherMode !== 'timespan') return; |
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.
total nit but early return seems weird in this case?
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.
is this a if (gatherMode === 'timespan') {
request, or something 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.
is this a
if (gatherMode === 'timespan') {
request
yes
await driver.executionContext.evaluate(id => performance.mark(id), { | ||
args: [TraceProcessor.TIMESPAN_MARKER_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.
surely there's some better way of doing this, 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.
what do you have in mind? :)
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.
what do you have in mind? :)
just expressing incredulity there's no Tracing.insertEvent
or Page.performanceMark()
or whatever :)
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.
you mean like Tracing.recordClockSyncMarker
😄
@@ -513,7 +522,7 @@ class TraceProcessor { | |||
* | |||
* @param {LH.TraceEvent[]} events | |||
* @param {LH.TraceEvent} timeOriginEvent | |||
* @return {{lcp: LCPEvent | undefined, invalidated: boolean}} | |||
* @return {{lcp: LCPEvent | undefined}} |
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's unclear if this (and fmpFellBack
) is a necessary change for some purpose or if it's just a clearing out of stuff no one actually uses
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.
mostly just clearing stuff out, it simplified some of the logic and type gymnastics that would've been necessary to keep it.
@@ -579,7 +586,7 @@ class TraceProcessor { | |||
* @return {TraceOfTabWithoutFCP} | |||
*/ | |||
static computeTraceOfTab(trace, options) { | |||
const {timeOriginDeterminationMethod = 'lastNavigationStart'} = options || {}; | |||
const {timeOriginDeterminationMethod = 'auto'} = options || {}; |
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.
in what situations would 'auto'
be used?
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.
all situations, it's the default :)
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.
in what situations would
'auto'
be used?
all situations, it's the default :)
It could cause some difficult to debug surprises (e.g. something's not using navStart, you have to know the user's trace has a marker in it). Is the intention that most audits of the time traceOfTab will just work™ doing it this way, though?
@@ -182,6 +182,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer { | |||
const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics-container'); | |||
|
|||
metricAudits.forEach(item => { | |||
if (item.result.scoreDisplayMode === 'notApplicable') return; |
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.
does this not cause other weird things? or since scoreDisplayMode: 'error'
also has null scores we already handle it ok?
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 what you mean. This line just hides the notApplicable metrics from the metrics section of the category.
Or were you asking if metrics with notApplicable work with the score? If so, the answer is yes, just like others their weight just disappears (though separate weights for timespan is completely different bag of worms!)
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.
Or were you asking if metrics with notApplicable work with the score? If so, the answer is yes, just like others their weight just disappears (though separate weights for timespan is completely different bag of worms!)
yes, I was surprised notApplicable
just works because we've never had not-applicable metrics before, but it was a pleasant surprise :)
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.
thanks folks! I'll try to start breaking this up
@@ -133,6 +133,7 @@ class RenderBlockingResources extends Audit { | |||
const traceOfTab = await TraceOfTab.request(trace, context); | |||
const simulator = await LoadSimulator.request(simulatorData, context); | |||
const wastedCssBytes = await RenderBlockingResources.computeWastedCSSBytes(artifacts, context); | |||
TraceOfTab.assertIsNavigation(traceOfTab); |
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.
What if an audit forgets to call this but only implicitly relies on FCP being defined?
I'm not sure this is solvable with any method :/ If there is an implicit relationship that FCP is defined but the audit never references it, then that could always be forgotten with supportedMode
/dedicated types or classes/favorite method X, right? Or am I misunderstanding that specific concern?
For the case where we're relying on FCP existing because the audit uses it, that's exactly what the types are doing for us here.
rather than conditionals in trace-of-tab/trace-processor we split trace-processor so we have the thing everyone uses to identify the main process, frame tree, etc. then navigation auditing can pull in the timing/timestamp stuff from a separate module with that as input if/when needed?
A total fork of trace-processor is certainly possible, but I'm a little fuzzy on the primary benefit. Is the goal for the type signature of trace output in timespan audits to not have a key at all for firstContentfulPaint
instead of an optional one?
@@ -272,6 +273,8 @@ class RenderBlockingResources extends Audit { | |||
* @return {Promise<LH.Audit.Product>} | |||
*/ | |||
static async audit(artifacts, context) { | |||
if (artifacts.GatherContext.gatherMode !== 'navigation') return {score: 0, notApplicable: 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.
adding meta.supportedModes
to audits is one option, and might be the most straightforward to avoid a ton of notApplicable
audits. my primary hesitation is keeping up with the potential mismatch with gatherers and the common case not deviating from the inferred. perhaps it could just be optional though?
@adamraine thoughts?
// TODO(FR-COMPAT): read additional trace categories from overall settings? | ||
// TODO(FR-COMPAT): check if CSS/DOM domains have been enabled in another session and warn? | ||
await driver.defaultSession.sendCommand('Page.enable'); | ||
await driver.defaultSession.sendCommand('Tracing.start', { | ||
categories: Trace.getDefaultTraceCategories().join(','), | ||
options: 'sampling-frequency=10000', // 1000 is default and too slow. | ||
}); | ||
|
||
// Only inject the trace processor timespan marker, for timespan traces. | ||
if (gatherMode !== 'timespan') return; |
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.
is this a if (gatherMode === 'timespan') {
request, or something more?
await driver.executionContext.evaluate(id => performance.mark(id), { | ||
args: [TraceProcessor.TIMESPAN_MARKER_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.
what do you have in mind? :)
@@ -513,7 +522,7 @@ class TraceProcessor { | |||
* | |||
* @param {LH.TraceEvent[]} events | |||
* @param {LH.TraceEvent} timeOriginEvent | |||
* @return {{lcp: LCPEvent | undefined, invalidated: boolean}} | |||
* @return {{lcp: LCPEvent | undefined}} |
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.
mostly just clearing stuff out, it simplified some of the logic and type gymnastics that would've been necessary to keep it.
@@ -579,7 +586,7 @@ class TraceProcessor { | |||
* @return {TraceOfTabWithoutFCP} | |||
*/ | |||
static computeTraceOfTab(trace, options) { | |||
const {timeOriginDeterminationMethod = 'lastNavigationStart'} = options || {}; | |||
const {timeOriginDeterminationMethod = 'auto'} = options || {}; |
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.
all situations, it's the default :)
@@ -182,6 +182,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer { | |||
const metricsBoxesEl = this.dom.createChildOf(metricAuditsEl, 'div', 'lh-metrics-container'); | |||
|
|||
metricAudits.forEach(item => { | |||
if (item.result.scoreDisplayMode === 'notApplicable') return; |
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 what you mean. This line just hides the notApplicable metrics from the metrics section of the category.
Or were you asking if metrics with notApplicable work with the score? If so, the answer is yes, just like others their weight just disappears (though separate weights for timespan is completely different bag of worms!)
This is mostly obsolete and implemented at this point 🎉 |
Summary
With 8.0 edging out of the harbor, it's time to start discussing the next phase of FR, traces in timespan mode! 🎉
This encompasses several efforts...
This PR is a preview of a few of these efforts on a few audits to get a feel for what the future looks like. If anything feels horribly wrong, let's talk now to find an alternate path :)
I'll let this marinate for a few days and proceed breaking it up if there are no objections.
Related Issues/PRs
ref #11313