chore: save chrome trace on the client side (#24414)

This commit is contained in:
Pavel Feldman 2023-07-26 14:11:26 -07:00 committed by GitHub
parent e036603aa3
commit 4949cef09c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 64 additions and 33 deletions

View File

@ -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();
}

View File

@ -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<channels.BrowserChannel> implements api.Browser {
readonly _contexts = new Set<BrowserContext>();
@ -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;
@ -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> {

View File

@ -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({

View File

@ -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<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 {

View File

@ -114,6 +114,6 @@ export class CRPDF {
pageRanges,
preferCSSPageSize
});
return await readProtocolStream(this._client, result.stream!, null);
return await readProtocolStream(this._client, result.stream!);
}
}

View File

@ -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');
}
const bufs = [];
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);
bufs.push(buf);
if (fd)
await fd.write(buf);
await fd.write(buf);
}
if (fd)
await fd.close();
await fd.close();
await client.send('IO.close', { handle });
return Buffer.concat(bufs);
}
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);
chunks.push(buf);
}
await client.send('IO.close', { handle });
return Buffer.concat(chunks);
}
export function toConsoleMessageLocation(stackTrace: Protocol.Runtime.StackTrace | undefined): types.ConsoleMessageLocation {

View File

@ -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;
@ -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()) };
}
}
@ -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() {

View File

@ -1348,13 +1348,11 @@ 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[],
};
@ -1362,7 +1360,7 @@ export type BrowserStartTracingResult = void;
export type BrowserStopTracingParams = {};
export type BrowserStopTracingOptions = {};
export type BrowserStopTracingResult = {
binary: Binary,
artifact: ArtifactChannel,
};
export interface BrowserEvents {

View File

@ -955,7 +955,6 @@ Browser:
startTracing:
parameters:
page: Page?
path: string?
screenshots: boolean?
categories:
type: array?
@ -963,7 +962,7 @@ Browser:
stopTracing:
returns:
binary: binary
artifact: Artifact
events: