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