| #!/usr/bin/env ruby | |
| # -*- coding: utf-8 -*- | |
| # Copyright (C) 2023 Apple Inc. All rights reserved. | |
| # | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions | |
| # are met: | |
| # 1. Redistributions of source code must retain the above copyright | |
| # notice, this list of conditions and the following disclaimer. | |
| # 2. Redistributions in binary form must reproduce the above copyright | |
| # notice, this list of conditions and the following disclaimer in the | |
| # documentation and/or other materials provided with the distribution. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' | |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, | |
| # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS | |
| # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | |
| # THE POSSIBILITY OF SUCH DAMAGE. | |
| require 'json' | |
| require 'getoptlong' | |
| $rootDirectory = File.expand_path(File.join(File.dirname(__FILE__), "..", "..")) | |
| $flameGraphTitle = "" | |
| $flameGraphJSBuiltin = false | |
| def render samples, title | |
| template = <<-RESULT | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>JSC Sampling Profiler Output</title> | |
| <!-- d3-flamegraph.css --> | |
| <style> | |
| #{ IO::read(File.join($rootDirectory, "Source", "ThirdParty", "d3flamegraphjs", "d3-flamegraph.css")) } | |
| </style> | |
| <style> | |
| body { | |
| padding-top: 20px; | |
| padding-bottom: 20px; | |
| font-family: Menlo, monospace; | |
| font-size: 14px; | |
| color: #212529; | |
| } | |
| .header { | |
| padding-bottom: 20px; | |
| padding-right: 15px; | |
| padding-left: 15px; | |
| border-bottom: 1px solid #e5e5e5; | |
| } | |
| .header h3 { | |
| margin-top: 0; | |
| margin-bottom: 0; | |
| line-height: 40px; | |
| color: #6c757d; | |
| } | |
| .container { | |
| max-width: 1230px; | |
| padding-left: 15px; | |
| padding-right: 15px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| .d3-flame-graph rect { | |
| stroke-width: 1.0px; | |
| } | |
| .float-right { | |
| float: right; | |
| } | |
| #details { | |
| padding: 15px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <div class="float-right"> | |
| <form id="form"> | |
| <button onclick="javascript:resetZoom();">Reset zoom</button> | |
| <button onclick="javascript:clear();">Clear</button> | |
| <input type="text" id="term"> | |
| <button onclick="javascript:search();">Search</button> | |
| </form> | |
| </div> | |
| <h3>JSC Sampling Profiler Output</h3> | |
| </div> | |
| <div id="chart"></div> | |
| <div id="details"></div> | |
| </div> | |
| <!-- D3.js --> | |
| <script> | |
| #{ IO::read(File.join($rootDirectory, "Source", "ThirdParty", "d3js", "d3.v7.min.js")) } | |
| </script> | |
| <!-- d3-flamegraph --> | |
| <script> | |
| #{ IO::read(File.join($rootDirectory, "Source", "ThirdParty", "d3flamegraphjs", "d3-flamegraph.min.js")) } | |
| </script> | |
| <!-- d3-flamegraph-tooltip --> | |
| <script> | |
| #{ IO::read(File.join($rootDirectory, "Source", "ThirdParty", "d3flamegraphjs", "d3-flamegraph-tooltip.min.js")) } | |
| </script> | |
| <script type="application/json" id="data"> | |
| #{ JSON.generate(samples, :max_nesting => 10000) } | |
| </script> | |
| <script> | |
| let tip = flamegraph.tooltip.defaultFlamegraphTooltip().text(d => `name: ${d.data.name}, value: ${d.data.value}`); | |
| let flameGraph = flamegraph() | |
| .width(1200) | |
| .cellHeight(18) | |
| .transitionDuration(200) | |
| .transitionEase(d3.easeCubic) | |
| .sort(true) | |
| .tooltip(tip) | |
| .title(#{ JSON.generate(title) }) | |
| .selfValue(false) | |
| .setDetailsElement(document.getElementById("details")); | |
| let data = JSON.parse(document.getElementById("data").textContent); | |
| d3.select("#chart").datum(data).call(flameGraph); | |
| document.getElementById("form").addEventListener("submit", event => { | |
| event.preventDefault(); | |
| search(); | |
| }, false); | |
| function search() { | |
| let term = document.getElementById("term").value; | |
| flameGraph.search(term); | |
| } | |
| function clear() { | |
| document.getElementById('term').value = ''; | |
| flameGraph.clear(); | |
| } | |
| function resetZoom() { | |
| flameGraph.resetZoom(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| RESULT | |
| end | |
| def newChild name | |
| { | |
| "name" => name, | |
| "value" => 0, | |
| "children" => [], | |
| } | |
| end | |
| def update cursor, name | |
| cursor["value"] += 1 | |
| cursor["children"].each {|child| | |
| if child["name"] == name | |
| return child | |
| end | |
| } | |
| child = newChild(name) | |
| cursor["children"].push child | |
| child | |
| end | |
| def reconstruct database | |
| result = newChild("root") | |
| database["traces"].each do |stackTrace| | |
| cursor = result | |
| next if stackTrace["frames"].empty? | |
| stackTrace["frames"].reverse.map do |frame| | |
| jsbuiltin = "" | |
| if $flameGraphJSBuiltin | |
| flags = frame["flags"] | |
| if !flags.nil? && flags & 0b1 != 0 | |
| jsbuiltin = "/JSBUILTIN" | |
| end | |
| end | |
| hash, category, offset = frame["location"].split(":") | |
| cursor = update(cursor, "#{frame["name"]}#{hash}#{jsbuiltin}") | |
| end | |
| cursor["value"] += 1 | |
| end | |
| result | |
| end | |
| def usage | |
| puts <<-HELP | |
| Usage: | |
| --title s: | |
| Specify title in generated graph. | |
| -h, --help: | |
| Show this help. | |
| HELP | |
| exit(0) | |
| end | |
| GetoptLong.new( | |
| ['--help', '-h', GetoptLong::NO_ARGUMENT], | |
| ['--title', '-t', GetoptLong::REQUIRED_ARGUMENT], | |
| ['--js-builtin', '-b', GetoptLong::NO_ARGUMENT], | |
| ).each do |opt, arg| | |
| case opt | |
| when '--help' | |
| usage | |
| when '--title' | |
| $flameGraphTitle = arg | |
| when '--js-builtin' | |
| $flameGraphJSBuiltin = true | |
| end | |
| end | |
| def main | |
| json = nil | |
| if ARGV.empty? | |
| json = JSON.parse(STDIN.read) | |
| else | |
| json = JSON::parse(IO::read(ARGV[0])) | |
| end | |
| puts render(reconstruct(json), $flameGraphTitle) | |
| end | |
| main | |
| # vim: set sw=4 ts=4 et tw=80 : |