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