• 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 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  convertDictKeys(obj: any): any {
117    if (Array.isArray(obj)) {
118      return obj.map((v) => this.convertDictKeys(v));
119    }
120    if (typeof obj === 'object' && obj !== null) {
121      const converted: any = {};
122      for (const key of Object.keys(obj)) {
123        converted[this.toCamelCase(key, '_')] = this.convertDictKeys(obj[key]);
124      }
125      return converted;
126    }
127    return obj;
128  }
129
130  convertToDevToolsConfig(config: any): Protocol.Tracing.TraceConfig {
131    // DevTools uses a different naming style for config properties: Dictionary
132    // keys are named "camelCase" style, rather than "underscore_case" style as
133    // in the TraceConfig.
134    config = this.convertDictKeys(config);
135    // recordMode is specified as an enum with camelCase values.
136    if (config.recordMode) {
137      config.recordMode = this.toCamelCase(config.recordMode as string, '-');
138    }
139    return config as Protocol.Tracing.TraceConfig;
140  }
141
142  // TODO(nicomazz): write unit test for this
143  extractChromeConfig(perfettoConfig: TraceConfig):
144      Protocol.Tracing.TraceConfig {
145    for (const ds of perfettoConfig.dataSources) {
146      if (ds.config && ds.config.name === 'org.chromium.trace_event' &&
147          ds.config.chromeConfig && ds.config.chromeConfig.traceConfig) {
148        const chromeConfigJsonString = ds.config.chromeConfig.traceConfig;
149        const config = JSON.parse(chromeConfigJsonString);
150        return this.convertToDevToolsConfig(config);
151      }
152    }
153    return {};
154  }
155
156  freeBuffers() {
157    this.devtoolsSocket.detach();
158    this.sendMessage({type: 'FreeBuffersResponse'});
159  }
160
161  async readBuffers(offset = 0) {
162    if (!this.devtoolsSocket.isAttached() || this.streamHandle === undefined) {
163      this.sendErrorMessage('No tracing session to read from');
164      return;
165    }
166
167    const res = await this.api.IO.read(
168        {handle: this.streamHandle, offset, size: CHUNK_SIZE});
169    if (res === undefined) return;
170
171    const chunk = res.base64Encoded ? atob(res.data) : res.data;
172    // The 'as {} as UInt8Array' is done because we can't send ArrayBuffers
173    // trough a chrome.runtime.Port. The conversion from string to ArrayBuffer
174    // takes place on the other side of the port.
175    const response: ReadBuffersResponse = {
176      type: 'ReadBuffersResponse',
177      slices: [{data: chunk as {} as Uint8Array, lastSliceForPacket: res.eof}],
178    };
179    this.sendMessage(response);
180    if (res.eof) return;
181    this.readBuffers(offset + res.data.length);
182  }
183
184  async disableTracing() {
185    await this.endTracing(this.tracingSessionId);
186    this.sendMessage({type: 'DisableTracingResponse'});
187  }
188
189  async endTracing(tracingSessionId: number) {
190    if (tracingSessionId !== this.tracingSessionId) {
191      return;
192    }
193    if (this.tracingSessionOngoing) {
194      await this.api.Tracing.end();
195    }
196    this.tracingSessionOngoing = false;
197  }
198
199  getTraceStats() {
200    let percentFull = 0;  // If the statistics are not available yet, it is 0.
201    if (this.lastBufferUsageEvent && this.lastBufferUsageEvent.percentFull) {
202      percentFull = this.lastBufferUsageEvent.percentFull;
203    }
204    const stats: perfetto.protos.ITraceStats = {
205      bufferStats:
206          [{bufferSize: 1000, bytesWritten: Math.round(percentFull * 1000)}],
207    };
208    const response: GetTraceStatsResponse = {
209      type: 'GetTraceStatsResponse',
210      traceStats: stats,
211    };
212    this.sendMessage(response);
213  }
214
215  getCategories() {
216    const fetchCategories = async () => {
217      const categories = (await this.api.Tracing.getCategories()).categories;
218      this.uiPort.postMessage({type: 'GetCategoriesResponse', categories});
219    };
220    // If a target is already attached, we simply fetch the categories.
221    if (this.devtoolsSocket.isAttached()) {
222      fetchCategories();
223      return;
224    }
225    // Otherwise, we attach temporarily.
226    this.devtoolsSocket.attachToBrowser(async (error?: string) => {
227      if (error) {
228        this.sendErrorMessage(
229            `Could not attach to DevTools browser target ` +
230            `(req. Chrome >= M81): ${error}`);
231        return;
232      }
233      fetchCategories();
234      this.devtoolsSocket.detach();
235    });
236  }
237
238  resetState() {
239    this.devtoolsSocket.detach();
240    this.streamHandle = undefined;
241  }
242
243  onTracingComplete(params: Protocol.Tracing.TracingCompleteEvent) {
244    this.streamHandle = params.stream;
245    this.sendMessage({type: 'EnableTracingResponse'});
246  }
247
248  onBufferUsage(params: Protocol.Tracing.BufferUsageEvent) {
249    this.lastBufferUsageEvent = params;
250  }
251
252  handleStartTracing(traceConfigProto: Uint8Array) {
253    this.devtoolsSocket.attachToBrowser(async (error?: string) => {
254      if (error) {
255        this.sendErrorMessage(
256            `Could not attach to DevTools browser target ` +
257            `(req. Chrome >= M81): ${error}`);
258        return;
259      }
260
261      const requestParams: Protocol.Tracing.StartRequest = {
262        streamFormat: 'proto',
263        transferMode: 'ReturnAsStream',
264        streamCompression: 'gzip',
265        bufferUsageReportingInterval: 200,
266      };
267
268      const traceConfig = TraceConfig.decode(traceConfigProto);
269      if (browserSupportsPerfettoConfig()) {
270        const configEncoded = base64Encode(traceConfigProto);
271        await this.api.Tracing.start(
272            {perfettoConfig: configEncoded, ...requestParams});
273        this.tracingSessionOngoing = true;
274        const tracingSessionId = ++this.tracingSessionId;
275        setTimeout(
276            () => this.endTracing(tracingSessionId), traceConfig.durationMs);
277      } else {
278        console.log(
279            'Used Chrome version is too old to support ' +
280            'perfettoConfig parameter. Using chrome config only instead.');
281
282        if (hasSystemDataSourceConfig(traceConfig)) {
283          this.sendErrorMessage(
284              'System tracing is not supported by this Chrome version. Choose' +
285              ' the \'Chrome\' target instead to record a Chrome-only trace.');
286          return;
287        }
288
289        const chromeConfig = this.extractChromeConfig(traceConfig);
290        await this.api.Tracing.start(
291            {traceConfig: chromeConfig, ...requestParams});
292      }
293    });
294  }
295}
296