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 {Message, Method, rpc, RPCImplCallback} from 'protobufjs';
16
17import {
18  base64Encode,
19} from '../base/string_utils';
20import {Actions} from '../common/actions';
21import {TRACE_SUFFIX} from '../common/constants';
22import {
23  AndroidLogConfig,
24  AndroidLogId,
25  AndroidPowerConfig,
26  BufferConfig,
27  ChromeConfig,
28  ConsumerPort,
29  DataSourceConfig,
30  FtraceConfig,
31  HeapprofdConfig,
32  JavaContinuousDumpConfig,
33  JavaHprofConfig,
34  NativeContinuousDumpConfig,
35  ProcessStatsConfig,
36  SysStatsConfig,
37  TraceConfig,
38} from '../common/protos';
39import {MeminfoCounters, VmstatCounters} from '../common/protos';
40import {
41  AdbRecordingTarget,
42  isAdbTarget,
43  isAndroidP,
44  isChromeTarget,
45  isCrOSTarget,
46  RecordConfig,
47  RecordingTarget
48} from '../common/state';
49
50import {AdbOverWebUsb} from './adb';
51import {AdbConsumerPort} from './adb_shell_controller';
52import {AdbSocketConsumerPort} from './adb_socket_controller';
53import {ChromeExtensionConsumerPort} from './chrome_proxy_record_controller';
54import {
55  ConsumerPortResponse,
56  GetTraceStatsResponse,
57  isDisableTracingResponse,
58  isEnableTracingResponse,
59  isFreeBuffersResponse,
60  isGetTraceStatsResponse,
61  isReadBuffersResponse,
62} from './consumer_port_types';
63import {Controller} from './controller';
64import {App, globals} from './globals';
65import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
66
67type RPCImplMethod = (Method|rpc.ServiceMethod<Message<{}>, Message<{}>>);
68
69export function genConfigProto(
70    uiCfg: RecordConfig, target: RecordingTarget): Uint8Array {
71  return TraceConfig.encode(genConfig(uiCfg, target)).finish();
72}
73
74export function genConfig(
75    uiCfg: RecordConfig, target: RecordingTarget): TraceConfig {
76  const protoCfg = new TraceConfig();
77  protoCfg.durationMs = uiCfg.durationMs;
78
79  // Auxiliary buffer for slow-rate events.
80  // Set to 1/8th of the main buffer size, with reasonable limits.
81  let slowBufSizeKb = uiCfg.bufferSizeMb * (1024 / 8);
82  slowBufSizeKb = Math.min(slowBufSizeKb, 2 * 1024);
83  slowBufSizeKb = Math.max(slowBufSizeKb, 256);
84
85  // Main buffer for ftrace and other high-freq events.
86  const fastBufSizeKb = uiCfg.bufferSizeMb * 1024 - slowBufSizeKb;
87
88  protoCfg.buffers.push(new BufferConfig());
89  protoCfg.buffers.push(new BufferConfig());
90  protoCfg.buffers[1].sizeKb = slowBufSizeKb;
91  protoCfg.buffers[0].sizeKb = fastBufSizeKb;
92
93  if (uiCfg.mode === 'STOP_WHEN_FULL') {
94    protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.DISCARD;
95    protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.DISCARD;
96  } else {
97    protoCfg.buffers[0].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
98    protoCfg.buffers[1].fillPolicy = BufferConfig.FillPolicy.RING_BUFFER;
99    protoCfg.flushPeriodMs = 30000;
100    if (uiCfg.mode === 'LONG_TRACE') {
101      protoCfg.writeIntoFile = true;
102      protoCfg.fileWritePeriodMs = uiCfg.fileWritePeriodMs;
103      protoCfg.maxFileSizeBytes = uiCfg.maxFileSizeMb * 1e6;
104    }
105
106    // Clear incremental state every 5 seconds when tracing into a ring buffer.
107    const incStateConfig = new TraceConfig.IncrementalStateConfig();
108    incStateConfig.clearPeriodMs = 5000;
109    protoCfg.incrementalStateConfig = incStateConfig;
110  }
111
112  const ftraceEvents = new Set<string>(uiCfg.ftrace ? uiCfg.ftraceEvents : []);
113  const atraceCats = new Set<string>(uiCfg.atrace ? uiCfg.atraceCats : []);
114  const atraceApps = new Set<string>();
115  const chromeCategories = new Set<string>();
116  uiCfg.chromeCategoriesSelected.forEach(it => chromeCategories.add(it));
117
118  let procThreadAssociationPolling = false;
119  let procThreadAssociationFtrace = false;
120  let trackInitialOomScore = false;
121
122  if (uiCfg.cpuSched) {
123    procThreadAssociationPolling = true;
124    procThreadAssociationFtrace = true;
125    ftraceEvents.add('sched/sched_switch');
126    ftraceEvents.add('power/suspend_resume');
127    ftraceEvents.add('sched/sched_wakeup');
128    ftraceEvents.add('sched/sched_wakeup_new');
129    ftraceEvents.add('sched/sched_waking');
130    ftraceEvents.add('power/suspend_resume');
131  }
132
133  if (uiCfg.cpuFreq) {
134    ftraceEvents.add('power/cpu_frequency');
135    ftraceEvents.add('power/cpu_idle');
136    ftraceEvents.add('power/suspend_resume');
137  }
138
139  if (uiCfg.gpuFreq) {
140    ftraceEvents.add('power/gpu_frequency');
141  }
142
143  if (uiCfg.gpuMemTotal) {
144    ftraceEvents.add('gpu_mem/gpu_mem_total');
145
146    if (!isChromeTarget(target) || isCrOSTarget(target)) {
147      const ds = new TraceConfig.DataSource();
148      ds.config = new DataSourceConfig();
149      ds.config.name = 'android.gpu.memory';
150      protoCfg.dataSources.push(ds);
151    }
152  }
153
154  if (uiCfg.cpuSyscall) {
155    ftraceEvents.add('raw_syscalls/sys_enter');
156    ftraceEvents.add('raw_syscalls/sys_exit');
157  }
158
159  if (procThreadAssociationFtrace) {
160    ftraceEvents.add('sched/sched_process_exit');
161    ftraceEvents.add('sched/sched_process_free');
162    ftraceEvents.add('task/task_newtask');
163    ftraceEvents.add('task/task_rename');
164  }
165
166  if (uiCfg.batteryDrain) {
167    const ds = new TraceConfig.DataSource();
168    ds.config = new DataSourceConfig();
169    ds.config.name = 'android.power';
170    ds.config.androidPowerConfig = new AndroidPowerConfig();
171    ds.config.androidPowerConfig.batteryPollMs = uiCfg.batteryDrainPollMs;
172    ds.config.androidPowerConfig.batteryCounters = [
173      AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CAPACITY_PERCENT,
174      AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CHARGE,
175      AndroidPowerConfig.BatteryCounters.BATTERY_COUNTER_CURRENT,
176    ];
177    ds.config.androidPowerConfig.collectPowerRails = true;
178    if (!isChromeTarget(target) || isCrOSTarget(target)) {
179      protoCfg.dataSources.push(ds);
180    }
181  }
182
183  if (uiCfg.boardSensors) {
184    ftraceEvents.add('regulator/regulator_set_voltage');
185    ftraceEvents.add('regulator/regulator_set_voltage_complete');
186    ftraceEvents.add('power/clock_enable');
187    ftraceEvents.add('power/clock_disable');
188    ftraceEvents.add('power/clock_set_rate');
189    ftraceEvents.add('power/suspend_resume');
190  }
191
192  let sysStatsCfg: SysStatsConfig|undefined = undefined;
193
194  if (uiCfg.cpuCoarse) {
195    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
196    sysStatsCfg.statPeriodMs = uiCfg.cpuCoarsePollMs;
197    sysStatsCfg.statCounters = [
198      SysStatsConfig.StatCounters.STAT_CPU_TIMES,
199      SysStatsConfig.StatCounters.STAT_FORK_COUNT,
200    ];
201  }
202
203  if (uiCfg.memHiFreq) {
204    procThreadAssociationPolling = true;
205    procThreadAssociationFtrace = true;
206    ftraceEvents.add('mm_event/mm_event_record');
207    ftraceEvents.add('kmem/rss_stat');
208    ftraceEvents.add('ion/ion_stat');
209    ftraceEvents.add('dmabuf_heap/dma_heap_stat');
210    ftraceEvents.add('kmem/ion_heap_grow');
211    ftraceEvents.add('kmem/ion_heap_shrink');
212  }
213
214  if (uiCfg.meminfo) {
215    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
216    sysStatsCfg.meminfoPeriodMs = uiCfg.meminfoPeriodMs;
217    sysStatsCfg.meminfoCounters = uiCfg.meminfoCounters.map(name => {
218      // tslint:disable-next-line no-any
219      return MeminfoCounters[name as any as number] as any as number;
220    });
221  }
222
223  if (uiCfg.vmstat) {
224    if (sysStatsCfg === undefined) sysStatsCfg = new SysStatsConfig();
225    sysStatsCfg.vmstatPeriodMs = uiCfg.vmstatPeriodMs;
226    sysStatsCfg.vmstatCounters = uiCfg.vmstatCounters.map(name => {
227      // tslint:disable-next-line no-any
228      return VmstatCounters[name as any as number] as any as number;
229    });
230  }
231
232  if (uiCfg.memLmk) {
233    // For in-kernel LMK (roughly older devices until Go and Pixel 3).
234    ftraceEvents.add('lowmemorykiller/lowmemory_kill');
235
236    // For userspace LMKd (newer devices).
237    // 'lmkd' is not really required because the code in lmkd.c emits events
238    // with ATRACE_TAG_ALWAYS. We need something just to ensure that the final
239    // config will enable atrace userspace events.
240    atraceApps.add('lmkd');
241
242    ftraceEvents.add('oom/oom_score_adj_update');
243    procThreadAssociationPolling = true;
244    trackInitialOomScore = true;
245  }
246
247  let heapprofd: HeapprofdConfig|undefined = undefined;
248  if (uiCfg.heapProfiling) {
249    // TODO(hjd): Check or inform user if buffer size are too small.
250    const cfg = new HeapprofdConfig();
251    cfg.samplingIntervalBytes = uiCfg.hpSamplingIntervalBytes;
252    if (uiCfg.hpSharedMemoryBuffer >= 8192 &&
253        uiCfg.hpSharedMemoryBuffer % 4096 === 0) {
254      cfg.shmemSizeBytes = uiCfg.hpSharedMemoryBuffer;
255    }
256    for (const value of uiCfg.hpProcesses.split('\n')) {
257      if (value === '') {
258        // Ignore empty lines
259      } else if (isNaN(+value)) {
260        cfg.processCmdline.push(value);
261      } else {
262        cfg.pid.push(+value);
263      }
264    }
265    if (uiCfg.hpContinuousDumpsInterval > 0) {
266      const cdc = cfg.continuousDumpConfig = new NativeContinuousDumpConfig();
267      cdc.dumpIntervalMs = uiCfg.hpContinuousDumpsInterval;
268      if (uiCfg.hpContinuousDumpsPhase > 0) {
269        cdc.dumpPhaseMs = uiCfg.hpContinuousDumpsPhase;
270      }
271    }
272    cfg.blockClient = uiCfg.hpBlockClient;
273    if (uiCfg.hpAllHeaps) {
274      cfg.allHeaps = true;
275    }
276    heapprofd = cfg;
277  }
278
279  let javaHprof: JavaHprofConfig|undefined = undefined;
280  if (uiCfg.javaHeapDump) {
281    const cfg = new JavaHprofConfig();
282    for (const value of uiCfg.jpProcesses.split('\n')) {
283      if (value === '') {
284        // Ignore empty lines
285      } else if (isNaN(+value)) {
286        cfg.processCmdline.push(value);
287      } else {
288        cfg.pid.push(+value);
289      }
290    }
291    if (uiCfg.jpContinuousDumpsInterval > 0) {
292      const cdc = cfg.continuousDumpConfig = new JavaContinuousDumpConfig();
293      cdc.dumpIntervalMs = uiCfg.jpContinuousDumpsInterval;
294      if (uiCfg.hpContinuousDumpsPhase > 0) {
295        cdc.dumpPhaseMs = uiCfg.jpContinuousDumpsPhase;
296      }
297    }
298    javaHprof = cfg;
299  }
300
301  if (uiCfg.procStats || procThreadAssociationPolling || trackInitialOomScore) {
302    const ds = new TraceConfig.DataSource();
303    ds.config = new DataSourceConfig();
304    ds.config.targetBuffer = 1;  // Aux
305    ds.config.name = 'linux.process_stats';
306    ds.config.processStatsConfig = new ProcessStatsConfig();
307    if (uiCfg.procStats) {
308      ds.config.processStatsConfig.procStatsPollMs = uiCfg.procStatsPeriodMs;
309    }
310    if (procThreadAssociationPolling || trackInitialOomScore) {
311      ds.config.processStatsConfig.scanAllProcessesOnStart = true;
312    }
313    if (!isChromeTarget(target) || isCrOSTarget(target)) {
314      protoCfg.dataSources.push(ds);
315    }
316  }
317
318  if (uiCfg.androidLogs) {
319    const ds = new TraceConfig.DataSource();
320    ds.config = new DataSourceConfig();
321    ds.config.name = 'android.log';
322    ds.config.androidLogConfig = new AndroidLogConfig();
323    ds.config.androidLogConfig.logIds = uiCfg.androidLogBuffers.map(name => {
324      // tslint:disable-next-line no-any
325      return AndroidLogId[name as any as number] as any as number;
326    });
327
328    if (!isChromeTarget(target) || isCrOSTarget(target)) {
329      protoCfg.dataSources.push(ds);
330    }
331  }
332
333  if (uiCfg.androidFrameTimeline) {
334    const ds = new TraceConfig.DataSource();
335    ds.config = new DataSourceConfig();
336    ds.config.name = 'android.surfaceflinger.frametimeline';
337    if (!isChromeTarget(target) || isCrOSTarget(target)) {
338      protoCfg.dataSources.push(ds);
339    }
340  }
341
342  if (uiCfg.chromeLogs) {
343    chromeCategories.add('log');
344  }
345
346  if (uiCfg.taskScheduling) {
347    chromeCategories.add('toplevel');
348    chromeCategories.add('sequence_manager');
349    chromeCategories.add('disabled-by-default-toplevel.flow');
350  }
351
352  if (uiCfg.ipcFlows) {
353    chromeCategories.add('toplevel');
354    chromeCategories.add('disabled-by-default-ipc.flow');
355    chromeCategories.add('mojom');
356  }
357
358  if (uiCfg.jsExecution) {
359    chromeCategories.add('toplevel');
360    chromeCategories.add('v8');
361  }
362
363  if (uiCfg.webContentRendering) {
364    chromeCategories.add('toplevel');
365    chromeCategories.add('blink');
366    chromeCategories.add('cc');
367    chromeCategories.add('gpu');
368  }
369
370  if (uiCfg.uiRendering) {
371    chromeCategories.add('toplevel');
372    chromeCategories.add('cc');
373    chromeCategories.add('gpu');
374    chromeCategories.add('viz');
375    chromeCategories.add('ui');
376    chromeCategories.add('views');
377  }
378
379  if (uiCfg.inputEvents) {
380    chromeCategories.add('toplevel');
381    chromeCategories.add('benchmark');
382    chromeCategories.add('evdev');
383    chromeCategories.add('input');
384    chromeCategories.add('disabled-by-default-toplevel.flow');
385  }
386
387  if (uiCfg.navigationAndLoading) {
388    chromeCategories.add('loading');
389    chromeCategories.add('net');
390    chromeCategories.add('netlog');
391    chromeCategories.add('navigation');
392    chromeCategories.add('browser');
393  }
394
395  if (chromeCategories.size !== 0) {
396    let chromeRecordMode = '';
397    if (uiCfg.mode === 'STOP_WHEN_FULL') {
398      chromeRecordMode = 'record-until-full';
399    } else {
400      chromeRecordMode = 'record-continuously';
401    }
402    const configStruct = {
403      record_mode: chromeRecordMode,
404      included_categories: [...chromeCategories.values()],
405      memory_dump_config: {},
406    };
407    if (chromeCategories.has('disabled-by-default-memory-infra')) {
408      configStruct.memory_dump_config = {
409        allowed_dump_modes: ['background', 'light', 'detailed'],
410        triggers: [{
411          min_time_between_dumps_ms: 10000,
412          mode: 'detailed',
413          type: 'periodic_interval',
414        }],
415      };
416    }
417    const traceConfigJson = JSON.stringify(configStruct);
418
419    const traceDs = new TraceConfig.DataSource();
420    traceDs.config = new DataSourceConfig();
421    traceDs.config.name = 'org.chromium.trace_event';
422    traceDs.config.chromeConfig = new ChromeConfig();
423    traceDs.config.chromeConfig.traceConfig = traceConfigJson;
424    protoCfg.dataSources.push(traceDs);
425
426
427    const metadataDs = new TraceConfig.DataSource();
428    metadataDs.config = new DataSourceConfig();
429    metadataDs.config.name = 'org.chromium.trace_metadata';
430    metadataDs.config.chromeConfig = new ChromeConfig();
431    metadataDs.config.chromeConfig.traceConfig = traceConfigJson;
432    protoCfg.dataSources.push(metadataDs);
433  }
434
435  if (uiCfg.screenRecord) {
436    atraceCats.add('gfx');
437  }
438
439  // Keep these last. The stages above can enrich them.
440
441  if (sysStatsCfg !== undefined &&
442      (!isChromeTarget(target) || isCrOSTarget(target))) {
443    const ds = new TraceConfig.DataSource();
444    ds.config = new DataSourceConfig();
445    ds.config.name = 'linux.sys_stats';
446    ds.config.sysStatsConfig = sysStatsCfg;
447    protoCfg.dataSources.push(ds);
448  }
449
450  if (heapprofd !== undefined &&
451      (!isChromeTarget(target) || isCrOSTarget(target))) {
452    const ds = new TraceConfig.DataSource();
453    ds.config = new DataSourceConfig();
454    ds.config.targetBuffer = 0;
455    ds.config.name = 'android.heapprofd';
456    ds.config.heapprofdConfig = heapprofd;
457    protoCfg.dataSources.push(ds);
458  }
459
460  if (javaHprof !== undefined &&
461      (!isChromeTarget(target) || isCrOSTarget(target))) {
462    const ds = new TraceConfig.DataSource();
463    ds.config = new DataSourceConfig();
464    ds.config.targetBuffer = 0;
465    ds.config.name = 'android.java_hprof';
466    ds.config.javaHprofConfig = javaHprof;
467    protoCfg.dataSources.push(ds);
468  }
469
470  if (uiCfg.ftrace || uiCfg.atraceApps.length > 0 || ftraceEvents.size > 0 ||
471      atraceCats.size > 0 || atraceApps.size > 0) {
472    const ds = new TraceConfig.DataSource();
473    ds.config = new DataSourceConfig();
474    ds.config.name = 'linux.ftrace';
475    ds.config.ftraceConfig = new FtraceConfig();
476    // Override the advanced ftrace parameters only if the user has ticked the
477    // "Advanced ftrace config" tab.
478    if (uiCfg.ftrace) {
479      ds.config.ftraceConfig.bufferSizeKb = uiCfg.ftraceBufferSizeKb;
480      ds.config.ftraceConfig.drainPeriodMs = uiCfg.ftraceDrainPeriodMs;
481      for (const line of uiCfg.ftraceExtraEvents.split('\n')) {
482        if (line.trim().length > 0) ftraceEvents.add(line.trim());
483      }
484    }
485    for (const line of uiCfg.atraceApps.split('\n')) {
486      if (line.trim().length > 0) atraceApps.add(line.trim());
487    }
488
489    if (atraceCats.size > 0 || atraceApps.size > 0) {
490      ftraceEvents.add('ftrace/print');
491    }
492
493    let ftraceEventsArray: string[] = [];
494    if (isAndroidP(target)) {
495      for (const ftraceEvent of ftraceEvents) {
496        // On P, we don't support groups so strip all group names from ftrace
497        // events.
498        const groupAndName = ftraceEvent.split('/');
499        if (groupAndName.length !== 2) {
500          ftraceEventsArray.push(ftraceEvent);
501          continue;
502        }
503        // Filter out any wildcard event groups which was not supported
504        // before Q.
505        if (groupAndName[1] === '*') {
506          continue;
507        }
508        ftraceEventsArray.push(groupAndName[1]);
509      }
510    } else {
511      ftraceEventsArray = Array.from(ftraceEvents);
512    }
513
514    ds.config.ftraceConfig.ftraceEvents = ftraceEventsArray;
515    ds.config.ftraceConfig.atraceCategories = Array.from(atraceCats);
516    ds.config.ftraceConfig.atraceApps = Array.from(atraceApps);
517    if (!isChromeTarget(target) || isCrOSTarget(target)) {
518      protoCfg.dataSources.push(ds);
519    }
520  }
521
522  return protoCfg;
523}
524
525export function toPbtxt(configBuffer: Uint8Array): string {
526  const msg = TraceConfig.decode(configBuffer);
527  const json = msg.toJSON();
528  function snakeCase(s: string): string {
529    return s.replace(/[A-Z]/g, c => '_' + c.toLowerCase());
530  }
531  // With the ahead of time compiled protos we can't seem to tell which
532  // fields are enums.
533  function isEnum(value: string): boolean {
534    return value.startsWith('MEMINFO_') || value.startsWith('VMSTAT_') ||
535        value.startsWith('STAT_') || value.startsWith('LID_') ||
536        value.startsWith('BATTERY_COUNTER_') || value === 'DISCARD' ||
537        value === 'RING_BUFFER';
538  }
539  // Since javascript doesn't have 64 bit numbers when converting protos to
540  // json the proto library encodes them as strings. This is lossy since
541  // we can't tell which strings that look like numbers are actually strings
542  // and which are actually numbers. Ideally we would reflect on the proto
543  // definition somehow but for now we just hard code keys which have this
544  // problem in the config.
545  function is64BitNumber(key: string): boolean {
546    return [
547      'maxFileSizeBytes',
548      'samplingIntervalBytes',
549      'shmemSizeBytes',
550      'pid'
551    ].includes(key);
552  }
553  function* message(msg: {}, indent: number): IterableIterator<string> {
554    for (const [key, value] of Object.entries(msg)) {
555      const isRepeated = Array.isArray(value);
556      const isNested = typeof value === 'object' && !isRepeated;
557      for (const entry of (isRepeated ? value as Array<{}> : [value])) {
558        yield ' '.repeat(indent) + `${snakeCase(key)}${isNested ? '' : ':'} `;
559        if (typeof entry === 'string') {
560          if (isEnum(entry) || is64BitNumber(key)) {
561            yield entry;
562          } else {
563            yield `"${entry.replace(new RegExp('"', 'g'), '\\"')}"`;
564          }
565        } else if (typeof entry === 'number') {
566          yield entry.toString();
567        } else if (typeof entry === 'boolean') {
568          yield entry.toString();
569        } else if (typeof entry === 'object' && entry !== null) {
570          yield '{\n';
571          yield* message(entry, indent + 4);
572          yield ' '.repeat(indent) + '}';
573        } else {
574          throw new Error(`Record proto entry "${entry}" with unexpected type ${
575              typeof entry}`);
576        }
577        yield '\n';
578      }
579    }
580  }
581  return [...message(json, 0)].join('');
582}
583
584export class RecordController extends Controller<'main'> implements Consumer {
585  private app: App;
586  private config: RecordConfig|null = null;
587  private extensionPort: MessagePort;
588  private recordingInProgress = false;
589  private consumerPort: ConsumerPort;
590  private traceBuffer: Uint8Array[] = [];
591  private bufferUpdateInterval: ReturnType<typeof setTimeout>|undefined;
592  private adb = new AdbOverWebUsb();
593  private recordedTraceSuffix = TRACE_SUFFIX;
594
595  // We have a different controller for each targetOS. The correct one will be
596  // created when needed, and stored here. When the key is a string, it is the
597  // serial of the target (used for android devices). When the key is a single
598  // char, it is the 'targetOS'
599  private controllerPromises = new Map<string, Promise<RpcConsumerPort>>();
600
601  constructor(args: {app: App, extensionPort: MessagePort}) {
602    super('main');
603    this.app = args.app;
604    this.consumerPort = ConsumerPort.create(this.rpcImpl.bind(this));
605    this.extensionPort = args.extensionPort;
606  }
607
608  run() {
609    // TODO(eseckler): Use ConsumerPort's QueryServiceState instead
610    // of posting a custom extension message to retrieve the category list.
611    if (this.app.state.updateChromeCategories === true) {
612      if (this.app.state.extensionInstalled) {
613        this.extensionPort.postMessage({method: 'GetCategories'});
614      }
615      globals.dispatch(Actions.setUpdateChromeCategories({update: false}));
616    }
617    if (this.app.state.recordConfig === this.config &&
618        this.app.state.recordingInProgress === this.recordingInProgress) {
619      return;
620    }
621    this.config = this.app.state.recordConfig;
622
623    const configProto =
624        genConfigProto(this.config, this.app.state.recordingTarget);
625    const configProtoText = toPbtxt(configProto);
626    const configProtoBase64 = base64Encode(configProto);
627    const commandline = `
628      echo '${configProtoBase64}' |
629      base64 --decode |
630      adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace" &&
631      adb pull /data/misc/perfetto-traces/trace /tmp/trace
632    `;
633    const traceConfig = genConfig(this.config, this.app.state.recordingTarget);
634    // TODO(hjd): This should not be TrackData after we unify the stores.
635    this.app.publish('TrackData', {
636      id: 'config',
637      data: {
638        commandline,
639        pbBase64: configProtoBase64,
640        pbtxt: configProtoText,
641        traceConfig
642      }
643    });
644
645    // If the recordingInProgress boolean state is different, it means that we
646    // have to start or stop recording a trace.
647    if (this.app.state.recordingInProgress === this.recordingInProgress) return;
648    this.recordingInProgress = this.app.state.recordingInProgress;
649
650    if (this.recordingInProgress) {
651      this.startRecordTrace(traceConfig);
652    } else {
653      this.stopRecordTrace();
654    }
655  }
656
657  startRecordTrace(traceConfig: TraceConfig) {
658    this.scheduleBufferUpdateRequests();
659    this.traceBuffer = [];
660    this.consumerPort.enableTracing({traceConfig});
661  }
662
663  stopRecordTrace() {
664    if (this.bufferUpdateInterval) clearInterval(this.bufferUpdateInterval);
665    this.consumerPort.disableTracing({});
666  }
667
668  scheduleBufferUpdateRequests() {
669    if (this.bufferUpdateInterval) clearInterval(this.bufferUpdateInterval);
670    this.bufferUpdateInterval = setInterval(() => {
671      this.consumerPort.getTraceStats({});
672    }, 200);
673  }
674
675  readBuffers() {
676    this.consumerPort.readBuffers({});
677  }
678
679  onConsumerPortResponse(data: ConsumerPortResponse) {
680    if (data === undefined) return;
681    if (isReadBuffersResponse(data)) {
682      if (!data.slices || data.slices.length === 0) return;
683      // TODO(nicomazz): handle this as intended by consumer_port.proto.
684      console.assert(data.slices.length === 1);
685      if (data.slices[0].data) this.traceBuffer.push(data.slices[0].data);
686      if (data.slices[0].lastSliceForPacket) this.onTraceComplete();
687    } else if (isEnableTracingResponse(data)) {
688      this.readBuffers();
689    } else if (isGetTraceStatsResponse(data)) {
690      const percentage = this.getBufferUsagePercentage(data);
691      if (percentage) {
692        globals.publish('BufferUsage', {percentage});
693      }
694    } else if (isFreeBuffersResponse(data)) {
695      // No action required.
696    } else if (isDisableTracingResponse(data)) {
697      // No action required.
698    } else {
699      console.error('Unrecognized consumer port response:', data);
700    }
701  }
702
703  onTraceComplete() {
704    this.consumerPort.freeBuffers({});
705    globals.dispatch(Actions.setRecordingStatus({status: undefined}));
706    if (globals.state.recordingCancelled) {
707      globals.dispatch(
708          Actions.setLastRecordingError({error: 'Recording cancelled.'}));
709      this.traceBuffer = [];
710      return;
711    }
712    const trace = this.generateTrace();
713    globals.dispatch(Actions.openTraceFromBuffer({
714      title: 'Recorded trace',
715      buffer: trace.buffer,
716      fileName: `recorded_trace${this.recordedTraceSuffix}`,
717    }));
718    this.traceBuffer = [];
719  }
720
721  // TODO(nicomazz): stream each chunk into the trace processor, instead of
722  // creating a big long trace.
723  generateTrace() {
724    let traceLen = 0;
725    for (const chunk of this.traceBuffer) traceLen += chunk.length;
726    const completeTrace = new Uint8Array(traceLen);
727    let written = 0;
728    for (const chunk of this.traceBuffer) {
729      completeTrace.set(chunk, written);
730      written += chunk.length;
731    }
732    return completeTrace;
733  }
734
735  getBufferUsagePercentage(data: GetTraceStatsResponse): number {
736    if (!data.traceStats || !data.traceStats.bufferStats) return 0.0;
737    let maximumUsage = 0;
738    for (const buffer of data.traceStats.bufferStats) {
739      const used = buffer.bytesWritten as number;
740      const total = buffer.bufferSize as number;
741      maximumUsage = Math.max(maximumUsage, used / total);
742    }
743    return maximumUsage;
744  }
745
746  onError(message: string) {
747    console.error('Error in record controller: ', message);
748    globals.dispatch(
749        Actions.setLastRecordingError({error: message.substr(0, 150)}));
750    globals.dispatch(Actions.stopRecording({}));
751  }
752
753  onStatus(message: string) {
754    globals.dispatch(Actions.setRecordingStatus({status: message}));
755  }
756
757  // Depending on the recording target, different implementation of the
758  // consumer_port will be used.
759  // - Chrome target: This forwards the messages that have to be sent
760  // to the extension to the frontend. This is necessary because this
761  // controller is running in a separate worker, that can't directly send
762  // messages to the extension.
763  // - Android device target: WebUSB is used to communicate using the adb
764  // protocol. Actually, there is no full consumer_port implementation, but
765  // only the support to start tracing and fetch the file.
766  async getTargetController(target: RecordingTarget): Promise<RpcConsumerPort> {
767    const identifier = this.getTargetIdentifier(target);
768
769    // The reason why caching the target 'record controller' Promise is that
770    // multiple rcp calls can happen while we are trying to understand if an
771    // android device has a socket connection available or not.
772    const precedentPromise = this.controllerPromises.get(identifier);
773    if (precedentPromise) return precedentPromise;
774
775    const controllerPromise =
776        new Promise<RpcConsumerPort>(async (resolve, _) => {
777          let controller: RpcConsumerPort|undefined = undefined;
778          if (isChromeTarget(target)) {
779            controller =
780                new ChromeExtensionConsumerPort(this.extensionPort, this);
781          } else if (isAdbTarget(target)) {
782            this.onStatus(`Please allow USB debugging on device.
783                 If you press cancel, reload the page.`);
784            const socketAccess = await this.hasSocketAccess(target);
785
786            controller = socketAccess ?
787                new AdbSocketConsumerPort(this.adb, this) :
788                new AdbConsumerPort(this.adb, this);
789          } else {
790            throw Error(`No device connected`);
791          }
792
793          if (!controller) throw Error(`Unknown target: ${target}`);
794          resolve(controller);
795        });
796
797    this.controllerPromises.set(identifier, controllerPromise);
798    return controllerPromise;
799  }
800
801  private getTargetIdentifier(target: RecordingTarget): string {
802    return isAdbTarget(target) ? target.serial : target.os;
803  }
804
805  private async hasSocketAccess(target: AdbRecordingTarget) {
806    const devices = await navigator.usb.getDevices();
807    const device = devices.find(d => d.serialNumber === target.serial);
808    console.assert(device);
809    if (!device) return Promise.resolve(false);
810    return AdbSocketConsumerPort.hasSocketAccess(device, this.adb);
811  }
812
813  private async rpcImpl(
814      method: RPCImplMethod, requestData: Uint8Array,
815      _callback: RPCImplCallback) {
816    try {
817      const state = this.app.state;
818      // TODO(hjd): This is a bit weird. We implicity send each RPC message to
819      // whichever target is currently selected (creating that target if needed)
820      // it would be nicer if the setup/teardown was more explicit.
821      const target = await this.getTargetController(state.recordingTarget);
822      this.recordedTraceSuffix = target.getRecordedTraceSuffix();
823      target.handleCommand(method.name, requestData);
824    } catch (e) {
825      console.error(`error invoking ${method}: ${e.message}`);
826    }
827  }
828}
829