1// Copyright (C) 2018 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 {
16  AndroidLogConfig,
17  AndroidLogId,
18  AndroidPowerConfig,
19  BufferConfig,
20  DataSourceConfig,
21  FtraceConfig,
22  ProcessStatsConfig,
23  SysStatsConfig,
24  TraceConfig
25} from '../common/protos';
26import {MeminfoCounters, VmstatCounters} from '../common/protos';
27import {RecordConfig} from '../common/state';
28
29import {Controller} from './controller';
30import {App} from './globals';
31
32export function uint8ArrayToBase64(buffer: Uint8Array): string {
33  return btoa(String.fromCharCode.apply(null, Array.from(buffer)));
34}
35
36export function genConfigProto(uiCfg: RecordConfig): Uint8Array {
37  const protoCfg = new TraceConfig();
38  protoCfg.durationMs = uiCfg.durationMs;
39
40  // Auxiliary buffer for slow-rate events.
41  // Set to 1/8th of the main buffer size, with reasonable limits.
42  let slowBufSizeKb = uiCfg.bufferSizeMb * (1024 / 8);
43  slowBufSizeKb = Math.min(slowBufSizeKb, 2 * 1024);
44  slowBufSizeKb = Math.max(slowBufSizeKb, 256);
45
46  // Main buffer for ftrace and other high-freq events.
47  const fastBufSizeKb = uiCfg.bufferSizeMb * 1024 - slowBufSizeKb;
48
49  protoCfg.buffers.push(new BufferConfig());
50  protoCfg.buffers.push(new BufferConfig());
51  protoCfg.buffers[1].sizeKb = slowBufSizeKb;
52  protoCfg.buffers[0].sizeKb = fastBufSizeKb;
53
54  if (uiCfg.mode === 'STOP_WHEN_FULL') {
55    protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.DISCARD;
56    protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.DISCARD;
57  } else {
58    protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
59    protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
60    protoCfg.flushPeriodMs = 30000;
61    if (uiCfg.mode === 'LONG_TRACE') {
62      protoCfg.writeIntoFile = true;
63      protoCfg.fileWritePeriodMs = uiCfg.fileWritePeriodMs;
64      protoCfg.maxFileSizeBytes = uiCfg.maxFileSizeMb * 1e6;
65    }
66  }
67
68  const ftraceEvents = new Set<string>(uiCfg.ftrace ? uiCfg.ftraceEvents : []);
69  const atraceCats = new Set<string>(uiCfg.atrace ? uiCfg.atraceCats : []);
70  const atraceApps = new Set<string>();
71  let procThreadAssociationPolling = false;
72  let procThreadAssociationFtrace = false;
73  let trackInitialOomScore = false;
74
75  if (uiCfg.cpuSched || uiCfg.cpuLatency) {
76    procThreadAssociationPolling = true;
77    procThreadAssociationFtrace = true;
78    ftraceEvents.add('sched/sched_switch');
79    ftraceEvents.add('power/suspend_resume');
80    if (uiCfg.cpuLatency) {
81      ftraceEvents.add('sched/sched_wakeup');
82      ftraceEvents.add('sched/sched_wakeup_new');
83      ftraceEvents.add('power/suspend_resume');
84    }
85  }
86
87  if (uiCfg.cpuFreq) {
88    ftraceEvents.add('power/cpu_frequency');
89    ftraceEvents.add('power/cpu_idle');
90    ftraceEvents.add('power/suspend_resume');
91  }
92
93  if (procThreadAssociationFtrace) {
94    ftraceEvents.add('sched/sched_process_exit');
95    ftraceEvents.add('sched/sched_process_free');
96    ftraceEvents.add('task/task_newtask');
97    ftraceEvents.add('task/task_rename');
98  }
99
100  if (uiCfg.batteryDrain) {
101    const ds = new TraceConfig.DataSource();
102    ds.config = new DataSourceConfig();
103    ds.config.name = 'android.power';
104    ds.config.androidPowerConfig = new AndroidPowerConfig();
105    ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs;
106    ds.config.androidPowerConfig.batteryCounters = [
107      AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT,
108      AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
109      AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
110    ];
111    ds.config.androidPowerConfig.collectPowerRails = true;
112    protoCfg.dataSources.push(ds);
113  }
114
115  if (uiCfg.boardSensors) {
116    ftraceEvents.add('regulator/regulator_set_voltage');
117    ftraceEvents.add('regulator/regulator_set_voltage_complete');
118    ftraceEvents.add('power/clock_enable');
119    ftraceEvents.add('power/clock_disable');
120    ftraceEvents.add('power/clock_set_rate');
121    ftraceEvents.add('power/suspend_resume');
122  }
123
124  let sysStatsCfg: SysStatsConfig|undefined = undefined;
125
126  if (uiCfg.cpuCoarse) {
127    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
128    sysStatsCfg.statPeriodMs = uiCfg.cpuCoarsePollMs;
129    sysStatsCfg.statCounters = [
130      SysStatsConfig.StatCounters.STAT_CPU_TIMES,
131      SysStatsConfig.StatCounters.STAT_FORK_COUNT,
132    ];
133  }
134
135  if (uiCfg.memHiFreq) {
136    procThreadAssociationPolling = true;
137    procThreadAssociationFtrace = true;
138    ftraceEvents.add('kmem/rss_stat');
139    ftraceEvents.add('kmem/mm_event');
140    ftraceEvents.add('kmem/ion_heap_grow');
141    ftraceEvents.add('kmem/ion_heap_shrink');
142  }
143
144  if (uiCfg.meminfo) {
145    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
146    sysStatsCfg.meminfoPeriodMs = uiCfg.meminfoPeriodMs;
147    sysStatsCfg.meminfoCounters = uiCfg.meminfoCounters.map(name => {
148      // tslint:disable-next-line no-any
149      return MeminfoCounters[name as any as number] as any as number;
150    });
151  }
152
153  if (uiCfg.vmstat) {
154    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
155    sysStatsCfg.vmstatPeriodMs = uiCfg.vmstatPeriodMs;
156    sysStatsCfg.vmstatCounters = uiCfg.vmstatCounters.map(name => {
157      // tslint:disable-next-line no-any
158      return VmstatCounters[name as any as number] as any as number;
159    });
160  }
161
162  if (uiCfg.memLmk) {
163    // For in-kernel LMK (roughly older devices until Go and Pixel 3).
164    ftraceEvents.add('lowmemorykiller/lowmemory_kill');
165
166    // For userspace LMKd (newer devices).
167    // 'lmkd' is not really required because the code in lmkd.c emits events
168    // with ATRACE_TAG_ALWAYS. We need something just to ensure that the final
169    // config will enable atrace userspace events.
170    atraceApps.add('lmkd');
171
172    ftraceEvents.add('oom/oom_score_adj_update');
173    procThreadAssociationPolling = true;
174    trackInitialOomScore = true;
175  }
176
177  if (uiCfg.procStats || procThreadAssociationPolling || trackInitialOomScore) {
178    const ds = new TraceConfig.DataSource();
179    ds.config = new DataSourceConfig();
180    ds.config.targetBuffer = 1;  // Aux
181    ds.config.name = 'linux.process_stats';
182    ds.config.processStatsConfig = new ProcessStatsConfig();
183    if (uiCfg.procStats) {
184      ds.config.processStatsConfig.procStatsPollMs = uiCfg.procStatsPeriodMs;
185    }
186    if (procThreadAssociationPolling || trackInitialOomScore) {
187      ds.config.processStatsConfig.scanAllProcessesOnStart = true;
188    }
189    protoCfg.dataSources.push(ds);
190  }
191
192  if (uiCfg.androidLogs) {
193    const ds = new TraceConfig.DataSource();
194    ds.config = new DataSourceConfig();
195    ds.config.name = 'android.log';
196    ds.config.androidLogConfig = new AndroidLogConfig();
197    ds.config.androidLogConfig.logIds = uiCfg.androidLogBuffers.map(name => {
198      // tslint:disable-next-line no-any
199      return AndroidLogId[name as any as number] as any as number;
200    });
201
202    protoCfg.dataSources.push(ds);
203  }
204
205  // Keep these last. The stages above can enrich them.
206
207  if (sysStatsCfg !== undefined) {
208    const ds = new TraceConfig.DataSource();
209    ds.config = new DataSourceConfig();
210    ds.config.name = 'linux.sys_stats';
211    ds.config.sysStatsConfig = sysStatsCfg;
212    protoCfg.dataSources.push(ds);
213  }
214
215  if (uiCfg.ftrace || uiCfg.atraceApps.length > 0 || ftraceEvents.size > 0 ||
216      atraceCats.size > 0 || atraceApps.size > 0) {
217    const ds = new TraceConfig.DataSource();
218    ds.config = new DataSourceConfig();
219    ds.config.name = 'linux.ftrace';
220    ds.config.ftraceConfig = new FtraceConfig();
221    // Override the advanced ftrace parameters only if the user has ticked the
222    // "Advanced ftrace config" tab.
223    if (uiCfg.ftrace) {
224      ds.config.ftraceConfig.bufferSizeKb = uiCfg.ftraceBufferSizeKb;
225      ds.config.ftraceConfig.drainPeriodMs = uiCfg.ftraceDrainPeriodMs;
226      for (const line of uiCfg.ftraceExtraEvents.split('\n')) {
227        if (line.trim().length > 0) ftraceEvents.add(line.trim());
228      }
229    }
230    for (const line of uiCfg.atraceApps.split('\n')) {
231      if (line.trim().length > 0) atraceApps.add(line.trim());
232    }
233
234    if (atraceCats.size > 0 || atraceApps.size > 0) {
235      ftraceEvents.add('ftrace/print');
236    }
237
238    ds.config.ftraceConfig.ftraceEvents = Array.from(ftraceEvents);
239    ds.config.ftraceConfig.atraceCategories = Array.from(atraceCats);
240    ds.config.ftraceConfig.atraceApps = Array.from(atraceApps);
241    protoCfg.dataSources.push(ds);
242  }
243
244  const buffer = TraceConfig.encode(protoCfg).finish();
245  return buffer;
246}
247
248export function toPbtxt(configBuffer: Uint8Array): string {
249  const msg = TraceConfig.decode(configBuffer);
250  const json = msg.toJSON();
251  function snakeCase(s: string): string {
252    return s.replace(/[A-Z]/g, c => '_' + c.toLowerCase());
253  }
254  // With the ahead of time compiled protos we can't seem to tell which
255  // fields are enums.
256  function looksLikeEnum(value: string): boolean {
257    return value.startsWith('MEMINFO_') || value.startsWith('VMSTAT_') ||
258        value.startsWith('STAT_') || value.startsWith('LID_') ||
259        value.startsWith('BATTERY_COUNTER_') || value === 'DISCARD' ||
260        value === 'RING_BUFFER';
261  }
262  function* message(msg: {}, indent: number): IterableIterator<string> {
263    for (const [key, value] of Object.entries(msg)) {
264      const isRepeated = Array.isArray(value);
265      const isNested = typeof value === 'object' && !isRepeated;
266      for (const entry of (isRepeated ? value as Array<{}> : [value])) {
267        yield ' '.repeat(indent) + `${snakeCase(key)}${isNested ? '' : ':'} `;
268        if (typeof entry === 'string') {
269          yield looksLikeEnum(entry) ? entry : `"${entry}"`;
270        } else if (typeof entry === 'number') {
271          yield entry.toString();
272        } else if (typeof entry === 'boolean') {
273          yield entry.toString();
274        } else {
275          yield '{\n';
276          yield* message(entry, indent + 4);
277          yield ' '.repeat(indent) + '}';
278        }
279        yield '\n';
280      }
281    }
282  }
283  return [...message(json, 0)].join('');
284}
285
286export class RecordController extends Controller<'main'> {
287  private app: App;
288  private config: RecordConfig|null = null;
289
290  constructor(args: {app: App}) {
291    super('main');
292    this.app = args.app;
293  }
294
295  run() {
296    if (this.app.state.recordConfig === this.config) return;
297    this.config = this.app.state.recordConfig;
298    const configProto = genConfigProto(this.config);
299    const configProtoText = toPbtxt(configProto);
300    const commandline = `
301      echo '${uint8ArrayToBase64(configProto)}' |
302      base64 --decode |
303      adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace" &&
304      adb pull /data/misc/perfetto-traces/trace /tmp/trace
305    `;
306    // TODO(hjd): This should not be TrackData after we unify the stores.
307    this.app.publish('TrackData', {
308      id: 'config',
309      data: {
310        commandline,
311        pbtxt: configProtoText,
312      }
313    });
314  }
315}
316