• 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 * as m from 'mithril';
16
17import {assertExists, assertTrue} from '../base/logging';
18import {Actions} from '../common/actions';
19import {getCurrentChannel} from '../common/channels';
20import {TRACE_SUFFIX} from '../common/constants';
21import {ConversionJobStatus} from '../common/conversion_jobs';
22import {EngineMode, TraceArrayBufferSource} from '../common/state';
23import * as version from '../gen/perfetto_version';
24
25import {Animation} from './animation';
26import {onClickCopy} from './clipboard';
27import {globals} from './globals';
28import {toggleHelp} from './help_modal';
29import {
30  isLegacyTrace,
31  openFileWithLegacyTraceViewer,
32} from './legacy_trace_viewer';
33import {showModal} from './modal';
34import {Router} from './router';
35import {isDownloadable, isShareable} from './trace_attrs';
36import {
37  convertToJson,
38  convertTraceToJsonAndDownload,
39  convertTraceToSystraceAndDownload
40} from './trace_converter';
41
42const ALL_PROCESSES_QUERY = 'select name, pid from process order by name;';
43
44const CPU_TIME_FOR_PROCESSES = `
45select
46  process.name,
47  sum(dur)/1e9 as cpu_sec
48from sched
49join thread using(utid)
50join process using(upid)
51group by upid
52order by cpu_sec desc
53limit 100;`;
54
55const CYCLES_PER_P_STATE_PER_CPU = `
56select
57  cpu,
58  freq,
59  dur,
60  sum(dur * freq)/1e6 as mcycles
61from (
62  select
63    cpu,
64    value as freq,
65    lead(ts) over (partition by cpu order by ts) - ts as dur
66  from counter
67  inner join cpu_counter_track on counter.track_id = cpu_counter_track.id
68  where name = 'cpufreq'
69) group by cpu, freq
70order by mcycles desc limit 32;`;
71
72const CPU_TIME_BY_CPU_BY_PROCESS = `
73select
74  process.name as process,
75  thread.name as thread,
76  cpu,
77  sum(dur) / 1e9 as cpu_sec
78from sched
79inner join thread using(utid)
80inner join process using(upid)
81group by utid, cpu
82order by cpu_sec desc
83limit 30;`;
84
85const HEAP_GRAPH_BYTES_PER_TYPE = `
86select
87  o.upid,
88  o.graph_sample_ts,
89  c.name,
90  sum(o.self_size) as total_self_size
91from heap_graph_object o join heap_graph_class c on o.type_id = c.id
92group by
93 o.upid,
94 o.graph_sample_ts,
95 c.name
96order by total_self_size desc
97limit 100;`;
98
99const SQL_STATS = `
100with first as (select started as ts from sqlstats limit 1)
101select query,
102    round((max(ended - started, 0))/1e6) as runtime_ms,
103    round((started - first.ts)/1e6) as t_start_ms
104from sqlstats, first
105order by started desc`;
106
107const GITILES_URL =
108    'https://android.googlesource.com/platform/external/perfetto';
109
110let lastTabTitle = '';
111
112function createCannedQuery(query: string): (_: Event) => void {
113  return (e: Event) => {
114    e.preventDefault();
115    globals.dispatch(Actions.executeQuery({
116      engineId: '0',
117      queryId: 'command',
118      query,
119    }));
120  };
121}
122
123function showDebugTrack(): (_: Event) => void {
124  return (e: Event) => {
125    e.preventDefault();
126    globals.dispatch(Actions.addDebugTrack({
127      engineId: Object.keys(globals.state.engines)[0],
128      name: 'Debug Slices',
129    }));
130  };
131}
132
133const EXAMPLE_ANDROID_TRACE_URL =
134    'https://storage.googleapis.com/perfetto-misc/example_android_trace_15s';
135
136const EXAMPLE_CHROME_TRACE_URL =
137    'https://storage.googleapis.com/perfetto-misc/example_chrome_trace_4s_1.json';
138
139interface SectionItem {
140  t: string;
141  a: string|((e: Event) => void);
142  i: string;
143  isPending?: () => boolean;
144  isVisible?: () => boolean;
145  internalUserOnly?: boolean;
146  checkDownloadDisabled?: boolean;
147}
148
149interface Section {
150  title: string;
151  summary: string;
152  items: SectionItem[];
153  expanded?: boolean;
154  hideIfNoTraceLoaded?: boolean;
155  appendOpenedTraceTitle?: boolean;
156}
157
158const SECTIONS: Section[] = [
159
160  {
161    title: 'Navigation',
162    summary: 'Open or record a new trace',
163    expanded: true,
164    items: [
165      {t: 'Open trace file', a: popupFileSelectionDialog, i: 'folder_open'},
166      {
167        t: 'Open with legacy UI',
168        a: popupFileSelectionDialogOldUI,
169        i: 'filter_none'
170      },
171      {t: 'Record new trace', a: navigateRecord, i: 'fiber_smart_record'},
172    ],
173  },
174
175  {
176    title: 'Current Trace',
177    summary: 'Actions on the current trace',
178    expanded: true,
179    hideIfNoTraceLoaded: true,
180    appendOpenedTraceTitle: true,
181    items: [
182      {t: 'Show timeline', a: navigateViewer, i: 'line_style'},
183      {
184        t: 'Share',
185        a: shareTrace,
186        i: 'share',
187        internalUserOnly: true,
188        isPending: () => globals.getConversionJobStatus('create_permalink') ===
189            ConversionJobStatus.InProgress,
190      },
191      {
192        t: 'Download',
193        a: downloadTrace,
194        i: 'file_download',
195        checkDownloadDisabled: true,
196      },
197      {t: 'Query (SQL)', a: navigateAnalyze, i: 'control_camera'},
198      {t: 'Metrics', a: navigateMetrics, i: 'speed'},
199      {t: 'Info and stats', a: navigateInfo, i: 'info'},
200    ],
201  },
202
203  {
204    title: 'Convert trace',
205    summary: 'Convert to other formats',
206    expanded: true,
207    hideIfNoTraceLoaded: true,
208    items: [
209      {
210        t: 'Switch to legacy UI',
211        a: openCurrentTraceWithOldUI,
212        i: 'filter_none',
213        isPending: () => globals.getConversionJobStatus('open_in_legacy') ===
214            ConversionJobStatus.InProgress,
215      },
216      {
217        t: 'Convert to .json',
218        a: convertTraceToJson,
219        i: 'file_download',
220        isPending: () => 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: () => globals.getConversionJobStatus('convert_systrace') ===
231            ConversionJobStatus.InProgress,
232        checkDownloadDisabled: true,
233      },
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', i: 'find_in_page'},
263      {t: 'Flags', a: navigateFlags, i: 'emoji_flags'},
264      {
265        t: 'Report a bug',
266        a: 'https://goto.google.com/perfetto-ui-bug',
267        i: 'bug_report'
268      },
269    ],
270  },
271
272  {
273    title: 'Sample queries',
274    summary: 'Compute summary statistics',
275    items: [
276      {t: 'Show Debug Track', a: showDebugTrack(), i: 'view_day'},
277      {
278        t: 'All Processes',
279        a: createCannedQuery(ALL_PROCESSES_QUERY),
280        i: 'search'
281      },
282      {
283        t: 'CPU Time by process',
284        a: createCannedQuery(CPU_TIME_FOR_PROCESSES),
285        i: 'search'
286      },
287      {
288        t: 'Cycles by p-state by CPU',
289        a: createCannedQuery(CYCLES_PER_P_STATE_PER_CPU),
290        i: 'search'
291      },
292      {
293        t: 'CPU Time by CPU by process',
294        a: createCannedQuery(CPU_TIME_BY_CPU_BY_PROCESS),
295        i: 'search'
296      },
297      {
298        t: 'Heap Graph: Bytes per type',
299        a: createCannedQuery(HEAP_GRAPH_BYTES_PER_TYPE),
300        i: 'search'
301      },
302      {
303        t: 'Debug SQL performance',
304        a: createCannedQuery(SQL_STATS),
305        i: 'bug_report'
306      },
307    ],
308  },
309
310];
311
312function openHelp(e: Event) {
313  e.preventDefault();
314  toggleHelp();
315}
316
317function getFileElement(): HTMLInputElement {
318  return assertExists(
319      document.querySelector<HTMLInputElement>('input[type=file]'));
320}
321
322function popupFileSelectionDialog(e: Event) {
323  e.preventDefault();
324  delete getFileElement().dataset['useCatapultLegacyUi'];
325  getFileElement().click();
326}
327
328function popupFileSelectionDialogOldUI(e: Event) {
329  e.preventDefault();
330  getFileElement().dataset['useCatapultLegacyUi'] = '1';
331  getFileElement().click();
332}
333
334function downloadTraceFromUrl(url: string): Promise<File> {
335  return m.request({
336    method: 'GET',
337    url,
338    // TODO(hjd): Once mithril is updated we can use responseType here rather
339    // than using config and remove the extract below.
340    config: xhr => {
341      xhr.responseType = 'blob';
342      xhr.onprogress = progress => {
343        const percent = (100 * progress.loaded / progress.total).toFixed(1);
344        globals.dispatch(Actions.updateStatus({
345          msg: `Downloading trace ${percent}%`,
346          timestamp: Date.now() / 1000,
347        }));
348      };
349    },
350    extract: xhr => {
351      return xhr.response;
352    }
353  });
354}
355
356export async function getCurrentTrace(): Promise<Blob> {
357  // Caller must check engine exists.
358  const engine = assertExists(Object.values(globals.state.engines)[0]);
359  const src = engine.source;
360  if (src.type === 'ARRAY_BUFFER') {
361    return new Blob([src.buffer]);
362  } else if (src.type === 'FILE') {
363    return src.file;
364  } else if (src.type === 'URL') {
365    return downloadTraceFromUrl(src.url);
366  } else {
367    throw new Error(`Loading to catapult from source with type ${src.type}`);
368  }
369}
370
371function openCurrentTraceWithOldUI(e: Event) {
372  e.preventDefault();
373  assertTrue(isTraceLoaded());
374  globals.logging.logEvent('Trace Actions', 'Open current trace in legacy UI');
375  if (!isTraceLoaded) return;
376  getCurrentTrace()
377      .then(file => {
378        openInOldUIWithSizeCheck(file);
379      })
380      .catch(error => {
381        throw new Error(`Failed to get current trace ${error}`);
382      });
383}
384
385function convertTraceToSystrace(e: Event) {
386  e.preventDefault();
387  assertTrue(isTraceLoaded());
388  globals.logging.logEvent('Trace Actions', 'Convert to .systrace');
389  if (!isTraceLoaded) return;
390  getCurrentTrace()
391      .then(file => {
392        convertTraceToSystraceAndDownload(file);
393      })
394      .catch(error => {
395        throw new Error(`Failed to get current trace ${error}`);
396      });
397}
398
399function convertTraceToJson(e: Event) {
400  e.preventDefault();
401  assertTrue(isTraceLoaded());
402  globals.logging.logEvent('Trace Actions', 'Convert to .json');
403  if (!isTraceLoaded) return;
404  getCurrentTrace()
405      .then(file => {
406        convertTraceToJsonAndDownload(file);
407      })
408      .catch(error => {
409        throw new Error(`Failed to get current trace ${error}`);
410      });
411}
412
413export function isTraceLoaded(): boolean {
414  const engine = Object.values(globals.state.engines)[0];
415  return engine !== undefined;
416}
417
418function openTraceUrl(url: string): (e: Event) => void {
419  return e => {
420    globals.logging.logEvent('Trace Actions', 'Open example trace');
421    e.preventDefault();
422    globals.dispatch(Actions.openTraceFromUrl({url}));
423  };
424}
425
426function onInputElementFileSelectionChanged(e: Event) {
427  if (!(e.target instanceof HTMLInputElement)) {
428    throw new Error('Not an input element');
429  }
430  if (!e.target.files) return;
431  const file = e.target.files[0];
432  // Reset the value so onchange will be fired with the same file.
433  e.target.value = '';
434
435  if (e.target.dataset['useCatapultLegacyUi'] === '1') {
436    openWithLegacyUi(file);
437    return;
438  }
439
440  globals.logging.logEvent('Trace Actions', 'Open trace from file');
441  globals.dispatch(Actions.openTraceFromFile({file}));
442}
443
444async function openWithLegacyUi(file: File) {
445  // Switch back to the old catapult UI.
446  globals.logging.logEvent('Trace Actions', 'Open trace in Legacy UI');
447  if (await isLegacyTrace(file)) {
448    openFileWithLegacyTraceViewer(file);
449    return;
450  }
451  openInOldUIWithSizeCheck(file);
452}
453
454function openInOldUIWithSizeCheck(trace: Blob) {
455  // Perfetto traces smaller than 50mb can be safely opened in the legacy UI.
456  if (trace.size < 1024 * 1024 * 50) {
457    convertToJson(trace);
458    return;
459  }
460
461  // Give the user the option to truncate larger perfetto traces.
462  const size = Math.round(trace.size / (1024 * 1024));
463  showModal({
464    title: 'Legacy UI may fail to open this trace',
465    content:
466        m('div',
467          m('p',
468            `This trace is ${size}mb, opening it in the legacy UI ` +
469                `may fail.`),
470          m('p',
471            'More options can be found at ',
472            m('a',
473              {
474                href: 'https://goto.google.com/opening-large-traces',
475                target: '_blank'
476              },
477              'go/opening-large-traces'),
478            '.')),
479    buttons: [
480      {
481        text: 'Open full trace (not recommended)',
482        primary: false,
483        id: 'open',
484        action: () => {
485          convertToJson(trace);
486        }
487      },
488      {
489        text: 'Open beginning of trace',
490        primary: true,
491        id: 'truncate-start',
492        action: () => {
493          convertToJson(trace, /*truncate*/ 'start');
494        }
495      },
496      {
497        text: 'Open end of trace',
498        primary: true,
499        id: 'truncate-end',
500        action: () => {
501          convertToJson(trace, /*truncate*/ 'end');
502        }
503      }
504
505    ]
506  });
507  return;
508}
509
510function navigateRecord(e: Event) {
511  e.preventDefault();
512  Router.navigate('#!/record');
513}
514
515function navigateAnalyze(e: Event) {
516  e.preventDefault();
517  Router.navigate('#!/query');
518}
519
520function navigateFlags(e: Event) {
521  e.preventDefault();
522  Router.navigate('#!/flags');
523}
524
525function navigateMetrics(e: Event) {
526  e.preventDefault();
527  Router.navigate('#!/metrics');
528}
529
530function navigateInfo(e: Event) {
531  e.preventDefault();
532  Router.navigate('#!/info');
533}
534
535function navigateViewer(e: Event) {
536  e.preventDefault();
537  Router.navigate('#!/viewer');
538}
539
540function shareTrace(e: Event) {
541  e.preventDefault();
542  const engine = assertExists(Object.values(globals.state.engines)[0]);
543  const traceUrl = (engine.source as (TraceArrayBufferSource)).url || '';
544
545  // If the trace is not shareable (has been pushed via postMessage()) but has
546  // a url, create a pseudo-permalink by echoing back the URL.
547  if (!isShareable()) {
548    const msg =
549        [m('p',
550           'This trace was opened by an external site and as such cannot ' +
551               'be re-shared preserving the UI state.')];
552    if (traceUrl) {
553      msg.push(m('p', 'By using the URL below you can open this trace again.'));
554      msg.push(m('p', 'Clicking will copy the URL into the clipboard.'));
555      msg.push(createTraceLink(traceUrl, traceUrl));
556    }
557
558    showModal({
559      title: 'Cannot create permalink from external trace',
560      content: m('div', msg),
561      buttons: []
562    });
563    return;
564  }
565
566  if (!isShareable() || !isTraceLoaded()) return;
567
568  const result = confirm(
569      `Upload UI state and generate a permalink. ` +
570      `The trace will be accessible by anybody with the permalink.`);
571  if (result) {
572    globals.logging.logEvent('Trace Actions', 'Create permalink');
573    globals.dispatch(Actions.createPermalink({isRecordingConfig: false}));
574  }
575}
576
577function downloadTrace(e: Event) {
578  e.preventDefault();
579  if (!isDownloadable() || !isTraceLoaded()) return;
580  globals.logging.logEvent('Trace Actions', 'Download trace');
581
582  const engine = Object.values(globals.state.engines)[0];
583  if (!engine) return;
584  let url = '';
585  let fileName = `trace${TRACE_SUFFIX}`;
586  const src = engine.source;
587  if (src.type === 'URL') {
588    url = src.url;
589    fileName = url.split('/').slice(-1)[0];
590  } else if (src.type === 'ARRAY_BUFFER') {
591    const blob = new Blob([src.buffer], {type: 'application/octet-stream'});
592    if (src.fileName) {
593      fileName = src.fileName;
594    }
595    url = URL.createObjectURL(blob);
596  } else if (src.type === 'FILE') {
597    const file = src.file;
598    url = URL.createObjectURL(file);
599    fileName = file.name;
600  } else {
601    throw new Error(`Download from ${JSON.stringify(src)} is not supported`);
602  }
603
604  const a = document.createElement('a');
605  a.href = url;
606  a.download = fileName;
607  a.target = '_blank';
608  document.body.appendChild(a);
609  a.click();
610  document.body.removeChild(a);
611  URL.revokeObjectURL(url);
612}
613
614
615const EngineRPCWidget: m.Component = {
616  view() {
617    let cssClass = '';
618    let title = 'Number of pending SQL queries';
619    let label: string;
620    let failed = false;
621    let mode: EngineMode|undefined;
622
623    // We are assuming we have at most one engine here.
624    const engines = Object.values(globals.state.engines);
625    assertTrue(engines.length <= 1);
626    for (const engine of engines) {
627      mode = engine.mode;
628      if (engine.failed !== undefined) {
629        cssClass += '.red';
630        title = 'Query engine crashed\n' + engine.failed;
631        failed = true;
632      }
633    }
634
635    // If we don't have an engine yet, guess what will be the mode that will
636    // be used next time we'll create one. Even if we guess it wrong (somehow
637    // trace_controller.ts takes a different decision later, e.g. because the
638    // RPC server is shut down after we load the UI and cached httpRpcState)
639    // this will eventually become  consistent once the engine is created.
640    if (mode === undefined) {
641      if (globals.frontendLocalState.httpRpcState.connected &&
642          globals.state.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') {
643        mode = 'HTTP_RPC';
644      } else {
645        mode = 'WASM';
646      }
647    }
648
649    if (mode === 'HTTP_RPC') {
650      cssClass += '.green';
651      label = 'RPC';
652      title += '\n(Query engine: native accelerator over HTTP+RPC)';
653    } else {
654      label = 'WSM';
655      title += '\n(Query engine: built-in WASM)';
656    }
657
658    return m(
659        `.dbg-info-square${cssClass}`,
660        {title},
661        m('div', label),
662        m('div', `${failed ? 'FAIL' : globals.numQueuedQueries}`));
663  }
664};
665
666const ServiceWorkerWidget: m.Component = {
667  view() {
668    let cssClass = '';
669    let title = 'Service Worker: ';
670    let label = 'N/A';
671    const ctl = globals.serviceWorkerController;
672    if ((!('serviceWorker' in navigator))) {
673      label = 'N/A';
674      title += 'not supported by the browser (requires HTTPS)';
675    } else if (ctl.bypassed) {
676      label = 'OFF';
677      cssClass = '.red';
678      title += 'Bypassed, using live network. Double-click to re-enable';
679    } else if (ctl.installing) {
680      label = 'UPD';
681      cssClass = '.amber';
682      title += 'Installing / updating ...';
683    } else if (!navigator.serviceWorker.controller) {
684      label = 'N/A';
685      title += 'Not available, using network';
686    } else {
687      label = 'ON';
688      cssClass = '.green';
689      title += 'Serving from cache. Ready for offline use';
690    }
691
692    const toggle = async () => {
693      if (globals.serviceWorkerController.bypassed) {
694        globals.serviceWorkerController.setBypass(false);
695        return;
696      }
697      showModal({
698        title: 'Disable service worker?',
699        content: m(
700            'div',
701            m('p', `If you continue the service worker will be disabled until
702                      manually re-enabled.`),
703            m('p', `All future requests will be served from the network and the
704                    UI won't be available offline.`),
705            m('p', `You should do this only if you are debugging the UI
706                    or if you are experiencing caching-related problems.`),
707            m('p', `Disabling will cause a refresh of the UI, the current state
708                    will be lost.`),
709            ),
710        buttons: [
711          {
712            text: 'Disable and reload',
713            primary: true,
714            id: 'sw-bypass-enable',
715            action: () => {
716              globals.serviceWorkerController.setBypass(true).then(
717                  () => location.reload());
718            }
719          },
720          {
721            text: 'Cancel',
722            primary: false,
723            id: 'sw-bypass-cancel',
724            action: () => {}
725          }
726        ]
727      });
728    };
729
730    return m(
731        `.dbg-info-square${cssClass}`,
732        {title, ondblclick: toggle},
733        m('div', 'SW'),
734        m('div', label));
735  }
736};
737
738const SidebarFooter: m.Component = {
739  view() {
740    return m(
741        '.sidebar-footer',
742        m('button',
743          {
744            onclick: () => globals.dispatch(Actions.togglePerfDebug({})),
745          },
746          m('i.material-icons',
747            {title: 'Toggle Perf Debug Mode'},
748            'assessment')),
749        m(EngineRPCWidget),
750        m(ServiceWorkerWidget),
751        m(
752            '.version',
753            m('a',
754              {
755                href: `${GITILES_URL}/+/${version.SCM_REVISION}/ui`,
756                title: `Channel: ${getCurrentChannel()}`,
757                target: '_blank',
758              },
759              `${version.VERSION.substr(0, 11)}`),
760            ),
761    );
762  }
763};
764
765
766export class Sidebar implements m.ClassComponent {
767  private _redrawWhileAnimating =
768      new Animation(() => globals.rafScheduler.scheduleFullRedraw());
769  view() {
770    const vdomSections = [];
771    for (const section of SECTIONS) {
772      if (section.hideIfNoTraceLoaded && !isTraceLoaded()) continue;
773      const vdomItems = [];
774      for (const item of section.items) {
775        if (item.isVisible !== undefined && !item.isVisible()) {
776          continue;
777        }
778        let css = '';
779        let attrs = {
780          onclick: typeof item.a === 'function' ? item.a : null,
781          href: typeof item.a === 'string' ? item.a : '#',
782          target: typeof item.a === 'string' ? '_blank' : null,
783          disabled: false,
784          id: item.t.toLowerCase().replace(/[^\w]/g, '_')
785        };
786        if (item.isPending && item.isPending()) {
787          attrs.onclick = e => e.preventDefault();
788          css = '.pending';
789        }
790        if (item.internalUserOnly && !globals.isInternalUser) {
791          continue;
792        }
793        if (item.checkDownloadDisabled && !isDownloadable()) {
794          attrs = {
795            onclick: e => {
796              e.preventDefault();
797              alert('Can not download external trace.');
798            },
799            href: '#',
800            target: null,
801            disabled: true,
802            id: ''
803          };
804        }
805        vdomItems.push(m(
806            'li', m(`a${css}`, attrs, m('i.material-icons', item.i), item.t)));
807      }
808      if (section.appendOpenedTraceTitle) {
809        const engines = Object.values(globals.state.engines);
810        if (engines.length === 1) {
811          let traceTitle = '';
812          let traceUrl = '';
813          switch (engines[0].source.type) {
814            case 'FILE':
815              // Split on both \ and / (because C:\Windows\paths\are\like\this).
816              traceTitle = engines[0].source.file.name.split(/[/\\]/).pop()!;
817              const fileSizeMB = Math.ceil(engines[0].source.file.size / 1e6);
818              traceTitle += ` (${fileSizeMB} MB)`;
819              break;
820            case 'URL':
821              traceUrl = engines[0].source.url;
822              traceTitle = traceUrl.split('/').pop()!;
823              break;
824            case 'ARRAY_BUFFER':
825              traceTitle = engines[0].source.title;
826              traceUrl = engines[0].source.url || '';
827              break;
828            case 'HTTP_RPC':
829              traceTitle = 'External trace (RPC)';
830              break;
831            default:
832              break;
833          }
834          if (traceTitle !== '') {
835            const tabTitle = `${traceTitle} - Perfetto UI`;
836            if (tabTitle !== lastTabTitle) {
837              document.title = lastTabTitle = tabTitle;
838            }
839            vdomItems.unshift(m('li', createTraceLink(traceTitle, traceUrl)));
840          }
841        }
842      }
843      vdomSections.push(
844          m(`section${section.expanded ? '.expanded' : ''}`,
845            m('.section-header',
846              {
847                onclick: () => {
848                  section.expanded = !section.expanded;
849                  globals.rafScheduler.scheduleFullRedraw();
850                }
851              },
852              m('h1', {title: section.summary}, section.title),
853              m('h2', section.summary)),
854            m('.section-content', m('ul', vdomItems))));
855    }
856    return m(
857        'nav.sidebar',
858        {
859          class: globals.state.sidebarVisible ? 'show-sidebar' : 'hide-sidebar',
860          // 150 here matches --sidebar-timing in the css.
861          ontransitionstart: () => this._redrawWhileAnimating.start(150),
862          ontransitionend: () => this._redrawWhileAnimating.stop(),
863        },
864        m(
865            `header.${getCurrentChannel()}`,
866            m(`img[src=${globals.root}assets/brand.png].brand`),
867            m('button.sidebar-button',
868              {
869                onclick: () => {
870                  globals.dispatch(Actions.toggleSidebar({}));
871                },
872              },
873              m('i.material-icons',
874                {
875                  title: globals.state.sidebarVisible ? 'Hide menu' :
876                                                        'Show menu',
877                },
878                'menu')),
879            ),
880        m('input.trace_file[type=file]',
881          {onchange: onInputElementFileSelectionChanged}),
882        m('.sidebar-scroll',
883          m(
884              '.sidebar-scroll-container',
885              ...vdomSections,
886              m(SidebarFooter),
887              )),
888    );
889  }
890}
891
892function createTraceLink(title: string, url: string) {
893  if (url === '') {
894    return m('a.trace-file-name', title);
895  }
896  const linkProps = {
897    href: url,
898    title: 'Click to copy the URL',
899    target: '_blank',
900    onclick: onClickCopy(url)
901  };
902  return m('a.trace-file-name', linkProps, title);
903}
904