blob: 9d1bf845302faa6f484174b65dfca98252f9baae [file] [log] [blame]
// Copyright (C) 2018 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 {
AndroidLogConfig,
AndroidLogId,
AndroidPowerConfig,
BufferConfig,
DataSourceConfig,
FtraceConfig,
ProcessStatsConfig,
SysStatsConfig,
TraceConfig
} from '../common/protos';
import {MeminfoCounters, VmstatCounters} from '../common/protos';
import {RecordConfig} from '../common/state';
import {Controller} from './controller';
import {App} from './globals';
export function uint8ArrayToBase64(buffer: Uint8Array): string {
return btoa(String.fromCharCode.apply(null, Array.from(buffer)));
}
export function genConfigProto(uiCfg: RecordConfig): Uint8Array {
const protoCfg = new TraceConfig();
protoCfg.durationMs = uiCfg.durationMs;
// Auxiliary buffer for slow-rate events.
// Set to 1/8th of the main buffer size, with reasonable limits.
let slowBufSizeKb = uiCfg.bufferSizeMb * (1024 / 8);
slowBufSizeKb = Math.min(slowBufSizeKb, 2 * 1024);
slowBufSizeKb = Math.max(slowBufSizeKb, 256);
// Main buffer for ftrace and other high-freq events.
const fastBufSizeKb = uiCfg.bufferSizeMb * 1024 - slowBufSizeKb;
protoCfg.buffers.push(new BufferConfig());
protoCfg.buffers.push(new BufferConfig());
protoCfg.buffers[1].sizeKb = slowBufSizeKb;
protoCfg.buffers[0].sizeKb = fastBufSizeKb;
if (uiCfg.mode === 'STOP_WHEN_FULL') {
protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.DISCARD;
protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.DISCARD;
} else {
protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
protoCfg.flushPeriodMs = 30000;
if (uiCfg.mode === 'LONG_TRACE') {
protoCfg.writeIntoFile = true;
protoCfg.fileWritePeriodMs = uiCfg.fileWritePeriodMs;
protoCfg.maxFileSizeBytes = uiCfg.maxFileSizeMb * 1e6;
}
}
const ftraceEvents = new Set<string>(uiCfg.ftrace ? uiCfg.ftraceEvents : []);
const atraceCats = new Set<string>(uiCfg.atrace ? uiCfg.atraceCats : []);
const atraceApps = new Set<string>();
let procThreadAssociationPolling = false;
let procThreadAssociationFtrace = false;
let trackInitialOomScore = false;
if (uiCfg.cpuSched || uiCfg.cpuLatency) {
procThreadAssociationPolling = true;
procThreadAssociationFtrace = true;
ftraceEvents.add('sched/sched_switch');
ftraceEvents.add('power/suspend_resume');
if (uiCfg.cpuLatency) {
ftraceEvents.add('sched/sched_wakeup');
ftraceEvents.add('sched/sched_wakeup_new');
ftraceEvents.add('power/suspend_resume');
}
}
if (uiCfg.cpuFreq) {
ftraceEvents.add('power/cpu_frequency');
ftraceEvents.add('power/cpu_idle');
ftraceEvents.add('power/suspend_resume');
}
if (procThreadAssociationFtrace) {
ftraceEvents.add('sched/sched_process_exit');
ftraceEvents.add('sched/sched_process_free');
ftraceEvents.add('task/task_newtask');
ftraceEvents.add('task/task_rename');
}
if (uiCfg.batteryDrain) {
const ds = new TraceConfig.DataSource();
ds.config = new DataSourceConfig();
ds.config.name = 'android.power';
ds.config.androidPowerConfig = new AndroidPowerConfig();
ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs;
ds.config.androidPowerConfig.batteryCounters = [
AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT,
AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
];
ds.config.androidPowerConfig.collectPowerRails = true;
protoCfg.dataSources.push(ds);
}
if (uiCfg.boardSensors) {
ftraceEvents.add('regulator/regulator_set_voltage');
ftraceEvents.add('regulator/regulator_set_voltage_complete');
ftraceEvents.add('power/clock_enable');
ftraceEvents.add('power/clock_disable');
ftraceEvents.add('power/clock_set_rate');
ftraceEvents.add('power/suspend_resume');
}
let sysStatsCfg: SysStatsConfig|undefined = undefined;
if (uiCfg.cpuCoarse) {
if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
sysStatsCfg.statPeriodMs = uiCfg.cpuCoarsePollMs;
sysStatsCfg.statCounters = [
SysStatsConfig.StatCounters.STAT_CPU_TIMES,
SysStatsConfig.StatCounters.STAT_FORK_COUNT,
];
}
if (uiCfg.memHiFreq) {
procThreadAssociationPolling = true;
procThreadAssociationFtrace = true;
ftraceEvents.add('kmem/rss_stat');
ftraceEvents.add('kmem/mm_event');
ftraceEvents.add('kmem/ion_heap_grow');
ftraceEvents.add('kmem/ion_heap_shrink');
}
if (uiCfg.meminfo) {
if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
sysStatsCfg.meminfoPeriodMs = uiCfg.meminfoPeriodMs;
sysStatsCfg.meminfoCounters = uiCfg.meminfoCounters.map(name => {
// tslint:disable-next-line no-any
return MeminfoCounters[name as any as number] as any as number;
});
}
if (uiCfg.vmstat) {
if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
sysStatsCfg.vmstatPeriodMs = uiCfg.vmstatPeriodMs;
sysStatsCfg.vmstatCounters = uiCfg.vmstatCounters.map(name => {
// tslint:disable-next-line no-any
return VmstatCounters[name as any as number] as any as number;
});
}
if (uiCfg.memLmk) {
// For in-kernel LMK (roughly older devices until Go and Pixel 3).
ftraceEvents.add('lowmemorykiller/lowmemory_kill');
// For userspace LMKd (newer devices).
// 'lmkd' is not really required because the code in lmkd.c emits events
// with ATRACE_TAG_ALWAYS. We need something just to ensure that the final
// config will enable atrace userspace events.
atraceApps.add('lmkd');
ftraceEvents.add('oom/oom_score_adj_update');
procThreadAssociationPolling = true;
trackInitialOomScore = true;
}
if (uiCfg.procStats || procThreadAssociationPolling || trackInitialOomScore) {
const ds = new TraceConfig.DataSource();
ds.config = new DataSourceConfig();
ds.config.targetBuffer = 1; // Aux
ds.config.name = 'linux.process_stats';
ds.config.processStatsConfig = new ProcessStatsConfig();
if (uiCfg.procStats) {
ds.config.processStatsConfig.procStatsPollMs = uiCfg.procStatsPeriodMs;
}
if (procThreadAssociationPolling || trackInitialOomScore) {
ds.config.processStatsConfig.scanAllProcessesOnStart = true;
}
protoCfg.dataSources.push(ds);
}
if (uiCfg.androidLogs) {
const ds = new TraceConfig.DataSource();
ds.config = new DataSourceConfig();
ds.config.name = 'android.log';
ds.config.androidLogConfig = new AndroidLogConfig();
ds.config.androidLogConfig.logIds = uiCfg.androidLogBuffers.map(name => {
// tslint:disable-next-line no-any
return AndroidLogId[name as any as number] as any as number;
});
protoCfg.dataSources.push(ds);
}
// Keep these last. The stages above can enrich them.
if (sysStatsCfg !== undefined) {
const ds = new TraceConfig.DataSource();
ds.config = new DataSourceConfig();
ds.config.name = 'linux.sys_stats';
ds.config.sysStatsConfig = sysStatsCfg;
protoCfg.dataSources.push(ds);
}
if (uiCfg.ftrace || uiCfg.atraceApps.length > 0 || ftraceEvents.size > 0 ||
atraceCats.size > 0 || atraceApps.size > 0) {
const ds = new TraceConfig.DataSource();
ds.config = new DataSourceConfig();
ds.config.name = 'linux.ftrace';
ds.config.ftraceConfig = new FtraceConfig();
// Override the advanced ftrace parameters only if the user has ticked the
// "Advanced ftrace config" tab.
if (uiCfg.ftrace) {
ds.config.ftraceConfig.bufferSizeKb = uiCfg.ftraceBufferSizeKb;
ds.config.ftraceConfig.drainPeriodMs = uiCfg.ftraceDrainPeriodMs;
for (const line of uiCfg.ftraceExtraEvents.split('\n')) {
if (line.trim().length > 0) ftraceEvents.add(line.trim());
}
}
for (const line of uiCfg.atraceApps.split('\n')) {
if (line.trim().length > 0) atraceApps.add(line.trim());
}
if (atraceCats.size > 0 || atraceApps.size > 0) {
ftraceEvents.add('ftrace/print');
}
ds.config.ftraceConfig.ftraceEvents = Array.from(ftraceEvents);
ds.config.ftraceConfig.atraceCategories = Array.from(atraceCats);
ds.config.ftraceConfig.atraceApps = Array.from(atraceApps);
protoCfg.dataSources.push(ds);
}
const buffer = TraceConfig.encode(protoCfg).finish();
return buffer;
}
export function toPbtxt(configBuffer: Uint8Array): string {
const msg = TraceConfig.decode(configBuffer);
const json = msg.toJSON();
function snakeCase(s: string): string {
return s.replace(/[A-Z]/g, c => '_' + c.toLowerCase());
}
// With the ahead of time compiled protos we can't seem to tell which
// fields are enums.
function looksLikeEnum(value: string): boolean {
return value.startsWith('MEMINFO_') || value.startsWith('VMSTAT_') ||
value.startsWith('STAT_') || value.startsWith('LID_') ||
value.startsWith('BATTERY_COUNTER_') || value === 'DISCARD' ||
value === 'RING_BUFFER';
}
function* message(msg: {}, indent: number): IterableIterator<string> {
for (const [key, value] of Object.entries(msg)) {
const isRepeated = Array.isArray(value);
const isNested = typeof value === 'object' && !isRepeated;
for (const entry of (isRepeated ? value as Array<{}> : [value])) {
yield ' '.repeat(indent) + `${snakeCase(key)}${isNested ? '' : ':'} `;
if (typeof entry === 'string') {
yield looksLikeEnum(entry) ? entry : `"${entry}"`;
} else if (typeof entry === 'number') {
yield entry.toString();
} else if (typeof entry === 'boolean') {
yield entry.toString();
} else {
yield '{\n';
yield* message(entry, indent + 4);
yield ' '.repeat(indent) + '}';
}
yield '\n';
}
}
}
return [...message(json, 0)].join('');
}
export class RecordController extends Controller<'main'> {
private app: App;
private config: RecordConfig|null = null;
constructor(args: {app: App}) {
super('main');
this.app = args.app;
}
run() {
if (this.app.state.recordConfig === this.config) return;
this.config = this.app.state.recordConfig;
const configProto = genConfigProto(this.config);
const configProtoText = toPbtxt(configProto);
const commandline = `
echo '${uint8ArrayToBase64(configProto)}' |
base64 --decode |
adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace" &&
adb pull /data/misc/perfetto-traces/trace /tmp/trace
`;
// TODO(hjd): This should not be TrackData after we unify the stores.
this.app.publish('TrackData', {
id: 'config',
data: {
commandline,
pbtxt: configProtoText,
}
});
}
}