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