From 4949cef09cfdae1c9f64eee977fcf777bc3c6621 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 26 Jul 2023 14:11:26 -0700 Subject: [PATCH] chore: save chrome trace on the client side (#24414) --- .../playwright-core/src/client/artifact.ts | 14 +++++++++ .../playwright-core/src/client/browser.ts | 15 +++++++++- .../playwright-core/src/protocol/validator.ts | 3 +- .../src/server/chromium/crBrowser.ts | 20 +++++++------ .../src/server/chromium/crPdf.ts | 2 +- .../src/server/chromium/crProtocolHelper.ts | 29 +++++++++++-------- .../server/dispatchers/browserDispatcher.ts | 5 ++-- packages/protocol/src/channels.ts | 4 +-- packages/protocol/src/protocol.yml | 3 +- 9 files changed, 63 insertions(+), 32 deletions(-) diff --git a/packages/playwright-core/src/client/artifact.ts b/packages/playwright-core/src/client/artifact.ts index 90fb0895dd544..84fc9ef9835bd 100644 --- a/packages/playwright-core/src/client/artifact.ts +++ b/packages/playwright-core/src/client/artifact.ts @@ -60,6 +60,20 @@ export class Artifact extends ChannelOwner { return stream.stream(); } + async readIntoBuffer(): Promise { + const stream = (await this.createReadStream())!; + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + stream.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + stream.on('end', () => { + resolve(Buffer.concat(chunks)); + }); + stream.on('error', reject); + }); + } + async cancel(): Promise { return this._channel.cancel(); } diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 6dd0b43477a5e..e36cb0d168627 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import fs from 'fs'; import type * as channels from '@protocol/channels'; import { BrowserContext, prepareBrowserContextParams } from './browserContext'; import type { Page } from './page'; @@ -24,6 +25,8 @@ import { isSafeCloseError, kBrowserClosedError } from '../common/errors'; import type * as api from '../../types/types'; import { CDPSession } from './cdpSession'; import type { BrowserType } from './browserType'; +import { Artifact } from './artifact'; +import { mkdirIfNeeded } from '../utils'; export class Browser extends ChannelOwner implements api.Browser { readonly _contexts = new Set(); @@ -33,6 +36,7 @@ export class Browser extends ChannelOwner implements ap _browserType!: BrowserType; _options: LaunchOptions = {}; readonly _name: string; + private _path: string | undefined; // Used from @playwright/test fixtures. _connectHeaders?: HeadersArray; @@ -104,11 +108,20 @@ export class Browser extends ChannelOwner implements ap } async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { + this._path = options.path; await this._channel.startTracing({ ...options, page: page ? page._channel : undefined }); } async stopTracing(): Promise { - return (await this._channel.stopTracing()).binary; + const artifact = Artifact.from((await this._channel.stopTracing()).artifact); + const buffer = await artifact.readIntoBuffer(); + await artifact.delete(); + if (this._path) { + await mkdirIfNeeded(this._path); + await fs.promises.writeFile(this._path, buffer); + this._path = undefined; + } + return buffer; } async close(): Promise { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 575784da91958..a6983b2089d87 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -723,14 +723,13 @@ scheme.BrowserNewBrowserCDPSessionResult = tObject({ }); scheme.BrowserStartTracingParams = tObject({ page: tOptional(tChannel(['Page'])), - path: tOptional(tString), screenshots: tOptional(tBoolean), categories: tOptional(tArray(tString)), }); scheme.BrowserStartTracingResult = tOptional(tObject({})); scheme.BrowserStopTracingParams = tOptional(tObject({})); scheme.BrowserStopTracingResult = tObject({ - binary: tBinary, + artifact: tChannel(['Artifact']), }); scheme.EventTargetInitializer = tOptional(tObject({})); scheme.EventTargetWaitForEventInfoParams = tObject({ diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 35ede903de20c..8204bb8fbedae 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -16,9 +16,10 @@ */ import type { BrowserOptions } from '../browser'; +import path from 'path'; import { Browser } from '../browser'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; -import { assert } from '../../utils'; +import { assert, createGuid } from '../../utils'; import * as network from '../network'; import type { PageBinding, PageDelegate, Worker } from '../page'; import { Page } from '../page'; @@ -30,11 +31,12 @@ import type * as channels from '@protocol/channels'; import type { CRSession } from './crConnection'; import { ConnectionEvents, CRConnection } from './crConnection'; import { CRPage } from './crPage'; -import { readProtocolStream } from './crProtocolHelper'; +import { saveProtocolStream } from './crProtocolHelper'; import type { Protocol } from './protocol'; import type { CRDevTools } from './crDevTools'; import { CRServiceWorker } from './crServiceWorker'; import type { SdkObject } from '../instrumentation'; +import { Artifact } from '../artifact'; export class CRBrowser extends Browser { readonly _connection: CRConnection; @@ -48,7 +50,6 @@ export class CRBrowser extends Browser { private _version = ''; private _tracingRecording = false; - private _tracingPath: string | null = ''; private _tracingClient: CRSession | undefined; private _userAgent: string = ''; @@ -276,7 +277,7 @@ export class CRBrowser extends Browser { return await this._connection.createBrowserSession(); } - async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { + async startTracing(page?: Page, options: { screenshots?: boolean; categories?: string[]; } = {}) { assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.'); this._tracingClient = page ? (page._delegate as CRPage)._mainFrameSession._client : this._session; @@ -287,7 +288,6 @@ export class CRBrowser extends Browser { 'disabled-by-default-v8.cpu_profiler', 'disabled-by-default-v8.cpu_profiler.hires' ]; const { - path = null, screenshots = false, categories = defaultCategories, } = options; @@ -295,7 +295,6 @@ export class CRBrowser extends Browser { if (screenshots) categories.push('disabled-by-default-devtools.screenshot'); - this._tracingPath = path; this._tracingRecording = true; await this._tracingClient.send('Tracing.start', { transferMode: 'ReturnAsStream', @@ -303,15 +302,18 @@ export class CRBrowser extends Browser { }); } - async stopTracing(): Promise { + async stopTracing(): Promise { assert(this._tracingClient, 'Tracing was not started.'); const [event] = await Promise.all([ new Promise(f => this._tracingClient!.once('Tracing.tracingComplete', f)), this._tracingClient.send('Tracing.end') ]); - const result = await readProtocolStream(this._tracingClient, (event as any).stream!, this._tracingPath); + const tracingPath = path.join(this.options.artifactsDir, createGuid() + '.crtrace'); + await saveProtocolStream(this._tracingClient, (event as any).stream!, tracingPath); this._tracingRecording = false; - return result; + const artifact = new Artifact(this, tracingPath); + artifact.reportFinished(); + return artifact; } isConnected(): boolean { diff --git a/packages/playwright-core/src/server/chromium/crPdf.ts b/packages/playwright-core/src/server/chromium/crPdf.ts index 359a1c4571631..0219c18444565 100644 --- a/packages/playwright-core/src/server/chromium/crPdf.ts +++ b/packages/playwright-core/src/server/chromium/crPdf.ts @@ -114,6 +114,6 @@ export class CRPDF { pageRanges, preferCSSPageSize }); - return await readProtocolStream(this._client, result.stream!, null); + return await readProtocolStream(this._client, result.stream!); } } diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts index db7c04a4adae6..36bc712aa6ac7 100644 --- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts +++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts @@ -40,26 +40,31 @@ export async function releaseObject(client: CRSession, objectId: string) { await client.send('Runtime.releaseObject', { objectId }).catch(error => {}); } -export async function readProtocolStream(client: CRSession, handle: string, path: string | null): Promise { +export async function saveProtocolStream(client: CRSession, handle: string, path: string) { let eof = false; - let fd: fs.promises.FileHandle | undefined; - if (path) { - await mkdirIfNeeded(path); - fd = await fs.promises.open(path, 'w'); + await mkdirIfNeeded(path); + const fd = await fs.promises.open(path, 'w'); + while (!eof) { + const response = await client.send('IO.read', { handle }); + eof = response.eof; + const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); + await fd.write(buf); } - const bufs = []; + await fd.close(); + await client.send('IO.close', { handle }); +} + +export async function readProtocolStream(client: CRSession, handle: string): Promise { + let eof = false; + const chunks = []; while (!eof) { const response = await client.send('IO.read', { handle }); eof = response.eof; const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); - bufs.push(buf); - if (fd) - await fd.write(buf); + chunks.push(buf); } - if (fd) - await fd.close(); await client.send('IO.close', { handle }); - return Buffer.concat(bufs); + return Buffer.concat(chunks); } export function toConsoleMessageLocation(stackTrace: Protocol.Runtime.StackTrace | undefined): types.ConsoleMessageLocation { diff --git a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts index 3d3dcd8213f63..85e609747003a 100644 --- a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts @@ -28,6 +28,7 @@ import { serverSideCallMetadata } from '../instrumentation'; import { BrowserContext } from '../browserContext'; import { Selectors } from '../selectors'; import type { BrowserTypeDispatcher } from './browserTypeDispatcher'; +import { ArtifactDispatcher } from './artifactDispatcher'; export class BrowserDispatcher extends Dispatcher implements channels.BrowserChannel { _type_Browser = true; @@ -81,7 +82,7 @@ export class BrowserDispatcher extends Dispatcher