diff options
Diffstat (limited to 'simpleperf/scripts/purgatorio/templates')
-rw-r--r-- | simpleperf/scripts/purgatorio/templates/index.html.jinja2 | 66 | ||||
-rw-r--r-- | simpleperf/scripts/purgatorio/templates/main.js | 245 | ||||
-rw-r--r-- | simpleperf/scripts/purgatorio/templates/styles.css | 133 |
3 files changed, 444 insertions, 0 deletions
diff --git a/simpleperf/scripts/purgatorio/templates/index.html.jinja2 b/simpleperf/scripts/purgatorio/templates/index.html.jinja2 new file mode 100644 index 00000000..f3e6046c --- /dev/null +++ b/simpleperf/scripts/purgatorio/templates/index.html.jinja2 @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>{{ title }}</title> + <script + src="https://code.jquery.com/jquery-3.5.1.min.js" + integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" + crossorigin="anonymous"></script> + + <meta charset="utf-8"> + + {{ resources }} + + <style> + {% include 'styles.css' %} + </style> + + <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.0.6/dist/d3-flamegraph.css"> + +<script + src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" + integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" + crossorigin="anonymous"></script> + <script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script> + <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.0.6/dist/d3-flamegraph.min.js"></script> + + </head> + <body> + <div id="help_dialog" class="dialog"> + <div class="dialog_area"> + <span class="dialog_close">×</span> + <p> <b>Main plot (upper left):</b> pan with click+mouse movement, zoom in/out with the mouse + wheel, hover on sample clusters to see backtraces. Select samples with the rectangular + selection tool or by clicking on them. Select holding shift to add or ctrl+shift to + remove samples to or from the selection. Different tools can be enabled/disabled from + the toolbox.</p> + <p><b>Flame graph (upper right):</b> click on specific items to zoom in.</p> + <p><b>Sample table (lower right):</b> select processes to filter in the Flame graph.</p> + </div> + </div> + + <div class="top_right"> + <button id="help_button" class="help" text-align="right">HELP</button> + </div> + <div class="left"> {{ plot_div.graph }} </div> + <div class="middle_right"> + <div id="flame"/> + </div> + + <div class="bottom_right"> + <div style="display: flex; justify-content: space-around"> + <div> + <label for="regex">Filter by regex:</label> + <input type="text" id="regex" oninput="update_selections()"/> + </div> + <div> + Invert callstack <input type="checkbox" id="inverted_checkbox" onclick="update_selections()"> + </div> + </div> + {{ plot_div.table }} + </div> + + <script>{% include 'main.js' %}</script> + {{ plot_script }} + </body> +</html> diff --git a/simpleperf/scripts/purgatorio/templates/main.js b/simpleperf/scripts/purgatorio/templates/main.js new file mode 100644 index 00000000..ec8e7cbe --- /dev/null +++ b/simpleperf/scripts/purgatorio/templates/main.js @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2021 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. + */ + +function generateHash (name) { + // Return a vector (0.0->1.0) that is a hash of the input string. + // The hash is computed to favor early characters over later ones, so + // that strings with similar starts have similar vectors. Only the first + // 6 characters are considered. + const MAX_CHAR = 6 + + var hash = 0 + var maxHash = 0 + var weight = 1 + var mod = 10 + + if (name) { + for (var i = 0; i < name.length; i++) { + if (i > MAX_CHAR) { break } + hash += weight * (name.charCodeAt(i) % mod) + maxHash += weight * (mod - 1) + weight *= 0.70 + } + if (maxHash > 0) { hash = hash / maxHash } + } + return hash +} + +function offCpuColorMapper (d) { + if (d.highlight) return '#E600E6' + + let name = d.data.n || d.data.name + let vector = 0 + const nameArr = name.split('`') + + if (nameArr.length > 1) { + name = nameArr[nameArr.length - 1] // drop module name if present + } + name = name.split('(')[0] // drop extra info + vector = generateHash(name) + + const r = 0 + Math.round(55 * (1 - vector)) + const g = 0 + Math.round(230 * (1 - vector)) + const b = 200 + Math.round(55 * vector) + + return 'rgb(' + r + ',' + g + ',' + b + ')' +} + +var flame = flamegraph() + .cellHeight(18) + .width(window.innerWidth * 3 / 10 - 20) // 30% width + .transitionDuration(750) + .minFrameSize(5) + .transitionEase(d3.easeCubic) + .inverted(false) + .sort(true) + .title("") + //.differential(false) + //.elided(false) + .selfValue(false) + .setColorMapper(offCpuColorMapper); + + +function update_table() { + let inverted = document.getElementById("inverted_checkbox").checked + let regex + let graph_source = Bokeh.documents[0].get_model_by_name('graph').renderers[0].data_source + let table_source = Bokeh.documents[0].get_model_by_name('table').source + + let graph_selection = graph_source.selected.indices + let threads = graph_source.data.thread + let callchains = graph_source.data.callchain + + let selection_len = graph_selection.length; + + if (document.getElementById("regex").value) { + regex = new RegExp(document.getElementById("regex").value) + } + + table_source.data.thread = [] + table_source.data.count = [] + table_source.data.index = [] + + for (let i = 0; i < selection_len; i ++) { + let entry = "<no callchain>" + + if (regex !== undefined && !regex.test(callchains[graph_selection[i]])) { + continue; + } + + if (inverted) { + let callchain = callchains[graph_selection[i]].split("<br>") + + for (let e = 0; e < callchain.length; e ++) { + if (callchain[e] != "") { // last entry is apparently always an empty string + entry = callchain[e] + break + } + } + } else { + entry = threads[graph_selection[i]] + } + + let pos = table_source.data.thread.indexOf(entry) + + if(pos == -1) { + table_source.data.thread.push(entry) + table_source.data.count.push(1) + table_source.data.index.push(table_source.data.thread.length) + } else { + table_source.data.count[pos] ++ + } + } + + table_source.selected.indices = [] + table_source.change.emit() +} + + +function should_insert_callchain(callchain, items, filter_index, inverted) { + for (t = 0; t < filter_index.length; t ++) { + if (callchain[0] === items[filter_index[t]]) { + return true + } + } + + if (filter_index.length > 0) { + return false + } + + return true +} + + +function insert_callchain(root, callchain, inverted) { + let root_pos = -1 + let node = root + + node.value ++ + + for (let e = 0; e < callchain.length; e ++) { + let entry = callchain[e].replace(/^\s+|\s+$/g, '') + let entry_pos = -1 + + for (let j = 0; j < node.children.length; j ++) { + if (node.children[j].name == entry) { + entry_pos = j + break + } + } + + if (entry_pos == -1) { + node.children.push({name: entry, value:0, children:[]}) + entry_pos = node.children.length - 1 + } + + node = node.children[entry_pos] + node.value ++ + } +} + + +function update_flamegraph() { + let inverted = document.getElementById("inverted_checkbox").checked + let root = {name: inverted ? "samples" : "processes", value: 0, children: []} + + let graph_source = Bokeh.documents[0].get_model_by_name('graph').renderers[0].data_source + let graph_selection = graph_source.selected.indices + let callchains = graph_source.data.callchain + let graph_threads = graph_source.data.thread + + let table_source = Bokeh.documents[0].get_model_by_name('table').source + let table_selection = table_source.selected.indices + let table_threads = table_source.data.thread + let regex + + if (document.getElementById("regex").value) { + regex = new RegExp(document.getElementById("regex").value) + } + + for (let i = 0; i < graph_selection.length; i ++) { + let thread = graph_threads[graph_selection[i]] + let callchain = callchains[graph_selection[i]].split("<br>") + callchain = callchain.filter(function(e){return e != ""}) + + if (regex !== undefined && !regex.test(callchains[graph_selection[i]])) { + continue; + } + + if (callchain.length == 0) { + callchain.push("<no callchain>") + } + + callchain.push(thread) + + if (!inverted){ + callchain = callchain.reverse() + } + + if (should_insert_callchain(callchain, table_threads, table_selection)) { + insert_callchain(root, callchain) + } + } + + if (root.children.length == 1) { + root = root.children[0] + } + + d3.select("#flame") + .datum(root) + .call(flame) +} + +var help_dialog = document.getElementById("help_dialog"); + +document.getElementById("help_button").onclick = function() { + help_dialog.style.display = "block"; +} + +window.onclick = function(event) { + if (event.target == help_dialog) { + help_dialog.style.display = "none"; + } +} + +document.getElementsByClassName("dialog_close")[0].onclick = function() { + help_dialog.style.display = "none"; +} + +function update_selections() { + update_flamegraph() + update_table() +} diff --git a/simpleperf/scripts/purgatorio/templates/styles.css b/simpleperf/scripts/purgatorio/templates/styles.css new file mode 100644 index 00000000..4378b0b4 --- /dev/null +++ b/simpleperf/scripts/purgatorio/templates/styles.css @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2021 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. + */ + +body { + font-family: sans-serif; +} + +::-webkit-scrollbar { + width: 1em; +} + +::-webkit-scrollbar-track { + box-shadow: inset 0 0 0.1em white; + border-radius: 0.5em; +} + +::-webkit-scrollbar-thumb { + background: lightgrey; + border-radius: 0.5em; +} + +div.left { + position:fixed; + top: 0; + left: 0; + width: 70%; + height: 100%; +} + +div.top_right { + position: fixed; + top: 0; + right: 0; + text-align: right; + width: 30%; + height: 1em; +} + +div.middle_right { + position:fixed; + top: 2%; + right: 0; + width: 30%; + height: 78%; + overflow-y: scroll; +} + +div.bottom_right { + position: fixed; + width: 30%; + height: 20%; + bottom: 0; + right: 0; +} + +button { + border: none; + outline: none; + padding: 0; + background: white; + font-size: 0.5em; +} + +button.help:before +{ + content: '?'; + display: inline-block; + font-weight: bold; + text-align: center; + font-size: 1.4em; + width: 1.5em; + height: 1.5em; + line-height: 1.6em; + border-radius: 1.2em; + margin-right: 0.3em; + color: GoldenRod; + background: white; + border: 0.1em solid GoldenRod; +} + +button.help:hover:before +{ + color: white; + background: GoldenRod; +} + +.dialog { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.4); +} + +.dialog_area { + background-color: white; + margin: 20% auto; + border: 0.05em solid gray; + border-radius: 0.5em; + padding-left: 0.5em; + padding-right: 0.5em; + width: 50%; +} + +.dialog_close { + color: darkgray; + float: right; + font-size: 2em; + font-weight: bold; +} + +.dialog_close:focus, +.dialog_close:hover { + cursor: pointer; + color: black; +} |