diff --git a/lighthouse-core/gather/computed/dependency-graph/estimator/estimator.js b/lighthouse-core/gather/computed/dependency-graph/estimator/estimator.js index e5c96d33121c..d958557f24d1 100644 --- a/lighthouse-core/gather/computed/dependency-graph/estimator/estimator.js +++ b/lighthouse-core/gather/computed/dependency-graph/estimator/estimator.js @@ -84,6 +84,7 @@ class Estimator { for (const [connectionId, records] of recordsByConnection.entries()) { const isSsl = records[0].parsedURL.scheme === 'https'; + const isH2 = records[0].protocol === 'h2'; let responseTime = records.reduce( (min, record) => Math.min(min, Estimator.getResponseTime(record)), Infinity @@ -93,7 +94,7 @@ class Estimator { responseTime = this._defaultResponseTime; } - const connection = new TcpConnection(this._rtt, this._throughput, responseTime, isSsl); + const connection = new TcpConnection(this._rtt, this._throughput, responseTime, isSsl, isH2); connections.set(connectionId, connection); } @@ -283,6 +284,7 @@ class Estimator { ); connection.setCongestionWindow(calculation.congestionWindow); + connection.setExtraBytesDownloaded(calculation.extraBytesDownloaded); if (isFinished) { connection.setWarmed(true); diff --git a/lighthouse-core/gather/computed/dependency-graph/estimator/tcp-connection.js b/lighthouse-core/gather/computed/dependency-graph/estimator/tcp-connection.js index 19ca55d80304..d43c4efea73a 100644 --- a/lighthouse-core/gather/computed/dependency-graph/estimator/tcp-connection.js +++ b/lighthouse-core/gather/computed/dependency-graph/estimator/tcp-connection.js @@ -14,14 +14,17 @@ class TcpConnection { * @param {number} throughput * @param {number=} responseTime * @param {boolean=} ssl + * @param {boolean=} h2 */ - constructor(rtt, throughput, responseTime = 0, ssl = true) { + constructor(rtt, throughput, responseTime = 0, ssl = true, h2 = false) { this._warmed = false; this._ssl = ssl; + this._h2 = h2; this._rtt = rtt; this._availableThroughput = throughput; this._responseTime = responseTime; this._congestionWindow = INITIAL_CONGESTION_WINDOW; + this._overflowBytesDownloaded = 0; } /** @@ -68,6 +71,14 @@ class TcpConnection { this._warmed = warmed; } + /** + * @param {number} bytes + */ + setExtraBytesDownloaded(bytes) { + if (!this._h2) return; + this._overflowBytesDownloaded = bytes; + } + /** * Simulates a network download of a particular number of bytes over an optional maximum amount of time * and returns information about the ending state. @@ -80,7 +91,15 @@ class TcpConnection { * @param {number=} maximumTimeToElapse * @return {{timeElapsed: number, roundTrips: number, bytesDownloaded: number, congestionWindow: number}} */ - calculateTimeToDownload(bytesToDownload, timeAlreadyElapsed = 0, maximumTimeToElapse = Infinity) { + calculateTimeToDownload( + bytesToDownload, + timeAlreadyElapsed = 0, + maximumTimeToElapse = Infinity + ) { + if (this._warmed && this._h2) { + bytesToDownload -= this._overflowBytesDownloaded; + } + const twoWayLatency = this._rtt; const oneWayLatency = twoWayLatency / 2; const maximumCongestionWindow = this._computeMaximumCongestionWindowInSegments(); @@ -99,46 +118,41 @@ class TcpConnection { } let roundTrips = Math.ceil(handshakeAndRequest / twoWayLatency); - const timeToFirstByte = handshakeAndRequest + this._responseTime + oneWayLatency; + let timeToFirstByte = handshakeAndRequest + this._responseTime + oneWayLatency; + if (this._warmed && this._h2) timeToFirstByte = 0; + const timeElapsedForTTFB = Math.max(timeToFirstByte - timeAlreadyElapsed, 0); const maximumDownloadTimeToElapse = maximumTimeToElapse - timeElapsedForTTFB; let congestionWindow = Math.min(this._congestionWindow, maximumCongestionWindow); - let bytesDownloaded = 0; + let totalBytesDownloaded = 0; if (timeElapsedForTTFB > 0) { - bytesDownloaded = congestionWindow * TCP_SEGMENT_SIZE; + totalBytesDownloaded = congestionWindow * TCP_SEGMENT_SIZE; } else { roundTrips = 0; } let downloadTimeElapsed = 0; - let bytesRemaining = bytesToDownload - bytesDownloaded; + let bytesRemaining = bytesToDownload - totalBytesDownloaded; while (bytesRemaining > 0 && downloadTimeElapsed <= maximumDownloadTimeToElapse) { roundTrips++; downloadTimeElapsed += twoWayLatency; congestionWindow = Math.max(Math.min(maximumCongestionWindow, congestionWindow * 2), 1); const bytesDownloadedInWindow = congestionWindow * TCP_SEGMENT_SIZE; - bytesDownloaded += bytesDownloadedInWindow; + totalBytesDownloaded += bytesDownloadedInWindow; bytesRemaining -= bytesDownloadedInWindow; } const timeElapsed = timeElapsedForTTFB + downloadTimeElapsed; - bytesDownloaded = Math.min(bytesDownloaded, bytesToDownload); - - if (Number.isFinite(maximumTimeToElapse)) { - return { - roundTrips, - timeElapsed, - bytesDownloaded, - congestionWindow, - }; - } + const extraBytesDownloaded = this._h2 ? Math.max(totalBytesDownloaded - bytesToDownload, 0) : 0; + const bytesDownloaded = Math.max(Math.min(totalBytesDownloaded, bytesToDownload), 0); return { roundTrips, timeElapsed, bytesDownloaded, + extraBytesDownloaded, congestionWindow, }; } diff --git a/lighthouse-core/test/audits/predictive-perf-test.js b/lighthouse-core/test/audits/predictive-perf-test.js index 7c88058d70a6..24b5e9205a31 100644 --- a/lighthouse-core/test/audits/predictive-perf-test.js +++ b/lighthouse-core/test/audits/predictive-perf-test.js @@ -25,14 +25,14 @@ describe('Performance: predictive performance audit', () => { }, Runner.instantiateComputedArtifacts()); return PredictivePerf.audit(artifacts).then(output => { - assert.equal(output.score, 95); - assert.equal(Math.round(output.rawValue), 2638); - assert.equal(output.displayValue, '2,640\xa0ms'); + assert.equal(output.score, 96); + assert.equal(Math.round(output.rawValue), 2453); + assert.equal(output.displayValue, '2,450\xa0ms'); const valueOf = name => Math.round(output.extendedInfo.value[name]); - assert.equal(valueOf('optimisticFMP'), 1362); + assert.equal(valueOf('optimisticFMP'), 754); assert.equal(valueOf('pessimisticFMP'), 2070); - assert.equal(valueOf('optimisticTTCI'), 3470); + assert.equal(valueOf('optimisticTTCI'), 3340); assert.equal(valueOf('pessimisticTTCI'), 3649); }); }); diff --git a/lighthouse-core/test/gather/computed/dependency-graph/estimator/tcp-connection-test.js b/lighthouse-core/test/gather/computed/dependency-graph/estimator/tcp-connection-test.js index 1fc639ae6dac..628bf2ed8525 100644 --- a/lighthouse-core/test/gather/computed/dependency-graph/estimator/tcp-connection-test.js +++ b/lighthouse-core/test/gather/computed/dependency-graph/estimator/tcp-connection-test.js @@ -45,26 +45,51 @@ describe('DependencyGraph/Estimator/TcpConnection', () => { const connection = new TcpConnection(100, Infinity); assert.deepEqual(connection.calculateTimeToDownload(50000), { bytesDownloaded: 50000, + extraBytesDownloaded: 0, congestionWindow: 40, roundTrips: 5, timeElapsed: 500, }); - connection.setCongestionWindow(80); // will download all in one round trip + connection.setCongestionWindow(40); // will download all in one round trip assert.deepEqual(connection.calculateTimeToDownload(50000), { bytesDownloaded: 50000, - congestionWindow: 80, + extraBytesDownloaded: 0, + congestionWindow: 40, roundTrips: 3, timeElapsed: 300, }); }); }); + describe('.setExtraBytesDownloaded', () => { + it('adjusts the time to download appropriately for H2 connections', () => { + const connection = new TcpConnection(100, Infinity, 0, true, true); + connection.setWarmed(true); + assert.equal(connection.calculateTimeToDownload(30000).timeElapsed, 200); + connection.setExtraBytesDownloaded(20000); + assert.equal(connection.calculateTimeToDownload(30000).timeElapsed, 100); + connection.setExtraBytesDownloaded(50000); + assert.equal(connection.calculateTimeToDownload(30000).timeElapsed, 0); + }); + + it('does not adjust the time to download for non-H2 connections', () => { + const connection = new TcpConnection(100, Infinity, 0, true, false); + connection.setWarmed(true); + assert.equal(connection.calculateTimeToDownload(30000).timeElapsed, 200); + connection.setExtraBytesDownloaded(20000); + assert.equal(connection.calculateTimeToDownload(30000).timeElapsed, 200); + connection.setExtraBytesDownloaded(50000); + assert.equal(connection.calculateTimeToDownload(30000).timeElapsed, 200); + }); + }); + describe('.calculateTimeToDownload', () => { context('when maximumTime is not set', () => { it('should provide the correct values small payload non-SSL', () => { const connection = new TcpConnection(100, Infinity, 0, false); assert.deepEqual(connection.calculateTimeToDownload(7300), { bytesDownloaded: 7300, + extraBytesDownloaded: 0, congestionWindow: 10, roundTrips: 2, timeElapsed: 200, @@ -75,6 +100,18 @@ describe('DependencyGraph/Estimator/TcpConnection', () => { const connection = new TcpConnection(100, Infinity, 0, true); assert.deepEqual(connection.calculateTimeToDownload(7300), { bytesDownloaded: 7300, + extraBytesDownloaded: 0, + congestionWindow: 10, + roundTrips: 3, + timeElapsed: 300, + }); + }); + + it('should provide the correct values small payload H2', () => { + const connection = new TcpConnection(100, Infinity, 0, true, true); + assert.deepEqual(connection.calculateTimeToDownload(7300), { + bytesDownloaded: 7300, + extraBytesDownloaded: 7300, congestionWindow: 10, roundTrips: 3, timeElapsed: 300, @@ -86,6 +123,7 @@ describe('DependencyGraph/Estimator/TcpConnection', () => { const connection = new TcpConnection(100, Infinity, responseTime, true); assert.deepEqual(connection.calculateTimeToDownload(7300), { bytesDownloaded: 7300, + extraBytesDownloaded: 0, congestionWindow: 10, roundTrips: 3, timeElapsed: 300 + responseTime, @@ -97,6 +135,7 @@ describe('DependencyGraph/Estimator/TcpConnection', () => { const bytesToDownload = 10 * 1000 * 1000; // 10 mb assert.deepEqual(connection.calculateTimeToDownload(bytesToDownload), { bytesDownloaded: bytesToDownload, + extraBytesDownloaded: 0, congestionWindow: 68, roundTrips: 105, timeElapsed: 10500, @@ -105,20 +144,35 @@ describe('DependencyGraph/Estimator/TcpConnection', () => { it('should provide the correct values resumed small payload', () => { const connection = new TcpConnection(100, Infinity, 0, true); - assert.deepEqual(connection.calculateTimeToDownload(7300, 250), { - bytesDownloaded: 7300, + assert.deepEqual(connection.calculateTimeToDownload(5000, 250), { + bytesDownloaded: 5000, + extraBytesDownloaded: 0, congestionWindow: 10, roundTrips: 3, timeElapsed: 50, }); }); + it('should provide the correct values resumed small payload H2', () => { + const connection = new TcpConnection(100, Infinity, 0, true, true); + connection.setWarmed(true); + connection.setExtraBytesDownloaded(10000); + assert.deepEqual(connection.calculateTimeToDownload(7300), { + bytesDownloaded: 0, + extraBytesDownloaded: 2700, // 10000 - 7300 + congestionWindow: 10, + roundTrips: 0, + timeElapsed: 0, + }); + }); + it('should provide the correct values resumed large payload', () => { const connection = new TcpConnection(100, 8 * 1000 * 1000); const bytesToDownload = 5 * 1000 * 1000; // 5 mb connection.setCongestionWindow(68); assert.deepEqual(connection.calculateTimeToDownload(bytesToDownload, 5234), { bytesDownloaded: bytesToDownload, + extraBytesDownloaded: 0, congestionWindow: 68, roundTrips: 51, // 5 mb / (1460 * 68) timeElapsed: 5100, @@ -131,6 +185,7 @@ describe('DependencyGraph/Estimator/TcpConnection', () => { const connection = new TcpConnection(100, Infinity, 0, false); assert.deepEqual(connection.calculateTimeToDownload(7300, 0, 68), { bytesDownloaded: 7300, + extraBytesDownloaded: 0, congestionWindow: 10, roundTrips: 2, timeElapsed: 200, @@ -141,6 +196,7 @@ describe('DependencyGraph/Estimator/TcpConnection', () => { const connection = new TcpConnection(100, Infinity, 0, false); assert.deepEqual(connection.calculateTimeToDownload(7300, 0, 250), { bytesDownloaded: 7300, + extraBytesDownloaded: 0, congestionWindow: 10, roundTrips: 2, timeElapsed: 200, @@ -151,6 +207,7 @@ describe('DependencyGraph/Estimator/TcpConnection', () => { const connection = new TcpConnection(100, Infinity, 0, false); assert.deepEqual(connection.calculateTimeToDownload(7300, 75, 250), { bytesDownloaded: 7300, + extraBytesDownloaded: 0, congestionWindow: 10, roundTrips: 2, timeElapsed: 125, @@ -162,6 +219,7 @@ describe('DependencyGraph/Estimator/TcpConnection', () => { const bytesToDownload = 10 * 1000 * 1000; // 10 mb assert.deepEqual(connection.calculateTimeToDownload(bytesToDownload, 500, 740), { bytesDownloaded: 683280, // should be less than 68 * 1460 * 8 + extraBytesDownloaded: 0, congestionWindow: 68, roundTrips: 8, timeElapsed: 800, // skips the handshake because time already elapsed