Andrea Falcone | 1c4977f | 2020-07-23 10:58:25 -0400 | [diff] [blame] | 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 | function 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. |
| 48 | function createZoomHistoryStack(svgElement) { |
| 49 | svgElement.zoomStack = [svgElement.getElementById(svgElement.attributes['rootid'].value)]; |
| 50 | } |
| 51 | |
| 52 | function 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 | |
| 84 | function 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 | |
| 92 | function 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 | |
| 104 | function 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 | |
| 159 | function 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 | |
| 183 | function 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 | |
| 206 | let selected; |
| 207 | document.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 | |
| 243 | function 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 | } |