From 94c33da9467c945754852390d4ce0b1bcf78097f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 29 Oct 2021 17:20:17 -0800 Subject: [PATCH] feat(trace): throttle the screencast (#9893) --- .../src/server/chromium/crPage.ts | 4 +- .../src/server/firefox/ffPage.ts | 5 +- packages/playwright-core/src/server/page.ts | 68 +++++++++++++++++++ .../src/server/trace/recorder/tracing.ts | 3 + .../src/server/webkit/wkPage.ts | 5 +- 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 07347534d1..24c74f1cce 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -857,7 +857,9 @@ class FrameSession { } _onScreencastFrame(payload: Protocol.Page.screencastFramePayload) { - this._client.send('Page.screencastFrameAck', { sessionId: payload.sessionId }).catch(() => {}); + this._page.throttleScreencastFrameAck(() => { + this._client.send('Page.screencastFrameAck', { sessionId: payload.sessionId }).catch(() => {}); + }); const buffer = Buffer.from(payload.data, 'base64'); this._page.emit(Page.Events.ScreencastFrame, { buffer, diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index f9cd879207..d59fa054e7 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -500,7 +500,10 @@ export class FFPage implements PageDelegate { private _onScreencastFrame(event: Protocol.Page.screencastFramePayload) { if (!this._screencastId) return; - this._session.send('Page.screencastFrameAck', { screencastId: this._screencastId }).catch(e => debugLogger.log('error', e)); + const screencastId = this._screencastId; + this._page.throttleScreencastFrameAck(() => { + this._session.send('Page.screencastFrameAck', { screencastId }).catch(e => debugLogger.log('error', e)); + }); const buffer = Buffer.from(event.data, 'base64'); this._page.emit(Page.Events.ScreencastFrame, { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index effc29c53d..710a927fe2 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -144,6 +144,7 @@ export class Page extends SdkObject { _pageIsError: Error | undefined; _video: Artifact | null = null; _opener: Page | undefined; + private _frameThrottler = new FrameThrottler(10, 200); constructor(delegate: PageDelegate, browserContext: BrowserContext) { super(browserContext, 'page'); @@ -209,6 +210,7 @@ export class Page extends SdkObject { _didClose() { this._frameManager.dispose(); + this._frameThrottler.setEnabled(false); assert(this._closedState !== 'closed', 'Page closed twice'); this._closedState = 'closed'; this.emit(Page.Events.Close); @@ -217,12 +219,14 @@ export class Page extends SdkObject { _didCrash() { this._frameManager.dispose(); + this._frameThrottler.setEnabled(false); this.emit(Page.Events.Crash); this._crashedPromise.resolve(new Error('Page crashed')); } _didDisconnect() { this._frameManager.dispose(); + this._frameThrottler.setEnabled(false); assert(!this._disconnected, 'Page disconnected twice'); this._disconnected = true; this._disconnectedPromise.resolve(new Error('Page closed')); @@ -495,6 +499,16 @@ export class Page extends SdkObject { setScreencastOptions(options: { width: number, height: number, quality: number } | null) { this._delegate.setScreencastOptions(options).catch(e => debugLogger.log('error', e)); + this._frameThrottler.setEnabled(!!options); + } + + throttleScreencastFrameAck(ack: () => void) { + // Don't ack immediately, tracing has smart throttling logic that is implemented here. + this._frameThrottler.ack(ack); + } + + temporarlyDisableTracingScreencastThrottling() { + this._frameThrottler.recharge(); } firePageError(error: Error) { @@ -631,3 +645,57 @@ function addPageBinding(bindingName: string, needsHandle: boolean) { }; (globalThis as any)[bindingName].__installed = true; } + +class FrameThrottler { + private _acks: (() => void)[] = []; + private _interval: number; + private _nonThrottledFrames: number; + private _budget: number; + private _intervalId: NodeJS.Timeout | undefined; + + constructor(nonThrottledFrames: number, interval: number) { + this._nonThrottledFrames = nonThrottledFrames; + this._budget = nonThrottledFrames; + this._interval = interval; + } + + setEnabled(enabled: boolean) { + if (enabled) { + if (this._intervalId) + clearInterval(this._intervalId); + this._intervalId = setInterval(() => this._tick(), this._interval); + } else if (this._intervalId) { + clearInterval(this._intervalId); + this._intervalId = undefined; + } + } + + recharge() { + // Send all acks, reset budget. + for (const ack of this._acks) + ack(); + this._acks = []; + this._budget = this._nonThrottledFrames; + } + + ack(ack: () => void) { + // Either not engaged or video is also recording, don't throttle. + if (!this._intervalId) { + ack(); + return; + } + + // Do we have enough budget to respond w/o throttling? + if (--this._budget > 0) { + ack(); + return; + } + + // Schedule. + this._acks.push(ack); + } + + private _tick() { + this._acks.shift()?.(); + } +} diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index c02216243d..8975151238 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -254,6 +254,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha } async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) { + sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling(); // Set afterSnapshot name for all the actions that operate selectors. // Elements resolved from selectors will be marked on the snapshot. metadata.afterSnapshot = `after@${metadata.id}`; @@ -263,12 +264,14 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha } async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) { + sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling(); const actionSnapshot = this._captureSnapshot('action', sdkObject, metadata, element); this._pendingCalls.get(metadata.id)!.actionSnapshot = actionSnapshot; await actionSnapshot; } async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) { + sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling(); const pendingCall = this._pendingCalls.get(metadata.id); if (!pendingCall || pendingCall.afterSnapshot) return; diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 38f9675226..b94b9eb5f7 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -873,7 +873,10 @@ export class WKPage implements PageDelegate { } private _onScreencastFrame(event: Protocol.Screencast.screencastFramePayload) { - this._pageProxySession.send('Screencast.screencastFrameAck', { generation: this._screencastGeneration }).catch(e => debugLogger.log('error', e)); + const generation = this._screencastGeneration; + this._page.throttleScreencastFrameAck(() => { + this._pageProxySession.send('Screencast.screencastFrameAck', { generation }).catch(e => debugLogger.log('error', e)); + }); const buffer = Buffer.from(event.data, 'base64'); this._page.emit(Page.Events.ScreencastFrame, { buffer,