Skip to content

Commit

Permalink
chore: save chrome trace on the client side (#24414)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Jul 26, 2023
1 parent e036603 commit 4949cef
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 32 deletions.
14 changes: 14 additions & 0 deletions packages/playwright-core/src/client/artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ export class Artifact extends ChannelOwner<channels.ArtifactChannel> {
return stream.stream();
}

async readIntoBuffer(): Promise<Buffer> {
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<void> {
return this._channel.cancel();
}
Expand Down
15 changes: 14 additions & 1 deletion packages/playwright-core/src/client/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<channels.BrowserChannel> implements api.Browser {
readonly _contexts = new Set<BrowserContext>();
Expand All @@ -33,6 +36,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
_browserType!: BrowserType;
_options: LaunchOptions = {};
readonly _name: string;
private _path: string | undefined;

// Used from @playwright/test fixtures.
_connectHeaders?: HeadersArray;
Expand Down Expand Up @@ -104,11 +108,20 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> 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<Buffer> {
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<void> {
Expand Down
3 changes: 1 addition & 2 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
20 changes: 11 additions & 9 deletions packages/playwright-core/src/server/chromium/crBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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 = '';

Expand Down Expand Up @@ -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;

Expand All @@ -287,31 +288,32 @@ 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;

if (screenshots)
categories.push('disabled-by-default-devtools.screenshot');

this._tracingPath = path;
this._tracingRecording = true;
await this._tracingClient.send('Tracing.start', {
transferMode: 'ReturnAsStream',
categories: categories.join(',')
});
}

async stopTracing(): Promise<Buffer> {
async stopTracing(): Promise<Artifact> {
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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/chromium/crPdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,6 @@ export class CRPDF {
pageRanges,
preferCSSPageSize
});
return await readProtocolStream(this._client, result.stream!, null);
return await readProtocolStream(this._client, result.stream!);
}
}
29 changes: 17 additions & 12 deletions packages/playwright-core/src/server/chromium/crProtocolHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer> {
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<Buffer> {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Browser, channels.BrowserChannel, BrowserTypeDispatcher> implements channels.BrowserChannel {
_type_Browser = true;
Expand Down Expand Up @@ -81,7 +82,7 @@ export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChann
if (!this._object.options.isChromium)
throw new Error(`Tracing is only available in Chromium`);
const crBrowser = this._object as CRBrowser;
return { binary: await crBrowser.stopTracing() };
return { artifact: ArtifactDispatcher.from(this, await crBrowser.stopTracing()) };
}
}

Expand Down Expand Up @@ -142,7 +143,7 @@ export class ConnectedBrowserDispatcher extends Dispatcher<Browser, channels.Bro
if (!this._object.options.isChromium)
throw new Error(`Tracing is only available in Chromium`);
const crBrowser = this._object as CRBrowser;
return { binary: await crBrowser.stopTracing() };
return { artifact: ArtifactDispatcher.from(this, await crBrowser.stopTracing()) };
}

async cleanupContexts() {
Expand Down
4 changes: 1 addition & 3 deletions packages/protocol/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1348,21 +1348,19 @@ export type BrowserNewBrowserCDPSessionResult = {
};
export type BrowserStartTracingParams = {
page?: PageChannel,
path?: string,
screenshots?: boolean,
categories?: string[],
};
export type BrowserStartTracingOptions = {
page?: PageChannel,
path?: string,
screenshots?: boolean,
categories?: string[],
};
export type BrowserStartTracingResult = void;
export type BrowserStopTracingParams = {};
export type BrowserStopTracingOptions = {};
export type BrowserStopTracingResult = {
binary: Binary,
artifact: ArtifactChannel,
};

export interface BrowserEvents {
Expand Down
3 changes: 1 addition & 2 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -955,15 +955,14 @@ Browser:
startTracing:
parameters:
page: Page?
path: string?
screenshots: boolean?
categories:
type: array?
items: string

stopTracing:
returns:
binary: binary
artifact: Artifact


events:
Expand Down

0 comments on commit 4949cef

Please sign in to comment.