pf2 0.5.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/Cargo.lock +7 -27
  4. data/README.md +1 -1
  5. data/crates/backtrace-sys2/build.rs +1 -4
  6. data/crates/backtrace-sys2/src/libbacktrace/Makefile.am +116 -31
  7. data/crates/backtrace-sys2/src/libbacktrace/Makefile.in +295 -141
  8. data/crates/backtrace-sys2/src/libbacktrace/README.md +11 -1
  9. data/crates/backtrace-sys2/src/libbacktrace/alloc.c +1 -1
  10. data/crates/backtrace-sys2/src/libbacktrace/allocfail.c +1 -1
  11. data/crates/backtrace-sys2/src/libbacktrace/allocfail.sh +1 -1
  12. data/crates/backtrace-sys2/src/libbacktrace/atomic.c +1 -1
  13. data/crates/backtrace-sys2/src/libbacktrace/backtrace-supported.h.in +1 -1
  14. data/crates/backtrace-sys2/src/libbacktrace/backtrace.c +1 -1
  15. data/crates/backtrace-sys2/src/libbacktrace/backtrace.h +12 -12
  16. data/crates/backtrace-sys2/src/libbacktrace/btest.c +24 -8
  17. data/crates/backtrace-sys2/src/libbacktrace/config/libtool.m4 +162 -53
  18. data/crates/backtrace-sys2/src/libbacktrace/config.h.in +3 -0
  19. data/crates/backtrace-sys2/src/libbacktrace/configure +255 -66
  20. data/crates/backtrace-sys2/src/libbacktrace/configure.ac +27 -8
  21. data/crates/backtrace-sys2/src/libbacktrace/dwarf.c +37 -30
  22. data/crates/backtrace-sys2/src/libbacktrace/edtest.c +2 -2
  23. data/crates/backtrace-sys2/src/libbacktrace/edtest2.c +1 -1
  24. data/crates/backtrace-sys2/src/libbacktrace/elf.c +98 -76
  25. data/crates/backtrace-sys2/src/libbacktrace/fileline.c +1 -1
  26. data/crates/backtrace-sys2/src/libbacktrace/install-debuginfo-for-buildid.sh.in +2 -2
  27. data/crates/backtrace-sys2/src/libbacktrace/instrumented_alloc.c +1 -1
  28. data/crates/backtrace-sys2/src/libbacktrace/internal.h +41 -2
  29. data/crates/backtrace-sys2/src/libbacktrace/macho.c +25 -19
  30. data/crates/backtrace-sys2/src/libbacktrace/mmap.c +1 -1
  31. data/crates/backtrace-sys2/src/libbacktrace/mmapio.c +1 -1
  32. data/crates/backtrace-sys2/src/libbacktrace/mtest.c +4 -4
  33. data/crates/backtrace-sys2/src/libbacktrace/nounwind.c +1 -1
  34. data/crates/backtrace-sys2/src/libbacktrace/pecoff.c +192 -26
  35. data/crates/backtrace-sys2/src/libbacktrace/posix.c +1 -1
  36. data/crates/backtrace-sys2/src/libbacktrace/print.c +41 -16
  37. data/crates/backtrace-sys2/src/libbacktrace/read.c +1 -1
  38. data/crates/backtrace-sys2/src/libbacktrace/simple.c +1 -1
  39. data/crates/backtrace-sys2/src/libbacktrace/sort.c +1 -1
  40. data/crates/backtrace-sys2/src/libbacktrace/state.c +1 -1
  41. data/crates/backtrace-sys2/src/libbacktrace/stest.c +1 -1
  42. data/crates/backtrace-sys2/src/libbacktrace/test_format.c +1 -1
  43. data/crates/backtrace-sys2/src/libbacktrace/testlib.c +1 -1
  44. data/crates/backtrace-sys2/src/libbacktrace/testlib.h +1 -1
  45. data/crates/backtrace-sys2/src/libbacktrace/ttest.c +1 -1
  46. data/crates/backtrace-sys2/src/libbacktrace/unittest.c +1 -1
  47. data/crates/backtrace-sys2/src/libbacktrace/unknown.c +1 -1
  48. data/crates/backtrace-sys2/src/libbacktrace/xcoff.c +43 -32
  49. data/crates/backtrace-sys2/src/libbacktrace/xztest.c +2 -2
  50. data/crates/backtrace-sys2/src/libbacktrace/zstdtest.c +1 -1
  51. data/crates/backtrace-sys2/src/libbacktrace/ztest.c +1 -1
  52. data/ext/pf2/Cargo.toml +1 -1
  53. data/ext/pf2/src/lib.rs +1 -0
  54. data/ext/pf2/src/profile.rs +7 -3
  55. data/ext/pf2/src/profile_serializer.rs +6 -13
  56. data/ext/pf2/src/ringbuffer.rs +1 -3
  57. data/ext/pf2/src/ruby_init.rs +1 -4
  58. data/ext/pf2/src/sample.rs +1 -0
  59. data/ext/pf2/src/serialization/profile.rs +47 -0
  60. data/ext/pf2/src/serialization/serializer.rs +325 -0
  61. data/ext/pf2/src/serialization.rs +2 -0
  62. data/ext/pf2/src/session/configuration.rs +2 -1
  63. data/ext/pf2/src/session/new_thread_watcher.rs +1 -1
  64. data/ext/pf2/src/session/ruby_object.rs +1 -5
  65. data/ext/pf2/src/session.rs +20 -19
  66. data/ext/pf2/src/signal_scheduler.rs +12 -7
  67. data/ext/pf2/src/timer_thread_scheduler.rs +11 -3
  68. data/lib/pf2/cli.rb +3 -1
  69. data/lib/pf2/reporter/firefox_profiler.rb +397 -0
  70. data/lib/pf2/reporter/stack_weaver.rb +81 -0
  71. data/lib/pf2/reporter.rb +3 -392
  72. data/lib/pf2/serve.rb +3 -1
  73. data/lib/pf2/session.rb +2 -0
  74. data/lib/pf2/version.rb +3 -1
  75. data/lib/pf2.rb +4 -1
  76. data/rustfmt.toml +1 -0
  77. metadata +13 -12
  78. data/crates/backtrace-sys2/src/libbacktrace/libtool.m4 +0 -7436
  79. data/crates/backtrace-sys2/src/libbacktrace/ltoptions.m4 +0 -369
  80. data/crates/backtrace-sys2/src/libbacktrace/ltsugar.m4 +0 -123
  81. data/crates/backtrace-sys2/src/libbacktrace/ltversion.m4 +0 -23
  82. data/crates/backtrace-sys2/src/libbacktrace/lt~obsolete.m4 +0 -98
@@ -0,0 +1,397 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Pf2
6
+ module Reporter
7
+ # Generates Firefox Profiler's "processed profile format"
8
+ # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md
9
+ class FirefoxProfiler
10
+ def initialize(profile)
11
+ @profile = FirefoxProfiler.deep_intize_keys(profile)
12
+ end
13
+
14
+ def inspect
15
+ "" # TODO: provide something better
16
+ end
17
+
18
+ def emit
19
+ report = {
20
+ meta: {
21
+ interval: 10, # ms; TODO: replace with actual interval
22
+ start_time: 0,
23
+ process_type: 0,
24
+ product: 'ruby',
25
+ stackwalk: 0,
26
+ version: 28,
27
+ preprocessed_profile_version: 47,
28
+ symbolicated: true,
29
+ categories: [
30
+ {
31
+ name: "Logs",
32
+ color: "grey",
33
+ subcategories: ["Unused"],
34
+ },
35
+ {
36
+ name: "Ruby",
37
+ color: "red",
38
+ subcategories: ["Code"],
39
+ },
40
+ {
41
+ name: "Native",
42
+ color: "blue",
43
+ subcategories: ["Code"],
44
+ },
45
+ {
46
+ name: "Native",
47
+ color: "lightblue",
48
+ subcategories: ["Code"],
49
+ },
50
+ ],
51
+ marker_schema: [],
52
+ },
53
+ libs: [],
54
+ counters: [],
55
+ threads: @profile[:threads].values.map {|th| ThreadReport.new(th).emit }
56
+ }
57
+ FirefoxProfiler.deep_camelize_keys(report)
58
+ end
59
+
60
+ class ThreadReport
61
+ def initialize(thread)
62
+ @thread = thread
63
+
64
+ # Populated in other methods
65
+ @func_id_map = {}
66
+ @frame_id_map = {}
67
+ @stack_tree_id_map = {}
68
+
69
+ @string_table = {}
70
+ end
71
+
72
+ def inspect
73
+ "" # TODO: provide something better
74
+ end
75
+
76
+ def emit
77
+ x = weave_native_stack(@thread[:stack_tree])
78
+ @thread[:stack_tree] = x
79
+ func_table = build_func_table
80
+ frame_table = build_frame_table
81
+ stack_table = build_stack_table(func_table, frame_table)
82
+ samples = build_samples
83
+
84
+ string_table = build_string_table
85
+
86
+ {
87
+ process_type: 'default',
88
+ process_name: 'ruby',
89
+ process_startup_time: 0,
90
+ process_shutdown_time: nil,
91
+ register_time: 0,
92
+ unregister_time: nil,
93
+ paused_ranges: [],
94
+ name: "Thread (tid: #{@thread[:thread_id]})",
95
+ is_main_thread: true,
96
+ is_js_tracer: true,
97
+ # FIXME: We can fill the correct PID only after we correctly fill is_main_thread
98
+ # (only one thread could be marked as is_main_thread in a single process)
99
+ pid: @thread[:thread_id],
100
+ tid: @thread[:thread_id],
101
+ samples: samples,
102
+ markers: markers,
103
+ stack_table: stack_table,
104
+ frame_table: frame_table,
105
+ string_array: build_string_table,
106
+ func_table: func_table,
107
+ resource_table: {
108
+ lib: [],
109
+ name: [],
110
+ host: [],
111
+ type: [],
112
+ length: 0,
113
+ },
114
+ native_symbols: [],
115
+ }
116
+ end
117
+
118
+ def build_samples
119
+ ret = {
120
+ event_delay: [],
121
+ stack: [],
122
+ time: [],
123
+ duration: [],
124
+ # weight: nil,
125
+ # weight_type: 'samples',
126
+ }
127
+
128
+ @thread[:samples].each do |sample|
129
+ ret[:stack] << @stack_tree_id_map[sample[:stack_tree_id]]
130
+ ret[:time] << sample[:elapsed_ns] / 1000000 # ns -> ms
131
+ ret[:duration] << 1
132
+ ret[:event_delay] << 0
133
+ end
134
+
135
+ ret[:length] = ret[:stack].length
136
+ ret
137
+ end
138
+
139
+ def build_frame_table
140
+ ret = {
141
+ address: [],
142
+ category: [],
143
+ subcategory: [],
144
+ func: [],
145
+ inner_window_id: [],
146
+ implementation: [],
147
+ line: [],
148
+ column: [],
149
+ optimizations: [],
150
+ inline_depth: [],
151
+ native_symbol: [],
152
+ }
153
+
154
+ @thread[:frames].each.with_index do |(id, frame), i|
155
+ ret[:address] << frame[:address].to_s
156
+ ret[:category] << 1
157
+ ret[:subcategory] << 1
158
+ ret[:func] << i # TODO
159
+ ret[:inner_window_id] << nil
160
+ ret[:implementation] << nil
161
+ ret[:line] << frame[:callsite_lineno]
162
+ ret[:column] << nil
163
+ ret[:optimizations] << nil
164
+ ret[:inline_depth] << 0
165
+ ret[:native_symbol] << nil
166
+
167
+ @frame_id_map[id] = i
168
+ end
169
+
170
+ ret[:length] = ret[:address].length
171
+ ret
172
+ end
173
+
174
+ def build_func_table
175
+ ret = {
176
+ name: [],
177
+ is_js: [],
178
+ relevant_for_js: [],
179
+ resource: [],
180
+ file_name: [],
181
+ line_number: [],
182
+ column_number: [],
183
+ }
184
+
185
+ @thread[:frames].each.with_index do |(id, frame), i|
186
+ native = (frame[:entry_type] == 'Native')
187
+ label = "#{native ? "Native: " : ""}#{frame[:full_label]}"
188
+ ret[:name] << string_id(label)
189
+ ret[:is_js] << !native
190
+ ret[:relevant_for_js] << false
191
+ ret[:resource] << -1
192
+ ret[:file_name] << string_id(frame[:file_name])
193
+ ret[:line_number] << frame[:function_first_lineno]
194
+ ret[:column_number] << nil
195
+
196
+ @func_id_map[id] = i
197
+ end
198
+
199
+ ret[:length] = ret[:name].length
200
+ ret
201
+ end
202
+
203
+ # "Weave" the native stack into the Ruby stack.
204
+ #
205
+ # Strategy:
206
+ # - Split the stack into Ruby and Native parts
207
+ # - Start from the root of the Native stack
208
+ # - Dig in to the native stack until we hit a rb_vm_exec(), which marks a call into Ruby code
209
+ # - Switch to Ruby stack. Keep digging until we hit a Cfunc call, then switch back to Native stack
210
+ # - Repeat until we consume the entire stack
211
+ def weave_native_stack(stack_tree)
212
+ collected_paths = []
213
+ tree_to_array_of_paths(stack_tree, @thread[:frames], [], collected_paths)
214
+ collected_paths = collected_paths.map do |path|
215
+ next if path.size == 0
216
+
217
+ new_path = []
218
+ new_path << path.shift # root
219
+
220
+ # Split the stack into Ruby and Native parts
221
+ native_path, ruby_path = path.partition do |frame|
222
+ frame_id = frame[:frame_id]
223
+ @thread[:frames][frame_id][:entry_type] == 'Native'
224
+ end
225
+
226
+ mode = :native
227
+
228
+ loop do
229
+ break if ruby_path.size == 0 && native_path.size == 0
230
+
231
+ case mode
232
+ when :ruby
233
+ if ruby_path.size == 0
234
+ mode = :native
235
+ next
236
+ end
237
+
238
+ next_node = ruby_path[0]
239
+ new_path << ruby_path.shift
240
+ next_node_frame = @thread[:frames][next_node[:frame_id]]
241
+ if native_path.size > 0
242
+ # Search the remainder of the native stack for the same address
243
+ # Note: This isn't a very efficient way for the job... but it still works
244
+ ruby_addr = next_node_frame[:address]
245
+ native_path[0..].each do |native_node|
246
+ native_addr = @thread[:frames][native_node[:frame_id]][:address]
247
+ if ruby_addr && native_addr && ruby_addr == native_addr
248
+ # A match has been found. Switch to native mode
249
+ mode = :native
250
+ break
251
+ end
252
+ end
253
+ end
254
+ when :native
255
+ if native_path.size == 0
256
+ mode = :ruby
257
+ next
258
+ end
259
+
260
+ # Dig until we meet a rb_vm_exec
261
+ next_node = native_path[0]
262
+ new_path << native_path.shift
263
+ if @thread[:frames][next_node[:frame_id]][:full_label] =~ /vm_exec_core/ # VM_EXEC in vm_exec.h
264
+ mode = :ruby
265
+ end
266
+ end
267
+ end
268
+
269
+ new_path
270
+ end
271
+
272
+ # reconstruct stack_tree
273
+ new_stack_tree = array_of_paths_to_tree(collected_paths)
274
+ new_stack_tree
275
+ end
276
+
277
+ def tree_to_array_of_paths(stack_tree, frames, path, collected_paths)
278
+ new_path = path + [{ frame_id: stack_tree[:frame_id], node_id: stack_tree[:node_id] }]
279
+ if stack_tree[:children].empty?
280
+ collected_paths << new_path
281
+ else
282
+ stack_tree[:children].each do |frame_id, child|
283
+ tree_to_array_of_paths(child, frames, new_path, collected_paths)
284
+ end
285
+ end
286
+ end
287
+
288
+ def array_of_paths_to_tree(paths)
289
+ new_stack_tree = { children: {}, node_id: 0, frame_id: 0 }
290
+ paths.each do |path|
291
+ current = new_stack_tree
292
+ path[1..].each do |frame|
293
+ frame_id = frame[:frame_id]
294
+ node_id = frame[:node_id]
295
+ current[:children][frame_id] ||= { children: {}, node_id: node_id, frame_id: frame_id }
296
+ current = current[:children][frame_id]
297
+ end
298
+ end
299
+ new_stack_tree
300
+ end
301
+
302
+ def build_stack_table(func_table, frame_table)
303
+ ret = {
304
+ frame: [],
305
+ category: [],
306
+ subcategory: [],
307
+ prefix: [],
308
+ }
309
+
310
+ queue = []
311
+
312
+ @thread[:stack_tree][:children].each {|_, c| queue << [nil, c] }
313
+
314
+ loop do
315
+ break if queue.size == 0
316
+
317
+ prefix, node = queue.shift
318
+ ret[:frame] << @frame_id_map[node[:frame_id]]
319
+ ret[:category] << (build_string_table[func_table[:name][frame_table[:func][@frame_id_map[node[:frame_id]]]]].start_with?('Native:') ? 2 : 1)
320
+ ret[:subcategory] << nil
321
+ ret[:prefix] << prefix
322
+
323
+ # The index of this frame - children can refer to this frame using this index as prefix
324
+ frame_index = ret[:frame].length - 1
325
+ @stack_tree_id_map[node[:node_id]] = frame_index
326
+
327
+ # Enqueue children nodes
328
+ node[:children].each {|_, c| queue << [frame_index, c] }
329
+ end
330
+
331
+ ret[:length] = ret[:frame].length
332
+ ret
333
+ end
334
+
335
+ def build_string_table
336
+ @string_table.sort_by {|_, v| v}.map {|s| s[0] }
337
+ end
338
+
339
+ def string_id(str)
340
+ return @string_table[str] if @string_table.has_key?(str)
341
+ @string_table[str] = @string_table.length
342
+ @string_table[str]
343
+ end
344
+
345
+ def markers
346
+ {
347
+ data: [],
348
+ name: [],
349
+ time: [],
350
+ start_time: [],
351
+ end_time: [],
352
+ phase: [],
353
+ category: [],
354
+ length: 0
355
+ }
356
+ end
357
+ end
358
+
359
+ # Util functions
360
+ class << self
361
+ def snake_to_camel(s)
362
+ return "isJS" if s == "is_js"
363
+ return "relevantForJS" if s == "relevant_for_js"
364
+ return "innerWindowID" if s == "inner_window_id"
365
+ s.split('_').inject([]) {|buffer, p| buffer.push(buffer.size == 0 ? p : p.capitalize) }.join
366
+ end
367
+
368
+ def deep_transform_keys(value, &block)
369
+ case value
370
+ when Array
371
+ value.map {|v| deep_transform_keys(v, &block) }
372
+ when Hash
373
+ Hash[value.map {|k, v| [yield(k), deep_transform_keys(v, &block)] }]
374
+ else
375
+ value
376
+ end
377
+ end
378
+
379
+ def deep_camelize_keys(value)
380
+ deep_transform_keys(value) do |key|
381
+ snake_to_camel(key.to_s).to_sym
382
+ end
383
+ end
384
+
385
+ def deep_intize_keys(value)
386
+ deep_transform_keys(value) do |key|
387
+ if key.to_s.to_i.to_s == key.to_s
388
+ key.to_s.to_i
389
+ else
390
+ key
391
+ end
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pf2
4
+ module Reporter
5
+ class StackWeaver
6
+ def initialize(profile)
7
+ @profile = profile
8
+ end
9
+
10
+ def weave(ruby_stack, native_stack)
11
+ ruby_stack = ruby_stack.dup
12
+ native_stack = native_stack.dup
13
+
14
+ weaved_stack = []
15
+
16
+ current_stack = :native
17
+ loop do
18
+ break if ruby_stack.size == 0 && native_stack.size == 0
19
+ case current_stack
20
+ when :ruby
21
+ if ruby_stack.size == 0 # We've reached the end of the Ruby stack
22
+ current_stack = :native
23
+ next
24
+ end
25
+
26
+ location_index = ruby_stack.pop
27
+ weaved_stack.unshift(location_index)
28
+
29
+ current_stack = :native if should_switch_to_native?(location_index, native_stack.dup)
30
+
31
+ when :native
32
+ if native_stack.size == 0 # We've reached the end of the native stack
33
+ current_stack = :ruby
34
+ next
35
+ end
36
+
37
+ location_index = native_stack.pop
38
+ weaved_stack.unshift(location_index)
39
+
40
+ current_stack = :ruby if should_switch_to_ruby?(location_index)
41
+ end
42
+ end
43
+
44
+ weaved_stack
45
+ end
46
+
47
+ # @param [Integer] location_index
48
+ # @param [Array<Integer>] native_stack_remainder
49
+ def should_switch_to_native?(location_index, native_stack_remainder)
50
+ location = @profile[:locations][location_index]
51
+ function = @profile[:functions][location[:function_index]]
52
+ raise if function[:implementation] != :ruby # assert
53
+
54
+ # Is the current Ruby function a cfunc?
55
+ return false if function[:start_address] == nil
56
+
57
+ # Does a corresponding native function exist in the remainder of the native stack?
58
+ loop do
59
+ break if native_stack_remainder.size == 0
60
+ n_location_index = native_stack_remainder.pop
61
+ n_location = @profile[:locations][n_location_index]
62
+ n_function = @profile[:functions][n_location[:function_index]]
63
+
64
+ return true if function[:start_address] == n_function[:start_address]
65
+ end
66
+
67
+ false
68
+ end
69
+
70
+ def should_switch_to_ruby?(location_index)
71
+ location = @profile[:locations][location_index]
72
+ function = @profile[:functions][location[:function_index]]
73
+ raise if function[:implementation] != :native # assert
74
+
75
+ # If the next function is a vm_exec_core() (= VM_EXEC in vm_exec.h),
76
+ # we switch to the Ruby stack.
77
+ function[:name] == 'vm_exec_core'
78
+ end
79
+ end
80
+ end
81
+ end