| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {_TextDecoder} from 'custom_utils'; |
| |
| import {base64Encode} from '../base/string_utils'; |
| import {extractTraceConfig} from '../core/trace_config_utils'; |
| |
| import {AdbBaseConsumerPort, AdbConnectionState} from './adb_base_controller'; |
| import {Adb, AdbStream} from './adb_interfaces'; |
| import {ReadBuffersResponse} from './consumer_port_types'; |
| import {Consumer} from './record_controller_interfaces'; |
| |
| enum AdbShellState { |
| READY, |
| RECORDING, |
| FETCHING, |
| } |
| const DEFAULT_DESTINATION_FILE = '/data/misc/perfetto-traces/trace-by-ui'; |
| const textDecoder = new _TextDecoder(); |
| |
| export class AdbConsumerPort extends AdbBaseConsumerPort { |
| traceDestFile = DEFAULT_DESTINATION_FILE; |
| shellState: AdbShellState = AdbShellState.READY; |
| private recordShell?: AdbStream; |
| |
| constructor(adb: Adb, consumer: Consumer) { |
| super(adb, consumer); |
| this.adb = adb; |
| } |
| |
| async invoke(method: string, params: Uint8Array) { |
| // ADB connection & authentication is handled by the superclass. |
| console.assert(this.state === AdbConnectionState.CONNECTED); |
| |
| switch (method) { |
| case 'EnableTracing': |
| this.enableTracing(params); |
| break; |
| case 'ReadBuffers': |
| this.readBuffers(); |
| break; |
| case 'DisableTracing': |
| this.disableTracing(); |
| break; |
| case 'FreeBuffers': |
| this.freeBuffers(); |
| break; |
| case 'GetTraceStats': |
| break; |
| default: |
| this.sendErrorMessage(`Method not recognized: ${method}`); |
| break; |
| } |
| } |
| |
| async enableTracing(enableTracingProto: Uint8Array) { |
| try { |
| const traceConfigProto = extractTraceConfig(enableTracingProto); |
| if (!traceConfigProto) { |
| this.sendErrorMessage('Invalid config.'); |
| return; |
| } |
| |
| await this.startRecording(traceConfigProto); |
| this.setDurationStatus(enableTracingProto); |
| } catch (e) { |
| this.sendErrorMessage(e.message); |
| } |
| } |
| |
| async startRecording(configProto: Uint8Array) { |
| this.shellState = AdbShellState.RECORDING; |
| const recordCommand = this.generateStartTracingCommand(configProto); |
| this.recordShell = await this.adb.shell(recordCommand); |
| const output: string[] = []; |
| this.recordShell.onData = (raw) => output.push(textDecoder.decode(raw)); |
| this.recordShell.onClose = () => { |
| const response = output.join(); |
| if (!this.tracingEndedSuccessfully(response)) { |
| this.sendErrorMessage(response); |
| this.shellState = AdbShellState.READY; |
| return; |
| } |
| this.sendStatus('Recording ended successfully. Fetching the trace..'); |
| this.sendMessage({type: 'EnableTracingResponse'}); |
| this.recordShell = undefined; |
| }; |
| } |
| |
| tracingEndedSuccessfully(response: string): boolean { |
| return !response.includes(' 0 ms') && response.includes('Wrote '); |
| } |
| |
| async readBuffers() { |
| console.assert(this.shellState === AdbShellState.RECORDING); |
| this.shellState = AdbShellState.FETCHING; |
| |
| const readTraceShell = await this.adb.shell( |
| this.generateReadTraceCommand(), |
| ); |
| readTraceShell.onData = (raw) => |
| this.sendMessage(this.generateChunkReadResponse(raw)); |
| |
| readTraceShell.onClose = () => { |
| this.sendMessage( |
| this.generateChunkReadResponse(new Uint8Array(), /* last */ true), |
| ); |
| }; |
| } |
| |
| async getPidFromShellAsString() { |
| const pidStr = await this.adb.shellOutputAsString( |
| `ps -u shell | grep perfetto`, |
| ); |
| // We used to use awk '{print $2}' but older phones/Go phones don't have |
| // awk installed. Instead we implement similar functionality here. |
| const awk = pidStr.split(' ').filter((str) => str !== ''); |
| if (awk.length < 1) { |
| throw Error(`Unabled to find perfetto pid in string "${pidStr}"`); |
| } |
| return awk[1]; |
| } |
| |
| async disableTracing() { |
| if (!this.recordShell) return; |
| try { |
| // We are not using 'pidof perfetto' so that we can use more filters. 'ps |
| // -u shell' is meant to catch processes started from shell, so if there |
| // are other ongoing tracing sessions started by others, we are not |
| // killing them. |
| const pid = await this.getPidFromShellAsString(); |
| |
| if (pid.length === 0 || isNaN(Number(pid))) { |
| throw Error(`Perfetto pid not found. Impossible to stop/cancel the |
| recording. Command output: ${pid}`); |
| } |
| // Perfetto stops and finalizes the tracing session on SIGINT. |
| const killOutput = await this.adb.shellOutputAsString( |
| `kill -SIGINT ${pid}`, |
| ); |
| |
| if (killOutput.length !== 0) { |
| throw Error(`Unable to kill perfetto: ${killOutput}`); |
| } |
| } catch (e) { |
| this.sendErrorMessage(e.message); |
| } |
| } |
| |
| freeBuffers() { |
| this.shellState = AdbShellState.READY; |
| if (this.recordShell) { |
| this.recordShell.close(); |
| this.recordShell = undefined; |
| } |
| } |
| |
| generateChunkReadResponse( |
| data: Uint8Array, |
| last = false, |
| ): ReadBuffersResponse { |
| return { |
| type: 'ReadBuffersResponse', |
| slices: [{data, lastSliceForPacket: last}], |
| }; |
| } |
| |
| generateReadTraceCommand(): string { |
| // We attempt to delete the trace file after tracing. On a non-root shell, |
| // this will fail (due to selinux denial), but perfetto cmd will be able to |
| // override the file later. However, on a root shell, we need to clean up |
| // the file since perfetto cmd might otherwise fail to override it in a |
| // future session. |
| return `gzip -c ${this.traceDestFile} && rm -f ${this.traceDestFile}`; |
| } |
| |
| generateStartTracingCommand(tracingConfig: Uint8Array) { |
| const configBase64 = base64Encode(tracingConfig); |
| const perfettoCmd = `perfetto -c - -o ${this.traceDestFile}`; |
| return `echo '${configBase64}' | base64 -d | ${perfettoCmd}`; |
| } |
| } |