• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2019 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {Protocol} from 'devtools-protocol';
16import {ProtocolProxyApi} from 'devtools-protocol/types/protocol-proxy-api';
17import * as rpc from 'noice-json-rpc';
18
19import {base64Encode} from '../base/string_utils';
20import {
21  browserSupportsPerfettoConfig,
22  extractTraceConfig,
23  hasSystemDataSourceConfig
24} from '../base/trace_config_utils';
25import {TraceConfig} from '../common/protos';
26import {
27  ConsumerPortResponse,
28  GetTraceStatsResponse,
29  ReadBuffersResponse
30} from '../controller/consumer_port_types';
31import {RpcConsumerPort} from '../controller/record_controller_interfaces';
32import {perfetto} from '../gen/protos';
33
34import {DevToolsSocket} from './devtools_socket';
35
36const CHUNK_SIZE: number = 1024 * 1024 * 16;  // 16Mb
37
38export class ChromeTracingController extends RpcConsumerPort {
39  private streamHandle: string|undefined = undefined;
40  private uiPort: chrome.runtime.Port;
41  private api: ProtocolProxyApi.ProtocolApi;
42  private devtoolsSocket: DevToolsSocket;
43  private lastBufferUsageEvent: Protocol.Tracing.BufferUsageEvent|undefined;
44  private tracingSessionOngoing = false;
45  private tracingSessionId = 0;
46
47  constructor(port: chrome.runtime.Port) {
48    super({
49      onConsumerPortResponse: (message: ConsumerPortResponse) =>
50          this.uiPort.postMessage(message),
51
52      onError: (error: string) =>
53          this.uiPort.postMessage({type: 'ChromeExtensionError', error}),
54
55      onStatus: (status) =>
56          this.uiPort.postMessage({type: 'ChromeExtensionStatus', status})
57    });
58    this.uiPort = port;
59    this.devtoolsSocket = new DevToolsSocket();
60    this.devtoolsSocket.on('close', () => this.resetState());
61    const rpcClient = new rpc.Client(this.devtoolsSocket);
62    this.api = rpcClient.api();
63    this.api.Tracing.on('tracingComplete', this.onTracingComplete.bind(this));
64    this.api.Tracing.on('bufferUsage', this.onBufferUsage.bind(this));
65    this.uiPort.onDisconnect.addListener(() => {
66      this.devtoolsSocket.detach();
67    });
68  }
69
70  handleCommand(methodName: string, requestData: Uint8Array) {
71    switch (methodName) {
72      case 'EnableTracing':
73        this.enableTracing(requestData);
74        break;
75      case 'FreeBuffers':
76        this.freeBuffers();
77        break;
78      case 'ReadBuffers':
79        this.readBuffers();
80        break;
81      case 'DisableTracing':
82        this.disableTracing();
83        break;
84      case 'GetTraceStats':
85        this.getTraceStats();
86        break;
87      case 'GetCategories':
88        this.getCategories();
89        break;
90      default:
91        this.sendErrorMessage('Action not recognized');
92        console.log('Received not recognized message: ', methodName);
93        break;
94    }
95  }
96
97  enableTracing(enableTracingRequest: Uint8Array) {
98    this.resetState();
99    const traceConfigProto = extractTraceConfig(enableTracingRequest);
100    if (!traceConfigProto) {
101      this.sendErrorMessage('Invalid trace config');
102      return;
103    }
104
105    this.handleStartTracing(traceConfigProto);
106  }
107
108  toCamelCase(key: string, separator: string): string {
109    return key.split(separator)
110        .map((part, index) => {
111          return (index === 0) ? part : part[0].toUpperCase() + part.slice(1);
112        })
113        .join('');
114  }
115
116  // tslint:disable-next-line: no-any
117  convertDictKeys(obj: any): any {
118    if (Array.isArray(obj)) {
119      return obj.map(v => this.convertDictKeys(v));
120    }
121    if (typeof obj === 'object' && obj !== null) {
122      // tslint:disable-next-line: no-any
123      const converted: any = {};
124      for (const key of Object.keys(obj)) {
125        converted[this.toCamelCase(key, '_')] = this.convertDictKeys(obj[key]);
126      }
127      return converted;
128    }
129    return obj;
130  }
131
132  // tslint:disable-next-line: no-any
133  convertToDevToolsConfig(config: any): Protocol.Tracing.TraceConfig {
134    // DevTools uses a different naming style for config properties: Dictionary
135    // keys are named "camelCase" style, rather than "underscore_case" style as
136    // in the TraceConfig.
137    config = this.convertDictKeys(config);
138    // recordMode is specified as an enum with camelCase values.
139    if (config.recordMode) {
140      config.recordMode = this.toCamelCase(config.recordMode as string, '-');
141    }
142    return config as Protocol.Tracing.TraceConfig;
143  }
144
145  // TODO(nicomazz): write unit test for this
146  extractChromeConfig(perfettoConfig: TraceConfig):
147      Protocol.Tracing.TraceConfig {
148    for (const ds of perfettoConfig.dataSources) {
149      if (ds.config && ds.config.name === 'org.chromium.trace_event' &&
150          ds.config.chromeConfig && ds.config.chromeConfig.traceConfig) {
151        const chromeConfigJsonString = ds.config.chromeConfig.traceConfig;
152        const config = JSON.parse(chromeConfigJsonString);
153        return this.convertToDevToolsConfig(config);
154      }
155    }
156    return {};
157  }
158
159  freeBuffers() {
160    this.devtoolsSocket.detach();
161    this.sendMessage({type: 'FreeBuffersResponse'});
162  }
163
164  async readBuffers(offset = 0) {
165    if (!this.devtoolsSocket.isAttached() || this.streamHandle === undefined) {
166      this.sendErrorMessage('No tracing session to read from');
167      return;
168    }
169
170    const res = await this.api.IO.read(
171        {handle: this.streamHandle, offset, size: CHUNK_SIZE});
172    if (res === undefined) return;
173
174    const chunk = res.base64Encoded ? atob(res.data) : res.data;
175    // The 'as {} as UInt8Array' is done because we can't send ArrayBuffers
176    // trough a chrome.runtime.Port. The conversion from string to ArrayBuffer
177    // takes place on the other side of the port.
178    const response: ReadBuffersResponse = {
179      type: 'ReadBuffersResponse',
180      slices: [{data: chunk as {} as Uint8Array, lastSliceForPacket: res.eof}]
181    };
182    this.sendMessage(response);
183    if (res.eof) return;
184    this.readBuffers(offset + res.data.length);
185  }
186
187  async disableTracing() {
188    await this.endTracing(this.tracingSessionId);
189    this.sendMessage({type: 'DisableTracingResponse'});
190  }
191
192  async endTracing(tracingSessionId: number) {
193    if (tracingSessionId !== this.tracingSessionId) {
194      return;
195    }
196    if (this.tracingSessionOngoing) {
197      await this.api.Tracing.end();
198    }
199    this.tracingSessionOngoing = false;
200  }
201
202  getTraceStats() {
203    let percentFull = 0;  // If the statistics are not available yet, it is 0.
204    if (this.lastBufferUsageEvent && this.lastBufferUsageEvent.percentFull) {
205      percentFull = this.lastBufferUsageEvent.percentFull;
206    }
207    const stats: perfetto.protos.ITraceStats = {
208      bufferStats:
209          [{bufferSize: 1000, bytesWritten: Math.round(percentFull * 1000)}]
210    };
211    const response: GetTraceStatsResponse = {
212      type: 'GetTraceStatsResponse',
213      traceStats: stats
214    };
215    this.sendMessage(response);
216  }
217
218  getCategories() {
219    const fetchCategories = async () => {
220      const categories = (await this.api.Tracing.getCategories()).categories;
221      this.uiPort.postMessage({type: 'GetCategoriesResponse', categories});
222    };
223    // If a target is already attached, we simply fetch the categories.
224    if (this.devtoolsSocket.isAttached()) {
225      fetchCategories();
226      return;
227    }
228    // Otherwise, we attach temporarily.
229    this.devtoolsSocket.attachToBrowser(async (error?: string) => {
230      if (error) {
231        this.sendErrorMessage(
232            `Could not attach to DevTools browser target ` +
233            `(req. Chrome >= M81): ${error}`);
234        return;
235      }
236      fetchCategories();
237      this.devtoolsSocket.detach();
238    });
239  }
240
241  resetState() {
242    this.devtoolsSocket.detach();
243    this.streamHandle = undefined;
244  }
245
246  onTracingComplete(params: Protocol.Tracing.TracingCompleteEvent) {
247    this.streamHandle = params.stream;
248    this.sendMessage({type: 'EnableTracingResponse'});
249  }
250
251  onBufferUsage(params: Protocol.Tracing.BufferUsageEvent) {
252    this.lastBufferUsageEvent = params;
253  }
254
255  handleStartTracing(traceConfigProto: Uint8Array) {
256    this.devtoolsSocket.attachToBrowser(async (error?: string) => {
257      if (error) {
258        this.sendErrorMessage(
259            `Could not attach to DevTools browser target ` +
260            `(req. Chrome >= M81): ${error}`);
261        return;
262      }
263
264      const requestParams: Protocol.Tracing.StartRequest = {
265        streamFormat: 'proto',
266        transferMode: 'ReturnAsStream',
267        streamCompression: 'gzip',
268        bufferUsageReportingInterval: 200
269      };
270
271      const traceConfig = TraceConfig.decode(traceConfigProto);
272      if (browserSupportsPerfettoConfig()) {
273        const configEncoded = base64Encode(traceConfigProto);
274        await this.api.Tracing.start(
275            {perfettoConfig: configEncoded, ...requestParams});
276        this.tracingSessionOngoing = true;
277        const tracingSessionId = ++this.tracingSessionId;
278        setTimeout(
279            () => this.endTracing(tracingSessionId), traceConfig.durationMs);
280      } else {
281        console.log(
282            'Used Chrome version is too old to support ' +
283            'perfettoConfig parameter. Using chrome config only instead.');
284
285        if (hasSystemDataSourceConfig(traceConfig)) {
286          this.sendErrorMessage(
287              'System tracing is not supported by this Chrome version. Choose' +
288              ' the \'Chrome\' target instead to record a Chrome-only trace.');
289          return;
290        }
291
292        const chromeConfig = this.extractChromeConfig(traceConfig);
293        await this.api.Tracing.start(
294            {traceConfig: chromeConfig, ...requestParams});
295      }
296    });
297  }
298}
299