| // 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 Core from '../core/core.js'; |
| import type * as Lantern from '../types/types.js'; |
| |
| import {TCPConnection} from './TCPConnection.js'; |
| |
| const DEFAULT_SERVER_RESPONSE_TIME = 30; |
| const TLS_SCHEMES = ['https', 'wss']; |
| |
| // Each origin can have 6 simultaneous connections open |
| // https://cs.chromium.org/chromium/src/net/socket/client_socket_pool_manager.cc?type=cs&q="int+g_max_sockets_per_group" |
| const CONNECTIONS_PER_ORIGIN = 6; |
| |
| export class ConnectionPool { |
| options: Required<Lantern.Simulation.Options>; |
| records: Lantern.NetworkRequest[]; |
| connectionsByOrigin: Map<string, TCPConnection[]>; |
| connectionsByRequest: Map<Lantern.NetworkRequest, TCPConnection>; |
| _connectionsInUse: Set<TCPConnection>; |
| connectionReusedByRequestId: Map<string, boolean>; |
| |
| constructor(records: Lantern.NetworkRequest[], options: Required<Lantern.Simulation.Options>) { |
| this.options = options; |
| |
| this.records = records; |
| this.connectionsByOrigin = new Map(); |
| this.connectionsByRequest = new Map(); |
| this._connectionsInUse = new Set(); |
| this.connectionReusedByRequestId = Core.NetworkAnalyzer.estimateIfConnectionWasReused(records, { |
| forceCoarseEstimates: true, |
| }); |
| |
| this.initializeConnections(); |
| } |
| |
| connectionsInUse(): TCPConnection[] { |
| return Array.from(this._connectionsInUse); |
| } |
| |
| initializeConnections(): void { |
| const connectionReused = this.connectionReusedByRequestId; |
| const additionalRttByOrigin = this.options.additionalRttByOrigin; |
| const serverResponseTimeByOrigin = this.options.serverResponseTimeByOrigin; |
| |
| const recordsByOrigin = Core.NetworkAnalyzer.groupByOrigin(this.records); |
| for (const [origin, requests] of recordsByOrigin.entries()) { |
| const connections = []; |
| const additionalRtt = additionalRttByOrigin.get(origin) || 0; |
| const responseTime = serverResponseTimeByOrigin.get(origin) || DEFAULT_SERVER_RESPONSE_TIME; |
| |
| for (const request of requests) { |
| if (connectionReused.get(request.requestId)) { |
| continue; |
| } |
| |
| const isTLS = TLS_SCHEMES.includes(request.parsedURL.scheme); |
| const isH2 = request.protocol === 'h2'; |
| const connection = new TCPConnection( |
| this.options.rtt + additionalRtt, |
| this.options.throughput, |
| responseTime, |
| isTLS, |
| isH2, |
| ); |
| |
| connections.push(connection); |
| } |
| |
| if (!connections.length) { |
| throw new Core.LanternError(`Could not find a connection for origin: ${origin}`); |
| } |
| |
| // Make sure each origin has minimum number of connections available for max throughput. |
| // But only if it's not over H2 which maximizes throughput already. |
| const minConnections = connections[0].isH2() ? 1 : CONNECTIONS_PER_ORIGIN; |
| while (connections.length < minConnections) { |
| connections.push(connections[0].clone()); |
| } |
| |
| this.connectionsByOrigin.set(origin, connections); |
| } |
| } |
| |
| findAvailableConnectionWithLargestCongestionWindow(connections: TCPConnection[]): TCPConnection|null { |
| let maxConnection: TCPConnection|null = null; |
| for (let i = 0; i < connections.length; i++) { |
| const connection = connections[i]; |
| |
| // Connections that are in use are never available. |
| if (this._connectionsInUse.has(connection)) { |
| continue; |
| } |
| |
| // This connection is a match and is available! Update our max if it has a larger congestionWindow |
| const currentMax = (maxConnection?.congestionWindow) || -Infinity; |
| if (connection.congestionWindow > currentMax) { |
| maxConnection = connection; |
| } |
| } |
| |
| return maxConnection; |
| } |
| |
| /** |
| * This method finds an available connection to the origin specified by the network request or null |
| * if no connection was available. If returned, connection will not be available for other network |
| * records until release is called. |
| */ |
| acquire(request: Lantern.NetworkRequest): TCPConnection|null { |
| if (this.connectionsByRequest.has(request)) { |
| throw new Core.LanternError('Record already has a connection'); |
| } |
| |
| const origin = request.parsedURL.securityOrigin; |
| const connections = this.connectionsByOrigin.get(origin) || []; |
| const connectionToUse = this.findAvailableConnectionWithLargestCongestionWindow(connections); |
| |
| if (!connectionToUse) { |
| return null; |
| } |
| |
| this._connectionsInUse.add(connectionToUse); |
| this.connectionsByRequest.set(request, connectionToUse); |
| return connectionToUse; |
| } |
| |
| /** |
| * Return the connection currently being used to fetch a request. If no connection |
| * currently being used for this request, an error will be thrown. |
| */ |
| acquireActiveConnectionFromRequest(request: Lantern.NetworkRequest): TCPConnection { |
| const activeConnection = this.connectionsByRequest.get(request); |
| if (!activeConnection) { |
| throw new Core.LanternError('Could not find an active connection for request'); |
| } |
| |
| return activeConnection; |
| } |
| |
| release(request: Lantern.NetworkRequest): void { |
| const connection = this.connectionsByRequest.get(request); |
| this.connectionsByRequest.delete(request); |
| if (connection) { |
| this._connectionsInUse.delete(connection); |
| } |
| } |
| } |