// 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 { AdbRecordingTarget, getBuiltinChromeCategoryList, getDefaultRecordingTargets, hasActiveProbes, isAdbTarget, isAndroidP, isAndroidTarget, isChromeTarget, isCrOSTarget, RecordingTarget } from '../common/state'; import {MAX_TIME, RecordMode} from '../common/state'; import {AdbOverWebUsb} from '../controller/adb'; import {globals} from './globals'; import {createPage} from './pages'; import {recordConfigStore} from './record_config'; import { CodeSnippet, Dropdown, DropdownAttrs, Probe, ProbeAttrs, Slider, SliderAttrs, Textarea, TextareaAttrs, Toggle, ToggleAttrs } from './record_widgets'; import {Router} from './router'; const LOCAL_STORAGE_SHOW_CONFIG = 'showConfigs'; const POLL_INTERVAL_MS = [250, 500, 1000, 2500, 5000, 30000, 60000]; const ATRACE_CATEGORIES = new Map(); ATRACE_CATEGORIES.set('adb', 'ADB'); ATRACE_CATEGORIES.set('aidl', 'AIDL calls'); ATRACE_CATEGORIES.set('am', 'Activity Manager'); ATRACE_CATEGORIES.set('audio', 'Audio'); ATRACE_CATEGORIES.set('binder_driver', 'Binder Kernel driver'); ATRACE_CATEGORIES.set('binder_lock', 'Binder global lock trace'); ATRACE_CATEGORIES.set('bionic', 'Bionic C library'); ATRACE_CATEGORIES.set('camera', 'Camera'); ATRACE_CATEGORIES.set('dalvik', 'ART & Dalvik'); ATRACE_CATEGORIES.set('database', 'Database'); ATRACE_CATEGORIES.set('gfx', 'Graphics'); ATRACE_CATEGORIES.set('hal', 'Hardware Modules'); ATRACE_CATEGORIES.set('input', 'Input'); ATRACE_CATEGORIES.set('network', 'Network'); ATRACE_CATEGORIES.set('nnapi', 'Neural Network API'); ATRACE_CATEGORIES.set('pm', 'Package Manager'); ATRACE_CATEGORIES.set('power', 'Power Management'); ATRACE_CATEGORIES.set('res', 'Resource Loading'); ATRACE_CATEGORIES.set('rro', 'Resource Overlay'); ATRACE_CATEGORIES.set('rs', 'RenderScript'); ATRACE_CATEGORIES.set('sm', 'Sync Manager'); ATRACE_CATEGORIES.set('ss', 'System Server'); ATRACE_CATEGORIES.set('vibrator', 'Vibrator'); ATRACE_CATEGORIES.set('video', 'Video'); ATRACE_CATEGORIES.set('view', 'View System'); ATRACE_CATEGORIES.set('webview', 'WebView'); ATRACE_CATEGORIES.set('wm', 'Window Manager'); const LOG_BUFFERS = new Map(); LOG_BUFFERS.set('LID_CRASH', 'Crash'); LOG_BUFFERS.set('LID_DEFAULT', 'Main'); LOG_BUFFERS.set('LID_EVENTS', 'Binary events'); LOG_BUFFERS.set('LID_KERNEL', 'Kernel'); LOG_BUFFERS.set('LID_RADIO', 'Radio'); LOG_BUFFERS.set('LID_SECURITY', 'Security'); LOG_BUFFERS.set('LID_STATS', 'Stats'); LOG_BUFFERS.set('LID_SYSTEM', 'System'); const FTRACE_CATEGORIES = new Map(); 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'); FTRACE_CATEGORIES.set('fastrpc/*', 'fastrpc'); 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: (e: InputEvent) => { const checked = (e.target as HTMLInputElement).checked; 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=${globals.root}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) { const DOC_URL = 'https://perfetto.dev/docs/data-sources/battery-counters'; const descr = [m('div', m('span', `Polls charge counters and instantaneous power draw from the battery power management IC and the power rails from the PowerStats HAL (`), m('a', {href: DOC_URL, target: '_blank'}, 'see docs for more'), m('span', ')'))]; if (globals.isInternalUser) { descr.push(m( 'div', m('span', 'Googlers: See '), m('a', {href: 'http://go/power-rails-internal-doc', target: '_blank'}, 'this doc'), m('span', ` for instructions on how to change the refault rail selection on internal devices.`), )); } return m( `.record-section${cssClass}`, m(Probe, { title: 'Battery drain & power rails', img: 'rec_battery_counters.png', descr, setEnabled: (cfg, val) => cfg.batteryDrain = val, isEnabled: (cfg) => cfg.batteryDrain } as ProbeAttrs, m(Slider, { title: 'Poll interval', cssClass: '.thin', values: POLL_INTERVAL_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 GpuSettings(cssClass: string) { return m( `.record-section${cssClass}`, m(Probe, { title: 'GPU frequency', img: 'rec_cpu_freq.png', descr: 'Records gpu frequency via ftrace', setEnabled: (cfg, val) => cfg.gpuFreq = val, isEnabled: (cfg) => cfg.gpuFreq } as ProbeAttrs), m(Probe, { title: 'GPU memory', img: 'rec_gpu_mem_total.png', descr: `Allows to track per process and global total GPU memory usages. (Available on recent Android 12+ kernels)`, setEnabled: (cfg, val) => cfg.gpuMemTotal = val, isEnabled: (cfg) => cfg.gpuMemTotal } 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 interval', cssClass: '.thin', values: POLL_INTERVAL_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: 'Syscalls', img: 'rec_syscalls.png', descr: `Tracks the enter and exit of all syscalls. On Android requires a userdebug or eng build.`, setEnabled: (cfg, val) => cfg.cpuSyscall = val, isEnabled: (cfg) => cfg.cpuSyscall } as ProbeAttrs)); } function HeapSettings(cssClass: string) { const valuesForMS = [ 0, 1000, 10 * 1000, 30 * 1000, 60 * 1000, 5 * 60 * 1000, 10 * 60 * 1000, 30 * 60 * 1000, 60 * 60 * 1000 ]; const valuesForShMemBuff = [ 0, 512, 1024, 2 * 1024, 4 * 1024, 8 * 1024, 16 * 1024, 32 * 1024, 64 * 1024, 128 * 1024, 256 * 1024, 512 * 1024, 1024 * 1024, 64 * 1024 * 1024, 128 * 1024 * 1024, 256 * 1024 * 1024, 512 * 1024 * 1024 ]; return m( `.${cssClass}`, m(Textarea, { title: 'Names or pids of the processes to track', docsLink: 'https://perfetto.dev/docs/data-sources/native-heap-profiler#heapprofd-targets', placeholder: 'One per line, e.g.:\n' + 'system_server\n' + 'com.google.android.apps.photos\n' + '1503', set: (cfg, val) => cfg.hpProcesses = val, get: (cfg) => cfg.hpProcesses } as TextareaAttrs), m(Slider, { title: 'Sampling interval', cssClass: '.thin', values: [ 0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576 ], unit: 'B', min: 0, set: (cfg, val) => cfg.hpSamplingIntervalBytes = val, get: (cfg) => cfg.hpSamplingIntervalBytes } as SliderAttrs), m(Slider, { title: 'Continuous dumps interval ', description: 'Time between following dumps (0 = disabled)', cssClass: '.thin', values: valuesForMS, unit: 'ms', min: 0, set: (cfg, val) => { cfg.hpContinuousDumpsInterval = val; }, get: (cfg) => cfg.hpContinuousDumpsInterval } as SliderAttrs), m(Slider, { title: 'Continuous dumps phase', description: 'Time before first dump', cssClass: `.thin${ globals.state.recordConfig.hpContinuousDumpsInterval === 0 ? '.greyed-out' : ''}`, values: valuesForMS, unit: 'ms', min: 0, disabled: globals.state.recordConfig.hpContinuousDumpsInterval === 0, set: (cfg, val) => cfg.hpContinuousDumpsPhase = val, get: (cfg) => cfg.hpContinuousDumpsPhase } as SliderAttrs), m(Slider, { title: `Shared memory buffer`, cssClass: '.thin', values: valuesForShMemBuff.filter( value => value === 0 || value >= 8192 && value % 4096 === 0), unit: 'B', min: 0, set: (cfg, val) => cfg.hpSharedMemoryBuffer = val, get: (cfg) => cfg.hpSharedMemoryBuffer } as SliderAttrs), m(Toggle, { title: 'Block client', cssClass: '.thin', descr: `Slow down target application if profiler cannot keep up.`, setEnabled: (cfg, val) => cfg.hpBlockClient = val, isEnabled: (cfg) => cfg.hpBlockClient } as ToggleAttrs), m(Toggle, { title: 'All custom allocators (Q+)', cssClass: '.thin', descr: `If the target application exposes custom allocators, also sample from those.`, setEnabled: (cfg, val) => cfg.hpAllHeaps = val, isEnabled: (cfg) => cfg.hpAllHeaps } as ToggleAttrs) // TODO(hjd): Add advanced options. ); } function JavaHeapDumpSettings(cssClass: string) { const valuesForMS = [ 0, 1000, 10 * 1000, 30 * 1000, 60 * 1000, 5 * 60 * 1000, 10 * 60 * 1000, 30 * 60 * 1000, 60 * 60 * 1000 ]; return m( `.${cssClass}`, m(Textarea, { title: 'Names or pids of the processes to track', placeholder: 'One per line, e.g.:\n' + 'com.android.vending\n' + '1503', set: (cfg, val) => cfg.jpProcesses = val, get: (cfg) => cfg.jpProcesses } as TextareaAttrs), m(Slider, { title: 'Continuous dumps interval ', description: 'Time between following dumps (0 = disabled)', cssClass: '.thin', values: valuesForMS, unit: 'ms', min: 0, set: (cfg, val) => { cfg.jpContinuousDumpsInterval = val; }, get: (cfg) => cfg.jpContinuousDumpsInterval } as SliderAttrs), m(Slider, { title: 'Continuous dumps phase', description: 'Time before first dump', cssClass: `.thin${ globals.state.recordConfig.jpContinuousDumpsInterval === 0 ? '.greyed-out' : ''}`, values: valuesForMS, unit: 'ms', min: 0, disabled: globals.state.recordConfig.jpContinuousDumpsInterval === 0, set: (cfg, val) => cfg.jpContinuousDumpsPhase = val, get: (cfg) => cfg.jpContinuousDumpsPhase } as SliderAttrs), ); } function MemorySettings(cssClass: string) { const meminfoOpts = new Map(); for (const x in MeminfoCounters) { if (typeof MeminfoCounters[x] === 'number' && !`${x}`.endsWith('_UNSPECIFIED')) { meminfoOpts.set(x, x.replace('MEMINFO_', '').toLowerCase()); } } const vmstatOpts = new Map(); 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: 'Native heap profiling', img: 'rec_native_heap_profiler.png', descr: `Track native heap allocations & deallocations of an Android process. (Available on Android 10+)`, setEnabled: (cfg, val) => cfg.heapProfiling = val, isEnabled: (cfg) => cfg.heapProfiling } as ProbeAttrs, HeapSettings(cssClass)), m(Probe, { title: 'Java heap dumps', img: 'rec_java_heap_dump.png', descr: `Dump information about the Java object graph of an Android app. (Available on Android 11+)`, setEnabled: (cfg, val) => cfg.javaHeapDump = val, isEnabled: (cfg) => cfg.javaHeapDump } as ProbeAttrs, JavaHeapDumpSettings(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 interval', cssClass: '.thin', values: POLL_INTERVAL_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. Available 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 interval', cssClass: '.thin', values: POLL_INTERVAL_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 interval', cssClass: '.thin', values: POLL_INTERVAL_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', cssClass: '.multicolumn', options: LOG_BUFFERS, set: (cfg, val) => cfg.androidLogBuffers = val, get: (cfg) => cfg.androidLogBuffers } as DropdownAttrs)), m(Probe, { title: 'Frame timeline', img: 'rec_frame_timeline.png', descr: `Records expected/actual frame timings from surface_flinger. Requires Android 12 (S) or above.`, setEnabled: (cfg, val) => cfg.androidFrameTimeline = val, isEnabled: (cfg) => cfg.androidFrameTimeline } as ProbeAttrs)); } function ChromeSettings(cssClass: string) { return m( `.record-section${cssClass}`, m(Probe, { title: 'Task scheduling', img: null, descr: `Records events about task scheduling and execution on all threads`, setEnabled: (cfg, val) => cfg.taskScheduling = val, isEnabled: (cfg) => cfg.taskScheduling } as ProbeAttrs), m(Probe, { title: 'IPC flows', img: null, descr: `Records flow events for passing of IPC messages between processes.`, setEnabled: (cfg, val) => cfg.ipcFlows = val, isEnabled: (cfg) => cfg.ipcFlows } as ProbeAttrs), m(Probe, { title: 'Javascript execution', img: null, descr: `Records events about Javascript execution in the renderer processes.`, setEnabled: (cfg, val) => cfg.jsExecution = val, isEnabled: (cfg) => cfg.jsExecution } as ProbeAttrs), m(Probe, { title: 'Web content rendering', img: null, descr: `Records events about rendering, layout, and compositing of web content in Blink.`, setEnabled: (cfg, val) => cfg.webContentRendering = val, isEnabled: (cfg) => cfg.webContentRendering } as ProbeAttrs), m(Probe, { title: 'UI rendering & compositing', img: null, descr: `Records events about rendering of browser UI surfaces and compositing of surfaces.`, setEnabled: (cfg, val) => cfg.uiRendering = val, isEnabled: (cfg) => cfg.uiRendering } as ProbeAttrs), m(Probe, { title: 'Input events', img: null, descr: `Records input events and their flow between processes.`, setEnabled: (cfg, val) => cfg.inputEvents = val, isEnabled: (cfg) => cfg.inputEvents } as ProbeAttrs), m(Probe, { title: 'Navigation & Loading', img: null, descr: `Records network events for navigations and resources.`, setEnabled: (cfg, val) => cfg.navigationAndLoading = val, isEnabled: (cfg) => cfg.navigationAndLoading } as ProbeAttrs), m(Probe, { title: 'Chrome Logs', img: null, descr: `Records Chrome log messages`, setEnabled: (cfg, val) => cfg.chromeLogs = val, isEnabled: (cfg) => cfg.chromeLogs } as ProbeAttrs), ChromeCategoriesSelection()); } function ChromeCategoriesSelection() { // If we are attempting to record via the Chrome extension, we receive the // list of actually supported categories via DevTools. Otherwise, we fall back // to an integrated list of categories from a recent version of Chrome. let categories = globals.state.chromeCategories; if (!categories || !isChromeTarget(globals.state.recordingTarget)) { categories = getBuiltinChromeCategoryList(); } // Show "disabled-by-default" categories last. const categoriesMap = new Map(); const disabledByDefaultCategories: string[] = []; const disabledPrefix = 'disabled-by-default-'; categories.forEach(cat => { if (cat.startsWith(disabledPrefix)) { disabledByDefaultCategories.push(cat); } else { categoriesMap.set(cat, cat); } }); disabledByDefaultCategories.forEach(cat => { categoriesMap.set( cat, `${cat.replace(disabledPrefix, '')} (high overhead)`); }); return m(Dropdown, { title: 'Additional Chrome categories', cssClass: '.multicolumn.two-columns', options: categoriesMap, set: (cfg, val) => cfg.chromeCategoriesSelected = val, get: (cfg) => cfg.chromeCategoriesSelected } as DropdownAttrs); } function AdvancedSettings(cssClass: string) { const S = (x: number) => x * 1000; const M = (x: number) => x * 1000 * 60; return m( `.record-section${cssClass}`, m(Probe, { title: 'Advanced ftrace config', img: 'rec_ftrace.png', descr: `Enable individual events and tune the kernel-tracing (ftrace) module. The events enabled here are in addition to those from enabled by 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)), globals.state.videoEnabled ? m(Probe, { title: 'Screen recording', img: null, descr: `Records the screen along with running a trace. Max time of recording is 3 minutes (180 seconds).`, setEnabled: (cfg, val) => cfg.screenRecord = val, isEnabled: (cfg) => cfg.screenRecord, } as ProbeAttrs, m(Slider, { title: 'Max duration', icon: 'timer', values: [S(10), S(15), S(30), S(60), M(2), M(3)], isTime: true, unit: 'm:s', set: (cfg, val) => cfg.durationMs = val, get: (cfg) => cfg.durationMs, } as SliderAttrs)) : null); } function RecordHeader() { return m( '.record-header', m('.top-part', m('.target-and-status', RecordingPlatformSelection(), RecordingStatusLabel(), ErrorLabel()), recordingButtons()), RecordingNotes()); } function RecordingPlatformSelection() { if (globals.state.recordingInProgress) return []; const availableAndroidDevices = globals.state.availableAdbDevices; const recordingTarget = globals.state.recordingTarget; const targets = []; for (const {os, name} of getDefaultRecordingTargets()) { targets.push(m('option', {value: os}, name)); } for (const d of availableAndroidDevices) { targets.push(m('option', {value: d.serial}, d.name)); } const selectedIndex = isAdbTarget(recordingTarget) ? targets.findIndex(node => node.attrs.value === recordingTarget.serial) : targets.findIndex(node => node.attrs.value === recordingTarget.os); return m( '.target', m( 'label', 'Target platform:', m('select', { selectedIndex, onchange: (e: Event) => { onTargetChange((e.target as HTMLSelectElement).value); }, onupdate: (select) => { // Work around mithril bug // (https://github.com/MithrilJS/mithril.js/issues/2107): We may // update the select's options while also changing the // selectedIndex at the same time. The update of selectedIndex // may be applied before the new options are added to the select // element. Because the new selectedIndex may be outside of the // select's options at that time, we have to reselect the // correct index here after any new children were added. (select.dom as HTMLSelectElement).selectedIndex = selectedIndex; } }, ...targets), ), m('.chip', {onclick: addAndroidDevice}, m('button', 'Add ADB Device'), m('i.material-icons', 'add'))); } // |target| can be the TargetOs or the android serial. function onTargetChange(target: string) { const recordingTarget: RecordingTarget = globals.state.availableAdbDevices.find(d => d.serial === target) || getDefaultRecordingTargets().find(t => t.os === target) || getDefaultRecordingTargets()[0]; if (isChromeTarget(recordingTarget)) { globals.dispatch(Actions.setUpdateChromeCategories({update: true})); } globals.dispatch(Actions.setRecordingTarget({target: recordingTarget})); globals.rafScheduler.scheduleFullRedraw(); } function Instructions(cssClass: string) { return m( `.record-section.instructions${cssClass}`, m('header', 'Recording command'), localStorage.hasOwnProperty(LOCAL_STORAGE_SHOW_CONFIG) ? m('button.permalinkconfig', { onclick: () => { globals.dispatch( Actions.createPermalink({isRecordingConfig: true})); }, }, 'Share recording settings') : null, RecordingSnippet(), BufferUsageProgressBar(), m('.buttons', StopCancelButtons()), recordingLog()); } function displayRecordConfigs() { return recordConfigStore.recordConfigs.map((item) => { return m('.config', [ m('span.title-config', item.title), m('button', { class: 'config-button load', onclick: () => { globals.dispatch(Actions.setRecordConfig({config: item.config})); globals.rafScheduler.scheduleFullRedraw(); } }, 'load'), m('button', { class: 'config-button delete', onclick: () => { recordConfigStore.delete(item.key); globals.rafScheduler.scheduleFullRedraw(); } }, 'delete'), ]); }); } function getSavedConfigList() { if (recordConfigStore.recordConfigs.length === 0) { return []; } return displayRecordConfigs(); } export const ConfigTitleState = { title: '', getTitle: () => { return ConfigTitleState.title; }, setTitle: (value: string) => { ConfigTitleState.title = value; }, clearTitle: () => { ConfigTitleState.title = ''; } }; function Configurations(cssClass: string) { return m( `.record-section${cssClass}`, m('header', 'Save and load configurations'), m('.input-config', [ m('input', { value: ConfigTitleState.title, placeholder: 'Title for config', oninput() { ConfigTitleState.setTitle(this.value); } }), m('button', { class: 'config-button save', onclick: () => { recordConfigStore.save( globals.state.recordConfig, ConfigTitleState.getTitle()); globals.rafScheduler.scheduleFullRedraw(); ConfigTitleState.clearTitle(); } }, 'Save current config') ]), getSavedConfigList()); } function BufferUsageProgressBar() { if (!globals.state.recordingInProgress) return []; const bufferUsage = globals.bufferUsage ? globals.bufferUsage : 0.0; // Buffer usage is not available yet on Android. if (bufferUsage === 0) return []; return m( 'label', 'Buffer usage: ', m('progress', {max: 100, value: bufferUsage * 100})); } function RecordingNotes() { const sideloadUrl = 'https://perfetto.dev/docs/contributing/build-instructions#get-the-code'; const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing'; const cmdlineUrl = 'https://perfetto.dev/docs/quickstart/android-tracing#perfetto-cmdline'; const extensionURL = `https://chrome.google.com/webstore/detail/ perfetto-ui/lfmkphfpdbjijhpomgecfikhfohaoine`; const notes: m.Children = []; const msgFeatNotSupported = m('span', `Some probes are only supported in Perfetto versions running on Android Q+. `); const msgPerfettoNotSupported = m('span', `Perfetto is not supported natively before Android P. `); const msgSideload = m('span', `If you have a rooted device you can `, m('a', {href: sideloadUrl, target: '_blank'}, `sideload the latest version of Perfetto.`)); const msgRecordingNotSupported = m('.note', `Recording Perfetto traces from the UI is not supported natively before Android Q. If you are using a P device, please select 'Android P' as the 'Target Platform' and `, m('a', {href: cmdlineUrl, target: '_blank'}, `collect the trace using ADB.`)); const msgChrome = m('.note', `To trace Chrome from the Perfetto UI, you need to install our `, m('a', {href: extensionURL, target: '_blank'}, 'Chrome extension'), ' and then reload this page.'); const msgLinux = m('.note', `Use this `, m('a', {href: linuxUrl, target: '_blank'}, `quickstart guide`), ` to get started with tracing on Linux.`); const msgLongTraces = m( '.note', `Recording in long trace mode through the UI is not supported. Please copy the command and `, m('a', {href: cmdlineUrl, target: '_blank'}, `collect the trace using ADB.`)); const msgZeroProbes = m('.note', 'It looks like you didn\'t add any probes. ' + 'Please add at least one to get a non-empty trace.'); if (!hasActiveProbes(globals.state.recordConfig)) { notes.push(msgZeroProbes); } if (isAdbTarget(globals.state.recordingTarget)) { notes.push(msgRecordingNotSupported); } switch (globals.state.recordingTarget.os) { case 'Q': break; case 'P': notes.push(m('.note', msgFeatNotSupported, msgSideload)); break; case 'O': notes.push(m('.note', msgPerfettoNotSupported, msgSideload)); break; case 'L': notes.push(msgLinux); break; case 'C': if (!globals.state.extensionInstalled) notes.push(msgChrome); break; case 'CrOS': if (!globals.state.extensionInstalled) notes.push(msgChrome); break; default: } if (globals.state.recordConfig.mode === 'LONG_TRACE') { notes.unshift(msgLongTraces); } return notes.length > 0 ? m('div', notes) : []; } function RecordingSnippet() { const target = globals.state.recordingTarget; // We don't need commands to start tracing on chrome if (isChromeTarget(target)) { return globals.state.extensionInstalled ? m('div', m('label', `To trace Chrome from the Perfetto UI you just have to press 'Start Recording'.`)) : []; } return m(CodeSnippet, {text: getRecordCommand(target)}); } function getRecordCommand(target: RecordingTarget) { const data = globals.trackDataStore.get('config') as {commandline: string, pbtxt: string, pbBase64: string} | null; const cfg = globals.state.recordConfig; let time = cfg.durationMs / 1000; if (time > MAX_TIME) { time = MAX_TIME; } const pbBase64 = data ? data.pbBase64 : ''; const pbtx = data ? data.pbtxt : ''; let cmd = ''; if (cfg.screenRecord) { // Half-second delay to ensure Perfetto starts tracing before screenrecord // starts recording cmd += `(sleep 0.5 && adb shell screenrecord --time-limit ${time}`; cmd += ' "/sdcard/tracescr.mp4") &\\\n'; } if (isAndroidP(target)) { cmd += `echo '${pbBase64}' | \n`; cmd += 'base64 --decode | \n'; cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n'; } else { cmd += isAndroidTarget(target) ? 'adb shell perfetto \\\n' : 'perfetto \\\n'; cmd += ' -c - --txt \\\n'; cmd += ' -o /data/misc/perfetto-traces/trace \\\n'; cmd += '< globals.dispatch(Actions.stopRecording({}))}, 'Stop'); const cancel = m(`button`, {onclick: () => globals.dispatch(Actions.cancelRecording({}))}, 'Cancel'); return [stop, cancel]; } function onStartRecordingPressed() { location.href = '#!/record?p=instructions'; globals.rafScheduler.scheduleFullRedraw(); const target = globals.state.recordingTarget; if (isAndroidTarget(target) || isChromeTarget(target)) { globals.logging.logEvent('Record Trace', `Record trace (${target.os})`); globals.dispatch(Actions.startRecording({})); } } function RecordingStatusLabel() { const recordingStatus = globals.state.recordingStatus; if (!recordingStatus) return []; return m('label', recordingStatus); } function ErrorLabel() { const lastRecordingError = globals.state.lastRecordingError; if (!lastRecordingError) return []; return m('label.error-label', `Error: ${lastRecordingError}`); } function recordingLog() { const logs = globals.recordingLog; if (logs === undefined) return []; return m('.code-snippet.no-top-bar', m('code', logs)); } // The connection must be done in the frontend. After it, the serial ID will // be inserted in the state, and the worker will be able to connect to the // correct device. async function addAndroidDevice() { let device: USBDevice; try { device = await new AdbOverWebUsb().findDevice(); } catch (e) { const err = `No device found: ${e.name}: ${e.message}`; console.error(err, e); alert(err); return; } if (!device.serialNumber) { console.error('serial number undefined'); return; } // After the user has selected a device with the chrome UI, it will be // available when listing all the available device from WebUSB. Therefore, // we update the list of available devices. await updateAvailableAdbDevices(device.serialNumber); } export async function updateAvailableAdbDevices( preferredDeviceSerial?: string) { const devices = await new AdbOverWebUsb().getPairedDevices(); let recordingTarget: AdbRecordingTarget|undefined = undefined; const availableAdbDevices: AdbRecordingTarget[] = []; devices.forEach(d => { if (d.productName && d.serialNumber) { // TODO(nicomazz): At this stage, we can't know the OS version, so we // assume it is 'Q'. This can create problems with devices with an old // version of perfetto. The os detection should be done after the adb // connection, from adb_record_controller availableAdbDevices.push( {name: d.productName, serial: d.serialNumber, os: 'Q'}); if (preferredDeviceSerial && preferredDeviceSerial === d.serialNumber) { recordingTarget = availableAdbDevices[availableAdbDevices.length - 1]; } } }); globals.dispatch( Actions.setAvailableAdbDevices({devices: availableAdbDevices})); selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget); globals.rafScheduler.scheduleFullRedraw(); return availableAdbDevices; } function selectAndroidDeviceIfAvailable( availableAdbDevices: AdbRecordingTarget[], recordingTarget?: RecordingTarget) { if (!recordingTarget) { recordingTarget = globals.state.recordingTarget; } const deviceConnected = isAdbTarget(recordingTarget); const connectedDeviceDisconnected = deviceConnected && availableAdbDevices.find( e => e.serial === (recordingTarget as AdbRecordingTarget).serial) === undefined; if (availableAdbDevices.length) { // If there's an Android device available and the current selection isn't // one, select the Android device by default. If the current device isn't // available anymore, but another Android device is, select the other // Android device instead. if (!deviceConnected || connectedDeviceDisconnected) { recordingTarget = availableAdbDevices[0]; } globals.dispatch(Actions.setRecordingTarget({target: recordingTarget})); return; } // If the currently selected device was disconnected, reset the recording // target to the default one. if (connectedDeviceDisconnected) { globals.dispatch( Actions.setRecordingTarget({target: getDefaultRecordingTargets()[0]})); } } function recordMenu(routePage: string) { const target = globals.state.recordingTarget; const chromeProbe = m('a[href="#!/record?p=chrome"]', m(`li${routePage === 'chrome' ? '.active' : ''}`, m('i.material-icons', 'laptop_chromebook'), m('.title', 'Chrome'), m('.sub', 'Chrome traces'))); const recInProgress = globals.state.recordingInProgress; return m( '.record-menu', { class: recInProgress ? 'disabled' : '', onclick: () => globals.rafScheduler.scheduleFullRedraw() }, 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', 'Recording command'), m('.sub', 'Manually record trace'))), localStorage.hasOwnProperty(LOCAL_STORAGE_SHOW_CONFIG) ? m('a[href="#!/record?p=config"]', { onclick: () => { recordConfigStore.reloadFromLocalStorage(); } }, m(`li${routePage === 'config' ? '.active' : ''}`, m('i.material-icons', 'tune'), m('.title', 'Saved configs'), m('.sub', 'Manage local configs'))) : null), m('header', 'Probes'), m('ul', isChromeTarget(target) && !isCrOSTarget(target) ? [chromeProbe] : [ 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=gpu"]', m(`li${routePage === 'gpu' ? '.active' : ''}`, m('i.material-icons', 'aspect_ratio'), m('.title', 'GPU'), m('.sub', 'GPU frequency, memory'))), 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'))), chromeProbe, 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'))) ])); } export const RecordPage = createPage({ view() { const SECTIONS: {[property: string]: (cssClass: string) => m.Child} = { buffers: RecSettings, instructions: Instructions, config: Configurations, cpu: CpuSettings, gpu: GpuSettings, power: PowerSettings, memory: MemorySettings, android: AndroidSettings, chrome: ChromeSettings, advanced: AdvancedSettings, }; const pages: m.Children = []; const routePageParam = Router.param('p'); let routePage = typeof routePageParam === 'string' ? routePageParam : ''; 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', globals.state.recordingInProgress ? m('.hider') : [], m('.record-container', RecordHeader(), recordMenu(routePage), pages)); } });