new_audit(mainthread-work-breakdown): Audit for page-execution-timings#3520
new_audit(mainthread-work-breakdown): Audit for page-execution-timings#3520brendankenny merged 16 commits intomasterfrom
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.
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.
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.
That is one super-handy category break-down!
| const executionTimings = PageExecutionTimings.getExecutionTimingsByCategory(trace); | ||
| let totalExecutionTime = 0; | ||
|
|
||
| const results = timelineCategories.map(category => { |
There was a problem hiding this comment.
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.
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.
this is pretty straightforward. you might as well inline this stuff into your getExecutionTimingsByCategory method
There was a problem hiding this comment.
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.
and readFileSync doesn't work either!? geez what's going on with these files 😆
There was a problem hiding this comment.
Readfilesync prolly works too. I bet devtoolstimeline changes the trace
There was a problem hiding this comment.
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? |
| */ | ||
|
|
||
| /** | ||
| * @fileoverview Audit a page to see if it does not use <link> that block first paint. |
| const Util = require('../report/v2/renderer/util'); | ||
| const DevtoolsTimelineModel = require('../lib/traces/devtools-timeline-model'); | ||
|
|
||
| const group = { |
There was a problem hiding this comment.
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.
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.
maybe s/key/eventName to give a local hint on what the keys are?
|
|
||
| /** | ||
| * @param {!Array<TraceEvent>=} trace | ||
| * @return {!Map<string, Number>} |
| } | ||
|
|
||
| /** | ||
| * @param {!Array<TraceEvent>=} trace |
There was a problem hiding this comment.
I believe this isn't optional? (no =)
| static get meta() { | ||
| return { | ||
| category: 'Performance', | ||
| name: 'page-execution-timings', |
There was a problem hiding this comment.
thinking about hypothetical futures, should we keep this more scoped in name? load-execution-timings or trace-group-timings or...something :) @paulirish
| * @return {!Map<string, Number>} | ||
| */ | ||
| static getExecutionTimingsByCategory(trace) { | ||
| const timelineModel = new DevtoolsTimelineModel(trace); |
There was a problem hiding this comment.
is it possible to limit to events before TTCI? Not sure how to do this with DevtoolsTimelineModel
There was a problem hiding this comment.
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.
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.
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
paulirish
left a comment
There was a problem hiding this comment.
once this comes back we're good!
| }); | ||
|
|
||
| const headings = [ | ||
| {key: 'category', itemType: 'text', text: 'Category'}, |
|
|
||
| const headings = [ | ||
| {key: 'category', itemType: 'text', text: 'Category'}, | ||
| {key: 'group', itemType: 'text', text: 'Task Category'}, |
There was a problem hiding this comment.
let's have this be the first column.
There was a problem hiding this comment.
'Task Category' => 'Category'
| // end acceptable trace | ||
|
|
||
| // siteWithRedirects trace | ||
| artifacts = { |
There was a problem hiding this comment.
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.
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.
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.
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.
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. |
addyosmani
left a comment
There was a problem hiding this comment.
LGTM pending sign-off from @brendankenny and @paulirish
paulirish
left a comment
There was a problem hiding this comment.
we're good on my end.
@brendankenny you want to take another look?
|
We should definitely prioritize #3702 |
brendankenny
left a comment
There was a problem hiding this comment.
LGTM! Excited to see this in action





Fixes #3105
Credits go to @addyosmani to do the heavy lifting #1695