From 90ec8383189d4d122e68c51ef7f43528ccdfa754 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Feb 2025 13:06:03 -0800 Subject: [PATCH] chore: move zones into platform (#34786) --- .../src/client/browserContext.ts | 2 +- .../src/client/channelOwner.ts | 5 +-- .../playwright-core/src/client/connection.ts | 3 +- .../playwright-core/src/client/network.ts | 11 +++-- packages/playwright-core/src/client/page.ts | 2 +- packages/playwright-core/src/client/waiter.ts | 5 +-- .../playwright-core/src/common/platform.ts | 20 +++++++++- .../src/server/utils/nodePlatform.ts | 40 ++++++++++++++++++- .../src/{ => server}/utils/zones.ts | 33 ++++----------- packages/playwright-core/src/utils.ts | 2 +- packages/playwright/src/common/testType.ts | 4 +- packages/playwright/src/index.ts | 4 +- packages/playwright/src/matchers/expect.ts | 4 +- packages/playwright/src/worker/testInfo.ts | 4 +- 14 files changed, 85 insertions(+), 54 deletions(-) rename packages/playwright-core/src/{ => server}/utils/zones.ts (63%) diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 02aa857366ccb..d03cfcdecac9d 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -338,7 +338,7 @@ export class BrowserContext extends ChannelOwner } async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new network.RouteHandler(this._options.baseURL, url, handler, options.times)); + this._routes.unshift(new network.RouteHandler(this._platform, this._options.baseURL, url, handler, options.times)); await this._updateInterceptionPatterns(); } diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 40bf226c65e67..92deaecabd59d 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -18,7 +18,6 @@ import { EventEmitter } from './eventEmitter'; import { ValidationError, maybeFindValidator } from '../protocol/validator'; import { isUnderTest } from '../utils/isomorphic/debug'; import { captureLibraryStackTrace, stringifyStackFrames } from '../utils/isomorphic/stackTrace'; -import { zones } from '../utils/zones'; import type { ClientInstrumentation } from './clientInstrumentation'; import type { Connection } from './connection'; @@ -176,7 +175,7 @@ export abstract class ChannelOwner(func: (apiZone: ApiZone) => Promise, isInternal?: boolean): Promise { const logger = this._logger; - const existingApiZone = zones.zoneData('apiZone'); + const existingApiZone = this._platform.zones.current().data(); if (existingApiZone) return await func(existingApiZone); @@ -186,7 +185,7 @@ export abstract class ChannelOwner await func(apiZone)); + const result = await this._platform.zones.current().push(apiZone).run(async () => await func(apiZone)); if (!isInternal) { logApiCall(this._platform, logger, `<= ${apiZone.apiName} succeeded`); this._instrumentation.onApiCallEnd(apiZone); diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index e37c949f1f602..963b13a77fe3e 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -43,7 +43,6 @@ import { Worker } from './worker'; import { WritableStream } from './writableStream'; import { ValidationError, findValidator } from '../protocol/validator'; import { formatCallLog, rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; -import { zones } from '../utils/zones'; import type { ClientInstrumentation } from './clientInstrumentation'; import type { HeadersArray } from './types'; @@ -148,7 +147,7 @@ export class Connection extends EventEmitter { this._localUtils?.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {}); // We need to exit zones before calling into the server, otherwise // when we receive events from the server, we would be in an API zone. - zones.empty().run(() => this.onmessage({ ...message, metadata })); + this.platform.zones.empty.run(() => this.onmessage({ ...message, metadata })); return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method })); } diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index a2c64c08b1334..3770ca1320c80 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -30,7 +30,6 @@ import { LongStandingScope, ManualPromise } from '../utils/isomorphic/manualProm import { MultiMap } from '../utils/isomorphic/multimap'; import { isRegExp, isString } from '../utils/isomorphic/rtti'; import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; -import { zones } from '../utils/zones'; import { mime } from '../utilsBundle'; import type { BrowserContext } from './browserContext'; @@ -40,8 +39,8 @@ import type { Serializable } from '../../types/structs'; import type * as api from '../../types/types'; import type { HeadersArray } from '../common/types'; import type { URLMatch } from '../utils/isomorphic/urlMatch'; -import type { Zone } from '../utils/zones'; import type * as channels from '@protocol/channels'; +import type { Platform, Zone } from '../common/platform'; export type NetworkCookie = { name: string, @@ -821,14 +820,14 @@ export class RouteHandler { readonly handler: RouteHandlerCallback; private _ignoreException: boolean = false; private _activeInvocations: Set<{ complete: Promise, route: Route }> = new Set(); - private _svedZone: Zone; + private _savedZone: Zone; - constructor(baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { + constructor(platform: Platform, baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { this._baseURL = baseURL; this._times = times; this.url = url; this.handler = handler; - this._svedZone = zones.current().without('apiZone'); + this._savedZone = platform.zones.current().pop(); } static prepareInterceptionPatterns(handlers: RouteHandler[]) { @@ -852,7 +851,7 @@ export class RouteHandler { } public async handle(route: Route): Promise { - return await this._svedZone.run(async () => this._handleImpl(route)); + return await this._savedZone.run(async () => this._handleImpl(route)); } private async _handleImpl(route: Route): Promise { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index b954cca502547..062217937cd74 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -520,7 +520,7 @@ export class Page extends ChannelOwner implements api.Page } async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new RouteHandler(this._browserContext._options.baseURL, url, handler, options.times)); + this._routes.unshift(new RouteHandler(this._platform, this._browserContext._options.baseURL, url, handler, options.times)); await this._updateInterceptionPatterns(); } diff --git a/packages/playwright-core/src/client/waiter.ts b/packages/playwright-core/src/client/waiter.ts index e17f93d940c80..7d07d2128ed25 100644 --- a/packages/playwright-core/src/client/waiter.ts +++ b/packages/playwright-core/src/client/waiter.ts @@ -16,12 +16,11 @@ import { TimeoutError } from './errors'; import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; -import { zones } from '../utils/zones'; import type { ChannelOwner } from './channelOwner'; -import type { Zone } from '../utils/zones'; import type * as channels from '@protocol/channels'; import type { EventEmitter } from 'events'; +import type { Zone } from '../common/platform'; export class Waiter { private _dispose: (() => void)[]; @@ -36,7 +35,7 @@ export class Waiter { constructor(channelOwner: ChannelOwner, event: string) { this._waitId = channelOwner._platform.createGuid(); this._channelOwner = channelOwner; - this._savedZone = zones.current().without('apiZone'); + this._savedZone = channelOwner._platform.zones.current().pop(); this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {}); this._dispose = [ diff --git a/packages/playwright-core/src/common/platform.ts b/packages/playwright-core/src/common/platform.ts index 64cef77aabbc3..bb73ec6d31935 100644 --- a/packages/playwright-core/src/common/platform.ts +++ b/packages/playwright-core/src/common/platform.ts @@ -22,6 +22,19 @@ import { webColors, noColors } from '../utils/isomorphic/colors'; import type { Colors } from '../utils/isomorphic/colors'; +export type Zone = { + push(data: unknown): Zone; + pop(): Zone; + run(func: () => R): R; + data(): T | undefined; +}; + +const noopZone: Zone = { + push: () => noopZone, + pop: () => noopZone, + run: func => func(), + data: () => undefined, +}; export type Platform = { calculateSha1(text: string): Promise; @@ -34,6 +47,7 @@ export type Platform = { path: () => typeof path; pathSeparator: string; ws?: (url: string) => WebSocket; + zones: { empty: Zone, current: () => Zone; }; }; export const webPlatform: Platform = { @@ -69,6 +83,8 @@ export const webPlatform: Platform = { pathSeparator: '/', ws: (url: string) => new WebSocket(url), + + zones: { empty: noopZone, current: () => noopZone }, }; export const emptyPlatform: Platform = { @@ -98,5 +114,7 @@ export const emptyPlatform: Platform = { throw new Error('Function not implemented.'); }, - pathSeparator: '/' + pathSeparator: '/', + + zones: { empty: noopZone, current: () => noopZone }, }; diff --git a/packages/playwright-core/src/server/utils/nodePlatform.ts b/packages/playwright-core/src/server/utils/nodePlatform.ts index d4b8b5d7ad495..49f6b23ffe25d 100644 --- a/packages/playwright-core/src/server/utils/nodePlatform.ts +++ b/packages/playwright-core/src/server/utils/nodePlatform.ts @@ -20,8 +20,39 @@ import * as path from 'path'; import * as util from 'util'; import { colors } from '../../utilsBundle'; -import { Platform } from '../../common/platform'; import { debugLogger } from './debugLogger'; +import { currentZone, emptyZone } from './zones'; + +import type { Platform, Zone } from '../../common/platform'; +import type { Zone as ZoneImpl } from './zones'; + +class NodeZone implements Zone { + private _zone: ZoneImpl; + + constructor(zone: ZoneImpl) { + this._zone = zone; + } + + push(data: T) { + return new NodeZone(this._zone.with('apiZone', data)); + } + + pop() { + return new NodeZone(this._zone.without('apiZone')); + } + + run(func: () => R): R { + return this._zone.run(func); + } + + runIgnoreCurrent(func: () => R): R { + return emptyZone.run(func); + } + + data(): T | undefined { + return this._zone.data('apiZone'); + } +} export const nodePlatform: Platform = { calculateSha1: (text: string) => { @@ -48,5 +79,10 @@ export const nodePlatform: Platform = { path: () => path, - pathSeparator: path.sep + pathSeparator: path.sep, + + zones: { + current: () => new NodeZone(currentZone()), + empty: new NodeZone(emptyZone), + } }; diff --git a/packages/playwright-core/src/utils/zones.ts b/packages/playwright-core/src/server/utils/zones.ts similarity index 63% rename from packages/playwright-core/src/utils/zones.ts rename to packages/playwright-core/src/server/utils/zones.ts index 32664c3898fa4..b5860167b0696 100644 --- a/packages/playwright-core/src/utils/zones.ts +++ b/packages/playwright-core/src/server/utils/zones.ts @@ -18,36 +18,13 @@ import { AsyncLocalStorage } from 'async_hooks'; export type ZoneType = 'apiZone' | 'stepZone'; -class ZoneManager { - private readonly _asyncLocalStorage = new AsyncLocalStorage(); - private readonly _emptyZone = Zone.createEmpty(this._asyncLocalStorage); - - run(type: ZoneType, data: T, func: () => R): R { - return this.current().with(type, data).run(func); - } - - zoneData(type: ZoneType): T | undefined { - return this.current().data(type); - } - - current(): Zone { - return this._asyncLocalStorage.getStore() ?? this._emptyZone; - } - - empty(): Zone { - return this._emptyZone; - } -} +const asyncLocalStorage = new AsyncLocalStorage(); export class Zone { private readonly _asyncLocalStorage: AsyncLocalStorage; private readonly _data: ReadonlyMap; - static createEmpty(asyncLocalStorage: AsyncLocalStorage) { - return new Zone(asyncLocalStorage, new Map()); - } - - private constructor(asyncLocalStorage: AsyncLocalStorage, store: Map) { + constructor(asyncLocalStorage: AsyncLocalStorage, store: Map) { this._asyncLocalStorage = asyncLocalStorage; this._data = store; } @@ -71,4 +48,8 @@ export class Zone { } } -export const zones = new ZoneManager(); +export const emptyZone = new Zone(asyncLocalStorage, new Map()); + +export function currentZone(): Zone { + return asyncLocalStorage.getStore() ?? emptyZone; +} diff --git a/packages/playwright-core/src/utils.ts b/packages/playwright-core/src/utils.ts index a61f84801c211..839aa390a862b 100644 --- a/packages/playwright-core/src/utils.ts +++ b/packages/playwright-core/src/utils.ts @@ -30,7 +30,6 @@ export * from './utils/isomorphic/headers'; export * from './utils/isomorphic/semaphore'; export * from './utils/isomorphic/stackTrace'; export * from './utils/zipFile'; -export * from './utils/zones'; export * from './server/utils/ascii'; export * from './server/utils/comparators'; @@ -51,5 +50,6 @@ export * from './server/utils/spawnAsync'; export * from './server/utils/task'; export * from './server/utils/userAgent'; export * from './server/utils/wsServer'; +export * from './server/utils/zones'; export { colors } from './utilsBundle'; diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 7395e38217b39..d9c828cc62861 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -15,7 +15,7 @@ */ import { errors } from 'playwright-core'; -import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, zones } from 'playwright-core/lib/utils'; +import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, currentZone } from 'playwright-core/lib/utils'; import { currentTestInfo, currentlyLoadingFileSuite, setCurrentlyLoadingFileSuite } from './globals'; import { Suite, TestCase } from './test'; @@ -266,7 +266,7 @@ export class TestTypeImpl { if (!testInfo) throw new Error(`test.step() can only be called from a test`); const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); - return await zones.run('stepZone', step, async () => { + return await currentZone().with('stepZone', step).run(async () => { try { let result: Awaited>> | undefined = undefined; result = await raceAgainstDeadline(async () => { diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index a8d0cc0387bbd..9d6089614c9cc 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -18,7 +18,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as playwrightLibrary from 'playwright-core'; -import { addInternalStackPrefix, asLocator, createGuid, debugMode, isString, jsonStringifyForceASCII, zones } from 'playwright-core/lib/utils'; +import { addInternalStackPrefix, asLocator, createGuid, currentZone, debugMode, isString, jsonStringifyForceASCII } from 'playwright-core/lib/utils'; import { currentTestInfo } from './common/globals'; import { rootTestType } from './common/testType'; @@ -260,7 +260,7 @@ const playwrightFixtures: Fixtures = ({ // Some special calls do not get into steps. if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd') return; - const zone = zones.zoneData('stepZone'); + const zone = currentZone().data('stepZone'); if (zone && zone.category === 'expect') { // Display the internal locator._expect call under the name of the enclosing expect call, // and connect it to the existing expect step. diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 707e5457e151f..58eb9be9fc820 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -17,9 +17,9 @@ import { captureRawStack, createGuid, + currentZone, isString, pollAgainstDeadline } from 'playwright-core/lib/utils'; -import { zones } from 'playwright-core/lib/utils'; import { ExpectError, isJestError } from './matcherHint'; import { @@ -380,7 +380,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { try { setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info }); const callback = () => matcher.call(target, ...args); - const result = zones.run('stepZone', step, callback); + const result = currentZone().with('stepZone', step).run(callback); if (result instanceof Promise) return result.then(finalizer).catch(reportStepError); finalizer(); diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 5e964af5e0b05..cbb26b6c2f2bc 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -17,7 +17,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, zones } from 'playwright-core/lib/utils'; +import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util'; @@ -245,7 +245,7 @@ export class TestInfoImpl implements TestInfo { } private _parentStep() { - return zones.zoneData('stepZone') + return currentZone().data('stepZone') ?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent. }