|  | // 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, | 
|  | } | 
|  | }); | 
|  | } | 
|  | } |