1// Copyright (C) 2022 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 {defer, Deferred} from '../../base/deferred'; 16import {assertExists, assertTrue} from '../../base/logging'; 17import {binaryDecode, binaryEncode} from '../../base/string_utils'; 18import { 19 ChromeExtensionMessage, 20 isChromeExtensionError, 21 isChromeExtensionStatus, 22 isGetCategoriesResponse, 23} from '../../controller/chrome_proxy_record_controller'; 24import { 25 isDisableTracingResponse, 26 isEnableTracingResponse, 27 isFreeBuffersResponse, 28 isGetTraceStatsResponse, 29 isReadBuffersResponse, 30} from '../../controller/consumer_port_types'; 31import { 32 EnableTracingRequest, 33 IBufferStats, 34 ISlice, 35 TraceConfig, 36} from '../protos'; 37import {RecordingError} from './recording_error_handling'; 38import { 39 TracingSession, 40 TracingSessionListener, 41} from './recording_interfaces_v2'; 42import { 43 BUFFER_USAGE_INCORRECT_FORMAT, 44 BUFFER_USAGE_NOT_ACCESSIBLE, 45 EXTENSION_ID, 46 MALFORMED_EXTENSION_MESSAGE, 47} from './recording_utils'; 48 49// This class implements the protocol described in 50// https://perfetto.dev/docs/design-docs/api-and-abi#tracing-protocol-abi 51// However, with the Chrome extension we communicate using JSON messages. 52export class ChromeTracedTracingSession implements TracingSession { 53 // Needed for ReadBufferResponse: all the trace packets are split into 54 // several slices. |partialPacket| is the buffer for them. Once we receive a 55 // slice with the flag |lastSliceForPacket|, a new packet is created. 56 private partialPacket: ISlice[] = []; 57 58 // For concurrent calls to 'GetCategories', we return the same value. 59 private pendingGetCategoriesMessage?: Deferred<string[]>; 60 61 private pendingStatsMessages = new Array<Deferred<IBufferStats[]>>(); 62 63 // Port through which we communicate with the extension. 64 private chromePort: chrome.runtime.Port; 65 // True when Perfetto is connected via the port to the tracing session. 66 private isPortConnected: boolean; 67 68 constructor(private tracingSessionListener: TracingSessionListener) { 69 this.chromePort = chrome.runtime.connect(EXTENSION_ID); 70 this.isPortConnected = true; 71 } 72 73 start(config: TraceConfig): void { 74 if (!this.isPortConnected) return; 75 const duration = config.durationMs; 76 this.tracingSessionListener.onStatus(`Recording in progress${ 77 duration ? ' for ' + duration.toString() + ' ms' : ''}...`); 78 79 const enableTracingRequest = new EnableTracingRequest(); 80 enableTracingRequest.traceConfig = config; 81 const enableTracingRequestProto = binaryEncode( 82 EnableTracingRequest.encode(enableTracingRequest).finish()); 83 this.chromePort.postMessage( 84 {method: 'EnableTracing', requestData: enableTracingRequestProto}); 85 } 86 87 // The 'cancel' method will end the tracing session and will NOT return the 88 // trace. Therefore, we do not need to keep the connection open. 89 cancel(): void { 90 if (!this.isPortConnected) return; 91 this.terminateConnection(); 92 } 93 94 // The 'stop' method will end the tracing session and cause the trace to be 95 // returned via a callback. We maintain the connection to the target so we can 96 // extract the trace. 97 // See 'DisableTracing' in: 98 // https://perfetto.dev/docs/design-docs/life-of-a-tracing-session 99 stop(): void { 100 if (!this.isPortConnected) return; 101 this.chromePort.postMessage({method: 'DisableTracing'}); 102 } 103 104 getCategories(): Promise<string[]> { 105 if (!this.isPortConnected) { 106 throw new RecordingError( 107 'Attempting to get categories from a ' + 108 'disconnected tracing session.'); 109 } 110 if (this.pendingGetCategoriesMessage) { 111 return this.pendingGetCategoriesMessage; 112 } 113 114 this.chromePort.postMessage({method: 'GetCategories'}); 115 return this.pendingGetCategoriesMessage = defer<string[]>(); 116 } 117 118 async getTraceBufferUsage(): Promise<number> { 119 if (!this.isPortConnected) return 0; 120 const bufferStats = await this.getBufferStats(); 121 let percentageUsed = -1; 122 for (const buffer of bufferStats) { 123 const used = assertExists(buffer.bytesWritten); 124 const total = assertExists(buffer.bufferSize); 125 if (total >= 0) { 126 percentageUsed = Math.max(percentageUsed, used / total); 127 } 128 } 129 130 if (percentageUsed === -1) { 131 throw new RecordingError(BUFFER_USAGE_INCORRECT_FORMAT); 132 } 133 return percentageUsed; 134 } 135 136 initConnection(): void { 137 this.chromePort.onMessage.addListener((message: ChromeExtensionMessage) => { 138 this.handleExtensionMessage(message); 139 }); 140 } 141 142 private getBufferStats(): Promise<IBufferStats[]> { 143 this.chromePort.postMessage({method: 'GetTraceStats'}); 144 145 const statsMessage = defer<IBufferStats[]>(); 146 this.pendingStatsMessages.push(statsMessage); 147 return statsMessage; 148 } 149 150 private terminateConnection(): void { 151 this.chromePort.postMessage({method: 'FreeBuffers'}); 152 this.clearState(); 153 } 154 155 private clearState() { 156 this.chromePort.disconnect(); 157 this.isPortConnected = false; 158 for (const statsMessage of this.pendingStatsMessages) { 159 statsMessage.reject(new RecordingError(BUFFER_USAGE_NOT_ACCESSIBLE)); 160 } 161 this.pendingStatsMessages = []; 162 this.pendingGetCategoriesMessage = undefined; 163 } 164 165 private handleExtensionMessage(message: ChromeExtensionMessage) { 166 if (isChromeExtensionError(message)) { 167 this.terminateConnection(); 168 this.tracingSessionListener.onError(message.error); 169 } else if (isChromeExtensionStatus(message)) { 170 this.tracingSessionListener.onStatus(message.status); 171 } else if (isReadBuffersResponse(message)) { 172 if (!message.slices) { 173 return; 174 } 175 for (const messageSlice of message.slices) { 176 // The extension sends the binary data as a string. 177 // see http://shortn/_oPmO2GT6Vb 178 if (typeof messageSlice.data !== 'string') { 179 throw new RecordingError(MALFORMED_EXTENSION_MESSAGE); 180 } 181 const decodedSlice = { 182 data: binaryDecode(messageSlice.data), 183 }; 184 this.partialPacket.push(decodedSlice); 185 if (messageSlice.lastSliceForPacket) { 186 let bufferSize = 0; 187 for (const slice of this.partialPacket) { 188 bufferSize += slice.data!.length; 189 } 190 191 const completeTrace = new Uint8Array(bufferSize); 192 let written = 0; 193 for (const slice of this.partialPacket) { 194 const data = slice.data!; 195 completeTrace.set(data, written); 196 written += data.length; 197 } 198 // The trace already comes encoded as a proto. 199 this.tracingSessionListener.onTraceData(completeTrace); 200 this.terminateConnection(); 201 } 202 } 203 } else if (isGetCategoriesResponse(message)) { 204 assertExists(this.pendingGetCategoriesMessage) 205 .resolve(message.categories); 206 this.pendingGetCategoriesMessage = undefined; 207 } else if (isEnableTracingResponse(message)) { 208 // Once the service notifies us that a tracing session is enabled, 209 // we can start streaming the response using 'ReadBuffers'. 210 this.chromePort.postMessage({method: 'ReadBuffers'}); 211 } else if (isGetTraceStatsResponse(message)) { 212 const maybePendingStatsMessage = this.pendingStatsMessages.shift(); 213 if (maybePendingStatsMessage) { 214 maybePendingStatsMessage.resolve( 215 message?.traceStats?.bufferStats || []); 216 } 217 } else if (isFreeBuffersResponse(message)) { 218 // No action required. If we successfully read a whole trace, 219 // we close the connection. Alternatively, if the tracing finishes 220 // with an exception or if the user cancels it, we also close the 221 // connection. 222 } else { 223 assertTrue(isDisableTracingResponse(message)); 224 // No action required. Same reasoning as for FreeBuffers. 225 } 226 } 227} 228