blob: 5d87f7a2a8f9fca4eccc11149658b39a0988bd3c [file] [log] [blame]
// 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,
isAndroidTarget,
isChromeTarget,
isLinuxTarget,
TargetOs
} from '../common/state';
import {MAX_TIME, RecordMode} from '../common/state';
import {AdbOverWebUsb} from '../controller/adb';
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 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));
}
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),
m(Probe, {
title: 'Syscalls',
img: null,
descr: `Tracks the enter and exit of all syscalls.`,
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(
`.record-section${cssClass}`,
m(Textarea, {
title: 'Names or pids of the processes to track',
placeholder: 'One per line, e.g.:\n' +
'system_server\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)
// TODO(tneda): Add advanced options.
);
}
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: 'Heap profiling',
// TODO(tneda): Image should be one with flamegraphs.
img: 'rec_meminfo.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: '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. 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 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 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() {
// The categories are displayed only if the extension is installed, because
// they come from the chrome.debugging API, not available from normal web
// pages.
const categories = globals.state.chromeCategories;
if (!categories) return [];
// Show "disabled-by-default" categories last.
const categoriesMap = new Map<string, string>();
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.chrome-categories',
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 baseTargets = [
m('option', {value: 'Q'}, 'Android Q+'),
m('option', {value: 'P'}, 'Android P'),
m('option', {value: 'O'}, 'Android O-'),
m('option', {value: 'C'}, 'Chrome'),
m('option', {value: 'L'}, 'Linux desktop')
];
const availableAndroidDevices = globals.state.availableDevices;
const selected = globals.state.androidDeviceConnected;
const choices: m.Children[] =
availableAndroidDevices.map(d => m('option', {value: d.serial}, d.name));
const selectedIndex = selected ? baseTargets.length +
availableAndroidDevices.findIndex(d => d.serial === selected.serial) :
0;
return m(
'.target',
m(
'label',
'Target platform:',
m('select',
{onchange: m.withAttr('value', onTargetChange), selectedIndex},
...baseTargets,
...choices),
),
m('.chip',
{onclick: addAndroidDevice},
m('button', 'Add Device'),
m('i.material-icons', 'add')));
}
// Target can be the targetOS or the android serial
function onTargetChange(target: string) {
const adbDevice =
globals.state.availableDevices.find(d => d.serial === target);
globals.dispatch(Actions.setAndroidDevice({target: adbDevice}));
const traceCfg = produce(globals.state.recordConfig, draft => {
draft.targetOS = adbDevice ? adbDevice.os as TargetOs : target as TargetOs;
});
globals.dispatch(Actions.setRecordConfig({config: traceCfg}));
globals.rafScheduler.scheduleFullRedraw();
}
function Instructions(cssClass: string) {
return m(
`.record-section.instructions${cssClass}`,
m('header', 'Instructions'),
RecordingSnippet(),
BufferUsageProgressBar(),
m('.buttons', StopCancelButtons()),
recordingLog());
}
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 docUrl = '//docs.perfetto.dev/#/build-instructions?id=get-the-code';
const extensionURL = `https://chrome.google.com/webstore/a/google.com/detail/
perfetto-ui/lfmkphfpdbjijhpomgecfikhfohaoine`;
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 msgChrome =
m('div',
`To trace Chrome from the Perfetto UI, you need to install our `,
m('a', {href: extensionURL}, 'Chrome extension'),
' and then reload this page.');
const msgLinux =
m('div',
`In order to use perfetto on Linux you need to
compile it and run the following command from the build
output directory. `,
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;
case 'C':
if (!globals.state.extensionInstalled) notes.push(msgChrome);
break;
default:
}
return notes.length > 0 ? m('.note', notes) : [];
}
function RecordingSnippet() {
const targetOs = globals.state.recordConfig.targetOS;
// We don't need commands to start tracing on chrome
if (isChromeTarget(targetOs)) {
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(targetOs), hardWhitespace: true});
}
function getRecordCommand(targetOs: TargetOs) {
const data = globals.trackDataStore.get('config') as
{commandline: string, pbtxt: string} |
null;
const cfg = globals.state.recordConfig;
let time = cfg.durationMs / 1000;
if (time > MAX_TIME) {
time = MAX_TIME;
}
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';
}
cmd +=
isAndroidTarget(targetOs) ? 'adb shell perfetto \\\n' : 'perfetto \\\n';
cmd += ' -c - --txt \\\n';
cmd += ' -o /data/misc/perfetto-traces/trace \\\n';
cmd += '<<EOF\n\n';
cmd += pbtx;
cmd += '\nEOF\n';
return cmd;
}
function recordingButtons() {
const state = globals.state;
const realDeviceTarget = state.androidDeviceConnected !== undefined;
const recInProgress = state.recordingInProgress;
const start =
m(`button`,
{
class: recInProgress ? '' : 'selected',
onclick: onStartRecordingPressed
},
'Start Recording');
const showCmd =
m(`button`,
{
onclick: () => {
location.href = '#!/record?p=instructions';
globals.rafScheduler.scheduleFullRedraw();
}
},
'Show Command');
const buttons: m.Children = [];
const targetOs = state.recordConfig.targetOS;
if (isAndroidTarget(targetOs)) {
if (!recInProgress) {
buttons.push(showCmd);
if (realDeviceTarget) buttons.push(start);
}
} else if (isChromeTarget(targetOs) && state.extensionInstalled) {
buttons.push(start);
} else if (isLinuxTarget(targetOs)) {
buttons.push(showCmd);
}
return m('.button', buttons);
}
function StopCancelButtons() {
if (!globals.state.recordingInProgress) return [];
const stop =
m(`button.selected`,
{onclick: () => 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 targetOs = globals.state.recordConfig.targetOS;
if (isAndroidTarget(targetOs) || isChromeTarget(targetOs)) {
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() {
const device = await new AdbOverWebUsb().findDevice();
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();
onTargetChange(device.serialNumber);
}
export async function updateAvailableAdbDevices() {
const devices = await new AdbOverWebUsb().getPairedDevices();
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'});
}
});
globals.dispatch(Actions.setAvailableDevices({devices: availableAdbDevices}));
selectAndroidDeviceIfAvailable(availableAdbDevices);
globals.rafScheduler.scheduleFullRedraw();
return availableAdbDevices;
}
function selectAndroidDeviceIfAvailable(
availableAdbDevices: AdbRecordingTarget[]) {
// If there is an android device attached, but not selected, select it by
// default.
if (!globals.state.androidDeviceConnected && availableAdbDevices.length) {
globals.dispatch(
Actions.setAndroidDevice({target: availableAdbDevices[0]}));
return;
}
// If a device was selected, but it's not available anymore, reset the
// androidConnectedDevice to null.
const deviceConnected = globals.state.androidDeviceConnected;
if (deviceConnected &&
availableAdbDevices.find(e => e.serial === deviceConnected.serial) ===
undefined) {
globals.dispatch(Actions.setAndroidDevice({target: undefined}));
}
}
function recordMenu(routePage: string) {
const targetOS = globals.state.recordConfig.targetOS;
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', 'Instructions'),
m('.sub', 'Generate config and instructions')))),
m('header', 'Probes'),
m('ul', isChromeTarget(targetOS) ? [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'))),
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,
cpu: CpuSettings,
gpu: GpuSettings,
power: PowerSettings,
memory: MemorySettings,
android: AndroidSettings,
chrome: ChromeSettings,
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',
globals.state.recordingInProgress ? m('.hider') : [],
m('.record-container', RecordHeader(), recordMenu(routePage), pages));
}
});