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

new_audit(mainthread-work-breakdown): Audit for page-execution-timings #3520

Merged
merged 16 commits into from
Nov 17, 2017

Conversation

wardpeet
Copy link
Collaborator

Fixes #3105

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

@wardpeet
Copy link
Collaborator Author

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

@wardpeet wardpeet changed the title feat(page-execution-timings): Audit for page-execution-timings new-audit(page-execution-timings): Audit for page-execution-timings Oct 10, 2017
@wardpeet
Copy link
Collaborator Author

wardpeet commented Oct 10, 2017

I'd prefer to make sure this includes everything. But in order to help clarify script costs.. I see two solutions here:

  1. Add a TaskCategory column that would help clarify Script vs Paint, etc.
  2. Add a color chip next to the titles.. All yellow/orange would be quick to notice.

How do I add a color ship? Change the category renderer?

@wardpeet
Copy link
Collaborator Author

Example report:
paulirish.com
image

m.nieuwsblad.be
image

@wardpeet wardpeet force-pushed the feature/page-execution-timings branch from f1257b0 to 06b4bf8 Compare October 10, 2017 20:07
const Util = require('../report/v2/renderer/util');
const DevtoolsTimelineModel = require('../lib/traces/devtools-timeline-model');

const timelineCategories = [
Copy link
Member

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 },
  ...
};

Copy link
Member

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,
}

Copy link
Member

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!

Copy link
Collaborator Author

@wardpeet wardpeet Oct 12, 2017

Choose a reason for hiding this comment

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

where do you get that list when I capture a trace I can only find a handful of them
image

const executionTimings = PageExecutionTimings.getExecutionTimingsByCategory(trace);
let totalExecutionTime = 0;

const results = timelineCategories.map(category => {
Copy link
Member

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),
    };
});

Copy link
Collaborator Author

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) {
Copy link
Member

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

Copy link
Collaborator Author

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

@paulirish paulirish changed the title new-audit(page-execution-timings): Audit for page-execution-timings new_audit(page-execution-timings): Audit for page-execution-timings Oct 11, 2017
@wardpeet wardpeet force-pushed the feature/page-execution-timings branch from ce147f2 to 7c99f86 Compare October 13, 2017 15:14
@wardpeet wardpeet force-pushed the feature/page-execution-timings branch from 7c99f86 to b1d5507 Compare October 13, 2017 15:18
// 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) => {
Copy link
Collaborator

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 😆

Copy link
Collaborator Author

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

Copy link
Collaborator

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
Copy link
Collaborator Author

I think this audit is good to go, only have my doubts about the meta data.
score is always false so we show it as a perf Diagnostic
image

@benschwarz
Copy link
Contributor

@wardpeet - “executiontime” should be two words 👍

Looking really, really good otherwise.

@wardpeet
Copy link
Collaborator Author

@brendankenny should I add it to any of the real url smoketests?

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.

review!

*/

/**
* @fileoverview Audit a page to see if it does not use <link> that block first paint.
Copy link
Member

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 = {
Copy link
Member

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))));
Copy link
Member

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) =>
Copy link
Member

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>}
Copy link
Member

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
Copy link
Member

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',
Copy link
Member

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

Copy link
Member

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);
Copy link
Member

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

Copy link
Member

@paulirish paulirish Oct 18, 2017

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.
Copy link
Member

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.

Copy link
Collaborator Author

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

@paulirish paulirish self-assigned this Oct 31, 2017
@brendankenny brendankenny changed the title new_audit(page-execution-timings): Audit for page-execution-timings new_audit(mainthread-work-breakdown): Audit for page-execution-timings Oct 31, 2017
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.

once this comes back we're good!

});

const headings = [
{key: 'category', itemType: 'text', text: 'Category'},
Copy link
Member

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'},
Copy link
Member

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.

Copy link
Member

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 = {
Copy link
Member

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();
Copy link
Member

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);
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

before and after:
image

(different data)

Copy link
Member

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!

Copy link
Collaborator Author

@wardpeet wardpeet Nov 3, 2017

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!

@addyosmani
Copy link
Member

addyosmani commented Nov 14, 2017

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.

Copy link
Member

@addyosmani addyosmani left a 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

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.

we're good on my end.

@brendankenny you want to take another look?

@GoogleChrome GoogleChrome deleted a comment from googlebot Nov 17, 2017
@paulirish paulirish assigned brendankenny and unassigned paulirish Nov 17, 2017
@brendankenny
Copy link
Member

We should definitely prioritize #3702

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.

LGTM! Excited to see this in action

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.

8 participants