-
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
new_audit(mainthread-work-breakdown): Audit for page-execution-timings #3520
Conversation
I'm not sure how to unit test this? should I record a trace and run it against it or mock devtoolsModel? Mocking devtoolsModel looks easier |
How do I add a color ship? Change the category renderer? |
f1257b0
to
06b4bf8
Compare
const Util = require('../report/v2/renderer/util'); | ||
const DevtoolsTimelineModel = require('../lib/traces/devtools-timeline-model'); | ||
|
||
const timelineCategories = [ |
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.
Let's go with an enum for the groups:
const timelineGroups = {
JavaScript: 'JavaScript',
PaintLayout: 'Paint/Layout',
DOMCSS: 'DOM/CSS'.
Images: 'Images'
};
const timelineCategories = {
'Compile Script': timelineGroups.JavaScript },
'Evaluate Script': timelineGroups.JavaScript },
'Run Microtasks': timelineGroups.JavaScript },
'Minor GC': timelineGroups.JavaScript },
...
};
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've just updated https://docs.google.com/spreadsheets/d/1jqbSfdkB5RMtuifJwQsGb70WUUhWvzir7hq41wYHXsU/edit#gid=0 with the latest and greatest.
and for convenience.. here's the categories lookup:
const group = {
loading: 'Network request loading',
parseHTML: 'Parsing DOM',
styleLayout: 'Style & Layout',
compositing: 'Compositing',
painting: 'Paint',
gpu: 'GPU',
scripting: 'Script Evaluation',
scriptParseCompile: 'Script Parsing & Compile',
scriptGC: 'Garbage collection',
other: 'Other',
images: 'Images',
};
const taskToGroup = {
'Animation': group.painting,
'Async Task': group.other,
'Frame Start': group.painting,
'Frame Start (main thread)': group.painting,
'Cancel Animation Frame': group.scripting,
'Cancel Idle Callback': group.scripting,
'Compile Script': group.scriptParseCompile,
'Composite Layers': group.compositing,
'Console Time': group.scripting,
'Image Decode': group.images,
'Draw Frame': group.painting,
'Embedder Callback': group.scripting,
'Evaluate Script': group.scripting,
'Event': group.scripting,
'Animation Frame Fired': group.scripting,
'Fire Idle Callback': group.scripting,
'Function Call': group.scripting,
'DOM GC': group.scriptGC,
'GC Event': group.scriptGC,
'GPU': group.gpu,
'Hit Test': group.compositing,
'Invalidate Layout': group.styleLayout,
'JS Frame': group.scripting,
'Input Latency': group.scripting,
'Layout': group.styleLayout,
'Major GC': group.scriptGC,
'DOMContentLoaded event': group.scripting,
'First paint': group.painting,
'FMP': group.painting,
'FMP candidate': group.painting,
'Load event': group.scripting,
'Minor GC': group.scriptGC,
'Paint': group.painting,
'Paint Image': group.images,
'Paint Setup': group.painting,
'Parse Stylesheet': group.parseHTML,
'Parse HTML': group.parseHTML,
'Parse Script': group.scriptParseCompile,
'Other': group.other,
'Rasterize Paint': group.painting,
'Recalculate Style': group.styleLayout,
'Request Animation Frame': group.scripting,
'Request Idle Callback': group.scripting,
'Request Main Thread Frame': group.painting,
'Image Resize': group.images,
'Finish Loading': group.loading,
'Receive Data': group.loading,
'Receive Response': group.loading,
'Send Request': group.loading,
'Run Microtasks': group.scripting,
'Schedule Style Recalculation': group.styleLayout,
'Scroll': group.compositing,
'Task': group.other,
'Timer Fired': group.scripting,
'Install Timer': group.scripting,
'Remove Timer': group.scripting,
'Timestamp': group.scripting,
'Update Layer': group.compositing,
'Update Layer Tree': group.compositing,
'User Timing': group.scripting,
'Create WebSocket': group.scripting,
'Destroy WebSocket': group.scripting,
'Receive WebSocket Handshake': group.scripting,
'Send WebSocket Handshake': group.scripting,
'XHR Load': group.scripting,
'XHR Ready State Change': group.scripting,
}
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.
That is one super-handy category break-down!
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.
const executionTimings = PageExecutionTimings.getExecutionTimingsByCategory(trace); | ||
let totalExecutionTime = 0; | ||
|
||
const results = timelineCategories.map(category => { |
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.
feels better to loop over your real results instead of iterating the lookup.
with the categories i shared above.. you have constant time lookup:
Array.from(executionTimings).map(([eventName, duration]) => {
totalExecutionTime += duration;
return {
category: eventName,
group: taskToGroup[eventName],
duration: Util.formatMilliseconds(duration, 1),
};
});
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.
nice! much better actually 👍
* @param {!Array<TraceEvent>=} trace | ||
* @param {!WebInspector.TimelineProfileTree.Node} A grouped and sorted tree | ||
*/ | ||
static getTimingsByCategory(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.
this is pretty straightforward. you might as well inline this stuff into your getExecutionTimingsByCategory
method
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.
will switch back to the old way, I tried override that methode to mock DevtoolsTimeLinemodel
ce147f2
to
7c99f86
Compare
7c99f86
to
b1d5507
Compare
// sadly require(file) is not working correctly. | ||
// traceParser parser returns preact trace data the same as JSON.parse | ||
// fails when require is used | ||
const readFile = (file, cb) => { |
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.
and readFileSync
doesn't work either!? geez what's going on with these files 😆
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.
Readfilesync prolly works too. I bet devtoolstimeline changes 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.
Ah ok, require
caches the same object so you're probably right, could we switch to readfilesync then for brevity?
@wardpeet - “executiontime” should be two words 👍 Looking really, really good otherwise. |
@brendankenny should I add it to any of the real url smoketests? |
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.
review!
*/ | ||
|
||
/** | ||
* @fileoverview Audit a page to see if it does not use <link> that block first paint. |
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.
need to update :P
const Util = require('../report/v2/renderer/util'); | ||
const DevtoolsTimelineModel = require('../lib/traces/devtools-timeline-model'); | ||
|
||
const group = { |
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.
add some brief comments to these to explain what they are?
|
||
const result = new Map(); | ||
bottomUpByName.children.forEach((value, key) => | ||
result.set(key, Number(value.selfTime.toFixed(1)))); |
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.
rounding doesn't look necessary here? Seems like it should only happen for parts of the audit result meant for display (and duration
is sent to Util.formatMilliseconds
taking care of that)
const bottomUpByName = timelineModel.bottomUpGroupBy('EventName'); | ||
|
||
const result = new Map(); | ||
bottomUpByName.children.forEach((value, key) => |
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.
maybe s/key
/eventName
to give a local hint on what the keys are?
|
||
/** | ||
* @param {!Array<TraceEvent>=} trace | ||
* @return {!Map<string, Number>} |
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.
lowercase number
} | ||
|
||
/** | ||
* @param {!Array<TraceEvent>=} 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.
I believe this isn't optional? (no =
)
static get meta() { | ||
return { | ||
category: 'Performance', | ||
name: 'page-execution-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.
thinking about hypothetical futures, should we keep this more scoped in name? load-execution-timings
or trace-group-timings
or...something :) @paulirish
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.
mainthread-work-breakdown ?
* @return {!Map<string, Number>} | ||
*/ | ||
static getExecutionTimingsByCategory(trace) { | ||
const timelineModel = new DevtoolsTimelineModel(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.
is it possible to limit to events before TTCI? Not sure how to do this with DevtoolsTimelineModel
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 it possible to limit to events before TTCI? Not sure how to do this with DevtoolsTimelineModel
discussed offline. this could be done in a followup.. it wouldn't make a huge difference though it will be nice.
const fs = require('fs'); | ||
const assert = require('assert'); | ||
|
||
// sadly require(file) is not working correctly. |
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 a bit worrying because it means DevtoolsTimelineModel will be changing the main trace as well. Looking at the test diff, it looks like it's only changing column/line numbers (to make them 0-indexed) but that's still not great :)
Talking here we were thinking about either Object.freeze
or cloning options. Not sure what's best from a performance perspective, though.
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 exactly just numbers are changing.
cloning would probably take a while as you'll need a deep clone which can be pretty heavy. Will object.freeze work with timeline as they won't be able to edit the values.
I can poke around to see what works
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.
once this comes back we're good!
}); | ||
|
||
const headings = [ | ||
{key: 'category', itemType: 'text', text: 'Category'}, |
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.
'Category' => 'Work'
|
||
const headings = [ | ||
{key: 'category', itemType: 'text', text: 'Category'}, | ||
{key: 'group', itemType: 'text', text: 'Task Category'}, |
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.
let's have this be the first column.
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.
'Task Category' => 'Category'
// end acceptable trace | ||
|
||
// siteWithRedirects trace | ||
artifacts = { |
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 you break these apart into individual it()
s?
right now i want to run the middle one :))
} | ||
// end load trace | ||
|
||
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.
afaik you dont need done since this isn't async
{key: 'group', itemType: 'text', text: 'Task Category'}, | ||
{key: 'duration', itemType: 'text', text: 'Time spent'}, | ||
]; | ||
const tableDetails = PageExecutionTimings.makeTableDetails(headings, results); |
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 you sort this data?
the bottomupbyname was already sorted by Duration so we dont need to do that, but let's sort by each group
's total time spent.
so basically we'll have all the Script Evaluation work (as that will likely be the top one), with its tasks then sorted in decreasing order of time, then the rest of the groups.
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.
here's the patch for all this work:
diff --git a/lighthouse-core/audits/mainthread-work-breakdown.js b/lighthouse-core/audits/mainthread-work-breakdown.js
index 0dd563e4..c3fd7461 100644
--- a/lighthouse-core/audits/mainthread-work-breakdown.js
+++ b/lighthouse-core/audits/mainthread-work-breakdown.js
@@ -139,22 +139,31 @@ class PageExecutionTimings extends Audit {
let totalExecutionTime = 0;
const extendedInfo = {};
+ const categoryTotals = {};
+
const results = Array.from(executionTimings).map(([eventName, duration]) => {
totalExecutionTime += duration;
extendedInfo[eventName] = duration;
+ const groupName = taskToGroup[eventName];
+
+ const catTotal = categoryTotals[groupName] || 0;
+ categoryTotals[groupName] = catTotal + duration;
return {
category: eventName,
- group: taskToGroup[eventName],
+ group: groupName,
duration: Util.formatMilliseconds(duration, 1),
};
});
const headings = [
- {key: 'category', itemType: 'text', text: 'Category'},
- {key: 'group', itemType: 'text', text: 'Task Category'},
+ {key: 'group', itemType: 'text', text: 'Category'},
+ {key: 'category', itemType: 'text', text: 'Work'},
{key: 'duration', itemType: 'text', text: 'Time spent'},
];
+
+ results.stableSort((a, b) => categoryTotals[b.group] - categoryTotals[a.group]);
const tableDetails = PageExecutionTimings.makeTableDetails(headings, results);
return {
i swear at some point ill stop editing your PRs but i enjoy them so much i want to try some things out! :) sorry!
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 problem! Makes my job much easier anyway so keep editing away! 😄 💯 Also I learn different techniques so it's a win for me as well!
Let's get a check in on whether this is good to go from @paulirish when he returns. I believe this was good mentioned as good to land the last sprint review we completed. |
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 pending sign-off from @brendankenny and @paulirish
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're good on my end.
@brendankenny you want to take another look?
We should definitely prioritize #3702 |
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! Excited to see this in action
Fixes #3105
Credits go to @addyosmani to do the heavy lifting #1695