• 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 {assertExists, assertTrue} from '../base/logging';
18import {isString} from '../base/object_utils';
19import {Actions} from '../common/actions';
20import {getCurrentChannel} from '../common/channels';
21import {TRACE_SUFFIX} from '../common/constants';
22import {ConversionJobStatus} from '../common/conversion_jobs';
23import {
24  disableMetatracingAndGetTrace,
25  enableMetatracing,
26  isMetatracingEnabled,
27} from '../common/metatracing';
28import {EngineMode} from '../common/state';
29import {featureFlags} from '../core/feature_flags';
30import {raf} from '../core/raf_scheduler';
31import {SCM_REVISION, VERSION} from '../gen/perfetto_version';
32import {EngineBase} from '../trace_processor/engine';
33import {showModal} from '../widgets/modal';
34
35import {Animation} from './animation';
36import {downloadData, downloadUrl} from './download_utils';
37import {globals} from './globals';
38import {toggleHelp} from './help_modal';
39import {
40  isLegacyTrace,
41  openFileWithLegacyTraceViewer,
42} from './legacy_trace_viewer';
43import {Router} from './router';
44import {createTraceLink, isDownloadable, shareTrace} from './trace_attrs';
45import {
46  convertToJson,
47  convertTraceToJsonAndDownload,
48  convertTraceToSystraceAndDownload,
49} from './trace_converter';
50import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
51
52const GITILES_URL =
53  'https://android.googlesource.com/platform/external/perfetto';
54
55let lastTabTitle = '';
56
57function getBugReportUrl(): string {
58  if (globals.isInternalUser) {
59    return 'https://goto.google.com/perfetto-ui-bug';
60  } else {
61    return 'https://github.com/google/perfetto/issues/new';
62  }
63}
64
65const HIRING_BANNER_FLAG = featureFlags.register({
66  id: 'showHiringBanner',
67  name: 'Show hiring banner',
68  description: 'Show the "We\'re hiring" banner link in the side bar.',
69  defaultValue: false,
70});
71
72const WIDGETS_PAGE_IN_NAV_FLAG = featureFlags.register({
73  id: 'showWidgetsPageInNav',
74  name: 'Show widgets page',
75  description: 'Show a link to the widgets page in the side bar.',
76  defaultValue: false,
77});
78
79const PLUGINS_PAGE_IN_NAV_FLAG = featureFlags.register({
80  id: 'showPluginsPageInNav',
81  name: 'Show plugins page',
82  description: 'Show a link to the plugins page in the side bar.',
83  defaultValue: false,
84});
85
86const INSIGHTS_PAGE_IN_NAV_FLAG = featureFlags.register({
87  id: 'showInsightsPageInNav',
88  name: 'Show insights page',
89  description: 'Show a link to the insights page in the side bar.',
90  defaultValue: false,
91});
92
93const VIZ_PAGE_IN_NAV_FLAG = featureFlags.register({
94  id: 'showVizPageInNav',
95  name: 'Show viz page',
96  description: 'Show a link to the viz page in the side bar.',
97  defaultValue: true,
98});
99
100function shouldShowHiringBanner(): boolean {
101  return globals.isInternalUser && HIRING_BANNER_FLAG.get();
102}
103
104export const EXAMPLE_ANDROID_TRACE_URL =
105  'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
106
107export const EXAMPLE_CHROME_TRACE_URL =
108  'https://storage.googleapis.com/perfetto-misc/chrome_example_wikipedia.perfetto_trace.gz';
109
110interface SectionItem {
111  t: string;
112  a: string | ((e: Event) => void);
113  i: string;
114  isPending?: () => boolean;
115  isVisible?: () => boolean;
116  internalUserOnly?: boolean;
117  checkDownloadDisabled?: boolean;
118  checkMetatracingEnabled?: boolean;
119  checkMetatracingDisabled?: boolean;
120}
121
122interface Section {
123  title: string;
124  summary: string;
125  items: SectionItem[];
126  expanded?: boolean;
127  hideIfNoTraceLoaded?: boolean;
128  appendOpenedTraceTitle?: boolean;
129}
130
131function getSections(): Section[] {
132  return [
133    {
134      title: 'Navigation',
135      summary: 'Open or record a new trace',
136      expanded: true,
137      items: [
138        {t: 'Open trace file', a: popupFileSelectionDialog, i: 'folder_open'},
139        {
140          t: 'Open with legacy UI',
141          a: popupFileSelectionDialogOldUI,
142          i: 'filter_none',
143        },
144        {t: 'Record new trace', a: navigateRecord, i: 'fiber_smart_record'},
145        {
146          t: 'Widgets',
147          a: navigateWidgets,
148          i: 'widgets',
149          isVisible: () => WIDGETS_PAGE_IN_NAV_FLAG.get(),
150        },
151        {
152          t: 'Plugins',
153          a: navigatePlugins,
154          i: 'extension',
155          isVisible: () => PLUGINS_PAGE_IN_NAV_FLAG.get(),
156        },
157      ],
158    },
159
160    {
161      title: 'Current Trace',
162      summary: 'Actions on the current trace',
163      expanded: true,
164      hideIfNoTraceLoaded: true,
165      appendOpenedTraceTitle: true,
166      items: [
167        {t: 'Show timeline', a: navigateViewer, i: 'line_style'},
168        {
169          t: 'Share',
170          a: handleShareTrace,
171          i: 'share',
172          internalUserOnly: true,
173          isPending: () =>
174            globals.getConversionJobStatus('create_permalink') ===
175            ConversionJobStatus.InProgress,
176        },
177        {
178          t: 'Download',
179          a: downloadTrace,
180          i: 'file_download',
181          checkDownloadDisabled: true,
182        },
183        {t: 'Query (SQL)', a: navigateQuery, i: 'database'},
184        {
185          t: 'Insights',
186          a: navigateInsights,
187          i: 'insights',
188          isVisible: () => INSIGHTS_PAGE_IN_NAV_FLAG.get(),
189        },
190        {
191          t: 'Viz',
192          a: navigateViz,
193          i: 'area_chart',
194          isVisible: () => VIZ_PAGE_IN_NAV_FLAG.get(),
195        },
196        {t: 'Metrics', a: navigateMetrics, i: 'speed'},
197        {t: 'Info and stats', a: navigateInfo, i: 'info'},
198      ],
199    },
200
201    {
202      title: 'Convert trace',
203      summary: 'Convert to other formats',
204      expanded: true,
205      hideIfNoTraceLoaded: true,
206      items: [
207        {
208          t: 'Switch to legacy UI',
209          a: openCurrentTraceWithOldUI,
210          i: 'filter_none',
211          isPending: () =>
212            globals.getConversionJobStatus('open_in_legacy') ===
213            ConversionJobStatus.InProgress,
214        },
215        {
216          t: 'Convert to .json',
217          a: convertTraceToJson,
218          i: 'file_download',
219          isPending: () =>
220            globals.getConversionJobStatus('convert_json') ===
221            ConversionJobStatus.InProgress,
222          checkDownloadDisabled: true,
223        },
224
225        {
226          t: 'Convert to .systrace',
227          a: convertTraceToSystrace,
228          i: 'file_download',
229          isVisible: () => globals.hasFtrace,
230          isPending: () =>
231            globals.getConversionJobStatus('convert_systrace') ===
232            ConversionJobStatus.InProgress,
233          checkDownloadDisabled: true,
234        },
235      ],
236    },
237
238    {
239      title: 'Example Traces',
240      expanded: true,
241      summary: 'Open an example trace',
242      items: [
243        {
244          t: 'Open Android example',
245          a: openTraceUrl(EXAMPLE_ANDROID_TRACE_URL),
246          i: 'description',
247        },
248        {
249          t: 'Open Chrome example',
250          a: openTraceUrl(EXAMPLE_CHROME_TRACE_URL),
251          i: 'description',
252        },
253      ],
254    },
255
256    {
257      title: 'Support',
258      expanded: true,
259      summary: 'Documentation & Bugs',
260      items: [
261        {t: 'Keyboard shortcuts', a: openHelp, i: 'help'},
262        {t: 'Documentation', a: 'https://perfetto.dev/docs', i: 'find_in_page'},
263        {t: 'Flags', a: navigateFlags, i: 'emoji_flags'},
264        {
265          t: 'Report a bug',
266          a: getBugReportUrl(),
267          i: 'bug_report',
268        },
269        {
270          t: 'Record metatrace',
271          a: recordMetatrace,
272          i: 'fiber_smart_record',
273          checkMetatracingDisabled: true,
274        },
275        {
276          t: 'Finalise metatrace',
277          a: finaliseMetatrace,
278          i: 'file_download',
279          checkMetatracingEnabled: true,
280        },
281      ],
282    },
283  ];
284}
285
286function openHelp(e: Event) {
287  e.preventDefault();
288  toggleHelp();
289}
290
291function getFileElement(): HTMLInputElement {
292  return assertExists(
293    document.querySelector<HTMLInputElement>('input[type=file]'),
294  );
295}
296
297function popupFileSelectionDialog(e: Event) {
298  e.preventDefault();
299  delete getFileElement().dataset['useCatapultLegacyUi'];
300  getFileElement().click();
301}
302
303function popupFileSelectionDialogOldUI(e: Event) {
304  e.preventDefault();
305  getFileElement().dataset['useCatapultLegacyUi'] = '1';
306  getFileElement().click();
307}
308
309function downloadTraceFromUrl(url: string): Promise<File> {
310  return m.request({
311    method: 'GET',
312    url,
313    // TODO(hjd): Once mithril is updated we can use responseType here rather
314    // than using config and remove the extract below.
315    config: (xhr) => {
316      xhr.responseType = 'blob';
317      xhr.onprogress = (progress) => {
318        const percent = ((100 * progress.loaded) / progress.total).toFixed(1);
319        globals.dispatch(
320          Actions.updateStatus({
321            msg: `Downloading trace ${percent}%`,
322            timestamp: Date.now() / 1000,
323          }),
324        );
325      };
326    },
327    extract: (xhr) => {
328      return xhr.response;
329    },
330  });
331}
332
333export async function getCurrentTrace(): Promise<Blob> {
334  // Caller must check engine exists.
335  const engine = assertExists(globals.getCurrentEngine());
336  const src = engine.source;
337  if (src.type === 'ARRAY_BUFFER') {
338    return new Blob([src.buffer]);
339  } else if (src.type === 'FILE') {
340    return src.file;
341  } else if (src.type === 'URL') {
342    return downloadTraceFromUrl(src.url);
343  } else {
344    throw new Error(`Loading to catapult from source with type ${src.type}`);
345  }
346}
347
348function openCurrentTraceWithOldUI(e: Event) {
349  e.preventDefault();
350  assertTrue(isTraceLoaded());
351  globals.logging.logEvent('Trace Actions', 'Open current trace in legacy UI');
352  if (!isTraceLoaded()) return;
353  getCurrentTrace()
354    .then((file) => {
355      openInOldUIWithSizeCheck(file);
356    })
357    .catch((error) => {
358      throw new Error(`Failed to get current trace ${error}`);
359    });
360}
361
362function convertTraceToSystrace(e: Event) {
363  e.preventDefault();
364  assertTrue(isTraceLoaded());
365  globals.logging.logEvent('Trace Actions', 'Convert to .systrace');
366  if (!isTraceLoaded()) return;
367  getCurrentTrace()
368    .then((file) => {
369      convertTraceToSystraceAndDownload(file);
370    })
371    .catch((error) => {
372      throw new Error(`Failed to get current trace ${error}`);
373    });
374}
375
376function convertTraceToJson(e: Event) {
377  e.preventDefault();
378  assertTrue(isTraceLoaded());
379  globals.logging.logEvent('Trace Actions', 'Convert to .json');
380  if (!isTraceLoaded()) return;
381  getCurrentTrace()
382    .then((file) => {
383      convertTraceToJsonAndDownload(file);
384    })
385    .catch((error) => {
386      throw new Error(`Failed to get current trace ${error}`);
387    });
388}
389
390export function isTraceLoaded(): boolean {
391  return globals.getCurrentEngine() !== undefined;
392}
393
394export function openTraceUrl(url: string): (e: Event) => void {
395  return (e) => {
396    globals.logging.logEvent('Trace Actions', 'Open example trace');
397    e.preventDefault();
398    globals.dispatch(Actions.openTraceFromUrl({url}));
399  };
400}
401
402function onInputElementFileSelectionChanged(e: Event) {
403  if (!(e.target instanceof HTMLInputElement)) {
404    throw new Error('Not an input element');
405  }
406  if (!e.target.files) return;
407  const file = e.target.files[0];
408  // Reset the value so onchange will be fired with the same file.
409  e.target.value = '';
410
411  if (e.target.dataset['useCatapultLegacyUi'] === '1') {
412    openWithLegacyUi(file);
413    return;
414  }
415
416  globals.logging.logEvent('Trace Actions', 'Open trace from file');
417  globals.dispatch(Actions.openTraceFromFile({file}));
418}
419
420async function openWithLegacyUi(file: File) {
421  // Switch back to the old catapult UI.
422  globals.logging.logEvent('Trace Actions', 'Open trace in Legacy UI');
423  if (await isLegacyTrace(file)) {
424    openFileWithLegacyTraceViewer(file);
425    return;
426  }
427  openInOldUIWithSizeCheck(file);
428}
429
430function openInOldUIWithSizeCheck(trace: Blob) {
431  // Perfetto traces smaller than 50mb can be safely opened in the legacy UI.
432  if (trace.size < 1024 * 1024 * 50) {
433    convertToJson(trace);
434    return;
435  }
436
437  // Give the user the option to truncate larger perfetto traces.
438  const size = Math.round(trace.size / (1024 * 1024));
439  showModal({
440    title: 'Legacy UI may fail to open this trace',
441    content: m(
442      'div',
443      m(
444        'p',
445        `This trace is ${size}mb, opening it in the legacy UI ` + `may fail.`,
446      ),
447      m(
448        'p',
449        'More options can be found at ',
450        m(
451          'a',
452          {
453            href: 'https://goto.google.com/opening-large-traces',
454            target: '_blank',
455          },
456          'go/opening-large-traces',
457        ),
458        '.',
459      ),
460    ),
461    buttons: [
462      {
463        text: 'Open full trace (not recommended)',
464        action: () => convertToJson(trace),
465      },
466      {
467        text: 'Open beginning of trace',
468        action: () => convertToJson(trace, /* truncate*/ 'start'),
469      },
470      {
471        text: 'Open end of trace',
472        primary: true,
473        action: () => convertToJson(trace, /* truncate*/ 'end'),
474      },
475    ],
476  });
477  return;
478}
479
480function navigateRecord(e: Event) {
481  e.preventDefault();
482  Router.navigate('#!/record');
483}
484
485function navigateWidgets(e: Event) {
486  e.preventDefault();
487  Router.navigate('#!/widgets');
488}
489
490function navigatePlugins(e: Event) {
491  e.preventDefault();
492  Router.navigate('#!/plugins');
493}
494
495function navigateQuery(e: Event) {
496  e.preventDefault();
497  Router.navigate('#!/query');
498}
499
500function navigateInsights(e: Event) {
501  e.preventDefault();
502  Router.navigate('#!/insights');
503}
504
505function navigateViz(e: Event) {
506  e.preventDefault();
507  Router.navigate('#!/viz');
508}
509
510function navigateFlags(e: Event) {
511  e.preventDefault();
512  Router.navigate('#!/flags');
513}
514
515function navigateMetrics(e: Event) {
516  e.preventDefault();
517  Router.navigate('#!/metrics');
518}
519
520function navigateInfo(e: Event) {
521  e.preventDefault();
522  Router.navigate('#!/info');
523}
524
525function navigateViewer(e: Event) {
526  e.preventDefault();
527  Router.navigate('#!/viewer');
528}
529
530function handleShareTrace(e: Event) {
531  e.preventDefault();
532  shareTrace();
533}
534
535function downloadTrace(e: Event) {
536  e.preventDefault();
537  if (!isDownloadable() || !isTraceLoaded()) return;
538  globals.logging.logEvent('Trace Actions', 'Download trace');
539
540  const engine = globals.getCurrentEngine();
541  if (!engine) return;
542  let url = '';
543  let fileName = `trace${TRACE_SUFFIX}`;
544  const src = engine.source;
545  if (src.type === 'URL') {
546    url = src.url;
547    fileName = url.split('/').slice(-1)[0];
548  } else if (src.type === 'ARRAY_BUFFER') {
549    const blob = new Blob([src.buffer], {type: 'application/octet-stream'});
550    const inputFileName = window.prompt(
551      'Please enter a name for your file or leave blank',
552    );
553    if (inputFileName) {
554      fileName = `${inputFileName}.perfetto_trace.gz`;
555    } else if (src.fileName) {
556      fileName = src.fileName;
557    }
558    url = URL.createObjectURL(blob);
559  } else if (src.type === 'FILE') {
560    const file = src.file;
561    url = URL.createObjectURL(file);
562    fileName = file.name;
563  } else {
564    throw new Error(`Download from ${JSON.stringify(src)} is not supported`);
565  }
566  downloadUrl(fileName, url);
567}
568
569function getCurrentEngine(): EngineBase | undefined {
570  const engineId = globals.getCurrentEngine()?.id;
571  if (engineId === undefined) return undefined;
572  return globals.engines.get(engineId);
573}
574
575function highPrecisionTimersAvailable(): boolean {
576  // High precision timers are available either when the page is cross-origin
577  // isolated or when the trace processor is a standalone binary.
578  return (
579    window.crossOriginIsolated ||
580    globals.getCurrentEngine()?.mode === 'HTTP_RPC'
581  );
582}
583
584function recordMetatrace(e: Event) {
585  e.preventDefault();
586  globals.logging.logEvent('Trace Actions', 'Record metatrace');
587
588  const engine = getCurrentEngine();
589  if (!engine) return;
590
591  if (!highPrecisionTimersAvailable()) {
592    const PROMPT = `High-precision timers are not available to WASM trace processor yet.
593
594Modern browsers restrict high-precision timers to cross-origin-isolated pages.
595As Perfetto UI needs to open traces via postMessage, it can't be cross-origin
596isolated until browsers ship support for
597'Cross-origin-opener-policy: restrict-properties'.
598
599Do you still want to record a metatrace?
600Note that events under timer precision (1ms) will dropped.
601Alternatively, connect to a trace_processor_shell --httpd instance.
602`;
603    showModal({
604      title: `Trace processor doesn't have high-precision timers`,
605      content: m('.modal-pre', PROMPT),
606      buttons: [
607        {
608          text: 'YES, record metatrace',
609          primary: true,
610          action: () => {
611            enableMetatracing();
612            engine.enableMetatrace();
613          },
614        },
615        {
616          text: 'NO, cancel',
617        },
618      ],
619    });
620  } else {
621    engine.enableMetatrace();
622  }
623}
624
625async function finaliseMetatrace(e: Event) {
626  e.preventDefault();
627  globals.logging.logEvent('Trace Actions', 'Finalise metatrace');
628
629  const jsEvents = disableMetatracingAndGetTrace();
630
631  const engine = getCurrentEngine();
632  if (!engine) return;
633
634  const result = await engine.stopAndGetMetatrace();
635  if (result.error.length !== 0) {
636    throw new Error(`Failed to read metatrace: ${result.error}`);
637  }
638
639  downloadData('metatrace', result.metatrace, jsEvents);
640}
641
642const EngineRPCWidget: m.Component = {
643  view() {
644    let cssClass = '';
645    let title = 'Number of pending SQL queries';
646    let label: string;
647    let failed = false;
648    let mode: EngineMode | undefined;
649
650    const engine = globals.state.engine;
651    if (engine !== undefined) {
652      mode = engine.mode;
653      if (engine.failed !== undefined) {
654        cssClass += '.red';
655        title = 'Query engine crashed\n' + engine.failed;
656        failed = true;
657      }
658    }
659
660    // If we don't have an engine yet, guess what will be the mode that will
661    // be used next time we'll create one. Even if we guess it wrong (somehow
662    // trace_controller.ts takes a different decision later, e.g. because the
663    // RPC server is shut down after we load the UI and cached httpRpcState)
664    // this will eventually become  consistent once the engine is created.
665    if (mode === undefined) {
666      if (
667        globals.httpRpcState.connected &&
668        globals.state.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE'
669      ) {
670        mode = 'HTTP_RPC';
671      } else {
672        mode = 'WASM';
673      }
674    }
675
676    if (mode === 'HTTP_RPC') {
677      cssClass += '.green';
678      label = 'RPC';
679      title += '\n(Query engine: native accelerator over HTTP+RPC)';
680    } else {
681      label = 'WSM';
682      title += '\n(Query engine: built-in WASM)';
683    }
684
685    return m(
686      `.dbg-info-square${cssClass}`,
687      {title},
688      m('div', label),
689      m('div', `${failed ? 'FAIL' : globals.numQueuedQueries}`),
690    );
691  },
692};
693
694const ServiceWorkerWidget: m.Component = {
695  view() {
696    let cssClass = '';
697    let title = 'Service Worker: ';
698    let label = 'N/A';
699    const ctl = globals.serviceWorkerController;
700    if (!('serviceWorker' in navigator)) {
701      label = 'N/A';
702      title += 'not supported by the browser (requires HTTPS)';
703    } else if (ctl.bypassed) {
704      label = 'OFF';
705      cssClass = '.red';
706      title += 'Bypassed, using live network. Double-click to re-enable';
707    } else if (ctl.installing) {
708      label = 'UPD';
709      cssClass = '.amber';
710      title += 'Installing / updating ...';
711    } else if (!navigator.serviceWorker.controller) {
712      label = 'N/A';
713      title += 'Not available, using network';
714    } else {
715      label = 'ON';
716      cssClass = '.green';
717      title += 'Serving from cache. Ready for offline use';
718    }
719
720    const toggle = async () => {
721      if (globals.serviceWorkerController.bypassed) {
722        globals.serviceWorkerController.setBypass(false);
723        return;
724      }
725      showModal({
726        title: 'Disable service worker?',
727        content: m(
728          'div',
729          m(
730            'p',
731            `If you continue the service worker will be disabled until
732                      manually re-enabled.`,
733          ),
734          m(
735            'p',
736            `All future requests will be served from the network and the
737                    UI won't be available offline.`,
738          ),
739          m(
740            'p',
741            `You should do this only if you are debugging the UI
742                    or if you are experiencing caching-related problems.`,
743          ),
744          m(
745            'p',
746            `Disabling will cause a refresh of the UI, the current state
747                    will be lost.`,
748          ),
749        ),
750        buttons: [
751          {
752            text: 'Disable and reload',
753            primary: true,
754            action: () => {
755              globals.serviceWorkerController
756                .setBypass(true)
757                .then(() => location.reload());
758            },
759          },
760          {text: 'Cancel'},
761        ],
762      });
763    };
764
765    return m(
766      `.dbg-info-square${cssClass}`,
767      {title, ondblclick: toggle},
768      m('div', 'SW'),
769      m('div', label),
770    );
771  },
772};
773
774const SidebarFooter: m.Component = {
775  view() {
776    return m(
777      '.sidebar-footer',
778      m(EngineRPCWidget),
779      m(ServiceWorkerWidget),
780      m(
781        '.version',
782        m(
783          'a',
784          {
785            href: `${GITILES_URL}/+/${SCM_REVISION}/ui`,
786            title: `Channel: ${getCurrentChannel()}`,
787            target: '_blank',
788          },
789          VERSION,
790        ),
791      ),
792    );
793  },
794};
795
796class HiringBanner implements m.ClassComponent {
797  view() {
798    return m(
799      '.hiring-banner',
800      m(
801        'a',
802        {
803          href: 'http://go/perfetto-open-roles',
804          target: '_blank',
805        },
806        "We're hiring!",
807      ),
808    );
809  }
810}
811
812export class Sidebar implements m.ClassComponent {
813  private _redrawWhileAnimating = new Animation(() => raf.scheduleFullRedraw());
814  view() {
815    if (globals.hideSidebar) return null;
816    const vdomSections = [];
817    for (const section of getSections()) {
818      if (section.hideIfNoTraceLoaded && !isTraceLoaded()) continue;
819      const vdomItems = [];
820      for (const item of section.items) {
821        if (item.isVisible !== undefined && !item.isVisible()) {
822          continue;
823        }
824        let css = '';
825        let attrs = {
826          onclick: typeof item.a === 'function' ? item.a : null,
827          href: isString(item.a) ? item.a : '#',
828          target: isString(item.a) ? '_blank' : null,
829          disabled: false,
830          id: item.t.toLowerCase().replace(/[^\w]/g, '_'),
831        };
832        if (item.isPending && item.isPending()) {
833          attrs.onclick = (e) => e.preventDefault();
834          css = '.pending';
835        }
836        if (item.internalUserOnly && !globals.isInternalUser) {
837          continue;
838        }
839        if (item.checkMetatracingEnabled || item.checkMetatracingDisabled) {
840          if (
841            item.checkMetatracingEnabled === true &&
842            !isMetatracingEnabled()
843          ) {
844            continue;
845          }
846          if (
847            item.checkMetatracingDisabled === true &&
848            isMetatracingEnabled()
849          ) {
850            continue;
851          }
852          if (
853            item.checkMetatracingDisabled &&
854            !highPrecisionTimersAvailable()
855          ) {
856            attrs.disabled = true;
857          }
858        }
859        if (item.checkDownloadDisabled && !isDownloadable()) {
860          attrs = {
861            onclick: (e) => {
862              e.preventDefault();
863              alert('Can not download external trace.');
864            },
865            href: '#',
866            target: null,
867            disabled: true,
868            id: '',
869          };
870        }
871        vdomItems.push(
872          m('li', m(`a${css}`, attrs, m('i.material-icons', item.i), item.t)),
873        );
874      }
875      if (section.appendOpenedTraceTitle) {
876        const engine = globals.state.engine;
877        if (engine !== undefined) {
878          let traceTitle = '';
879          let traceUrl = '';
880          switch (engine.source.type) {
881            case 'FILE':
882              // Split on both \ and / (because C:\Windows\paths\are\like\this).
883              traceTitle = engine.source.file.name.split(/[/\\]/).pop()!;
884              const fileSizeMB = Math.ceil(engine.source.file.size / 1e6);
885              traceTitle += ` (${fileSizeMB} MB)`;
886              break;
887            case 'URL':
888              traceUrl = engine.source.url;
889              traceTitle = traceUrl.split('/').pop()!;
890              break;
891            case 'ARRAY_BUFFER':
892              traceTitle = engine.source.title;
893              traceUrl = engine.source.url || '';
894              const arrayBufferSizeMB = Math.ceil(
895                engine.source.buffer.byteLength / 1e6,
896              );
897              traceTitle += ` (${arrayBufferSizeMB} MB)`;
898              break;
899            case 'HTTP_RPC':
900              traceTitle = `RPC @ ${HttpRpcEngine.hostAndPort}`;
901              break;
902            default:
903              break;
904          }
905          if (traceTitle !== '') {
906            const tabTitle = `${traceTitle} - Perfetto UI`;
907            if (tabTitle !== lastTabTitle) {
908              document.title = lastTabTitle = tabTitle;
909            }
910            vdomItems.unshift(m('li', createTraceLink(traceTitle, traceUrl)));
911          }
912        }
913      }
914      vdomSections.push(
915        m(
916          `section${section.expanded ? '.expanded' : ''}`,
917          m(
918            '.section-header',
919            {
920              onclick: () => {
921                section.expanded = !section.expanded;
922                raf.scheduleFullRedraw();
923              },
924            },
925            m('h1', {title: section.summary}, section.title),
926            m('h2', section.summary),
927          ),
928          m('.section-content', m('ul', vdomItems)),
929        ),
930      );
931    }
932    return m(
933      'nav.sidebar',
934      {
935        class: globals.state.sidebarVisible ? 'show-sidebar' : 'hide-sidebar',
936        // 150 here matches --sidebar-timing in the css.
937        // TODO(hjd): Should link to the CSS variable.
938        ontransitionstart: (e: TransitionEvent) => {
939          if (e.target !== e.currentTarget) return;
940          this._redrawWhileAnimating.start(150);
941        },
942        ontransitionend: (e: TransitionEvent) => {
943          if (e.target !== e.currentTarget) return;
944          this._redrawWhileAnimating.stop();
945        },
946      },
947      shouldShowHiringBanner() ? m(HiringBanner) : null,
948      m(
949        `header.${getCurrentChannel()}`,
950        m(`img[src=${globals.root}assets/brand.png].brand`),
951        m(
952          'button.sidebar-button',
953          {
954            onclick: () => {
955              globals.commandManager.runCommand(
956                'dev.perfetto.CoreCommands#ToggleLeftSidebar',
957              );
958            },
959          },
960          m(
961            'i.material-icons',
962            {
963              title: globals.state.sidebarVisible ? 'Hide menu' : 'Show menu',
964            },
965            'menu',
966          ),
967        ),
968      ),
969      m('input.trace_file[type=file]', {
970        onchange: onInputElementFileSelectionChanged,
971      }),
972      m(
973        '.sidebar-scroll',
974        m('.sidebar-scroll-container', ...vdomSections, m(SidebarFooter)),
975      ),
976    );
977  }
978}
979