-
Notifications
You must be signed in to change notification settings - Fork 9.3k
/
time-to-interactive.js
179 lines (159 loc) · 6.76 KB
/
time-to-interactive.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
/**
* @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;
// This aligns with the external TTI targets in https://goo.gl/yXqxpL
const SCORING_TARGET = 5000;
class TTIMetric extends Audit {
/**
* @return {!AuditMeta}
*/
static get meta() {
return {
category: 'Performance',
name: 'time-to-interactive',
description: 'Time To Interactive (alpha)',
optimalValue: SCORING_TARGET.toLocaleString(),
requiredArtifacts: ['traceContents']
};
}
/**
* 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 {!Promise<!AuditResult>} The score from the audit, ranging from 0-100.
*/
static audit(artifacts) {
const trace = artifacts.traces[Audit.DEFAULT_PASS];
const pendingSpeedline = artifacts.requestSpeedline(trace);
const pendingFMP = FMPMetric.audit(artifacts);
// We start looking at Math.Max(FMPMetric, visProgress[0.85])
return Promise.all([pendingSpeedline, pendingFMP]).then(results => {
const speedline = results[0];
const fmpResult = results[1];
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 trace = artifacts.traces[Audit.DEFAULT_PASS];
const model = tracingProcessor.init(trace);
const endOfTraceTime = model.bounds.max;
// TODO: Wait for DOMContentLoadedEndEvent
const fMPts = timings.fMPfull + timings.navStart;
// look at speedline results for 85% starting at FMP
let visuallyReadyTiming = 0;
if (speedline.frames) {
const eightyFivePctVC = speedline.frames.find(frame => {
return frame.getTimeStamp() >= fMPts && frame.getProgress() >= 85;
});
if (eightyFivePctVC) {
visuallyReadyTiming = eightyFivePctVC.getTimeStamp() - timings.navStart;
}
}
// 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;
const 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, trace, 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 = parseFloat(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(startTime);
// 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,
debugString: speedline.debugString,
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.message || err
});
}