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 15import m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import { 19 AdbRecordingTarget, 20 getDefaultRecordingTargets, 21 hasActiveProbes, 22 isAdbTarget, 23 isAndroidP, 24 isAndroidTarget, 25 isChromeTarget, 26 isCrOSTarget, 27 isLinuxTarget, 28 isWindowsTarget, 29 LoadedConfig, 30 MAX_TIME, 31 RecordingTarget, 32} from '../common/state'; 33import {AdbOverWebUsb} from '../controller/adb'; 34import { 35 createEmptyRecordConfig, 36 RecordConfig, 37} from '../controller/record_config_types'; 38import {featureFlags} from '../core/feature_flags'; 39import {raf} from '../core/raf_scheduler'; 40 41import {globals} from './globals'; 42import {createPage, PageAttrs} from './pages'; 43import { 44 autosaveConfigStore, 45 recordConfigStore, 46 recordTargetStore, 47} from './record_config'; 48import {CodeSnippet} from './record_widgets'; 49import {AdvancedSettings} from './recording/advanced_settings'; 50import {AndroidSettings} from './recording/android_settings'; 51import {ChromeSettings} from './recording/chrome_settings'; 52import {CpuSettings} from './recording/cpu_settings'; 53import {GpuSettings} from './recording/gpu_settings'; 54import {LinuxPerfSettings} from './recording/linux_perf_settings'; 55import {MemorySettings} from './recording/memory_settings'; 56import {PowerSettings} from './recording/power_settings'; 57import {RecordingSectionAttrs} from './recording/recording_sections'; 58import {RecordingSettings} from './recording/recording_settings'; 59import {EtwSettings} from './recording/etw_settings'; 60import {createPermalink} from './permalink'; 61 62export const PERSIST_CONFIG_FLAG = featureFlags.register({ 63 id: 'persistConfigsUI', 64 name: 'Config persistence UI', 65 description: 'Show experimental config persistence UI on the record page.', 66 defaultValue: true, 67}); 68 69export const RECORDING_SECTIONS = [ 70 'buffers', 71 'instructions', 72 'config', 73 'cpu', 74 'etw', 75 'gpu', 76 'power', 77 'memory', 78 'android', 79 'chrome', 80 'tracePerf', 81 'advanced', 82]; 83 84function RecordHeader() { 85 return m( 86 '.record-header', 87 m( 88 '.top-part', 89 m( 90 '.target-and-status', 91 RecordingPlatformSelection(), 92 RecordingStatusLabel(), 93 ErrorLabel(), 94 ), 95 recordingButtons(), 96 ), 97 RecordingNotes(), 98 ); 99} 100 101function RecordingPlatformSelection() { 102 if (globals.state.recordingInProgress) return []; 103 104 const availableAndroidDevices = globals.state.availableAdbDevices; 105 const recordingTarget = globals.state.recordingTarget; 106 107 const targets = []; 108 for (const {os, name} of getDefaultRecordingTargets()) { 109 targets.push(m('option', {value: os}, name)); 110 } 111 for (const d of availableAndroidDevices) { 112 targets.push(m('option', {value: d.serial}, d.name)); 113 } 114 115 const selectedIndex = isAdbTarget(recordingTarget) 116 ? targets.findIndex((node) => node.attrs.value === recordingTarget.serial) 117 : targets.findIndex((node) => node.attrs.value === recordingTarget.os); 118 119 return m( 120 '.target', 121 m( 122 'label', 123 'Target platform:', 124 m( 125 'select', 126 { 127 selectedIndex, 128 onchange: (e: Event) => { 129 onTargetChange((e.target as HTMLSelectElement).value); 130 }, 131 onupdate: (select) => { 132 // Work around mithril bug 133 // (https://github.com/MithrilJS/mithril.js/issues/2107): We may 134 // update the select's options while also changing the 135 // selectedIndex at the same time. The update of selectedIndex 136 // may be applied before the new options are added to the select 137 // element. Because the new selectedIndex may be outside of the 138 // select's options at that time, we have to reselect the 139 // correct index here after any new children were added. 140 (select.dom as HTMLSelectElement).selectedIndex = selectedIndex; 141 }, 142 }, 143 ...targets, 144 ), 145 ), 146 m( 147 '.chip', 148 {onclick: addAndroidDevice}, 149 m('button', 'Add ADB Device'), 150 m('i.material-icons', 'add'), 151 ), 152 ); 153} 154 155// |target| can be the TargetOs or the android serial. 156function onTargetChange(target: string) { 157 const recordingTarget: RecordingTarget = 158 globals.state.availableAdbDevices.find((d) => d.serial === target) || 159 getDefaultRecordingTargets().find((t) => t.os === target) || 160 getDefaultRecordingTargets()[0]; 161 162 if (isChromeTarget(recordingTarget)) { 163 globals.dispatch(Actions.setFetchChromeCategories({fetch: true})); 164 } 165 166 globals.dispatch(Actions.setRecordingTarget({target: recordingTarget})); 167 recordTargetStore.save(target); 168 raf.scheduleFullRedraw(); 169} 170 171function Instructions(cssClass: string) { 172 return m( 173 `.record-section.instructions${cssClass}`, 174 m('header', 'Recording command'), 175 PERSIST_CONFIG_FLAG.get() 176 ? m( 177 'button.permalinkconfig', 178 { 179 onclick: () => { 180 createPermalink({isRecordingConfig: true}); 181 }, 182 }, 183 'Share recording settings', 184 ) 185 : null, 186 RecordingSnippet(), 187 BufferUsageProgressBar(), 188 m('.buttons', StopCancelButtons()), 189 recordingLog(), 190 ); 191} 192 193export function loadedConfigEqual( 194 cfg1: LoadedConfig, 195 cfg2: LoadedConfig, 196): boolean { 197 return cfg1.type === 'NAMED' && cfg2.type === 'NAMED' 198 ? cfg1.name === cfg2.name 199 : cfg1.type === cfg2.type; 200} 201 202export function loadConfigButton( 203 config: RecordConfig, 204 configType: LoadedConfig, 205): m.Vnode { 206 return m( 207 'button', 208 { 209 class: 'config-button', 210 title: 'Apply configuration settings', 211 disabled: loadedConfigEqual(configType, globals.state.lastLoadedConfig), 212 onclick: () => { 213 globals.dispatch(Actions.setRecordConfig({config, configType})); 214 raf.scheduleFullRedraw(); 215 }, 216 }, 217 m('i.material-icons', 'file_upload'), 218 ); 219} 220 221export function displayRecordConfigs() { 222 const configs = []; 223 if (autosaveConfigStore.hasSavedConfig) { 224 configs.push( 225 m('.config', [ 226 m('span.title-config', m('strong', 'Latest started recording')), 227 loadConfigButton(autosaveConfigStore.get(), {type: 'AUTOMATIC'}), 228 ]), 229 ); 230 } 231 for (const validated of recordConfigStore.recordConfigs) { 232 const item = validated.result; 233 configs.push( 234 m('.config', [ 235 m('span.title-config', item.title), 236 loadConfigButton(item.config, {type: 'NAMED', name: item.title}), 237 m( 238 'button', 239 { 240 class: 'config-button', 241 title: 'Overwrite configuration with current settings', 242 onclick: () => { 243 if ( 244 confirm( 245 `Overwrite config "${item.title}" with current settings?`, 246 ) 247 ) { 248 recordConfigStore.overwrite( 249 globals.state.recordConfig, 250 item.key, 251 ); 252 globals.dispatch( 253 Actions.setRecordConfig({ 254 config: item.config, 255 configType: {type: 'NAMED', name: item.title}, 256 }), 257 ); 258 raf.scheduleFullRedraw(); 259 } 260 }, 261 }, 262 m('i.material-icons', 'save'), 263 ), 264 m( 265 'button', 266 { 267 class: 'config-button', 268 title: 'Remove configuration', 269 onclick: () => { 270 recordConfigStore.delete(item.key); 271 raf.scheduleFullRedraw(); 272 }, 273 }, 274 m('i.material-icons', 'delete'), 275 ), 276 ]), 277 ); 278 279 const errorItems = []; 280 for (const extraKey of validated.extraKeys) { 281 errorItems.push(m('li', `${extraKey} is unrecognised`)); 282 } 283 for (const invalidKey of validated.invalidKeys) { 284 errorItems.push(m('li', `${invalidKey} contained an invalid value`)); 285 } 286 287 if (errorItems.length > 0) { 288 configs.push( 289 m( 290 '.parsing-errors', 291 'One or more errors have been found while loading configuration "' + 292 item.title + 293 '". Loading is possible, but make sure to check ' + 294 'the settings afterwards.', 295 m('ul', errorItems), 296 ), 297 ); 298 } 299 } 300 return configs; 301} 302 303export const ConfigTitleState = { 304 title: '', 305 getTitle: () => { 306 return ConfigTitleState.title; 307 }, 308 setTitle: (value: string) => { 309 ConfigTitleState.title = value; 310 }, 311 clearTitle: () => { 312 ConfigTitleState.title = ''; 313 }, 314}; 315 316export function Configurations(cssClass: string) { 317 const canSave = recordConfigStore.canSave(ConfigTitleState.getTitle()); 318 return m( 319 `.record-section${cssClass}`, 320 m('header', 'Save and load configurations'), 321 m('.input-config', [ 322 m('input', { 323 value: ConfigTitleState.title, 324 placeholder: 'Title for config', 325 oninput() { 326 ConfigTitleState.setTitle(this.value); 327 raf.scheduleFullRedraw(); 328 }, 329 }), 330 m( 331 'button', 332 { 333 class: 'config-button', 334 disabled: !canSave, 335 title: canSave 336 ? 'Save current config' 337 : 'Duplicate name, saving disabled', 338 onclick: () => { 339 recordConfigStore.save( 340 globals.state.recordConfig, 341 ConfigTitleState.getTitle(), 342 ); 343 raf.scheduleFullRedraw(); 344 ConfigTitleState.clearTitle(); 345 }, 346 }, 347 m('i.material-icons', 'save'), 348 ), 349 m( 350 'button', 351 { 352 class: 'config-button', 353 title: 'Clear current configuration', 354 onclick: () => { 355 if ( 356 confirm( 357 'Current configuration will be cleared. ' + 'Are you sure?', 358 ) 359 ) { 360 globals.dispatch( 361 Actions.setRecordConfig({ 362 config: createEmptyRecordConfig(), 363 configType: {type: 'NONE'}, 364 }), 365 ); 366 raf.scheduleFullRedraw(); 367 } 368 }, 369 }, 370 m('i.material-icons', 'delete_forever'), 371 ), 372 ]), 373 displayRecordConfigs(), 374 ); 375} 376 377function BufferUsageProgressBar() { 378 if (!globals.state.recordingInProgress) return []; 379 380 const bufferUsage = globals.bufferUsage ?? 0.0; 381 // Buffer usage is not available yet on Android. 382 if (bufferUsage === 0) return []; 383 384 return m( 385 'label', 386 'Buffer usage: ', 387 m('progress', {max: 100, value: bufferUsage * 100}), 388 ); 389} 390 391function RecordingNotes() { 392 const sideloadUrl = 393 'https://perfetto.dev/docs/contributing/build-instructions#get-the-code'; 394 const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing'; 395 const cmdlineUrl = 396 'https://perfetto.dev/docs/quickstart/android-tracing#perfetto-cmdline'; 397 const extensionURL = `https://chrome.google.com/webstore/detail/perfetto-ui/lfmkphfpdbjijhpomgecfikhfohaoine`; 398 399 const notes: m.Children = []; 400 401 const msgFeatNotSupported = m( 402 'span', 403 `Some probes are only supported in Perfetto versions running 404 on Android Q+. `, 405 ); 406 407 const msgPerfettoNotSupported = m( 408 'span', 409 `Perfetto is not supported natively before Android P. `, 410 ); 411 412 const msgSideload = m( 413 'span', 414 `If you have a rooted device you can `, 415 m( 416 'a', 417 {href: sideloadUrl, target: '_blank'}, 418 `sideload the latest version of 419 Perfetto.`, 420 ), 421 ); 422 423 const msgRecordingNotSupported = m( 424 '.note', 425 `Recording Perfetto traces from the UI is not supported natively 426 before Android Q. If you are using a P device, please select 'Android P' 427 as the 'Target Platform' and `, 428 m( 429 'a', 430 {href: cmdlineUrl, target: '_blank'}, 431 `collect the trace using ADB.`, 432 ), 433 ); 434 435 const msgChrome = m( 436 '.note', 437 `To trace Chrome from the Perfetto UI, you need to install our `, 438 m('a', {href: extensionURL, target: '_blank'}, 'Chrome extension'), 439 ' and then reload this page. ', 440 ); 441 442 const msgWinEtw = m( 443 '.note', 444 `To trace with Etw on Windows from the Perfetto UI, you to run chrome with`, 445 `administrator permission and you need to install our `, 446 m('a', {href: extensionURL, target: '_blank'}, 'Chrome extension'), 447 ' and then reload this page.', 448 ); 449 450 const msgLinux = m( 451 '.note', 452 `Use this `, 453 m('a', {href: linuxUrl, target: '_blank'}, `quickstart guide`), 454 ` to get started with tracing on Linux.`, 455 ); 456 457 const msgLongTraces = m( 458 '.note', 459 `Recording in long trace mode through the UI is not supported. Please copy 460 the command and `, 461 m( 462 'a', 463 {href: cmdlineUrl, target: '_blank'}, 464 `collect the trace using ADB.`, 465 ), 466 ); 467 468 const msgZeroProbes = m( 469 '.note', 470 "It looks like you didn't add any probes. " + 471 'Please add at least one to get a non-empty trace.', 472 ); 473 474 if (!hasActiveProbes(globals.state.recordConfig)) { 475 notes.push(msgZeroProbes); 476 } 477 478 if (isAdbTarget(globals.state.recordingTarget)) { 479 notes.push(msgRecordingNotSupported); 480 } 481 switch (globals.state.recordingTarget.os) { 482 case 'Q': 483 break; 484 case 'P': 485 notes.push(m('.note', msgFeatNotSupported, msgSideload)); 486 break; 487 case 'O': 488 notes.push(m('.note', msgPerfettoNotSupported, msgSideload)); 489 break; 490 case 'L': 491 notes.push(msgLinux); 492 break; 493 case 'C': 494 if (!globals.state.extensionInstalled) notes.push(msgChrome); 495 break; 496 case 'CrOS': 497 if (!globals.state.extensionInstalled) notes.push(msgChrome); 498 break; 499 case 'Win': 500 if (!globals.state.extensionInstalled) notes.push(msgWinEtw); 501 break; 502 default: 503 } 504 if (globals.state.recordConfig.mode === 'LONG_TRACE') { 505 notes.unshift(msgLongTraces); 506 } 507 508 return notes.length > 0 ? m('div', notes) : []; 509} 510 511function RecordingSnippet() { 512 const target = globals.state.recordingTarget; 513 514 // We don't need commands to start tracing on chrome 515 if (isChromeTarget(target)) { 516 return globals.state.extensionInstalled && 517 !globals.state.recordingInProgress 518 ? m( 519 'div', 520 m( 521 'label', 522 `To trace Chrome from the Perfetto UI you just have to press 523 'Start Recording'.`, 524 ), 525 ) 526 : []; 527 } 528 return m(CodeSnippet, {text: getRecordCommand(target)}); 529} 530 531function getRecordCommand(target: RecordingTarget) { 532 const data = globals.trackDataStore.get('config') as { 533 commandline: string; 534 pbtxt: string; 535 pbBase64: string; 536 } | null; 537 538 const cfg = globals.state.recordConfig; 539 let time = cfg.durationMs / 1000; 540 541 if (time > MAX_TIME) { 542 time = MAX_TIME; 543 } 544 545 const pbBase64 = data ? data.pbBase64 : ''; 546 const pbtx = data ? data.pbtxt : ''; 547 let cmd = ''; 548 if (isAndroidP(target)) { 549 cmd += `echo '${pbBase64}' | \n`; 550 cmd += 'base64 --decode | \n'; 551 cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n'; 552 } else { 553 cmd += isAndroidTarget(target) 554 ? 'adb shell perfetto \\\n' 555 : 'perfetto \\\n'; 556 cmd += ' -c - --txt \\\n'; 557 cmd += ' -o /data/misc/perfetto-traces/trace \\\n'; 558 cmd += '<<EOF\n\n'; 559 cmd += pbtx; 560 cmd += '\nEOF\n'; 561 } 562 return cmd; 563} 564 565function recordingButtons() { 566 const state = globals.state; 567 const target = state.recordingTarget; 568 const recInProgress = state.recordingInProgress; 569 570 const start = m( 571 `button`, 572 { 573 class: recInProgress ? '' : 'selected', 574 onclick: onStartRecordingPressed, 575 }, 576 'Start Recording', 577 ); 578 579 const buttons: m.Children = []; 580 581 if (isAndroidTarget(target)) { 582 if ( 583 !recInProgress && 584 isAdbTarget(target) && 585 globals.state.recordConfig.mode !== 'LONG_TRACE' 586 ) { 587 buttons.push(start); 588 } 589 } else if ( 590 (isWindowsTarget(target) || isChromeTarget(target)) && 591 state.extensionInstalled 592 ) { 593 buttons.push(start); 594 } 595 return m('.button', buttons); 596} 597 598function StopCancelButtons() { 599 if (!globals.state.recordingInProgress) return []; 600 601 const stop = m( 602 `button.selected`, 603 {onclick: () => globals.dispatch(Actions.stopRecording({}))}, 604 'Stop', 605 ); 606 607 const cancel = m( 608 `button`, 609 {onclick: () => globals.dispatch(Actions.cancelRecording({}))}, 610 'Cancel', 611 ); 612 613 return [stop, cancel]; 614} 615 616function onStartRecordingPressed() { 617 location.href = '#!/record/instructions'; 618 raf.scheduleFullRedraw(); 619 autosaveConfigStore.save(globals.state.recordConfig); 620 621 const target = globals.state.recordingTarget; 622 if ( 623 isAndroidTarget(target) || 624 isChromeTarget(target) || 625 isWindowsTarget(target) 626 ) { 627 globals.logging.logEvent('Record Trace', `Record trace (${target.os})`); 628 globals.dispatch(Actions.startRecording({})); 629 } 630} 631 632function RecordingStatusLabel() { 633 const recordingStatus = globals.state.recordingStatus; 634 if (!recordingStatus) return []; 635 return m('label', recordingStatus); 636} 637 638export function ErrorLabel() { 639 const lastRecordingError = globals.state.lastRecordingError; 640 if (!lastRecordingError) return []; 641 return m('label.error-label', `Error: ${lastRecordingError}`); 642} 643 644function recordingLog() { 645 const logs = globals.recordingLog; 646 if (logs === undefined) return []; 647 return m('.code-snippet.no-top-bar', m('code', logs)); 648} 649 650// The connection must be done in the frontend. After it, the serial ID will 651// be inserted in the state, and the worker will be able to connect to the 652// correct device. 653async function addAndroidDevice() { 654 let device: USBDevice; 655 try { 656 device = await new AdbOverWebUsb().findDevice(); 657 } catch (e) { 658 const err = `No device found: ${e.name}: ${e.message}`; 659 console.error(err, e); 660 alert(err); 661 return; 662 } 663 664 if (!device.serialNumber) { 665 console.error('serial number undefined'); 666 return; 667 } 668 669 // After the user has selected a device with the chrome UI, it will be 670 // available when listing all the available device from WebUSB. Therefore, 671 // we update the list of available devices. 672 await updateAvailableAdbDevices(device.serialNumber); 673} 674 675// We really should be getting the API version from the adb target, but 676// currently its too complicated to do that (== most likely, we need to finish 677// recordingV2 migration). For now, add an escape hatch to use Android S as a 678// default, given that the main features we want are gated by API level 31 and S 679// is old enough to be the default most of the time. 680const USE_ANDROID_S_AS_DEFAULT_FLAG = featureFlags.register({ 681 id: 'recordingPageUseSAsDefault', 682 name: 'Use Android S as a default recording target', 683 description: 'Use Android S as a default recording target instead of Q', 684 defaultValue: false, 685}); 686 687export async function updateAvailableAdbDevices( 688 preferredDeviceSerial?: string, 689) { 690 const devices = await new AdbOverWebUsb().getPairedDevices(); 691 692 let recordingTarget: AdbRecordingTarget | undefined = undefined; 693 694 const availableAdbDevices: AdbRecordingTarget[] = []; 695 devices.forEach((d) => { 696 if (d.productName && d.serialNumber) { 697 // TODO(nicomazz): At this stage, we can't know the OS version, so we 698 // assume it is 'Q'. This can create problems with devices with an old 699 // version of perfetto. The os detection should be done after the adb 700 // connection, from adb_record_controller 701 availableAdbDevices.push({ 702 name: d.productName, 703 serial: d.serialNumber, 704 os: USE_ANDROID_S_AS_DEFAULT_FLAG.get() ? 'S' : 'Q', 705 }); 706 if (preferredDeviceSerial && preferredDeviceSerial === d.serialNumber) { 707 recordingTarget = availableAdbDevices[availableAdbDevices.length - 1]; 708 } 709 } 710 }); 711 712 globals.dispatch( 713 Actions.setAvailableAdbDevices({devices: availableAdbDevices}), 714 ); 715 selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget); 716 raf.scheduleFullRedraw(); 717 return availableAdbDevices; 718} 719 720function selectAndroidDeviceIfAvailable( 721 availableAdbDevices: AdbRecordingTarget[], 722 recordingTarget?: RecordingTarget, 723) { 724 if (!recordingTarget) { 725 recordingTarget = globals.state.recordingTarget; 726 } 727 const deviceConnected = isAdbTarget(recordingTarget); 728 const connectedDeviceDisconnected = 729 deviceConnected && 730 availableAdbDevices.find( 731 (e) => e.serial === (recordingTarget as AdbRecordingTarget).serial, 732 ) === undefined; 733 734 if (availableAdbDevices.length) { 735 // If there's an Android device available and the current selection isn't 736 // one, select the Android device by default. If the current device isn't 737 // available anymore, but another Android device is, select the other 738 // Android device instead. 739 if (!deviceConnected || connectedDeviceDisconnected) { 740 recordingTarget = availableAdbDevices[0]; 741 } 742 743 globals.dispatch(Actions.setRecordingTarget({target: recordingTarget})); 744 return; 745 } 746 747 // If the currently selected device was disconnected, reset the recording 748 // target to the default one. 749 if (connectedDeviceDisconnected) { 750 globals.dispatch( 751 Actions.setRecordingTarget({target: getDefaultRecordingTargets()[0]}), 752 ); 753 } 754} 755 756function recordMenu(routePage: string) { 757 const target = globals.state.recordingTarget; 758 const chromeProbe = m( 759 'a[href="#!/record/chrome"]', 760 m( 761 `li${routePage === 'chrome' ? '.active' : ''}`, 762 m('i.material-icons', 'laptop_chromebook'), 763 m('.title', 'Chrome'), 764 m('.sub', 'Chrome traces'), 765 ), 766 ); 767 const cpuProbe = m( 768 'a[href="#!/record/cpu"]', 769 m( 770 `li${routePage === 'cpu' ? '.active' : ''}`, 771 m('i.material-icons', 'subtitles'), 772 m('.title', 'CPU'), 773 m('.sub', 'CPU usage, scheduling, wakeups'), 774 ), 775 ); 776 const gpuProbe = m( 777 'a[href="#!/record/gpu"]', 778 m( 779 `li${routePage === 'gpu' ? '.active' : ''}`, 780 m('i.material-icons', 'aspect_ratio'), 781 m('.title', 'GPU'), 782 m('.sub', 'GPU frequency, memory'), 783 ), 784 ); 785 const powerProbe = m( 786 'a[href="#!/record/power"]', 787 m( 788 `li${routePage === 'power' ? '.active' : ''}`, 789 m('i.material-icons', 'battery_charging_full'), 790 m('.title', 'Power'), 791 m('.sub', 'Battery and other energy counters'), 792 ), 793 ); 794 const memoryProbe = m( 795 'a[href="#!/record/memory"]', 796 m( 797 `li${routePage === 'memory' ? '.active' : ''}`, 798 m('i.material-icons', 'memory'), 799 m('.title', 'Memory'), 800 m('.sub', 'Physical mem, VM, LMK'), 801 ), 802 ); 803 const androidProbe = m( 804 'a[href="#!/record/android"]', 805 m( 806 `li${routePage === 'android' ? '.active' : ''}`, 807 m('i.material-icons', 'android'), 808 m('.title', 'Android apps & svcs'), 809 m('.sub', 'atrace and logcat'), 810 ), 811 ); 812 const advancedProbe = m( 813 'a[href="#!/record/advanced"]', 814 m( 815 `li${routePage === 'advanced' ? '.active' : ''}`, 816 m('i.material-icons', 'settings'), 817 m('.title', 'Advanced settings'), 818 m('.sub', 'Complicated stuff for wizards'), 819 ), 820 ); 821 const tracePerfProbe = m( 822 'a[href="#!/record/tracePerf"]', 823 m( 824 `li${routePage === 'tracePerf' ? '.active' : ''}`, 825 m('i.material-icons', 'full_stacked_bar_chart'), 826 m('.title', 'Stack Samples'), 827 m('.sub', 'Lightweight stack polling'), 828 ), 829 ); 830 const etwProbe = m( 831 'a[href="#!/record/etw"]', 832 m( 833 `li${routePage === 'etw' ? '.active' : ''}`, 834 m('i.material-icons', 'subtitles'), 835 m('.title', 'ETW Tracing Config'), 836 m('.sub', 'Context switch, Thread state'), 837 ), 838 ); 839 const recInProgress = globals.state.recordingInProgress; 840 841 const probes = []; 842 if (isCrOSTarget(target) || isLinuxTarget(target)) { 843 probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe); 844 } else if (isChromeTarget(target)) { 845 probes.push(chromeProbe); 846 } else if (isWindowsTarget(target)) { 847 probes.push(chromeProbe, etwProbe); 848 } else { 849 probes.push( 850 cpuProbe, 851 gpuProbe, 852 powerProbe, 853 memoryProbe, 854 androidProbe, 855 chromeProbe, 856 tracePerfProbe, 857 advancedProbe, 858 ); 859 } 860 861 return m( 862 '.record-menu', 863 { 864 class: recInProgress ? 'disabled' : '', 865 onclick: () => raf.scheduleFullRedraw(), 866 }, 867 m('header', 'Trace config'), 868 m( 869 'ul', 870 m( 871 'a[href="#!/record/buffers"]', 872 m( 873 `li${routePage === 'buffers' ? '.active' : ''}`, 874 m('i.material-icons', 'tune'), 875 m('.title', 'Recording settings'), 876 m('.sub', 'Buffer mode, size and duration'), 877 ), 878 ), 879 m( 880 'a[href="#!/record/instructions"]', 881 m( 882 `li${routePage === 'instructions' ? '.active' : ''}`, 883 m('i.material-icons-filled.rec', 'fiber_manual_record'), 884 m('.title', 'Recording command'), 885 m('.sub', 'Manually record trace'), 886 ), 887 ), 888 PERSIST_CONFIG_FLAG.get() 889 ? m( 890 'a[href="#!/record/config"]', 891 { 892 onclick: () => { 893 recordConfigStore.reloadFromLocalStorage(); 894 }, 895 }, 896 m( 897 `li${routePage === 'config' ? '.active' : ''}`, 898 m('i.material-icons', 'save'), 899 m('.title', 'Saved configs'), 900 m('.sub', 'Manage local configs'), 901 ), 902 ) 903 : null, 904 ), 905 m('header', 'Probes'), 906 m('ul', probes), 907 ); 908} 909 910export function maybeGetActiveCss(routePage: string, section: string): string { 911 return routePage === section ? '.active' : ''; 912} 913 914export const RecordPage = createPage({ 915 view({attrs}: m.Vnode<PageAttrs>) { 916 const pages: m.Children = []; 917 // we need to remove the `/` character from the route 918 let routePage = attrs.subpage ? attrs.subpage.substr(1) : ''; 919 if (!RECORDING_SECTIONS.includes(routePage)) { 920 routePage = 'buffers'; 921 } 922 pages.push(recordMenu(routePage)); 923 924 pages.push( 925 m(RecordingSettings, { 926 dataSources: [], 927 cssClass: maybeGetActiveCss(routePage, 'buffers'), 928 } as RecordingSectionAttrs), 929 ); 930 pages.push(Instructions(maybeGetActiveCss(routePage, 'instructions'))); 931 pages.push(Configurations(maybeGetActiveCss(routePage, 'config'))); 932 933 const settingsSections = new Map([ 934 ['cpu', CpuSettings], 935 ['gpu', GpuSettings], 936 ['power', PowerSettings], 937 ['memory', MemorySettings], 938 ['android', AndroidSettings], 939 ['chrome', ChromeSettings], 940 ['tracePerf', LinuxPerfSettings], 941 ['advanced', AdvancedSettings], 942 ['etw', EtwSettings], 943 ]); 944 for (const [section, component] of settingsSections.entries()) { 945 pages.push( 946 m(component, { 947 dataSources: [], 948 cssClass: maybeGetActiveCss(routePage, section), 949 } as RecordingSectionAttrs), 950 ); 951 } 952 953 if (isChromeTarget(globals.state.recordingTarget)) { 954 globals.dispatch(Actions.setFetchChromeCategories({fetch: true})); 955 } 956 957 return m( 958 '.record-page', 959 globals.state.recordingInProgress ? m('.hider') : [], 960 m( 961 '.record-container', 962 RecordHeader(), 963 m('.record-container-content', recordMenu(routePage), pages), 964 ), 965 ); 966 }, 967}); 968