vernier 1.9.0 → 1.10.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/ext/vernier/heap_tracker.cc +2 -4
- data/ext/vernier/vernier.cc +2 -0
- data/lib/vernier/autorun.rb +4 -1
- data/lib/vernier/marker.rb +1 -1
- data/lib/vernier/middleware.rb +6 -1
- data/lib/vernier/output/markdown.rb +416 -0
- data/lib/vernier/parsed_profile.rb +2 -2
- data/lib/vernier/result.rb +10 -0
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +15 -14
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a504e7ef885ed393a864128d15ca792af4e6ab60f30cdf90afbc36aa5d577b34
|
|
4
|
+
data.tar.gz: 87ab42b6cc3ea1effae71f3b95bdc4d90c117abf1aadc3d425bb30b4573dafa9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f52d1d392009084c1511b7b2cc24c51fdb5da983d0fe1dd60286ed9fe0564229ec409513cd179f2af3ceb96f9a085e2f9f48b556ac5ae6b597aba7c13bd56cf8
|
|
7
|
+
data.tar.gz: 7b8255043801305c221a967a25974ff1e5896393adf8c820784dd980a2eeecfa49f63509b7eeddf1c0f81a7279c29ea87ffa88622de306d0d5b430c23a40c109
|
data/ext/vernier/heap_tracker.cc
CHANGED
|
@@ -47,8 +47,6 @@ class HeapTracker {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
void record_newobj(VALUE obj) {
|
|
50
|
-
objects_allocated++;
|
|
51
|
-
|
|
52
50
|
RawSample sample;
|
|
53
51
|
sample.sample();
|
|
54
52
|
if (sample.empty()) {
|
|
@@ -59,13 +57,13 @@ class HeapTracker {
|
|
|
59
57
|
}
|
|
60
58
|
int stack_index = stack_table->stack_index(sample);
|
|
61
59
|
|
|
60
|
+
objects_allocated++;
|
|
62
61
|
int idx = object_list.size();
|
|
63
62
|
object_list.push_back(obj);
|
|
64
63
|
frame_list.push_back(stack_index);
|
|
65
64
|
object_index.emplace(obj, idx);
|
|
66
65
|
|
|
67
|
-
assert(
|
|
68
|
-
assert(objects_allocated == object_list.size());
|
|
66
|
+
assert(frame_list.size() == object_list.size());
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
void rebuild() {
|
data/ext/vernier/vernier.cc
CHANGED
data/lib/vernier/autorun.rb
CHANGED
|
@@ -48,8 +48,11 @@ module Vernier
|
|
|
48
48
|
end
|
|
49
49
|
prefix = "profile-"
|
|
50
50
|
timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
|
|
51
|
-
suffix =
|
|
51
|
+
suffix = case options[:format]
|
|
52
|
+
when "cpuprofile"
|
|
52
53
|
".vernier.cpuprofile"
|
|
54
|
+
when "markdown", "md"
|
|
55
|
+
".vernier.md"
|
|
53
56
|
else
|
|
54
57
|
".vernier.json.gz"
|
|
55
58
|
end
|
data/lib/vernier/marker.rb
CHANGED
data/lib/vernier/middleware.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
module Vernier
|
|
2
2
|
class Middleware
|
|
3
|
+
HOOKS = [
|
|
4
|
+
(:rails if defined?(Rails))
|
|
5
|
+
].compact.freeze
|
|
6
|
+
private_constant :HOOKS
|
|
7
|
+
|
|
3
8
|
def initialize(app, permit: ->(_env) { true })
|
|
4
9
|
@app = app
|
|
5
10
|
@permit = permit
|
|
@@ -15,7 +20,7 @@ module Vernier
|
|
|
15
20
|
interval = request.GET.fetch("vernier_interval", 200).to_i
|
|
16
21
|
allocation_interval = request.GET.fetch("vernier_allocation_interval", 200).to_i
|
|
17
22
|
|
|
18
|
-
result = Vernier.trace(interval:, allocation_interval:, hooks:
|
|
23
|
+
result = Vernier.trace(interval:, allocation_interval:, hooks: HOOKS) do
|
|
19
24
|
@app.call(env)
|
|
20
25
|
end
|
|
21
26
|
body = result.to_firefox(gzip: true)
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "filename_filter"
|
|
4
|
+
|
|
5
|
+
module Vernier
|
|
6
|
+
module Output
|
|
7
|
+
class Markdown
|
|
8
|
+
DEFAULT_TOP_N = 20
|
|
9
|
+
DEFAULT_LINES_PER_FILE = 5
|
|
10
|
+
|
|
11
|
+
def initialize(profile, top_n: DEFAULT_TOP_N, lines_per_file: DEFAULT_LINES_PER_FILE)
|
|
12
|
+
@profile = profile
|
|
13
|
+
@top_n = top_n
|
|
14
|
+
@lines_per_file = lines_per_file
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def output
|
|
18
|
+
out = +""
|
|
19
|
+
out << build_title
|
|
20
|
+
out << build_summary
|
|
21
|
+
out << build_hotspots
|
|
22
|
+
out << build_threads
|
|
23
|
+
out << build_files
|
|
24
|
+
out
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def build_title
|
|
30
|
+
"# Vernier Profile\n\n"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_summary
|
|
34
|
+
out = +"## Summary\n\n"
|
|
35
|
+
|
|
36
|
+
mode = @profile.meta[:mode] rescue nil
|
|
37
|
+
weight_unit = mode == :retained ? "bytes" : "samples"
|
|
38
|
+
|
|
39
|
+
out << "| Metric | Value |\n"
|
|
40
|
+
out << "|--------|-------|\n"
|
|
41
|
+
out << "| Mode | #{mode || 'unknown'} |\n"
|
|
42
|
+
|
|
43
|
+
if @profile.respond_to?(:elapsed_seconds)
|
|
44
|
+
begin
|
|
45
|
+
out << "| Duration | #{format("%.2f", @profile.elapsed_seconds)} seconds |\n"
|
|
46
|
+
rescue
|
|
47
|
+
# elapsed_seconds may fail if end_time not set
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
out << "| Total Samples | #{total_samples} |\n"
|
|
52
|
+
out << "| Total Unique Samples | #{@profile.total_unique_samples rescue 'N/A'} |\n"
|
|
53
|
+
out << "| Threads | #{thread_count} |\n"
|
|
54
|
+
out << "| Weight Unit | #{weight_unit} |\n"
|
|
55
|
+
|
|
56
|
+
if @profile.respond_to?(:pid) && @profile.pid
|
|
57
|
+
out << "| PID | #{@profile.pid} |\n"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
out << "\n"
|
|
61
|
+
|
|
62
|
+
# User metadata if available
|
|
63
|
+
if @profile.respond_to?(:meta) && @profile.meta[:user_metadata]
|
|
64
|
+
metadata = @profile.meta[:user_metadata]
|
|
65
|
+
unless metadata.empty?
|
|
66
|
+
out << "### User Metadata\n\n"
|
|
67
|
+
out << "| Key | Value |\n"
|
|
68
|
+
out << "|-----|-------|\n"
|
|
69
|
+
metadata.each do |k, v|
|
|
70
|
+
out << "| #{escape_markdown(k.to_s)} | #{escape_markdown(v.to_s)} |\n"
|
|
71
|
+
end
|
|
72
|
+
out << "\n"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
out
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_hotspots
|
|
80
|
+
out = +"## Top Hotspots\n\n"
|
|
81
|
+
|
|
82
|
+
thread = main_thread
|
|
83
|
+
return out << "_No samples collected._\n\n" unless thread
|
|
84
|
+
|
|
85
|
+
stack_table = get_stack_table(thread)
|
|
86
|
+
samples = thread[:samples] || []
|
|
87
|
+
weights = thread[:weights] || []
|
|
88
|
+
|
|
89
|
+
return out << "_No samples collected._\n\n" if samples.empty?
|
|
90
|
+
|
|
91
|
+
# Compute self weights per function
|
|
92
|
+
total = weights.sum
|
|
93
|
+
return out << "_No samples collected._\n\n" if total == 0
|
|
94
|
+
|
|
95
|
+
top_by_func = Hash.new { |h, k| h[k] = { self: 0, total: 0, file: nil, line: nil } }
|
|
96
|
+
|
|
97
|
+
samples.zip(weights).each do |stack_idx, weight|
|
|
98
|
+
# Self time: top frame only
|
|
99
|
+
frame_idx = stack_table.stack_frame_idx(stack_idx)
|
|
100
|
+
func_idx = stack_table.frame_func_idx(frame_idx)
|
|
101
|
+
name = stack_table.func_name(func_idx)
|
|
102
|
+
filename = stack_table.func_filename(func_idx)
|
|
103
|
+
first_lineno = stack_table.func_first_lineno(func_idx)
|
|
104
|
+
|
|
105
|
+
top_by_func[name][:self] += weight
|
|
106
|
+
top_by_func[name][:file] ||= filename
|
|
107
|
+
top_by_func[name][:line] ||= first_lineno
|
|
108
|
+
|
|
109
|
+
# Total time: walk up the stack
|
|
110
|
+
seen = {}
|
|
111
|
+
current_stack_idx = stack_idx
|
|
112
|
+
while current_stack_idx
|
|
113
|
+
frame_idx = stack_table.stack_frame_idx(current_stack_idx)
|
|
114
|
+
func_idx = stack_table.frame_func_idx(frame_idx)
|
|
115
|
+
func_name = stack_table.func_name(func_idx)
|
|
116
|
+
|
|
117
|
+
unless seen[func_name]
|
|
118
|
+
seen[func_name] = true
|
|
119
|
+
top_by_func[func_name][:total] += weight
|
|
120
|
+
top_by_func[func_name][:file] ||= stack_table.func_filename(func_idx)
|
|
121
|
+
top_by_func[func_name][:line] ||= stack_table.func_first_lineno(func_idx)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
current_stack_idx = stack_table.stack_parent_idx(current_stack_idx)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
out << top_functions_table("By Self Time", top_by_func, total, :self)
|
|
129
|
+
out << top_functions_table("By Total Time", top_by_func, total, :total)
|
|
130
|
+
|
|
131
|
+
out
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def top_functions_table(title, funcs, total, sort_key)
|
|
135
|
+
out = +"### #{title}\n\n"
|
|
136
|
+
sorted = funcs.sort_by { |_, v| -v[sort_key] }.first(@top_n)
|
|
137
|
+
|
|
138
|
+
primary = sort_key == :self ? "Self" : "Total"
|
|
139
|
+
secondary = sort_key == :self ? "Total" : "Self"
|
|
140
|
+
out << "| Rank | #{primary} % | #{secondary} % | Function | Location |\n"
|
|
141
|
+
out << "|------|--------|---------|----------|----------|\n"
|
|
142
|
+
|
|
143
|
+
sorted.each_with_index do |(name, data), idx|
|
|
144
|
+
self_pct = 100.0 * data[:self] / total
|
|
145
|
+
total_pct = 100.0 * data[:total] / total
|
|
146
|
+
primary_pct = sort_key == :self ? self_pct : total_pct
|
|
147
|
+
secondary_pct = sort_key == :self ? total_pct : self_pct
|
|
148
|
+
location = format_location(data[:file], data[:line])
|
|
149
|
+
out << "| #{idx + 1} | #{format("%.1f", primary_pct)}% | #{format("%.1f", secondary_pct)}% | #{format_code_span(name)} | #{escape_markdown(location)} |\n"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
out << "\n"
|
|
153
|
+
out
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def build_threads
|
|
157
|
+
out = +"## Threads\n\n"
|
|
158
|
+
|
|
159
|
+
threads_data = get_threads
|
|
160
|
+
return out << "_No thread information available._\n\n" if threads_data.empty?
|
|
161
|
+
|
|
162
|
+
threads_data.each do |thread_id, thread|
|
|
163
|
+
name = get_thread_name(thread) || "Thread #{thread_id}"
|
|
164
|
+
is_main = get_thread_main(thread) ? "yes" : "no"
|
|
165
|
+
tid = get_thread_tid(thread) || thread_id
|
|
166
|
+
|
|
167
|
+
samples = thread[:samples] || []
|
|
168
|
+
weights = thread[:weights] || []
|
|
169
|
+
sample_count = samples.size
|
|
170
|
+
weight_sum = weights.sum
|
|
171
|
+
|
|
172
|
+
out << "### #{escape_markdown(name)}\n\n"
|
|
173
|
+
out << "| Property | Value |\n"
|
|
174
|
+
out << "|----------|-------|\n"
|
|
175
|
+
out << "| TID | #{tid} |\n"
|
|
176
|
+
out << "| Main Thread | #{is_main} |\n"
|
|
177
|
+
out << "| Samples | #{sample_count} |\n"
|
|
178
|
+
out << "| Total Weight | #{weight_sum} |\n"
|
|
179
|
+
out << "\n"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
out
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def build_files
|
|
186
|
+
out = +"## Hot Files\n\n"
|
|
187
|
+
|
|
188
|
+
thread = main_thread
|
|
189
|
+
return out << "_No file information available._\n\n" unless thread
|
|
190
|
+
|
|
191
|
+
stack_table = get_stack_table(thread)
|
|
192
|
+
samples = thread[:samples] || []
|
|
193
|
+
weights = thread[:weights] || []
|
|
194
|
+
|
|
195
|
+
return out << "_No samples collected._\n\n" if samples.empty?
|
|
196
|
+
|
|
197
|
+
total = weights.sum
|
|
198
|
+
return out << "_No samples collected._\n\n" if total == 0
|
|
199
|
+
|
|
200
|
+
# Build samples by file and line (similar to FileListing)
|
|
201
|
+
samples_by_frame = Hash.new { |h, k| h[k] = { self: 0, total: 0 } }
|
|
202
|
+
|
|
203
|
+
samples.zip(weights).each do |stack_idx, weight|
|
|
204
|
+
# Self time: top frame only
|
|
205
|
+
top_frame_idx = stack_table.stack_frame_idx(stack_idx)
|
|
206
|
+
samples_by_frame[top_frame_idx][:self] += weight
|
|
207
|
+
|
|
208
|
+
# Total time: walk up the stack
|
|
209
|
+
current_stack_idx = stack_idx
|
|
210
|
+
while current_stack_idx
|
|
211
|
+
frame_idx = stack_table.stack_frame_idx(current_stack_idx)
|
|
212
|
+
samples_by_frame[frame_idx][:total] += weight
|
|
213
|
+
current_stack_idx = stack_table.stack_parent_idx(current_stack_idx)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Group by file
|
|
218
|
+
samples_by_file = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = { self: 0, total: 0 } } }
|
|
219
|
+
|
|
220
|
+
samples_by_frame.each do |frame_idx, data|
|
|
221
|
+
func_idx = stack_table.frame_func_idx(frame_idx)
|
|
222
|
+
filename = stack_table.func_filename(func_idx)
|
|
223
|
+
line = stack_table.frame_line_no(frame_idx)
|
|
224
|
+
|
|
225
|
+
filename = filter_filename(filename)
|
|
226
|
+
samples_by_file[filename][line][:self] += data[:self]
|
|
227
|
+
samples_by_file[filename][line][:total] += data[:total]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Filter to relevant files (>1% self time, exclude gem/rubylib/<)
|
|
231
|
+
relevant_files = samples_by_file.select do |filename, lines|
|
|
232
|
+
next false if filename.start_with?("gem:")
|
|
233
|
+
next false if filename.start_with?("rubylib:")
|
|
234
|
+
next false if filename.start_with?("<")
|
|
235
|
+
|
|
236
|
+
lines.values.map { |d| d[:self] }.max > total * 0.01
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
if relevant_files.empty?
|
|
240
|
+
return out << "_No significant file hotspots found._\n\n"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Sort files by total weight
|
|
244
|
+
sorted_files = relevant_files.sort_by { |_, lines| -lines.values.map { |d| d[:self] }.sum }
|
|
245
|
+
|
|
246
|
+
sorted_files.each do |filename, lines|
|
|
247
|
+
out << "### #{escape_markdown(filename)}\n\n"
|
|
248
|
+
|
|
249
|
+
# Get top lines by self weight
|
|
250
|
+
sorted_lines = lines.sort_by { |_, d| -d[:self] }.first(@lines_per_file)
|
|
251
|
+
|
|
252
|
+
out << "| Line | Self % | Total % | Code |\n"
|
|
253
|
+
out << "|------|--------|---------|------|\n"
|
|
254
|
+
|
|
255
|
+
sorted_lines.each do |line_no, data|
|
|
256
|
+
self_pct = 100.0 * data[:self] / total
|
|
257
|
+
total_pct = 100.0 * data[:total] / total
|
|
258
|
+
|
|
259
|
+
source_line = read_source_line(filename, line_no)
|
|
260
|
+
code = source_line ? truncate_code(source_line) : "_source unavailable_"
|
|
261
|
+
|
|
262
|
+
out << "| #{line_no} | #{format("%.1f", self_pct)}% | #{format("%.1f", total_pct)}% | #{format_code_span(code)} |\n"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
out << "\n"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
out
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Helper methods
|
|
272
|
+
|
|
273
|
+
def main_thread
|
|
274
|
+
@profile.main_thread
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def get_threads
|
|
278
|
+
if @profile.respond_to?(:threads)
|
|
279
|
+
threads = @profile.threads
|
|
280
|
+
if threads.is_a?(Hash)
|
|
281
|
+
threads
|
|
282
|
+
elsif threads.is_a?(Array)
|
|
283
|
+
# ParsedProfile returns array
|
|
284
|
+
threads.each_with_index.to_h { |t, i| [i, t] }
|
|
285
|
+
else
|
|
286
|
+
{}
|
|
287
|
+
end
|
|
288
|
+
else
|
|
289
|
+
{}
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def get_stack_table(thread)
|
|
294
|
+
if thread.respond_to?(:stack_table)
|
|
295
|
+
thread.stack_table
|
|
296
|
+
else
|
|
297
|
+
@profile._stack_table
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def get_thread_name(thread)
|
|
302
|
+
if thread.is_a?(Hash)
|
|
303
|
+
thread[:name]
|
|
304
|
+
elsif thread.respond_to?(:data)
|
|
305
|
+
thread.data["name"]
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def get_thread_main(thread)
|
|
310
|
+
if thread.is_a?(Hash)
|
|
311
|
+
thread[:is_main]
|
|
312
|
+
elsif thread.respond_to?(:main_thread?)
|
|
313
|
+
thread.main_thread?
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def get_thread_tid(thread)
|
|
318
|
+
if thread.is_a?(Hash)
|
|
319
|
+
thread[:tid]
|
|
320
|
+
elsif thread.respond_to?(:data)
|
|
321
|
+
thread.data["tid"]
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def total_samples
|
|
326
|
+
if @profile.respond_to?(:total_samples)
|
|
327
|
+
@profile.total_samples
|
|
328
|
+
else
|
|
329
|
+
main = main_thread
|
|
330
|
+
main ? (main[:samples]&.size || 0) : 0
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def thread_count
|
|
335
|
+
threads = get_threads
|
|
336
|
+
threads.is_a?(Hash) ? threads.size : threads.length
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def filter_filename(filename)
|
|
340
|
+
@filename_filter ||= if live_profile?
|
|
341
|
+
FilenameFilter.new
|
|
342
|
+
else
|
|
343
|
+
->(x) { x }
|
|
344
|
+
end
|
|
345
|
+
@filename_filter.call(filename)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def live_profile?
|
|
349
|
+
thread = main_thread
|
|
350
|
+
thread.is_a?(Hash)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def format_location(file, line)
|
|
354
|
+
return "unknown" unless file
|
|
355
|
+
filtered = filter_filename(file)
|
|
356
|
+
line ? "#{filtered}:#{line}" : filtered
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def read_source_line(filename, line_no)
|
|
360
|
+
return nil unless line_no && line_no > 0
|
|
361
|
+
|
|
362
|
+
# For filtered filenames, try to resolve back to real path
|
|
363
|
+
real_path = resolve_filename(filename)
|
|
364
|
+
return nil unless real_path && File.exist?(real_path)
|
|
365
|
+
|
|
366
|
+
lines = File.readlines(real_path)
|
|
367
|
+
lines[line_no - 1]&.chomp
|
|
368
|
+
rescue
|
|
369
|
+
nil
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def resolve_filename(filename)
|
|
373
|
+
return nil if filename.start_with?("gem:", "rubylib:", "<")
|
|
374
|
+
return filename if File.exist?(filename)
|
|
375
|
+
|
|
376
|
+
# Try expanding relative to cwd
|
|
377
|
+
expanded = File.expand_path(filename)
|
|
378
|
+
return expanded if File.exist?(expanded)
|
|
379
|
+
|
|
380
|
+
nil
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def truncate_code(code, max_length: 60)
|
|
384
|
+
code = code.strip
|
|
385
|
+
if code.length > max_length
|
|
386
|
+
code[0, max_length - 3] + "..."
|
|
387
|
+
else
|
|
388
|
+
code
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def format_code_span(text)
|
|
393
|
+
content = text.to_s.gsub("|", "\\|")
|
|
394
|
+
|
|
395
|
+
# Find longest run of consecutive backticks in content
|
|
396
|
+
max_run = content.scan(/`+/).map(&:length).max || 0
|
|
397
|
+
delimiter = "`" * (max_run + 1)
|
|
398
|
+
|
|
399
|
+
if delimiter.length > 1
|
|
400
|
+
"#{delimiter} #{content} #{delimiter}"
|
|
401
|
+
else
|
|
402
|
+
"#{delimiter}#{content}#{delimiter}"
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def escape_markdown(text)
|
|
407
|
+
return "" unless text
|
|
408
|
+
text.to_s
|
|
409
|
+
.gsub("&", "&")
|
|
410
|
+
.gsub("<", "<")
|
|
411
|
+
.gsub(">", ">")
|
|
412
|
+
.gsub(/([|`*_\[\]])/, '\\\\\1')
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
@@ -29,7 +29,7 @@ module Vernier
|
|
|
29
29
|
@frame_lines = thread_data["frameTable"]["line"]
|
|
30
30
|
@func_names = thread_data["funcTable"]["name"]
|
|
31
31
|
@func_filenames = thread_data["funcTable"]["fileName"]
|
|
32
|
-
|
|
32
|
+
@func_first_linenos = thread_data["funcTable"]["lineNumber"]
|
|
33
33
|
@strings = thread_data["stringArray"]
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -49,7 +49,7 @@ module Vernier
|
|
|
49
49
|
def func_filename_idx(idx) = @func_filenames[idx]
|
|
50
50
|
def func_name(idx) = @strings[func_name_idx(idx)]
|
|
51
51
|
def func_filename(idx) = @strings[func_filename_idx(idx)]
|
|
52
|
-
def func_first_lineno(idx) = @
|
|
52
|
+
def func_first_lineno(idx) = @func_first_linenos[idx]
|
|
53
53
|
|
|
54
54
|
include StackTableHelpers
|
|
55
55
|
end
|
data/lib/vernier/result.rb
CHANGED
|
@@ -30,6 +30,10 @@ module Vernier
|
|
|
30
30
|
Output::Cpuprofile.new(self).output
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def to_markdown(top_n: 20, lines_per_file: 5)
|
|
34
|
+
Output::Markdown.new(self, top_n: top_n, lines_per_file: lines_per_file).output
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
def write(out:, format: "firefox")
|
|
34
38
|
case format
|
|
35
39
|
when "cpuprofile"
|
|
@@ -38,6 +42,12 @@ module Vernier
|
|
|
38
42
|
else
|
|
39
43
|
File.binwrite(out, to_cpuprofile)
|
|
40
44
|
end
|
|
45
|
+
when "markdown", "md"
|
|
46
|
+
if out.respond_to?(:write)
|
|
47
|
+
out.write(to_markdown)
|
|
48
|
+
else
|
|
49
|
+
File.binwrite(out, to_markdown)
|
|
50
|
+
end
|
|
41
51
|
when "firefox", nil
|
|
42
52
|
if out.respond_to?(:write)
|
|
43
53
|
out.write(to_firefox)
|
data/lib/vernier/version.rb
CHANGED
data/lib/vernier.rb
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
3
|
+
require "vernier/version"
|
|
4
|
+
require "vernier/collector"
|
|
5
|
+
require "vernier/stack_table"
|
|
6
|
+
require "vernier/heap_tracker"
|
|
7
|
+
require "vernier/memory_leak_detector"
|
|
8
|
+
require "vernier/parsed_profile"
|
|
9
|
+
require "vernier/result"
|
|
10
|
+
require "vernier/hooks"
|
|
11
|
+
require "vernier/output/firefox"
|
|
12
|
+
require "vernier/output/cpuprofile"
|
|
13
|
+
require "vernier/output/top"
|
|
14
|
+
require "vernier/output/file_listing"
|
|
15
|
+
require "vernier/output/filename_filter"
|
|
16
|
+
require "vernier/output/markdown"
|
|
17
|
+
require "vernier/vernier"
|
|
17
18
|
|
|
18
19
|
module Vernier
|
|
19
20
|
class Error < StandardError; end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vernier
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Hawthorn
|
|
@@ -104,6 +104,7 @@ files:
|
|
|
104
104
|
- lib/vernier/output/file_listing.rb
|
|
105
105
|
- lib/vernier/output/filename_filter.rb
|
|
106
106
|
- lib/vernier/output/firefox.rb
|
|
107
|
+
- lib/vernier/output/markdown.rb
|
|
107
108
|
- lib/vernier/output/top.rb
|
|
108
109
|
- lib/vernier/parsed_profile.rb
|
|
109
110
|
- lib/vernier/result.rb
|
|
@@ -133,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
133
134
|
- !ruby/object:Gem::Version
|
|
134
135
|
version: '0'
|
|
135
136
|
requirements: []
|
|
136
|
-
rubygems_version:
|
|
137
|
+
rubygems_version: 4.0.3
|
|
137
138
|
specification_version: 4
|
|
138
139
|
summary: A next generation CRuby profiler
|
|
139
140
|
test_files: []
|