1/**
2 * Copyright 2017 Google Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16import { assert } from './assert.js';
17import { helper } from './helper.js';
18import { CDPSession } from './Connection.js';
19
20/**
21 * @public
22 */
23export interface TracingOptions {
24  path?: string;
25  screenshots?: boolean;
26  categories?: string[];
27}
28
29/**
30 * The Tracing class exposes the tracing audit interface.
31 * @remarks
32 * You can use `tracing.start` and `tracing.stop` to create a trace file
33 * which can be opened in Chrome DevTools or {@link https://chromedevtools.github.io/timeline-viewer/ | timeline viewer}.
34 *
35 * @example
36 * ```js
37 * await page.tracing.start({path: 'trace.json'});
38 * await page.goto('https://www.google.com');
39 * await page.tracing.stop();
40 * ```
41 *
42 * @public
43 */
44export class Tracing {
45  _client: CDPSession;
46  _recording = false;
47  _path = '';
48
49  /**
50   * @internal
51   */
52  constructor(client: CDPSession) {
53    this._client = client;
54  }
55
56  /**
57   * Starts a trace for the current page.
58   * @remarks
59   * Only one trace can be active at a time per browser.
60   * @param options - Optional `TracingOptions`.
61   */
62  async start(options: TracingOptions = {}): Promise<void> {
63    assert(
64      !this._recording,
65      'Cannot start recording trace while already recording trace.'
66    );
67
68    const defaultCategories = [
69      '-*',
70      'devtools.timeline',
71      'v8.execute',
72      'disabled-by-default-devtools.timeline',
73      'disabled-by-default-devtools.timeline.frame',
74      'toplevel',
75      'blink.console',
76      'blink.user_timing',
77      'latencyInfo',
78      'disabled-by-default-devtools.timeline.stack',
79      'disabled-by-default-v8.cpu_profiler',
80    ];
81    const {
82      path = null,
83      screenshots = false,
84      categories = defaultCategories,
85    } = options;
86
87    if (screenshots) categories.push('disabled-by-default-devtools.screenshot');
88
89    const excludedCategories = categories
90      .filter((cat) => cat.startsWith('-'))
91      .map((cat) => cat.slice(1));
92    const includedCategories = categories.filter((cat) => !cat.startsWith('-'));
93
94    this._path = path;
95    this._recording = true;
96    await this._client.send('Tracing.start', {
97      transferMode: 'ReturnAsStream',
98      traceConfig: {
99        excludedCategories,
100        includedCategories,
101      },
102    });
103  }
104
105  /**
106   * Stops a trace started with the `start` method.
107   * @returns Promise which resolves to buffer with trace data.
108   */
109  async stop(): Promise<Buffer> {
110    let fulfill: (value: Buffer) => void;
111    let reject: (err: Error) => void;
112    const contentPromise = new Promise<Buffer>((x, y) => {
113      fulfill = x;
114      reject = y;
115    });
116    this._client.once('Tracing.tracingComplete', async (event) => {
117      try {
118        const readable = await helper.getReadableFromProtocolStream(
119          this._client,
120          event.stream
121        );
122        const buffer = await helper.getReadableAsBuffer(readable, this._path);
123        fulfill(buffer);
124      } catch (error) {
125        reject(error);
126      }
127    });
128    await this._client.send('Tracing.end');
129    this._recording = false;
130    return contentPromise;
131  }
132}
133