blob: 4c7b6a26bd8ca5ab1462625900d389b5d9a382d0 [file] [log] [blame]
// 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 '../base/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}`;
}
}