From 97cf86d20ab0b69645660da69f027c4ee15711c0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 23 Apr 2021 18:34:52 -0700 Subject: [PATCH] chore: make instrumentation per-context (#6302) --- src/server/browserContext.ts | 40 ++++++++++-- src/server/instrumentation.ts | 21 +++---- src/server/playwright.ts | 16 +---- src/server/supplements/debugger.ts | 52 ++++++---------- src/server/supplements/har/harTracer.ts | 22 +------ src/server/supplements/inspectorController.ts | 53 ---------------- src/server/supplements/recorder/csharp.ts | 2 +- src/server/supplements/recorder/java.ts | 2 +- src/server/supplements/recorder/javascript.ts | 2 +- src/server/supplements/recorder/python.ts | 2 +- src/server/supplements/recorderSupplement.ts | 55 +++++++++-------- src/server/trace/common/traceEvents.ts | 1 - src/server/trace/recorder/tracer.ts | 61 ++++--------------- src/web/traceViewer/ui/snapshotTab.tsx | 2 +- utils/check_deps.js | 3 +- 15 files changed, 118 insertions(+), 216 deletions(-) delete mode 100644 src/server/supplements/inspectorController.ts diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 3068b9d443..dc222aaa4c 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -27,7 +27,11 @@ import { Progress } from './progress'; import { Selectors, serverSelectors } from './selectors'; import * as types from './types'; import path from 'path'; -import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation'; +import { CallMetadata, internalCallMetadata, createInstrumentation, SdkObject } from './instrumentation'; +import { Debugger } from './supplements/debugger'; +import { Tracer } from './trace/recorder/tracer'; +import { HarTracer } from './supplements/har/harTracer'; +import { RecorderSupplement } from './supplements/recorderSupplement'; export abstract class BrowserContext extends SdkObject { static Events = { @@ -51,6 +55,7 @@ export abstract class BrowserContext extends SdkObject { readonly _browserContextId: string | undefined; private _selectors?: Selectors; private _origins = new Set(); + private _harTracer: HarTracer | undefined; constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { super(browser, 'browser-context'); @@ -60,6 +65,9 @@ export abstract class BrowserContext extends SdkObject { this._browserContextId = browserContextId; this._isPersistentContext = !browserContextId; this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill); + + if (this._options.recordHar) + this._harTracer = new HarTracer(this, this._options.recordHar); } _setSelectors(selectors: Selectors) { @@ -71,7 +79,31 @@ export abstract class BrowserContext extends SdkObject { } async _initialize() { - await this.instrumentation.onContextCreated(this); + if (this.attribution.isInternal) + return; + // Create instrumentation per context. + this.instrumentation = createInstrumentation(); + + // Debugger will pause execution upon page.pause in headed mode. + const contextDebugger = new Debugger(this); + this.instrumentation.addListener(contextDebugger); + + if (this._options._traceDir) + this.instrumentation.addListener(new Tracer(this, this._options._traceDir)); + + + // When PWDEBUG=1, show inspector for each context. + if (debugMode() === 'inspector') + await RecorderSupplement.show(this, { pauseOnNextStatement: true }); + + // When paused, show inspector. + if (contextDebugger.isPaused()) + RecorderSupplement.showInspector(this); + contextDebugger.on(Debugger.Events.PausedStateChanged, () => { + RecorderSupplement.showInspector(this); + }); + + await this.instrumentation.onContextCreated(); } async _ensureVideosPath() { @@ -231,7 +263,7 @@ export abstract class BrowserContext extends SdkObject { this.emit(BrowserContext.Events.BeforeClose); this._closedStatus = 'closing'; - await this.instrumentation.onContextWillDestroy(this); + await this._harTracer?.flush(); // Cleanup. const promises: Promise[] = []; @@ -260,7 +292,7 @@ export abstract class BrowserContext extends SdkObject { await this._browser.close(); // Bookkeeping. - await this.instrumentation.onContextDidDestroy(this); + await this.instrumentation.onContextDestroyed(); this._didCloseInternal(); } await this._closePromise; diff --git a/src/server/instrumentation.ts b/src/server/instrumentation.ts index 5074abeb46..d1c56f9e05 100644 --- a/src/server/instrumentation.ts +++ b/src/server/instrumentation.ts @@ -25,6 +25,7 @@ import type { Frame } from './frames'; import type { Page } from './page'; export type Attribution = { + isInternal: boolean, browserType?: BrowserType; browser?: Browser; context?: BrowserContext; @@ -67,34 +68,32 @@ export class SdkObject extends EventEmitter { } export interface Instrumentation { - onContextCreated(context: BrowserContext): Promise; - onContextWillDestroy(context: BrowserContext): Promise; - onContextDidDestroy(context: BrowserContext): Promise; - + addListener(listener: InstrumentationListener): void; + onContextCreated(): Promise; + onContextDestroyed(): Promise; onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise; onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise; onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void; onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise; - onEvent(sdkObject: SdkObject, metadata: CallMetadata): void; } export interface InstrumentationListener { - onContextCreated?(context: BrowserContext): Promise; - onContextWillDestroy?(context: BrowserContext): Promise; - onContextDidDestroy?(context: BrowserContext): Promise; - + onContextCreated?(): Promise; + onContextDestroyed?(): Promise; onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise; onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise; onCallLog?(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void; onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise; - onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void; } -export function multiplexInstrumentation(listeners: InstrumentationListener[]): Instrumentation { +export function createInstrumentation(): Instrumentation { + const listeners: InstrumentationListener[] = []; return new Proxy({}, { get: (obj: any, prop: string) => { + if (prop === 'addListener') + return (listener: InstrumentationListener) => listeners.push(listener); if (!prop.startsWith('on')) return obj[prop]; return async (...params: any[]) => { diff --git a/src/server/playwright.ts b/src/server/playwright.ts index 2caf33cf6b..39eaf083b5 100644 --- a/src/server/playwright.ts +++ b/src/server/playwright.ts @@ -15,7 +15,6 @@ */ import path from 'path'; -import { Tracer } from './trace/recorder/tracer'; import { Android } from './android/android'; import { AdbBackend } from './android/backendAdb'; import { PlaywrightOptions } from './browser'; @@ -23,12 +22,9 @@ import { Chromium } from './chromium/chromium'; import { Electron } from './electron/electron'; import { Firefox } from './firefox/firefox'; import { Selectors, serverSelectors } from './selectors'; -import { HarTracer } from './supplements/har/harTracer'; -import { InspectorController } from './supplements/inspectorController'; import { WebKit } from './webkit/webkit'; import { Registry } from '../utils/registry'; -import { InstrumentationListener, multiplexInstrumentation, SdkObject } from './instrumentation'; -import { Debugger } from './supplements/debugger'; +import { createInstrumentation, SdkObject } from './instrumentation'; export class Playwright extends SdkObject { readonly selectors: Selectors; @@ -40,15 +36,7 @@ export class Playwright extends SdkObject { readonly options: PlaywrightOptions; constructor(isInternal: boolean) { - const listeners: InstrumentationListener[] = []; - if (!isInternal) { - listeners.push(new Debugger()); - listeners.push(new Tracer()); - listeners.push(new HarTracer()); - listeners.push(new InspectorController()); - } - const instrumentation = multiplexInstrumentation(listeners); - super({ attribution: {}, instrumentation } as any, undefined, 'Playwright'); + super({ attribution: { isInternal }, instrumentation: createInstrumentation() } as any, undefined, 'Playwright'); this.options = { registry: new Registry(path.join(__dirname, '..', '..')), rootSdkObject: this, diff --git a/src/server/supplements/debugger.ts b/src/server/supplements/debugger.ts index a09189d9f0..f9555b877b 100644 --- a/src/server/supplements/debugger.ts +++ b/src/server/supplements/debugger.ts @@ -19,54 +19,38 @@ import { debugMode, isUnderTest, monotonicTime } from '../../utils/utils'; import { BrowserContext } from '../browserContext'; import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation'; import * as consoleApiSource from '../../generated/consoleApiSource'; +import { debugLogger } from '../../utils/debugLogger'; -export class Debugger implements InstrumentationListener { - async onContextCreated(context: BrowserContext): Promise { - ContextDebugger.getOrCreate(context); - if (debugMode() === 'console') - await context.extendInjectedScript(consoleApiSource.source); - } +const symbol = Symbol('Debugger'); - async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { - await ContextDebugger.lookup(sdkObject.attribution.context!)?.onBeforeCall(sdkObject, metadata); - } - - async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise { - await ContextDebugger.lookup(sdkObject.attribution.context!)?.onBeforeInputAction(sdkObject, metadata); - } -} - -const symbol = Symbol('ContextDebugger'); - -export class ContextDebugger extends EventEmitter { +export class Debugger extends EventEmitter implements InstrumentationListener { private _pauseOnNextStatement = false; private _pausedCallsMetadata = new Map void, sdkObject: SdkObject }>(); private _enabled: boolean; + private _context: BrowserContext; static Events = { PausedStateChanged: 'pausedstatechanged' }; - static getOrCreate(context: BrowserContext): ContextDebugger { - let contextDebugger = (context as any)[symbol] as ContextDebugger; - if (!contextDebugger) { - contextDebugger = new ContextDebugger(); - (context as any)[symbol] = contextDebugger; - } - return contextDebugger; - } - - constructor() { + constructor(context: BrowserContext) { super(); + this._context = context; + (this._context as any)[symbol] = this; this._enabled = debugMode() === 'inspector'; if (this._enabled) this.pauseOnNextStatement(); } - static lookup(context?: BrowserContext): ContextDebugger | undefined { + static lookup(context?: BrowserContext): Debugger | undefined { if (!context) return; - return (context as any)[symbol] as ContextDebugger | undefined; + return (context as any)[symbol] as Debugger | undefined; + } + + async onContextCreated() { + if (debugMode() === 'console') + await this._context.extendInjectedScript(consoleApiSource.source); } async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { @@ -79,13 +63,17 @@ export class ContextDebugger extends EventEmitter { await this.pause(sdkObject, metadata); } + async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise { + debugLogger.log(logName as any, message); + } + async pause(sdkObject: SdkObject, metadata: CallMetadata) { this._enabled = true; metadata.pauseStartTime = monotonicTime(); const result = new Promise(resolve => { this._pausedCallsMetadata.set(metadata, { resolve, sdkObject }); }); - this.emit(ContextDebugger.Events.PausedStateChanged); + this.emit(Debugger.Events.PausedStateChanged); return result; } @@ -97,7 +85,7 @@ export class ContextDebugger extends EventEmitter { resolve(); } this._pausedCallsMetadata.clear(); - this.emit(ContextDebugger.Events.PausedStateChanged); + this.emit(Debugger.Events.PausedStateChanged); } pauseOnNextStatement() { diff --git a/src/server/supplements/har/harTracer.ts b/src/server/supplements/har/harTracer.ts index f644255bff..6010c4d6f6 100644 --- a/src/server/supplements/har/harTracer.ts +++ b/src/server/supplements/har/harTracer.ts @@ -20,36 +20,16 @@ import { BrowserContext } from '../../browserContext'; import { helper } from '../../helper'; import * as network from '../../network'; import { Page } from '../../page'; -import { InstrumentationListener } from '../../instrumentation'; import * as har from './har'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); -export class HarTracer implements InstrumentationListener { - private _contextTracers = new Map(); - - async onContextCreated(context: BrowserContext): Promise { - if (!context._options.recordHar) - return; - const contextTracer = new HarContextTracer(context, context._options.recordHar); - this._contextTracers.set(context, contextTracer); - } - - async onContextWillDestroy(context: BrowserContext): Promise { - const contextTracer = this._contextTracers.get(context); - if (contextTracer) { - this._contextTracers.delete(context); - await contextTracer.flush(); - } - } -} - type HarOptions = { path: string; omitContent?: boolean; }; -class HarContextTracer { +export class HarTracer { private _options: HarOptions; private _log: har.Log; private _pageEntries = new Map(); diff --git a/src/server/supplements/inspectorController.ts b/src/server/supplements/inspectorController.ts deleted file mode 100644 index 93483062ed..0000000000 --- a/src/server/supplements/inspectorController.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { BrowserContext } from '../browserContext'; -import { RecorderSupplement } from './recorderSupplement'; -import { debugLogger } from '../../utils/debugLogger'; -import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation'; -import { ContextDebugger } from './debugger'; - -export class InspectorController implements InstrumentationListener { - async onContextCreated(context: BrowserContext): Promise { - const contextDebugger = ContextDebugger.lookup(context)!; - if (contextDebugger.isPaused()) - RecorderSupplement.show(context, {}).catch(() => {}); - contextDebugger.on(ContextDebugger.Events.PausedStateChanged, () => { - RecorderSupplement.show(context, {}).catch(() => {}); - }); - } - - async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { - const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context); - recorder?.onBeforeCall(sdkObject, metadata); - } - - async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { - const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context); - recorder?.onAfterCall(sdkObject, metadata); - } - - async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise { - const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context); - recorder?.onBeforeInputAction(sdkObject, metadata); - } - - async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise { - debugLogger.log(logName as any, message); - const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context); - recorder?.updateCallLog([metadata]); - } -} diff --git a/src/server/supplements/recorder/csharp.ts b/src/server/supplements/recorder/csharp.ts index 482a180ecf..58e8954762 100644 --- a/src/server/supplements/recorder/csharp.ts +++ b/src/server/supplements/recorder/csharp.ts @@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toS import { ActionInContext } from './codeGenerator'; import { actionTitle, Action } from './recorderActions'; import { MouseClickOptions, toModifiers } from './utils'; -import deviceDescriptors = require('../../deviceDescriptors'); +import deviceDescriptors from '../../deviceDescriptors'; export class CSharpLanguageGenerator implements LanguageGenerator { id = 'csharp'; diff --git a/src/server/supplements/recorder/java.ts b/src/server/supplements/recorder/java.ts index b61633337e..8c8b83bac7 100644 --- a/src/server/supplements/recorder/java.ts +++ b/src/server/supplements/recorder/java.ts @@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, toSignalMap } from './lang import { ActionInContext } from './codeGenerator'; import { Action, actionTitle } from './recorderActions'; import { toModifiers } from './utils'; -import deviceDescriptors = require('../../deviceDescriptors'); +import deviceDescriptors from '../../deviceDescriptors'; import { JavaScriptFormatter } from './javascript'; export class JavaLanguageGenerator implements LanguageGenerator { diff --git a/src/server/supplements/recorder/javascript.ts b/src/server/supplements/recorder/javascript.ts index 8867a765da..a341c14a66 100644 --- a/src/server/supplements/recorder/javascript.ts +++ b/src/server/supplements/recorder/javascript.ts @@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toS import { ActionInContext } from './codeGenerator'; import { Action, actionTitle } from './recorderActions'; import { MouseClickOptions, toModifiers } from './utils'; -import deviceDescriptors = require('../../deviceDescriptors'); +import deviceDescriptors from '../../deviceDescriptors'; export class JavaScriptLanguageGenerator implements LanguageGenerator { id = 'javascript'; diff --git a/src/server/supplements/recorder/python.ts b/src/server/supplements/recorder/python.ts index b6aaa542bf..363cb31a3b 100644 --- a/src/server/supplements/recorder/python.ts +++ b/src/server/supplements/recorder/python.ts @@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toS import { ActionInContext } from './codeGenerator'; import { actionTitle, Action } from './recorderActions'; import { MouseClickOptions, toModifiers } from './utils'; -import deviceDescriptors = require('../../deviceDescriptors'); +import deviceDescriptors from '../../deviceDescriptors'; export class PythonLanguageGenerator implements LanguageGenerator { id = 'python'; diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index c9d416ba4b..15e3199caa 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -29,19 +29,19 @@ import { PythonLanguageGenerator } from './recorder/python'; import * as recorderSource from '../../generated/recorderSource'; import * as consoleApiSource from '../../generated/consoleApiSource'; import { RecorderApp } from './recorder/recorderApp'; -import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation'; +import { CallMetadata, InstrumentationListener, internalCallMetadata, SdkObject } from '../instrumentation'; import { Point } from '../../common/types'; import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes'; import { isUnderTest } from '../../utils/utils'; import { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter'; import { metadataToCallLog } from './recorder/recorderUtils'; -import { ContextDebugger } from './debugger'; +import { Debugger } from './debugger'; type BindingSource = { frame: Frame, page: Page }; const symbol = Symbol('RecorderSupplement'); -export class RecorderSupplement { +export class RecorderSupplement implements InstrumentationListener { private _generator: CodeGenerator; private _pageAliases = new Map(); private _lastPopupOrdinal = 0; @@ -59,7 +59,11 @@ export class RecorderSupplement { private _hoveredSnapshot: { callLogId: string, phase: 'before' | 'after' | 'action' } | undefined; private _snapshots = new Set(); private _allMetadatas = new Map(); - private _contextDebugger: ContextDebugger; + private _debugger: Debugger; + + static showInspector(context: BrowserContext) { + RecorderSupplement.show(context, {}).catch(() => {}); + } static show(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { let recorderPromise = (context as any)[symbol] as Promise; @@ -71,15 +75,10 @@ export class RecorderSupplement { return recorderPromise; } - static lookup(context: BrowserContext | undefined): Promise | undefined { - if (!context) - return; - return (context as any)[symbol] as Promise | undefined; - } - constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { this._context = context; - this._contextDebugger = ContextDebugger.getOrCreate(context); + this._debugger = Debugger.lookup(context)!; + context.instrumentation.addListener(this); this._params = params; this._mode = params.startRecording ? 'recording' : 'none'; const language = params.language || context._options.sdkLanguage; @@ -152,21 +151,21 @@ export class RecorderSupplement { } if (data.event === 'callLogHovered') { this._hoveredSnapshot = undefined; - if (this._contextDebugger.isPaused() && data.params.callLogId) + if (this._debugger.isPaused() && data.params.callLogId) this._hoveredSnapshot = data.params; this._refreshOverlay(); return; } if (data.event === 'step') { - this._contextDebugger.resume(true); + this._debugger.resume(true); return; } if (data.event === 'resume') { - this._contextDebugger.resume(false); + this._debugger.resume(false); return; } if (data.event === 'pause') { - this._contextDebugger.pauseOnNextStatement(); + this._debugger.pauseOnNextStatement(); return; } if (data.event === 'clear') { @@ -177,7 +176,7 @@ export class RecorderSupplement { await Promise.all([ recorderApp.setMode(this._mode), - recorderApp.setPaused(this._contextDebugger.isPaused()), + recorderApp.setPaused(this._debugger.isPaused()), this._pushAllSources() ]); @@ -233,27 +232,27 @@ export class RecorderSupplement { }); await this._context.exposeBinding('_playwrightResume', false, () => { - this._contextDebugger.resume(false); + this._debugger.resume(false); }); const snapshotBaseUrl = await this._snapshotter.initialize() + '/snapshot/'; await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest(), snapshotBaseUrl }); await this._context.extendInjectedScript(consoleApiSource.source); - if (this._contextDebugger.isPaused()) + if (this._debugger.isPaused()) this._pausedStateChanged(); - this._contextDebugger.on(ContextDebugger.Events.PausedStateChanged, () => this._pausedStateChanged()); + this._debugger.on(Debugger.Events.PausedStateChanged, () => this._pausedStateChanged()); (this._context as any).recorderAppForTest = recorderApp; } _pausedStateChanged() { // If we are called upon page.pause, we don't have metadatas, populate them. - for (const { metadata, sdkObject } of this._contextDebugger.pausedDetails()) { + for (const { metadata, sdkObject } of this._debugger.pausedDetails()) { if (!this._currentCallsMetadata.has(metadata)) this.onBeforeCall(sdkObject, metadata); } - this._recorderApp!.setPaused(this._contextDebugger.isPaused()); + this._recorderApp!.setPaused(this._debugger.isPaused()); this._updateUserSources(); this.updateCallLog([...this._currentCallsMetadata.keys()]); } @@ -398,7 +397,7 @@ export class RecorderSupplement { } } - onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) { + async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) { if (this._mode === 'recording') return; this._captureSnapshot(sdkObject, metadata, 'before'); @@ -412,7 +411,7 @@ export class RecorderSupplement { } } - onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) { + async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) { if (this._mode === 'recording') return; this._captureSnapshot(sdkObject, metadata, 'after'); @@ -441,7 +440,7 @@ export class RecorderSupplement { this._userSources.set(file, source); } if (line) { - const paused = this._contextDebugger.isPaused(metadata); + const paused = this._debugger.isPaused(metadata); source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') }); source.revealLine = line; fileToSelect = source.file; @@ -456,12 +455,16 @@ export class RecorderSupplement { this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()]); } - onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) { + async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) { if (this._mode === 'recording') return; this._captureSnapshot(sdkObject, metadata, 'action'); } + async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise { + this.updateCallLog([metadata]); + } + updateCallLog(metadatas: CallMetadata[]) { if (this._mode === 'recording') return; @@ -472,7 +475,7 @@ export class RecorderSupplement { let status: CallLogStatus = 'done'; if (this._currentCallsMetadata.has(metadata)) status = 'in-progress'; - if (this._contextDebugger.isPaused(metadata)) + if (this._debugger.isPaused(metadata)) status = 'paused'; logs.push(metadataToCallLog(metadata, status, this._snapshots)); } diff --git a/src/server/trace/common/traceEvents.ts b/src/server/trace/common/traceEvents.ts index 3207c8fa99..88fcc612a1 100644 --- a/src/server/trace/common/traceEvents.ts +++ b/src/server/trace/common/traceEvents.ts @@ -63,7 +63,6 @@ export type ActionTraceEvent = { type: 'action' | 'event', contextId: string, metadata: CallMetadata, - snapshots?: { title: string, snapshotName: string }[], }; export type DialogOpenedEvent = { diff --git a/src/server/trace/recorder/tracer.ts b/src/server/trace/recorder/tracer.ts index 7199abd959..bd0ec39b80 100644 --- a/src/server/trace/recorder/tracer.ts +++ b/src/server/trace/recorder/tracer.ts @@ -32,53 +32,18 @@ const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs)); const envTrace = getFromENV('PWTRACE_RESOURCE_DIR'); export class Tracer implements InstrumentationListener { - private _contextTracers = new Map(); - - async onContextCreated(context: BrowserContext): Promise { - const traceDir = context._options._traceDir; - if (!traceDir) - return; - const resourcesDir = envTrace || path.join(traceDir, 'resources'); - const tracePath = path.join(traceDir, context._options._debugName!); - const contextTracer = new ContextTracer(context, resourcesDir, tracePath); - await contextTracer.start(); - this._contextTracers.set(context, contextTracer); - } - - async onContextDidDestroy(context: BrowserContext): Promise { - const contextTracer = this._contextTracers.get(context); - if (contextTracer) { - await contextTracer.dispose().catch(e => {}); - this._contextTracers.delete(context); - } - } - - async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise { - this._contextTracers.get(sdkObject.attribution.context!)?.onBeforeInputAction(sdkObject, metadata, element); - } - - async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { - this._contextTracers.get(sdkObject.attribution.context!)?.onBeforeCall(sdkObject, metadata); - } - - async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { - this._contextTracers.get(sdkObject.attribution.context!)?.onAfterCall(sdkObject, metadata); - } - - async onEvent(sdkObject: SdkObject, metadata: CallMetadata): Promise { - this._contextTracers.get(sdkObject.attribution.context!)?.onEvent(sdkObject, metadata); - } -} - -class ContextTracer { private _contextId: string; private _appendEventChain: Promise; private _snapshotter: PersistentSnapshotter; private _eventListeners: RegisteredListener[]; private _disposed = false; private _pendingCalls = new Map(); + private _context: BrowserContext; - constructor(context: BrowserContext, resourcesDir: string, tracePrefix: string) { + constructor(context: BrowserContext, traceDir: string) { + this._context = context; + const resourcesDir = envTrace || path.join(traceDir, 'resources'); + const tracePrefix = path.join(traceDir, context._options._debugName!); const traceFile = tracePrefix + '-actions.trace'; this._contextId = 'context@' + createGuid(); this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile); @@ -99,10 +64,6 @@ class ContextTracer { ]; } - async start() { - await this._snapshotter.start(false); - } - _captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) { if (!sdkObject.attribution.page) return; @@ -111,16 +72,20 @@ class ContextTracer { this._snapshotter.captureSnapshot(sdkObject.attribution.page, snapshotName, element); } - onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) { + async onContextCreated(): Promise { + await this._snapshotter.start(false); + } + + async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) { this._captureSnapshot('before', sdkObject, metadata); this._pendingCalls.set(metadata.id, { sdkObject, metadata }); } - onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) { + async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) { this._captureSnapshot('action', sdkObject, metadata, element); } - onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) { + async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) { this._captureSnapshot('after', sdkObject, metadata); if (!sdkObject.attribution.page) return; @@ -239,7 +204,7 @@ class ContextTracer { }); } - async dispose() { + async onContextDestroyed() { this._disposed = true; helper.removeEventListeners(this._eventListeners); await this._snapshotter.dispose(); diff --git a/src/web/traceViewer/ui/snapshotTab.tsx b/src/web/traceViewer/ui/snapshotTab.tsx index 0fd524c038..d7fdd49be1 100644 --- a/src/web/traceViewer/ui/snapshotTab.tsx +++ b/src/web/traceViewer/ui/snapshotTab.tsx @@ -30,7 +30,7 @@ export const SnapshotTab: React.FunctionComponent<{ let [snapshotIndex, setSnapshotIndex] = React.useState(0); const snapshotMap = new Map(); - for (const snapshot of actionEntry?.snapshots || []) + for (const snapshot of actionEntry?.metadata.snapshots || []) snapshotMap.set(snapshot.title, snapshot); const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('after'); const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[]; diff --git a/utils/check_deps.js b/utils/check_deps.js index ecbce2a57b..cd37769d72 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -140,7 +140,8 @@ DEPS['src/server/injected/'] = ['src/server/common/']; DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/']; DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/']; -DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/trace/recorder/tracer.ts', 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/']; +DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/']; +DEPS['src/server/browserContext.ts'] = [...DEPS['src/server/'], 'src/server/trace/recorder/tracer.ts']; DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**']; // Tracing is a client/server plugin, nothing should depend on it.