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