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