pf2 0.1.0 → 0.2.0

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