pf2 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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