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