| // Copyright (C) 2018 The Android Open Source Project | 
 | // | 
 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
 | // you may not use this file except in compliance with the License. | 
 | // You may obtain a copy of the License at | 
 | // | 
 | //      http://www.apache.org/licenses/LICENSE-2.0 | 
 | // | 
 | // Unless required by applicable law or agreed to in writing, software | 
 | // distributed under the License is distributed on an "AS IS" BASIS, | 
 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 | // See the License for the specific language governing permissions and | 
 | // limitations under the License. | 
 |  | 
 | import {produce} from 'immer'; | 
 | import * as m from 'mithril'; | 
 |  | 
 | import {Actions} from '../common/actions'; | 
 | import {MeminfoCounters, VmstatCounters} from '../common/protos'; | 
 | import {RecordMode} from '../common/state'; | 
 |  | 
 | import {globals} from './globals'; | 
 | import {createPage} from './pages'; | 
 | import { | 
 |   CodeSnippet, | 
 |   Dropdown, | 
 |   DropdownAttrs, | 
 |   Probe, | 
 |   ProbeAttrs, | 
 |   Slider, | 
 |   SliderAttrs, | 
 |   Textarea, | 
 |   TextareaAttrs | 
 | } from './record_widgets'; | 
 | import {Router} from './router'; | 
 |  | 
 |  | 
 | const POLL_RATE_MS = [250, 500, 1000, 2500, 5000, 30000, 60000]; | 
 |  | 
 | const ATRACE_CATEGORIES = new Map<string, string>(); | 
 | ATRACE_CATEGORIES.set('gfx', 'Graphics'); | 
 | ATRACE_CATEGORIES.set('input', 'Input'); | 
 | ATRACE_CATEGORIES.set('view', 'View System'); | 
 | ATRACE_CATEGORIES.set('webview', 'WebView'); | 
 | ATRACE_CATEGORIES.set('wm', 'Window Manager'); | 
 | ATRACE_CATEGORIES.set('am', 'Activity Manager'); | 
 | ATRACE_CATEGORIES.set('sm', 'Sync Manager'); | 
 | ATRACE_CATEGORIES.set('audio', 'Audio'); | 
 | ATRACE_CATEGORIES.set('video', 'Video'); | 
 | ATRACE_CATEGORIES.set('camera', 'Camera'); | 
 | ATRACE_CATEGORIES.set('hal', 'Hardware Modules'); | 
 | ATRACE_CATEGORIES.set('res', 'Resource Loading'); | 
 | ATRACE_CATEGORIES.set('dalvik', 'ART & Dalvik'); | 
 | ATRACE_CATEGORIES.set('rs', 'RenderScript'); | 
 | ATRACE_CATEGORIES.set('bionic', 'Bionic C library'); | 
 | ATRACE_CATEGORIES.set('gfx', 'Graphics'); | 
 | ATRACE_CATEGORIES.set('power', 'Power Management'); | 
 | ATRACE_CATEGORIES.set('pm', 'Package Manager'); | 
 | ATRACE_CATEGORIES.set('ss', 'System Server'); | 
 | ATRACE_CATEGORIES.set('database', 'Database'); | 
 | ATRACE_CATEGORIES.set('network', 'Network'); | 
 | ATRACE_CATEGORIES.set('adb', 'ADB'); | 
 | ATRACE_CATEGORIES.set('vibrator', 'Vibrator'); | 
 | ATRACE_CATEGORIES.set('aidl', 'AIDL calls'); | 
 | ATRACE_CATEGORIES.set('nnapi', 'Neural Network API'); | 
 | ATRACE_CATEGORIES.set('rro', 'Resource Overlay'); | 
 |  | 
 | const LOG_BUFFERS = new Map<string, string>(); | 
 | LOG_BUFFERS.set('LID_RADIO', 'Radio'); | 
 | LOG_BUFFERS.set('LID_EVENTS', 'Binary events'); | 
 | LOG_BUFFERS.set('LID_SYSTEM', 'System'); | 
 | LOG_BUFFERS.set('LID_CRASH', 'Crash'); | 
 | LOG_BUFFERS.set('LID_SECURITY', 'Security'); | 
 | LOG_BUFFERS.set('LID_KERNEL', 'Kernel'); | 
 |  | 
 | const FTRACE_CATEGORIES = new Map<string, string>(); | 
 | FTRACE_CATEGORIES.set('binder/*', 'binder'); | 
 | FTRACE_CATEGORIES.set('block/*', 'block'); | 
 | FTRACE_CATEGORIES.set('clk/*', 'clk'); | 
 | FTRACE_CATEGORIES.set('ext4/*', 'ext4'); | 
 | FTRACE_CATEGORIES.set('f2fs/*', 'f2fs'); | 
 | FTRACE_CATEGORIES.set('i2c/*', 'i2c'); | 
 | FTRACE_CATEGORIES.set('irq/*', 'irq'); | 
 | FTRACE_CATEGORIES.set('kmem/*', 'kmem'); | 
 | FTRACE_CATEGORIES.set('memory_bus/*', 'memory_bus'); | 
 | FTRACE_CATEGORIES.set('mmc/*', 'mmc'); | 
 | FTRACE_CATEGORIES.set('oom/*', 'oom'); | 
 | FTRACE_CATEGORIES.set('power/*', 'power'); | 
 | FTRACE_CATEGORIES.set('regulator/*', 'regulator'); | 
 | FTRACE_CATEGORIES.set('sched/*', 'sched'); | 
 | FTRACE_CATEGORIES.set('sync/*', 'sync'); | 
 | FTRACE_CATEGORIES.set('task/*', 'task'); | 
 | FTRACE_CATEGORIES.set('task/*', 'task'); | 
 | FTRACE_CATEGORIES.set('vmscan/*', 'vmscan'); | 
 |  | 
 | function RecSettings(cssClass: string) { | 
 |   const S = (x: number) => x * 1000; | 
 |   const M = (x: number) => x * 1000 * 60; | 
 |   const H = (x: number) => x * 1000 * 60 * 60; | 
 |  | 
 |   const cfg = globals.state.recordConfig; | 
 |  | 
 |   const recButton = (mode: RecordMode, title: string, img: string) => { | 
 |     const checkboxArgs = { | 
 |       checked: cfg.mode === mode, | 
 |       onchange: m.withAttr( | 
 |           'checked', | 
 |           (checked: boolean) => { | 
 |             if (!checked) return; | 
 |             const traceCfg = produce(globals.state.recordConfig, draft => { | 
 |               draft.mode = mode; | 
 |             }); | 
 |             globals.dispatch(Actions.setRecordConfig({config: traceCfg})); | 
 |           }) | 
 |     }; | 
 |     return m( | 
 |         `label${cfg.mode === mode ? '.selected' : ''}`, | 
 |         m(`input[type=radio][name=rec_mode]`, checkboxArgs), | 
 |         m(`img[src=assets/${img}]`), | 
 |         m('span', title)); | 
 |   }; | 
 |  | 
 |   return m( | 
 |       `.record-section${cssClass}`, | 
 |       m('header', 'Recording mode'), | 
 |       m('.record-mode', | 
 |         recButton('STOP_WHEN_FULL', 'Stop when full', 'rec_one_shot.png'), | 
 |         recButton('RING_BUFFER', 'Ring buffer', 'rec_ring_buf.png'), | 
 |         recButton('LONG_TRACE', 'Long trace', 'rec_long_trace.png'), ), | 
 |  | 
 |       m(Slider, { | 
 |         title: 'In-memory buffer size', | 
 |         icon: '360', | 
 |         values: [4, 8, 16, 32, 64, 128, 256, 512], | 
 |         unit: 'MB', | 
 |         set: (cfg, val) => cfg.bufferSizeMb = val, | 
 |         get: (cfg) => cfg.bufferSizeMb | 
 |       } as SliderAttrs), | 
 |  | 
 |       m(Slider, { | 
 |         title: 'Max duration', | 
 |         icon: 'timer', | 
 |         values: [S(10), S(15), S(30), S(60), M(5), M(30), H(1), H(6), H(12)], | 
 |         isTime: true, | 
 |         unit: 'h:m:s', | 
 |         set: (cfg, val) => cfg.durationMs = val, | 
 |         get: (cfg) => cfg.durationMs | 
 |       } as SliderAttrs), | 
 |       m(Slider, { | 
 |         title: 'Max file size', | 
 |         icon: 'save', | 
 |         cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '', | 
 |         values: [5, 25, 50, 100, 500, 1000, 1000 * 5, 1000 * 10], | 
 |         unit: 'MB', | 
 |         set: (cfg, val) => cfg.maxFileSizeMb = val, | 
 |         get: (cfg) => cfg.maxFileSizeMb | 
 |       } as SliderAttrs), | 
 |       m(Slider, { | 
 |         title: 'Flush on disk every', | 
 |         cssClass: cfg.mode !== 'LONG_TRACE' ? '.hide' : '', | 
 |         icon: 'av_timer', | 
 |         values: [100, 250, 500, 1000, 2500, 5000], | 
 |         unit: 'ms', | 
 |         set: (cfg, val) => cfg.fileWritePeriodMs = val, | 
 |         get: (cfg) => cfg.fileWritePeriodMs || 0 | 
 |       } as SliderAttrs)); | 
 | } | 
 |  | 
 | function PowerSettings(cssClass: string) { | 
 |   return m( | 
 |       `.record-section${cssClass}`, | 
 |       m(Probe, | 
 |         { | 
 |           title: 'Battery drain', | 
 |           img: 'rec_battery_counters.png', | 
 |           descr: `Polls charge counters and instantaneous power draw from | 
 |                     the battery power management IC.`, | 
 |           setEnabled: (cfg, val) => cfg.batteryDrain = val, | 
 |           isEnabled: (cfg) => cfg.batteryDrain | 
 |         } as ProbeAttrs, | 
 |         m(Slider, { | 
 |           title: 'Poll rate', | 
 |           cssClass: '.thin', | 
 |           values: POLL_RATE_MS, | 
 |           unit: 'ms', | 
 |           set: (cfg, val) => cfg.batteryDrainPollMs = val, | 
 |           get: (cfg) => cfg.batteryDrainPollMs | 
 |         } as SliderAttrs)), | 
 |       m(Probe, { | 
 |         title: 'Board voltages & frequencies', | 
 |         img: 'rec_board_voltage.png', | 
 |         descr: 'Tracks voltage and frequency changes from board sensors', | 
 |         setEnabled: (cfg, val) => cfg.boardSensors = val, | 
 |         isEnabled: (cfg) => cfg.boardSensors | 
 |       } as ProbeAttrs)); | 
 | } | 
 |  | 
 | function CpuSettings(cssClass: string) { | 
 |   return m( | 
 |       `.record-section${cssClass}`, | 
 |       m(Probe, | 
 |         { | 
 |           title: 'Coarse CPU usage counter', | 
 |           img: 'rec_cpu_coarse.png', | 
 |           descr: `Lightweight polling of CPU usage counters via /proc/stat. | 
 |                     Allows to periodically monitor CPU usage.`, | 
 |           setEnabled: (cfg, val) => cfg.cpuCoarse = val, | 
 |           isEnabled: (cfg) => cfg.cpuCoarse | 
 |         } as ProbeAttrs, | 
 |         m(Slider, { | 
 |           title: 'Poll rate', | 
 |           cssClass: '.thin', | 
 |           values: POLL_RATE_MS, | 
 |           unit: 'ms', | 
 |           set: (cfg, val) => cfg.cpuCoarsePollMs = val, | 
 |           get: (cfg) => cfg.cpuCoarsePollMs | 
 |         } as SliderAttrs)), | 
 |       m(Probe, { | 
 |         title: 'Scheduling details', | 
 |         img: 'rec_cpu_fine.png', | 
 |         descr: 'Enables high-detailed tracking of scheduling events', | 
 |         setEnabled: (cfg, val) => cfg.cpuSched = val, | 
 |         isEnabled: (cfg) => cfg.cpuSched | 
 |       } as ProbeAttrs), | 
 |       m(Probe, { | 
 |         title: 'CPU frequency and idle states', | 
 |         img: 'rec_cpu_freq.png', | 
 |         descr: 'Records cpu frequency and idle state changes via ftrace', | 
 |         setEnabled: (cfg, val) => cfg.cpuFreq = val, | 
 |         isEnabled: (cfg) => cfg.cpuFreq | 
 |       } as ProbeAttrs), | 
 |       m(Probe, { | 
 |         title: 'Scheduling chains / latency analysis', | 
 |         img: 'rec_cpu_wakeup.png', | 
 |         descr: `Tracks causality of scheduling transitions. When a task | 
 |                 X transitions from blocked -> runnable, keeps track of the | 
 |                 task Y that X's transition (e.g. posting a semaphore).`, | 
 |         setEnabled: (cfg, val) => cfg.cpuLatency = val, | 
 |         isEnabled: (cfg) => cfg.cpuLatency | 
 |       } as ProbeAttrs)); | 
 | } | 
 |  | 
 | function MemorySettings(cssClass: string) { | 
 |   const meminfoOpts = new Map<string, string>(); | 
 |   for (const x in MeminfoCounters) { | 
 |     if (typeof MeminfoCounters[x] === 'number' && | 
 |         !`${x}`.endsWith('_UNSPECIFIED')) { | 
 |       meminfoOpts.set(x, x.replace('MEMINFO_', '').toLowerCase()); | 
 |     } | 
 |   } | 
 |   const vmstatOpts = new Map<string, string>(); | 
 |   for (const x in VmstatCounters) { | 
 |     if (typeof VmstatCounters[x] === 'number' && | 
 |         !`${x}`.endsWith('_UNSPECIFIED')) { | 
 |       vmstatOpts.set(x, x.replace('VMSTAT_', '').toLowerCase()); | 
 |     } | 
 |   } | 
 |   return m( | 
 |       `.record-section${cssClass}`, | 
 |       m(Probe, | 
 |         { | 
 |           title: 'Kernel meminfo', | 
 |           img: 'rec_meminfo.png', | 
 |           descr: 'Polling of /proc/meminfo', | 
 |           setEnabled: (cfg, val) => cfg.meminfo = val, | 
 |           isEnabled: (cfg) => cfg.meminfo | 
 |         } as ProbeAttrs, | 
 |         m(Slider, { | 
 |           title: 'Poll rate', | 
 |           cssClass: '.thin', | 
 |           values: POLL_RATE_MS, | 
 |           unit: 'ms', | 
 |           set: (cfg, val) => cfg.meminfoPeriodMs = val, | 
 |           get: (cfg) => cfg.meminfoPeriodMs | 
 |         } as SliderAttrs), | 
 |         m(Dropdown, { | 
 |           title: 'Select counters', | 
 |           cssClass: '.multicolumn', | 
 |           options: meminfoOpts, | 
 |           set: (cfg, val) => cfg.meminfoCounters = val, | 
 |           get: (cfg) => cfg.meminfoCounters | 
 |         } as DropdownAttrs)), | 
 |       m(Probe, { | 
 |         title: 'High-frequency memory events', | 
 |         img: 'rec_mem_hifreq.png', | 
 |         descr: `Allows to track short memory spikes and transitories through | 
 |                 ftrace's mm_event, rss_stat and ion events. Avialable only | 
 |                 on recent Android Q+ kernels`, | 
 |         setEnabled: (cfg, val) => cfg.memHiFreq = val, | 
 |         isEnabled: (cfg) => cfg.memHiFreq | 
 |       } as ProbeAttrs), | 
 |       m(Probe, { | 
 |         title: 'Low memory killer', | 
 |         img: 'rec_lmk.png', | 
 |         descr: `Record LMK events. Works both with the old in-kernel LMK | 
 |                 and the newer userspace lmkd. It also tracks OOM score | 
 |                 adjustments.`, | 
 |         setEnabled: (cfg, val) => cfg.memLmk = val, | 
 |         isEnabled: (cfg) => cfg.memLmk | 
 |       } as ProbeAttrs), | 
 |       m(Probe, | 
 |         { | 
 |           title: 'Per process stats', | 
 |           img: 'rec_ps_stats.png', | 
 |           descr: `Periodically samples all processes in the system tracking: | 
 |                     their thread list, memory counters (RSS, swap and other | 
 |                     /proc/status counters) and oom_score_adj.`, | 
 |           setEnabled: (cfg, val) => cfg.procStats = val, | 
 |           isEnabled: (cfg) => cfg.procStats | 
 |         } as ProbeAttrs, | 
 |         m(Slider, { | 
 |           title: 'Poll rate', | 
 |           cssClass: '.thin', | 
 |           values: POLL_RATE_MS, | 
 |           unit: 'ms', | 
 |           set: (cfg, val) => cfg.procStatsPeriodMs = val, | 
 |           get: (cfg) => cfg.procStatsPeriodMs | 
 |         } as SliderAttrs)), | 
 |       m(Probe, | 
 |         { | 
 |           title: 'Virtual memory stats', | 
 |           img: 'rec_vmstat.png', | 
 |           descr: `Periodically polls virtual memory stats from /proc/vmstat. | 
 |                     Allows to gather statistics about swap, eviction, | 
 |                     compression and pagecache efficiency`, | 
 |           setEnabled: (cfg, val) => cfg.vmstat = val, | 
 |           isEnabled: (cfg) => cfg.vmstat | 
 |         } as ProbeAttrs, | 
 |         m(Slider, { | 
 |           title: 'Poll rate', | 
 |           cssClass: '.thin', | 
 |           values: POLL_RATE_MS, | 
 |           unit: 'ms', | 
 |           set: (cfg, val) => cfg.vmstatPeriodMs = val, | 
 |           get: (cfg) => cfg.vmstatPeriodMs | 
 |         } as SliderAttrs), | 
 |         m(Dropdown, { | 
 |           title: 'Select counters', | 
 |           cssClass: '.multicolumn', | 
 |           options: vmstatOpts, | 
 |           set: (cfg, val) => cfg.vmstatCounters = val, | 
 |           get: (cfg) => cfg.vmstatCounters | 
 |         } as DropdownAttrs))); | 
 | } | 
 |  | 
 |  | 
 | function AndroidSettings(cssClass: string) { | 
 |   return m( | 
 |       `.record-section${cssClass}`, | 
 |       m(Probe, | 
 |         { | 
 |           title: 'Atrace userspace annotations', | 
 |           img: 'rec_atrace.png', | 
 |           descr: `Enables C++ / Java codebase annotations (ATRACE_BEGIN() / | 
 |                     os.Trace())`, | 
 |           setEnabled: (cfg, val) => cfg.atrace = val, | 
 |           isEnabled: (cfg) => cfg.atrace | 
 |         } as ProbeAttrs, | 
 |         m(Dropdown, { | 
 |           title: 'Categories', | 
 |           cssClass: '.multicolumn.atrace-categories', | 
 |           options: ATRACE_CATEGORIES, | 
 |           set: (cfg, val) => cfg.atraceCats = val, | 
 |           get: (cfg) => cfg.atraceCats | 
 |         } as DropdownAttrs), | 
 |         m(Textarea, { | 
 |           placeholder: 'Extra apps to profile, one per line, e.g.:\n' + | 
 |               'com.android.phone\n' + | 
 |               'com.android.nfc', | 
 |           set: (cfg, val) => cfg.atraceApps = val, | 
 |           get: (cfg) => cfg.atraceApps | 
 |         } as TextareaAttrs)), | 
 |       m(Probe, | 
 |         { | 
 |           title: 'Event log (logcat)', | 
 |           img: 'rec_logcat.png', | 
 |           descr: `Streams the event log into the trace. If no buffer filter is | 
 |                     specified, all buffers are selected.`, | 
 |           setEnabled: (cfg, val) => cfg.androidLogs = val, | 
 |           isEnabled: (cfg) => cfg.androidLogs | 
 |         } as ProbeAttrs, | 
 |         m(Dropdown, { | 
 |           title: 'Buffers', | 
 |           options: LOG_BUFFERS, | 
 |           set: (cfg, val) => cfg.androidLogBuffers = val, | 
 |           get: (cfg) => cfg.androidLogBuffers | 
 |         } as DropdownAttrs), )); | 
 | } | 
 |  | 
 |  | 
 | function AdvancedSettings(cssClass: string) { | 
 |   return m( | 
 |       `.record-section${cssClass}`, | 
 |       m(Probe, | 
 |         { | 
 |           title: 'Advanced ftrace config', | 
 |           img: 'rec_ftrace.png', | 
 |           descr: `Tunes the kernel-tracing (ftrace) module and allows to | 
 |                     enable extra events. The events enabled here are on top | 
 |                     of the ones derived when enabling the other probes.`, | 
 |           setEnabled: (cfg, val) => cfg.ftrace = val, | 
 |           isEnabled: (cfg) => cfg.ftrace | 
 |         } as ProbeAttrs, | 
 |         m(Slider, { | 
 |           title: 'Buf size', | 
 |           cssClass: '.thin', | 
 |           values: [512, 1024, 2 * 1024, 4 * 1024, 16 * 1024, 32 * 1024], | 
 |           unit: 'KB', | 
 |           set: (cfg, val) => cfg.ftraceBufferSizeKb = val, | 
 |           get: (cfg) => cfg.ftraceBufferSizeKb | 
 |         } as SliderAttrs), | 
 |         m(Slider, { | 
 |           title: 'Drain rate', | 
 |           cssClass: '.thin', | 
 |           values: [100, 250, 500, 1000, 2500, 5000], | 
 |           unit: 'ms', | 
 |           set: (cfg, val) => cfg.ftraceDrainPeriodMs = val, | 
 |           get: (cfg) => cfg.ftraceDrainPeriodMs | 
 |         } as SliderAttrs), | 
 |         m(Dropdown, { | 
 |           title: 'Event groups', | 
 |           cssClass: '.multicolumn.ftrace-events', | 
 |           options: FTRACE_CATEGORIES, | 
 |           set: (cfg, val) => cfg.ftraceEvents = val, | 
 |           get: (cfg) => cfg.ftraceEvents | 
 |         } as DropdownAttrs), | 
 |         m(Textarea, { | 
 |           placeholder: 'Add extra events, one per line, e.g.:\n' + | 
 |               'sched/sched_switch\n' + | 
 |               'kmem/*', | 
 |           set: (cfg, val) => cfg.ftraceExtraEvents = val, | 
 |           get: (cfg) => cfg.ftraceExtraEvents | 
 |         } as TextareaAttrs))); | 
 | } | 
 |  | 
 | function Instructions(cssClass: string) { | 
 |   const data = globals.trackDataStore.get('config') as { | 
 |     commandline: string, | 
 |     pbtxt: string, | 
 |   } | null; | 
 |  | 
 |   const pbtx = data ? data.pbtxt : ''; | 
 |   let cmd = ''; | 
 |   cmd += 'adb shell perfetto \\\n'; | 
 |   cmd += '  -c - --txt \\\n'; | 
 |   cmd += '  -o /data/misc/perfetto-traces/trace \\\n'; | 
 |   cmd += '<<EOF\n\n'; | 
 |   cmd += pbtx; | 
 |   cmd += '\nEOF\n'; | 
 |   const docUrl = '//docs.perfetto.dev/#/build-instructions?id=get-the-code'; | 
 |  | 
 |  | 
 |   const notes: m.Children = []; | 
 |   const doc = | 
 |       m('span', 'Follow the ', m('a', {href: docUrl}, 'instructions here')); | 
 |  | 
 |   const msgFeatNotSupported = | 
 |       m('div', `Some of the probes are only supported in the | 
 |       last version of perfetto running on Android Q+`); | 
 |  | 
 |   const msgPerfettoNotSupported = | 
 |       m('div', `Perfetto is not supported natively before Android P.`); | 
 |  | 
 |   const msgSideload = | 
 |       m('div', | 
 |         `If you have a rooted device you can sideload the latest version of | 
 |          perfetto. `, | 
 |         doc); | 
 |  | 
 |   const msgLinux = | 
 |       m('div', `In order to use perfetto on Linux you need to | 
 |       compile it and run from the standalone build. `, doc); | 
 |  | 
 |   switch (globals.state.recordConfig.targetOS) { | 
 |     case 'Q': | 
 |       break; | 
 |     case 'P': | 
 |       notes.push(msgFeatNotSupported); | 
 |       notes.push(msgSideload); | 
 |       break; | 
 |     case 'O': | 
 |       notes.push(msgPerfettoNotSupported); | 
 |       notes.push(msgSideload); | 
 |       break; | 
 |     case 'L': | 
 |       notes.push(msgLinux); | 
 |       break; | 
 |     default: | 
 |   } | 
 |  | 
 |   const onOsChange = (os: string) => { | 
 |     const traceCfg = produce(globals.state.recordConfig, draft => { | 
 |       draft.targetOS = os; | 
 |     }); | 
 |     globals.dispatch(Actions.setRecordConfig({config: traceCfg})); | 
 |   }; | 
 |  | 
 |   return m( | 
 |       `.record-section.instructions${cssClass}`, | 
 |       m('header', 'Instructions'), | 
 |       m('label', | 
 |         'Select target platform', | 
 |         m('select', | 
 |           {onchange: m.withAttr('value', onOsChange)}, | 
 |           m('option', {value: 'Q'}, 'Android Q+'), | 
 |           m('option', {value: 'P'}, 'Android P'), | 
 |           m('option', {value: 'O'}, 'Android O-'), | 
 |           m('option', {value: 'L'}, 'Linux desktop'))), | 
 |       notes.length > 0 ? m('.note', notes) : [], | 
 |       m(CodeSnippet, {text: cmd, hardWhitespace: true}), ); | 
 | } | 
 |  | 
 | export const RecordPage = createPage({ | 
 |   view() { | 
 |     const SECTIONS: {[property: string]: (cssClass: string) => m.Child} = { | 
 |       buffers: RecSettings, | 
 |       instructions: Instructions, | 
 |       cpu: CpuSettings, | 
 |       power: PowerSettings, | 
 |       memory: MemorySettings, | 
 |       android: AndroidSettings, | 
 |       advanced: AdvancedSettings, | 
 |     }; | 
 |  | 
 |     const pages: m.Children = []; | 
 |     let routePage = Router.param('p'); | 
 |     if (!Object.keys(SECTIONS).includes(routePage)) { | 
 |       routePage = 'buffers'; | 
 |     } | 
 |     for (const key of Object.keys(SECTIONS)) { | 
 |       const cssClass = routePage === key ? '.active' : ''; | 
 |       pages.push(SECTIONS[key](cssClass)); | 
 |     } | 
 |  | 
 |     return m( | 
 |         '.record-page', | 
 |         m('.record-container', | 
 |           m('.record-menu', | 
 |             m('header', 'Trace config'), | 
 |             m('ul', | 
 |               m('a[href="#!/record?p=buffers"]', | 
 |                 m(`li${routePage === 'buffers' ? '.active' : ''}`, | 
 |                   m('i.material-icons', 'tune'), | 
 |                   m('.title', 'Recording settings'), | 
 |                   m('.sub', 'Buffer mode, size and duration'))), | 
 |               m('a[href="#!/record?p=instructions"]', | 
 |                 m(`li${routePage === 'instructions' ? '.active' : ''}`, | 
 |                   m('i.material-icons.rec', 'fiber_manual_record'), | 
 |                   m('.title', 'Start recording'), | 
 |                   m('.sub', 'Generate config and instructions'))), ), | 
 |             m('header', 'Probes'), | 
 |             m('ul', | 
 |               m('a[href="#!/record?p=cpu"]', | 
 |                 m(`li${routePage === 'cpu' ? '.active' : ''}`, | 
 |                   m('i.material-icons', 'subtitles'), | 
 |                   m('.title', 'CPU'), | 
 |                   m('.sub', 'CPU usage, scheduling, wakeups'))), | 
 |               m('a[href="#!/record?p=power"]', | 
 |                 m(`li${routePage === 'power' ? '.active' : ''}`, | 
 |                   m('i.material-icons', 'battery_charging_full'), | 
 |                   m('.title', 'Power'), | 
 |                   m('.sub', 'Battery and other energy counters'))), | 
 |               m('a[href="#!/record?p=memory"]', | 
 |                 m(`li${routePage === 'memory' ? '.active' : ''}`, | 
 |                   m('i.material-icons', 'memory'), | 
 |                   m('.title', 'Memory'), | 
 |                   m('.sub', 'Physical mem, VM, LMK'))), | 
 |               m('a[href="#!/record?p=android"]', | 
 |                 m(`li${routePage === 'android' ? '.active' : ''}`, | 
 |                   m('i.material-icons', 'android'), | 
 |                   m('.title', 'Android apps & svcs'), | 
 |                   m('.sub', 'atrace and logcat'))), | 
 |               m('a[href="#!/record?p=advanced"]', | 
 |                 m(`li${routePage === 'advanced' ? '.active' : ''}`, | 
 |                   m('i.material-icons', 'settings'), | 
 |                   m('.title', 'Advanced settings'), | 
 |                   m('.sub', 'Complicated stuff for wizards'))), )), | 
 |           pages)); | 
 |   } | 
 | }); |