Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add file listing output to vernier view #112

Merged
merged 10 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 8 additions & 52 deletions exe/vernier
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env ruby

$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)

require "optparse"
require "vernier/version"

Expand Down Expand Up @@ -55,60 +57,14 @@ FLAGS:

def self.inverted_tree(top, file)
# Print the inverted tree from a Vernier profile
require "json"

is_gzip = File.binread(file, 2) == "\x1F\x8B".b # check for gzip header

json = if is_gzip
require "zlib"
Zlib::GzipReader.open(file) { |gz| gz.read }
else
File.read file
end

info = JSON.load json

main = info["threads"].find { |thread| thread["isMainThread"] }

weight_by_frame = Hash.new(0)

stack_frames = main["stackTable"]["frame"]
frame_table = main["frameTable"]["func"]
func_table = main["funcTable"]["name"]
string_array = main["stringArray"]
require "vernier/parsed_profile"
require "vernier/output/top"
require "vernier/output/file_listing"

main["samples"]["stack"].zip(main["samples"]["weight"]).each do |stack, weight|
top_frame_index = stack_frames[stack]
func_index = frame_table[top_frame_index]
string_index = func_table[func_index]
str = string_array[string_index]
weight_by_frame[str] += weight
end

total = weight_by_frame.values.inject :+

header = ["Samples", "%", ""]
widths = header.map(&:bytesize)

columns = weight_by_frame.sort_by { |k,v| v }.reverse.first(top).map { |k,v|
entry = [v.to_s, ((v / total.to_f) * 100).round(1).to_s, k]
entry.each_with_index { |str, i| widths[i] = str.bytesize if widths[i] < str.bytesize }
entry
}

print_separator widths
print_row header, widths
print_separator widths
columns.each { print_row(_1, widths) }
print_separator widths
end

def self.print_row(list, widths)
puts("|" + list.map.with_index { |str, i| " " + str.ljust(widths[i] + 1) }.join("|") + "|")
end
parsed_profile = Vernier::ParsedProfile.read_file(file)

def self.print_separator(widths)
puts("+" + widths.map { |i| "-" * (i + 2) }.join("+") + "+")
puts Vernier::Output::Top.new(parsed_profile).output
puts Vernier::Output::FileListing.new(parsed_profile).output
end
end
end
Expand Down
3 changes: 3 additions & 0 deletions lib/vernier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
require_relative "vernier/version"
require_relative "vernier/collector"
require_relative "vernier/stack_table"
require_relative "vernier/parsed_profile"
require_relative "vernier/result"
require_relative "vernier/hooks"
require_relative "vernier/vernier"
require_relative "vernier/output/firefox"
require_relative "vernier/output/top"
require_relative "vernier/output/file_listing"
require_relative "vernier/output/filename_filter"

module Vernier
class Error < StandardError; end
Expand Down
113 changes: 113 additions & 0 deletions lib/vernier/output/file_listing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# frozen_string_literal: true

require_relative "filename_filter"

module Vernier
module Output
class FileListing
class SamplesByLocation
attr_accessor :self, :total
def initialize
@self = @total = 0
end

def +(other)
ret = SamplesByLocation.new
ret.self = @self + other.self
ret.total = @total + other.total
ret
end
end

def initialize(profile)
@profile = profile
end

def output
output = +""

thread = @profile.main_thread
if Hash === thread
# live profile
stack_table = @profile._stack_table
weights = thread[:weights]
samples = thread[:samples]
filename_filter = FilenameFilter.new
else
stack_table = thread.stack_table
weights = thread.weights
samples = thread.samples
filename_filter = ->(x) { x }
end

self_samples_by_frame = Hash.new do |h, k|
h[k] = SamplesByLocation.new
end

total = weights.sum

samples.zip(weights).each do |stack_idx, weight|
# self time
top_frame_index = stack_table.stack_frame_idx(stack_idx)
self_samples_by_frame[top_frame_index].self += weight

# total time
while stack_idx
frame_idx = stack_table.stack_frame_idx(stack_idx)
self_samples_by_frame[frame_idx].total += weight
stack_idx = stack_table.stack_parent_idx(stack_idx)
end
end

samples_by_file = Hash.new do |h, k|
h[k] = Hash.new do |h2, k2|
h2[k2] = SamplesByLocation.new
end
end

self_samples_by_frame.each do |frame, samples|
line = stack_table.frame_line_no(frame)
func_index = stack_table.frame_func_idx(frame)
filename = stack_table.func_filename(func_index)

samples_by_file[filename][line] += samples
end

samples_by_file.transform_keys! do |filename|
filename_filter.call(filename)
end

relevant_files = samples_by_file.select do |k, v|
next if k.start_with?("gem:")
next if k.start_with?("rubylib:")
next if k.start_with?("<")
v.values.map(&:total).sum > total * 0.01
end
relevant_files.keys.sort.each do |filename|
output << "="*80 << "\n"
output << filename << "\n"
output << "-"*80 << "\n"
format_file(output, filename, samples_by_file, total: total)
end
output << "="*80 << "\n"
end

def format_file(output, filename, all_samples, total:)
samples = all_samples[filename]

# file_name, lines, file_wall, file_cpu, file_idle, file_sort
output << sprintf(" TOTAL | SELF | LINE SOURCE\n")
File.readlines(filename).each_with_index do |line, i|
lineno = i + 1
calls = samples[lineno]

if calls && calls.total > 0
output << sprintf("%5.1f%% | %5.1f%% | % 4i %s", 100 * calls.total / total.to_f, 100 * calls.self / total.to_f, lineno, line)
else
output << sprintf(" | | % 4i %s", lineno, line)
end
end
end
end
end
end
30 changes: 30 additions & 0 deletions lib/vernier/output/filename_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module Vernier
module Output
class FilenameFilter
def initialize
@pwd = "#{Dir.pwd}/"
@gem_regex = %r{\A#{Regexp.union(Gem.path)}/gems/}
@gem_match_regex = %r{\A#{Regexp.union(Gem.path)}/gems/([a-zA-Z](?:[a-zA-Z0-9\.\_]|-[a-zA-Z])*)-([0-9][0-9A-Za-z\-_\.]*)/(.*)\z}
@rubylibdir = "#{RbConfig::CONFIG["rubylibdir"]}/"
end

attr_reader :pwd, :gem_regex, :gem_match_regex, :rubylibdir

def call(filename)
if filename.match?(gem_regex)
gem_match_regex =~ filename
"gem:#$1-#$2:#$3"
elsif filename.start_with?(pwd)
filename.delete_prefix(pwd)
elsif filename.start_with?(rubylibdir)
path = filename.delete_prefix(rubylibdir)
"rubylib:#{RUBY_VERSION}:#{path}"
else
filename
end
end
end
end
end
20 changes: 4 additions & 16 deletions lib/vernier/output/firefox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require "json"
require "rbconfig"

require_relative "filename_filter"

module Vernier
module Output
# https://profiler.firefox.com/
Expand Down Expand Up @@ -324,23 +326,9 @@ def initialize(ruby_thread_id, profile, categorizer, name:, tid:, samples:, weig
end

def filter_filenames(filenames)
pwd = "#{Dir.pwd}/"
gem_regex = %r{\A#{Regexp.union(Gem.path)}/gems/}
gem_match_regex = %r{\A#{Regexp.union(Gem.path)}/gems/([a-zA-Z](?:[a-zA-Z0-9\.\_]|-[a-zA-Z])*)-([0-9][0-9A-Za-z\-_\.]*)/(.*)\z}
rubylibdir = "#{RbConfig::CONFIG["rubylibdir"]}/"

filter = FilenameFilter.new
filenames.map do |filename|
if filename.match?(gem_regex)
gem_match_regex =~ filename
"gem:#$1-#$2:#$3"
elsif filename.start_with?(pwd)
filename.delete_prefix(pwd)
elsif filename.start_with?(rubylibdir)
path = filename.delete_prefix(rubylibdir)
"rubylib:#{RUBY_VERSION}:#{path}"
else
filename
end
filter.call(filename)
end
end

Expand Down
68 changes: 60 additions & 8 deletions lib/vernier/output/top.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,75 @@ def initialize(profile)
@profile = profile
end

class Table
def initialize(header)
@header = header
@rows = []
yield self
end

def <<(row)
@rows << row
end

def to_s
(
[
row_separator,
format_row(@header),
row_separator
] + @rows.map do |row|
format_row(row)
end + [row_separator]
).join("\n")
end

def widths
@widths ||=
(@rows + [@header]).transpose.map do |col|
col.map(&:size).max
end
end

def row_separator
@row_separator = "+" + widths.map { |i| "-" * (i + 2) }.join("+") + "+"
end

def format_row(row)
"|" + row.map.with_index { |str, i| " " + str.ljust(widths[i] + 1) }.join("|") + "|"
end
end

def output
thread = @profile.main_thread
stack_table =
if thread.respond_to?(:stack_table)
thread.stack_table
else
@profile._stack_table
end

stack_weights = Hash.new(0)
@profile.samples.zip(@profile.weights) do |stack_idx, weight|
thread[:samples].zip(thread[:weights]) do |stack_idx, weight|
stack_weights[stack_idx] += weight
end

total = stack_weights.values.sum

top_by_self = Hash.new(0)
stack_weights.each do |stack_idx, weight|
stack = @profile.stack(stack_idx)
top_by_self[stack.leaf_frame.name] += weight
frame_idx = stack_table.stack_frame_idx(stack_idx)
func_idx = stack_table.frame_func_idx(frame_idx)
name = stack_table.func_name(func_idx)
top_by_self[name] += weight
end

s = +""
top_by_self.sort_by(&:last).reverse.each do |frame, samples|
s << "#{samples}\t#{frame}\n"
end
s
Table.new %w[Samples % name] do |t|
top_by_self.sort_by(&:last).reverse.each do |frame, samples|
pct = 100.0 * samples / total
t << [samples.to_s, pct.round(1).to_s, frame]
end
end.to_s
end
end
end
Expand Down
Loading
Loading