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}