vernier 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +38 -12
- data/examples/fiber_stalls.rb +51 -0
- data/exe/vernier +11 -52
- data/ext/vernier/extconf.rb +2 -0
- data/ext/vernier/memory.cc +144 -0
- data/ext/vernier/periodic_thread.hh +141 -0
- data/ext/vernier/signal_safe_semaphore.hh +72 -0
- data/ext/vernier/timestamp.hh +138 -0
- data/ext/vernier/vernier.cc +142 -339
- data/ext/vernier/vernier.hh +4 -0
- data/lib/vernier/autorun.rb +17 -1
- data/lib/vernier/collector.rb +37 -9
- data/lib/vernier/hooks/memory_usage.rb +37 -0
- data/lib/vernier/hooks.rb +1 -0
- data/lib/vernier/marker.rb +2 -0
- data/lib/vernier/middleware.rb +1 -1
- data/lib/vernier/output/file_listing.rb +152 -0
- data/lib/vernier/output/filename_filter.rb +30 -0
- data/lib/vernier/output/firefox.rb +39 -26
- data/lib/vernier/output/top.rb +60 -8
- data/lib/vernier/parsed_profile.rb +102 -0
- data/lib/vernier/result.rb +4 -92
- data/lib/vernier/stack_table.rb +3 -42
- data/lib/vernier/stack_table_helpers.rb +140 -0
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +3 -0
- metadata +13 -6
data/lib/vernier/autorun.rb
CHANGED
@@ -29,8 +29,24 @@ module Vernier
|
|
29
29
|
def self.stop
|
30
30
|
result = @collector.stop
|
31
31
|
@collector = nil
|
32
|
+
|
32
33
|
output_path = options[:output]
|
33
|
-
output_path
|
34
|
+
unless output_path
|
35
|
+
output_dir = options[:output_dir]
|
36
|
+
unless output_dir
|
37
|
+
if File.writable?(".")
|
38
|
+
output_dir = "."
|
39
|
+
else
|
40
|
+
output_dir = Dir.tmpdir
|
41
|
+
end
|
42
|
+
end
|
43
|
+
prefix = "profile-"
|
44
|
+
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
45
|
+
suffix = ".vernier.json.gz"
|
46
|
+
|
47
|
+
output_path = File.expand_path("#{output_dir}/#{prefix}#{timestamp}-#{$$}#{suffix}")
|
48
|
+
end
|
49
|
+
|
34
50
|
result.write(out: output_path)
|
35
51
|
|
36
52
|
STDERR.puts(result.inspect)
|
data/lib/vernier/collector.rb
CHANGED
@@ -31,6 +31,8 @@ module Vernier
|
|
31
31
|
case hook.to_sym
|
32
32
|
when :rails, :activesupport
|
33
33
|
@hooks << Vernier::Hooks::ActiveSupport.new(self)
|
34
|
+
when :memory_usage
|
35
|
+
@hooks << Vernier::Hooks::MemoryUsage.new(self)
|
34
36
|
else
|
35
37
|
warn "unknown hook: #{hook.inspect}"
|
36
38
|
end
|
@@ -97,18 +99,44 @@ module Vernier
|
|
97
99
|
|
98
100
|
marker_strings = Marker.name_table
|
99
101
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
102
|
+
markers_by_thread_id = (@markers || []).group_by(&:first)
|
103
|
+
|
104
|
+
result.threads.each do |tid, thread|
|
105
|
+
last_fiber = nil
|
106
|
+
markers = []
|
107
|
+
|
108
|
+
markers.concat markers_by_thread_id.fetch(tid, [])
|
109
|
+
|
110
|
+
original_markers = thread[:markers] || []
|
111
|
+
original_markers += result.gc_markers || []
|
112
|
+
original_markers.each do |data|
|
113
|
+
type, phase, ts, te, stack, extra_info = data
|
114
|
+
if type == Marker::Type::FIBER_SWITCH
|
115
|
+
if last_fiber
|
116
|
+
start_event = markers[last_fiber]
|
117
|
+
markers << [nil, "Fiber Running", start_event[2], ts, Marker::Phase::INTERVAL, start_event[5].merge(type: "Fiber Running", cause: nil)]
|
118
|
+
end
|
119
|
+
last_fiber = markers.size
|
120
|
+
end
|
121
|
+
name = marker_strings[type]
|
122
|
+
sym = Marker::MARKER_SYMBOLS[type]
|
123
|
+
data = { type: sym }
|
124
|
+
data[:cause] = { stack: stack } if stack
|
125
|
+
data.merge!(extra_info) if extra_info
|
126
|
+
markers << [tid, name, ts, te, phase, data]
|
127
|
+
end
|
128
|
+
if last_fiber
|
129
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
130
|
+
start_event = markers[last_fiber]
|
131
|
+
markers << [nil, "Fiber Running", start_event[2], end_time, Marker::Phase::INTERVAL, start_event[5].merge(type: "Fiber Running", cause: nil)]
|
132
|
+
end
|
133
|
+
|
134
|
+
thread[:markers] = markers
|
107
135
|
end
|
108
136
|
|
109
|
-
markers.concat @markers
|
137
|
+
#markers.concat @markers
|
110
138
|
|
111
|
-
result.instance_variable_set(:@markers, markers)
|
139
|
+
#result.instance_variable_set(:@markers, markers)
|
112
140
|
|
113
141
|
if @out
|
114
142
|
result.write(out: @out)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vernier
|
4
|
+
module Hooks
|
5
|
+
class MemoryUsage
|
6
|
+
def initialize(collector)
|
7
|
+
@collector = collector
|
8
|
+
@tracker = Vernier::MemoryTracker.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def enable
|
12
|
+
@tracker.start
|
13
|
+
end
|
14
|
+
|
15
|
+
def disable
|
16
|
+
@tracker.stop
|
17
|
+
end
|
18
|
+
|
19
|
+
def firefox_counters
|
20
|
+
timestamps, memory = @tracker.results
|
21
|
+
memory = ([0] + memory).each_cons(2).map { _2 - _1 }
|
22
|
+
{
|
23
|
+
name: "memory",
|
24
|
+
category: "Memory",
|
25
|
+
description: "Memory usage in bytes",
|
26
|
+
pid: Process.pid,
|
27
|
+
mainThreadIndex: 0,
|
28
|
+
samples: {
|
29
|
+
time: timestamps.map { _1 / 1_000_000.0 },
|
30
|
+
count: memory,
|
31
|
+
length: timestamps.length
|
32
|
+
}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/vernier/hooks.rb
CHANGED
data/lib/vernier/marker.rb
CHANGED
data/lib/vernier/middleware.rb
CHANGED
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "filename_filter"
|
4
|
+
require "cgi/util"
|
5
|
+
|
6
|
+
module Vernier
|
7
|
+
module Output
|
8
|
+
class FileListing
|
9
|
+
class SamplesByLocation
|
10
|
+
attr_accessor :self, :total
|
11
|
+
def initialize
|
12
|
+
@self = @total = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def +(other)
|
16
|
+
ret = SamplesByLocation.new
|
17
|
+
ret.self = @self + other.self
|
18
|
+
ret.total = @total + other.total
|
19
|
+
ret
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(profile)
|
24
|
+
@profile = profile
|
25
|
+
end
|
26
|
+
|
27
|
+
def samples_by_file
|
28
|
+
thread = @profile.main_thread
|
29
|
+
if Hash === thread
|
30
|
+
# live profile
|
31
|
+
stack_table = @profile._stack_table
|
32
|
+
filename_filter = FilenameFilter.new
|
33
|
+
else
|
34
|
+
stack_table = thread.stack_table
|
35
|
+
filename_filter = ->(x) { x }
|
36
|
+
end
|
37
|
+
|
38
|
+
weights = thread[:weights]
|
39
|
+
samples = thread[:samples]
|
40
|
+
|
41
|
+
self_samples_by_frame = Hash.new do |h, k|
|
42
|
+
h[k] = SamplesByLocation.new
|
43
|
+
end
|
44
|
+
|
45
|
+
samples.zip(weights).each do |stack_idx, weight|
|
46
|
+
# self time
|
47
|
+
top_frame_index = stack_table.stack_frame_idx(stack_idx)
|
48
|
+
self_samples_by_frame[top_frame_index].self += weight
|
49
|
+
|
50
|
+
# total time
|
51
|
+
while stack_idx
|
52
|
+
frame_idx = stack_table.stack_frame_idx(stack_idx)
|
53
|
+
self_samples_by_frame[frame_idx].total += weight
|
54
|
+
stack_idx = stack_table.stack_parent_idx(stack_idx)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
samples_by_file = Hash.new do |h, k|
|
59
|
+
h[k] = Hash.new do |h2, k2|
|
60
|
+
h2[k2] = SamplesByLocation.new
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
self_samples_by_frame.each do |frame, samples|
|
65
|
+
line = stack_table.frame_line_no(frame)
|
66
|
+
func_index = stack_table.frame_func_idx(frame)
|
67
|
+
filename = stack_table.func_filename(func_index)
|
68
|
+
|
69
|
+
samples_by_file[filename][line] += samples
|
70
|
+
end
|
71
|
+
|
72
|
+
samples_by_file.transform_keys! do |filename|
|
73
|
+
filename_filter.call(filename)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def output(template: nil)
|
78
|
+
output = +""
|
79
|
+
|
80
|
+
relevant_files = samples_by_file.select do |k, v|
|
81
|
+
next if k.start_with?("gem:")
|
82
|
+
next if k.start_with?("rubylib:")
|
83
|
+
next if k.start_with?("<")
|
84
|
+
v.values.map(&:total).sum > total * 0.01
|
85
|
+
end
|
86
|
+
|
87
|
+
if template == "html"
|
88
|
+
html_output(output, relevant_files)
|
89
|
+
else
|
90
|
+
relevant_files.keys.sort.each do |filename|
|
91
|
+
output << "="*80 << "\n"
|
92
|
+
output << filename << "\n"
|
93
|
+
output << "-"*80 << "\n"
|
94
|
+
format_file(output, filename, samples_by_file, total: total)
|
95
|
+
end
|
96
|
+
output << "="*80 << "\n"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def total
|
101
|
+
thread = @profile.main_thread
|
102
|
+
thread[:weights].sum
|
103
|
+
end
|
104
|
+
|
105
|
+
def format_file(output, filename, all_samples, total:)
|
106
|
+
samples = all_samples[filename]
|
107
|
+
|
108
|
+
# file_name, lines, file_wall, file_cpu, file_idle, file_sort
|
109
|
+
output << sprintf(" TOTAL | SELF | LINE SOURCE\n")
|
110
|
+
File.readlines(filename).each_with_index do |line, i|
|
111
|
+
lineno = i + 1
|
112
|
+
calls = samples[lineno]
|
113
|
+
|
114
|
+
if calls && calls.total > 0
|
115
|
+
output << sprintf("%5.1f%% | %5.1f%% | % 4i %s", 100 * calls.total / total.to_f, 100 * calls.self / total.to_f, lineno, line)
|
116
|
+
else
|
117
|
+
output << sprintf(" | | % 4i %s", lineno, line)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def html_output(output, relevant_files)
|
123
|
+
output << "<pre>"
|
124
|
+
output << " SELF FILE\n"
|
125
|
+
relevant_files.sort_by {|k, v| -v.values.map(&:self).sum }.each do |filename, file_contents|
|
126
|
+
tmpl = "<details style=\"display:inline-block;vertical-align:top;\"><summary>%s</summary>"
|
127
|
+
output << sprintf("% 5.1f%% #{tmpl}\n", file_contents.values.map(&:self).sum * 100 / total.to_f, filename)
|
128
|
+
format_file_html(output, filename, relevant_files)
|
129
|
+
output << "</details>\n"
|
130
|
+
end
|
131
|
+
output << "</pre>"
|
132
|
+
end
|
133
|
+
|
134
|
+
def format_file_html(output, filename, relevant_files)
|
135
|
+
samples = relevant_files[filename]
|
136
|
+
|
137
|
+
# file_name, lines, file_wall, file_cpu, file_idle, file_sort
|
138
|
+
output << sprintf(" TOTAL | SELF | LINE SOURCE\n")
|
139
|
+
File.readlines(filename).each_with_index do |line, i|
|
140
|
+
lineno = i + 1
|
141
|
+
calls = samples[lineno]
|
142
|
+
|
143
|
+
if calls && calls.total > 0
|
144
|
+
output << sprintf("%5.1f%% | %5.1f%% | % 4i %s", 100 * calls.total / total.to_f, 100 * calls.self / total.to_f, lineno, CGI::escapeHTML(line))
|
145
|
+
else
|
146
|
+
output << sprintf(" | | % 4i %s", lineno, CGI::escapeHTML(line))
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vernier
|
4
|
+
module Output
|
5
|
+
class FilenameFilter
|
6
|
+
def initialize
|
7
|
+
@pwd = "#{Dir.pwd}/"
|
8
|
+
@gem_regex = %r{\A#{Regexp.union(Gem.path)}/gems/}
|
9
|
+
@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}
|
10
|
+
@rubylibdir = "#{RbConfig::CONFIG["rubylibdir"]}/"
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :pwd, :gem_regex, :gem_match_regex, :rubylibdir
|
14
|
+
|
15
|
+
def call(filename)
|
16
|
+
if filename.match?(gem_regex)
|
17
|
+
gem_match_regex =~ filename
|
18
|
+
"gem:#$1-#$2:#$3"
|
19
|
+
elsif filename.start_with?(pwd)
|
20
|
+
filename.delete_prefix(pwd)
|
21
|
+
elsif filename.start_with?(rubylibdir)
|
22
|
+
path = filename.delete_prefix(rubylibdir)
|
23
|
+
"rubylib:#{RUBY_VERSION}:#{path}"
|
24
|
+
else
|
25
|
+
filename
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
require "json"
|
4
4
|
require "rbconfig"
|
5
5
|
|
6
|
+
require_relative "filename_filter"
|
7
|
+
|
6
8
|
module Vernier
|
7
9
|
module Output
|
8
10
|
# https://profiler.firefox.com/
|
@@ -104,15 +106,15 @@ module Vernier
|
|
104
106
|
attr_reader :profile
|
105
107
|
|
106
108
|
def data
|
107
|
-
markers_by_thread = profile.markers.group_by { |marker| marker[0] }
|
109
|
+
#markers_by_thread = profile.markers.group_by { |marker| marker[0] }
|
108
110
|
|
109
111
|
threads = profile.threads.map do |ruby_thread_id, thread_info|
|
110
|
-
markers = markers_by_thread[ruby_thread_id] || []
|
112
|
+
#markers = markers_by_thread[ruby_thread_id] || []
|
111
113
|
Thread.new(
|
112
114
|
ruby_thread_id,
|
113
115
|
profile,
|
114
116
|
@categorizer,
|
115
|
-
markers: markers,
|
117
|
+
#markers: markers,
|
116
118
|
**thread_info,
|
117
119
|
)
|
118
120
|
end
|
@@ -126,7 +128,7 @@ module Vernier
|
|
126
128
|
product: "Ruby/Vernier",
|
127
129
|
stackwalk: 1,
|
128
130
|
version: 28,
|
129
|
-
preprocessedProfileVersion:
|
131
|
+
preprocessedProfileVersion: 48,
|
130
132
|
symbolicated: true,
|
131
133
|
markerSchema: marker_schema,
|
132
134
|
sampleUnits: {
|
@@ -145,11 +147,20 @@ module Vernier
|
|
145
147
|
initialVisibleThreads: threads.each_index.to_a,
|
146
148
|
initialSelectedThreads: Array(threads.find_index(&:is_start))
|
147
149
|
},
|
150
|
+
counters: counter_data,
|
148
151
|
libs: [],
|
149
152
|
threads: threads.map(&:data)
|
150
153
|
}
|
151
154
|
end
|
152
155
|
|
156
|
+
def counter_data
|
157
|
+
profile.hooks.flat_map do |hook|
|
158
|
+
if hook.respond_to?(:firefox_counters)
|
159
|
+
hook.firefox_counters
|
160
|
+
end
|
161
|
+
end.compact
|
162
|
+
end
|
163
|
+
|
153
164
|
def marker_schema
|
154
165
|
hook_additions = profile.hooks.flat_map do |hook|
|
155
166
|
if hook.respond_to?(:firefox_marker_schema)
|
@@ -201,6 +212,18 @@ module Vernier
|
|
201
212
|
{ key: "gc_by", format: "string" },
|
202
213
|
]
|
203
214
|
},
|
215
|
+
{
|
216
|
+
name: "FIBER_SWITCH",
|
217
|
+
display: [ "marker-chart", "marker-table", "timeline-overview" ],
|
218
|
+
tooltipLabel: "{marker.name} - {marker.data.fiber_id}",
|
219
|
+
data: [
|
220
|
+
{
|
221
|
+
label: "Description",
|
222
|
+
value: "Switch running Fiber"
|
223
|
+
},
|
224
|
+
{ key: "fiber_id", format: "integer" },
|
225
|
+
]
|
226
|
+
},
|
204
227
|
*hook_additions
|
205
228
|
]
|
206
229
|
end
|
@@ -303,23 +326,9 @@ module Vernier
|
|
303
326
|
end
|
304
327
|
|
305
328
|
def filter_filenames(filenames)
|
306
|
-
|
307
|
-
gem_regex = %r{\A#{Regexp.union(Gem.path)}/gems/}
|
308
|
-
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}
|
309
|
-
rubylibdir = "#{RbConfig::CONFIG["rubylibdir"]}/"
|
310
|
-
|
329
|
+
filter = FilenameFilter.new
|
311
330
|
filenames.map do |filename|
|
312
|
-
|
313
|
-
gem_match_regex =~ filename
|
314
|
-
"gem:#$1-#$2:#$3"
|
315
|
-
elsif filename.start_with?(pwd)
|
316
|
-
filename.delete_prefix(pwd)
|
317
|
-
elsif filename.start_with?(rubylibdir)
|
318
|
-
path = filename.delete_prefix(rubylibdir)
|
319
|
-
"rubylib:#{RUBY_VERSION}:#{path}"
|
320
|
-
else
|
321
|
-
filename
|
322
|
-
end
|
331
|
+
filter.call(filename)
|
323
332
|
end
|
324
333
|
end
|
325
334
|
|
@@ -478,17 +487,21 @@ module Vernier
|
|
478
487
|
def frame_table
|
479
488
|
funcs = @stack_table_hash[:frame_table].fetch(:func)
|
480
489
|
lines = @stack_table_hash[:frame_table].fetch(:line)
|
481
|
-
size
|
490
|
+
raise unless lines.size == funcs.size
|
491
|
+
|
492
|
+
size = funcs.size
|
482
493
|
none = [nil] * size
|
483
|
-
|
494
|
+
default = [0] * size
|
495
|
+
unidentified = [-1] * size
|
484
496
|
|
485
|
-
|
497
|
+
categories = @frame_categories.map(&:idx)
|
498
|
+
subcategories = @frame_subcategories
|
486
499
|
|
487
500
|
{
|
488
|
-
address:
|
489
|
-
inlineDepth:
|
501
|
+
address: unidentified,
|
502
|
+
inlineDepth: default,
|
490
503
|
category: categories,
|
491
|
-
subcategory:
|
504
|
+
subcategory: subcategories,
|
492
505
|
func: funcs,
|
493
506
|
nativeSymbol: none,
|
494
507
|
innerWindowID: none,
|
data/lib/vernier/output/top.rb
CHANGED
@@ -7,23 +7,75 @@ module Vernier
|
|
7
7
|
@profile = profile
|
8
8
|
end
|
9
9
|
|
10
|
+
class Table
|
11
|
+
def initialize(header)
|
12
|
+
@header = header
|
13
|
+
@rows = []
|
14
|
+
yield self
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(row)
|
18
|
+
@rows << row
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
(
|
23
|
+
[
|
24
|
+
row_separator,
|
25
|
+
format_row(@header),
|
26
|
+
row_separator
|
27
|
+
] + @rows.map do |row|
|
28
|
+
format_row(row)
|
29
|
+
end + [row_separator]
|
30
|
+
).join("\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
def widths
|
34
|
+
@widths ||=
|
35
|
+
(@rows + [@header]).transpose.map do |col|
|
36
|
+
col.map(&:size).max
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def row_separator
|
41
|
+
@row_separator = "+" + widths.map { |i| "-" * (i + 2) }.join("+") + "+"
|
42
|
+
end
|
43
|
+
|
44
|
+
def format_row(row)
|
45
|
+
"|" + row.map.with_index { |str, i| " " + str.ljust(widths[i] + 1) }.join("|") + "|"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
10
49
|
def output
|
50
|
+
thread = @profile.main_thread
|
51
|
+
stack_table =
|
52
|
+
if thread.respond_to?(:stack_table)
|
53
|
+
thread.stack_table
|
54
|
+
else
|
55
|
+
@profile._stack_table
|
56
|
+
end
|
57
|
+
|
11
58
|
stack_weights = Hash.new(0)
|
12
|
-
|
59
|
+
thread[:samples].zip(thread[:weights]) do |stack_idx, weight|
|
13
60
|
stack_weights[stack_idx] += weight
|
14
61
|
end
|
15
62
|
|
63
|
+
total = stack_weights.values.sum
|
64
|
+
|
16
65
|
top_by_self = Hash.new(0)
|
17
66
|
stack_weights.each do |stack_idx, weight|
|
18
|
-
|
19
|
-
|
67
|
+
frame_idx = stack_table.stack_frame_idx(stack_idx)
|
68
|
+
func_idx = stack_table.frame_func_idx(frame_idx)
|
69
|
+
name = stack_table.func_name(func_idx)
|
70
|
+
top_by_self[name] += weight
|
20
71
|
end
|
21
72
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
73
|
+
Table.new %w[Samples % name] do |t|
|
74
|
+
top_by_self.sort_by(&:last).reverse.each do |frame, samples|
|
75
|
+
pct = 100.0 * samples / total
|
76
|
+
t << [samples.to_s, pct.round(1).to_s, frame]
|
77
|
+
end
|
78
|
+
end.to_s
|
27
79
|
end
|
28
80
|
end
|
29
81
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require_relative "stack_table_helpers"
|
5
|
+
|
6
|
+
module Vernier
|
7
|
+
class ParsedProfile
|
8
|
+
def self.read_file(filename)
|
9
|
+
# Print the inverted tree from a Vernier profile
|
10
|
+
is_gzip = File.binread(filename, 2) == "\x1F\x8B".b # check for gzip header
|
11
|
+
|
12
|
+
json = if is_gzip
|
13
|
+
require "zlib"
|
14
|
+
Zlib::GzipReader.open(filename) { |gz| gz.read }
|
15
|
+
else
|
16
|
+
File.read filename
|
17
|
+
end
|
18
|
+
|
19
|
+
info = JSON.load json
|
20
|
+
|
21
|
+
new(info)
|
22
|
+
end
|
23
|
+
|
24
|
+
class StackTable
|
25
|
+
def initialize(thread_data)
|
26
|
+
@stack_parents = thread_data["stackTable"]["prefix"]
|
27
|
+
@stack_frames = thread_data["stackTable"]["frame"]
|
28
|
+
@frame_funcs = thread_data["frameTable"]["func"]
|
29
|
+
@frame_lines = thread_data["frameTable"]["line"]
|
30
|
+
@func_names = thread_data["funcTable"]["name"]
|
31
|
+
@func_filenames = thread_data["funcTable"]["fileName"]
|
32
|
+
#@func_first_linenos = thread_data["funcTable"]["first"]
|
33
|
+
@strings = thread_data["stringArray"]
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :strings
|
37
|
+
|
38
|
+
def stack_count = @stack_parents.length
|
39
|
+
def frame_count = @frame_funcs.length
|
40
|
+
def func_count = @func_names.length
|
41
|
+
|
42
|
+
def stack_parent_idx(idx) = @stack_parents[idx]
|
43
|
+
def stack_frame_idx(idx) = @stack_frames[idx]
|
44
|
+
|
45
|
+
def frame_func_idx(idx) = @frame_funcs[idx]
|
46
|
+
def frame_line_no(idx) = @frame_lines[idx]
|
47
|
+
|
48
|
+
def func_name_idx(idx) = @func_names[idx]
|
49
|
+
def func_filename_idx(idx) = @func_filenames[idx]
|
50
|
+
def func_name(idx) = @strings[func_name_idx(idx)]
|
51
|
+
def func_filename(idx) = @strings[func_filename_idx(idx)]
|
52
|
+
def func_first_lineno(idx) = @func_first_lineno[idx]
|
53
|
+
|
54
|
+
include StackTableHelpers
|
55
|
+
end
|
56
|
+
|
57
|
+
class Thread
|
58
|
+
attr_reader :data
|
59
|
+
|
60
|
+
def initialize(data)
|
61
|
+
@data = data
|
62
|
+
end
|
63
|
+
|
64
|
+
def stack_table
|
65
|
+
@stack_table ||= StackTable.new(@data)
|
66
|
+
end
|
67
|
+
|
68
|
+
def main_thread?
|
69
|
+
@data["isMainThread"]
|
70
|
+
end
|
71
|
+
|
72
|
+
def samples
|
73
|
+
@data["samples"]["stack"]
|
74
|
+
end
|
75
|
+
|
76
|
+
def weights
|
77
|
+
@data["samples"]["weight"]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Emulate hash
|
81
|
+
def [](name)
|
82
|
+
send(name)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
attr_reader :data
|
87
|
+
def initialize(data)
|
88
|
+
@data = data
|
89
|
+
end
|
90
|
+
|
91
|
+
def threads
|
92
|
+
@threads ||=
|
93
|
+
@data["threads"].map do |thread_data|
|
94
|
+
Thread.new(thread_data)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def main_thread
|
99
|
+
threads.detect(&:main_thread?)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|