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