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