• 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
45  constructor(port: chrome.runtime.Port) {
46    super({
47      onConsumerPortResponse: (message: ConsumerPortResponse) =>
48          this.uiPort.postMessage(message),
49
50      onError: (error: string) =>
51          this.uiPort.postMessage({type: 'ChromeExtensionError', error}),
52
53      onStatus: (status) =>
54          this.uiPort.postMessage({type: 'ChromeExtensionStatus', status})
55    });
56    this.uiPort = port;
57    this.devtoolsSocket = new DevToolsSocket();
58    this.devtoolsSocket.on('close', () => this.resetState());
59    const rpcClient = new rpc.Client(this.devtoolsSocket);
60    this.api = rpcClient.api();
61    this.api.Tracing.on('tracingComplete', this.onTracingComplete.bind(this));
62    this.api.Tracing.on('bufferUsage', this.onBufferUsage.bind(this));
63  }
64
65  handleCommand(methodName: string, requestData: Uint8Array) {
66    switch (methodName) {
67      case 'EnableTracing':
68        this.enableTracing(requestData);
69        break;
70      case 'FreeBuffers':
71        this.freeBuffers();
72        break;
73      case 'ReadBuffers':
74        this.readBuffers();
75        break;
76      case 'DisableTracing':
77        this.disableTracing();
78        break;
79      case 'GetTraceStats':
80        this.getTraceStats();
81        break;
82      case 'GetCategories':
83        this.getCategories();
84        break;
85      default:
86        this.sendErrorMessage('Action not recognized');
87        console.log('Received not recognized message: ', methodName);
88        break;
89    }
90  }
91
92  enableTracing(enableTracingRequest: Uint8Array) {
93    this.resetState();
94    const traceConfigProto = extractTraceConfig(enableTracingRequest);
95    if (!traceConfigProto) {
96      this.sendErrorMessage('Invalid trace config');
97      return;
98    }
99
100    this.handleStartTracing(traceConfigProto);
101  }
102
103  toCamelCase(key: string, separator: string): string {
104    return key.split(separator)
105        .map((part, index) => {
106          return (index === 0) ? part : part[0].toUpperCase() + part.slice(1);
107        })
108        .join('');
109  }
110
111  // tslint:disable-next-line: no-any
112  convertDictKeys(obj: any): any {
113    if (Array.isArray(obj)) {
114      return obj.map(v => this.convertDictKeys(v));
115    }
116    if (typeof obj === 'object' && obj !== null) {
117      // tslint:disable-next-line: no-any
118      const converted: any = {};
119      for (const key of Object.keys(obj)) {
120        converted[this.toCamelCase(key, '_')] = this.convertDictKeys(obj[key]);
121      }
122      return converted;
123    }
124    return obj;
125  }
126
127  // tslint:disable-next-line: no-any
128  convertToDevToolsConfig(config: any): Protocol.Tracing.TraceConfig {
129    // DevTools uses a different naming style for config properties: Dictionary
130    // keys are named "camelCase" style, rather than "underscore_case" style as
131    // in the TraceConfig.
132    config = this.convertDictKeys(config);
133    // recordMode is specified as an enum with camelCase values.
134    if (config.recordMode) {
135      config.recordMode = this.toCamelCase(config.recordMode as string, '-');
136    }
137    return config as Protocol.Tracing.TraceConfig;
138  }
139
140  // TODO(nicomazz): write unit test for this
141  extractChromeConfig(perfettoConfig: TraceConfig):
142      Protocol.Tracing.TraceConfig {
143    for (const ds of perfettoConfig.dataSources) {
144      if (ds.config && ds.config.name === 'org.chromium.trace_event' &&
145          ds.config.chromeConfig && ds.config.chromeConfig.traceConfig) {
146        const chromeConfigJsonString = ds.config.chromeConfig.traceConfig;
147        const config = JSON.parse(chromeConfigJsonString);
148        return this.convertToDevToolsConfig(config);
149      }
150    }
151    return {};
152  }
153
154  freeBuffers() {
155    this.devtoolsSocket.detach();
156    this.sendMessage({type: 'FreeBuffersResponse'});
157  }
158
159  async readBuffers(offset = 0) {
160    if (!this.devtoolsSocket.isAttached() || this.streamHandle === undefined) {
161      this.sendErrorMessage('No tracing session to read from');
162      return;
163    }
164
165    const res = await this.api.IO.read(
166        {handle: this.streamHandle, offset, size: CHUNK_SIZE});
167    if (res === undefined) return;
168
169    const chunk = res.base64Encoded ? atob(res.data) : res.data;
170    // The 'as {} as UInt8Array' is done because we can't send ArrayBuffers
171    // trough a chrome.runtime.Port. The conversion from string to ArrayBuffer
172    // takes place on the other side of the port.
173    const response: ReadBuffersResponse = {
174      type: 'ReadBuffersResponse',
175      slices: [{data: chunk as {} as Uint8Array, lastSliceForPacket: res.eof}]
176    };
177    this.sendMessage(response);
178    if (res.eof) return;
179    this.readBuffers(offset + res.data.length);
180  }
181
182  async disableTracing() {
183    await this.api.Tracing.end();
184    this.sendMessage({type: 'DisableTracingResponse'});
185  }
186
187  getTraceStats() {
188    let percentFull = 0;  // If the statistics are not available yet, it is 0.
189    if (this.lastBufferUsageEvent && this.lastBufferUsageEvent.percentFull) {
190      percentFull = this.lastBufferUsageEvent.percentFull;
191    }
192    const stats: perfetto.protos.ITraceStats = {
193      bufferStats:
194          [{bufferSize: 1000, bytesWritten: Math.round(percentFull * 1000)}]
195    };
196    const response: GetTraceStatsResponse = {
197      type: 'GetTraceStatsResponse',
198      traceStats: stats
199    };
200    this.sendMessage(response);
201  }
202
203  getCategories() {
204    const fetchCategories = async () => {
205      const categories = (await this.api.Tracing.getCategories()).categories;
206      this.uiPort.postMessage({type: 'GetCategoriesResponse', categories});
207    };
208    // If a target is already attached, we simply fetch the categories.
209    if (this.devtoolsSocket.isAttached()) {
210      fetchCategories();
211      return;
212    }
213    // Otherwise, we attach temporarily.
214    this.devtoolsSocket.attachToBrowser(async (error?: string) => {
215      if (error) {
216        this.sendErrorMessage(
217            `Could not attach to DevTools browser target ` +
218            `(req. Chrome >= M81): ${error}`);
219        return;
220      }
221      fetchCategories();
222      this.devtoolsSocket.detach();
223    });
224  }
225
226  resetState() {
227    this.devtoolsSocket.detach();
228    this.streamHandle = undefined;
229  }
230
231  onTracingComplete(params: Protocol.Tracing.TracingCompleteEvent) {
232    this.streamHandle = params.stream;
233    this.sendMessage({type: 'EnableTracingResponse'});
234  }
235
236  onBufferUsage(params: Protocol.Tracing.BufferUsageEvent) {
237    this.lastBufferUsageEvent = params;
238  }
239
240  handleStartTracing(traceConfigProto: Uint8Array) {
241    this.devtoolsSocket.attachToBrowser(async (error?: string) => {
242      if (error) {
243        this.sendErrorMessage(
244            `Could not attach to DevTools browser target ` +
245            `(req. Chrome >= M81): ${error}`);
246        return;
247      }
248
249      const requestParams: Protocol.Tracing.StartRequest = {
250        streamFormat: 'proto',
251        transferMode: 'ReturnAsStream',
252        streamCompression: 'gzip',
253        bufferUsageReportingInterval: 200
254      };
255
256      if (browserSupportsPerfettoConfig()) {
257        const configEncoded = base64Encode(traceConfigProto);
258        await this.api.Tracing.start(
259            {perfettoConfig: configEncoded, ...requestParams});
260      } else {
261        console.log(
262            'Used Chrome version is too old to support ' +
263            'perfettoConfig parameter. Using chrome config only instead.');
264
265        const traceConfig = TraceConfig.decode(traceConfigProto);
266        if (hasSystemDataSourceConfig(traceConfig)) {
267          this.sendErrorMessage(
268              'System tracing is not supported by this Chrome version. Choose' +
269              ' the \'Chrome\' target instead to record a Chrome-only trace.');
270          return;
271        }
272
273        const chromeConfig = this.extractChromeConfig(traceConfig);
274        await this.api.Tracing.start(
275            {traceConfig: chromeConfig, ...requestParams});
276      }
277    });
278  }
279}
280