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