| // Copyright (C) 2018 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import m from 'mithril'; |
| |
| import {Actions} from '../common/actions'; |
| import { |
| AdbRecordingTarget, |
| getDefaultRecordingTargets, |
| hasActiveProbes, |
| isAdbTarget, |
| isAndroidP, |
| isAndroidTarget, |
| isChromeTarget, |
| isCrOSTarget, |
| isLinuxTarget, |
| isWindowsTarget, |
| LoadedConfig, |
| MAX_TIME, |
| RecordingTarget, |
| } from '../common/state'; |
| import {AdbOverWebUsb} from '../controller/adb'; |
| import { |
| createEmptyRecordConfig, |
| RecordConfig, |
| } from '../controller/record_config_types'; |
| import {featureFlags} from '../core/feature_flags'; |
| import {raf} from '../core/raf_scheduler'; |
| |
| import {globals} from './globals'; |
| import {createPage, PageAttrs} from './pages'; |
| import { |
| autosaveConfigStore, |
| recordConfigStore, |
| recordTargetStore, |
| } from './record_config'; |
| import {CodeSnippet} from './record_widgets'; |
| import {AdvancedSettings} from './recording/advanced_settings'; |
| import {AndroidSettings} from './recording/android_settings'; |
| import {ChromeSettings} from './recording/chrome_settings'; |
| import {CpuSettings} from './recording/cpu_settings'; |
| import {GpuSettings} from './recording/gpu_settings'; |
| import {LinuxPerfSettings} from './recording/linux_perf_settings'; |
| import {MemorySettings} from './recording/memory_settings'; |
| import {PowerSettings} from './recording/power_settings'; |
| import {RecordingSectionAttrs} from './recording/recording_sections'; |
| import {RecordingSettings} from './recording/recording_settings'; |
| import {EtwSettings} from './recording/etw_settings'; |
| |
| export const PERSIST_CONFIG_FLAG = featureFlags.register({ |
| id: 'persistConfigsUI', |
| name: 'Config persistence UI', |
| description: 'Show experimental config persistence UI on the record page.', |
| defaultValue: true, |
| }); |
| |
| export const RECORDING_SECTIONS = [ |
| 'buffers', |
| 'instructions', |
| 'config', |
| 'cpu', |
| 'etw', |
| 'gpu', |
| 'power', |
| 'memory', |
| 'android', |
| 'chrome', |
| 'tracePerf', |
| 'advanced', |
| ]; |
| |
| function RecordHeader() { |
| return m( |
| '.record-header', |
| m( |
| '.top-part', |
| m( |
| '.target-and-status', |
| RecordingPlatformSelection(), |
| RecordingStatusLabel(), |
| ErrorLabel(), |
| ), |
| recordingButtons(), |
| ), |
| RecordingNotes(), |
| ); |
| } |
| |
| function RecordingPlatformSelection() { |
| if (globals.state.recordingInProgress) return []; |
| |
| const availableAndroidDevices = globals.state.availableAdbDevices; |
| const recordingTarget = globals.state.recordingTarget; |
| |
| const targets = []; |
| for (const {os, name} of getDefaultRecordingTargets()) { |
| targets.push(m('option', {value: os}, name)); |
| } |
| for (const d of availableAndroidDevices) { |
| targets.push(m('option', {value: d.serial}, d.name)); |
| } |
| |
| const selectedIndex = isAdbTarget(recordingTarget) |
| ? targets.findIndex((node) => node.attrs.value === recordingTarget.serial) |
| : targets.findIndex((node) => node.attrs.value === recordingTarget.os); |
| |
| return m( |
| '.target', |
| m( |
| 'label', |
| 'Target platform:', |
| m( |
| 'select', |
| { |
| selectedIndex, |
| onchange: (e: Event) => { |
| onTargetChange((e.target as HTMLSelectElement).value); |
| }, |
| onupdate: (select) => { |
| // Work around mithril bug |
| // (https://github.com/MithrilJS/mithril.js/issues/2107): We may |
| // update the select's options while also changing the |
| // selectedIndex at the same time. The update of selectedIndex |
| // may be applied before the new options are added to the select |
| // element. Because the new selectedIndex may be outside of the |
| // select's options at that time, we have to reselect the |
| // correct index here after any new children were added. |
| (select.dom as HTMLSelectElement).selectedIndex = selectedIndex; |
| }, |
| }, |
| ...targets, |
| ), |
| ), |
| m( |
| '.chip', |
| {onclick: addAndroidDevice}, |
| m('button', 'Add ADB Device'), |
| m('i.material-icons', 'add'), |
| ), |
| ); |
| } |
| |
| // |target| can be the TargetOs or the android serial. |
| function onTargetChange(target: string) { |
| const recordingTarget: RecordingTarget = |
| globals.state.availableAdbDevices.find((d) => d.serial === target) || |
| getDefaultRecordingTargets().find((t) => t.os === target) || |
| getDefaultRecordingTargets()[0]; |
| |
| if (isChromeTarget(recordingTarget)) { |
| globals.dispatch(Actions.setFetchChromeCategories({fetch: true})); |
| } |
| |
| globals.dispatch(Actions.setRecordingTarget({target: recordingTarget})); |
| recordTargetStore.save(target); |
| raf.scheduleFullRedraw(); |
| } |
| |
| function Instructions(cssClass: string) { |
| return m( |
| `.record-section.instructions${cssClass}`, |
| m('header', 'Recording command'), |
| PERSIST_CONFIG_FLAG.get() |
| ? m( |
| 'button.permalinkconfig', |
| { |
| onclick: () => { |
| globals.dispatch( |
| Actions.createPermalink({isRecordingConfig: true}), |
| ); |
| }, |
| }, |
| 'Share recording settings', |
| ) |
| : null, |
| RecordingSnippet(), |
| BufferUsageProgressBar(), |
| m('.buttons', StopCancelButtons()), |
| recordingLog(), |
| ); |
| } |
| |
| export function loadedConfigEqual( |
| cfg1: LoadedConfig, |
| cfg2: LoadedConfig, |
| ): boolean { |
| return cfg1.type === 'NAMED' && cfg2.type === 'NAMED' |
| ? cfg1.name === cfg2.name |
| : cfg1.type === cfg2.type; |
| } |
| |
| export function loadConfigButton( |
| config: RecordConfig, |
| configType: LoadedConfig, |
| ): m.Vnode { |
| return m( |
| 'button', |
| { |
| class: 'config-button', |
| title: 'Apply configuration settings', |
| disabled: loadedConfigEqual(configType, globals.state.lastLoadedConfig), |
| onclick: () => { |
| globals.dispatch(Actions.setRecordConfig({config, configType})); |
| raf.scheduleFullRedraw(); |
| }, |
| }, |
| m('i.material-icons', 'file_upload'), |
| ); |
| } |
| |
| export function displayRecordConfigs() { |
| const configs = []; |
| if (autosaveConfigStore.hasSavedConfig) { |
| configs.push( |
| m('.config', [ |
| m('span.title-config', m('strong', 'Latest started recording')), |
| loadConfigButton(autosaveConfigStore.get(), {type: 'AUTOMATIC'}), |
| ]), |
| ); |
| } |
| for (const validated of recordConfigStore.recordConfigs) { |
| const item = validated.result; |
| configs.push( |
| m('.config', [ |
| m('span.title-config', item.title), |
| loadConfigButton(item.config, {type: 'NAMED', name: item.title}), |
| m( |
| 'button', |
| { |
| class: 'config-button', |
| title: 'Overwrite configuration with current settings', |
| onclick: () => { |
| if ( |
| confirm( |
| `Overwrite config "${item.title}" with current settings?`, |
| ) |
| ) { |
| recordConfigStore.overwrite( |
| globals.state.recordConfig, |
| item.key, |
| ); |
| globals.dispatch( |
| Actions.setRecordConfig({ |
| config: item.config, |
| configType: {type: 'NAMED', name: item.title}, |
| }), |
| ); |
| raf.scheduleFullRedraw(); |
| } |
| }, |
| }, |
| m('i.material-icons', 'save'), |
| ), |
| m( |
| 'button', |
| { |
| class: 'config-button', |
| title: 'Remove configuration', |
| onclick: () => { |
| recordConfigStore.delete(item.key); |
| raf.scheduleFullRedraw(); |
| }, |
| }, |
| m('i.material-icons', 'delete'), |
| ), |
| ]), |
| ); |
| |
| const errorItems = []; |
| for (const extraKey of validated.extraKeys) { |
| errorItems.push(m('li', `${extraKey} is unrecognised`)); |
| } |
| for (const invalidKey of validated.invalidKeys) { |
| errorItems.push(m('li', `${invalidKey} contained an invalid value`)); |
| } |
| |
| if (errorItems.length > 0) { |
| configs.push( |
| m( |
| '.parsing-errors', |
| 'One or more errors have been found while loading configuration "' + |
| item.title + |
| '". Loading is possible, but make sure to check ' + |
| 'the settings afterwards.', |
| m('ul', errorItems), |
| ), |
| ); |
| } |
| } |
| return configs; |
| } |
| |
| export const ConfigTitleState = { |
| title: '', |
| getTitle: () => { |
| return ConfigTitleState.title; |
| }, |
| setTitle: (value: string) => { |
| ConfigTitleState.title = value; |
| }, |
| clearTitle: () => { |
| ConfigTitleState.title = ''; |
| }, |
| }; |
| |
| export function Configurations(cssClass: string) { |
| const canSave = recordConfigStore.canSave(ConfigTitleState.getTitle()); |
| return m( |
| `.record-section${cssClass}`, |
| m('header', 'Save and load configurations'), |
| m('.input-config', [ |
| m('input', { |
| value: ConfigTitleState.title, |
| placeholder: 'Title for config', |
| oninput() { |
| ConfigTitleState.setTitle(this.value); |
| raf.scheduleFullRedraw(); |
| }, |
| }), |
| m( |
| 'button', |
| { |
| class: 'config-button', |
| disabled: !canSave, |
| title: canSave |
| ? 'Save current config' |
| : 'Duplicate name, saving disabled', |
| onclick: () => { |
| recordConfigStore.save( |
| globals.state.recordConfig, |
| ConfigTitleState.getTitle(), |
| ); |
| raf.scheduleFullRedraw(); |
| ConfigTitleState.clearTitle(); |
| }, |
| }, |
| m('i.material-icons', 'save'), |
| ), |
| m( |
| 'button', |
| { |
| class: 'config-button', |
| title: 'Clear current configuration', |
| onclick: () => { |
| if ( |
| confirm( |
| 'Current configuration will be cleared. ' + 'Are you sure?', |
| ) |
| ) { |
| globals.dispatch( |
| Actions.setRecordConfig({ |
| config: createEmptyRecordConfig(), |
| configType: {type: 'NONE'}, |
| }), |
| ); |
| raf.scheduleFullRedraw(); |
| } |
| }, |
| }, |
| m('i.material-icons', 'delete_forever'), |
| ), |
| ]), |
| displayRecordConfigs(), |
| ); |
| } |
| |
| function BufferUsageProgressBar() { |
| if (!globals.state.recordingInProgress) return []; |
| |
| const bufferUsage = globals.bufferUsage ?? 0.0; |
| // Buffer usage is not available yet on Android. |
| if (bufferUsage === 0) return []; |
| |
| return m( |
| 'label', |
| 'Buffer usage: ', |
| m('progress', {max: 100, value: bufferUsage * 100}), |
| ); |
| } |
| |
| function RecordingNotes() { |
| const sideloadUrl = |
| 'https://perfetto.dev/docs/contributing/build-instructions#get-the-code'; |
| const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing'; |
| const cmdlineUrl = |
| 'https://perfetto.dev/docs/quickstart/android-tracing#perfetto-cmdline'; |
| const extensionURL = `https://chrome.google.com/webstore/detail/perfetto-ui/lfmkphfpdbjijhpomgecfikhfohaoine`; |
| |
| const notes: m.Children = []; |
| |
| const msgFeatNotSupported = m( |
| 'span', |
| `Some probes are only supported in Perfetto versions running |
| on Android Q+. `, |
| ); |
| |
| const msgPerfettoNotSupported = m( |
| 'span', |
| `Perfetto is not supported natively before Android P. `, |
| ); |
| |
| const msgSideload = m( |
| 'span', |
| `If you have a rooted device you can `, |
| m( |
| 'a', |
| {href: sideloadUrl, target: '_blank'}, |
| `sideload the latest version of |
| Perfetto.`, |
| ), |
| ); |
| |
| const msgRecordingNotSupported = m( |
| '.note', |
| `Recording Perfetto traces from the UI is not supported natively |
| before Android Q. If you are using a P device, please select 'Android P' |
| as the 'Target Platform' and `, |
| m( |
| 'a', |
| {href: cmdlineUrl, target: '_blank'}, |
| `collect the trace using ADB.`, |
| ), |
| ); |
| |
| const msgChrome = m( |
| '.note', |
| `To trace Chrome from the Perfetto UI, you need to install our `, |
| m('a', {href: extensionURL, target: '_blank'}, 'Chrome extension'), |
| ' and then reload this page.', |
| ); |
| |
| const msgLinux = m( |
| '.note', |
| `Use this `, |
| m('a', {href: linuxUrl, target: '_blank'}, `quickstart guide`), |
| ` to get started with tracing on Linux.`, |
| ); |
| |
| const msgLongTraces = m( |
| '.note', |
| `Recording in long trace mode through the UI is not supported. Please copy |
| the command and `, |
| m( |
| 'a', |
| {href: cmdlineUrl, target: '_blank'}, |
| `collect the trace using ADB.`, |
| ), |
| ); |
| |
| const msgZeroProbes = m( |
| '.note', |
| "It looks like you didn't add any probes. " + |
| 'Please add at least one to get a non-empty trace.', |
| ); |
| |
| if (!hasActiveProbes(globals.state.recordConfig)) { |
| notes.push(msgZeroProbes); |
| } |
| |
| if (isAdbTarget(globals.state.recordingTarget)) { |
| notes.push(msgRecordingNotSupported); |
| } |
| switch (globals.state.recordingTarget.os) { |
| case 'Q': |
| break; |
| case 'P': |
| notes.push(m('.note', msgFeatNotSupported, msgSideload)); |
| break; |
| case 'O': |
| notes.push(m('.note', msgPerfettoNotSupported, msgSideload)); |
| break; |
| case 'L': |
| notes.push(msgLinux); |
| break; |
| case 'C': |
| if (!globals.state.extensionInstalled) notes.push(msgChrome); |
| break; |
| case 'CrOS': |
| if (!globals.state.extensionInstalled) notes.push(msgChrome); |
| break; |
| default: |
| } |
| if (globals.state.recordConfig.mode === 'LONG_TRACE') { |
| notes.unshift(msgLongTraces); |
| } |
| |
| return notes.length > 0 ? m('div', notes) : []; |
| } |
| |
| function RecordingSnippet() { |
| const target = globals.state.recordingTarget; |
| |
| // We don't need commands to start tracing on chrome |
| if (isChromeTarget(target)) { |
| return globals.state.extensionInstalled && |
| !globals.state.recordingInProgress |
| ? m( |
| 'div', |
| m( |
| 'label', |
| `To trace Chrome from the Perfetto UI you just have to press |
| 'Start Recording'.`, |
| ), |
| ) |
| : []; |
| } |
| return m(CodeSnippet, {text: getRecordCommand(target)}); |
| } |
| |
| function getRecordCommand(target: RecordingTarget) { |
| const data = globals.trackDataStore.get('config') as { |
| commandline: string; |
| pbtxt: string; |
| pbBase64: string; |
| } | null; |
| |
| const cfg = globals.state.recordConfig; |
| let time = cfg.durationMs / 1000; |
| |
| if (time > MAX_TIME) { |
| time = MAX_TIME; |
| } |
| |
| const pbBase64 = data ? data.pbBase64 : ''; |
| const pbtx = data ? data.pbtxt : ''; |
| let cmd = ''; |
| if (isAndroidP(target)) { |
| cmd += `echo '${pbBase64}' | \n`; |
| cmd += 'base64 --decode | \n'; |
| cmd += 'adb shell "perfetto -c - -o /data/misc/perfetto-traces/trace"\n'; |
| } else { |
| cmd += isAndroidTarget(target) |
| ? 'adb shell perfetto \\\n' |
| : 'perfetto \\\n'; |
| cmd += ' -c - --txt \\\n'; |
| cmd += ' -o /data/misc/perfetto-traces/trace \\\n'; |
| cmd += '<<EOF\n\n'; |
| cmd += pbtx; |
| cmd += '\nEOF\n'; |
| } |
| return cmd; |
| } |
| |
| function recordingButtons() { |
| const state = globals.state; |
| const target = state.recordingTarget; |
| const recInProgress = state.recordingInProgress; |
| |
| const start = m( |
| `button`, |
| { |
| class: recInProgress ? '' : 'selected', |
| onclick: onStartRecordingPressed, |
| }, |
| 'Start Recording', |
| ); |
| |
| const buttons: m.Children = []; |
| |
| if (isAndroidTarget(target)) { |
| if ( |
| !recInProgress && |
| isAdbTarget(target) && |
| globals.state.recordConfig.mode !== 'LONG_TRACE' |
| ) { |
| buttons.push(start); |
| } |
| } else if (isChromeTarget(target) && state.extensionInstalled) { |
| buttons.push(start); |
| } |
| return m('.button', buttons); |
| } |
| |
| function StopCancelButtons() { |
| if (!globals.state.recordingInProgress) return []; |
| |
| const stop = m( |
| `button.selected`, |
| {onclick: () => globals.dispatch(Actions.stopRecording({}))}, |
| 'Stop', |
| ); |
| |
| const cancel = m( |
| `button`, |
| {onclick: () => globals.dispatch(Actions.cancelRecording({}))}, |
| 'Cancel', |
| ); |
| |
| return [stop, cancel]; |
| } |
| |
| function onStartRecordingPressed() { |
| location.href = '#!/record/instructions'; |
| raf.scheduleFullRedraw(); |
| autosaveConfigStore.save(globals.state.recordConfig); |
| |
| const target = globals.state.recordingTarget; |
| if (isAndroidTarget(target) || isChromeTarget(target)) { |
| globals.logging.logEvent('Record Trace', `Record trace (${target.os})`); |
| globals.dispatch(Actions.startRecording({})); |
| } |
| } |
| |
| function RecordingStatusLabel() { |
| const recordingStatus = globals.state.recordingStatus; |
| if (!recordingStatus) return []; |
| return m('label', recordingStatus); |
| } |
| |
| export function ErrorLabel() { |
| const lastRecordingError = globals.state.lastRecordingError; |
| if (!lastRecordingError) return []; |
| return m('label.error-label', `Error: ${lastRecordingError}`); |
| } |
| |
| function recordingLog() { |
| const logs = globals.recordingLog; |
| if (logs === undefined) return []; |
| return m('.code-snippet.no-top-bar', m('code', logs)); |
| } |
| |
| // The connection must be done in the frontend. After it, the serial ID will |
| // be inserted in the state, and the worker will be able to connect to the |
| // correct device. |
| async function addAndroidDevice() { |
| let device: USBDevice; |
| try { |
| device = await new AdbOverWebUsb().findDevice(); |
| } catch (e) { |
| const err = `No device found: ${e.name}: ${e.message}`; |
| console.error(err, e); |
| alert(err); |
| return; |
| } |
| |
| if (!device.serialNumber) { |
| console.error('serial number undefined'); |
| return; |
| } |
| |
| // After the user has selected a device with the chrome UI, it will be |
| // available when listing all the available device from WebUSB. Therefore, |
| // we update the list of available devices. |
| await updateAvailableAdbDevices(device.serialNumber); |
| } |
| |
| // We really should be getting the API version from the adb target, but |
| // currently its too complicated to do that (== most likely, we need to finish |
| // recordingV2 migration). For now, add an escape hatch to use Android S as a |
| // default, given that the main features we want are gated by API level 31 and S |
| // is old enough to be the default most of the time. |
| const USE_ANDROID_S_AS_DEFAULT_FLAG = featureFlags.register({ |
| id: 'recordingPageUseSAsDefault', |
| name: 'Use Android S as a default recording target', |
| description: 'Use Android S as a default recording target instead of Q', |
| defaultValue: false, |
| }); |
| |
| export async function updateAvailableAdbDevices( |
| preferredDeviceSerial?: string, |
| ) { |
| const devices = await new AdbOverWebUsb().getPairedDevices(); |
| |
| let recordingTarget: AdbRecordingTarget | undefined = undefined; |
| |
| const availableAdbDevices: AdbRecordingTarget[] = []; |
| devices.forEach((d) => { |
| if (d.productName && d.serialNumber) { |
| // TODO(nicomazz): At this stage, we can't know the OS version, so we |
| // assume it is 'Q'. This can create problems with devices with an old |
| // version of perfetto. The os detection should be done after the adb |
| // connection, from adb_record_controller |
| availableAdbDevices.push({ |
| name: d.productName, |
| serial: d.serialNumber, |
| os: USE_ANDROID_S_AS_DEFAULT_FLAG.get() ? 'S' : 'Q', |
| }); |
| if (preferredDeviceSerial && preferredDeviceSerial === d.serialNumber) { |
| recordingTarget = availableAdbDevices[availableAdbDevices.length - 1]; |
| } |
| } |
| }); |
| |
| globals.dispatch( |
| Actions.setAvailableAdbDevices({devices: availableAdbDevices}), |
| ); |
| selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget); |
| raf.scheduleFullRedraw(); |
| return availableAdbDevices; |
| } |
| |
| function selectAndroidDeviceIfAvailable( |
| availableAdbDevices: AdbRecordingTarget[], |
| recordingTarget?: RecordingTarget, |
| ) { |
| if (!recordingTarget) { |
| recordingTarget = globals.state.recordingTarget; |
| } |
| const deviceConnected = isAdbTarget(recordingTarget); |
| const connectedDeviceDisconnected = |
| deviceConnected && |
| availableAdbDevices.find( |
| (e) => e.serial === (recordingTarget as AdbRecordingTarget).serial, |
| ) === undefined; |
| |
| if (availableAdbDevices.length) { |
| // If there's an Android device available and the current selection isn't |
| // one, select the Android device by default. If the current device isn't |
| // available anymore, but another Android device is, select the other |
| // Android device instead. |
| if (!deviceConnected || connectedDeviceDisconnected) { |
| recordingTarget = availableAdbDevices[0]; |
| } |
| |
| globals.dispatch(Actions.setRecordingTarget({target: recordingTarget})); |
| return; |
| } |
| |
| // If the currently selected device was disconnected, reset the recording |
| // target to the default one. |
| if (connectedDeviceDisconnected) { |
| globals.dispatch( |
| Actions.setRecordingTarget({target: getDefaultRecordingTargets()[0]}), |
| ); |
| } |
| } |
| |
| function recordMenu(routePage: string) { |
| const target = globals.state.recordingTarget; |
| const chromeProbe = m( |
| 'a[href="#!/record/chrome"]', |
| m( |
| `li${routePage === 'chrome' ? '.active' : ''}`, |
| m('i.material-icons', 'laptop_chromebook'), |
| m('.title', 'Chrome'), |
| m('.sub', 'Chrome traces'), |
| ), |
| ); |
| const cpuProbe = m( |
| 'a[href="#!/record/cpu"]', |
| m( |
| `li${routePage === 'cpu' ? '.active' : ''}`, |
| m('i.material-icons', 'subtitles'), |
| m('.title', 'CPU'), |
| m('.sub', 'CPU usage, scheduling, wakeups'), |
| ), |
| ); |
| const gpuProbe = m( |
| 'a[href="#!/record/gpu"]', |
| m( |
| `li${routePage === 'gpu' ? '.active' : ''}`, |
| m('i.material-icons', 'aspect_ratio'), |
| m('.title', 'GPU'), |
| m('.sub', 'GPU frequency, memory'), |
| ), |
| ); |
| const powerProbe = m( |
| 'a[href="#!/record/power"]', |
| m( |
| `li${routePage === 'power' ? '.active' : ''}`, |
| m('i.material-icons', 'battery_charging_full'), |
| m('.title', 'Power'), |
| m('.sub', 'Battery and other energy counters'), |
| ), |
| ); |
| const memoryProbe = m( |
| 'a[href="#!/record/memory"]', |
| m( |
| `li${routePage === 'memory' ? '.active' : ''}`, |
| m('i.material-icons', 'memory'), |
| m('.title', 'Memory'), |
| m('.sub', 'Physical mem, VM, LMK'), |
| ), |
| ); |
| const androidProbe = m( |
| 'a[href="#!/record/android"]', |
| m( |
| `li${routePage === 'android' ? '.active' : ''}`, |
| m('i.material-icons', 'android'), |
| m('.title', 'Android apps & svcs'), |
| m('.sub', 'atrace and logcat'), |
| ), |
| ); |
| const advancedProbe = m( |
| 'a[href="#!/record/advanced"]', |
| m( |
| `li${routePage === 'advanced' ? '.active' : ''}`, |
| m('i.material-icons', 'settings'), |
| m('.title', 'Advanced settings'), |
| m('.sub', 'Complicated stuff for wizards'), |
| ), |
| ); |
| const tracePerfProbe = m( |
| 'a[href="#!/record/tracePerf"]', |
| m( |
| `li${routePage === 'tracePerf' ? '.active' : ''}`, |
| m('i.material-icons', 'full_stacked_bar_chart'), |
| m('.title', 'Stack Samples'), |
| m('.sub', 'Lightweight stack polling'), |
| ), |
| ); |
| const etwProbe = m( |
| 'a[href="#!/record/etw"]', |
| m( |
| `li${routePage === 'etw' ? '.active' : ''}`, |
| m('i.material-icons', 'subtitles'), |
| m('.title', 'ETW Tracing Config'), |
| m('.sub', 'Context switch, Thread state'), |
| ), |
| ); |
| const recInProgress = globals.state.recordingInProgress; |
| |
| const probes = []; |
| if (isCrOSTarget(target) || isLinuxTarget(target)) { |
| probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe); |
| } else if (isChromeTarget(target)) { |
| probes.push(chromeProbe); |
| } else if (isWindowsTarget(target)) { |
| probes.push(chromeProbe, etwProbe); |
| } else { |
| probes.push( |
| cpuProbe, |
| gpuProbe, |
| powerProbe, |
| memoryProbe, |
| androidProbe, |
| chromeProbe, |
| tracePerfProbe, |
| advancedProbe, |
| ); |
| } |
| |
| return m( |
| '.record-menu', |
| { |
| class: recInProgress ? 'disabled' : '', |
| onclick: () => raf.scheduleFullRedraw(), |
| }, |
| m('header', 'Trace config'), |
| m( |
| 'ul', |
| m( |
| 'a[href="#!/record/buffers"]', |
| m( |
| `li${routePage === 'buffers' ? '.active' : ''}`, |
| m('i.material-icons', 'tune'), |
| m('.title', 'Recording settings'), |
| m('.sub', 'Buffer mode, size and duration'), |
| ), |
| ), |
| m( |
| 'a[href="#!/record/instructions"]', |
| m( |
| `li${routePage === 'instructions' ? '.active' : ''}`, |
| m('i.material-icons-filled.rec', 'fiber_manual_record'), |
| m('.title', 'Recording command'), |
| m('.sub', 'Manually record trace'), |
| ), |
| ), |
| PERSIST_CONFIG_FLAG.get() |
| ? m( |
| 'a[href="#!/record/config"]', |
| { |
| onclick: () => { |
| recordConfigStore.reloadFromLocalStorage(); |
| }, |
| }, |
| m( |
| `li${routePage === 'config' ? '.active' : ''}`, |
| m('i.material-icons', 'save'), |
| m('.title', 'Saved configs'), |
| m('.sub', 'Manage local configs'), |
| ), |
| ) |
| : null, |
| ), |
| m('header', 'Probes'), |
| m('ul', probes), |
| ); |
| } |
| |
| export function maybeGetActiveCss(routePage: string, section: string): string { |
| return routePage === section ? '.active' : ''; |
| } |
| |
| export const RecordPage = createPage({ |
| view({attrs}: m.Vnode<PageAttrs>) { |
| const pages: m.Children = []; |
| // we need to remove the `/` character from the route |
| let routePage = attrs.subpage ? attrs.subpage.substr(1) : ''; |
| if (!RECORDING_SECTIONS.includes(routePage)) { |
| routePage = 'buffers'; |
| } |
| pages.push(recordMenu(routePage)); |
| |
| pages.push( |
| m(RecordingSettings, { |
| dataSources: [], |
| cssClass: maybeGetActiveCss(routePage, 'buffers'), |
| } as RecordingSectionAttrs), |
| ); |
| pages.push(Instructions(maybeGetActiveCss(routePage, 'instructions'))); |
| pages.push(Configurations(maybeGetActiveCss(routePage, 'config'))); |
| |
| const settingsSections = new Map([ |
| ['cpu', CpuSettings], |
| ['gpu', GpuSettings], |
| ['power', PowerSettings], |
| ['memory', MemorySettings], |
| ['android', AndroidSettings], |
| ['chrome', ChromeSettings], |
| ['tracePerf', LinuxPerfSettings], |
| ['advanced', AdvancedSettings], |
| ['etw', EtwSettings], |
| ]); |
| for (const [section, component] of settingsSections.entries()) { |
| pages.push( |
| m(component, { |
| dataSources: [], |
| cssClass: maybeGetActiveCss(routePage, section), |
| } as RecordingSectionAttrs), |
| ); |
| } |
| |
| if (isChromeTarget(globals.state.recordingTarget)) { |
| globals.dispatch(Actions.setFetchChromeCategories({fetch: true})); |
| } |
| |
| return m( |
| '.record-page', |
| globals.state.recordingInProgress ? m('.hider') : [], |
| m( |
| '.record-container', |
| RecordHeader(), |
| m('.record-container-content', recordMenu(routePage), pages), |
| ), |
| ); |
| }, |
| }); |