feat(trace): throttle the screencast (#9893)
This commit is contained in:
parent
8991bbde33
commit
94c33da946
|
|
@ -857,7 +857,9 @@ class FrameSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onScreencastFrame(payload: Protocol.Page.screencastFramePayload) {
|
_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');
|
const buffer = Buffer.from(payload.data, 'base64');
|
||||||
this._page.emit(Page.Events.ScreencastFrame, {
|
this._page.emit(Page.Events.ScreencastFrame, {
|
||||||
buffer,
|
buffer,
|
||||||
|
|
|
||||||
|
|
@ -500,7 +500,10 @@ export class FFPage implements PageDelegate {
|
||||||
private _onScreencastFrame(event: Protocol.Page.screencastFramePayload) {
|
private _onScreencastFrame(event: Protocol.Page.screencastFramePayload) {
|
||||||
if (!this._screencastId)
|
if (!this._screencastId)
|
||||||
return;
|
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');
|
const buffer = Buffer.from(event.data, 'base64');
|
||||||
this._page.emit(Page.Events.ScreencastFrame, {
|
this._page.emit(Page.Events.ScreencastFrame, {
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ export class Page extends SdkObject {
|
||||||
_pageIsError: Error | undefined;
|
_pageIsError: Error | undefined;
|
||||||
_video: Artifact | null = null;
|
_video: Artifact | null = null;
|
||||||
_opener: Page | undefined;
|
_opener: Page | undefined;
|
||||||
|
private _frameThrottler = new FrameThrottler(10, 200);
|
||||||
|
|
||||||
constructor(delegate: PageDelegate, browserContext: BrowserContext) {
|
constructor(delegate: PageDelegate, browserContext: BrowserContext) {
|
||||||
super(browserContext, 'page');
|
super(browserContext, 'page');
|
||||||
|
|
@ -209,6 +210,7 @@ export class Page extends SdkObject {
|
||||||
|
|
||||||
_didClose() {
|
_didClose() {
|
||||||
this._frameManager.dispose();
|
this._frameManager.dispose();
|
||||||
|
this._frameThrottler.setEnabled(false);
|
||||||
assert(this._closedState !== 'closed', 'Page closed twice');
|
assert(this._closedState !== 'closed', 'Page closed twice');
|
||||||
this._closedState = 'closed';
|
this._closedState = 'closed';
|
||||||
this.emit(Page.Events.Close);
|
this.emit(Page.Events.Close);
|
||||||
|
|
@ -217,12 +219,14 @@ export class Page extends SdkObject {
|
||||||
|
|
||||||
_didCrash() {
|
_didCrash() {
|
||||||
this._frameManager.dispose();
|
this._frameManager.dispose();
|
||||||
|
this._frameThrottler.setEnabled(false);
|
||||||
this.emit(Page.Events.Crash);
|
this.emit(Page.Events.Crash);
|
||||||
this._crashedPromise.resolve(new Error('Page crashed'));
|
this._crashedPromise.resolve(new Error('Page crashed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
_didDisconnect() {
|
_didDisconnect() {
|
||||||
this._frameManager.dispose();
|
this._frameManager.dispose();
|
||||||
|
this._frameThrottler.setEnabled(false);
|
||||||
assert(!this._disconnected, 'Page disconnected twice');
|
assert(!this._disconnected, 'Page disconnected twice');
|
||||||
this._disconnected = true;
|
this._disconnected = true;
|
||||||
this._disconnectedPromise.resolve(new Error('Page closed'));
|
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) {
|
setScreencastOptions(options: { width: number, height: number, quality: number } | null) {
|
||||||
this._delegate.setScreencastOptions(options).catch(e => debugLogger.log('error', e));
|
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) {
|
firePageError(error: Error) {
|
||||||
|
|
@ -631,3 +645,57 @@ function addPageBinding(bindingName: string, needsHandle: boolean) {
|
||||||
};
|
};
|
||||||
(globalThis as any)[bindingName].__installed = true;
|
(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()?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||||
|
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
||||||
// Set afterSnapshot name for all the actions that operate selectors.
|
// Set afterSnapshot name for all the actions that operate selectors.
|
||||||
// Elements resolved from selectors will be marked on the snapshot.
|
// Elements resolved from selectors will be marked on the snapshot.
|
||||||
metadata.afterSnapshot = `after@${metadata.id}`;
|
metadata.afterSnapshot = `after@${metadata.id}`;
|
||||||
|
|
@ -263,12 +264,14 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
|
||||||
}
|
}
|
||||||
|
|
||||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
|
||||||
|
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
||||||
const actionSnapshot = this._captureSnapshot('action', sdkObject, metadata, element);
|
const actionSnapshot = this._captureSnapshot('action', sdkObject, metadata, element);
|
||||||
this._pendingCalls.get(metadata.id)!.actionSnapshot = actionSnapshot;
|
this._pendingCalls.get(metadata.id)!.actionSnapshot = actionSnapshot;
|
||||||
await actionSnapshot;
|
await actionSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||||
|
sdkObject.attribution.page?.temporarlyDisableTracingScreencastThrottling();
|
||||||
const pendingCall = this._pendingCalls.get(metadata.id);
|
const pendingCall = this._pendingCalls.get(metadata.id);
|
||||||
if (!pendingCall || pendingCall.afterSnapshot)
|
if (!pendingCall || pendingCall.afterSnapshot)
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -873,7 +873,10 @@ export class WKPage implements PageDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onScreencastFrame(event: Protocol.Screencast.screencastFramePayload) {
|
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');
|
const buffer = Buffer.from(event.data, 'base64');
|
||||||
this._page.emit(Page.Events.ScreencastFrame, {
|
this._page.emit(Page.Events.ScreencastFrame, {
|
||||||
buffer,
|
buffer,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue