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