| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Lantern from '../lantern.js'; |
| |
| const {ConnectionPool} = Lantern.Simulation; |
| |
| describe('ConnectionPool', () => { |
| const rtt = 100; |
| const throughput = 10000 * 1024; |
| let requestId: number; |
| |
| function request(data: Partial<Lantern.Types.NetworkRequest> = {}): Lantern.Types.NetworkRequest { |
| const url = data.url || 'http://example.com'; |
| const origin = new URL(url).origin; |
| const scheme = url.split(':')[0]; |
| const host = new URL(url).hostname; |
| |
| return { |
| requestId: String(requestId++), |
| url, |
| protocol: 'http/1.1', |
| parsedURL: {scheme, securityOrigin: origin, host}, |
| ...data, |
| } as Lantern.Types.NetworkRequest; |
| } |
| |
| function simulationOptions(options: { |
| rtt?: number, |
| throughput?: number, |
| additionalRttByOrigin?: Map<string, number>, |
| serverResponseTimeByOrigin?: Map<string, number>, |
| }): Required<Lantern.Types.Simulation.Options> { |
| const defaults: Required<Lantern.Types.Simulation.Options> = { |
| rtt: 150, |
| throughput: 1024, |
| additionalRttByOrigin: new Map(), |
| serverResponseTimeByOrigin: new Map(), |
| // These options are not used by ConnectionPool, but are required in the types. |
| cpuSlowdownMultiplier: 1, |
| layoutTaskMultiplier: 1, |
| observedThroughput: 1, |
| maximumConcurrentRequests: 8, |
| }; |
| return Object.assign(defaults, options); |
| } |
| |
| beforeEach(() => { |
| requestId = 1; |
| }); |
| |
| describe('#constructor', () => { |
| it('should create the pool', () => { |
| const pool = new ConnectionPool([request()], simulationOptions({rtt, throughput})); |
| // Make sure 6 connections are created for each origin |
| assert.lengthOf(pool.connectionsByOrigin.get('http://example.com') ?? [], 6); |
| // Make sure it populates connectionWasReused |
| assert.isFalse(pool.connectionReusedByRequestId.get('1')); |
| |
| const connection = pool.connectionsByOrigin.get('http://example.com')?.[0]; |
| assert.isOk(connection); |
| assert.strictEqual(connection.rtt, rtt); |
| assert.strictEqual(connection.throughput, throughput); |
| assert.strictEqual(connection.serverLatency, 30); // sets to default value |
| }); |
| |
| it('should set TLS properly', () => { |
| const recordA = request({url: 'https://example.com'}); |
| const pool = new ConnectionPool([recordA], simulationOptions({rtt, throughput})); |
| const connection = pool.connectionsByOrigin.get('https://example.com')?.[0]; |
| assert.isOk(connection?.ssl, 'should have set connection TLS'); |
| }); |
| |
| it('should set H2 properly', () => { |
| const recordA = request({protocol: 'h2'}); |
| const pool = new ConnectionPool([recordA], simulationOptions({rtt, throughput})); |
| const connection = pool.connectionsByOrigin.get('http://example.com')?.[0]; |
| assert.isOk(connection?.isH2(), 'should have set HTTP/2'); |
| assert.lengthOf(pool.connectionsByOrigin.get('http://example.com') ?? [], 1); |
| }); |
| |
| it('should set origin-specific RTT properly', () => { |
| const additionalRttByOrigin = new Map([['http://example.com', 63]]); |
| const pool = new ConnectionPool([request()], simulationOptions({rtt, throughput, additionalRttByOrigin})); |
| const connection = pool.connectionsByOrigin.get('http://example.com')?.[0]; |
| assert.isOk(connection); |
| assert.strictEqual(connection.rtt, rtt + 63); |
| }); |
| |
| it('should set origin-specific server latency properly', () => { |
| const serverResponseTimeByOrigin = new Map([['http://example.com', 63]]); |
| const pool = new ConnectionPool([request()], simulationOptions({rtt, throughput, serverResponseTimeByOrigin})); |
| const connection = pool.connectionsByOrigin.get('http://example.com')?.[0]; |
| assert.isOk(connection); |
| assert.strictEqual(connection.serverLatency, 63); |
| }); |
| }); |
| |
| describe('.acquire', () => { |
| it('should remember the connection associated with each request', () => { |
| const requestA = request(); |
| const requestB = request(); |
| const pool = new ConnectionPool([requestA, requestB], simulationOptions({rtt, throughput})); |
| |
| const connectionForA = pool.acquire(requestA); |
| const connectionForB = pool.acquire(requestB); |
| for (let i = 0; i < 10; i++) { |
| assert.strictEqual(pool.acquireActiveConnectionFromRequest(requestA), connectionForA); |
| assert.strictEqual(pool.acquireActiveConnectionFromRequest(requestB), connectionForB); |
| } |
| |
| assert.deepEqual(pool.connectionsInUse(), [connectionForA, connectionForB]); |
| }); |
| |
| it('should allocate at least 6 connections', () => { |
| const pool = new ConnectionPool([request()], simulationOptions({rtt, throughput})); |
| for (let i = 0; i < 6; i++) { |
| assert.isOk(pool.acquire(request()), `did not find connection for ${i}th request`); |
| } |
| }); |
| |
| it('should allocate all connections', () => { |
| const records = new Array(7).fill(undefined, 0, 7).map(() => request()); |
| const pool = new ConnectionPool(records, simulationOptions({rtt, throughput})); |
| const connections = records.map(request => pool.acquire(request)); |
| assert.isOk(connections[0], 'did not find connection for 1st request'); |
| assert.isOk(connections[5], 'did not find connection for 6th request'); |
| assert.isOk(connections[6], 'did not find connection for 7th request'); |
| }); |
| |
| it('should be oblivious to connection reuse', () => { |
| const coldRecord = request(); |
| const warmRecord = request(); |
| const pool = new ConnectionPool([coldRecord, warmRecord], simulationOptions({rtt, throughput})); |
| pool.connectionReusedByRequestId.set(warmRecord.requestId, true); |
| |
| assert.isOk(pool.acquire(coldRecord), 'should have acquired connection'); |
| assert.isOk(pool.acquire(warmRecord), 'should have acquired connection'); |
| pool.release(coldRecord); |
| |
| for (const connection of pool.connectionsByOrigin.get('http://example.com') ?? []) { |
| connection.setWarmed(true); |
| } |
| |
| assert.isOk(pool.acquire(coldRecord), 'should have acquired connection'); |
| assert.isOk(pool.acquireActiveConnectionFromRequest(warmRecord), 'should have acquired connection'); |
| }); |
| |
| it('should acquire in order of warmness', () => { |
| const recordA = request(); |
| const recordB = request(); |
| const recordC = request(); |
| const pool = new ConnectionPool([recordA, recordB, recordC], simulationOptions({rtt, throughput})); |
| pool.connectionReusedByRequestId.set(recordA.requestId, true); |
| pool.connectionReusedByRequestId.set(recordB.requestId, true); |
| pool.connectionReusedByRequestId.set(recordC.requestId, true); |
| |
| const connections = pool.connectionsByOrigin.get('http://example.com'); |
| assert.isOk(connections); |
| const [connectionWarm, connectionWarmer, connectionWarmest] = connections; |
| connectionWarm.setWarmed(true); |
| connectionWarm.setCongestionWindow(10); |
| connectionWarmer.setWarmed(true); |
| connectionWarmer.setCongestionWindow(100); |
| connectionWarmest.setWarmed(true); |
| connectionWarmest.setCongestionWindow(1000); |
| |
| assert.strictEqual(pool.acquire(recordA), connectionWarmest); |
| assert.strictEqual(pool.acquire(recordB), connectionWarmer); |
| assert.strictEqual(pool.acquire(recordC), connectionWarm); |
| }); |
| }); |
| |
| describe('.release', () => { |
| it('noop for request without connection', () => { |
| const requestA = request(); |
| const pool = new ConnectionPool([requestA], simulationOptions({rtt, throughput})); |
| assert.isUndefined(pool.release(requestA)); |
| }); |
| |
| it('frees the connection for reissue', () => { |
| const requests = new Array(6).fill(undefined, 0, 7).map(() => request()); |
| const pool = new ConnectionPool(requests, simulationOptions({rtt, throughput})); |
| requests.push(request()); |
| |
| requests.forEach(request => pool.acquire(request)); |
| |
| assert.lengthOf(pool.connectionsInUse(), 6); |
| assert.isNotOk(pool.acquire(requests[6]), 'had connection that is in use'); |
| |
| pool.release(requests[0]); |
| assert.lengthOf(pool.connectionsInUse(), 5); |
| |
| assert.isOk(pool.acquire(requests[6]), 'could not reissue released connection'); |
| assert.isNotOk(pool.acquire(requests[0]), 'had connection that is in use'); |
| }); |
| }); |
| }); |