• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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