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