• 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
15
16import {produce} from 'immer';
17import * as m from 'mithril';
18
19import {Actions} from '../common/actions';
20import {MeminfoCounters, VmstatCounters} from '../common/protos';
21import {
22  AdbRecordingTarget,
23  getBuiltinChromeCategoryList,
24  getDefaultRecordingTargets,
25  isAdbTarget,
26  isAndroidP,
27  isAndroidTarget,
28  isChromeTarget,
29  RecordingTarget
30} from '../common/state';
31import {MAX_TIME, RecordMode} from '../common/state';
32import {AdbOverWebUsb} from '../controller/adb';
33
34import {globals} from './globals';
35import {createPage} from './pages';
36import {
37  CodeSnippet,
38  Dropdown,
39  DropdownAttrs,
40  Probe,
41  ProbeAttrs,
42  Slider,
43  SliderAttrs,
44  Textarea,
45  TextareaAttrs
46} from './record_widgets';
47import {Router} from './router';
48
49const POLL_INTERVAL_MS = [250, 500, 1000, 2500, 5000, 30000, 60000];
50
51const ATRACE_CATEGORIES = new Map<string, string>();
52ATRACE_CATEGORIES.set('gfx', 'Graphics');
53ATRACE_CATEGORIES.set('input', 'Input');
54ATRACE_CATEGORIES.set('view', 'View System');
55ATRACE_CATEGORIES.set('webview', 'WebView');
56ATRACE_CATEGORIES.set('wm', 'Window Manager');
57ATRACE_CATEGORIES.set('am', 'Activity Manager');
58ATRACE_CATEGORIES.set('sm', 'Sync Manager');
59ATRACE_CATEGORIES.set('audio', 'Audio');
60ATRACE_CATEGORIES.set('video', 'Video');
61ATRACE_CATEGORIES.set('camera', 'Camera');
62ATRACE_CATEGORIES.set('hal', 'Hardware Modules');
63ATRACE_CATEGORIES.set('res', 'Resource Loading');
64ATRACE_CATEGORIES.set('dalvik', 'ART & Dalvik');
65ATRACE_CATEGORIES.set('rs', 'RenderScript');
66ATRACE_CATEGORIES.set('bionic', 'Bionic C library');
67ATRACE_CATEGORIES.set('gfx', 'Graphics');
68ATRACE_CATEGORIES.set('power', 'Power Management');
69ATRACE_CATEGORIES.set('pm', 'Package Manager');
70ATRACE_CATEGORIES.set('ss', 'System Server');
71ATRACE_CATEGORIES.set('database', 'Database');
72ATRACE_CATEGORIES.set('network', 'Network');
73ATRACE_CATEGORIES.set('adb', 'ADB');
74ATRACE_CATEGORIES.set('vibrator', 'Vibrator');
75ATRACE_CATEGORIES.set('aidl', 'AIDL calls');
76ATRACE_CATEGORIES.set('nnapi', 'Neural Network API');
77ATRACE_CATEGORIES.set('rro', 'Resource Overlay');
78ATRACE_CATEGORIES.set('binder_driver', 'Binder Kernel driver');
79ATRACE_CATEGORIES.set('binder_lock', 'Binder global lock trace');
80
81const LOG_BUFFERS = new Map<string, string>();
82LOG_BUFFERS.set('LID_DEFAULT', 'Main');
83LOG_BUFFERS.set('LID_RADIO', 'Radio');
84LOG_BUFFERS.set('LID_EVENTS', 'Binary events');
85LOG_BUFFERS.set('LID_SYSTEM', 'System');
86LOG_BUFFERS.set('LID_CRASH', 'Crash');
87LOG_BUFFERS.set('LID_STATS', 'Stats');
88LOG_BUFFERS.set('LID_SECURITY', 'Security');
89LOG_BUFFERS.set('LID_KERNEL', 'Kernel');
90
91const FTRACE_CATEGORIES = new Map<string, string>();
92FTRACE_CATEGORIES.set('binder/*', 'binder');
93FTRACE_CATEGORIES.set('block/*', 'block');
94FTRACE_CATEGORIES.set('clk/*', 'clk');
95FTRACE_CATEGORIES.set('ext4/*', 'ext4');
96FTRACE_CATEGORIES.set('f2fs/*', 'f2fs');
97FTRACE_CATEGORIES.set('i2c/*', 'i2c');
98FTRACE_CATEGORIES.set('irq/*', 'irq');
99FTRACE_CATEGORIES.set('kmem/*', 'kmem');
100FTRACE_CATEGORIES.set('memory_bus/*', 'memory_bus');
101FTRACE_CATEGORIES.set('mmc/*', 'mmc');
102FTRACE_CATEGORIES.set('oom/*', 'oom');
103FTRACE_CATEGORIES.set('power/*', 'power');
104FTRACE_CATEGORIES.set('regulator/*', 'regulator');
105FTRACE_CATEGORIES.set('sched/*', 'sched');
106FTRACE_CATEGORIES.set('sync/*', 'sync');
107FTRACE_CATEGORIES.set('task/*', 'task');
108FTRACE_CATEGORIES.set('task/*', 'task');
109FTRACE_CATEGORIES.set('vmscan/*', 'vmscan');
110
111function RecSettings(cssClass: string) {
112  const S = (x: number) => x * 1000;
113  const M = (x: number) => x * 1000 * 60;
114  const H = (x: number) => x * 1000 * 60 * 60;
115
116  const cfg = globals.state.recordConfig;
117
118  const recButton = (mode: RecordMode, title: string, img: string) => {
119    const checkboxArgs = {
120      checked: cfg.mode === mode,
121      onchange: m.withAttr(
122          'checked',
123          (checked: boolean) => {
124            if (!checked) return;
125            const traceCfg = produce(globals.state.recordConfig, draft => {
126              draft.mode = mode;
127            });
128            globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
129          })
130    };
131    return m(
132        `label${cfg.mode === mode ? '.selected' : ''}`,
133        m(`input[type=radio][name=rec_mode]`, checkboxArgs),
134        m(`img[src=assets/${img}]`),
135        m('span', title));
136  };
137
138  return m(
139      `.record-section${cssClass}`,
140      m('header', 'Recording mode'),
141      m('.record-mode',
142        recButton('STOP_WHEN_FULL', 'Stop when full', 'rec_one_shot.png'),
143        recButton('RING_BUFFER', 'Ring buffer', 'rec_ring_buf.png'),
144        recButton('LONG_TRACE', 'Long trace', 'rec_long_trace.png')),
145
146      m(Slider, {
147        title: 'In-memory buffer size',
148        icon: '360',
149        values: [4, 8, 16, 32, 64, 128, 256, 512],
150        unit: 'MB',
151        set: (cfg, val) => cfg.bufferSizeMb = val,
152        get: (cfg) => cfg.bufferSizeMb
153      } as SliderAttrs),
154
155      m(Slider, {
156        title: 'Max duration',
157        icon: 'timer',
158        values: [S(10), S(15), S(30), S(60), M(5), M(30), H(1), H(6), H(12)],
159        isTime: true,
160        unit: 'h:m:s',
161        set: (cfg, val) => cfg.durationMs = val,
162        get: (cfg) => cfg.durationMs
163      } as SliderAttrs),
164      m(Slider, {
165        title: 'Max file size',
166        icon: 'save',
167        cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '',
168        values: [5, 25, 50, 100, 500, 1000, 1000 * 5, 1000 * 10],
169        unit: 'MB',
170        set: (cfg, val) => cfg.maxFileSizeMb = val,
171        get: (cfg) => cfg.maxFileSizeMb
172      } as SliderAttrs),
173      m(Slider, {
174        title: 'Flush on disk every',
175        cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '',
176        icon: 'av_timer',
177        values: [100, 250, 500, 1000, 2500, 5000],
178        unit: 'ms',
179        set: (cfg, val) => cfg.fileWritePeriodMs = val,
180        get: (cfg) => cfg.fileWritePeriodMs || 0
181      } as SliderAttrs));
182}
183
184function PowerSettings(cssClass: string) {
185  return m(
186      `.record-section${cssClass}`,
187      m(Probe,
188        {
189          title: 'Battery drain',
190          img: 'rec_battery_counters.png',
191          descr: `Polls charge counters and instantaneous power draw from
192                    the battery power management IC.`,
193          setEnabled: (cfg, val) => cfg.batteryDrain = val,
194          isEnabled: (cfg) => cfg.batteryDrain
195        } as ProbeAttrs,
196        m(Slider, {
197          title: 'Poll interval',
198          cssClass: '.thin',
199          values: POLL_INTERVAL_MS,
200          unit: 'ms',
201          set: (cfg, val) => cfg.batteryDrainPollMs = val,
202          get: (cfg) => cfg.batteryDrainPollMs
203        } as SliderAttrs)),
204      m(Probe, {
205        title: 'Board voltages & frequencies',
206        img: 'rec_board_voltage.png',
207        descr: 'Tracks voltage and frequency changes from board sensors',
208        setEnabled: (cfg, val) => cfg.boardSensors = val,
209        isEnabled: (cfg) => cfg.boardSensors
210      } as ProbeAttrs));
211}
212
213function GpuSettings(cssClass: string) {
214  return m(`.record-section${cssClass}`, m(Probe, {
215             title: 'GPU frequency',
216             img: 'rec_cpu_freq.png',
217             descr: 'Records gpu frequency via ftrace',
218             setEnabled: (cfg, val) => cfg.gpuFreq = val,
219             isEnabled: (cfg) => cfg.gpuFreq
220           } as ProbeAttrs));
221}
222
223function CpuSettings(cssClass: string) {
224  return m(
225      `.record-section${cssClass}`,
226      m(Probe,
227        {
228          title: 'Coarse CPU usage counter',
229          img: 'rec_cpu_coarse.png',
230          descr: `Lightweight polling of CPU usage counters via /proc/stat.
231                    Allows to periodically monitor CPU usage.`,
232          setEnabled: (cfg, val) => cfg.cpuCoarse = val,
233          isEnabled: (cfg) => cfg.cpuCoarse
234        } as ProbeAttrs,
235        m(Slider, {
236          title: 'Poll interval',
237          cssClass: '.thin',
238          values: POLL_INTERVAL_MS,
239          unit: 'ms',
240          set: (cfg, val) => cfg.cpuCoarsePollMs = val,
241          get: (cfg) => cfg.cpuCoarsePollMs
242        } as SliderAttrs)),
243      m(Probe, {
244        title: 'Scheduling details',
245        img: 'rec_cpu_fine.png',
246        descr: 'Enables high-detailed tracking of scheduling events',
247        setEnabled: (cfg, val) => cfg.cpuSched = val,
248        isEnabled: (cfg) => cfg.cpuSched
249      } as ProbeAttrs),
250      m(Probe, {
251        title: 'CPU frequency and idle states',
252        img: 'rec_cpu_freq.png',
253        descr: 'Records cpu frequency and idle state changes via ftrace',
254        setEnabled: (cfg, val) => cfg.cpuFreq = val,
255        isEnabled: (cfg) => cfg.cpuFreq
256      } as ProbeAttrs),
257      m(Probe, {
258        title: 'Scheduling chains / latency analysis',
259        img: 'rec_cpu_wakeup.png',
260        descr: `Tracks causality of scheduling transitions. When a task
261                X transitions from blocked -> runnable, keeps track of the
262                task Y that X's transition (e.g. posting a semaphore).`,
263        setEnabled: (cfg, val) => cfg.cpuLatency = val,
264        isEnabled: (cfg) => cfg.cpuLatency
265      } as ProbeAttrs),
266      m(Probe, {
267        title: 'Syscalls',
268        img: null,
269        descr: `Tracks the enter and exit of all syscalls.`,
270        setEnabled: (cfg, val) => cfg.cpuSyscall = val,
271        isEnabled: (cfg) => cfg.cpuSyscall
272      } as ProbeAttrs));
273}
274
275function HeapSettings(cssClass: string) {
276  const valuesForMS = [
277    0,
278    1000,
279    10 * 1000,
280    30 * 1000,
281    60 * 1000,
282    5 * 60 * 1000,
283    10 * 60 * 1000,
284    30 * 60 * 1000,
285    60 * 60 * 1000
286  ];
287  const valuesForShMemBuff = [
288    0,
289    512,
290    1024,
291    2 * 1024,
292    4 * 1024,
293    8 * 1024,
294    16 * 1024,
295    32 * 1024,
296    64 * 1024,
297    128 * 1024,
298    256 * 1024,
299    512 * 1024,
300    1024 * 1024,
301    64 * 1024 * 1024,
302    128 * 1024 * 1024,
303    256 * 1024 * 1024,
304    512 * 1024 * 1024
305  ];
306
307  return m(
308      `.${cssClass}`,
309      m(Textarea, {
310        title: 'Names or pids of the processes to track',
311        placeholder: 'One per line, e.g.:\n' +
312            'system_server\n' +
313            '1503',
314        set: (cfg, val) => cfg.hpProcesses = val,
315        get: (cfg) => cfg.hpProcesses
316      } as TextareaAttrs),
317      m(Slider, {
318        title: 'Sampling interval',
319        cssClass: '.thin',
320        values: [
321          0,     1,     2,      4,      8,      16,     32,   64,
322          128,   256,   512,    1024,   2048,   4096,   8192, 16384,
323          32768, 65536, 131072, 262144, 524288, 1048576
324        ],
325        unit: 'B',
326        min: 0,
327        set: (cfg, val) => cfg.hpSamplingIntervalBytes = val,
328        get: (cfg) => cfg.hpSamplingIntervalBytes
329      } as SliderAttrs),
330      m(Slider, {
331        title: 'Continuous dumps interval ',
332        description: 'Time between following dumps (0 = disabled)',
333        cssClass: '.thin',
334        values: valuesForMS,
335        unit: 'ms',
336        min: 0,
337        set: (cfg, val) => {
338          cfg.hpContinuousDumpsInterval = val;
339        },
340        get: (cfg) => cfg.hpContinuousDumpsInterval
341      } as SliderAttrs),
342      m(Slider, {
343        title: 'Continuous dumps phase',
344        description: 'Time before first dump',
345        cssClass: `.thin${
346            globals.state.recordConfig.hpContinuousDumpsInterval === 0 ?
347                '.greyed-out' :
348                ''}`,
349        values: valuesForMS,
350        unit: 'ms',
351        min: 0,
352        disabled: globals.state.recordConfig.hpContinuousDumpsInterval === 0,
353        set: (cfg, val) => cfg.hpContinuousDumpsPhase = val,
354        get: (cfg) => cfg.hpContinuousDumpsPhase
355      } as SliderAttrs),
356      m(Slider, {
357        title: `Shared memory buffer`,
358        cssClass: '.thin',
359        values: valuesForShMemBuff.filter(
360            value => value === 0 || value >= 8192 && value % 4096 === 0),
361        unit: 'B',
362        min: 0,
363        set: (cfg, val) => cfg.hpSharedMemoryBuffer = val,
364        get: (cfg) => cfg.hpSharedMemoryBuffer
365      } as SliderAttrs)
366      // TODO(taylori): Add advanced options.
367  );
368}
369
370function JavaHeapDumpSettings(cssClass: string) {
371  const valuesForMS = [
372    0,
373    1000,
374    10 * 1000,
375    30 * 1000,
376    60 * 1000,
377    5 * 60 * 1000,
378    10 * 60 * 1000,
379    30 * 60 * 1000,
380    60 * 60 * 1000
381  ];
382
383  return m(
384      `.${cssClass}`,
385      m(Textarea, {
386        title: 'Names or pids of the processes to track',
387        placeholder: 'One per line, e.g.:\n' +
388            'com.android.vending\n' +
389            '1503',
390        set: (cfg, val) => cfg.jpProcesses = val,
391        get: (cfg) => cfg.jpProcesses
392      } as TextareaAttrs),
393      m(Slider, {
394        title: 'Continuous dumps interval ',
395        description: 'Time between following dumps (0 = disabled)',
396        cssClass: '.thin',
397        values: valuesForMS,
398        unit: 'ms',
399        min: 0,
400        set: (cfg, val) => {
401          cfg.jpContinuousDumpsInterval = val;
402        },
403        get: (cfg) => cfg.jpContinuousDumpsInterval
404      } as SliderAttrs),
405      m(Slider, {
406        title: 'Continuous dumps phase',
407        description: 'Time before first dump',
408        cssClass: `.thin${
409            globals.state.recordConfig.jpContinuousDumpsInterval === 0 ?
410                '.greyed-out' :
411                ''}`,
412        values: valuesForMS,
413        unit: 'ms',
414        min: 0,
415        disabled: globals.state.recordConfig.jpContinuousDumpsInterval === 0,
416        set: (cfg, val) => cfg.jpContinuousDumpsPhase = val,
417        get: (cfg) => cfg.jpContinuousDumpsPhase
418      } as SliderAttrs),
419  );
420}
421
422function MemorySettings(cssClass: string) {
423  const meminfoOpts = new Map<string, string>();
424  for (const x in MeminfoCounters) {
425    if (typeof MeminfoCounters[x] === 'number' &&
426        !`${x}`.endsWith('_UNSPECIFIED')) {
427      meminfoOpts.set(x, x.replace('MEMINFO_', '').toLowerCase());
428    }
429  }
430  const vmstatOpts = new Map<string, string>();
431  for (const x in VmstatCounters) {
432    if (typeof VmstatCounters[x] === 'number' &&
433        !`${x}`.endsWith('_UNSPECIFIED')) {
434      vmstatOpts.set(x, x.replace('VMSTAT_', '').toLowerCase());
435    }
436  }
437  return m(
438      `.record-section${cssClass}`,
439      m(Probe,
440        {
441          title: 'Native heap profiling',
442          img: 'rec_native_heap_profiler.png',
443          descr: `Track native heap allocations & deallocations of an Android
444               process. (Available on Android 10+)`,
445          setEnabled: (cfg, val) => cfg.heapProfiling = val,
446          isEnabled: (cfg) => cfg.heapProfiling
447        } as ProbeAttrs,
448        HeapSettings(cssClass)),
449      m(Probe,
450        {
451          title: 'Java heap dumps',
452          img: 'rec_java_heap_dump.png',
453          descr: `Dump information about the Java object graph of an
454          Android app. (Available on Android 11+)`,
455          setEnabled: (cfg, val) => cfg.javaHeapDump = val,
456          isEnabled: (cfg) => cfg.javaHeapDump
457        } as ProbeAttrs,
458        JavaHeapDumpSettings(cssClass)),
459      m(Probe,
460        {
461          title: 'Kernel meminfo',
462          img: 'rec_meminfo.png',
463          descr: 'Polling of /proc/meminfo',
464          setEnabled: (cfg, val) => cfg.meminfo = val,
465          isEnabled: (cfg) => cfg.meminfo
466        } as ProbeAttrs,
467        m(Slider, {
468          title: 'Poll interval',
469          cssClass: '.thin',
470          values: POLL_INTERVAL_MS,
471          unit: 'ms',
472          set: (cfg, val) => cfg.meminfoPeriodMs = val,
473          get: (cfg) => cfg.meminfoPeriodMs
474        } as SliderAttrs),
475        m(Dropdown, {
476          title: 'Select counters',
477          cssClass: '.multicolumn',
478          options: meminfoOpts,
479          set: (cfg, val) => cfg.meminfoCounters = val,
480          get: (cfg) => cfg.meminfoCounters
481        } as DropdownAttrs)),
482      m(Probe, {
483        title: 'High-frequency memory events',
484        img: 'rec_mem_hifreq.png',
485        descr: `Allows to track short memory spikes and transitories through
486                ftrace's mm_event, rss_stat and ion events. Available only
487                on recent Android Q+ kernels`,
488        setEnabled: (cfg, val) => cfg.memHiFreq = val,
489        isEnabled: (cfg) => cfg.memHiFreq
490      } as ProbeAttrs),
491      m(Probe, {
492        title: 'Low memory killer',
493        img: 'rec_lmk.png',
494        descr: `Record LMK events. Works both with the old in-kernel LMK
495                and the newer userspace lmkd. It also tracks OOM score
496                adjustments.`,
497        setEnabled: (cfg, val) => cfg.memLmk = val,
498        isEnabled: (cfg) => cfg.memLmk
499      } as ProbeAttrs),
500      m(Probe,
501        {
502          title: 'Per process stats',
503          img: 'rec_ps_stats.png',
504          descr: `Periodically samples all processes in the system tracking:
505                    their thread list, memory counters (RSS, swap and other
506                    /proc/status counters) and oom_score_adj.`,
507          setEnabled: (cfg, val) => cfg.procStats = val,
508          isEnabled: (cfg) => cfg.procStats
509        } as ProbeAttrs,
510        m(Slider, {
511          title: 'Poll interval',
512          cssClass: '.thin',
513          values: POLL_INTERVAL_MS,
514          unit: 'ms',
515          set: (cfg, val) => cfg.procStatsPeriodMs = val,
516          get: (cfg) => cfg.procStatsPeriodMs
517        } as SliderAttrs)),
518      m(Probe,
519        {
520          title: 'Virtual memory stats',
521          img: 'rec_vmstat.png',
522          descr: `Periodically polls virtual memory stats from /proc/vmstat.
523                    Allows to gather statistics about swap, eviction,
524                    compression and pagecache efficiency`,
525          setEnabled: (cfg, val) => cfg.vmstat = val,
526          isEnabled: (cfg) => cfg.vmstat
527        } as ProbeAttrs,
528        m(Slider, {
529          title: 'Poll interval',
530          cssClass: '.thin',
531          values: POLL_INTERVAL_MS,
532          unit: 'ms',
533          set: (cfg, val) => cfg.vmstatPeriodMs = val,
534          get: (cfg) => cfg.vmstatPeriodMs
535        } as SliderAttrs),
536        m(Dropdown, {
537          title: 'Select counters',
538          cssClass: '.multicolumn',
539          options: vmstatOpts,
540          set: (cfg, val) => cfg.vmstatCounters = val,
541          get: (cfg) => cfg.vmstatCounters
542        } as DropdownAttrs)));
543}
544
545
546function AndroidSettings(cssClass: string) {
547  return m(
548      `.record-section${cssClass}`,
549      m(Probe,
550        {
551          title: 'Atrace userspace annotations',
552          img: 'rec_atrace.png',
553          descr: `Enables C++ / Java codebase annotations (ATRACE_BEGIN() /
554                    os.Trace())`,
555          setEnabled: (cfg, val) => cfg.atrace = val,
556          isEnabled: (cfg) => cfg.atrace
557        } as ProbeAttrs,
558        m(Dropdown, {
559          title: 'Categories',
560          cssClass: '.multicolumn.atrace-categories',
561          options: ATRACE_CATEGORIES,
562          set: (cfg, val) => cfg.atraceCats = val,
563          get: (cfg) => cfg.atraceCats
564        } as DropdownAttrs),
565        m(Textarea, {
566          placeholder: 'Extra apps to profile, one per line, e.g.:\n' +
567              'com.android.phone\n' +
568              'com.android.nfc',
569          set: (cfg, val) => cfg.atraceApps = val,
570          get: (cfg) => cfg.atraceApps
571        } as TextareaAttrs)),
572      m(Probe,
573        {
574          title: 'Event log (logcat)',
575          img: 'rec_logcat.png',
576          descr: `Streams the event log into the trace. If no buffer filter is
577                    specified, all buffers are selected.`,
578          setEnabled: (cfg, val) => cfg.androidLogs = val,
579          isEnabled: (cfg) => cfg.androidLogs
580        } as ProbeAttrs,
581        m(Dropdown, {
582          title: 'Buffers',
583          options: LOG_BUFFERS,
584          set: (cfg, val) => cfg.androidLogBuffers = val,
585          get: (cfg) => cfg.androidLogBuffers
586        } as DropdownAttrs)));
587}
588
589
590function ChromeSettings(cssClass: string) {
591  return m(
592      `.record-section${cssClass}`,
593      m(Probe, {
594        title: 'Task scheduling',
595        img: null,
596        descr: `Records events about task scheduling and execution on all
597                  threads`,
598        setEnabled: (cfg, val) => cfg.taskScheduling = val,
599        isEnabled: (cfg) => cfg.taskScheduling
600      } as ProbeAttrs),
601      m(Probe, {
602        title: 'IPC flows',
603        img: null,
604        descr: `Records flow events for passing of IPC messages between
605                processes.`,
606        setEnabled: (cfg, val) => cfg.ipcFlows = val,
607        isEnabled: (cfg) => cfg.ipcFlows
608      } as ProbeAttrs),
609      m(Probe, {
610        title: 'Javascript execution',
611        img: null,
612        descr: `Records events about Javascript execution in the renderer
613                    processes.`,
614        setEnabled: (cfg, val) => cfg.jsExecution = val,
615        isEnabled: (cfg) => cfg.jsExecution
616      } as ProbeAttrs),
617      m(Probe, {
618        title: 'Web content rendering',
619        img: null,
620        descr: `Records events about rendering, layout, and compositing of
621        web content in Blink.`,
622        setEnabled: (cfg, val) => cfg.webContentRendering = val,
623        isEnabled: (cfg) => cfg.webContentRendering
624      } as ProbeAttrs),
625      m(Probe, {
626        title: 'UI rendering & compositing',
627        img: null,
628        descr: `Records events about rendering of browser UI surfaces and
629        compositing of surfaces.`,
630        setEnabled: (cfg, val) => cfg.uiRendering = val,
631        isEnabled: (cfg) => cfg.uiRendering
632      } as ProbeAttrs),
633      m(Probe, {
634        title: 'Input events',
635        img: null,
636        descr: `Records input events and their flow between processes.`,
637        setEnabled: (cfg, val) => cfg.inputEvents = val,
638        isEnabled: (cfg) => cfg.inputEvents
639      } as ProbeAttrs),
640      m(Probe, {
641        title: 'Navigation & Loading',
642        img: null,
643        descr: `Records network events for navigations and resources.`,
644        setEnabled: (cfg, val) => cfg.navigationAndLoading = val,
645        isEnabled: (cfg) => cfg.navigationAndLoading
646      } as ProbeAttrs),
647      m(Probe, {
648        title: 'Chrome Logs',
649        img: null,
650        descr: `Records Chrome log messages`,
651        setEnabled: (cfg, val) => cfg.chromeLogs = val,
652        isEnabled: (cfg) => cfg.chromeLogs
653      } as ProbeAttrs),
654      ChromeCategoriesSelection());
655}
656
657function ChromeCategoriesSelection() {
658  // If we are attempting to record via the Chrome extension, we receive the
659  // list of actually supported categories via DevTools. Otherwise, we fall back
660  // to an integrated list of categories from a recent version of Chrome.
661  let categories = globals.state.chromeCategories;
662  if (!categories || !isChromeTarget(globals.state.recordingTarget)) {
663    categories = getBuiltinChromeCategoryList();
664  }
665
666  // Show "disabled-by-default" categories last.
667  const categoriesMap = new Map<string, string>();
668  const disabledByDefaultCategories: string[] = [];
669  const disabledPrefix = 'disabled-by-default-';
670  categories.forEach(cat => {
671    if (cat.startsWith(disabledPrefix)) {
672      disabledByDefaultCategories.push(cat);
673    } else {
674      categoriesMap.set(cat, cat);
675    }
676  });
677  disabledByDefaultCategories.forEach(cat => {
678    categoriesMap.set(
679        cat, `${cat.replace(disabledPrefix, '')} (high overhead)`);
680  });
681
682  return m(Dropdown, {
683    title: 'Additional Chrome categories',
684    cssClass: '.multicolumn.two-columns',
685    options: categoriesMap,
686    set: (cfg, val) => cfg.chromeCategoriesSelected = val,
687    get: (cfg) => cfg.chromeCategoriesSelected
688  } as DropdownAttrs);
689}
690
691function AdvancedSettings(cssClass: string) {
692  const S = (x: number) => x * 1000;
693  const M = (x: number) => x * 1000 * 60;
694  return m(
695      `.record-section${cssClass}`,
696      m(Probe,
697        {
698          title: 'Advanced ftrace config',
699          img: 'rec_ftrace.png',
700          descr: `Enable individual events and tune the kernel-tracing (ftrace)
701                  module. The events enabled here are in addition to those from
702                  enabled by other probes.`,
703          setEnabled: (cfg, val) => cfg.ftrace = val,
704          isEnabled: (cfg) => cfg.ftrace
705        } as ProbeAttrs,
706        m(Slider, {
707          title: 'Buf size',
708          cssClass: '.thin',
709          values: [512, 1024, 2 * 1024, 4 * 1024, 16 * 1024, 32 * 1024],
710          unit: 'KB',
711          set: (cfg, val) => cfg.ftraceBufferSizeKb = val,
712          get: (cfg) => cfg.ftraceBufferSizeKb
713        } as SliderAttrs),
714        m(Slider, {
715          title: 'Drain rate',
716          cssClass: '.thin',
717          values: [100, 250, 500, 1000, 2500, 5000],
718          unit: 'ms',
719          set: (cfg, val) => cfg.ftraceDrainPeriodMs = val,
720          get: (cfg) => cfg.ftraceDrainPeriodMs
721        } as SliderAttrs),
722        m(Dropdown, {
723          title: 'Event groups',
724          cssClass: '.multicolumn.ftrace-events',
725          options: FTRACE_CATEGORIES,
726          set: (cfg, val) => cfg.ftraceEvents = val,
727          get: (cfg) => cfg.ftraceEvents
728        } as DropdownAttrs),
729        m(Textarea, {
730          placeholder: 'Add extra events, one per line, e.g.:\n' +
731              'sched/sched_switch\n' +
732              'kmem/*',
733          set: (cfg, val) => cfg.ftraceExtraEvents = val,
734          get: (cfg) => cfg.ftraceExtraEvents
735        } as TextareaAttrs)),
736      globals.state.videoEnabled ?
737          m(Probe,
738            {
739              title: 'Screen recording',
740              img: null,
741              descr: `Records the screen along with running a trace. Max
742                  time of recording is 3 minutes (180 seconds).`,
743              setEnabled: (cfg, val) => cfg.screenRecord = val,
744              isEnabled: (cfg) => cfg.screenRecord,
745            } as ProbeAttrs,
746            m(Slider, {
747              title: 'Max duration',
748              icon: 'timer',
749              values: [S(10), S(15), S(30), S(60), M(2), M(3)],
750              isTime: true,
751              unit: 'm:s',
752              set: (cfg, val) => cfg.durationMs = val,
753              get: (cfg) => cfg.durationMs,
754            } as SliderAttrs)) :
755          null);
756}
757
758function RecordHeader() {
759  return m(
760      '.record-header',
761      m('.top-part',
762        m('.target-and-status',
763          RecordingPlatformSelection(),
764          RecordingStatusLabel(),
765          ErrorLabel()),
766        recordingButtons()),
767      RecordingNotes());
768}
769
770function RecordingPlatformSelection() {
771  if (globals.state.recordingInProgress) return [];
772
773  const availableAndroidDevices = globals.state.availableAdbDevices;
774  const recordingTarget = globals.state.recordingTarget;
775
776  const targets = [];
777  for (const {os, name} of getDefaultRecordingTargets()) {
778    targets.push(m('option', {value: os}, name));
779  }
780  for (const d of availableAndroidDevices) {
781    targets.push(m('option', {value: d.serial}, d.name));
782  }
783
784  const selectedIndex = isAdbTarget(recordingTarget) ?
785      targets.findIndex(node => node.attrs.value === recordingTarget.serial) :
786      targets.findIndex(node => node.attrs.value === recordingTarget.os);
787
788  return m(
789      '.target',
790      m(
791          'label',
792          'Target platform:',
793          m('select',
794            {
795              selectedIndex,
796              onchange: m.withAttr('value', onTargetChange),
797              onupdate: (select) => {
798                // Work around mithril bug
799                // (https://github.com/MithrilJS/mithril.js/issues/2107): We may
800                // update the select's options while also changing the
801                // selectedIndex at the same time. The update of selectedIndex
802                // may be applied before the new options are added to the select
803                // element. Because the new selectedIndex may be outside of the
804                // select's options at that time, we have to reselect the
805                // correct index here after any new children were added.
806                (select.dom as HTMLSelectElement).selectedIndex = selectedIndex;
807              }
808            },
809            ...targets),
810          ),
811      m('.chip',
812        {onclick: addAndroidDevice},
813        m('button', 'Add ADB Device'),
814        m('i.material-icons', 'add')));
815}
816
817// |target| can be the TargetOs or the android serial.
818function onTargetChange(target: string) {
819  const recordingTarget: RecordingTarget =
820      globals.state.availableAdbDevices.find(d => d.serial === target) ||
821      getDefaultRecordingTargets().find(t => t.os === target) ||
822      getDefaultRecordingTargets()[0];
823  globals.dispatch(Actions.setRecordingTarget({target: recordingTarget}));
824  globals.rafScheduler.scheduleFullRedraw();
825}
826
827function Instructions(cssClass: string) {
828  return m(
829      `.record-section.instructions${cssClass}`,
830      m('header', 'Instructions'),
831      RecordingSnippet(),
832      BufferUsageProgressBar(),
833      m('.buttons', StopCancelButtons()),
834      recordingLog());
835}
836
837function BufferUsageProgressBar() {
838  if (!globals.state.recordingInProgress) return [];
839
840  const bufferUsage = globals.bufferUsage ? globals.bufferUsage : 0.0;
841  // Buffer usage is not available yet on Android.
842  if (bufferUsage === 0) return [];
843
844  return m(
845      'label',
846      'Buffer usage: ',
847      m('progress', {max: 100, value: bufferUsage * 100}));
848}
849
850function RecordingNotes() {
851  const docUrl = '//docs.perfetto.dev/#/build-instructions?id=get-the-code';
852  const extensionURL = `https://chrome.google.com/webstore/detail/
853      perfetto-ui/lfmkphfpdbjijhpomgecfikhfohaoine`;
854
855  const notes: m.Children = [];
856  const doc =
857      m('span', 'Follow the ', m('a', {href: docUrl}, 'instructions here.'));
858
859  const msgFeatNotSupported =
860      m('div', `Some of the probes are only supported in the
861      last version of perfetto running on Android Q+`);
862
863  const msgPerfettoNotSupported =
864      m('div', `Perfetto is not supported natively before Android P.`);
865
866  const msgRecordingNotSupported =
867      m('div', `Recording Perfetto traces from the UI is not supported natively
868     before Android Q. If you are using a P device, please select 'Android P'
869     as the 'Target Platform' and collect the trace using ADB`);
870
871  const msgSideload =
872      m('div',
873        `If you have a rooted device you can sideload the latest version of
874         perfetto. `,
875        doc);
876
877  const msgChrome =
878      m('div',
879        `To trace Chrome from the Perfetto UI, you need to install our `,
880        m('a', {href: extensionURL}, 'Chrome extension'),
881        ' and then reload this page.');
882
883  const msgLinux =
884      m('div',
885        `In order to use perfetto on Linux you need to
886      compile it and run the following command from the build
887      output directory. `,
888        doc);
889
890  if (isAdbTarget(globals.state.recordingTarget)) {
891    notes.push(msgRecordingNotSupported);
892  }
893  switch (globals.state.recordingTarget.os) {
894    case 'Q':
895      break;
896    case 'P':
897      notes.push(msgFeatNotSupported);
898      notes.push(msgSideload);
899      break;
900    case 'O':
901      notes.push(msgPerfettoNotSupported);
902      notes.push(msgSideload);
903      break;
904    case 'L':
905      notes.push(msgLinux);
906      break;
907    case 'C':
908      if (!globals.state.extensionInstalled) notes.push(msgChrome);
909      break;
910    default:
911  }
912
913  return notes.length > 0 ? m('.note', notes) : [];
914}
915
916function RecordingSnippet() {
917  const target = globals.state.recordingTarget;
918
919  // We don't need commands to start tracing on chrome
920  if (isChromeTarget(target)) {
921    return globals.state.extensionInstalled ?
922        m('div',
923          m('label',
924            `To trace Chrome from the Perfetto UI you just have to press
925         'Start Recording'.`)) :
926        [];
927  }
928  return m(CodeSnippet, {text: getRecordCommand(target)});
929}
930
931function getRecordCommand(target: RecordingTarget) {
932  const data = globals.trackDataStore.get('config') as
933          {commandline: string, pbtxt: string, pbBase64: string} |
934      null;
935
936  const cfg = globals.state.recordConfig;
937  let time = cfg.durationMs / 1000;
938
939  if (time > MAX_TIME) {
940    time = MAX_TIME;
941  }
942
943  const pbBase64 = data ? data.pbBase64 : '';
944  const pbtx = data ? data.pbtxt : '';
945  let cmd = '';
946  if (cfg.screenRecord) {
947    // Half-second delay to ensure Perfetto starts tracing before screenrecord
948    // starts recording
949    cmd += `(sleep 0.5 && adb shell screenrecord --time-limit ${time}`;
950    cmd += ' "/sdcard/tracescr.mp4") &\\\n';
951  }
952  if (isAndroidP(target)) {
953    cmd += `echo '${pbBase64}' | \n`;
954    cmd += 'base64 --decode | \n';
955    cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n';
956  } else {
957    cmd +=
958        isAndroidTarget(target) ? 'adb shell perfetto \\\n' : 'perfetto \\\n';
959    cmd += '  -c - --txt \\\n';
960    cmd += '  -o /data/misc/perfetto-traces/trace \\\n';
961    cmd += '<<EOF\n\n';
962    cmd += pbtx;
963    cmd += '\nEOF\n';
964  }
965  return cmd;
966}
967
968function recordingButtons() {
969  const state = globals.state;
970  const target = state.recordingTarget;
971  const recInProgress = state.recordingInProgress;
972
973  const start =
974      m(`button`,
975        {
976          class: recInProgress ? '' : 'selected',
977          onclick: onStartRecordingPressed
978        },
979        'Start Recording');
980
981  const buttons: m.Children = [];
982
983  if (isAndroidTarget(target)) {
984    if (!recInProgress && isAdbTarget(target)) {
985      buttons.push(start);
986    }
987  } else if (isChromeTarget(target) && state.extensionInstalled) {
988    buttons.push(start);
989  }
990  return m('.button', buttons);
991}
992
993function StopCancelButtons() {
994  if (!globals.state.recordingInProgress) return [];
995
996  const stop =
997      m(`button.selected`,
998        {onclick: () => globals.dispatch(Actions.stopRecording({}))},
999        'Stop');
1000
1001  const cancel =
1002      m(`button`,
1003        {onclick: () => globals.dispatch(Actions.cancelRecording({}))},
1004        'Cancel');
1005
1006  return [stop, cancel];
1007}
1008
1009function onStartRecordingPressed() {
1010  location.href = '#!/record?p=instructions';
1011  globals.rafScheduler.scheduleFullRedraw();
1012
1013  const target = globals.state.recordingTarget;
1014  if (isAndroidTarget(target) || isChromeTarget(target)) {
1015    globals.dispatch(Actions.startRecording({}));
1016  }
1017}
1018
1019function RecordingStatusLabel() {
1020  const recordingStatus = globals.state.recordingStatus;
1021  if (!recordingStatus) return [];
1022  return m('label', recordingStatus);
1023}
1024
1025function ErrorLabel() {
1026  const lastRecordingError = globals.state.lastRecordingError;
1027  if (!lastRecordingError) return [];
1028  return m('label.error-label', `Error:  ${lastRecordingError}`);
1029}
1030
1031function recordingLog() {
1032  const logs = globals.recordingLog;
1033  if (logs === undefined) return [];
1034  return m('.code-snippet.no-top-bar', m('code', logs));
1035}
1036
1037// The connection must be done in the frontend. After it, the serial ID will
1038// be inserted in the state, and the worker will be able to connect to the
1039// correct device.
1040async function addAndroidDevice() {
1041  let device: USBDevice;
1042  try {
1043    device = await new AdbOverWebUsb().findDevice();
1044  } catch (e) {
1045    const err = `No device found: ${e.name}: ${e.message}`;
1046    console.error(err, e);
1047    alert(err);
1048    return;
1049  }
1050
1051  if (!device.serialNumber) {
1052    console.error('serial number undefined');
1053    return;
1054  }
1055
1056  // After the user has selected a device with the chrome UI, it will be
1057  // available when listing all the available device from WebUSB. Therefore,
1058  // we update the list of available devices.
1059  await updateAvailableAdbDevices(device.serialNumber);
1060}
1061
1062export async function updateAvailableAdbDevices(
1063    preferredDeviceSerial?: string) {
1064  const devices = await new AdbOverWebUsb().getPairedDevices();
1065
1066  let recordingTarget: AdbRecordingTarget|undefined = undefined;
1067
1068  const availableAdbDevices: AdbRecordingTarget[] = [];
1069  devices.forEach(d => {
1070    if (d.productName && d.serialNumber) {
1071      // TODO(nicomazz): At this stage, we can't know the OS version, so we
1072      // assume it is 'Q'. This can create problems with devices with an old
1073      // version of perfetto. The os detection should be done after the adb
1074      // connection, from adb_record_controller
1075      availableAdbDevices.push(
1076          {name: d.productName, serial: d.serialNumber, os: 'Q'});
1077      if (preferredDeviceSerial && preferredDeviceSerial === d.serialNumber) {
1078        recordingTarget = availableAdbDevices[availableAdbDevices.length - 1];
1079      }
1080    }
1081  });
1082
1083  globals.dispatch(
1084      Actions.setAvailableAdbDevices({devices: availableAdbDevices}));
1085  selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget);
1086  globals.rafScheduler.scheduleFullRedraw();
1087  return availableAdbDevices;
1088}
1089
1090function selectAndroidDeviceIfAvailable(
1091    availableAdbDevices: AdbRecordingTarget[],
1092    recordingTarget?: RecordingTarget) {
1093  if (!recordingTarget) {
1094    recordingTarget = globals.state.recordingTarget;
1095  }
1096  const deviceConnected = isAdbTarget(recordingTarget);
1097  const connectedDeviceDisconnected = deviceConnected &&
1098      availableAdbDevices.find(
1099          e => e.serial === (recordingTarget as AdbRecordingTarget).serial) ===
1100          undefined;
1101
1102  if (availableAdbDevices.length) {
1103    // If there's an Android device available and the current selection isn't
1104    // one, select the Android device by default. If the current device isn't
1105    // available anymore, but another Android device is, select the other
1106    // Android device instead.
1107    if (!deviceConnected || connectedDeviceDisconnected) {
1108      recordingTarget = availableAdbDevices[0];
1109    }
1110
1111    globals.dispatch(Actions.setRecordingTarget({target: recordingTarget}));
1112    return;
1113  }
1114
1115  // If the currently selected device was disconnected, reset the recording
1116  // target to the default one.
1117  if (connectedDeviceDisconnected) {
1118    globals.dispatch(
1119        Actions.setRecordingTarget({target: getDefaultRecordingTargets()[0]}));
1120  }
1121}
1122
1123function recordMenu(routePage: string) {
1124  const target = globals.state.recordingTarget;
1125  const chromeProbe =
1126      m('a[href="#!/record?p=chrome"]',
1127        m(`li${routePage === 'chrome' ? '.active' : ''}`,
1128          m('i.material-icons', 'laptop_chromebook'),
1129          m('.title', 'Chrome'),
1130          m('.sub', 'Chrome traces')));
1131  const recInProgress = globals.state.recordingInProgress;
1132
1133  return m(
1134      '.record-menu',
1135      {
1136        class: recInProgress ? 'disabled' : '',
1137        onclick: () => globals.rafScheduler.scheduleFullRedraw()
1138      },
1139      m('header', 'Trace config'),
1140      m('ul',
1141        m('a[href="#!/record?p=buffers"]',
1142          m(`li${routePage === 'buffers' ? '.active' : ''}`,
1143            m('i.material-icons', 'tune'),
1144            m('.title', 'Recording settings'),
1145            m('.sub', 'Buffer mode, size and duration'))),
1146        m('a[href="#!/record?p=instructions"]',
1147          m(`li${routePage === 'instructions' ? '.active' : ''}`,
1148            m('i.material-icons.rec', 'fiber_manual_record'),
1149            m('.title', 'Instructions'),
1150            m('.sub', 'Generate config and instructions')))),
1151      m('header', 'Probes'),
1152      m('ul', isChromeTarget(target) ? [chromeProbe] : [
1153        m('a[href="#!/record?p=cpu"]',
1154          m(`li${routePage === 'cpu' ? '.active' : ''}`,
1155            m('i.material-icons', 'subtitles'),
1156            m('.title', 'CPU'),
1157            m('.sub', 'CPU usage, scheduling, wakeups'))),
1158        m('a[href="#!/record?p=gpu"]',
1159          m(`li${routePage === 'gpu' ? '.active' : ''}`,
1160            m('i.material-icons', 'aspect_ratio'),
1161            m('.title', 'GPU'),
1162            m('.sub', 'GPU frequency'))),
1163        m('a[href="#!/record?p=power"]',
1164          m(`li${routePage === 'power' ? '.active' : ''}`,
1165            m('i.material-icons', 'battery_charging_full'),
1166            m('.title', 'Power'),
1167            m('.sub', 'Battery and other energy counters'))),
1168        m('a[href="#!/record?p=memory"]',
1169          m(`li${routePage === 'memory' ? '.active' : ''}`,
1170            m('i.material-icons', 'memory'),
1171            m('.title', 'Memory'),
1172            m('.sub', 'Physical mem, VM, LMK'))),
1173        m('a[href="#!/record?p=android"]',
1174          m(`li${routePage === 'android' ? '.active' : ''}`,
1175            m('i.material-icons', 'android'),
1176            m('.title', 'Android apps & svcs'),
1177            m('.sub', 'atrace and logcat'))),
1178        chromeProbe,
1179        m('a[href="#!/record?p=advanced"]',
1180          m(`li${routePage === 'advanced' ? '.active' : ''}`,
1181            m('i.material-icons', 'settings'),
1182            m('.title', 'Advanced settings'),
1183            m('.sub', 'Complicated stuff for wizards')))
1184      ]));
1185}
1186
1187
1188export const RecordPage = createPage({
1189  view() {
1190    const SECTIONS: {[property: string]: (cssClass: string) => m.Child} = {
1191      buffers: RecSettings,
1192      instructions: Instructions,
1193      cpu: CpuSettings,
1194      gpu: GpuSettings,
1195      power: PowerSettings,
1196      memory: MemorySettings,
1197      android: AndroidSettings,
1198      chrome: ChromeSettings,
1199      advanced: AdvancedSettings,
1200    };
1201
1202    const pages: m.Children = [];
1203    let routePage = Router.param('p');
1204    if (!Object.keys(SECTIONS).includes(routePage)) {
1205      routePage = 'buffers';
1206    }
1207    for (const key of Object.keys(SECTIONS)) {
1208      const cssClass = routePage === key ? '.active' : '';
1209      pages.push(SECTIONS[key](cssClass));
1210    }
1211
1212    return m(
1213        '.record-page',
1214        globals.state.recordingInProgress ? m('.hider') : [],
1215        m('.record-container', RecordHeader(), recordMenu(routePage), pages));
1216  }
1217});
1218