| /* | |
| * Copyright (C) 2017 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| 'use strict'; | |
| function flamegraphInit() { | |
| let flamegraph = document.getElementById('flamegraph_id'); | |
| let svgs = flamegraph.getElementsByTagName('svg'); | |
| for (let i = 0; i < svgs.length; ++i) { | |
| createZoomHistoryStack(svgs[i]); | |
| adjust_text_size(svgs[i]); | |
| } | |
| function throttle(callback) { | |
| let running = false; | |
| return function() { | |
| if (!running) { | |
| running = true; | |
| window.requestAnimationFrame(function () { | |
| callback(); | |
| running = false; | |
| }); | |
| } | |
| }; | |
| } | |
| window.addEventListener('resize', throttle(function() { | |
| let flamegraph = document.getElementById('flamegraph_id'); | |
| let svgs = flamegraph.getElementsByTagName('svg'); | |
| for (let i = 0; i < svgs.length; ++i) { | |
| adjust_text_size(svgs[i]); | |
| } | |
| })); | |
| } | |
| // Create a stack add the root svg element in it. | |
| function createZoomHistoryStack(svgElement) { | |
| svgElement.zoomStack = [svgElement.getElementById(svgElement.attributes['rootid'].value)]; | |
| } | |
| function adjust_node_text_size(x, svgWidth) { | |
| let title = x.getElementsByTagName('title')[0]; | |
| let text = x.getElementsByTagName('text')[0]; | |
| let rect = x.getElementsByTagName('rect')[0]; | |
| let width = parseFloat(rect.attributes['width'].value) * svgWidth * 0.01; | |
| // Don't even bother trying to find a best fit. The area is too small. | |
| if (width < 28) { | |
| text.textContent = ''; | |
| return; | |
| } | |
| // Remove dso and #samples which are here only for mouseover purposes. | |
| let methodName = title.textContent.split(' | ')[0]; | |
| let numCharacters; | |
| for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) { | |
| // Avoid reflow by using hard-coded estimate instead of | |
| // text.getSubStringLength(0, numCharacters). | |
| if (numCharacters * 7.5 <= width) { | |
| break; | |
| } | |
| } | |
| if (numCharacters == methodName.length) { | |
| text.textContent = methodName; | |
| return; | |
| } | |
| text.textContent = methodName.substring(0, numCharacters-2) + '..'; | |
| } | |
| function adjust_text_size(svgElement) { | |
| let svgWidth = window.innerWidth; | |
| let x = svgElement.getElementsByTagName('g'); | |
| for (let i = 0; i < x.length; i++) { | |
| adjust_node_text_size(x[i], svgWidth); | |
| } | |
| } | |
| function zoom(e) { | |
| let svgElement = e.ownerSVGElement; | |
| let zoomStack = svgElement.zoomStack; | |
| zoomStack.push(e); | |
| displaySVGElement(svgElement); | |
| select(e); | |
| // Show zoom out button. | |
| svgElement.getElementById('zoom_rect').style.display = 'block'; | |
| svgElement.getElementById('zoom_text').style.display = 'block'; | |
| } | |
| function displaySVGElement(svgElement) { | |
| let zoomStack = svgElement.zoomStack; | |
| let e = zoomStack[zoomStack.length - 1]; | |
| let clicked_rect = e.getElementsByTagName('rect')[0]; | |
| let clicked_origin_x; | |
| let clicked_origin_y = clicked_rect.attributes['oy'].value; | |
| let clicked_origin_width; | |
| if (zoomStack.length == 1) { | |
| // Show all nodes when zoomStack only contains the root node. | |
| // This is needed to show flamegraph containing more than one node at the root level. | |
| clicked_origin_x = 0; | |
| clicked_origin_width = 100; | |
| } else { | |
| clicked_origin_x = clicked_rect.attributes['ox'].value; | |
| clicked_origin_width = clicked_rect.attributes['owidth'].value; | |
| } | |
| let svgBox = svgElement.getBoundingClientRect(); | |
| let svgBoxHeight = svgBox.height; | |
| let svgBoxWidth = 100; | |
| let scaleFactor = svgBoxWidth / clicked_origin_width; | |
| let callsites = svgElement.getElementsByTagName('g'); | |
| for (let i = 0; i < callsites.length; i++) { | |
| let text = callsites[i].getElementsByTagName('text')[0]; | |
| let rect = callsites[i].getElementsByTagName('rect')[0]; | |
| let rect_o_x = parseFloat(rect.attributes['ox'].value); | |
| let rect_o_y = parseFloat(rect.attributes['oy'].value); | |
| // Avoid multiple forced reflow by hiding nodes. | |
| if (rect_o_y > clicked_origin_y) { | |
| rect.style.display = 'none'; | |
| text.style.display = 'none'; | |
| continue; | |
| } | |
| rect.style.display = 'block'; | |
| text.style.display = 'block'; | |
| let newrec_x = rect.attributes['x'].value = (rect_o_x - clicked_origin_x) * scaleFactor + | |
| '%'; | |
| let newrec_y = rect.attributes['y'].value = rect_o_y + (svgBoxHeight - clicked_origin_y | |
| - 17 - 2); | |
| text.attributes['y'].value = newrec_y + 12; | |
| text.attributes['x'].value = newrec_x; | |
| rect.attributes['width'].value = (rect.attributes['owidth'].value * scaleFactor) + '%'; | |
| } | |
| adjust_text_size(svgElement); | |
| } | |
| function unzoom(e) { | |
| let svgOwner = e.ownerSVGElement; | |
| let stack = svgOwner.zoomStack; | |
| // Unhighlight whatever was selected. | |
| if (selected) { | |
| selected.classList.remove('s'); | |
| } | |
| // Stack management: Never remove the last element which is the flamegraph root. | |
| if (stack.length > 1) { | |
| let previouslySelected = stack.pop(); | |
| select(previouslySelected); | |
| } | |
| // Hide zoom out button. | |
| if (stack.length == 1) { | |
| svgOwner.getElementById('zoom_rect').style.display = 'none'; | |
| svgOwner.getElementById('zoom_text').style.display = 'none'; | |
| } | |
| displaySVGElement(svgOwner); | |
| } | |
| function search(e) { | |
| let term = prompt('Search for:', ''); | |
| let callsites = e.ownerSVGElement.getElementsByTagName('g'); | |
| if (!term) { | |
| for (let i = 0; i < callsites.length; i++) { | |
| let rect = callsites[i].getElementsByTagName('rect')[0]; | |
| rect.attributes['fill'].value = rect.attributes['ofill'].value; | |
| } | |
| return; | |
| } | |
| for (let i = 0; i < callsites.length; i++) { | |
| let title = callsites[i].getElementsByTagName('title')[0]; | |
| let rect = callsites[i].getElementsByTagName('rect')[0]; | |
| if (title.textContent.indexOf(term) != -1) { | |
| rect.attributes['fill'].value = 'rgb(230,100,230)'; | |
| } else { | |
| rect.attributes['fill'].value = rect.attributes['ofill'].value; | |
| } | |
| } | |
| } | |
| let selected; | |
| document.addEventListener('keydown', (e) => { | |
| if (!selected) { | |
| return false; | |
| } | |
| let nav = selected.attributes['nav'].value.split(','); | |
| let navigation_index; | |
| switch (e.keyCode) { | |
| // case 38: // ARROW UP | |
| case 87: navigation_index = 0; break; // W | |
| // case 32 : // ARROW LEFT | |
| case 65: navigation_index = 1; break; // A | |
| // case 43: // ARROW DOWN | |
| case 68: navigation_index = 3; break; // S | |
| // case 39: // ARROW RIGHT | |
| case 83: navigation_index = 2; break; // D | |
| case 32: zoom(selected); return false; // SPACE | |
| case 8: // BACKSPACE | |
| unzoom(selected); return false; | |
| default: return true; | |
| } | |
| if (nav[navigation_index] == '0') { | |
| return false; | |
| } | |
| let target_element = selected.ownerSVGElement.getElementById(nav[navigation_index]); | |
| select(target_element); | |
| return false; | |
| }); | |
| function select(e) { | |
| if (selected) { | |
| selected.classList.remove('s'); | |
| } | |
| selected = e; | |
| selected.classList.add('s'); | |
| // Update info bar | |
| let titleElement = selected.getElementsByTagName('title')[0]; | |
| let text = titleElement.textContent; | |
| // Parse title | |
| let method_and_info = text.split(' | '); | |
| let methodName = method_and_info[0]; | |
| let info = method_and_info[1]; | |
| // Parse info | |
| // '/system/lib64/libhwbinder.so (4 events: 0.28%)' | |
| let regexp = /(.*) \((.*)\)/g; | |
| let match = regexp.exec(info); | |
| if (match.length > 2) { | |
| let percentage = match[2]; | |
| // Write percentage | |
| let percentageTextElement = selected.ownerSVGElement.getElementById('percent_text'); | |
| percentageTextElement.textContent = percentage; | |
| // console.log("'" + percentage + "'") | |
| } | |
| // Set fields | |
| let barTextElement = selected.ownerSVGElement.getElementById('info_text'); | |
| barTextElement.textContent = methodName; | |
| } |