pf2 0.1.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 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: []