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