• 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
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