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