-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This is a 0.1 definition of Time to Interactive (TTI) which considers
the main thread availability to help identify a better "page is loaded and ready to be used" timestamp. Requirements: * first meaningful paint has hit * we're 85% visually complete * domContentLoaded has fired * The main thread is available enough to handle user (first 500ms window where Est Input Latency is <50ms at the 90% percentile.) Currently, scoring of tti to a 0-100 score hasn't been validated. Also very 0.1. pr ticket: 450 fixes: #27 Many thanks to Brendan Kenny. Cheers to Addy Osmani as well. No tweeting though, guys. ;)
- Loading branch information
Showing
7 changed files
with
276 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
/** | ||
* @license | ||
* Copyright 2016 Google Inc. All rights reserved. | ||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const Audit = require('./audit'); | ||
const TracingProcessor = require('../lib/traces/tracing-processor'); | ||
const FMPMetric = require('./first-meaningful-paint'); | ||
const Formatter = require('../formatters/formatter'); | ||
|
||
// Parameters (in ms) for log-normal CDF scoring. To see the curve: | ||
// https://www.desmos.com/calculator/jlrx14q4w8 | ||
const SCORING_POINT_OF_DIMINISHING_RETURNS = 1700; | ||
const SCORING_MEDIAN = 5000; | ||
|
||
class TTIMetric extends Audit { | ||
/** | ||
* @return {!AuditMeta} | ||
*/ | ||
static get meta() { | ||
return { | ||
category: 'Performance', | ||
name: 'time-to-interactive', | ||
description: 'Time To Interactive (alpha)', | ||
optimalValue: SCORING_POINT_OF_DIMINISHING_RETURNS.toLocaleString(), | ||
requiredArtifacts: ['traceContents', 'speedline'] | ||
}; | ||
} | ||
|
||
/** | ||
* Identify the time the page is "interactive" | ||
* @see https://docs.google.com/document/d/1oiy0_ych1v2ADhyG_QW7Ps4BNER2ShlJjx2zCbVzVyY/edit# | ||
* | ||
* The user thinks the page is ready - (They believe the page is done enough to start interacting with) | ||
* - Layout has stabilized & key webfonts are visible. | ||
* AKA: First meaningful paint has fired. | ||
* - Page is nearly visually complete | ||
* Visual completion is 85% | ||
* | ||
* The page is actually ready for user: | ||
* - domContentLoadedEventEnd has fired | ||
* Definition: HTML parsing has finished, all DOMContentLoaded handlers have run. | ||
* No risk of DCL event handlers changing the page | ||
* No surprises of inactive buttons/actions as DOM element event handlers should be bound | ||
* - The main thread is available enough to handle user input | ||
* first 500ms window where Est Input Latency is <50ms at the 90% percentile. | ||
* | ||
* WARNING: This metric WILL change its calculation. If you rely on its numbers now, know that they | ||
* will be changing in the future to a more accurate number. | ||
* | ||
* @param {!Artifacts} artifacts The artifacts from the gather phase. | ||
* @return {!AuditResult} The score from the audit, ranging from 0-100. | ||
*/ | ||
static audit(artifacts) { | ||
// We start looking at Math.Max(FMPMetric, visProgress[0.85]) | ||
return FMPMetric.audit(artifacts).then(fmpResult => { | ||
if (fmpResult.rawValue === -1) { | ||
return generateError(fmpResult.debugString); | ||
} | ||
const fmpTiming = parseFloat(fmpResult.rawValue); | ||
const timings = fmpResult.extendedInfo && fmpResult.extendedInfo.value && | ||
fmpResult.extendedInfo.value.timings; | ||
|
||
// Process the trace | ||
const tracingProcessor = new TracingProcessor(); | ||
const model = tracingProcessor.init(artifacts.traceContents); | ||
const endOfTraceTime = model.bounds.max; | ||
|
||
// TODO: Wait for DOMContentLoadedEndEvent | ||
// TODO: Wait for UA loading indicator to be done | ||
|
||
// TODO CHECK these units are the same | ||
const fMPts = timings.fMPfull + timings.navStart; | ||
|
||
// look at speedline results for 85% starting at FMP | ||
let eightyFivePctVC = artifacts.Speedline.frames.find(frame => { | ||
return frame.getTimeStamp() >= fMPts && frame.getProgress() >= 85; | ||
}); | ||
|
||
// Check to avoid closure compiler null dereferencing errors | ||
if (eightyFivePctVC === undefined) { | ||
eightyFivePctVC = 0; | ||
} | ||
|
||
// TODO CHECK these units are the same | ||
const visuallyReadyTiming = (eightyFivePctVC.getTimeStamp() - timings.navStart) || 0; | ||
|
||
// Find first 500ms window where Est Input Latency is <50ms at the 90% percentile. | ||
let startTime = Math.max(fmpTiming, visuallyReadyTiming) - 50; | ||
let endTime; | ||
let currentLatency = Infinity; | ||
const percentiles = [0.9]; // [0.75, 0.9, 0.99, 1]; | ||
const threshold = 50; | ||
let foundLatencies = []; | ||
|
||
// When we've found a latency that's good enough, we're good. | ||
while (currentLatency > threshold) { | ||
// While latency is too high, increment just 50ms and look again. | ||
startTime += 50; | ||
endTime = startTime + 500; | ||
// If there's no more room in the trace to look, we're done. | ||
if (endTime > endOfTraceTime) { | ||
return generateError('Entire trace was found to be busy.'); | ||
} | ||
// Get our expected latency for the time window | ||
const latencies = TracingProcessor.getRiskToResponsiveness( | ||
model, artifacts.traceContents, startTime, endTime, percentiles); | ||
const estLatency = latencies[0].time.toFixed(2); | ||
foundLatencies.push({ | ||
estLatency: estLatency, | ||
startTime: startTime.toFixed(1) | ||
}); | ||
|
||
// Grab this latency and try the threshold again | ||
currentLatency = estLatency; | ||
} | ||
const timeToInteractive = startTime.toFixed(1); | ||
|
||
// Use the CDF of a log-normal distribution for scoring. | ||
// < 1200ms: score≈100 | ||
// 5000ms: score=50 | ||
// >= 15000ms: score≈0 | ||
const distribution = TracingProcessor.getLogNormalDistribution(SCORING_MEDIAN, | ||
SCORING_POINT_OF_DIMINISHING_RETURNS); | ||
let score = 100 * distribution.computeComplementaryPercentile(timeToInteractive); | ||
|
||
// Clamp the score to 0 <= x <= 100. | ||
score = Math.min(100, score); | ||
score = Math.max(0, score); | ||
score = Math.round(score); | ||
|
||
const extendedInfo = { | ||
timings: { | ||
fMP: fmpTiming.toFixed(1), | ||
visuallyReady: visuallyReadyTiming.toFixed(1), | ||
mainThreadAvail: startTime.toFixed(1) | ||
}, | ||
expectedLatencyAtTTI: currentLatency, | ||
foundLatencies | ||
}; | ||
|
||
return TTIMetric.generateAuditResult({ | ||
score, | ||
rawValue: timeToInteractive, | ||
displayValue: `${timeToInteractive}ms`, | ||
optimalValue: this.meta.optimalValue, | ||
extendedInfo: { | ||
value: extendedInfo, | ||
formatter: Formatter.SUPPORTED_FORMATS.NULL | ||
} | ||
}); | ||
}).catch(err => { | ||
return generateError(err); | ||
}); | ||
} | ||
} | ||
|
||
module.exports = TTIMetric; | ||
|
||
function generateError(err) { | ||
return TTIMetric.generateAuditResult({ | ||
value: -1, | ||
rawValue: -1, | ||
optimalValue: TTIMetric.meta.optimalValue, | ||
debugString: err | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/** | ||
* Copyright 2016 Google Inc. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
'use strict'; | ||
|
||
const Audit = require('../../audits/time-to-interactive.js'); | ||
const SpeedlineGather = require('../../driver/gatherers/speedline'); | ||
const assert = require('assert'); | ||
|
||
const traceContents = require('../fixtures/traces/progressive-app.json'); | ||
const speedlineGather = new SpeedlineGather(); | ||
|
||
/* eslint-env mocha */ | ||
describe('Performance: time-to-interactive audit', () => { | ||
it('scores a -1 with invalid trace data', () => { | ||
return Audit.audit({ | ||
traceContents: '[{"pid": 15256,"tid": 1295,"t', | ||
Speedline: { | ||
first: 500 | ||
} | ||
}).then(output => { | ||
assert.equal(output.rawValue, -1); | ||
assert(output.debugString); | ||
}); | ||
}); | ||
|
||
it('evaluates valid input correctly', () => { | ||
let artifacts = { | ||
traceContents: traceContents | ||
}; | ||
return speedlineGather.afterPass({}, artifacts).then(_ => { | ||
artifacts.Speedline = speedlineGather.artifact; | ||
return Audit.audit(artifacts).then(output => { | ||
assert.equal(output.rawValue, '1105.8'); | ||
assert.equal(output.extendedInfo.value.expectedLatencyAtTTI, '20.72'); | ||
assert.equal(output.extendedInfo.value.timings.fMP, '1099.5'); | ||
assert.equal(output.extendedInfo.value.timings.mainThreadAvail, '1105.8'); | ||
assert.equal(output.extendedInfo.value.timings.visuallyReady, '1105.8'); | ||
}); | ||
}); | ||
}); | ||
}); |