pf2 0.1.0 → 0.2.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,242 @@
1
+ #![deny(unsafe_op_in_unsafe_fn)]
2
+
3
+ mod configuration;
4
+ mod timer_installer;
5
+
6
+ use self::configuration::{Configuration, TimeMode};
7
+ use self::timer_installer::TimerInstaller;
8
+ use crate::profile::Profile;
9
+ use crate::profile_serializer::ProfileSerializer;
10
+ use crate::sample::Sample;
11
+
12
+ use core::panic;
13
+ use std::collections::HashSet;
14
+ use std::ffi::{c_int, c_void, CString};
15
+ use std::mem::ManuallyDrop;
16
+ use std::sync::{Arc, RwLock};
17
+ use std::thread;
18
+ use std::time::Duration;
19
+ use std::{mem, ptr::null_mut};
20
+
21
+ use rb_sys::*;
22
+
23
+ use crate::util::*;
24
+
25
+ #[derive(Debug)]
26
+ pub struct SignalScheduler {
27
+ configuration: configuration::Configuration,
28
+ profile: Option<Arc<RwLock<Profile>>>,
29
+ }
30
+
31
+ pub struct SignalHandlerArgs {
32
+ profile: Arc<RwLock<Profile>>,
33
+ context_ruby_thread: VALUE,
34
+ }
35
+
36
+ impl SignalScheduler {
37
+ fn new() -> Self {
38
+ Self {
39
+ configuration: Configuration {
40
+ time_mode: TimeMode::CpuTime,
41
+ },
42
+ profile: None,
43
+ }
44
+ }
45
+
46
+ fn start(
47
+ &mut self,
48
+ _rbself: VALUE,
49
+ ruby_threads_rary: VALUE,
50
+ track_new_threads: VALUE,
51
+ ) -> VALUE {
52
+ let track_new_threads = RTEST(track_new_threads);
53
+
54
+ let profile = Arc::new(RwLock::new(Profile::new()));
55
+ self.start_profile_buffer_flusher_thread(&profile);
56
+ self.install_signal_handler();
57
+
58
+ let mut target_ruby_threads = HashSet::new();
59
+ unsafe {
60
+ for i in 0..RARRAY_LEN(ruby_threads_rary) {
61
+ let ruby_thread: VALUE = rb_ary_entry(ruby_threads_rary, i);
62
+ target_ruby_threads.insert(ruby_thread);
63
+ }
64
+ }
65
+ TimerInstaller::install_timer_to_ruby_threads(
66
+ self.configuration.clone(),
67
+ &target_ruby_threads,
68
+ Arc::clone(&profile),
69
+ track_new_threads,
70
+ );
71
+
72
+ self.profile = Some(profile);
73
+
74
+ Qtrue.into()
75
+ }
76
+
77
+ fn stop(&mut self, _rbself: VALUE) -> VALUE {
78
+ if let Some(profile) = &self.profile {
79
+ // Finalize
80
+ match profile.try_write() {
81
+ Ok(mut profile) => {
82
+ profile.flush_temporary_sample_buffer();
83
+ }
84
+ Err(_) => {
85
+ println!("[pf2 ERROR] stop: Failed to acquire profile lock.");
86
+ return Qfalse.into();
87
+ }
88
+ }
89
+
90
+ let profile = profile.try_read().unwrap();
91
+ log::debug!("Number of samples: {}", profile.samples.len());
92
+
93
+ let serialized = ProfileSerializer::serialize(&profile);
94
+ let serialized = CString::new(serialized).unwrap();
95
+ unsafe { rb_str_new_cstr(serialized.as_ptr()) }
96
+ } else {
97
+ panic!("stop() called before start()");
98
+ }
99
+ }
100
+
101
+ // Install signal handler for profiling events to the current process.
102
+ fn install_signal_handler(&self) {
103
+ let mut sa: libc::sigaction = unsafe { mem::zeroed() };
104
+ sa.sa_sigaction = Self::signal_handler as usize;
105
+ sa.sa_flags = libc::SA_SIGINFO;
106
+ let err = unsafe { libc::sigaction(libc::SIGALRM, &sa, null_mut()) };
107
+ if err != 0 {
108
+ panic!("sigaction failed: {}", err);
109
+ }
110
+ }
111
+
112
+ // Respond to the signal and collect a sample.
113
+ // This function is called when a timer fires.
114
+ //
115
+ // Expected to be async-signal-safe, but the current implementation is not.
116
+ extern "C" fn signal_handler(
117
+ _sig: c_int,
118
+ info: *mut libc::siginfo_t,
119
+ _ucontext: *mut libc::ucontext_t,
120
+ ) {
121
+ let args = unsafe {
122
+ let ptr = extract_si_value_sival_ptr(info) as *mut SignalHandlerArgs;
123
+ ManuallyDrop::new(Box::from_raw(ptr))
124
+ };
125
+
126
+ let mut profile = match args.profile.try_write() {
127
+ Ok(profile) => profile,
128
+ Err(_) => {
129
+ // FIXME: Do we want to properly collect GC samples? I don't know yet.
130
+ log::trace!("Failed to acquire profile lock (garbage collection possibly in progress). Dropping sample.");
131
+ return;
132
+ }
133
+ };
134
+
135
+ let sample = Sample::capture(args.context_ruby_thread); // NOT async-signal-safe
136
+ if profile.temporary_sample_buffer.push(sample).is_err() {
137
+ log::debug!("Temporary sample buffer full. Dropping sample.");
138
+ }
139
+ }
140
+
141
+ fn start_profile_buffer_flusher_thread(&self, profile: &Arc<RwLock<Profile>>) {
142
+ let profile = Arc::clone(profile);
143
+ thread::spawn(move || loop {
144
+ log::trace!("Flushing temporary sample buffer");
145
+ match profile.try_write() {
146
+ Ok(mut profile) => {
147
+ profile.flush_temporary_sample_buffer();
148
+ }
149
+ Err(_) => {
150
+ log::debug!("flusher: Failed to acquire profile lock");
151
+ }
152
+ }
153
+ thread::sleep(Duration::from_millis(500));
154
+ });
155
+ }
156
+
157
+ // Ruby Methods
158
+
159
+ pub unsafe extern "C" fn rb_start(
160
+ rbself: VALUE,
161
+ ruby_threads: VALUE,
162
+ track_new_threads: VALUE,
163
+ ) -> VALUE {
164
+ let mut collector = unsafe { Self::get_struct_from(rbself) };
165
+ collector.start(rbself, ruby_threads, track_new_threads)
166
+ }
167
+
168
+ pub unsafe extern "C" fn rb_stop(rbself: VALUE) -> VALUE {
169
+ let mut collector = unsafe { Self::get_struct_from(rbself) };
170
+ collector.stop(rbself)
171
+ }
172
+
173
+ // Functions for TypedData
174
+
175
+ // Extract the SignalScheduler struct from a Ruby object
176
+ unsafe fn get_struct_from(obj: VALUE) -> ManuallyDrop<Box<Self>> {
177
+ unsafe {
178
+ let ptr = rb_check_typeddata(obj, &RBDATA);
179
+ ManuallyDrop::new(Box::from_raw(ptr as *mut SignalScheduler))
180
+ }
181
+ }
182
+
183
+ #[allow(non_snake_case)]
184
+ pub unsafe extern "C" fn rb_alloc(_rbself: VALUE) -> VALUE {
185
+ let collector = Box::new(SignalScheduler::new());
186
+ unsafe { Arc::increment_strong_count(&collector) };
187
+
188
+ unsafe {
189
+ let rb_mPf2: VALUE = rb_define_module(cstr!("Pf2"));
190
+ let rb_cSignalScheduler =
191
+ rb_define_class_under(rb_mPf2, cstr!("SignalScheduler"), rb_cObject);
192
+
193
+ // "Wrap" the SignalScheduler struct into a Ruby object
194
+ rb_data_typed_object_wrap(
195
+ rb_cSignalScheduler,
196
+ Box::into_raw(collector) as *mut c_void,
197
+ &RBDATA,
198
+ )
199
+ }
200
+ }
201
+
202
+ unsafe extern "C" fn dmark(ptr: *mut c_void) {
203
+ unsafe {
204
+ let collector = ManuallyDrop::new(Box::from_raw(ptr as *mut SignalScheduler));
205
+ if let Some(profile) = &collector.profile {
206
+ match profile.read() {
207
+ Ok(profile) => {
208
+ profile.dmark();
209
+ }
210
+ Err(_) => {
211
+ panic!("[pf2 FATAL] dmark: Failed to acquire profile lock.");
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ unsafe extern "C" fn dfree(ptr: *mut c_void) {
219
+ unsafe {
220
+ drop(Box::from_raw(ptr as *mut SignalScheduler));
221
+ }
222
+ }
223
+
224
+ unsafe extern "C" fn dsize(_: *const c_void) -> size_t {
225
+ // FIXME: Report something better
226
+ mem::size_of::<SignalScheduler>() as size_t
227
+ }
228
+ }
229
+
230
+ static mut RBDATA: rb_data_type_t = rb_data_type_t {
231
+ wrap_struct_name: cstr!("SignalScheduler"),
232
+ function: rb_data_type_struct__bindgen_ty_1 {
233
+ dmark: Some(SignalScheduler::dmark),
234
+ dfree: Some(SignalScheduler::dfree),
235
+ dsize: Some(SignalScheduler::dsize),
236
+ dcompact: None,
237
+ reserved: [null_mut(); 1],
238
+ },
239
+ parent: null_mut(),
240
+ data: null_mut(),
241
+ flags: 0,
242
+ };
@@ -0,0 +1,243 @@
1
+ #![deny(unsafe_op_in_unsafe_fn)]
2
+
3
+ use std::ffi::{c_void, CString};
4
+ use std::mem::ManuallyDrop;
5
+ use std::ptr::null_mut;
6
+ use std::sync::atomic::{AtomicBool, Ordering};
7
+ use std::sync::{Arc, RwLock};
8
+ use std::thread;
9
+ use std::time::Duration;
10
+
11
+ use rb_sys::*;
12
+
13
+ use crate::profile::Profile;
14
+ use crate::profile_serializer::ProfileSerializer;
15
+ use crate::sample::Sample;
16
+ use crate::util::*;
17
+
18
+ #[derive(Clone, Debug)]
19
+ pub struct TimerThreadScheduler {
20
+ ruby_threads: Arc<RwLock<Vec<VALUE>>>,
21
+ profile: Option<Arc<RwLock<Profile>>>,
22
+ stop_requested: Arc<AtomicBool>,
23
+ }
24
+
25
+ #[derive(Debug)]
26
+ struct PostponedJobArgs {
27
+ ruby_threads: Arc<RwLock<Vec<VALUE>>>,
28
+ profile: Arc<RwLock<Profile>>,
29
+ }
30
+
31
+ impl TimerThreadScheduler {
32
+ fn new() -> Self {
33
+ TimerThreadScheduler {
34
+ ruby_threads: Arc::new(RwLock::new(vec![])),
35
+ profile: None,
36
+ stop_requested: Arc::new(AtomicBool::new(false)),
37
+ }
38
+ }
39
+
40
+ fn start(&mut self, _rbself: VALUE, ruby_threads: VALUE) -> VALUE {
41
+ // Register threads
42
+ let stored_threads = &mut self.ruby_threads.try_write().unwrap();
43
+ unsafe {
44
+ for i in 0..RARRAY_LEN(ruby_threads) {
45
+ stored_threads.push(rb_ary_entry(ruby_threads, i));
46
+ }
47
+ }
48
+
49
+ // Create Profile
50
+ let profile = Arc::new(RwLock::new(Profile::new()));
51
+ self.start_profile_buffer_flusher_thread(&profile);
52
+
53
+ // Start monitoring thread
54
+ let stop_requested = Arc::clone(&self.stop_requested);
55
+ let postponed_job_args: Box<PostponedJobArgs> = Box::new(PostponedJobArgs {
56
+ ruby_threads: Arc::clone(&self.ruby_threads),
57
+ profile: Arc::clone(&profile),
58
+ });
59
+ let postponed_job_handle: rb_postponed_job_handle_t = unsafe {
60
+ rb_postponed_job_preregister(
61
+ 0,
62
+ Some(Self::postponed_job),
63
+ Box::into_raw(postponed_job_args) as *mut c_void, // FIXME: leak
64
+ )
65
+ };
66
+ thread::spawn(move || Self::thread_main_loop(stop_requested, postponed_job_handle));
67
+
68
+ self.profile = Some(profile);
69
+
70
+ Qtrue.into()
71
+ }
72
+
73
+ fn thread_main_loop(
74
+ stop_requested: Arc<AtomicBool>,
75
+ postponed_job_handle: rb_postponed_job_handle_t,
76
+ ) {
77
+ loop {
78
+ if stop_requested.fetch_and(true, Ordering::Relaxed) {
79
+ break;
80
+ }
81
+ unsafe {
82
+ rb_postponed_job_trigger(postponed_job_handle);
83
+ }
84
+ // sleep for 50 ms
85
+ thread::sleep(Duration::from_millis(50));
86
+ }
87
+ }
88
+
89
+ fn stop(&self, _rbself: VALUE) -> VALUE {
90
+ // Stop the collector thread
91
+ self.stop_requested.store(true, Ordering::Relaxed);
92
+
93
+ if let Some(profile) = &self.profile {
94
+ // Finalize
95
+ match profile.try_write() {
96
+ Ok(mut profile) => {
97
+ profile.flush_temporary_sample_buffer();
98
+ }
99
+ Err(_) => {
100
+ println!("[pf2 ERROR] stop: Failed to acquire profile lock.");
101
+ return Qfalse.into();
102
+ }
103
+ }
104
+
105
+ let profile = profile.try_read().unwrap();
106
+ log::debug!("Number of samples: {}", profile.samples.len());
107
+
108
+ let serialized = ProfileSerializer::serialize(&profile);
109
+ let serialized = CString::new(serialized).unwrap();
110
+ unsafe { rb_str_new_cstr(serialized.as_ptr()) }
111
+ } else {
112
+ panic!("stop() called before start()");
113
+ }
114
+ }
115
+
116
+ unsafe extern "C" fn postponed_job(ptr: *mut c_void) {
117
+ unsafe {
118
+ rb_gc_disable();
119
+ }
120
+ let args = unsafe { ManuallyDrop::new(Box::from_raw(ptr as *mut PostponedJobArgs)) };
121
+
122
+ let mut profile = match args.profile.try_write() {
123
+ Ok(profile) => profile,
124
+ Err(_) => {
125
+ // FIXME: Do we want to properly collect GC samples? I don't know yet.
126
+ log::trace!("Failed to acquire profile lock (garbage collection possibly in progress). Dropping sample.");
127
+ return;
128
+ }
129
+ };
130
+
131
+ // Collect stack information from specified Ruby Threads
132
+ let ruby_threads = args.ruby_threads.try_read().unwrap();
133
+ for ruby_thread in ruby_threads.iter() {
134
+ // Check if the thread is still alive
135
+ if unsafe { rb_funcall(*ruby_thread, rb_intern(cstr!("status")), 0) } == Qfalse as u64 {
136
+ continue;
137
+ }
138
+
139
+ let sample = Sample::capture(*ruby_thread);
140
+ if profile.temporary_sample_buffer.push(sample).is_err() {
141
+ log::debug!("Temporary sample buffer full. Dropping sample.");
142
+ }
143
+ }
144
+ unsafe {
145
+ rb_gc_enable();
146
+ }
147
+ }
148
+
149
+ fn start_profile_buffer_flusher_thread(&self, profile: &Arc<RwLock<Profile>>) {
150
+ let profile = Arc::clone(profile);
151
+ thread::spawn(move || loop {
152
+ log::trace!("Flushing temporary sample buffer");
153
+ match profile.try_write() {
154
+ Ok(mut profile) => {
155
+ profile.flush_temporary_sample_buffer();
156
+ }
157
+ Err(_) => {
158
+ log::debug!("flusher: Failed to acquire profile lock");
159
+ }
160
+ }
161
+ thread::sleep(Duration::from_millis(500));
162
+ });
163
+ }
164
+
165
+ // Ruby Methods
166
+
167
+ // SampleCollector.start
168
+ pub unsafe extern "C" fn rb_start(rbself: VALUE, ruby_threads: VALUE, _: VALUE) -> VALUE {
169
+ let mut collector = Self::get_struct_from(rbself);
170
+ collector.start(rbself, ruby_threads)
171
+ }
172
+
173
+ // SampleCollector.stop
174
+ pub unsafe extern "C" fn rb_stop(rbself: VALUE) -> VALUE {
175
+ let collector = Self::get_struct_from(rbself);
176
+ collector.stop(rbself)
177
+ }
178
+
179
+ // Functions for TypedData
180
+
181
+ fn get_struct_from(obj: VALUE) -> ManuallyDrop<Box<Self>> {
182
+ unsafe {
183
+ let ptr = rb_check_typeddata(obj, &RBDATA);
184
+ ManuallyDrop::new(Box::from_raw(ptr as *mut TimerThreadScheduler))
185
+ }
186
+ }
187
+
188
+ #[allow(non_snake_case)]
189
+ pub unsafe extern "C" fn rb_alloc(_rbself: VALUE) -> VALUE {
190
+ let collector = TimerThreadScheduler::new();
191
+
192
+ unsafe {
193
+ let rb_mPf2: VALUE = rb_define_module(cstr!("Pf2"));
194
+ let rb_cTimerThreadScheduler =
195
+ rb_define_class_under(rb_mPf2, cstr!("TimerThreadScheduler"), rb_cObject);
196
+
197
+ rb_data_typed_object_wrap(
198
+ rb_cTimerThreadScheduler,
199
+ Box::into_raw(Box::new(collector)) as *mut _ as *mut c_void,
200
+ &RBDATA,
201
+ )
202
+ }
203
+ }
204
+
205
+ unsafe extern "C" fn dmark(ptr: *mut c_void) {
206
+ unsafe {
207
+ let collector = ManuallyDrop::new(Box::from_raw(ptr as *mut TimerThreadScheduler));
208
+ if let Some(profile) = &collector.profile {
209
+ match profile.read() {
210
+ Ok(profile) => {
211
+ profile.dmark();
212
+ }
213
+ Err(_) => {
214
+ panic!("[pf2 FATAL] dmark: Failed to acquire profile lock.");
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ unsafe extern "C" fn dfree(ptr: *mut c_void) {
221
+ unsafe {
222
+ drop(Box::from_raw(ptr as *mut TimerThreadScheduler));
223
+ }
224
+ }
225
+ unsafe extern "C" fn dsize(_: *const c_void) -> size_t {
226
+ // FIXME: Report something better
227
+ std::mem::size_of::<TimerThreadScheduler>() as size_t
228
+ }
229
+ }
230
+
231
+ static mut RBDATA: rb_data_type_t = rb_data_type_t {
232
+ wrap_struct_name: cstr!("TimerThreadScheduler"),
233
+ function: rb_data_type_struct__bindgen_ty_1 {
234
+ dmark: Some(TimerThreadScheduler::dmark),
235
+ dfree: Some(TimerThreadScheduler::dfree),
236
+ dsize: Some(TimerThreadScheduler::dsize),
237
+ dcompact: None,
238
+ reserved: [null_mut(); 1],
239
+ },
240
+ parent: null_mut(),
241
+ data: null_mut(),
242
+ flags: 0,
243
+ };
@@ -0,0 +1,30 @@
1
+ use core::mem::transmute;
2
+ use rb_sys::*;
3
+ use std::ffi::c_void;
4
+
5
+ // Convert str literal to C string literal
6
+ macro_rules! cstr {
7
+ ($s:expr) => {
8
+ concat!($s, "\0").as_ptr() as *const std::ffi::c_char
9
+ };
10
+ }
11
+ pub(crate) use cstr;
12
+
13
+ pub type RubyCFunc = unsafe extern "C" fn() -> VALUE;
14
+
15
+ // TODO: rewrite as macro
16
+ pub fn to_ruby_cfunc1<T>(f: unsafe extern "C" fn(T) -> VALUE) -> RubyCFunc {
17
+ unsafe { transmute::<unsafe extern "C" fn(T) -> VALUE, RubyCFunc>(f) }
18
+ }
19
+ pub fn to_ruby_cfunc3<T, U, V>(f: unsafe extern "C" fn(T, U, V) -> VALUE) -> RubyCFunc {
20
+ unsafe { transmute::<unsafe extern "C" fn(T, U, V) -> VALUE, RubyCFunc>(f) }
21
+ }
22
+
23
+ #[allow(non_snake_case)]
24
+ pub fn RTEST(v: VALUE) -> bool {
25
+ v != Qfalse as VALUE && v != Qnil as VALUE
26
+ }
27
+
28
+ extern "C" {
29
+ pub fn extract_si_value_sival_ptr(info: *mut libc::siginfo_t) -> *mut c_void;
30
+ }
data/lib/pf2/cli.rb CHANGED
@@ -27,7 +27,7 @@ module Pf2
27
27
  end
28
28
  option_parser.parse!(argv)
29
29
 
30
- profile = Marshal.load(IO.binread(ARGV[0]))
30
+ profile = JSON.parse(File.read(ARGV[0]), symbolize_names: true, max_nesting: false)
31
31
  report = JSON.generate(Pf2::Reporter.new(profile).emit)
32
32
 
33
33
  if options[:output_file]
data/lib/pf2/reporter.rb CHANGED
@@ -5,7 +5,7 @@ module Pf2
5
5
  # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md
6
6
  class Reporter
7
7
  def initialize(profile)
8
- @profile = profile
8
+ @profile = Reporter.deep_intize_keys(profile)
9
9
  end
10
10
 
11
11
  def inspect
@@ -13,15 +13,15 @@ module Pf2
13
13
  end
14
14
 
15
15
  def emit
16
- x = {
16
+ report = {
17
17
  meta: {
18
18
  interval: 10, # ms; TODO: replace with actual interval
19
19
  start_time: 0,
20
20
  process_type: 0,
21
21
  product: 'ruby',
22
22
  stackwalk: 0,
23
- version: 19,
24
- preprocessed_profile_version: 28,
23
+ version: 28,
24
+ preprocessed_profile_version: 47,
25
25
  symbolicated: true,
26
26
  categories: [
27
27
  {
@@ -40,12 +40,13 @@ module Pf2
40
40
  subcategories: ["Code"],
41
41
  },
42
42
  ],
43
+ marker_schema: [],
43
44
  },
44
45
  libs: [],
45
46
  counters: [],
46
47
  threads: @profile[:threads].values.map {|th| ThreadReport.new(th).emit }
47
48
  }
48
- Reporter.deep_camelize_keys(x)
49
+ Reporter.deep_camelize_keys(report)
49
50
  end
50
51
 
51
52
  class ThreadReport
@@ -83,7 +84,9 @@ module Pf2
83
84
  name: "Thread (tid: #{@thread[:thread_id]})",
84
85
  is_main_thread: true,
85
86
  is_js_tracer: true,
86
- pid: 1,
87
+ # FIXME: We can fill the correct PID only after we correctly fill is_main_thread
88
+ # (only one thread could be marked as is_main_thread in a single process)
89
+ pid: @thread[:thread_id],
87
90
  tid: @thread[:thread_id],
88
91
  samples: samples,
89
92
  markers: markers,
@@ -114,7 +117,7 @@ module Pf2
114
117
 
115
118
  @thread[:samples].each do |sample|
116
119
  ret[:stack] << @stack_tree_id_map[sample[:stack_tree_id]]
117
- ret[:time] << sample[:timestamp] / 1000000 # ns -> ms
120
+ ret[:time] << sample[:elapsed_ns] / 1000000 # ns -> ms
118
121
  ret[:duration] << 1
119
122
  ret[:event_delay] << 0
120
123
  end
@@ -134,6 +137,8 @@ module Pf2
134
137
  line: [],
135
138
  column: [],
136
139
  optimizations: [],
140
+ inline_depth: [],
141
+ native_symbol: [],
137
142
  }
138
143
 
139
144
  @thread[:frames].each.with_index do |(id, frame), i|
@@ -146,6 +151,8 @@ module Pf2
146
151
  ret[:line] << nil
147
152
  ret[:column] << nil
148
153
  ret[:optimizations] << nil
154
+ ret[:inline_depth] << 0
155
+ ret[:native_symbol] << nil
149
156
 
150
157
  @frame_id_map[id] = i
151
158
  end
@@ -229,6 +236,9 @@ module Pf2
229
236
  data: [],
230
237
  name: [],
231
238
  time: [],
239
+ start_time: [],
240
+ end_time: [],
241
+ phase: [],
232
242
  category: [],
233
243
  length: 0
234
244
  }
@@ -244,17 +254,32 @@ module Pf2
244
254
  s.split('_').inject([]) {|buffer, p| buffer.push(buffer.size == 0 ? p : p.capitalize) }.join
245
255
  end
246
256
 
247
- def deep_camelize_keys(value)
257
+ def deep_transform_keys(value, &block)
248
258
  case value
249
259
  when Array
250
- value.map {|v| deep_camelize_keys(v) }
260
+ value.map {|v| deep_transform_keys(v, &block) }
251
261
  when Hash
252
- Hash[value.map {|k, v| [snake_to_camel(k.to_s).to_sym, deep_camelize_keys(v)] }]
262
+ Hash[value.map {|k, v| [yield(k), deep_transform_keys(v, &block)] }]
253
263
  else
254
264
  value
255
265
  end
256
266
  end
267
+
268
+ def deep_camelize_keys(value)
269
+ deep_transform_keys(value) do |key|
270
+ snake_to_camel(key.to_s).to_sym
271
+ end
272
+ end
273
+
274
+ def deep_intize_keys(value)
275
+ deep_transform_keys(value) do |key|
276
+ if key.to_s.to_i.to_s == key.to_s
277
+ key.to_s.to_i
278
+ else
279
+ key
280
+ end
281
+ end
282
+ end
257
283
  end
258
284
  end
259
285
  end
260
-
data/lib/pf2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pf2
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/pf2.rb CHANGED
@@ -4,13 +4,31 @@ require_relative 'pf2/version'
4
4
  module Pf2
5
5
  class Error < StandardError; end
6
6
 
7
- @@threads = []
7
+ def self.default_scheduler_class
8
+ # SignalScheduler is Linux-only. Use TimerThreadScheduler on other platforms.
9
+ if defined?(SignalScheduler)
10
+ SignalScheduler
11
+ else
12
+ TimerThreadScheduler
13
+ end
14
+ end
15
+
16
+ def self.default_scheduler
17
+ @@default_scheduler ||= default_scheduler_class.new
18
+ end
19
+
20
+ def self.start(...)
21
+ default_scheduler.start(...)
22
+ end
8
23
 
9
- def self.threads
10
- @@threads
24
+ def self.stop(...)
25
+ default_scheduler.stop(...)
11
26
  end
12
27
 
13
- def self.threads=(th)
14
- @@threads = th
28
+ def self.profile(&block)
29
+ raise ArgumentError, "block required" unless block_given?
30
+ start([Thread.current], true)
31
+ yield
32
+ stop
15
33
  end
16
34
  end