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