Skip to content

Commit

Permalink
feat(predictive-perf): add basic H2 support
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhulce committed Aug 23, 2017
1 parent 8e4bc96 commit ee098da
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 27 deletions.
Expand Up @@ -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
Expand All @@ -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);
}
Expand Down Expand Up @@ -283,6 +284,7 @@ class Estimator {
);

connection.setCongestionWindow(calculation.congestionWindow);
connection.setExtraBytesDownloaded(calculation.extraBytesDownloaded);

if (isFinished) {
connection.setWarmed(true);
Expand Down
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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.
Expand All @@ -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();
Expand All @@ -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,
};
}
Expand Down
10 changes: 5 additions & 5 deletions lighthouse-core/test/audits/predictive-perf-test.js
Expand Up @@ -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);
});
});
Expand Down
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand Down

0 comments on commit ee098da

Please sign in to comment.