pf2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8e386916b39ce131297e3e29ec4eca107fe9b2ebe98e69db68f1c84091fac724
4
+ data.tar.gz: 84a658c4f5f336a39a52b2ed58ccb600e7b4f541cd58b4aeea041863db4edfc9
5
+ SHA512:
6
+ metadata.gz: 16d6addbe2abbf348158dca6f2c7e2d048048a22d8a93b194556195e0c9d7c00c51c50f8b82bd462b263d7db6bc8211da1112d5d436fcfdafba08591b618e901
7
+ data.tar.gz: 0603d44895aeea77484c6862cbd2660dffa131c4ad5761aaa94e5c67c44bce04491b7ab29078897b095b44d07fc31dbb7351056cbbc005d3f097c2cb83744fc0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [0.1.0] - 2023-10-04
2
+
3
+ - Initial release
4
+
5
+ ## [Unreleased]
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Daisuke Aritomo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Pf2
2
+
3
+ A sampling-based profiler for Ruby.
4
+ Works only on a patched version of CRuby (MRI) at the moment.
5
+
6
+ ## Installation
7
+
8
+ TBD
9
+
10
+ ## Usage
11
+
12
+ TBD
13
+
14
+ ## Development
15
+
16
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
17
+
18
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
19
+
20
+ ## Contributing
21
+
22
+ Bug reports and pull requests are welcome on GitHub at https://github.com/osyoyu/pf2.
23
+
24
+ ## License
25
+
26
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/extensiontask'
3
+
4
+ task default: %i[]
5
+
6
+ Rake::ExtensionTask.new 'pf2' do |ext|
7
+ ext.lib_dir = 'lib/pf2'
8
+ end
data/exe/pf2 ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pf2/cli'
4
+ exit Pf2::CLI.run(ARGV)
@@ -0,0 +1,5 @@
1
+ require 'mkmf'
2
+
3
+ abort 'missing rb_profile_thread_frames()' unless have_func 'rb_profile_thread_frames'
4
+
5
+ create_makefile 'pf2/pf2'
data/ext/pf2/pf2.c ADDED
@@ -0,0 +1,246 @@
1
+ #include <errno.h>
2
+ #include <signal.h>
3
+ #include <stdbool.h>
4
+ #include <stdio.h>
5
+ #include <stdlib.h>
6
+ #include <unistd.h>
7
+
8
+ #include <ruby.h>
9
+ #include <ruby/debug.h>
10
+ #include <ruby/thread.h>
11
+
12
+ #define MAX_BUFFER_SIZE 3000
13
+
14
+ struct pf2_buffer_t {
15
+ VALUE framebuffer[MAX_BUFFER_SIZE];
16
+ int linebuffer[MAX_BUFFER_SIZE];
17
+ };
18
+
19
+ // Ruby functions
20
+ void Init_pf2(void);
21
+ VALUE rb_start(VALUE self, VALUE debug);
22
+ VALUE rb_stop(VALUE self);
23
+
24
+ static void pf2_start(void);
25
+ static void pf2_stop(void);
26
+ static void pf2_signal_handler(int signo);
27
+ static void pf2_postponed_job(void *_);
28
+
29
+ static void pf2_record(struct pf2_buffer_t *buffer);
30
+ static VALUE find_or_create_thread_results(VALUE results, pid_t thread_id);
31
+
32
+ // Buffer to record rb_profile_frames() results
33
+ struct pf2_buffer_t buffer;
34
+ // The time when the profiler started
35
+ struct timespec initial_time;
36
+ // Debug print?
37
+ bool _debug = false;
38
+
39
+ void
40
+ Init_pf2(void)
41
+ {
42
+ VALUE rb_mPf2 = rb_define_module("Pf2");
43
+ rb_define_module_function(rb_mPf2, "start", rb_start, 1);
44
+ rb_define_module_function(rb_mPf2, "stop", rb_stop, 0);
45
+ }
46
+
47
+ VALUE
48
+ rb_start(VALUE self, VALUE debug) {
49
+ _debug = RTEST(debug);
50
+
51
+ /**
52
+ * {
53
+ * sequence: 0,
54
+ * threads: {},
55
+ * }
56
+ */
57
+ VALUE results = rb_hash_new();
58
+ rb_hash_aset(results, ID2SYM(rb_intern_const("sequence")), INT2FIX(0));
59
+ rb_hash_aset(results, ID2SYM(rb_intern_const("threads")), rb_hash_new());
60
+
61
+ rb_iv_set(self, "@results", results);
62
+
63
+ pf2_start();
64
+
65
+ if (_debug) {
66
+ rb_funcall(rb_mKernel, rb_intern("puts"), 1, rb_str_new_cstr("[debug] Pf2 started"));
67
+ }
68
+
69
+ return results;
70
+ }
71
+
72
+ VALUE
73
+ rb_stop(VALUE self) {
74
+ pf2_stop();
75
+
76
+ if (_debug) {
77
+ rb_funcall(rb_mKernel, rb_intern("puts"), 1, rb_str_new_cstr("[debug] Pf2 stopped"));
78
+ }
79
+
80
+ return rb_iv_get(self, "@results");
81
+ }
82
+
83
+ static void
84
+ pf2_start(void)
85
+ {
86
+ clock_gettime(CLOCK_MONOTONIC, &initial_time);
87
+
88
+ // Configure timer for every 10 ms
89
+ // TODO: Make interval configurable
90
+ struct itimerval timer;
91
+ timer.it_value.tv_sec = 1;
92
+ timer.it_value.tv_usec = 0;
93
+ timer.it_interval.tv_sec = 0;
94
+ timer.it_interval.tv_usec = 10 * 1000; // 10 ms
95
+ if (signal(SIGALRM, pf2_signal_handler) == SIG_ERR) {
96
+ rb_syserr_fail(errno, "Failed to configure profiling timer");
97
+ };
98
+ if (setitimer(ITIMER_REAL, &timer, NULL) == -1) {
99
+ rb_syserr_fail(errno, "Failed to configure profiling timer");
100
+ };
101
+ }
102
+
103
+ static void
104
+ pf2_stop(void)
105
+ {
106
+ struct itimerval timer = { 0 }; // stop
107
+ setitimer(ITIMER_REAL, &timer, NULL);
108
+ }
109
+
110
+ // async-signal-safe
111
+ static void
112
+ pf2_signal_handler(int signo)
113
+ {
114
+ rb_postponed_job_register_one(0, pf2_postponed_job, 0);
115
+ }
116
+
117
+ static void
118
+ pf2_postponed_job(void *_) {
119
+ pf2_record(&buffer);
120
+ };
121
+
122
+ // Buffer structure
123
+ static void
124
+ pf2_record(struct pf2_buffer_t *buffer)
125
+ {
126
+ // get the current time
127
+ struct timespec ts;
128
+ clock_gettime(CLOCK_MONOTONIC, &ts);
129
+
130
+ VALUE rb_mPf2 = rb_const_get(rb_cObject, rb_intern("Pf2"));
131
+ VALUE results = rb_iv_get(rb_mPf2, "@results");
132
+
133
+ // Iterate over all Threads
134
+ VALUE threads = rb_iv_get(rb_mPf2, "@@threads");
135
+ for (int i = 0; i < RARRAY_LEN(threads); i++) {
136
+ VALUE thread = rb_ary_entry(threads, i);
137
+ VALUE thread_status = rb_funcall(thread, rb_intern("status"), 0);
138
+ if (NIL_P(thread) || thread_status == Qfalse) {
139
+ // Thread is dead, just ignore
140
+ continue;
141
+ }
142
+
143
+ pid_t thread_id = NUM2INT(rb_funcall(thread, rb_intern("native_thread_id"), 0));
144
+ VALUE thread_results = find_or_create_thread_results(results, thread_id);
145
+ assert(!NIL_P(thread_results));
146
+
147
+ // The actual querying
148
+ int stack_depth = rb_profile_thread_frames(thread, 0, MAX_BUFFER_SIZE, buffer->framebuffer, buffer->linebuffer);
149
+
150
+ // TODO: Reimplement Pf2-internal data structures without CRuby
151
+ // (which will allow us to release the GVL at this point)
152
+ // rb_thread_call_without_gvl(...);
153
+
154
+ VALUE frames_table = rb_hash_lookup(thread_results, ID2SYM(rb_intern_const("frames")));
155
+ assert(!NIL_P(frames_table));
156
+ VALUE samples = rb_hash_lookup(thread_results, ID2SYM(rb_intern_const("samples")));
157
+ assert(!NIL_P(samples));
158
+
159
+ // Dig down the stack (top of call stack -> bottom (root))
160
+ VALUE stack_tree_p = rb_hash_lookup(thread_results, ID2SYM(rb_intern_const("stack_tree")));
161
+ for (int i = stack_depth - 1; i >= 0; i--) {
162
+ assert(NIL_P(buffer->framebuffer[i]));
163
+
164
+ // Collect & record frame information
165
+ VALUE frame_obj_id = rb_obj_id(buffer->framebuffer[i]);
166
+ VALUE frame_table_entry = rb_hash_aref(frames_table, frame_obj_id);
167
+ if (NIL_P(frame_table_entry)) {
168
+ frame_table_entry = rb_hash_new();
169
+ rb_hash_aset(frame_table_entry, ID2SYM(rb_intern_const("full_label")), rb_profile_frame_full_label(buffer->framebuffer[i]));
170
+ rb_hash_aset(frames_table, frame_obj_id, frame_table_entry);
171
+ }
172
+
173
+ VALUE children = rb_hash_aref(stack_tree_p, ID2SYM(rb_intern_const("children")));
174
+ VALUE next_node = rb_hash_lookup(children, frame_obj_id);
175
+ // If this is the first time we see this frame, register it to the stack tree
176
+ if (NIL_P(next_node)) { // not found
177
+ next_node = rb_hash_new();
178
+
179
+ // Increment sequence
180
+ VALUE next =
181
+ rb_funcall(
182
+ rb_hash_lookup(results, ID2SYM(rb_intern_const("sequence"))),
183
+ rb_intern("+"),
184
+ 1,
185
+ INT2FIX(1)
186
+ );
187
+ rb_hash_aset(results, ID2SYM(rb_intern_const("sequence")), next);
188
+
189
+ rb_hash_aset(next_node, ID2SYM(rb_intern_const("node_id")), INT2FIX(next));
190
+ rb_hash_aset(next_node, ID2SYM(rb_intern_const("frame_id")), frame_obj_id);
191
+ rb_hash_aset(next_node, ID2SYM(rb_intern_const("full_label")), rb_profile_frame_full_label(buffer->framebuffer[i]));
192
+ rb_hash_aset(next_node, ID2SYM(rb_intern_const("children")), rb_hash_new());
193
+
194
+ rb_hash_aset(children, frame_obj_id, next_node);
195
+ }
196
+
197
+ VALUE stack_tree_id = rb_hash_aref(next_node, ID2SYM(rb_intern_const("node_id")));
198
+
199
+ // If on leaf
200
+ if (i == 0) {
201
+ // Record sample
202
+ VALUE sample = rb_hash_new();
203
+ rb_hash_aset(sample, ID2SYM(rb_intern_const("stack_tree_id")), stack_tree_id);
204
+ unsigned long long nsec = (ts.tv_sec - initial_time.tv_sec) * 1000000000 + ts.tv_nsec - initial_time.tv_nsec;
205
+ rb_hash_aset(sample, ID2SYM(rb_intern_const("timestamp")), ULL2NUM(nsec));
206
+ rb_ary_push(samples, sample);
207
+ }
208
+
209
+ stack_tree_p = next_node;
210
+ }
211
+ }
212
+ }
213
+
214
+ static VALUE
215
+ find_or_create_thread_results(VALUE results, pid_t thread_id) {
216
+ assert(!NIL_P(results));
217
+ assert(!NIL_P(thread));
218
+
219
+ VALUE threads = rb_hash_aref(results, ID2SYM(rb_intern_const("threads")));
220
+ VALUE thread_results = rb_hash_aref(threads, INT2NUM(thread_id));
221
+ if (NIL_P(thread_results)) {
222
+ /**
223
+ * {
224
+ * thread_id: 1,
225
+ * frames: [],
226
+ * stack_tree: {
227
+ * node_id: ...,
228
+ * children: {}
229
+ * },
230
+ * samples: [],
231
+ * }
232
+ */
233
+ thread_results = rb_hash_new();
234
+ rb_hash_aset(thread_results, ID2SYM(rb_intern_const("thread_id")), INT2NUM(thread_id));
235
+
236
+ rb_hash_aset(thread_results, ID2SYM(rb_intern_const("frames")), rb_hash_new());
237
+ VALUE stack_tree = rb_hash_aset(thread_results, ID2SYM(rb_intern_const("stack_tree")), rb_hash_new());
238
+ rb_hash_aset(stack_tree, ID2SYM(rb_intern_const("node_id")), ID2SYM(rb_intern_const("root")));
239
+ rb_hash_aset(stack_tree, ID2SYM(rb_intern_const("children")), rb_hash_new());
240
+ rb_hash_aset(thread_results, ID2SYM(rb_intern_const("samples")), rb_ary_new());
241
+ rb_hash_aset(thread_results, ID2SYM(rb_intern_const("gvl_timings")), rb_ary_new());
242
+
243
+ rb_hash_aset(threads, INT2NUM(thread_id), thread_results);
244
+ }
245
+ return thread_results;
246
+ }
data/lib/pf2/cli.rb ADDED
@@ -0,0 +1,42 @@
1
+ require 'optparse'
2
+
3
+ require 'pf2'
4
+ require 'pf2/reporter'
5
+
6
+ module Pf2
7
+ class CLI
8
+ def self.run(...)
9
+ new.run(...)
10
+ end
11
+
12
+ def run(argv)
13
+ options = {}
14
+ option_parser = OptionParser.new do |opts|
15
+ opts.on('-v', '--version', 'Prints version') do
16
+ puts Pf2::VERSION
17
+ exit
18
+ end
19
+
20
+ opts.on('-h', '--help', 'Prints this help') do
21
+ puts opts
22
+ end
23
+
24
+ opts.on('-o', '--output FILE', 'Output file') do |path|
25
+ options[:output_file] = path
26
+ end
27
+ end
28
+ option_parser.parse!(argv)
29
+
30
+ profile = Marshal.load(IO.binread(ARGV[0]))
31
+ report = JSON.generate(Pf2::Reporter.new(profile).emit)
32
+
33
+ if options[:output_file]
34
+ File.write(options[:output_file], report)
35
+ else
36
+ puts report
37
+ end
38
+
39
+ return 0
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,260 @@
1
+ require 'json'
2
+
3
+ module Pf2
4
+ # Generates Firefox Profiler's "processed profile format"
5
+ # https://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md
6
+ class Reporter
7
+ def initialize(profile)
8
+ @profile = profile
9
+ end
10
+
11
+ def inspect
12
+ "" # TODO: provide something better
13
+ end
14
+
15
+ def emit
16
+ x = {
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: 19,
24
+ preprocessed_profile_version: 28,
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: "lightblue",
40
+ subcategories: ["Code"],
41
+ },
42
+ ],
43
+ },
44
+ libs: [],
45
+ counters: [],
46
+ threads: @profile[:threads].values.map {|th| ThreadReport.new(th).emit }
47
+ }
48
+ Reporter.deep_camelize_keys(x)
49
+ end
50
+
51
+ class ThreadReport
52
+ def initialize(thread)
53
+ @thread = thread
54
+
55
+ # Populated in other methods
56
+ @func_id_map = {}
57
+ @frame_id_map = {}
58
+ @stack_tree_id_map = {}
59
+
60
+ @string_table = {}
61
+ end
62
+
63
+ def inspect
64
+ "" # TODO: provide something better
65
+ end
66
+
67
+ def emit
68
+ func_table = build_func_table
69
+ frame_table = build_frame_table
70
+ stack_table = build_stack_table
71
+ samples = build_samples
72
+
73
+ string_table = build_string_table
74
+
75
+ {
76
+ process_type: 'default',
77
+ process_name: 'ruby',
78
+ process_startup_time: 0,
79
+ process_shutdown_time: nil,
80
+ register_time: 0,
81
+ unregister_time: nil,
82
+ paused_ranges: [],
83
+ name: "Thread (tid: #{@thread[:thread_id]})",
84
+ is_main_thread: true,
85
+ is_js_tracer: true,
86
+ pid: 1,
87
+ tid: @thread[:thread_id],
88
+ samples: samples,
89
+ markers: markers,
90
+ stack_table: stack_table,
91
+ frame_table: frame_table,
92
+ string_array: build_string_table,
93
+ func_table: func_table,
94
+ resource_table: {
95
+ lib: [],
96
+ name: [],
97
+ host: [],
98
+ type: [],
99
+ length: 0,
100
+ },
101
+ native_symbols: [],
102
+ }
103
+ end
104
+
105
+ def build_samples
106
+ ret = {
107
+ event_delay: [],
108
+ stack: [],
109
+ time: [],
110
+ duration: [],
111
+ # weight: nil,
112
+ # weight_type: 'samples',
113
+ }
114
+
115
+ @thread[:samples].each do |sample|
116
+ ret[:stack] << @stack_tree_id_map[sample[:stack_tree_id]]
117
+ ret[:time] << sample[:timestamp] / 1000000 # ns -> ms
118
+ ret[:duration] << 1
119
+ ret[:event_delay] << 0
120
+ end
121
+
122
+ ret[:length] = ret[:stack].length
123
+ ret
124
+ end
125
+
126
+ def build_frame_table
127
+ ret = {
128
+ address: [],
129
+ category: [],
130
+ subcategory: [],
131
+ func: [],
132
+ inner_window_id: [],
133
+ implementation: [],
134
+ line: [],
135
+ column: [],
136
+ optimizations: [],
137
+ }
138
+
139
+ @thread[:frames].each.with_index do |(id, frame), i|
140
+ ret[:address] << nil
141
+ ret[:category] << 1
142
+ ret[:subcategory] << 1
143
+ ret[:func] << i # TODO
144
+ ret[:inner_window_id] << nil
145
+ ret[:implementation] << nil
146
+ ret[:line] << nil
147
+ ret[:column] << nil
148
+ ret[:optimizations] << nil
149
+
150
+ @frame_id_map[id] = i
151
+ end
152
+
153
+ ret[:length] = ret[:address].length
154
+ ret
155
+ end
156
+
157
+ def build_func_table
158
+ ret = {
159
+ name: [],
160
+ is_js: [],
161
+ relevant_for_js: [],
162
+ resource: [],
163
+ file_name: [],
164
+ line_number: [],
165
+ column_number: [],
166
+ }
167
+
168
+ @thread[:frames].each.with_index do |(id, frame), i|
169
+ ret[:name] << string_id(frame[:full_label])
170
+ ret[:is_js] << false
171
+ ret[:relevant_for_js] << false
172
+ ret[:resource] << -1
173
+ ret[:file_name] << nil
174
+ ret[:line_number] << nil
175
+ ret[:column_number] << nil
176
+
177
+ @func_id_map[id] = i
178
+ end
179
+
180
+ ret[:length] = ret[:name].length
181
+ ret
182
+ end
183
+
184
+ def build_stack_table
185
+ ret = {
186
+ frame: [],
187
+ category: [],
188
+ subcategory: [],
189
+ prefix: [],
190
+ }
191
+
192
+ queue = []
193
+
194
+ @thread[:stack_tree][:children].each {|_, c| queue << [nil, c] }
195
+
196
+ loop do
197
+ break if queue.size == 0
198
+
199
+ prefix, node = queue.shift
200
+ ret[:frame] << @frame_id_map[node[:frame_id]]
201
+ ret[:category] << 1
202
+ ret[:subcategory] << nil
203
+ ret[:prefix] << prefix
204
+
205
+ # The index of this frame - children can refer to this frame using this index as prefix
206
+ frame_index = ret[:frame].length - 1
207
+ @stack_tree_id_map[node[:node_id]] = frame_index
208
+
209
+ # Enqueue children nodes
210
+ node[:children].each {|_, c| queue << [frame_index, c] }
211
+ end
212
+
213
+ ret[:length] = ret[:frame].length
214
+ ret
215
+ end
216
+
217
+ def build_string_table
218
+ @string_table.sort_by {|_, v| v}.map {|s| s[0] }
219
+ end
220
+
221
+ def string_id(str)
222
+ return @string_table[str] if @string_table.has_key?(str)
223
+ @string_table[str] = @string_table.length
224
+ @string_table[str]
225
+ end
226
+
227
+ def markers
228
+ {
229
+ data: [],
230
+ name: [],
231
+ time: [],
232
+ category: [],
233
+ length: 0
234
+ }
235
+ end
236
+ end
237
+
238
+ # Util functions
239
+ class << self
240
+ def snake_to_camel(s)
241
+ return "isJS" if s == "is_js"
242
+ return "relevantForJS" if s == "relevant_for_js"
243
+ return "innerWindowID" if s == "inner_window_id"
244
+ s.split('_').inject([]) {|buffer, p| buffer.push(buffer.size == 0 ? p : p.capitalize) }.join
245
+ end
246
+
247
+ def deep_camelize_keys(value)
248
+ case value
249
+ when Array
250
+ value.map {|v| deep_camelize_keys(v) }
251
+ when Hash
252
+ Hash[value.map {|k, v| [snake_to_camel(k.to_s).to_sym, deep_camelize_keys(v)] }]
253
+ else
254
+ value
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
260
+
@@ -0,0 +1,3 @@
1
+ module Pf2
2
+ VERSION = '0.1.0'
3
+ end
data/lib/pf2.rb ADDED
@@ -0,0 +1,16 @@
1
+ require_relative 'pf2/pf2'
2
+ require_relative 'pf2/version'
3
+
4
+ module Pf2
5
+ class Error < StandardError; end
6
+
7
+ @@threads = []
8
+
9
+ def self.threads
10
+ @@threads
11
+ end
12
+
13
+ def self.threads=(th)
14
+ @@threads = th
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pf2
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daisuke Aritomo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-10-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake-compiler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description:
28
+ email:
29
+ - osyoyu@osyoyu.com
30
+ executables:
31
+ - pf2
32
+ extensions:
33
+ - ext/pf2/extconf.rb
34
+ extra_rdoc_files: []
35
+ files:
36
+ - CHANGELOG.md
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - exe/pf2
41
+ - ext/pf2/extconf.rb
42
+ - ext/pf2/pf2.c
43
+ - lib/pf2.rb
44
+ - lib/pf2/cli.rb
45
+ - lib/pf2/reporter.rb
46
+ - lib/pf2/version.rb
47
+ homepage: https://github.com/osyoyu/pf2
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ allowed_push_host: https://rubygems.org
52
+ homepage_uri: https://github.com/osyoyu/pf2
53
+ source_code_uri: https://github.com/osyoyu/pf2
54
+ changelog_uri: https://github.com/osyoyu/pf2/blob/master/CHANGELOG.md
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 3.3.0.dev
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.5.0.dev
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Yet another Ruby profiler
74
+ test_files: []