diff --git a/test/conductor/BUILD.gn b/test/conductor/BUILD.gn index 840c80c077..a635ef8ed1 100644 --- a/test/conductor/BUILD.gn +++ b/test/conductor/BUILD.gn @@ -19,6 +19,7 @@ node_ts_library("implementation") { "frontend_tab.ts", "hooks.ts", "mocha_hooks.ts", + "pool.ts", "puppeteer-state.ts", "target_tab.ts", "test_runner_config.ts", diff --git a/test/conductor/frontend_tab.ts b/test/conductor/frontend_tab.ts index 488e27674a..7f2f295bd1 100644 --- a/test/conductor/frontend_tab.ts +++ b/test/conductor/frontend_tab.ts @@ -40,6 +40,8 @@ export class DevToolsFrontendTab { name: 'elements', selector: '.elements', }; + // We use the counter to give each tab a unique origin. + private static tabCounter = 0; private constructor(readonly page: puppeteer.Page, frontendUrl: string) { this.#frontendUrl = frontendUrl; @@ -56,7 +58,10 @@ export class DevToolsFrontendTab { // We load the DevTools frontend on a unique origin. Otherwise we would share 'localhost' with // target pages. This could cause difficult to debug problems as things like window.localStorage // would be shared and requests would be "same-origin". - const frontendUrl = `https://i1.devtools-frontend.test:${testServerPort}/${devToolsAppURL}?ws=localhost:${ + // We also use a unique ID per DevTools frontend instance, to avoid the same issue with other + // frontend instances. + const id = DevToolsFrontendTab.tabCounter++; + const frontendUrl = `https://i${id}.devtools-frontend.test:${testServerPort}/${devToolsAppURL}?ws=localhost:${ getDebugPort(browser)}/devtools/page/${targetId}`; const frontend = await browser.newPage(); diff --git a/test/conductor/pool.ts b/test/conductor/pool.ts new file mode 100644 index 0000000000..72945c4dac --- /dev/null +++ b/test/conductor/pool.ts @@ -0,0 +1,109 @@ +// Copyright 2022 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// use require here due to +// https://github.com/evanw/esbuild/issues/587#issuecomment-901397213 +import puppeteer = require('puppeteer'); + +import {DevToolsFrontendTab} from './frontend_tab.js'; +import {TargetTab} from './target_tab.js'; + +interface FrontendTargetTabs { + frontend: DevToolsFrontendTab; + target: TargetTab; +} + +export interface FrontedTargetPoolOptions { + browser: puppeteer.Browser; + testServerPort: number; + /** + * Defaults to FrontendTargetPool.POOL_SIZE. + * Setting `poolSize` to 0 will create target/frontend pairs on-demand. + */ + poolSize?: number; +} + +/** + * A pool of DevTools frontend tab plus target tab pairs. Every time a pair + * is taken from the pool, the pool will automatically prepare another pair. + * + * Tab pairs are not intended to be reused and its the consumer's responsibility + * to clean them up properly. + */ +export class FrontendTargetPool { + private static readonly POOL_SIZE = 5; + + #pool = new Pool(); + #browser: puppeteer.Browser; + #testServerPort: number; + + private constructor(browser: puppeteer.Browser, testServerPort: number) { + this.#browser = browser; + this.#testServerPort = testServerPort; + } + + /** Returns a pool with `options.poolSize` tab pairs ready to go. */ + static create(options: FrontedTargetPoolOptions): FrontendTargetPool { + const {browser, testServerPort, poolSize = FrontendTargetPool.POOL_SIZE} = options; + + const tabPool = new FrontendTargetPool(browser, testServerPort); + for (let i = 0; i < poolSize; ++i) { + void tabPool.addTabPairToPool(); + } + return tabPool; + } + + private async addTabPairToPool(): Promise { + const target = await TargetTab.create(this.#browser); + const frontend = await DevToolsFrontendTab.create( + {browser: this.#browser, testServerPort: this.#testServerPort, targetId: target.targetId()}); + this.#pool.put({target, frontend}); + } + + async takeTabPair(): Promise { + const pair = this.#pool.take(); + + // We took a pair, so lets queue up the creation of a fresh pair. + // It's important that we do not block here, the fresh pair is prepared + // in the background. + // Also note that this approach allows a pool size of 0. For every + // `takeTabPair`, we call `addTabPairToPool`. The resulting pair is then + // used to resolve the `pair` promise of the `takeTabPair` call. + void this.addTabPairToPool(); + + return pair; + } +} + +/** + * A simple Promise-based pool. Esentially a queue. + * + * When the pool is empty, consumers get a promise that resolves as soon as + * the pool is refilled. + */ +export class Pool { + #pool: T[] = []; + #queue: Array<(value: T) => void> = []; + + put(value: T) { + if (this.#queue.length > 0) { + const waitee = this.#queue.shift() as (value: T) => void; + waitee(value); + return; + } + + this.#pool.push(value); + } + + take(): Promise { + if (this.#pool.length > 0) { + const value = this.#pool.shift() as T; + return Promise.resolve(value); + } + + return new Promise(resolve => { + this.#queue.push(resolve); + }); + } +}