1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16'use strict'; 17 18// Use IIFE to avoid leaking names to other scripts. 19(function () { 20 21function getTimeInMs() { 22 return new Date().getTime(); 23} 24 25class TimeLog { 26 constructor() { 27 this.start = getTimeInMs(); 28 } 29 30 log(name) { 31 let end = getTimeInMs(); 32 console.log(name, end - this.start, 'ms'); 33 this.start = end; 34 } 35} 36 37class ProgressBar { 38 constructor() { 39 let str = ` 40 <div class="modal" tabindex="-1" role="dialog"> 41 <div class="modal-dialog" role="document"> 42 <div class="modal-content"> 43 <div class="modal-header"><h5 class="modal-title">Loading page...</h5></div> 44 <div class="modal-body"> 45 <div class="progress"> 46 <div class="progress-bar" role="progressbar" 47 style="width: 0%" aria-valuenow="0" aria-valuemin="0" 48 aria-valuemax="100">0%</div> 49 </div> 50 </div> 51 </div> 52 </div> 53 </div> 54 `; 55 this.modal = $(str).appendTo($('body')); 56 this.progress = 0; 57 this.shownCallback = null; 58 this.modal.on('shown.bs.modal', () => this._onShown()); 59 // Shorten progress bar update time. 60 this.modal.find('.progress-bar').css('transition-duration', '0ms'); 61 this.shown = false; 62 } 63 64 // progress is [0-100]. Return a Promise resolved when the update is shown. 65 updateAsync(text, progress) { 66 progress = parseInt(progress); // Truncate float number to integer. 67 return this.showAsync().then(() => { 68 if (text) { 69 this.modal.find('.modal-title').text(text); 70 } 71 this.progress = progress; 72 this.modal.find('.progress-bar').css('width', progress + '%') 73 .attr('aria-valuenow', progress).text(progress + '%'); 74 // Leave 100ms for the progess bar to update. 75 return createPromise((resolve) => setTimeout(resolve, 100)); 76 }); 77 } 78 79 showAsync() { 80 if (this.shown) { 81 return createPromise(); 82 } 83 return createPromise((resolve) => { 84 this.shownCallback = resolve; 85 this.modal.modal({ 86 show: true, 87 keyboard: false, 88 backdrop: false, 89 }); 90 }); 91 } 92 93 _onShown() { 94 this.shown = true; 95 if (this.shownCallback) { 96 let callback = this.shownCallback; 97 this.shownCallback = null; 98 callback(); 99 } 100 } 101 102 hide() { 103 this.shown = false; 104 this.modal.modal('hide'); 105 } 106} 107 108function openHtml(name, attrs={}) { 109 let s = `<${name} `; 110 for (let key in attrs) { 111 s += `${key}="${attrs[key]}" `; 112 } 113 s += '>'; 114 return s; 115} 116 117function closeHtml(name) { 118 return `</${name}>`; 119} 120 121function getHtml(name, attrs={}) { 122 let text; 123 if ('text' in attrs) { 124 text = attrs.text; 125 delete attrs.text; 126 } 127 let s = openHtml(name, attrs); 128 if (text) { 129 s += text; 130 } 131 s += closeHtml(name); 132 return s; 133} 134 135function getTableRow(cols, colName, attrs={}) { 136 let s = openHtml('tr', attrs); 137 for (let col of cols) { 138 s += `<${colName}>${col}</${colName}>`; 139 } 140 s += '</tr>'; 141 return s; 142} 143 144function getProcessName(pid) { 145 let name = gProcesses[pid]; 146 return name ? `${pid} (${name})`: pid.toString(); 147} 148 149function getThreadName(tid) { 150 let name = gThreads[tid]; 151 return name ? `${tid} (${name})`: tid.toString(); 152} 153 154function getLibName(libId) { 155 return gLibList[libId]; 156} 157 158function getFuncName(funcId) { 159 return gFunctionMap[funcId].f; 160} 161 162function getLibNameOfFunction(funcId) { 163 return getLibName(gFunctionMap[funcId].l); 164} 165 166function getFuncSourceRange(funcId) { 167 let func = gFunctionMap[funcId]; 168 if (func.hasOwnProperty('s')) { 169 return {fileId: func.s[0], startLine: func.s[1], endLine: func.s[2]}; 170 } 171 return null; 172} 173 174function getFuncDisassembly(funcId) { 175 let func = gFunctionMap[funcId]; 176 return func.hasOwnProperty('d') ? func.d : null; 177} 178 179function getSourceFilePath(sourceFileId) { 180 return gSourceFiles[sourceFileId].path; 181} 182 183function getSourceCode(sourceFileId) { 184 return gSourceFiles[sourceFileId].code; 185} 186 187function isClockEvent(eventInfo) { 188 return eventInfo.eventName.includes('task-clock') || 189 eventInfo.eventName.includes('cpu-clock'); 190} 191 192let createId = function() { 193 let currentId = 0; 194 return () => `id${++currentId}`; 195}(); 196 197class TabManager { 198 constructor(divContainer) { 199 let id = createId(); 200 divContainer.append(`<ul class="nav nav-pills mb-3 mt-3 ml-3" id="${id}" role="tablist"> 201 </ul><hr/><div class="tab-content" id="${id}Content"></div>`); 202 this.ul = divContainer.find(`#${id}`); 203 this.content = divContainer.find(`#${id}Content`); 204 // Map from title to [tabObj, drawn=false|true]. 205 this.tabs = new Map(); 206 this.tabActiveCallback = null; 207 } 208 209 addTab(title, tabObj) { 210 let id = createId(); 211 this.content.append(`<div class="tab-pane" id="${id}" role="tabpanel" 212 aria-labelledby="${id}-tab"></div>`); 213 this.ul.append(` 214 <li class="nav-item"> 215 <a class="nav-link" id="${id}-tab" data-toggle="pill" href="#${id}" role="tab" 216 aria-controls="${id}" aria-selected="false">${title}</a> 217 </li>`); 218 tabObj.init(this.content.find(`#${id}`)); 219 this.tabs.set(title, [tabObj, false]); 220 this.ul.find(`#${id}-tab`).on('shown.bs.tab', () => this.onTabActive(title)); 221 return tabObj; 222 } 223 224 setActiveAsync(title) { 225 let tabObj = this.findTab(title); 226 return createPromise((resolve) => { 227 this.tabActiveCallback = resolve; 228 let id = tabObj.div.attr('id') + '-tab'; 229 this.ul.find(`#${id}`).tab('show'); 230 }); 231 } 232 233 onTabActive(title) { 234 let array = this.tabs.get(title); 235 let tabObj = array[0]; 236 let drawn = array[1]; 237 if (!drawn) { 238 tabObj.draw(); 239 array[1] = true; 240 } 241 if (this.tabActiveCallback) { 242 let callback = this.tabActiveCallback; 243 this.tabActiveCallback = null; 244 callback(); 245 } 246 } 247 248 findTab(title) { 249 let array = this.tabs.get(title); 250 return array ? array[0] : null; 251 } 252} 253 254function createEventTabs(id) { 255 let ul = `<ul class="nav nav-pills mb-3 mt-3 ml-3" id="${id}" role="tablist">`; 256 let content = `<div class="tab-content" id="${id}Content">`; 257 for (let i = 0; i < gSampleInfo.length; ++i) { 258 let subId = id + '_' + i; 259 let title = gSampleInfo[i].eventName; 260 ul += ` 261 <li class="nav-item"> 262 <a class="nav-link" id="${subId}-tab" data-toggle="pill" href="#${subId}" role="tab" 263 aria-controls="${subId}" aria-selected="${i == 0 ? "true" : "false"}">${title}</a> 264 </li>`; 265 content += ` 266 <div class="tab-pane" id="${subId}" role="tabpanel" aria-labelledby="${subId}-tab"> 267 </div>`; 268 } 269 ul += '</ul>'; 270 content += '</div>'; 271 return ul + content; 272} 273 274function createViewsForEvents(div, createViewCallback) { 275 let views = []; 276 if (gSampleInfo.length == 1) { 277 views.push(createViewCallback(div, gSampleInfo[0])); 278 } else if (gSampleInfo.length > 1) { 279 // If more than one event, draw them in tabs. 280 let id = createId(); 281 div.append(createEventTabs(id)); 282 for (let i = 0; i < gSampleInfo.length; ++i) { 283 let subId = id + '_' + i; 284 views.push(createViewCallback(div.find(`#${subId}`), gSampleInfo[i])); 285 } 286 div.find(`#${id}_0-tab`).tab('show'); 287 } 288 return views; 289} 290 291// Return a promise to draw views. 292function drawViewsAsync(views, totalProgress, drawViewCallback) { 293 if (views.length == 0) { 294 return createPromise(); 295 } 296 let drawPos = 0; 297 let eachProgress = totalProgress / views.length; 298 function drawAsync() { 299 if (drawPos == views.length) { 300 return createPromise(); 301 } 302 return drawViewCallback(views[drawPos++], eachProgress).then(drawAsync); 303 } 304 return drawAsync(); 305} 306 307// Show global information retrieved from the record file, including: 308// record time 309// machine type 310// Android version 311// record cmdline 312// total samples 313class RecordFileView { 314 constructor(divContainer) { 315 this.div = $('<div>'); 316 this.div.appendTo(divContainer); 317 } 318 319 draw() { 320 google.charts.setOnLoadCallback(() => this.realDraw()); 321 } 322 323 realDraw() { 324 this.div.empty(); 325 // Draw a table of 'Name', 'Value'. 326 let rows = []; 327 if (gRecordInfo.recordTime) { 328 rows.push(['Record Time', gRecordInfo.recordTime]); 329 } 330 if (gRecordInfo.machineType) { 331 rows.push(['Machine Type', gRecordInfo.machineType]); 332 } 333 if (gRecordInfo.androidVersion) { 334 rows.push(['Android Version', gRecordInfo.androidVersion]); 335 } 336 if (gRecordInfo.androidBuildFingerprint) { 337 rows.push(['Build Fingerprint', gRecordInfo.androidBuildFingerprint]); 338 } 339 if (gRecordInfo.kernelVersion) { 340 rows.push(['Kernel Version', gRecordInfo.kernelVersion]); 341 } 342 if (gRecordInfo.recordCmdline) { 343 rows.push(['Record cmdline', gRecordInfo.recordCmdline]); 344 } 345 rows.push(['Total Samples', '' + gRecordInfo.totalSamples]); 346 347 let data = new google.visualization.DataTable(); 348 data.addColumn('string', ''); 349 data.addColumn('string', ''); 350 data.addRows(rows); 351 for (let i = 0; i < rows.length; ++i) { 352 data.setProperty(i, 0, 'className', 'boldTableCell'); 353 } 354 let table = new google.visualization.Table(this.div.get(0)); 355 table.draw(data, { 356 width: '100%', 357 sort: 'disable', 358 allowHtml: true, 359 cssClassNames: { 360 'tableCell': 'tableCell', 361 }, 362 }); 363 } 364} 365 366// Show pieChart of event count percentage of each process, thread, library and function. 367class ChartView { 368 constructor(divContainer, eventInfo) { 369 this.div = $('<div>').appendTo(divContainer); 370 this.eventInfo = eventInfo; 371 this.processInfo = null; 372 this.threadInfo = null; 373 this.libInfo = null; 374 this.states = { 375 SHOW_EVENT_INFO: 1, 376 SHOW_PROCESS_INFO: 2, 377 SHOW_THREAD_INFO: 3, 378 SHOW_LIB_INFO: 4, 379 }; 380 if (isClockEvent(this.eventInfo)) { 381 this.getSampleWeight = function (eventCount) { 382 return (eventCount / 1000000.0).toFixed(3).toLocaleString() + ' ms'; 383 }; 384 } else { 385 this.getSampleWeight = (eventCount) => eventCount.toLocaleString(); 386 } 387 } 388 389 _getState() { 390 if (this.libInfo) { 391 return this.states.SHOW_LIB_INFO; 392 } 393 if (this.threadInfo) { 394 return this.states.SHOW_THREAD_INFO; 395 } 396 if (this.processInfo) { 397 return this.states.SHOW_PROCESS_INFO; 398 } 399 return this.states.SHOW_EVENT_INFO; 400 } 401 402 _goBack() { 403 let state = this._getState(); 404 if (state == this.states.SHOW_PROCESS_INFO) { 405 this.processInfo = null; 406 } else if (state == this.states.SHOW_THREAD_INFO) { 407 this.threadInfo = null; 408 } else if (state == this.states.SHOW_LIB_INFO) { 409 this.libInfo = null; 410 } 411 this.draw(); 412 } 413 414 _selectHandler(chart) { 415 let selectedItem = chart.getSelection()[0]; 416 if (selectedItem) { 417 let state = this._getState(); 418 if (state == this.states.SHOW_EVENT_INFO) { 419 this.processInfo = this.eventInfo.processes[selectedItem.row]; 420 } else if (state == this.states.SHOW_PROCESS_INFO) { 421 this.threadInfo = this.processInfo.threads[selectedItem.row]; 422 } else if (state == this.states.SHOW_THREAD_INFO) { 423 this.libInfo = this.threadInfo.libs[selectedItem.row]; 424 } 425 this.draw(); 426 } 427 } 428 429 draw() { 430 google.charts.setOnLoadCallback(() => this.realDraw()); 431 } 432 433 realDraw() { 434 this.div.empty(); 435 this._drawTitle(); 436 this._drawPieChart(); 437 } 438 439 _drawTitle() { 440 // Draw a table of 'Name', 'Event Count'. 441 let rows = []; 442 rows.push(['Event Type: ' + this.eventInfo.eventName, 443 this.getSampleWeight(this.eventInfo.eventCount)]); 444 if (this.processInfo) { 445 rows.push(['Process: ' + getProcessName(this.processInfo.pid), 446 this.getSampleWeight(this.processInfo.eventCount)]); 447 } 448 if (this.threadInfo) { 449 rows.push(['Thread: ' + getThreadName(this.threadInfo.tid), 450 this.getSampleWeight(this.threadInfo.eventCount)]); 451 } 452 if (this.libInfo) { 453 rows.push(['Library: ' + getLibName(this.libInfo.libId), 454 this.getSampleWeight(this.libInfo.eventCount)]); 455 } 456 let data = new google.visualization.DataTable(); 457 data.addColumn('string', ''); 458 data.addColumn('string', ''); 459 data.addRows(rows); 460 for (let i = 0; i < rows.length; ++i) { 461 data.setProperty(i, 0, 'className', 'boldTableCell'); 462 } 463 let wrapperDiv = $('<div>'); 464 wrapperDiv.appendTo(this.div); 465 let table = new google.visualization.Table(wrapperDiv.get(0)); 466 table.draw(data, { 467 width: '100%', 468 sort: 'disable', 469 allowHtml: true, 470 cssClassNames: { 471 'tableCell': 'tableCell', 472 }, 473 }); 474 if (this._getState() != this.states.SHOW_EVENT_INFO) { 475 $('<button type="button" class="btn btn-primary">Back</button>').appendTo(this.div) 476 .click(() => this._goBack()); 477 } 478 } 479 480 _drawPieChart() { 481 let state = this._getState(); 482 let title = null; 483 let firstColumn = null; 484 let rows = []; 485 let thisObj = this; 486 function getItem(name, eventCount, totalEventCount) { 487 let sampleWeight = thisObj.getSampleWeight(eventCount); 488 let percent = (eventCount * 100.0 / totalEventCount).toFixed(2) + '%'; 489 return [name, eventCount, getHtml('pre', {text: name}) + 490 getHtml('b', {text: `${sampleWeight} (${percent})`})]; 491 } 492 493 if (state == this.states.SHOW_EVENT_INFO) { 494 title = 'Processes in event type ' + this.eventInfo.eventName; 495 firstColumn = 'Process'; 496 for (let process of this.eventInfo.processes) { 497 rows.push(getItem('Process: ' + getProcessName(process.pid), process.eventCount, 498 this.eventInfo.eventCount)); 499 } 500 } else if (state == this.states.SHOW_PROCESS_INFO) { 501 title = 'Threads in process ' + getProcessName(this.processInfo.pid); 502 firstColumn = 'Thread'; 503 for (let thread of this.processInfo.threads) { 504 rows.push(getItem('Thread: ' + getThreadName(thread.tid), thread.eventCount, 505 this.processInfo.eventCount)); 506 } 507 } else if (state == this.states.SHOW_THREAD_INFO) { 508 title = 'Libraries in thread ' + getThreadName(this.threadInfo.tid); 509 firstColumn = 'Library'; 510 for (let lib of this.threadInfo.libs) { 511 rows.push(getItem('Library: ' + getLibName(lib.libId), lib.eventCount, 512 this.threadInfo.eventCount)); 513 } 514 } else if (state == this.states.SHOW_LIB_INFO) { 515 title = 'Functions in library ' + getLibName(this.libInfo.libId); 516 firstColumn = 'Function'; 517 for (let func of this.libInfo.functions) { 518 rows.push(getItem('Function: ' + getFuncName(func.f), func.c[1], 519 this.libInfo.eventCount)); 520 } 521 } 522 let data = new google.visualization.DataTable(); 523 data.addColumn('string', firstColumn); 524 data.addColumn('number', 'EventCount'); 525 data.addColumn({type: 'string', role: 'tooltip', p: {html: true}}); 526 data.addRows(rows); 527 528 let wrapperDiv = $('<div>'); 529 wrapperDiv.appendTo(this.div); 530 let chart = new google.visualization.PieChart(wrapperDiv.get(0)); 531 chart.draw(data, { 532 title: title, 533 width: 1000, 534 height: 600, 535 tooltip: {isHtml: true}, 536 }); 537 google.visualization.events.addListener(chart, 'select', () => this._selectHandler(chart)); 538 } 539} 540 541 542class ChartStatTab { 543 init(div) { 544 this.div = div; 545 } 546 547 draw() { 548 new RecordFileView(this.div).draw(); 549 let views = createViewsForEvents(this.div, (div, eventInfo) => { 550 return new ChartView(div, eventInfo); 551 }); 552 for (let view of views) { 553 view.draw(); 554 } 555 } 556} 557 558 559class SampleTableTab { 560 init(div) { 561 this.div = div; 562 } 563 564 draw() { 565 let views = []; 566 createPromise() 567 .then(updateProgress('Draw SampleTable...', 0)) 568 .then(wait(() => { 569 this.div.empty(); 570 views = createViewsForEvents(this.div, (div, eventInfo) => { 571 return new SampleTableView(div, eventInfo); 572 }); 573 })) 574 .then(() => drawViewsAsync(views, 100, (view, progress) => view.drawAsync(progress))) 575 .then(hideProgress()); 576 } 577} 578 579// Select the way to show sample weight in SampleTableTab. 580// 1. Show percentage of event count. 581// 2. Show event count (For cpu-clock and task-clock events, it is time in ms). 582class SampleTableWeightSelectorView { 583 constructor(divContainer, eventInfo, onSelectChange) { 584 let options = new Map(); 585 options.set('percent', 'Show percentage of event count'); 586 options.set('event_count', 'Show event count'); 587 if (isClockEvent(eventInfo)) { 588 options.set('event_count_in_ms', 'Show event count in milliseconds'); 589 } 590 let buttons = []; 591 options.forEach((value, key) => { 592 buttons.push(`<button type="button" class="dropdown-item" key="${key}">${value} 593 </button>`); 594 }); 595 this.curOption = 'percent'; 596 this.eventCount = eventInfo.eventCount; 597 let id = createId(); 598 let str = ` 599 <div class="dropdown"> 600 <button type="button" class="btn btn-primary dropdown-toggle" id="${id}" 601 data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" 602 >${options.get(this.curOption)}</button> 603 <div class="dropdown-menu" aria-labelledby="${id}">${buttons.join('')}</div> 604 </div> 605 `; 606 divContainer.append(str); 607 divContainer.children().last().on('hidden.bs.dropdown', (e) => { 608 if (e.clickEvent) { 609 let button = $(e.clickEvent.target); 610 let newOption = button.attr('key'); 611 if (newOption && this.curOption != newOption) { 612 this.curOption = newOption; 613 divContainer.find(`#${id}`).text(options.get(this.curOption)); 614 onSelectChange(); 615 } 616 } 617 }); 618 } 619 620 getSampleWeightFunction() { 621 if (this.curOption == 'percent') { 622 return (eventCount) => (eventCount * 100.0 / this.eventCount).toFixed(2) + '%'; 623 } 624 if (this.curOption == 'event_count') { 625 return (eventCount) => eventCount.toLocaleString(); 626 } 627 if (this.curOption == 'event_count_in_ms') { 628 return (eventCount) => (eventCount / 1000000.0).toFixed(3).toLocaleString(); 629 } 630 } 631 632 getSampleWeightSuffix() { 633 if (this.curOption == 'event_count_in_ms') { 634 return ' ms'; 635 } 636 return ''; 637 } 638} 639 640 641class SampleTableView { 642 constructor(divContainer, eventInfo) { 643 this.id = createId(); 644 this.div = $('<div>', {id: this.id}).appendTo(divContainer); 645 this.eventInfo = eventInfo; 646 this.selectorView = null; 647 this.tableDiv = null; 648 } 649 650 drawAsync(totalProgress) { 651 return createPromise() 652 .then(wait(() => { 653 this.div.empty(); 654 this.selectorView = new SampleTableWeightSelectorView( 655 this.div, this.eventInfo, () => this.onSampleWeightChange()); 656 this.tableDiv = $('<div>').appendTo(this.div); 657 })) 658 .then(() => this._drawSampleTable(totalProgress)); 659 } 660 661 // Return a promise to draw SampleTable. 662 _drawSampleTable(totalProgress) { 663 let eventInfo = this.eventInfo; 664 let data = []; 665 return createPromise() 666 .then(wait(() => { 667 this.tableDiv.empty(); 668 let getSampleWeight = this.selectorView.getSampleWeightFunction(); 669 let sampleWeightSuffix = this.selectorView.getSampleWeightSuffix(); 670 // Draw a table of 'Total', 'Self', 'Samples', 'Process', 'Thread', 'Library', 671 // 'Function'. 672 let valueSuffix = sampleWeightSuffix.length > 0 ? `(in${sampleWeightSuffix})` : ''; 673 let titles = ['Total' + valueSuffix, 'Self' + valueSuffix, 'Samples', 'Process', 674 'Thread', 'Library', 'Function', 'HideKey']; 675 this.tableDiv.append(` 676 <table cellspacing="0" class="table table-striped table-bordered" 677 style="width:100%"> 678 <thead>${getTableRow(titles, 'th')}</thead> 679 <tbody></tbody> 680 <tfoot>${getTableRow(titles, 'th')}</tfoot> 681 </table>`); 682 for (let [i, process] of eventInfo.processes.entries()) { 683 let processName = getProcessName(process.pid); 684 for (let [j, thread] of process.threads.entries()) { 685 let threadName = getThreadName(thread.tid); 686 for (let [k, lib] of thread.libs.entries()) { 687 let libName = getLibName(lib.libId); 688 for (let [t, func] of lib.functions.entries()) { 689 let totalValue = getSampleWeight(func.c[2]); 690 let selfValue = getSampleWeight(func.c[1]); 691 let key = [i, j, k, t].join('_'); 692 data.push([totalValue, selfValue, func.c[0], processName, 693 threadName, libName, getFuncName(func.f), key]) 694 } 695 } 696 } 697 } 698 })) 699 .then(addProgress(totalProgress / 2)) 700 .then(wait(() => { 701 let table = this.tableDiv.find('table'); 702 let dataTable = table.DataTable({ 703 lengthMenu: [10, 20, 50, 100, -1], 704 pageLength: 100, 705 order: [[0, 'desc'], [1, 'desc'], [2, 'desc']], 706 data: data, 707 responsive: true, 708 columnDefs: [ 709 { orderSequence: [ 'desc' ], className: 'textRight', targets: [0, 1, 2] }, 710 ], 711 }); 712 dataTable.column(7).visible(false); 713 714 table.find('tr').css('cursor', 'pointer'); 715 table.on('click', 'tr', function() { 716 let data = dataTable.row(this).data(); 717 if (!data) { 718 // A row in header or footer. 719 return; 720 } 721 let key = data[7]; 722 if (!key) { 723 return; 724 } 725 let indexes = key.split('_'); 726 let processInfo = eventInfo.processes[indexes[0]]; 727 let threadInfo = processInfo.threads[indexes[1]]; 728 let lib = threadInfo.libs[indexes[2]]; 729 let func = lib.functions[indexes[3]]; 730 FunctionTab.showFunction(eventInfo, processInfo, threadInfo, lib, func); 731 }); 732 })); 733 } 734 735 onSampleWeightChange() { 736 createPromise() 737 .then(updateProgress('Draw SampleTable...', 0)) 738 .then(() => this._drawSampleTable(100)) 739 .then(hideProgress()); 740 } 741} 742 743 744// Show embedded flamegraph generated by inferno. 745class FlameGraphTab { 746 init(div) { 747 this.div = div; 748 } 749 750 draw() { 751 let views = []; 752 createPromise() 753 .then(updateProgress('Draw Flamegraph...', 0)) 754 .then(wait(() => { 755 this.div.empty(); 756 views = createViewsForEvents(this.div, (div, eventInfo) => { 757 return new FlameGraphViewList(div, eventInfo); 758 }); 759 })) 760 .then(() => drawViewsAsync(views, 100, (view, progress) => view.drawAsync(progress))) 761 .then(hideProgress()); 762 } 763} 764 765// Show FlameGraphs for samples in an event type, used in FlameGraphTab. 766// 1. Draw 10 FlameGraphs at one time, and use a "More" button to show more FlameGraphs. 767// 2. First draw background of Flamegraphs, then draw details in idle time. 768class FlameGraphViewList { 769 constructor(div, eventInfo) { 770 this.div = div; 771 this.eventInfo = eventInfo; 772 this.selectorView = null; 773 this.flamegraphDiv = null; 774 this.flamegraphs = []; 775 this.moreButton = null; 776 } 777 778 drawAsync(totalProgress) { 779 this.div.empty(); 780 this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo, 781 () => this.onSampleWeightChange()); 782 this.flamegraphDiv = $('<div>').appendTo(this.div); 783 return this._drawMoreFlameGraphs(10, totalProgress); 784 } 785 786 // Return a promise to draw flamegraphs. 787 _drawMoreFlameGraphs(moreCount, progress) { 788 let initProgress = progress / (1 + moreCount); 789 let newFlamegraphs = []; 790 return createPromise() 791 .then(wait(() => { 792 if (this.moreButton) { 793 this.moreButton.hide(); 794 } 795 let pId = 0; 796 let tId = 0; 797 let newCount = this.flamegraphs.length + moreCount; 798 for (let i = 0; i < newCount; ++i) { 799 if (pId == this.eventInfo.processes.length) { 800 break; 801 } 802 let process = this.eventInfo.processes[pId]; 803 let thread = process.threads[tId]; 804 if (i >= this.flamegraphs.length) { 805 let title = `Process ${getProcessName(process.pid)} ` + 806 `Thread ${getThreadName(thread.tid)} ` + 807 `(Samples: ${thread.sampleCount})`; 808 let totalCount = {countForProcess: process.eventCount, 809 countForThread: thread.eventCount}; 810 let flamegraph = new FlameGraphView(this.flamegraphDiv, title, totalCount, 811 thread.g.c, false); 812 flamegraph.draw(); 813 newFlamegraphs.push(flamegraph); 814 } 815 tId++; 816 if (tId == process.threads.length) { 817 pId++; 818 tId = 0; 819 } 820 } 821 if (pId < this.eventInfo.processes.length) { 822 // Show "More" Button. 823 if (!this.moreButton) { 824 this.div.append(` 825 <div style="text-align:center"> 826 <button type="button" class="btn btn-primary">More</button> 827 </div>`); 828 this.moreButton = this.div.children().last().find('button'); 829 this.moreButton.click(() => { 830 createPromise().then(updateProgress('Draw FlameGraph...', 0)) 831 .then(() => this._drawMoreFlameGraphs(10, 100)) 832 .then(hideProgress()); 833 }); 834 this.moreButton.hide(); 835 } 836 } else if (this.moreButton) { 837 this.moreButton.remove(); 838 this.moreButton = null; 839 } 840 for (let flamegraph of newFlamegraphs) { 841 this.flamegraphs.push(flamegraph); 842 } 843 })) 844 .then(addProgress(initProgress)) 845 .then(() => this.drawDetails(newFlamegraphs, progress - initProgress)); 846 } 847 848 drawDetails(flamegraphs, totalProgress) { 849 return createPromise() 850 .then(() => drawViewsAsync(flamegraphs, totalProgress, (view, progress) => { 851 return createPromise() 852 .then(wait(() => view.drawDetails(this.selectorView.getSampleWeightFunction()))) 853 .then(addProgress(progress)); 854 })) 855 .then(wait(() => { 856 if (this.moreButton) { 857 this.moreButton.show(); 858 } 859 })); 860 } 861 862 onSampleWeightChange() { 863 createPromise().then(updateProgress('Draw FlameGraph...', 0)) 864 .then(() => this.drawDetails(this.flamegraphs, 100)) 865 .then(hideProgress()); 866 } 867} 868 869// FunctionTab: show information of a function. 870// 1. Show the callgrpah and reverse callgraph of a function as flamegraphs. 871// 2. Show the annotated source code of the function. 872class FunctionTab { 873 static showFunction(eventInfo, processInfo, threadInfo, lib, func) { 874 let title = 'Function'; 875 let tab = gTabs.findTab(title); 876 if (!tab) { 877 tab = gTabs.addTab(title, new FunctionTab()); 878 } 879 gTabs.setActiveAsync(title) 880 .then(() => tab.setFunction(eventInfo, processInfo, threadInfo, lib, func)); 881 } 882 883 constructor() { 884 this.func = null; 885 this.selectPercent = 'thread'; 886 } 887 888 init(div) { 889 this.div = div; 890 } 891 892 setFunction(eventInfo, processInfo, threadInfo, lib, func) { 893 this.eventInfo = eventInfo; 894 this.processInfo = processInfo; 895 this.threadInfo = threadInfo; 896 this.lib = lib; 897 this.func = func; 898 this.selectorView = null; 899 this.views = []; 900 this.redraw(); 901 } 902 903 redraw() { 904 if (!this.func) { 905 return; 906 } 907 createPromise() 908 .then(updateProgress("Draw Function...", 0)) 909 .then(wait(() => { 910 this.div.empty(); 911 this._drawTitle(); 912 913 this.selectorView = new SampleWeightSelectorView(this.div, this.eventInfo, 914 () => this.onSampleWeightChange()); 915 let funcId = this.func.f; 916 let funcName = getFuncName(funcId); 917 function getNodesMatchingFuncId(root) { 918 let nodes = []; 919 function recursiveFn(node) { 920 if (node.f == funcId) { 921 nodes.push(node); 922 } else { 923 for (let child of node.c) { 924 recursiveFn(child); 925 } 926 } 927 } 928 recursiveFn(root); 929 return nodes; 930 } 931 let totalCount = {countForProcess: this.processInfo.eventCount, 932 countForThread: this.threadInfo.eventCount}; 933 let callgraphView = new FlameGraphView( 934 this.div, `Functions called by ${funcName}`, totalCount, 935 getNodesMatchingFuncId(this.threadInfo.g), false); 936 callgraphView.draw(); 937 this.views.push(callgraphView); 938 let reverseCallgraphView = new FlameGraphView( 939 this.div, `Functions calling ${funcName}`, totalCount, 940 getNodesMatchingFuncId(this.threadInfo.rg), true); 941 reverseCallgraphView.draw(); 942 this.views.push(reverseCallgraphView); 943 let sourceFiles = collectSourceFilesForFunction(this.func); 944 if (sourceFiles) { 945 this.div.append(getHtml('hr')); 946 this.div.append(getHtml('b', {text: 'SourceCode:'}) + '<br/>'); 947 this.views.push(new SourceCodeView(this.div, sourceFiles, totalCount)); 948 } 949 950 let disassembly = collectDisassemblyForFunction(this.func); 951 if (disassembly) { 952 this.div.append(getHtml('hr')); 953 this.div.append(getHtml('b', {text: 'Disassembly:'}) + '<br/>'); 954 this.views.push(new DisassemblyView(this.div, disassembly, totalCount)); 955 } 956 })) 957 .then(addProgress(25)) 958 .then(() => this.drawDetails(75)) 959 .then(hideProgress()); 960 } 961 962 draw() {} 963 964 _drawTitle() { 965 let eventName = this.eventInfo.eventName; 966 let processName = getProcessName(this.processInfo.pid); 967 let threadName = getThreadName(this.threadInfo.tid); 968 let libName = getLibName(this.lib.libId); 969 let funcName = getFuncName(this.func.f); 970 // Draw a table of 'Name', 'Value'. 971 let rows = []; 972 rows.push(['Event Type', eventName]); 973 rows.push(['Process', processName]); 974 rows.push(['Thread', threadName]); 975 rows.push(['Library', libName]); 976 rows.push(['Function', getHtml('pre', {text: funcName})]); 977 let data = new google.visualization.DataTable(); 978 data.addColumn('string', ''); 979 data.addColumn('string', ''); 980 data.addRows(rows); 981 for (let i = 0; i < rows.length; ++i) { 982 data.setProperty(i, 0, 'className', 'boldTableCell'); 983 } 984 let wrapperDiv = $('<div>'); 985 wrapperDiv.appendTo(this.div); 986 let table = new google.visualization.Table(wrapperDiv.get(0)); 987 table.draw(data, { 988 width: '100%', 989 sort: 'disable', 990 allowHtml: true, 991 cssClassNames: { 992 'tableCell': 'tableCell', 993 }, 994 }); 995 } 996 997 onSampleWeightChange() { 998 createPromise() 999 .then(updateProgress("Draw Function...", 0)) 1000 .then(() => this.drawDetails(100)) 1001 .then(hideProgress()); 1002 } 1003 1004 drawDetails(totalProgress) { 1005 let sampleWeightFunction = this.selectorView.getSampleWeightFunction(); 1006 return drawViewsAsync(this.views, totalProgress, (view, progress) => { 1007 return createPromise() 1008 .then(wait(() => view.drawDetails(sampleWeightFunction))) 1009 .then(addProgress(progress)); 1010 }); 1011 } 1012} 1013 1014 1015// Select the way to show sample weight in FlamegraphTab and FunctionTab. 1016// 1. Show percentage of event count relative to all processes. 1017// 2. Show percentage of event count relative to the current process. 1018// 3. Show percentage of event count relative to the current thread. 1019// 4. Show absolute event count. 1020// 5. Show event count in milliseconds, only possible for cpu-clock or task-clock events. 1021class SampleWeightSelectorView { 1022 constructor(divContainer, eventInfo, onSelectChange) { 1023 let options = new Map(); 1024 options.set('percent_to_all', 'Show percentage of event count relative to all processes'); 1025 options.set('percent_to_process', 1026 'Show percentage of event count relative to the current process'); 1027 options.set('percent_to_thread', 1028 'Show percentage of event count relative to the current thread'); 1029 options.set('event_count', 'Show event count'); 1030 if (isClockEvent(eventInfo)) { 1031 options.set('event_count_in_ms', 'Show event count in milliseconds'); 1032 } 1033 let buttons = []; 1034 options.forEach((value, key) => { 1035 buttons.push(`<button type="button" class="dropdown-item" key="${key}">${value} 1036 </button>`); 1037 }); 1038 this.curOption = 'percent_to_all'; 1039 let id = createId(); 1040 let str = ` 1041 <div class="dropdown"> 1042 <button type="button" class="btn btn-primary dropdown-toggle" id="${id}" 1043 data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" 1044 >${options.get(this.curOption)}</button> 1045 <div class="dropdown-menu" aria-labelledby="${id}">${buttons.join('')}</div> 1046 </div> 1047 `; 1048 divContainer.append(str); 1049 divContainer.children().last().on('hidden.bs.dropdown', (e) => { 1050 if (e.clickEvent) { 1051 let button = $(e.clickEvent.target); 1052 let newOption = button.attr('key'); 1053 if (newOption && this.curOption != newOption) { 1054 this.curOption = newOption; 1055 divContainer.find(`#${id}`).text(options.get(this.curOption)); 1056 onSelectChange(); 1057 } 1058 } 1059 }); 1060 this.countForAllProcesses = eventInfo.eventCount; 1061 } 1062 1063 getSampleWeightFunction() { 1064 if (this.curOption == 'percent_to_all') { 1065 let countForAllProcesses = this.countForAllProcesses; 1066 return function(eventCount, _) { 1067 let percent = eventCount * 100.0 / countForAllProcesses; 1068 return percent.toFixed(2) + '%'; 1069 }; 1070 } 1071 if (this.curOption == 'percent_to_process') { 1072 return function(eventCount, totalCount) { 1073 let percent = eventCount * 100.0 / totalCount.countForProcess; 1074 return percent.toFixed(2) + '%'; 1075 }; 1076 } 1077 if (this.curOption == 'percent_to_thread') { 1078 return function(eventCount, totalCount) { 1079 let percent = eventCount * 100.0 / totalCount.countForThread; 1080 return percent.toFixed(2) + '%'; 1081 }; 1082 } 1083 if (this.curOption == 'event_count') { 1084 return function(eventCount, _) { 1085 return eventCount.toLocaleString(); 1086 }; 1087 } 1088 if (this.curOption == 'event_count_in_ms') { 1089 return function(eventCount, _) { 1090 let timeInMs = eventCount / 1000000.0; 1091 return timeInMs.toFixed(3).toLocaleString() + ' ms'; 1092 }; 1093 } 1094 } 1095} 1096 1097// Given a callgraph, show the flamegraph. 1098class FlameGraphView { 1099 constructor(divContainer, title, totalCount, initNodes, reverseOrder) { 1100 this.id = createId(); 1101 this.div = $('<div>', {id: this.id, 1102 style: 'font-family: Monospace; font-size: 12px'}); 1103 this.div.appendTo(divContainer); 1104 this.title = title; 1105 this.totalCount = totalCount; 1106 this.reverseOrder = reverseOrder; 1107 this.sampleWeightFunction = null; 1108 this.svgNodeHeight = 17; 1109 this.initNodes = initNodes; 1110 this.sumCount = 0; 1111 for (let node of initNodes) { 1112 this.sumCount += node.s; 1113 } 1114 this.maxDepth = this._getMaxDepth(this.initNodes); 1115 this.svgHeight = this.svgNodeHeight * (this.maxDepth + 3); 1116 this.svgStr = null; 1117 this.svgDiv = null; 1118 this.svg = null; 1119 } 1120 1121 _getMaxDepth(nodes) { 1122 let isArray = Array.isArray(nodes); 1123 let sumCount; 1124 if (isArray) { 1125 sumCount = nodes.reduce((acc, cur) => acc + cur.s, 0); 1126 } else { 1127 sumCount = nodes.s; 1128 } 1129 let width = this._getWidthPercentage(sumCount); 1130 if (width < 0.1) { 1131 return 0; 1132 } 1133 let children = isArray ? this._splitChildrenForNodes(nodes) : nodes.c; 1134 let childDepth = 0; 1135 for (let child of children) { 1136 childDepth = Math.max(childDepth, this._getMaxDepth(child)); 1137 } 1138 return childDepth + 1; 1139 } 1140 1141 draw() { 1142 // Only draw skeleton. 1143 this.div.empty(); 1144 this.div.append(`<p><b>${this.title}</b></p>`); 1145 this.svgStr = []; 1146 this._renderBackground(); 1147 this.svgStr.push('</svg></div>'); 1148 this.div.append(this.svgStr.join('')); 1149 this.svgDiv = this.div.children().last(); 1150 this.div.append('<br/><br/>'); 1151 } 1152 1153 drawDetails(sampleWeightFunction) { 1154 this.sampleWeightFunction = sampleWeightFunction; 1155 this.svgStr = []; 1156 this._renderBackground(); 1157 this._renderSvgNodes(); 1158 this._renderUnzoomNode(); 1159 this._renderInfoNode(); 1160 this._renderPercentNode(); 1161 this._renderSearchNode(); 1162 // It is much faster to add html content to svgStr than adding it directly to svgDiv. 1163 this.svgDiv.html(this.svgStr.join('')); 1164 this.svgStr = []; 1165 this.svg = this.svgDiv.find('svg'); 1166 this._adjustTextSize(); 1167 this._enableZoom(); 1168 this._enableInfo(); 1169 this._enableSearch(); 1170 this._adjustTextSizeOnResize(); 1171 } 1172 1173 _renderBackground() { 1174 this.svgStr.push(` 1175 <div style="width: 100%; height: ${this.svgHeight}px;"> 1176 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 1177 version="1.1" width="100%" height="100%" style="border: 1px solid black; "> 1178 <defs > <linearGradient id="background_gradient_${this.id}" 1179 y1="0" y2="1" x1="0" x2="0" > 1180 <stop stop-color="#eeeeee" offset="5%" /> 1181 <stop stop-color="#efefb1" offset="90%" /> 1182 </linearGradient> 1183 </defs> 1184 <rect x="0" y="0" width="100%" height="100%" 1185 fill="url(#background_gradient_${this.id})" />`); 1186 } 1187 1188 _getYForDepth(depth) { 1189 if (this.reverseOrder) { 1190 return (depth + 3) * this.svgNodeHeight; 1191 } 1192 return this.svgHeight - (depth + 1) * this.svgNodeHeight; 1193 } 1194 1195 _getWidthPercentage(eventCount) { 1196 return eventCount * 100.0 / this.sumCount; 1197 } 1198 1199 _getHeatColor(widthPercentage) { 1200 return { 1201 r: Math.floor(245 + 10 * (1 - widthPercentage * 0.01)), 1202 g: Math.floor(110 + 105 * (1 - widthPercentage * 0.01)), 1203 b: 100, 1204 }; 1205 } 1206 1207 _renderSvgNodes() { 1208 let fakeNodes = [{c: this.initNodes}]; 1209 let children = this._splitChildrenForNodes(fakeNodes); 1210 let xOffset = 0; 1211 for (let child of children) { 1212 xOffset = this._renderSvgNodesWithSameRoot(child, 0, xOffset); 1213 } 1214 } 1215 1216 // Return an array of children nodes, with children having the same functionId merged in a 1217 // subarray. 1218 _splitChildrenForNodes(nodes) { 1219 let map = new Map(); 1220 for (let node of nodes) { 1221 for (let child of node.c) { 1222 let funcName = getFuncName(child.f); 1223 let subNodes = map.get(funcName); 1224 if (subNodes) { 1225 subNodes.push(child); 1226 } else { 1227 map.set(funcName, [child]); 1228 } 1229 } 1230 } 1231 const funcNames = [...map.keys()].sort(); 1232 let res = []; 1233 funcNames.forEach(function (funcName) { 1234 const subNodes = map.get(funcName); 1235 res.push(subNodes.length == 1 ? subNodes[0] : subNodes); 1236 }); 1237 return res; 1238 } 1239 1240 // nodes can be a CallNode, or an array of CallNodes with the same functionId. 1241 _renderSvgNodesWithSameRoot(nodes, depth, xOffset) { 1242 let x = xOffset; 1243 let y = this._getYForDepth(depth); 1244 let isArray = Array.isArray(nodes); 1245 let funcId; 1246 let sumCount; 1247 if (isArray) { 1248 funcId = nodes[0].f; 1249 sumCount = nodes.reduce((acc, cur) => acc + cur.s, 0); 1250 } else { 1251 funcId = nodes.f; 1252 sumCount = nodes.s; 1253 } 1254 let width = this._getWidthPercentage(sumCount); 1255 if (width < 0.1) { 1256 return xOffset; 1257 } 1258 let color = this._getHeatColor(width); 1259 let borderColor = {}; 1260 for (let key in color) { 1261 borderColor[key] = Math.max(0, color[key] - 50); 1262 } 1263 let funcName = getFuncName(funcId); 1264 let libName = getLibNameOfFunction(funcId); 1265 let sampleWeight = this.sampleWeightFunction(sumCount, this.totalCount); 1266 let title = funcName + ' | ' + libName + ' (' + sumCount + ' events: ' + 1267 sampleWeight + ')'; 1268 this.svgStr.push(`<g><title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}" 1269 depth="${depth}" width="${width}%" owidth="${width}" height="15.0" 1270 ofill="rgb(${color.r},${color.g},${color.b})" 1271 fill="rgb(${color.r},${color.g},${color.b})" 1272 style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/> 1273 <text x="${x}%" y="${y + 12}"></text></g>`); 1274 1275 let children = isArray ? this._splitChildrenForNodes(nodes) : nodes.c; 1276 let childXOffset = xOffset; 1277 for (let child of children) { 1278 childXOffset = this._renderSvgNodesWithSameRoot(child, depth + 1, childXOffset); 1279 } 1280 return xOffset + width; 1281 } 1282 1283 _renderUnzoomNode() { 1284 this.svgStr.push(`<rect id="zoom_rect_${this.id}" style="display:none;stroke:rgb(0,0,0);" 1285 rx="10" ry="10" x="10" y="10" width="80" height="30" 1286 fill="rgb(255,255,255)"/> 1287 <text id="zoom_text_${this.id}" x="19" y="30" style="display:none">Zoom out</text>`); 1288 } 1289 1290 _renderInfoNode() { 1291 this.svgStr.push(`<clipPath id="info_clip_path_${this.id}"> 1292 <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" 1293 width="789" height="30" fill="rgb(255,255,255)"/> 1294 </clipPath> 1295 <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" 1296 width="799" height="30" fill="rgb(255,255,255)"/> 1297 <text clip-path="url(#info_clip_path_${this.id})" 1298 id="info_text_${this.id}" x="128" y="30"></text>`); 1299 } 1300 1301 _renderPercentNode() { 1302 this.svgStr.push(`<rect style="stroke:rgb(0,0,0);" rx="10" ry="10" 1303 x="934" y="10" width="150" height="30" 1304 fill="rgb(255,255,255)"/> 1305 <text id="percent_text_${this.id}" text-anchor="end" 1306 x="1074" y="30"></text>`); 1307 } 1308 1309 _renderSearchNode() { 1310 this.svgStr.push(`<rect style="stroke:rgb(0,0,0); rx="10" ry="10" 1311 x="1150" y="10" width="80" height="30" 1312 fill="rgb(255,255,255)" class="search"/> 1313 <text x="1160" y="30" class="search">Search</text>`); 1314 } 1315 1316 _adjustTextSizeForNode(g) { 1317 let text = g.find('text'); 1318 let width = parseFloat(g.find('rect').attr('width')) * this.svgWidth * 0.01; 1319 if (width < 28) { 1320 text.text(''); 1321 return; 1322 } 1323 let methodName = g.find('title').text().split(' | ')[0]; 1324 let numCharacters; 1325 for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) { 1326 if (numCharacters * 7.5 <= width) { 1327 break; 1328 } 1329 } 1330 if (numCharacters == methodName.length) { 1331 text.text(methodName); 1332 } else { 1333 text.text(methodName.substring(0, numCharacters - 2) + '..'); 1334 } 1335 } 1336 1337 _adjustTextSize() { 1338 this.svgWidth = $(window).width(); 1339 let thisObj = this; 1340 this.svg.find('g').each(function(_, g) { 1341 thisObj._adjustTextSizeForNode($(g)); 1342 }); 1343 } 1344 1345 _enableZoom() { 1346 this.zoomStack = [null]; 1347 this.svg.find('g').css('cursor', 'pointer').click(zoom); 1348 this.svg.find(`#zoom_rect_${this.id}`).css('cursor', 'pointer').click(unzoom); 1349 this.svg.find(`#zoom_text_${this.id}`).css('cursor', 'pointer').click(unzoom); 1350 1351 let thisObj = this; 1352 function zoom() { 1353 thisObj.zoomStack.push(this); 1354 displayFromElement(this); 1355 thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'block'); 1356 thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'block'); 1357 } 1358 1359 function unzoom() { 1360 if (thisObj.zoomStack.length > 1) { 1361 thisObj.zoomStack.pop(); 1362 displayFromElement(thisObj.zoomStack[thisObj.zoomStack.length - 1]); 1363 if (thisObj.zoomStack.length == 1) { 1364 thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'none'); 1365 thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'none'); 1366 } 1367 } 1368 } 1369 1370 function displayFromElement(g) { 1371 let clickedOriginX = 0; 1372 let clickedDepth = 0; 1373 let clickedOriginWidth = 100; 1374 let scaleFactor = 1; 1375 if (g) { 1376 g = $(g); 1377 let clickedRect = g.find('rect'); 1378 clickedOriginX = parseFloat(clickedRect.attr('ox')); 1379 clickedDepth = parseInt(clickedRect.attr('depth')); 1380 clickedOriginWidth = parseFloat(clickedRect.attr('owidth')); 1381 scaleFactor = 100.0 / clickedOriginWidth; 1382 } 1383 thisObj.svg.find('g').each(function(_, g) { 1384 g = $(g); 1385 let text = g.find('text'); 1386 let rect = g.find('rect'); 1387 let depth = parseInt(rect.attr('depth')); 1388 let ox = parseFloat(rect.attr('ox')); 1389 let owidth = parseFloat(rect.attr('owidth')); 1390 if (depth < clickedDepth || ox < clickedOriginX - 1e-9 || 1391 ox + owidth > clickedOriginX + clickedOriginWidth + 1e-9) { 1392 rect.css('display', 'none'); 1393 text.css('display', 'none'); 1394 } else { 1395 rect.css('display', 'block'); 1396 text.css('display', 'block'); 1397 let nx = (ox - clickedOriginX) * scaleFactor + '%'; 1398 let ny = thisObj._getYForDepth(depth - clickedDepth); 1399 rect.attr('x', nx); 1400 rect.attr('y', ny); 1401 rect.attr('width', owidth * scaleFactor + '%'); 1402 text.attr('x', nx); 1403 text.attr('y', ny + 12); 1404 thisObj._adjustTextSizeForNode(g); 1405 } 1406 }); 1407 } 1408 } 1409 1410 _enableInfo() { 1411 this.selected = null; 1412 let thisObj = this; 1413 this.svg.find('g').on('mouseenter', function() { 1414 if (thisObj.selected) { 1415 thisObj.selected.css('stroke-width', '0'); 1416 } 1417 // Mark current node. 1418 let g = $(this); 1419 thisObj.selected = g; 1420 g.css('stroke', 'black').css('stroke-width', '0.5'); 1421 1422 // Parse title. 1423 let title = g.find('title').text(); 1424 let methodAndInfo = title.split(' | '); 1425 thisObj.svg.find(`#info_text_${thisObj.id}`).text(methodAndInfo[0]); 1426 1427 // Parse percentage. 1428 // '/system/lib64/libhwbinder.so (4 events: 0.28%)' 1429 let regexp = /.* \(.*:\s+(.*)\)/g; 1430 let match = regexp.exec(methodAndInfo[1]); 1431 let percentage = ''; 1432 if (match && match.length > 1) { 1433 percentage = match[1]; 1434 } 1435 thisObj.svg.find(`#percent_text_${thisObj.id}`).text(percentage); 1436 }); 1437 } 1438 1439 _enableSearch() { 1440 this.svg.find('.search').css('cursor', 'pointer').click(() => { 1441 let term = prompt('Search for:', ''); 1442 if (!term) { 1443 this.svg.find('g > rect').each(function() { 1444 this.attributes['fill'].value = this.attributes['ofill'].value; 1445 }); 1446 } else { 1447 this.svg.find('g').each(function() { 1448 let title = this.getElementsByTagName('title')[0]; 1449 let rect = this.getElementsByTagName('rect')[0]; 1450 if (title.textContent.indexOf(term) != -1) { 1451 rect.attributes['fill'].value = 'rgb(230,100,230)'; 1452 } else { 1453 rect.attributes['fill'].value = rect.attributes['ofill'].value; 1454 } 1455 }); 1456 } 1457 }); 1458 } 1459 1460 _adjustTextSizeOnResize() { 1461 function throttle(callback) { 1462 let running = false; 1463 return function() { 1464 if (!running) { 1465 running = true; 1466 window.requestAnimationFrame(function () { 1467 callback(); 1468 running = false; 1469 }); 1470 } 1471 }; 1472 } 1473 $(window).resize(throttle(() => this._adjustTextSize())); 1474 } 1475} 1476 1477 1478class SourceFile { 1479 1480 constructor(fileId) { 1481 this.path = getSourceFilePath(fileId); 1482 this.code = getSourceCode(fileId); 1483 this.showLines = {}; // map from line number to {eventCount, subtreeEventCount}. 1484 this.hasCount = false; 1485 } 1486 1487 addLineRange(startLine, endLine) { 1488 for (let i = startLine; i <= endLine; ++i) { 1489 if (i in this.showLines || !(i in this.code)) { 1490 continue; 1491 } 1492 this.showLines[i] = {eventCount: 0, subtreeEventCount: 0}; 1493 } 1494 } 1495 1496 addLineCount(lineNumber, eventCount, subtreeEventCount) { 1497 let line = this.showLines[lineNumber]; 1498 if (line) { 1499 line.eventCount += eventCount; 1500 line.subtreeEventCount += subtreeEventCount; 1501 this.hasCount = true; 1502 } 1503 } 1504} 1505 1506// Return a list of SourceFile related to a function. 1507function collectSourceFilesForFunction(func) { 1508 if (!func.hasOwnProperty('s')) { 1509 return null; 1510 } 1511 let hitLines = func.s; 1512 let sourceFiles = {}; // map from sourceFileId to SourceFile. 1513 1514 function getFile(fileId) { 1515 let file = sourceFiles[fileId]; 1516 if (!file) { 1517 file = sourceFiles[fileId] = new SourceFile(fileId); 1518 } 1519 return file; 1520 } 1521 1522 // Show lines for the function. 1523 let funcRange = getFuncSourceRange(func.f); 1524 if (funcRange) { 1525 let file = getFile(funcRange.fileId); 1526 file.addLineRange(funcRange.startLine); 1527 } 1528 1529 // Show lines for hitLines. 1530 for (let hitLine of hitLines) { 1531 let file = getFile(hitLine.f); 1532 file.addLineRange(hitLine.l - 5, hitLine.l + 5); 1533 file.addLineCount(hitLine.l, hitLine.e, hitLine.s); 1534 } 1535 1536 let result = []; 1537 // Show the source file containing the function before other source files. 1538 if (funcRange) { 1539 let file = getFile(funcRange.fileId); 1540 if (file.hasCount) { 1541 result.push(file); 1542 } 1543 delete sourceFiles[funcRange.fileId]; 1544 } 1545 for (let fileId in sourceFiles) { 1546 let file = sourceFiles[fileId]; 1547 if (file.hasCount) { 1548 result.push(file); 1549 } 1550 } 1551 return result.length > 0 ? result : null; 1552} 1553 1554// Show annotated source code of a function. 1555class SourceCodeView { 1556 1557 constructor(divContainer, sourceFiles, totalCount) { 1558 this.div = $('<div>'); 1559 this.div.appendTo(divContainer); 1560 this.sourceFiles = sourceFiles; 1561 this.totalCount = totalCount; 1562 } 1563 1564 drawDetails(sampleWeightFunction) { 1565 google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction)); 1566 } 1567 1568 realDraw(sampleWeightFunction) { 1569 this.div.empty(); 1570 // For each file, draw a table of 'Line', 'Total', 'Self', 'Code'. 1571 for (let sourceFile of this.sourceFiles) { 1572 let rows = []; 1573 let lineNumbers = Object.keys(sourceFile.showLines); 1574 lineNumbers.sort((a, b) => a - b); 1575 for (let lineNumber of lineNumbers) { 1576 let code = getHtml('pre', {text: sourceFile.code[lineNumber]}); 1577 let countInfo = sourceFile.showLines[lineNumber]; 1578 let totalValue = ''; 1579 let selfValue = ''; 1580 if (countInfo.subtreeEventCount != 0) { 1581 totalValue = sampleWeightFunction(countInfo.subtreeEventCount, this.totalCount); 1582 selfValue = sampleWeightFunction(countInfo.eventCount, this.totalCount); 1583 } 1584 rows.push([lineNumber, totalValue, selfValue, code]); 1585 } 1586 1587 let data = new google.visualization.DataTable(); 1588 data.addColumn('string', 'Line'); 1589 data.addColumn('string', 'Total'); 1590 data.addColumn('string', 'Self'); 1591 data.addColumn('string', 'Code'); 1592 data.addRows(rows); 1593 data.setColumnProperty(0, 'className', 'colForLine'); 1594 data.setColumnProperty(1, 'className', 'colForCount'); 1595 data.setColumnProperty(2, 'className', 'colForCount'); 1596 this.div.append(getHtml('pre', {text: sourceFile.path})); 1597 let wrapperDiv = $('<div>'); 1598 wrapperDiv.appendTo(this.div); 1599 let table = new google.visualization.Table(wrapperDiv.get(0)); 1600 table.draw(data, { 1601 width: '100%', 1602 sort: 'disable', 1603 frozenColumns: 3, 1604 allowHtml: true, 1605 }); 1606 } 1607 } 1608} 1609 1610// Return a list of disassembly related to a function. 1611function collectDisassemblyForFunction(func) { 1612 if (!func.hasOwnProperty('a')) { 1613 return null; 1614 } 1615 let hitAddrs = func.a; 1616 let rawCode = getFuncDisassembly(func.f); 1617 if (!rawCode) { 1618 return null; 1619 } 1620 1621 // Annotate disassembly with event count information. 1622 let annotatedCode = []; 1623 let codeForLastAddr = null; 1624 let hitAddrPos = 0; 1625 let hasCount = false; 1626 1627 function addEventCount(addr) { 1628 while (hitAddrPos < hitAddrs.length && BigInt(hitAddrs[hitAddrPos].a) < addr) { 1629 if (codeForLastAddr) { 1630 codeForLastAddr.eventCount += hitAddrs[hitAddrPos].e; 1631 codeForLastAddr.subtreeEventCount += hitAddrs[hitAddrPos].s; 1632 hasCount = true; 1633 } 1634 hitAddrPos++; 1635 } 1636 } 1637 1638 for (let line of rawCode) { 1639 let code = line[0]; 1640 let addr = BigInt(line[1]); 1641 1642 addEventCount(addr); 1643 let item = {code: code, eventCount: 0, subtreeEventCount: 0}; 1644 annotatedCode.push(item); 1645 // Objdump sets addr to 0 when a disassembly line is not associated with an addr. 1646 if (addr != 0) { 1647 codeForLastAddr = item; 1648 } 1649 } 1650 addEventCount(Number.MAX_VALUE); 1651 return hasCount ? annotatedCode : null; 1652} 1653 1654// Show annotated disassembly of a function. 1655class DisassemblyView { 1656 1657 constructor(divContainer, disassembly, totalCount) { 1658 this.div = $('<div>'); 1659 this.div.appendTo(divContainer); 1660 this.disassembly = disassembly; 1661 this.totalCount = totalCount; 1662 } 1663 1664 drawDetails(sampleWeightFunction) { 1665 google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction)); 1666 } 1667 1668 realDraw(sampleWeightFunction) { 1669 this.div.empty(); 1670 // Draw a table of 'Total', 'Self', 'Code'. 1671 let rows = []; 1672 for (let line of this.disassembly) { 1673 let code = getHtml('pre', {text: line.code}); 1674 let totalValue = ''; 1675 let selfValue = ''; 1676 if (line.subtreeEventCount != 0) { 1677 totalValue = sampleWeightFunction(line.subtreeEventCount, this.totalCount); 1678 selfValue = sampleWeightFunction(line.eventCount, this.totalCount); 1679 } 1680 rows.push([totalValue, selfValue, code]); 1681 } 1682 let data = new google.visualization.DataTable(); 1683 data.addColumn('string', 'Total'); 1684 data.addColumn('string', 'Self'); 1685 data.addColumn('string', 'Code'); 1686 data.addRows(rows); 1687 data.setColumnProperty(0, 'className', 'colForCount'); 1688 data.setColumnProperty(1, 'className', 'colForCount'); 1689 let wrapperDiv = $('<div>'); 1690 wrapperDiv.appendTo(this.div); 1691 let table = new google.visualization.Table(wrapperDiv.get(0)); 1692 table.draw(data, { 1693 width: '100%', 1694 sort: 'disable', 1695 frozenColumns: 2, 1696 allowHtml: true, 1697 }); 1698 } 1699} 1700 1701 1702function initGlobalObjects() { 1703 let recordData = $('#record_data').text(); 1704 gRecordInfo = JSON.parse(recordData); 1705 gProcesses = gRecordInfo.processNames; 1706 gThreads = gRecordInfo.threadNames; 1707 gLibList = gRecordInfo.libList; 1708 gFunctionMap = gRecordInfo.functionMap; 1709 gSampleInfo = gRecordInfo.sampleInfo; 1710 gSourceFiles = gRecordInfo.sourceFiles; 1711} 1712 1713function createTabs() { 1714 gTabs = new TabManager($('div#report_content')); 1715 gTabs.addTab('Chart Statistics', new ChartStatTab()); 1716 gTabs.addTab('Sample Table', new SampleTableTab()); 1717 gTabs.addTab('Flamegraph', new FlameGraphTab()); 1718} 1719 1720// Global draw objects 1721let gTabs; 1722let gProgressBar = new ProgressBar(); 1723 1724// Gobal Json Data 1725let gRecordInfo; 1726let gProcesses; 1727let gThreads; 1728let gLibList; 1729let gFunctionMap; 1730let gSampleInfo; 1731let gSourceFiles; 1732 1733function updateProgress(text, progress) { 1734 return () => gProgressBar.updateAsync(text, progress); 1735} 1736 1737function addProgress(progress) { 1738 return () => gProgressBar.updateAsync(null, gProgressBar.progress + progress); 1739} 1740 1741function hideProgress() { 1742 return () => gProgressBar.hide(); 1743} 1744 1745function createPromise(callback) { 1746 if (callback) { 1747 return new Promise((resolve, _) => callback(resolve)); 1748 } 1749 return new Promise((resolve,_) => resolve()); 1750} 1751 1752function waitDocumentReady() { 1753 return createPromise((resolve) => $(document).ready(resolve)); 1754} 1755 1756function wait(functionCall) { 1757 return () => { 1758 functionCall(); 1759 return createPromise(); 1760 }; 1761} 1762 1763createPromise() 1764 .then(updateProgress('Load page...', 0)) 1765 .then(waitDocumentReady) 1766 .then(updateProgress('Parse Json data...', 20)) 1767 .then(wait(initGlobalObjects)) 1768 .then(updateProgress('Create tabs...', 30)) 1769 .then(wait(createTabs)) 1770 .then(updateProgress('Draw ChartStat...', 40)) 1771 .then(() => gTabs.setActiveAsync('Chart Statistics')) 1772 .then(updateProgress(null, 100)) 1773 .then(hideProgress()); 1774})(); 1775