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