Skip to content

Commit 952675a

Browse files
committed
feat(trace): throttle the screencast
1 parent 8991bbd commit 952675a

File tree

5 files changed

+82
-3
lines changed

5 files changed

+82
-3
lines changed

packages/playwright-core/src/server/chromium/crPage.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,9 @@ class FrameSession {
857857
}
858858

859859
_onScreencastFrame(payload: Protocol.Page.screencastFramePayload) {
860-
this._client.send('Page.screencastFrameAck', { sessionId: payload.sessionId }).catch(() => {});
860+
this._page.throttleScreencastFrameAck(() => {
861+
this._client.send('Page.screencastFrameAck', { sessionId: payload.sessionId }).catch(() => {});
862+
});
861863
const buffer = Buffer.from(payload.data, 'base64');
862864
this._page.emit(Page.Events.ScreencastFrame, {
863865
buffer,

packages/playwright-core/src/server/firefox/ffPage.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,10 @@ export class FFPage implements PageDelegate {
500500
private _onScreencastFrame(event: Protocol.Page.screencastFramePayload) {
501501
if (!this._screencastId)
502502
return;
503-
this._session.send('Page.screencastFrameAck', { screencastId: this._screencastId }).catch(e => debugLogger.log('error', e));
503+
const screencastId = this._screencastId;
504+
this._page.throttleScreencastFrameAck(() => {
505+
this._session.send('Page.screencastFrameAck', { screencastId }).catch(e => debugLogger.log('error', e));
506+
});
504507

505508
const buffer = Buffer.from(event.data, 'base64');
506509
this._page.emit(Page.Events.ScreencastFrame, {

packages/playwright-core/src/server/page.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export class Page extends SdkObject {
144144
_pageIsError: Error | undefined;
145145
_video: Artifact | null = null;
146146
_opener: Page | undefined;
147+
private _frameThrottler = new FrameThrottler(10, 200);
147148

148149
constructor(delegate: PageDelegate, browserContext: BrowserContext) {
149150
super(browserContext, 'page');
@@ -209,6 +210,7 @@ export class Page extends SdkObject {
209210

210211
_didClose() {
211212
this._frameManager.dispose();
213+
this._frameThrottler.setEnabled(false);
212214
assert(this._closedState !== 'closed', 'Page closed twice');
213215
this._closedState = 'closed';
214216
this.emit(Page.Events.Close);
@@ -217,12 +219,14 @@ export class Page extends SdkObject {
217219

218220
_didCrash() {
219221
this._frameManager.dispose();
222+
this._frameThrottler.setEnabled(false);
220223
this.emit(Page.Events.Crash);
221224
this._crashedPromise.resolve(new Error('Page crashed'));
222225
}
223226

224227
_didDisconnect() {
225228
this._frameManager.dispose();
229+
this._frameThrottler.setEnabled(false);
226230
assert(!this._disconnected, 'Page disconnected twice');
227231
this._disconnected = true;
228232
this._disconnectedPromise.resolve(new Error('Page closed'));
@@ -495,6 +499,16 @@ export class Page extends SdkObject {
495499

496500
setScreencastOptions(options: { width: number, height: number, quality: number } | null) {
497501
this._delegate.setScreencastOptions(options).catch(e => debugLogger.log('error', e));
502+
this._frameThrottler.setEnabled(!!options);
503+
}
504+
505+
throttleScreencastFrameAck(ack: () => void) {
506+
// Don't ack immediately, tracing has smart throttling logic that is implemented here.
507+
this._frameThrottler.ack(ack);
508+
}
509+
510+
temporarlyDisableTracingScreencastThrottling() {
511+
this._frameThrottler.recharge();
498512
}
499513

500514
firePageError(error: Error) {
@@ -631,3 +645,57 @@ function addPageBinding(bindingName: string, needsHandle: boolean) {
631645
};
632646
(globalThis as any)[bindingName].__installed = true;
633647
}
648+
649+
class FrameThrottler {
650+
private _acks: (() => void)[] = [];
651+
private _interval: number;
652+
private _nonThrottledFrames: number;
653+
private _budget: number;
654+
private _intervalId: NodeJS.Timeout | undefined;
655+
656+
constructor(nonThrottledFrames: number, interval: number) {
657+
this._nonThrottledFrames = nonThrottledFrames;
658+
this._budget = nonThrottledFrames;
659+
this._interval = interval;
660+
}
661+
662+
setEnabled(enabled: boolean) {
663+
if (enabled) {
664+
if (this._intervalId)
665+
clearInterval(this._intervalId);
666+
this._intervalId = setInterval(() => this._tick(), this._interval);
667+
} else if (this._intervalId) {
668+
clearInterval(this._intervalId);
669+
this._intervalId = undefined;
670+
}
671+
}
672+
673+
recharge() {
674+
// Send all acks, reset budget.
675+
for (const ack of this._acks)
676+
ack();
677+
this._acks = [];
678+
this._budget = this._nonThrottledFrames;
679+
}
680+
681+
ack(ack: () => void) {
682+
// Either not engaged or video is also recording, don't throttle.
683+
if (!this._intervalId) {
684+
ack();
685+
return;
686+
}
687+
688+
// Do we have enough budget to respond w/o throttling?
689+
if (--this._budget > 0) {
690+
ack();
691+
return;
692+
}
693+
694+
// Schedule.
695+
this._acks.push(ack);
696+
}
697+
698+
private _tick() {
699+
this._acks.shift()?.();
700+
}
701+
}

packages/playwright-core/src/server/trace/recorder/tracing.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
254254
}
255255

256256
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
257+
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
257258
// Set afterSnapshot name for all the actions that operate selectors.
258259
// Elements resolved from selectors will be marked on the snapshot.
259260
metadata.afterSnapshot = `after@${metadata.id}`;
@@ -263,12 +264,14 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
263264
}
264265

265266
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
267+
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
266268
const actionSnapshot = this._captureSnapshot('action', sdkObject, metadata, element);
267269
this._pendingCalls.get(metadata.id)!.actionSnapshot = actionSnapshot;
268270
await actionSnapshot;
269271
}
270272

271273
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
274+
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
272275
const pendingCall = this._pendingCalls.get(metadata.id);
273276
if (!pendingCall || pendingCall.afterSnapshot)
274277
return;

packages/playwright-core/src/server/webkit/wkPage.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,10 @@ export class WKPage implements PageDelegate {
873873
}
874874

875875
private _onScreencastFrame(event: Protocol.Screencast.screencastFramePayload) {
876-
this._pageProxySession.send('Screencast.screencastFrameAck', { generation: this._screencastGeneration }).catch(e => debugLogger.log('error', e));
876+
const generation = this._screencastGeneration;
877+
this._page.throttleScreencastFrameAck(() => {
878+
this._pageProxySession.send('Screencast.screencastFrameAck', { generation }).catch(e => debugLogger.log('error', e));
879+
});
877880
const buffer = Buffer.from(event.data, 'base64');
878881
this._page.emit(Page.Events.ScreencastFrame, {
879882
buffer,

0 commit comments

Comments
 (0)