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 $(document).ready(function() { 20 21 function openHtml(name, attrs={}) { 22 let s = `<${name} `; 23 for (let key in attrs) { 24 s += `${key}="${attrs[key]}" `; 25 } 26 s += '>'; 27 return s; 28 } 29 30 function closeHtml(name) { 31 return `</${name}>`; 32 } 33 34 function getHtml(name, attrs={}) { 35 let text; 36 if ('text' in attrs) { 37 text = attrs.text; 38 delete attrs.text; 39 } 40 let s = openHtml(name, attrs); 41 if (text) { 42 s += text; 43 } 44 s += closeHtml(name); 45 return s; 46 } 47 48 function getTableRow(cols, colName, attrs={}) { 49 let s = openHtml('tr', attrs); 50 for (let col of cols) { 51 s += `<${colName}>${col}</${colName}>`; 52 } 53 s += '</tr>'; 54 return s; 55 } 56 57 function toPercentageStr(percentage) { 58 return percentage.toFixed(2) + '%'; 59 } 60 61 function getProcessName(pid) { 62 let name = gProcesses[pid]; 63 return name ? `${pid} (${name})`: pid.toString(); 64 } 65 66 function getThreadName(tid) { 67 let name = gThreads[tid]; 68 return name ? `${tid} (${name})`: tid.toString(); 69 } 70 71 function getLibName(libId) { 72 return gLibList[libId]; 73 } 74 75 function getFuncName(funcId) { 76 return gFunctionMap[funcId].f; 77 } 78 79 function getLibNameOfFunction(funcId) { 80 return getLibName(gFunctionMap[funcId].l); 81 } 82 83 function getFuncSourceRange(funcId) { 84 let func = gFunctionMap[funcId]; 85 if (func.hasOwnProperty('s')) { 86 return {fileId: func.s[0], startLine: func.s[1], endLine: func.s[2]}; 87 } 88 return null; 89 } 90 91 function getFuncDisassembly(funcId) { 92 let func = gFunctionMap[funcId]; 93 return func.hasOwnProperty('d') ? func.d : null; 94 } 95 96 function getSourceFilePath(sourceFileId) { 97 return gSourceFiles[sourceFileId].path; 98 } 99 100 function getSourceCode(sourceFileId) { 101 return gSourceFiles[sourceFileId].code; 102 } 103 104 function isClockEvent(eventInfo) { 105 return eventInfo.eventName.includes('task-clock') || 106 eventInfo.eventName.includes('cpu-clock'); 107 } 108 109 class TabManager { 110 constructor(divContainer) { 111 this.div = $('<div>', {id: 'tabs'}); 112 this.div.appendTo(divContainer); 113 this.div.append(getHtml('ul')); 114 this.tabs = []; 115 this.isDrawCalled = false; 116 } 117 118 addTab(title, tabObj) { 119 let id = 'tab_' + this.div.children().length; 120 let tabDiv = $('<div>', {id: id}); 121 tabDiv.appendTo(this.div); 122 this.div.children().first().append( 123 getHtml('li', {text: getHtml('a', {href: '#' + id, text: title})})); 124 tabObj.init(tabDiv); 125 this.tabs.push(tabObj); 126 if (this.isDrawCalled) { 127 this.div.tabs('refresh'); 128 } 129 return tabObj; 130 } 131 132 findTab(title) { 133 let links = this.div.find('li a'); 134 for (let i = 0; i < links.length; ++i) { 135 if (links.eq(i).text() == title) { 136 return this.tabs[i]; 137 } 138 } 139 return null; 140 } 141 142 draw() { 143 this.div.tabs({ 144 active: 0, 145 }); 146 this.tabs.forEach(function(tab) { 147 tab.draw(); 148 }); 149 this.isDrawCalled = true; 150 } 151 152 setActive(tabObj) { 153 for (let i = 0; i < this.tabs.length; ++i) { 154 if (this.tabs[i] == tabObj) { 155 this.div.tabs('option', 'active', i); 156 break; 157 } 158 } 159 } 160 } 161 162 // Show global information retrieved from the record file, including: 163 // record time 164 // machine type 165 // Android version 166 // record cmdline 167 // total samples 168 class RecordFileView { 169 constructor(divContainer) { 170 this.div = $('<div>'); 171 this.div.appendTo(divContainer); 172 } 173 174 draw() { 175 google.charts.setOnLoadCallback(() => this.realDraw()); 176 } 177 178 realDraw() { 179 this.div.empty(); 180 // Draw a table of 'Name', 'Value'. 181 let rows = []; 182 if (gRecordInfo.recordTime) { 183 rows.push(['Record Time', gRecordInfo.recordTime]); 184 } 185 if (gRecordInfo.machineType) { 186 rows.push(['Machine Type', gRecordInfo.machineType]); 187 } 188 if (gRecordInfo.androidVersion) { 189 rows.push(['Android Version', gRecordInfo.androidVersion]); 190 } 191 if (gRecordInfo.recordCmdline) { 192 rows.push(['Record cmdline', gRecordInfo.recordCmdline]); 193 } 194 rows.push(['Total Samples', '' + gRecordInfo.totalSamples]); 195 196 let data = new google.visualization.DataTable(); 197 data.addColumn('string', ''); 198 data.addColumn('string', ''); 199 data.addRows(rows); 200 for (let i = 0; i < rows.length; ++i) { 201 data.setProperty(i, 0, 'className', 'boldTableCell'); 202 } 203 let table = new google.visualization.Table(this.div.get(0)); 204 table.draw(data, { 205 width: '100%', 206 sort: 'disable', 207 allowHtml: true, 208 cssClassNames: { 209 'tableCell': 'tableCell', 210 }, 211 }); 212 } 213 } 214 215 // Show pieChart of event count percentage of each process, thread, library and function. 216 class ChartView { 217 constructor(divContainer, eventInfo) { 218 this.id = divContainer.children().length; 219 this.div = $('<div>', {id: 'chartstat_' + this.id}); 220 this.div.appendTo(divContainer); 221 this.eventInfo = eventInfo; 222 this.processInfo = null; 223 this.threadInfo = null; 224 this.libInfo = null; 225 this.states = { 226 SHOW_EVENT_INFO: 1, 227 SHOW_PROCESS_INFO: 2, 228 SHOW_THREAD_INFO: 3, 229 SHOW_LIB_INFO: 4, 230 }; 231 if (isClockEvent(this.eventInfo)) { 232 this.getSampleWeight = function (eventCount) { 233 return (eventCount / 1000000.0).toFixed(3) + ' ms'; 234 } 235 } else { 236 this.getSampleWeight = (eventCount) => '' + eventCount; 237 } 238 } 239 240 _getState() { 241 if (this.libInfo) { 242 return this.states.SHOW_LIB_INFO; 243 } 244 if (this.threadInfo) { 245 return this.states.SHOW_THREAD_INFO; 246 } 247 if (this.processInfo) { 248 return this.states.SHOW_PROCESS_INFO; 249 } 250 return this.states.SHOW_EVENT_INFO; 251 } 252 253 _goBack() { 254 let state = this._getState(); 255 if (state == this.states.SHOW_PROCESS_INFO) { 256 this.processInfo = null; 257 } else if (state == this.states.SHOW_THREAD_INFO) { 258 this.threadInfo = null; 259 } else if (state == this.states.SHOW_LIB_INFO) { 260 this.libInfo = null; 261 } 262 this.draw(); 263 } 264 265 _selectHandler(chart) { 266 let selectedItem = chart.getSelection()[0]; 267 if (selectedItem) { 268 let state = this._getState(); 269 if (state == this.states.SHOW_EVENT_INFO) { 270 this.processInfo = this.eventInfo.processes[selectedItem.row]; 271 } else if (state == this.states.SHOW_PROCESS_INFO) { 272 this.threadInfo = this.processInfo.threads[selectedItem.row]; 273 } else if (state == this.states.SHOW_THREAD_INFO) { 274 this.libInfo = this.threadInfo.libs[selectedItem.row]; 275 } 276 this.draw(); 277 } 278 } 279 280 draw() { 281 google.charts.setOnLoadCallback(() => this.realDraw()); 282 } 283 284 realDraw() { 285 this.div.empty(); 286 this._drawTitle(); 287 this._drawPieChart(); 288 } 289 290 _drawTitle() { 291 // Draw a table of 'Name', 'Event Count'. 292 let rows = []; 293 rows.push(['Event Type: ' + this.eventInfo.eventName, 294 this.getSampleWeight(this.eventInfo.eventCount)]); 295 if (this.processInfo) { 296 rows.push(['Process: ' + getProcessName(this.processInfo.pid), 297 this.getSampleWeight(this.processInfo.eventCount)]); 298 } 299 if (this.threadInfo) { 300 rows.push(['Thread: ' + getThreadName(this.threadInfo.tid), 301 this.getSampleWeight(this.threadInfo.eventCount)]); 302 } 303 if (this.libInfo) { 304 rows.push(['Library: ' + getLibName(this.libInfo.libId), 305 this.getSampleWeight(this.libInfo.eventCount)]); 306 } 307 let data = new google.visualization.DataTable(); 308 data.addColumn('string', ''); 309 data.addColumn('string', ''); 310 data.addRows(rows); 311 for (let i = 0; i < rows.length; ++i) { 312 data.setProperty(i, 0, 'className', 'boldTableCell'); 313 } 314 let wrapperDiv = $('<div>'); 315 wrapperDiv.appendTo(this.div); 316 let table = new google.visualization.Table(wrapperDiv.get(0)); 317 table.draw(data, { 318 width: '100%', 319 sort: 'disable', 320 allowHtml: true, 321 cssClassNames: { 322 'tableCell': 'tableCell', 323 }, 324 }); 325 if (this._getState() != this.states.SHOW_EVENT_INFO) { 326 let button = $('<button>', {text: 'Back'}); 327 button.appendTo(this.div); 328 button.button().click(() => this._goBack()); 329 } 330 } 331 332 _drawPieChart() { 333 let state = this._getState(); 334 let title = null; 335 let firstColumn = null; 336 let rows = []; 337 let thisObj = this; 338 function getItem(name, eventCount, totalEventCount) { 339 let sampleWeight = thisObj.getSampleWeight(eventCount); 340 let percent = (eventCount * 100.0 / totalEventCount).toFixed(2) + '%'; 341 return [name, eventCount, getHtml('pre', {text: name}) + 342 getHtml('b', {text: `${sampleWeight} (${percent})`})]; 343 } 344 345 if (state == this.states.SHOW_EVENT_INFO) { 346 title = 'Processes in event type ' + this.eventInfo.eventName; 347 firstColumn = 'Process'; 348 for (let process of this.eventInfo.processes) { 349 rows.push(getItem('Process: ' + getProcessName(process.pid), process.eventCount, 350 this.eventInfo.eventCount)); 351 } 352 } else if (state == this.states.SHOW_PROCESS_INFO) { 353 title = 'Threads in process ' + getProcessName(this.processInfo.pid); 354 firstColumn = 'Thread'; 355 for (let thread of this.processInfo.threads) { 356 rows.push(getItem('Thread: ' + getThreadName(thread.tid), thread.eventCount, 357 this.processInfo.eventCount)); 358 } 359 } else if (state == this.states.SHOW_THREAD_INFO) { 360 title = 'Libraries in thread ' + getThreadName(this.threadInfo.tid); 361 firstColumn = 'Library'; 362 for (let lib of this.threadInfo.libs) { 363 rows.push(getItem('Library: ' + getLibName(lib.libId), lib.eventCount, 364 this.threadInfo.eventCount)); 365 } 366 } else if (state == this.states.SHOW_LIB_INFO) { 367 title = 'Functions in library ' + getLibName(this.libInfo.libId); 368 firstColumn = 'Function'; 369 for (let func of this.libInfo.functions) { 370 rows.push(getItem('Function: ' + getFuncName(func.g.f), func.g.e, 371 this.libInfo.eventCount)); 372 } 373 } 374 let data = new google.visualization.DataTable(); 375 data.addColumn('string', firstColumn); 376 data.addColumn('number', 'EventCount'); 377 data.addColumn({type: 'string', role: 'tooltip', p: {html: true}}); 378 data.addRows(rows); 379 380 let wrapperDiv = $('<div>'); 381 wrapperDiv.appendTo(this.div); 382 let chart = new google.visualization.PieChart(wrapperDiv.get(0)); 383 chart.draw(data, { 384 title: title, 385 width: 1000, 386 height: 600, 387 tooltip: {isHtml: true}, 388 }); 389 google.visualization.events.addListener(chart, 'select', () => this._selectHandler(chart)); 390 } 391 } 392 393 394 class ChartStatTab { 395 constructor() { 396 } 397 398 init(div) { 399 this.div = div; 400 this.recordFileView = new RecordFileView(this.div); 401 this.chartViews = []; 402 for (let eventInfo of gSampleInfo) { 403 this.chartViews.push(new ChartView(this.div, eventInfo)); 404 } 405 } 406 407 draw() { 408 this.recordFileView.draw(); 409 for (let charView of this.chartViews) { 410 charView.draw(); 411 } 412 } 413 } 414 415 416 class SampleTableTab { 417 constructor() { 418 } 419 420 init(div) { 421 this.div = div; 422 this.selectorView = null; 423 this.sampleTableViews = []; 424 } 425 426 draw() { 427 this.selectorView = new SampleTableWeightSelectorView(this.div, gSampleInfo[0], 428 () => this.onSampleWeightChange()); 429 this.selectorView.draw(); 430 for (let eventInfo of gSampleInfo) { 431 this.div.append(getHtml('hr')); 432 this.sampleTableViews.push(new SampleTableView(this.div, eventInfo)); 433 } 434 this.onSampleWeightChange(); 435 } 436 437 onSampleWeightChange() { 438 for (let i = 0; i < gSampleInfo.length; ++i) { 439 let sampleWeightFunction = this.selectorView.getSampleWeightFunction(gSampleInfo[i]); 440 let sampleWeightSuffix = this.selectorView.getSampleWeightSuffix(gSampleInfo[i]); 441 this.sampleTableViews[i].draw(sampleWeightFunction, sampleWeightSuffix); 442 } 443 } 444 } 445 446 // Select the way to show sample weight in SampleTableTab. 447 // 1. Show percentage of event count. 448 // 2. Show event count (For cpu-clock and task-clock events, it is time in ms). 449 class SampleTableWeightSelectorView { 450 constructor(divContainer, firstEventInfo, onSelectChange) { 451 this.div = $('<div>'); 452 this.div.appendTo(divContainer); 453 this.onSelectChange = onSelectChange; 454 this.options = { 455 SHOW_PERCENT: 0, 456 SHOW_EVENT_COUNT: 1, 457 }; 458 if (isClockEvent(firstEventInfo)) { 459 this.curOption = this.options.SHOW_EVENT_COUNT; 460 } else { 461 this.curOption = this.options.SHOW_PERCENT; 462 } 463 } 464 465 draw() { 466 let options = ['Show percentage of event count', 'Show event count']; 467 let optionStr = ''; 468 for (let i = 0; i < options.length; ++i) { 469 optionStr += getHtml('option', {value: i, text: options[i]}); 470 } 471 this.div.append(getHtml('select', {text: optionStr})); 472 let selectMenu = this.div.children().last(); 473 selectMenu.children().eq(this.curOption).attr('selected', 'selected'); 474 let thisObj = this; 475 selectMenu.selectmenu({ 476 change: function() { 477 thisObj.curOption = this.value; 478 thisObj.onSelectChange(); 479 }, 480 width: '100%', 481 }); 482 } 483 484 getSampleWeightFunction(eventInfo) { 485 if (this.curOption == this.options.SHOW_PERCENT) { 486 return function(eventCount) { 487 return (eventCount * 100.0 / eventInfo.eventCount).toFixed(2) + '%'; 488 } 489 } 490 if (isClockEvent(eventInfo)) { 491 return (eventCount) => (eventCount / 1000000.0).toFixed(3); 492 } 493 return (eventCount) => '' + eventCount; 494 } 495 496 getSampleWeightSuffix(eventInfo) { 497 if (this.curOption == this.options.SHOW_EVENT_COUNT && isClockEvent(eventInfo)) { 498 return ' ms'; 499 } 500 return ''; 501 } 502 } 503 504 505 class SampleTableView { 506 constructor(divContainer, eventInfo) { 507 this.id = divContainer.children().length; 508 this.div = $('<div>'); 509 this.div.appendTo(divContainer); 510 this.eventInfo = eventInfo; 511 } 512 513 draw(getSampleWeight, sampleWeightSuffix) { 514 // Draw a table of 'Total', 'Self', 'Samples', 'Process', 'Thread', 'Library', 'Function'. 515 this.div.empty(); 516 let eventInfo = this.eventInfo; 517 let sampleWeight = getSampleWeight(eventInfo.eventCount); 518 this.div.append(getHtml('p', {text: `Sample table for event ${eventInfo.eventName}, ` + 519 `total count ${sampleWeight}${sampleWeightSuffix}`})); 520 let tableId = 'sampleTable_' + this.id; 521 let valueSuffix = sampleWeightSuffix.length > 0 ? `(in${sampleWeightSuffix})` : ''; 522 let titles = ['Total' + valueSuffix, 'Self' + valueSuffix, 'Samples', 523 'Process', 'Thread', 'Library', 'Function']; 524 let tableStr = openHtml('table', {id: tableId, cellspacing: '0', width: '100%'}) + 525 getHtml('thead', {text: getTableRow(titles, 'th')}) + 526 getHtml('tfoot', {text: getTableRow(titles, 'th')}) + 527 openHtml('tbody'); 528 for (let i = 0; i < eventInfo.processes.length; ++i) { 529 let processInfo = eventInfo.processes[i]; 530 let processName = getProcessName(processInfo.pid); 531 for (let j = 0; j < processInfo.threads.length; ++j) { 532 let threadInfo = processInfo.threads[j]; 533 let threadName = getThreadName(threadInfo.tid); 534 for (let k = 0; k < threadInfo.libs.length; ++k) { 535 let lib = threadInfo.libs[k]; 536 let libName = getLibName(lib.libId); 537 for (let t = 0; t < lib.functions.length; ++t) { 538 let func = lib.functions[t]; 539 let key = [i, j, k, t].join('_'); 540 let totalValue = getSampleWeight(func.g.s); 541 let selfValue = getSampleWeight(func.g.e); 542 tableStr += getTableRow([totalValue, selfValue, func.c, 543 processName, threadName, libName, 544 getFuncName(func.g.f)], 'td', {key: key}); 545 } 546 } 547 } 548 } 549 tableStr += closeHtml('tbody') + closeHtml('table'); 550 this.div.append(tableStr); 551 let table = this.div.find(`table#${tableId}`).dataTable({ 552 lengthMenu: [10, 20, 50, 100, -1], 553 processing: true, 554 order: [0, 'desc'], 555 responsive: true, 556 }); 557 558 table.find('tr').css('cursor', 'pointer'); 559 table.on('click', 'tr', function() { 560 let key = this.getAttribute('key'); 561 if (!key) { 562 return; 563 } 564 let indexes = key.split('_'); 565 let processInfo = eventInfo.processes[indexes[0]]; 566 let threadInfo = processInfo.threads[indexes[1]]; 567 let lib = threadInfo.libs[indexes[2]]; 568 let func = lib.functions[indexes[3]]; 569 FunctionTab.showFunction(eventInfo, processInfo, threadInfo, lib, func); 570 }); 571 } 572 } 573 574 575 // Show embedded flamegraph generated by inferno. 576 class FlameGraphTab { 577 constructor() { 578 } 579 580 init(div) { 581 this.div = div; 582 } 583 584 draw() { 585 $('div#flamegraph_id').appendTo(this.div).css('display', 'block'); 586 flamegraphInit(); 587 } 588 } 589 590 591 // FunctionTab: show information of a function. 592 // 1. Show the callgrpah and reverse callgraph of a function as flamegraphs. 593 // 2. Show the annotated source code of the function. 594 class FunctionTab { 595 static showFunction(eventInfo, processInfo, threadInfo, lib, func) { 596 let title = 'Function'; 597 let tab = gTabs.findTab(title); 598 if (!tab) { 599 tab = gTabs.addTab(title, new FunctionTab()); 600 } 601 tab.setFunction(eventInfo, processInfo, threadInfo, lib, func); 602 } 603 604 constructor() { 605 this.func = null; 606 this.selectPercent = 'thread'; 607 } 608 609 init(div) { 610 this.div = div; 611 } 612 613 setFunction(eventInfo, processInfo, threadInfo, lib, func) { 614 this.eventInfo = eventInfo; 615 this.processInfo = processInfo; 616 this.threadInfo = threadInfo; 617 this.lib = lib; 618 this.func = func; 619 this.selectorView = null; 620 this.callgraphView = null; 621 this.reverseCallgraphView = null; 622 this.sourceCodeView = null; 623 this.disassemblyView = null; 624 this.draw(); 625 gTabs.setActive(this); 626 } 627 628 draw() { 629 if (!this.func) { 630 return; 631 } 632 this.div.empty(); 633 this._drawTitle(); 634 635 this.selectorView = new FunctionSampleWeightSelectorView(this.div, this.eventInfo, 636 this.processInfo, this.threadInfo, () => this.onSampleWeightChange()); 637 this.selectorView.draw(); 638 639 this.div.append(getHtml('hr')); 640 let funcName = getFuncName(this.func.g.f); 641 this.div.append(getHtml('b', {text: `Functions called by ${funcName}`}) + '<br/>'); 642 this.callgraphView = new FlameGraphView(this.div, this.func.g, false); 643 644 this.div.append(getHtml('hr')); 645 this.div.append(getHtml('b', {text: `Functions calling ${funcName}`}) + '<br/>'); 646 this.reverseCallgraphView = new FlameGraphView(this.div, this.func.rg, true); 647 648 let sourceFiles = collectSourceFilesForFunction(this.func); 649 if (sourceFiles) { 650 this.div.append(getHtml('hr')); 651 this.div.append(getHtml('b', {text: 'SourceCode:'}) + '<br/>'); 652 this.sourceCodeView = new SourceCodeView(this.div, sourceFiles); 653 } 654 655 let disassembly = collectDisassemblyForFunction(this.func); 656 if (disassembly) { 657 this.div.append(getHtml('hr')); 658 this.div.append(getHtml('b', {text: 'Disassembly:'}) + '<br/>'); 659 this.disassemblyView = new DisassemblyView(this.div, disassembly); 660 } 661 662 this.onSampleWeightChange(); // Manually set sample weight function for the first time. 663 } 664 665 _drawTitle() { 666 let eventName = this.eventInfo.eventName; 667 let processName = getProcessName(this.processInfo.pid); 668 let threadName = getThreadName(this.threadInfo.tid); 669 let libName = getLibName(this.lib.libId); 670 let funcName = getFuncName(this.func.g.f); 671 // Draw a table of 'Name', 'Value'. 672 let rows = []; 673 rows.push(['Event Type', eventName]); 674 rows.push(['Process', processName]); 675 rows.push(['Thread', threadName]); 676 rows.push(['Library', libName]); 677 rows.push(['Function', getHtml('pre', {text: funcName})]); 678 let data = new google.visualization.DataTable(); 679 data.addColumn('string', ''); 680 data.addColumn('string', ''); 681 data.addRows(rows); 682 for (let i = 0; i < rows.length; ++i) { 683 data.setProperty(i, 0, 'className', 'boldTableCell'); 684 } 685 let wrapperDiv = $('<div>'); 686 wrapperDiv.appendTo(this.div); 687 let table = new google.visualization.Table(wrapperDiv.get(0)); 688 table.draw(data, { 689 width: '100%', 690 sort: 'disable', 691 allowHtml: true, 692 cssClassNames: { 693 'tableCell': 'tableCell', 694 }, 695 }); 696 } 697 698 onSampleWeightChange() { 699 let sampleWeightFunction = this.selectorView.getSampleWeightFunction(); 700 if (this.callgraphView) { 701 this.callgraphView.draw(sampleWeightFunction); 702 } 703 if (this.reverseCallgraphView) { 704 this.reverseCallgraphView.draw(sampleWeightFunction); 705 } 706 if (this.sourceCodeView) { 707 this.sourceCodeView.draw(sampleWeightFunction); 708 } 709 if (this.disassemblyView) { 710 this.disassemblyView.draw(sampleWeightFunction); 711 } 712 } 713 } 714 715 716 // Select the way to show sample weight in FunctionTab. 717 // 1. Show percentage of event count relative to all processes. 718 // 2. Show percentage of event count relative to the current process. 719 // 3. Show percentage of event count relative to the current thread. 720 // 4. Show absolute event count. 721 // 5. Show event count in milliseconds, only possible for cpu-clock or task-clock events. 722 class FunctionSampleWeightSelectorView { 723 constructor(divContainer, eventInfo, processInfo, threadInfo, onSelectChange) { 724 this.div = $('<div>'); 725 this.div.appendTo(divContainer); 726 this.onSelectChange = onSelectChange; 727 this.eventCountForAllProcesses = eventInfo.eventCount; 728 this.eventCountForProcess = processInfo.eventCount; 729 this.eventCountForThread = threadInfo.eventCount; 730 this.options = { 731 PERCENT_TO_ALL_PROCESSES: 0, 732 PERCENT_TO_CUR_PROCESS: 1, 733 PERCENT_TO_CUR_THREAD: 2, 734 RAW_EVENT_COUNT: 3, 735 EVENT_COUNT_IN_TIME: 4, 736 }; 737 let name = eventInfo.eventName; 738 this.supportEventCountInTime = isClockEvent(eventInfo); 739 if (this.supportEventCountInTime) { 740 this.curOption = this.options.EVENT_COUNT_IN_TIME; 741 } else { 742 this.curOption = this.options.PERCENT_TO_CUR_THREAD; 743 } 744 } 745 746 draw() { 747 let options = []; 748 options.push('Show percentage of event count relative to all processes.'); 749 options.push('Show percentage of event count relative to the current process.'); 750 options.push('Show percentage of event count relative to the current thread.'); 751 options.push('Show event count.'); 752 if (this.supportEventCountInTime) { 753 options.push('Show event count in milliseconds.'); 754 } 755 let optionStr = ''; 756 for (let i = 0; i < options.length; ++i) { 757 optionStr += getHtml('option', {value: i, text: options[i]}); 758 } 759 this.div.append(getHtml('select', {text: optionStr})); 760 let selectMenu = this.div.children().last(); 761 selectMenu.children().eq(this.curOption).attr('selected', 'selected'); 762 let thisObj = this; 763 selectMenu.selectmenu({ 764 change: function() { 765 thisObj.curOption = this.value; 766 thisObj.onSelectChange(); 767 }, 768 width: '100%', 769 }); 770 } 771 772 getSampleWeightFunction() { 773 let thisObj = this; 774 if (this.curOption == this.options.PERCENT_TO_ALL_PROCESSES) { 775 return function(eventCount) { 776 let percent = eventCount * 100.0 / thisObj.eventCountForAllProcesses; 777 return percent.toFixed(2) + '%'; 778 }; 779 } 780 if (this.curOption == this.options.PERCENT_TO_CUR_PROCESS) { 781 return function(eventCount) { 782 let percent = eventCount * 100.0 / thisObj.eventCountForProcess; 783 return percent.toFixed(2) + '%'; 784 }; 785 } 786 if (this.curOption == this.options.PERCENT_TO_CUR_THREAD) { 787 return function(eventCount) { 788 let percent = eventCount * 100.0 / thisObj.eventCountForThread; 789 return percent.toFixed(2) + '%'; 790 }; 791 } 792 if (this.curOption == this.options.RAW_EVENT_COUNT) { 793 return function(eventCount) { 794 return '' + eventCount; 795 }; 796 } 797 if (this.curOption == this.options.EVENT_COUNT_IN_TIME) { 798 return function(eventCount) { 799 let timeInMs = eventCount / 1000000.0; 800 return timeInMs.toFixed(3) + ' ms'; 801 }; 802 } 803 } 804 } 805 806 807 // Given a callgraph, show the flamegraph. 808 class FlameGraphView { 809 // If reverseOrder is false, the root of the flamegraph is at the bottom, 810 // otherwise it is at the top. 811 constructor(divContainer, callgraph, reverseOrder) { 812 this.id = divContainer.children().length; 813 this.div = $('<div>', {id: 'fg_' + this.id}); 814 this.div.appendTo(divContainer); 815 this.callgraph = callgraph; 816 this.reverseOrder = reverseOrder; 817 this.sampleWeightFunction = null; 818 this.svgWidth = $(window).width(); 819 this.svgNodeHeight = 17; 820 this.fontSize = 12; 821 822 function getMaxDepth(node) { 823 let depth = 0; 824 for (let child of node.c) { 825 depth = Math.max(depth, getMaxDepth(child)); 826 } 827 return depth + 1; 828 } 829 this.maxDepth = getMaxDepth(this.callgraph); 830 this.svgHeight = this.svgNodeHeight * (this.maxDepth + 3); 831 } 832 833 draw(sampleWeightFunction) { 834 this.sampleWeightFunction = sampleWeightFunction; 835 this.div.empty(); 836 this.div.css('width', '100%').css('height', this.svgHeight + 'px'); 837 let svgStr = '<svg xmlns="http://www.w3.org/2000/svg" \ 838 xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" \ 839 width="100%" height="100%" style="border: 1px solid black; font-family: Monospace;"> \ 840 </svg>'; 841 this.div.append(svgStr); 842 this.svg = this.div.find('svg'); 843 this._renderBackground(); 844 this._renderSvgNodes(this.callgraph, 0, 0); 845 this._renderUnzoomNode(); 846 this._renderInfoNode(); 847 this._renderPercentNode(); 848 // Make the added nodes in the svg visible. 849 this.div.html(this.div.html()); 850 this.svg = this.div.find('svg'); 851 this._adjustTextSize(); 852 this._enableZoom(); 853 this._enableInfo(); 854 this._adjustTextSizeOnResize(); 855 } 856 857 _renderBackground() { 858 this.svg.append(`<defs > <linearGradient id="background_gradient_${this.id}" 859 y1="0" y2="1" x1="0" x2="0" > \ 860 <stop stop-color="#eeeeee" offset="5%" /> \ 861 <stop stop-color="#efefb1" offset="90%" /> \ 862 </linearGradient> \ 863 </defs> \ 864 <rect x="0" y="0" width="100%" height="100%" \ 865 fill="url(#background_gradient_${this.id})" />`); 866 } 867 868 _getYForDepth(depth) { 869 if (this.reverseOrder) { 870 return (depth + 3) * this.svgNodeHeight; 871 } 872 return this.svgHeight - (depth + 1) * this.svgNodeHeight; 873 } 874 875 _getWidthPercentage(eventCount) { 876 return eventCount * 100.0 / this.callgraph.s; 877 } 878 879 _getHeatColor(widthPercentage) { 880 return { 881 r: Math.floor(245 + 10 * (1 - widthPercentage * 0.01)), 882 g: Math.floor(110 + 105 * (1 - widthPercentage * 0.01)), 883 b: 100, 884 }; 885 } 886 887 _renderSvgNodes(callNode, depth, xOffset) { 888 let x = xOffset; 889 let y = this._getYForDepth(depth); 890 let width = this._getWidthPercentage(callNode.s); 891 if (width < 0.1) { 892 return xOffset; 893 } 894 let color = this._getHeatColor(width); 895 let borderColor = {}; 896 for (let key in color) { 897 borderColor[key] = Math.max(0, color[key] - 50); 898 } 899 let funcName = getFuncName(callNode.f); 900 let libName = getLibNameOfFunction(callNode.f); 901 let sampleWeight = this.sampleWeightFunction(callNode.s); 902 let title = funcName + ' | ' + libName + ' (' + callNode.s + ' events: ' + 903 sampleWeight + ')'; 904 this.svg.append(`<g> <title>${title}</title> <rect x="${x}%" y="${y}" ox="${x}" \ 905 depth="${depth}" width="${width}%" owidth="${width}" height="15.0" \ 906 ofill="rgb(${color.r},${color.g},${color.b})" \ 907 fill="rgb(${color.r},${color.g},${color.b})" \ 908 style="stroke:rgb(${borderColor.r},${borderColor.g},${borderColor.b})"/> \ 909 <text x="${x}%" y="${y + 12}" font-size="${this.fontSize}" \ 910 font-family="Monospace"></text></g>`); 911 912 let childXOffset = xOffset; 913 for (let child of callNode.c) { 914 childXOffset = this._renderSvgNodes(child, depth + 1, childXOffset); 915 } 916 return xOffset + width; 917 } 918 919 _renderUnzoomNode() { 920 this.svg.append(`<rect id="zoom_rect_${this.id}" style="display:none;stroke:rgb(0,0,0);" \ 921 rx="10" ry="10" x="10" y="10" width="80" height="30" \ 922 fill="rgb(255,255,255)"/> \ 923 <text id="zoom_text_${this.id}" x="19" y="30" style="display:none">Zoom out</text>`); 924 } 925 926 _renderInfoNode() { 927 this.svg.append(`<clipPath id="info_clip_path_${this.id}"> \ 928 <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" \ 929 width="789" height="30" fill="rgb(255,255,255)"/> \ 930 </clipPath> \ 931 <rect style="stroke:rgb(0,0,0);" rx="10" ry="10" x="120" y="10" \ 932 width="799" height="30" fill="rgb(255,255,255)"/> \ 933 <text clip-path="url(#info_clip_path_${this.id})" \ 934 id="info_text_${this.id}" x="128" y="30"></text>`); 935 } 936 937 _renderPercentNode() { 938 this.svg.append(`<rect style="stroke:rgb(0,0,0);" rx="10" ry="10" \ 939 x="934" y="10" width="150" height="30" \ 940 fill="rgb(255,255,255)"/> \ 941 <text id="percent_text_${this.id}" text-anchor="end" \ 942 x="1074" y="30"></text>`); 943 } 944 945 _adjustTextSizeForNode(g) { 946 let text = g.find('text'); 947 let width = parseFloat(g.find('rect').attr('width')) * this.svgWidth * 0.01; 948 if (width < 28) { 949 text.text(''); 950 return; 951 } 952 let methodName = g.find('title').text().split(' | ')[0]; 953 let numCharacters; 954 for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) { 955 if (numCharacters * 7.5 <= width) { 956 break; 957 } 958 } 959 if (numCharacters == methodName.length) { 960 text.text(methodName); 961 } else { 962 text.text(methodName.substring(0, numCharacters - 2) + '..'); 963 } 964 } 965 966 _adjustTextSize() { 967 this.svgWidth = $(window).width(); 968 let thisObj = this; 969 this.svg.find('g').each(function(_, g) { 970 thisObj._adjustTextSizeForNode($(g)); 971 }); 972 } 973 974 _enableZoom() { 975 this.zoomStack = [this.svg.find('g').first().get(0)]; 976 this.svg.find('g').css('cursor', 'pointer').click(zoom); 977 this.svg.find(`#zoom_rect_${this.id}`).css('cursor', 'pointer').click(unzoom); 978 this.svg.find(`#zoom_text_${this.id}`).css('cursor', 'pointer').click(unzoom); 979 980 let thisObj = this; 981 function zoom() { 982 thisObj.zoomStack.push(this); 983 displayFromElement(this); 984 thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'block'); 985 thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'block'); 986 } 987 988 function unzoom() { 989 if (thisObj.zoomStack.length > 1) { 990 thisObj.zoomStack.pop(); 991 displayFromElement(thisObj.zoomStack[thisObj.zoomStack.length - 1]); 992 if (thisObj.zoomStack.length == 1) { 993 thisObj.svg.find(`#zoom_rect_${thisObj.id}`).css('display', 'none'); 994 thisObj.svg.find(`#zoom_text_${thisObj.id}`).css('display', 'none'); 995 } 996 } 997 } 998 999 function displayFromElement(g) { 1000 g = $(g); 1001 let clickedRect = g.find('rect'); 1002 let clickedOriginX = parseFloat(clickedRect.attr('ox')); 1003 let clickedDepth = parseInt(clickedRect.attr('depth')); 1004 let clickedOriginWidth = parseFloat(clickedRect.attr('owidth')); 1005 let scaleFactor = 100.0 / clickedOriginWidth; 1006 thisObj.svg.find('g').each(function(_, g) { 1007 g = $(g); 1008 let text = g.find('text'); 1009 let rect = g.find('rect'); 1010 let depth = parseInt(rect.attr('depth')); 1011 let ox = parseFloat(rect.attr('ox')); 1012 let owidth = parseFloat(rect.attr('owidth')); 1013 if (depth < clickedDepth || ox < clickedOriginX - 1e-9 || 1014 ox + owidth > clickedOriginX + clickedOriginWidth + 1e-9) { 1015 rect.css('display', 'none'); 1016 text.css('display', 'none'); 1017 } else { 1018 rect.css('display', 'block'); 1019 text.css('display', 'block'); 1020 let nx = (ox - clickedOriginX) * scaleFactor + '%'; 1021 let ny = thisObj._getYForDepth(depth - clickedDepth); 1022 rect.attr('x', nx); 1023 rect.attr('y', ny); 1024 rect.attr('width', owidth * scaleFactor + '%'); 1025 text.attr('x', nx); 1026 text.attr('y', ny + 12); 1027 thisObj._adjustTextSizeForNode(g); 1028 } 1029 }); 1030 } 1031 } 1032 1033 _enableInfo() { 1034 this.selected = null; 1035 let thisObj = this; 1036 this.svg.find('g').on('mouseenter', function() { 1037 if (thisObj.selected) { 1038 thisObj.selected.css('stroke-width', '0'); 1039 } 1040 // Mark current node. 1041 let g = $(this); 1042 thisObj.selected = g; 1043 g.css('stroke', 'black').css('stroke-width', '0.5'); 1044 1045 // Parse title. 1046 let title = g.find('title').text(); 1047 let methodAndInfo = title.split(' | '); 1048 thisObj.svg.find(`#info_text_${thisObj.id}`).text(methodAndInfo[0]); 1049 1050 // Parse percentage. 1051 // '/system/lib64/libhwbinder.so (4 events: 0.28%)' 1052 let regexp = /.* \(.*:\s+(.*)\)/g; 1053 let match = regexp.exec(methodAndInfo[1]); 1054 let percentage = ''; 1055 if (match && match.length > 1) { 1056 percentage = match[1]; 1057 } 1058 thisObj.svg.find(`#percent_text_${thisObj.id}`).text(percentage); 1059 }); 1060 } 1061 1062 _adjustTextSizeOnResize() { 1063 function throttle(callback) { 1064 let running = false; 1065 return function() { 1066 if (!running) { 1067 running = true; 1068 window.requestAnimationFrame(function () { 1069 callback(); 1070 running = false; 1071 }); 1072 } 1073 }; 1074 } 1075 $(window).resize(throttle(() => this._adjustTextSize())); 1076 } 1077 } 1078 1079 1080 class SourceFile { 1081 1082 constructor(fileId) { 1083 this.path = getSourceFilePath(fileId); 1084 this.code = getSourceCode(fileId); 1085 this.showLines = {}; // map from line number to {eventCount, subtreeEventCount}. 1086 this.hasCount = false; 1087 } 1088 1089 addLineRange(startLine, endLine) { 1090 for (let i = startLine; i <= endLine; ++i) { 1091 if (i in this.showLines || !(i in this.code)) { 1092 continue; 1093 } 1094 this.showLines[i] = {eventCount: 0, subtreeEventCount: 0}; 1095 } 1096 } 1097 1098 addLineCount(lineNumber, eventCount, subtreeEventCount) { 1099 let line = this.showLines[lineNumber]; 1100 if (line) { 1101 line.eventCount += eventCount; 1102 line.subtreeEventCount += subtreeEventCount; 1103 this.hasCount = true; 1104 } 1105 } 1106 } 1107 1108 // Return a list of SourceFile related to a function. 1109 function collectSourceFilesForFunction(func) { 1110 if (!func.hasOwnProperty('s')) { 1111 return null; 1112 } 1113 let hitLines = func.s; 1114 let sourceFiles = {}; // map from sourceFileId to SourceFile. 1115 1116 function getFile(fileId) { 1117 let file = sourceFiles[fileId]; 1118 if (!file) { 1119 file = sourceFiles[fileId] = new SourceFile(fileId); 1120 } 1121 return file; 1122 } 1123 1124 // Show lines for the function. 1125 let funcRange = getFuncSourceRange(func.g.f); 1126 if (funcRange) { 1127 let file = getFile(funcRange.fileId); 1128 file.addLineRange(funcRange.startLine); 1129 } 1130 1131 // Show lines for hitLines. 1132 for (let hitLine of hitLines) { 1133 let file = getFile(hitLine.f); 1134 file.addLineRange(hitLine.l - 5, hitLine.l + 5); 1135 file.addLineCount(hitLine.l, hitLine.e, hitLine.s); 1136 } 1137 1138 let result = []; 1139 // Show the source file containing the function before other source files. 1140 if (funcRange) { 1141 let file = getFile(funcRange.fileId); 1142 if (file.hasCount) { 1143 result.push(file); 1144 } 1145 delete sourceFiles[funcRange.fileId]; 1146 } 1147 for (let fileId in sourceFiles) { 1148 let file = sourceFiles[fileId]; 1149 if (file.hasCount) { 1150 result.push(file); 1151 } 1152 } 1153 return result.length > 0 ? result : null; 1154 } 1155 1156 // Show annotated source code of a function. 1157 class SourceCodeView { 1158 1159 constructor(divContainer, sourceFiles) { 1160 this.div = $('<div>'); 1161 this.div.appendTo(divContainer); 1162 this.sourceFiles = sourceFiles; 1163 } 1164 1165 draw(sampleWeightFunction) { 1166 google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction)); 1167 } 1168 1169 realDraw(sampleWeightFunction) { 1170 this.div.empty(); 1171 // For each file, draw a table of 'Line', 'Total', 'Self', 'Code'. 1172 for (let sourceFile of this.sourceFiles) { 1173 let rows = []; 1174 let lineNumbers = Object.keys(sourceFile.showLines); 1175 lineNumbers.sort((a, b) => a - b); 1176 for (let lineNumber of lineNumbers) { 1177 let code = getHtml('pre', {text: sourceFile.code[lineNumber]}); 1178 let countInfo = sourceFile.showLines[lineNumber]; 1179 let totalValue = ''; 1180 let selfValue = ''; 1181 if (countInfo.subtreeEventCount != 0) { 1182 totalValue = sampleWeightFunction(countInfo.subtreeEventCount); 1183 selfValue = sampleWeightFunction(countInfo.eventCount); 1184 } 1185 rows.push([lineNumber, totalValue, selfValue, code]); 1186 } 1187 1188 let data = new google.visualization.DataTable(); 1189 data.addColumn('string', 'Line'); 1190 data.addColumn('string', 'Total'); 1191 data.addColumn('string', 'Self'); 1192 data.addColumn('string', 'Code'); 1193 data.addRows(rows); 1194 for (let i = 0; i < rows.length; ++i) { 1195 data.setProperty(i, 0, 'className', 'colForLine'); 1196 for (let j = 1; j <= 2; ++j) { 1197 data.setProperty(i, j, 'className', 'colForCount'); 1198 } 1199 } 1200 this.div.append(getHtml('pre', {text: sourceFile.path})); 1201 let wrapperDiv = $('<div>'); 1202 wrapperDiv.appendTo(this.div); 1203 let table = new google.visualization.Table(wrapperDiv.get(0)); 1204 table.draw(data, { 1205 width: '100%', 1206 sort: 'disable', 1207 frozenColumns: 3, 1208 allowHtml: true, 1209 }); 1210 } 1211 } 1212 } 1213 1214 // Return a list of disassembly related to a function. 1215 function collectDisassemblyForFunction(func) { 1216 if (!func.hasOwnProperty('a')) { 1217 return null; 1218 } 1219 let hitAddrs = func.a; 1220 let rawCode = getFuncDisassembly(func.g.f); 1221 if (!rawCode) { 1222 return null; 1223 } 1224 1225 // Annotate disassembly with event count information. 1226 let annotatedCode = []; 1227 let codeForLastAddr = null; 1228 let hitAddrPos = 0; 1229 let hasCount = false; 1230 1231 function addEventCount(addr) { 1232 while (hitAddrPos < hitAddrs.length && hitAddrs[hitAddrPos].a < addr) { 1233 if (codeForLastAddr) { 1234 codeForLastAddr.eventCount += hitAddrs[hitAddrPos].e; 1235 codeForLastAddr.subtreeEventCount += hitAddrs[hitAddrPos].s; 1236 hasCount = true; 1237 } 1238 hitAddrPos++; 1239 } 1240 } 1241 1242 for (let line of rawCode) { 1243 let code = line[0]; 1244 let addr = line[1]; 1245 1246 addEventCount(addr); 1247 let item = {code: code, eventCount: 0, subtreeEventCount: 0}; 1248 annotatedCode.push(item); 1249 // Objdump sets addr to 0 when a disassembly line is not associated with an addr. 1250 if (addr != 0) { 1251 codeForLastAddr = item; 1252 } 1253 } 1254 addEventCount(Number.MAX_VALUE); 1255 return hasCount ? annotatedCode : null; 1256 } 1257 1258 // Show annotated disassembly of a function. 1259 class DisassemblyView { 1260 1261 constructor(divContainer, disassembly) { 1262 this.div = $('<div>'); 1263 this.div.appendTo(divContainer); 1264 this.disassembly = disassembly; 1265 } 1266 1267 draw(sampleWeightFunction) { 1268 google.charts.setOnLoadCallback(() => this.realDraw(sampleWeightFunction)); 1269 } 1270 1271 realDraw(sampleWeightFunction) { 1272 this.div.empty(); 1273 // Draw a table of 'Total', 'Self', 'Code'. 1274 let rows = []; 1275 for (let line of this.disassembly) { 1276 let code = getHtml('pre', {text: line.code}); 1277 let totalValue = ''; 1278 let selfValue = ''; 1279 if (line.subtreeEventCount != 0) { 1280 totalValue = sampleWeightFunction(line.subtreeEventCount); 1281 selfValue = sampleWeightFunction(line.eventCount); 1282 } 1283 rows.push([totalValue, selfValue, code]); 1284 } 1285 let data = new google.visualization.DataTable(); 1286 data.addColumn('string', 'Total'); 1287 data.addColumn('string', 'Self'); 1288 data.addColumn('string', 'Code'); 1289 data.addRows(rows); 1290 for (let i = 0; i < rows.length; ++i) { 1291 for (let j = 0; j < 2; ++j) { 1292 data.setProperty(i, j, 'className', 'colForCount'); 1293 } 1294 } 1295 let wrapperDiv = $('<div>'); 1296 wrapperDiv.appendTo(this.div); 1297 let table = new google.visualization.Table(wrapperDiv.get(0)); 1298 table.draw(data, { 1299 width: '100%', 1300 sort: 'disable', 1301 frozenColumns: 2, 1302 allowHtml: true, 1303 }); 1304 } 1305 } 1306 1307 1308 function initGlobalObjects() { 1309 gTabs = new TabManager($('div#report_content')); 1310 let recordData = $('#record_data').text(); 1311 gRecordInfo = JSON.parse(recordData); 1312 gProcesses = gRecordInfo.processNames; 1313 gThreads = gRecordInfo.threadNames; 1314 gLibList = gRecordInfo.libList; 1315 gFunctionMap = gRecordInfo.functionMap; 1316 gSampleInfo = gRecordInfo.sampleInfo; 1317 gSourceFiles = gRecordInfo.sourceFiles; 1318 } 1319 1320 function createTabs() { 1321 gTabs.addTab('Chart Statistics', new ChartStatTab()); 1322 gTabs.addTab('Sample Table', new SampleTableTab()); 1323 gTabs.addTab('Flamegraph', new FlameGraphTab()); 1324 gTabs.draw(); 1325 } 1326 1327 let gTabs; 1328 let gRecordInfo; 1329 let gProcesses; 1330 let gThreads; 1331 let gLibList; 1332 let gFunctionMap; 1333 let gSampleInfo; 1334 let gSourceFiles; 1335 1336 initGlobalObjects(); 1337 createTabs(); 1338 1339 });