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 18function flamegraphInit() { 19 let flamegraph = document.getElementById('flamegraph_id'); 20 let svgs = flamegraph.getElementsByTagName('svg'); 21 for (let i = 0; i < svgs.length; ++i) { 22 createZoomHistoryStack(svgs[i]); 23 adjust_text_size(svgs[i]); 24 } 25 26 function throttle(callback) { 27 let running = false; 28 return function() { 29 if (!running) { 30 running = true; 31 window.requestAnimationFrame(function () { 32 callback(); 33 running = false; 34 }); 35 } 36 }; 37 } 38 window.addEventListener('resize', throttle(function() { 39 let flamegraph = document.getElementById('flamegraph_id'); 40 let svgs = flamegraph.getElementsByTagName('svg'); 41 for (let i = 0; i < svgs.length; ++i) { 42 adjust_text_size(svgs[i]); 43 } 44 })); 45} 46 47// Create a stack add the root svg element in it. 48function createZoomHistoryStack(svgElement) { 49 svgElement.zoomStack = [svgElement.getElementById(svgElement.attributes['rootid'].value)]; 50} 51 52function adjust_node_text_size(x, svgWidth) { 53 let title = x.getElementsByTagName('title')[0]; 54 let text = x.getElementsByTagName('text')[0]; 55 let rect = x.getElementsByTagName('rect')[0]; 56 57 let width = parseFloat(rect.attributes['width'].value) * svgWidth * 0.01; 58 59 // Don't even bother trying to find a best fit. The area is too small. 60 if (width < 28) { 61 text.textContent = ''; 62 return; 63 } 64 // Remove dso and #samples which are here only for mouseover purposes. 65 let methodName = title.textContent.split(' | ')[0]; 66 67 let numCharacters; 68 for (numCharacters = methodName.length; numCharacters > 4; numCharacters--) { 69 // Avoid reflow by using hard-coded estimate instead of 70 // text.getSubStringLength(0, numCharacters). 71 if (numCharacters * 7.5 <= width) { 72 break; 73 } 74 } 75 76 if (numCharacters == methodName.length) { 77 text.textContent = methodName; 78 return; 79 } 80 81 text.textContent = methodName.substring(0, numCharacters-2) + '..'; 82} 83 84function adjust_text_size(svgElement) { 85 let svgWidth = window.innerWidth; 86 let x = svgElement.getElementsByTagName('g'); 87 for (let i = 0; i < x.length; i++) { 88 adjust_node_text_size(x[i], svgWidth); 89 } 90} 91 92function zoom(e) { 93 let svgElement = e.ownerSVGElement; 94 let zoomStack = svgElement.zoomStack; 95 zoomStack.push(e); 96 displaySVGElement(svgElement); 97 select(e); 98 99 // Show zoom out button. 100 svgElement.getElementById('zoom_rect').style.display = 'block'; 101 svgElement.getElementById('zoom_text').style.display = 'block'; 102} 103 104function displaySVGElement(svgElement) { 105 let zoomStack = svgElement.zoomStack; 106 let e = zoomStack[zoomStack.length - 1]; 107 let clicked_rect = e.getElementsByTagName('rect')[0]; 108 let clicked_origin_x; 109 let clicked_origin_y = clicked_rect.attributes['oy'].value; 110 let clicked_origin_width; 111 112 if (zoomStack.length == 1) { 113 // Show all nodes when zoomStack only contains the root node. 114 // This is needed to show flamegraph containing more than one node at the root level. 115 clicked_origin_x = 0; 116 clicked_origin_width = 100; 117 } else { 118 clicked_origin_x = clicked_rect.attributes['ox'].value; 119 clicked_origin_width = clicked_rect.attributes['owidth'].value; 120 } 121 122 123 let svgBox = svgElement.getBoundingClientRect(); 124 let svgBoxHeight = svgBox.height; 125 let svgBoxWidth = 100; 126 let scaleFactor = svgBoxWidth / clicked_origin_width; 127 128 let callsites = svgElement.getElementsByTagName('g'); 129 for (let i = 0; i < callsites.length; i++) { 130 let text = callsites[i].getElementsByTagName('text')[0]; 131 let rect = callsites[i].getElementsByTagName('rect')[0]; 132 133 let rect_o_x = parseFloat(rect.attributes['ox'].value); 134 let rect_o_y = parseFloat(rect.attributes['oy'].value); 135 136 // Avoid multiple forced reflow by hiding nodes. 137 if (rect_o_y > clicked_origin_y) { 138 rect.style.display = 'none'; 139 text.style.display = 'none'; 140 continue; 141 } 142 rect.style.display = 'block'; 143 text.style.display = 'block'; 144 145 let newrec_x = rect.attributes['x'].value = (rect_o_x - clicked_origin_x) * scaleFactor + 146 '%'; 147 let newrec_y = rect.attributes['y'].value = rect_o_y + (svgBoxHeight - clicked_origin_y 148 - 17 - 2); 149 150 text.attributes['y'].value = newrec_y + 12; 151 text.attributes['x'].value = newrec_x; 152 153 rect.attributes['width'].value = (rect.attributes['owidth'].value * scaleFactor) + '%'; 154 } 155 156 adjust_text_size(svgElement); 157} 158 159function unzoom(e) { 160 let svgOwner = e.ownerSVGElement; 161 let stack = svgOwner.zoomStack; 162 163 // Unhighlight whatever was selected. 164 if (selected) { 165 selected.classList.remove('s'); 166 } 167 168 // Stack management: Never remove the last element which is the flamegraph root. 169 if (stack.length > 1) { 170 let previouslySelected = stack.pop(); 171 select(previouslySelected); 172 } 173 174 // Hide zoom out button. 175 if (stack.length == 1) { 176 svgOwner.getElementById('zoom_rect').style.display = 'none'; 177 svgOwner.getElementById('zoom_text').style.display = 'none'; 178 } 179 180 displaySVGElement(svgOwner); 181} 182 183function search(e) { 184 let term = prompt('Search for:', ''); 185 let callsites = e.ownerSVGElement.getElementsByTagName('g'); 186 187 if (!term) { 188 for (let i = 0; i < callsites.length; i++) { 189 let rect = callsites[i].getElementsByTagName('rect')[0]; 190 rect.attributes['fill'].value = rect.attributes['ofill'].value; 191 } 192 return; 193 } 194 195 for (let i = 0; i < callsites.length; i++) { 196 let title = callsites[i].getElementsByTagName('title')[0]; 197 let rect = callsites[i].getElementsByTagName('rect')[0]; 198 if (title.textContent.indexOf(term) != -1) { 199 rect.attributes['fill'].value = 'rgb(230,100,230)'; 200 } else { 201 rect.attributes['fill'].value = rect.attributes['ofill'].value; 202 } 203 } 204} 205 206let selected; 207document.addEventListener('keydown', (e) => { 208 if (!selected) { 209 return false; 210 } 211 212 let nav = selected.attributes['nav'].value.split(','); 213 let navigation_index; 214 switch (e.keyCode) { 215 // case 38: // ARROW UP 216 case 87: navigation_index = 0; break; // W 217 218 // case 32 : // ARROW LEFT 219 case 65: navigation_index = 1; break; // A 220 221 // case 43: // ARROW DOWN 222 case 68: navigation_index = 3; break; // S 223 224 // case 39: // ARROW RIGHT 225 case 83: navigation_index = 2; break; // D 226 227 case 32: zoom(selected); return false; // SPACE 228 229 case 8: // BACKSPACE 230 unzoom(selected); return false; 231 default: return true; 232 } 233 234 if (nav[navigation_index] == '0') { 235 return false; 236 } 237 238 let target_element = selected.ownerSVGElement.getElementById(nav[navigation_index]); 239 select(target_element); 240 return false; 241}); 242 243function select(e) { 244 if (selected) { 245 selected.classList.remove('s'); 246 } 247 selected = e; 248 selected.classList.add('s'); 249 250 // Update info bar 251 let titleElement = selected.getElementsByTagName('title')[0]; 252 let text = titleElement.textContent; 253 254 // Parse title 255 let method_and_info = text.split(' | '); 256 let methodName = method_and_info[0]; 257 let info = method_and_info[1]; 258 259 // Parse info 260 // '/system/lib64/libhwbinder.so (4 events: 0.28%)' 261 let regexp = /(.*) \((.*)\)/g; 262 let match = regexp.exec(info); 263 if (match.length > 2) { 264 let percentage = match[2]; 265 // Write percentage 266 let percentageTextElement = selected.ownerSVGElement.getElementById('percent_text'); 267 percentageTextElement.textContent = percentage; 268 // console.log("'" + percentage + "'") 269 } 270 271 // Set fields 272 let barTextElement = selected.ownerSVGElement.getElementById('info_text'); 273 barTextElement.textContent = methodName; 274}