| #!/usr/bin/env ruby |
| # 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 'getoptlong' |
| require 'json' |
| |
| $samplingProfilerIgnoreExternalSourceID = false |
| $samplingProfilerTopFunctionsCount = 12 |
| $samplingProfilerTopBytecodesCount = 40 |
| |
| def usage |
| puts <<-HELP |
| Usage: |
| |
| -s, --ignore-external-source-id: |
| Ignore external source id when aggregating. |
| -c n, --top-function-count n: |
| Show n items of top functions. |
| -b n, --top-bytecode-count n: |
| Show n items of top bytecodes. |
| -h, --help: |
| Show this help. |
| HELP |
| exit(0) |
| end |
| |
| GetoptLong.new( |
| ['--help', '-h', GetoptLong::NO_ARGUMENT], |
| ['--ignore-external-source-id', '-s', GetoptLong::NO_ARGUMENT], |
| ['--top-function-count', '-c', GetoptLong::REQUIRED_ARGUMENT], |
| ['--top-bytecode-count', '-b', GetoptLong::REQUIRED_ARGUMENT], |
| ).each { |
| | opt, arg | |
| case opt |
| when '--help' |
| usage |
| when '--ignore-external-source-id' |
| $samplingProfilerIgnoreExternalSourceID = true |
| when '--top-function-count' |
| $samplingProfilerTopFunctionsCount = arg.to_i |
| when '--top-bytecode-count' |
| $samplingProfilerTopBytecodesCount = arg.to_i |
| end |
| } |
| |
| def reportTopFunctions database |
| totalSamples = 0 |
| functionCounts = Hash.new(0) |
| database["traces"].each do |stackTrace| |
| next if stackTrace["frames"].empty? |
| frame = stackTrace["frames"][0] |
| hash, category, offset = frame["location"].split(":") |
| description = "#{frame["name"]}#{hash}" |
| unless $samplingProfilerIgnoreExternalSourceID |
| description = "#{description}:#{frame["sourceID"]}" |
| end |
| functionCounts[description] += 1 |
| totalSamples += 1 |
| end |
| |
| def takeMax functionCounts |
| maxFrameDescription = nil |
| maxFrameCount = 0 |
| functionCounts.each do |key, value| |
| if value > maxFrameCount |
| maxFrameCount = value |
| maxFrameDescription = key |
| end |
| end |
| |
| if maxFrameDescription |
| functionCounts.delete(maxFrameDescription) |
| end |
| return maxFrameDescription, maxFrameCount |
| end |
| |
| timingInterval = database["interval"] * 1000.0 * 1000.0 |
| puts "Sampling rate: #{timingInterval} microseconds. Total samples: #{totalSamples}" |
| puts "Top functions as <numSamples 'functionName#hash:sourceID'>" |
| $samplingProfilerTopFunctionsCount.times do |
| key, value = takeMax(functionCounts) |
| break unless key |
| printf("%6u '%s'\n", value, key) |
| end |
| end |
| |
| $tiers = { |
| :llint => "LLInt", |
| :baseline => "Baseline", |
| :dfg => "DFG", |
| :ftl => "FTL", |
| :ipint => "IPInt", |
| :wasmllint => "WasmLLInt", |
| :bbq => "BBQ", |
| :omg => "OMG", |
| :wasm => "Wasm", |
| :host => "Host", |
| :regexp => "RegExp", |
| :cpp => "C/C++", |
| :unknownFrame => "Unknown Frame", |
| :unknownExecutable => "Unknown Executable", |
| } |
| |
| def reportTopBytecodes database |
| totalSamples = 0 |
| bytecodeCounts = Hash.new(0) |
| tierCounts = Hash.new(0) |
| |
| database["traces"].each do |stackTrace| |
| next if stackTrace["frames"].empty? |
| frame = stackTrace["frames"][0] |
| description = "#{frame["name"]}#{frame["location"]}" |
| inliner = frame["inliner"] |
| unless inliner.nil? |
| description = "#{description} <-- #{inliner["name"]}#{inliner["location"]}" |
| end |
| bytecodeCounts[description] += 1 |
| tierCounts[frame["category"]] += 1 |
| totalSamples += 1 |
| end |
| |
| def takeMax bytecodeCounts |
| maxFrameDescription = nil |
| maxFrameCount = 0 |
| bytecodeCounts.each do |key, value| |
| if value > maxFrameCount |
| maxFrameCount = value |
| maxFrameDescription = key |
| end |
| end |
| |
| if maxFrameDescription |
| bytecodeCounts.delete(maxFrameDescription) |
| end |
| return maxFrameDescription, maxFrameCount |
| end |
| |
| timingInterval = database["interval"] * 1000.0 * 1000.0 |
| puts "Sampling rate: #{timingInterval} microseconds. Total samples: #{totalSamples}" |
| puts "" |
| puts "Tier breakdown:" |
| puts "-----------------------------------" |
| |
| $tiers.each do |key, value| |
| printf("%s %s (%0.06f%%)\n", (value + ':').ljust(20, ' '), tierCounts[value].to_s.rjust(10, ' '), tierCounts[value] * 100.0 / totalSamples.to_f) |
| end |
| |
| puts "" |
| puts "Hottest bytecodes as <numSamples 'functionName#hash:JITType:bytecodeIndex'>" |
| |
| $samplingProfilerTopBytecodesCount.times do |
| key, value = takeMax(bytecodeCounts) |
| break unless key |
| printf("%6u '%s'\n", value, key) |
| end |
| end |
| |
| def main |
| json = nil |
| if ARGV.empty? |
| json = JSON.parse(STDIN.read) |
| else |
| json = JSON::parse(IO::read(ARGV[0])) |
| end |
| |
| reportTopFunctions(json) |
| puts "" |
| reportTopBytecodes(json) |
| end |
| |
| main |
| |
| # vim: set sw=4 ts=4 et tw=80 : |