Skip to content

Commit

Permalink
chore: move zones into platform (#34786)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Feb 13, 2025
1 parent 9ecf2f6 commit 90ec838
Show file tree
Hide file tree
Showing 14 changed files with 85 additions and 54 deletions.
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}

async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
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();
}

Expand Down
5 changes: 2 additions & 3 deletions packages/playwright-core/src/client/channelOwner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -176,7 +175,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel

async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> {
const logger = this._logger;
const existingApiZone = zones.zoneData<ApiZone>('apiZone');
const existingApiZone = this._platform.zones.current().data<ApiZone>();
if (existingApiZone)
return await func(existingApiZone);

Expand All @@ -186,7 +185,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
const apiZone: ApiZone = { apiName: stackTrace.apiName, frames: stackTrace.frames, isInternal, reported: false, userData: undefined, stepId: undefined };

try {
const result = await zones.run('apiZone', apiZone, async () => 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);
Expand Down
3 changes: 1 addition & 2 deletions packages/playwright-core/src/client/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 }));
}

Expand Down
11 changes: 5 additions & 6 deletions packages/playwright-core/src/client/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -821,14 +820,14 @@ export class RouteHandler {
readonly handler: RouteHandlerCallback;
private _ignoreException: boolean = false;
private _activeInvocations: Set<{ complete: Promise<void>, 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[]) {
Expand All @@ -852,7 +851,7 @@ export class RouteHandler {
}

public async handle(route: Route): Promise<boolean> {
return await this._svedZone.run(async () => this._handleImpl(route));
return await this._savedZone.run(async () => this._handleImpl(route));
}

private async _handleImpl(route: Route): Promise<boolean> {
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}

async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
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();
}

Expand Down
5 changes: 2 additions & 3 deletions packages/playwright-core/src/client/waiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)[];
Expand All @@ -36,7 +35,7 @@ export class Waiter {
constructor(channelOwner: ChannelOwner<channels.EventTargetChannel>, 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 = [
Expand Down
20 changes: 19 additions & 1 deletion packages/playwright-core/src/common/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>(func: () => R): R;
data<T>(): T | undefined;
};

const noopZone: Zone = {
push: () => noopZone,
pop: () => noopZone,
run: func => func(),
data: () => undefined,
};

export type Platform = {
calculateSha1(text: string): Promise<string>;
Expand All @@ -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 = {
Expand Down Expand Up @@ -69,6 +83,8 @@ export const webPlatform: Platform = {
pathSeparator: '/',

ws: (url: string) => new WebSocket(url),

zones: { empty: noopZone, current: () => noopZone },
};

export const emptyPlatform: Platform = {
Expand Down Expand Up @@ -98,5 +114,7 @@ export const emptyPlatform: Platform = {
throw new Error('Function not implemented.');
},

pathSeparator: '/'
pathSeparator: '/',

zones: { empty: noopZone, current: () => noopZone },
};
40 changes: 38 additions & 2 deletions packages/playwright-core/src/server/utils/nodePlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(data: T) {
return new NodeZone(this._zone.with('apiZone', data));
}

pop() {
return new NodeZone(this._zone.without('apiZone'));
}

run<R>(func: () => R): R {
return this._zone.run(func);
}

runIgnoreCurrent<R>(func: () => R): R {
return emptyZone.run(func);
}

data<T>(): T | undefined {
return this._zone.data('apiZone');
}
}

export const nodePlatform: Platform = {
calculateSha1: (text: string) => {
Expand All @@ -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),
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,13 @@ import { AsyncLocalStorage } from 'async_hooks';

export type ZoneType = 'apiZone' | 'stepZone';

class ZoneManager {
private readonly _asyncLocalStorage = new AsyncLocalStorage<Zone | undefined>();
private readonly _emptyZone = Zone.createEmpty(this._asyncLocalStorage);

run<T, R>(type: ZoneType, data: T, func: () => R): R {
return this.current().with(type, data).run(func);
}

zoneData<T>(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<Zone | undefined>();

export class Zone {
private readonly _asyncLocalStorage: AsyncLocalStorage<Zone | undefined>;
private readonly _data: ReadonlyMap<ZoneType, unknown>;

static createEmpty(asyncLocalStorage: AsyncLocalStorage<Zone | undefined>) {
return new Zone(asyncLocalStorage, new Map());
}

private constructor(asyncLocalStorage: AsyncLocalStorage<Zone | undefined>, store: Map<ZoneType, unknown>) {
constructor(asyncLocalStorage: AsyncLocalStorage<Zone | undefined>, store: Map<ZoneType, unknown>) {
this._asyncLocalStorage = asyncLocalStorage;
this._data = store;
}
Expand All @@ -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;
}
2 changes: 1 addition & 1 deletion packages/playwright-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
4 changes: 2 additions & 2 deletions packages/playwright/src/common/testType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
result = await raceAgainstDeadline(async () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -260,7 +260,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
// Some special calls do not get into steps.
if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd')
return;
const zone = zones.zoneData<TestStepInternal>('stepZone');
const zone = currentZone().data<TestStepInternal>('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.
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/matchers/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -380,7 +380,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
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();
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/worker/testInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -245,7 +245,7 @@ export class TestInfoImpl implements TestInfo {
}

private _parentStep() {
return zones.zoneData<TestStepInternal>('stepZone')
return currentZone().data<TestStepInternal>('stepZone')
?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
}

Expand Down

0 comments on commit 90ec838

Please sign in to comment.