pilfer 0.0.1.pre
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 +15 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +112 -0
- data/ext/extconf.rb +19 -0
- data/ext/rblineprof.c +619 -0
- data/lib/pilfer/logger.rb +63 -0
- data/lib/pilfer/profile.rb +39 -0
- data/lib/pilfer/profiler.rb +25 -0
- data/lib/pilfer/version.rb +3 -0
- data/pilfer.gemspec +32 -0
- data/script/package +7 -0
- data/script/release +15 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
N2VjNWFmMjYxZmMzNzYxOTI1YjZkNDVhNDVhMjE0MDYwY2NkYzBlMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MzRlMDIwMWIyZGMwY2QxZTY1ZTM4OTc1ZTU5MjA3YjU2NmFjYjA5OQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YWExZjg4Y2ExOGVkYWQ3NTE0YjYwODdmOGQ5ODgxZDQwZGU1YjY3NDIyNzA0
|
10
|
+
MmY3OGRiMzk2ZmUyMDcyNDg4YTJjZmQwNDY1MjE3ZTY3ZjliNjhiNjIxYjIy
|
11
|
+
M2QyODdjODJjZTZjYTliMzI1MTQ1ZTRiM2Y0MzQ2MDVjODEwNGE=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZTc3MzlkMmM5YTNkMzI0MGVjY2FhOGYxYmQzOTAxNzk4ZTA1ZWZmZWRjZGU1
|
14
|
+
OTMyZWU4YjFlODRiMDBkZTEzNTU4OTIwMDM3NDViMWRjYWIyNWQyMDE0OGU4
|
15
|
+
MTQ0NGExMjcwOTFhOTkwYTk3YTk4OTQ3NTYxNTI3MjUwZGU1ODc=
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Eric Lindvall and Larry Marburger
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# Pilfer
|
2
|
+
|
3
|
+
Look into your ruby with [rblineprof](https://github.com/tmm1/rblineprof/).
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
Profile a block of code saving the report to the file `profile.log`.
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
reporter = Pilfer::Logger.new('pilfer.log')
|
11
|
+
profiler = Pilfer::Profiler.new(reporter)
|
12
|
+
profiler.profile { do_something }
|
13
|
+
```
|
14
|
+
|
15
|
+
The report prints the source of each line of code executed and includes the
|
16
|
+
total execution time and the number of times the line was executed.
|
17
|
+
|
18
|
+
_TODO: Show profile response._
|
19
|
+
|
20
|
+
### Step 1: Create a reporter
|
21
|
+
|
22
|
+
Profiles can be sent to a [pilfer-server][] or written to a file or `IO`
|
23
|
+
object.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
reporter = Pilfer::Server.new('https://pilfer.com', 'abc123')
|
27
|
+
reporter = Pilfer::Logger.new('pilfer.log')
|
28
|
+
reporter = Pilfer::Logger.new($stdout)
|
29
|
+
```
|
30
|
+
|
31
|
+
The absolute path to each profiled file is used in the report. Set the path to
|
32
|
+
the application root with `:app_root` to have it trimmed from reported file
|
33
|
+
paths.
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
reporter = Pilfer::Logger.new($stdout, :app_root => '/my/app')
|
37
|
+
```
|
38
|
+
|
39
|
+
### Step 2: Create a profiler
|
40
|
+
|
41
|
+
Pass the reporter to a new `Pilfer::Profiler`.
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
profiler = Pilfer::Profiler.new(reporter)
|
45
|
+
```
|
46
|
+
|
47
|
+
### Step 3: Profile a block of code
|
48
|
+
|
49
|
+
Profile a block of code with `#profile`.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
profiler.profile { do_something }
|
53
|
+
```
|
54
|
+
|
55
|
+
Every file that's executed by the block--including code outside the
|
56
|
+
application like gems and standard libraries--will be included in the profile.
|
57
|
+
Use `#profile_files_matching` and provide a regular expression to limit
|
58
|
+
profiling to only matching file paths.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
matcher = %r{^#{Regexp.escape(Rails.root.to_s)}/app/models}
|
62
|
+
profiler.profile_files_matching(matcher) { do_something }
|
63
|
+
```
|
64
|
+
|
65
|
+
## Pilfer Server
|
66
|
+
|
67
|
+
[pilfer-server][] is your own, personal service for collecting and viewing
|
68
|
+
profile reports. Follow the [pilfer-server setup instructions][pilfer-server]
|
69
|
+
to stand up a new server and send it reports using its URL and token.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
reporter = Pilfer::Server.new('https://pilfer.com', 'abc123')
|
73
|
+
```
|
74
|
+
|
75
|
+
## Rack Middleware
|
76
|
+
|
77
|
+
Profile your Rack or Rails app using `Pilfer::Middleware`.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
reporter = Pilfer::Server.new('https://pilfer.com', 'abc123'
|
81
|
+
:app_root => Rails.root)
|
82
|
+
use Pilfer::Middleware, reporter
|
83
|
+
```
|
84
|
+
|
85
|
+
Restrict the files profiled by passing a regular expression with
|
86
|
+
`:files_matching`.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
matcher = %r{^#{Regexp.escape(Rails.root.to_s)}/(app|config|lib|vendor/plugin)}
|
90
|
+
use Pilfer::Middleware, reporter, :files_matching => matcher
|
91
|
+
```
|
92
|
+
|
93
|
+
You probably don't want to profile _every_ request. The given block will be
|
94
|
+
evaluated on each request to determine if a profile should be run.
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
use Pilfer::Middleware, reporter do
|
98
|
+
# Profile 1% of requests.
|
99
|
+
rand(100) == 1
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
The Rack environment is available to allow profiling on demand.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
use Pilfer::Middleware, reporter do |env|
|
107
|
+
env.query_string.include? 'profile=true'
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
|
112
|
+
[pilfer-server]: https://github.com/eric/pilfer-server
|
data/ext/extconf.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'mkmf'
|
2
|
+
|
3
|
+
if RUBY_VERSION >= "1.9"
|
4
|
+
require "debugger/ruby_core_source"
|
5
|
+
|
6
|
+
hdrs = proc {
|
7
|
+
have_type("rb_iseq_location_t", "vm_core.h")
|
8
|
+
|
9
|
+
have_header("vm_core.h") and
|
10
|
+
have_header("iseq.h")
|
11
|
+
}
|
12
|
+
|
13
|
+
unless Debugger::RubyCoreSource::create_makefile_with_core(hdrs, "rblineprof")
|
14
|
+
STDERR.puts "\nDebugger::RubyCoreSource::create_makefile failed"
|
15
|
+
exit(1)
|
16
|
+
end
|
17
|
+
else
|
18
|
+
create_makefile 'rblineprof'
|
19
|
+
end
|
data/ext/rblineprof.c
ADDED
@@ -0,0 +1,619 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
#include <stdbool.h>
|
3
|
+
#include <time.h>
|
4
|
+
#include <sys/time.h>
|
5
|
+
#include <sys/resource.h>
|
6
|
+
|
7
|
+
#ifdef RUBY_VM
|
8
|
+
#include <ruby/re.h>
|
9
|
+
#include <ruby/intern.h>
|
10
|
+
#include <vm_core.h>
|
11
|
+
#include <iseq.h>
|
12
|
+
|
13
|
+
// There's a compile error on 1.9.3. So:
|
14
|
+
#ifdef RTYPEDDATA_DATA
|
15
|
+
#define ruby_current_thread ((rb_thread_t *)RTYPEDDATA_DATA(rb_thread_current()))
|
16
|
+
#endif
|
17
|
+
#else
|
18
|
+
#include <st.h>
|
19
|
+
#include <re.h>
|
20
|
+
#include <intern.h>
|
21
|
+
#include <node.h>
|
22
|
+
#include <env.h>
|
23
|
+
typedef rb_event_t rb_event_flag_t;
|
24
|
+
#endif
|
25
|
+
|
26
|
+
static VALUE gc_hook;
|
27
|
+
|
28
|
+
/*
|
29
|
+
* Time in microseconds
|
30
|
+
*/
|
31
|
+
typedef uint64_t prof_time_t;
|
32
|
+
|
33
|
+
/*
|
34
|
+
* Profiling snapshot
|
35
|
+
*/
|
36
|
+
typedef struct snapshot {
|
37
|
+
prof_time_t wall_time;
|
38
|
+
prof_time_t cpu_time;
|
39
|
+
} snapshot_t;
|
40
|
+
|
41
|
+
/*
|
42
|
+
* A line of Ruby source code
|
43
|
+
*/
|
44
|
+
typedef struct sourceline {
|
45
|
+
uint64_t calls; // total number of call/c_call events
|
46
|
+
snapshot_t total;
|
47
|
+
} sourceline_t;
|
48
|
+
|
49
|
+
/*
|
50
|
+
* Struct representing a single Ruby file.
|
51
|
+
*/
|
52
|
+
typedef struct sourcefile {
|
53
|
+
char *filename;
|
54
|
+
|
55
|
+
/* per line timing */
|
56
|
+
long nlines;
|
57
|
+
sourceline_t *lines;
|
58
|
+
|
59
|
+
/* overall file timing */
|
60
|
+
snapshot_t total;
|
61
|
+
snapshot_t child;
|
62
|
+
uint64_t depth;
|
63
|
+
snapshot_t exclusive_start;
|
64
|
+
snapshot_t exclusive;
|
65
|
+
} sourcefile_t;
|
66
|
+
|
67
|
+
/*
|
68
|
+
* An individual stack frame used to track
|
69
|
+
* calls and returns from Ruby methods
|
70
|
+
*/
|
71
|
+
typedef struct stackframe {
|
72
|
+
// data emitted from Ruby to our profiler hook
|
73
|
+
rb_event_flag_t event;
|
74
|
+
#ifdef RUBY_VM
|
75
|
+
rb_thread_t *thread;
|
76
|
+
#else
|
77
|
+
NODE *node;
|
78
|
+
#endif
|
79
|
+
VALUE self;
|
80
|
+
ID mid;
|
81
|
+
VALUE klass;
|
82
|
+
|
83
|
+
char *filename;
|
84
|
+
long line;
|
85
|
+
|
86
|
+
snapshot_t start;
|
87
|
+
sourcefile_t *srcfile;
|
88
|
+
} stackframe_t;
|
89
|
+
|
90
|
+
/*
|
91
|
+
* Static properties and rbineprof configuration
|
92
|
+
*/
|
93
|
+
static struct {
|
94
|
+
bool enabled;
|
95
|
+
|
96
|
+
// stack
|
97
|
+
#define MAX_STACK_DEPTH 32768
|
98
|
+
stackframe_t stack[MAX_STACK_DEPTH];
|
99
|
+
uint64_t stack_depth;
|
100
|
+
|
101
|
+
// single file mode, store filename and line data directly
|
102
|
+
char *source_filename;
|
103
|
+
sourcefile_t file;
|
104
|
+
|
105
|
+
// regex mode, store file data in hash table
|
106
|
+
VALUE source_regex;
|
107
|
+
st_table *files;
|
108
|
+
|
109
|
+
// cache
|
110
|
+
struct {
|
111
|
+
char *file;
|
112
|
+
sourcefile_t *srcfile;
|
113
|
+
} cache;
|
114
|
+
}
|
115
|
+
rblineprof = {
|
116
|
+
.enabled = false,
|
117
|
+
.source_regex = Qfalse
|
118
|
+
};
|
119
|
+
|
120
|
+
static prof_time_t
|
121
|
+
cputime_usec()
|
122
|
+
{
|
123
|
+
#if defined(__linux__)
|
124
|
+
struct timespec ts;
|
125
|
+
|
126
|
+
if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts) == 0) {
|
127
|
+
return (prof_time_t)ts.tv_sec*1e6 +
|
128
|
+
(prof_time_t)ts.tv_nsec*1e-3;
|
129
|
+
}
|
130
|
+
#endif
|
131
|
+
|
132
|
+
#if defined(RUSAGE_SELF)
|
133
|
+
struct rusage usage;
|
134
|
+
|
135
|
+
getrusage(RUSAGE_SELF, &usage);
|
136
|
+
return (prof_time_t)usage.ru_utime.tv_sec*1e6 +
|
137
|
+
(prof_time_t)usage.ru_utime.tv_usec;
|
138
|
+
#endif
|
139
|
+
|
140
|
+
return 0;
|
141
|
+
}
|
142
|
+
|
143
|
+
static prof_time_t
|
144
|
+
walltime_usec()
|
145
|
+
{
|
146
|
+
struct timeval tv;
|
147
|
+
gettimeofday(&tv, NULL);
|
148
|
+
return (prof_time_t)tv.tv_sec*1e6 +
|
149
|
+
(prof_time_t)tv.tv_usec;
|
150
|
+
}
|
151
|
+
|
152
|
+
static inline snapshot_t
|
153
|
+
snapshot_diff(snapshot_t *t1, snapshot_t *t2)
|
154
|
+
{
|
155
|
+
snapshot_t diff = {
|
156
|
+
.wall_time = t1->wall_time - t2->wall_time,
|
157
|
+
.cpu_time = t1->cpu_time - t2->cpu_time,
|
158
|
+
};
|
159
|
+
|
160
|
+
return diff;
|
161
|
+
}
|
162
|
+
|
163
|
+
static inline void
|
164
|
+
snapshot_increment(snapshot_t *s, snapshot_t *inc)
|
165
|
+
{
|
166
|
+
s->wall_time += inc->wall_time;
|
167
|
+
s->cpu_time += inc->cpu_time;
|
168
|
+
}
|
169
|
+
|
170
|
+
static inline void
|
171
|
+
stackframe_record(stackframe_t *frame, snapshot_t now, stackframe_t *caller_frame)
|
172
|
+
{
|
173
|
+
sourcefile_t *srcfile = frame->srcfile;
|
174
|
+
long line = frame->line;
|
175
|
+
|
176
|
+
/* allocate space for per-line data the first time */
|
177
|
+
if (srcfile->lines == NULL) {
|
178
|
+
srcfile->nlines = line + 100;
|
179
|
+
srcfile->lines = ALLOC_N(sourceline_t, srcfile->nlines);
|
180
|
+
MEMZERO(srcfile->lines, sourceline_t, srcfile->nlines);
|
181
|
+
}
|
182
|
+
|
183
|
+
/* grow the per-line array if necessary */
|
184
|
+
if (line >= srcfile->nlines) {
|
185
|
+
long prev_nlines = srcfile->nlines;
|
186
|
+
srcfile->nlines = line + 100;
|
187
|
+
|
188
|
+
REALLOC_N(srcfile->lines, sourceline_t, srcfile->nlines);
|
189
|
+
MEMZERO(srcfile->lines + prev_nlines, sourceline_t, srcfile->nlines - prev_nlines);
|
190
|
+
}
|
191
|
+
|
192
|
+
snapshot_t diff = snapshot_diff(&now, &frame->start);
|
193
|
+
sourceline_t *srcline = &(srcfile->lines[line]);
|
194
|
+
|
195
|
+
/* Line profiler metrics.
|
196
|
+
*/
|
197
|
+
|
198
|
+
srcline->calls++;
|
199
|
+
|
200
|
+
/* Increment current line's total_time.
|
201
|
+
*
|
202
|
+
* Skip the special case where the stack frame we're returning to
|
203
|
+
* had the same file/line. This fixes double counting on crazy one-liners.
|
204
|
+
*/
|
205
|
+
if (!(caller_frame && caller_frame->srcfile == frame->srcfile && caller_frame->line == frame->line))
|
206
|
+
snapshot_increment(&srcline->total, &diff);
|
207
|
+
|
208
|
+
/* File profiler metrics.
|
209
|
+
*/
|
210
|
+
|
211
|
+
/* Increment the caller file's child_time.
|
212
|
+
*/
|
213
|
+
if (caller_frame && caller_frame->srcfile != srcfile)
|
214
|
+
snapshot_increment(&caller_frame->srcfile->child, &diff);
|
215
|
+
|
216
|
+
/* Increment current file's total_time, but only when we return
|
217
|
+
* to the outermost stack frame when we first entered the file.
|
218
|
+
*/
|
219
|
+
if (srcfile->depth == 0)
|
220
|
+
snapshot_increment(&srcfile->total, &diff);
|
221
|
+
}
|
222
|
+
|
223
|
+
static inline sourcefile_t*
|
224
|
+
sourcefile_lookup(char *filename)
|
225
|
+
{
|
226
|
+
sourcefile_t *srcfile = NULL;
|
227
|
+
|
228
|
+
if (rblineprof.source_filename) { // single file mode
|
229
|
+
#ifdef RUBY_VM
|
230
|
+
if (strcmp(rblineprof.source_filename, filename) == 0) {
|
231
|
+
#else
|
232
|
+
if (rblineprof.source_filename == filename) { // compare char*, not contents
|
233
|
+
#endif
|
234
|
+
srcfile = &rblineprof.file;
|
235
|
+
srcfile->filename = filename;
|
236
|
+
} else {
|
237
|
+
return NULL;
|
238
|
+
}
|
239
|
+
|
240
|
+
} else { // regex mode
|
241
|
+
st_lookup(rblineprof.files, (st_data_t)filename, (st_data_t *)&srcfile);
|
242
|
+
|
243
|
+
if ((VALUE)srcfile == Qnil) // known negative match, skip
|
244
|
+
return NULL;
|
245
|
+
|
246
|
+
if (!srcfile) { // unknown file, check against regex
|
247
|
+
VALUE backref = rb_backref_get();
|
248
|
+
rb_match_busy(backref);
|
249
|
+
long rc = rb_reg_search(rblineprof.source_regex, rb_str_new2(filename), 0, 0);
|
250
|
+
rb_backref_set(backref);
|
251
|
+
|
252
|
+
if (rc >= 0) {
|
253
|
+
srcfile = ALLOC_N(sourcefile_t, 1);
|
254
|
+
MEMZERO(srcfile, sourcefile_t, 1);
|
255
|
+
srcfile->filename = strdup(filename);
|
256
|
+
st_insert(rblineprof.files, (st_data_t)srcfile->filename, (st_data_t)srcfile);
|
257
|
+
} else { // no match, insert Qnil to prevent regex next time
|
258
|
+
st_insert(rblineprof.files, (st_data_t)strdup(filename), (st_data_t)Qnil);
|
259
|
+
return NULL;
|
260
|
+
}
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
return srcfile;
|
265
|
+
}
|
266
|
+
|
267
|
+
#ifdef RUBY_VM
|
268
|
+
/* Find the source of the current method call. This is based on rb_f_caller
|
269
|
+
* in vm_eval.c, and replicates the behavior of `caller.first` from ruby.
|
270
|
+
*
|
271
|
+
* On method calls, ruby 1.9 sends an extra RUBY_EVENT_CALL event with mid=0. The
|
272
|
+
* top-most cfp on the stack in these cases points to the 'def method' line, so we skip
|
273
|
+
* these and grab the second caller instead.
|
274
|
+
*/
|
275
|
+
static inline
|
276
|
+
rb_control_frame_t *
|
277
|
+
rb_vm_get_caller(rb_thread_t *th, rb_control_frame_t *cfp, ID mid)
|
278
|
+
{
|
279
|
+
int level = 0;
|
280
|
+
|
281
|
+
while (!RUBY_VM_CONTROL_FRAME_STACK_OVERFLOW_P(th, cfp)) {
|
282
|
+
if (++level == 1 && mid == 0) {
|
283
|
+
// skip method definition line
|
284
|
+
} else if (cfp->iseq != 0 && cfp->pc != 0) {
|
285
|
+
return cfp;
|
286
|
+
}
|
287
|
+
|
288
|
+
cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
|
289
|
+
}
|
290
|
+
|
291
|
+
return 0;
|
292
|
+
}
|
293
|
+
|
294
|
+
#ifdef HAVE_TYPE_RB_ISEQ_LOCATION_T
|
295
|
+
inline static int
|
296
|
+
calc_lineno(const rb_iseq_t *iseq, const VALUE *pc)
|
297
|
+
{
|
298
|
+
return rb_iseq_line_no(iseq, pc - iseq->iseq_encoded);
|
299
|
+
}
|
300
|
+
|
301
|
+
int
|
302
|
+
rb_vm_get_sourceline(const rb_control_frame_t *cfp)
|
303
|
+
{
|
304
|
+
int lineno = 0;
|
305
|
+
const rb_iseq_t *iseq = cfp->iseq;
|
306
|
+
|
307
|
+
if (RUBY_VM_NORMAL_ISEQ_P(iseq)) {
|
308
|
+
lineno = calc_lineno(cfp->iseq, cfp->pc);
|
309
|
+
}
|
310
|
+
return lineno;
|
311
|
+
}
|
312
|
+
#endif
|
313
|
+
#endif
|
314
|
+
|
315
|
+
static void
|
316
|
+
#ifdef RUBY_VM
|
317
|
+
profiler_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)
|
318
|
+
#else
|
319
|
+
profiler_hook(rb_event_flag_t event, NODE *node, VALUE self, ID mid, VALUE klass)
|
320
|
+
#endif
|
321
|
+
{
|
322
|
+
char *file;
|
323
|
+
long line;
|
324
|
+
stackframe_t *frame = NULL, *prev = NULL;
|
325
|
+
sourcefile_t *srcfile;
|
326
|
+
|
327
|
+
/* line profiler: maintain a stack of CALL events with timestamps. for
|
328
|
+
* each corresponding RETURN, account elapsed time to the calling line.
|
329
|
+
*
|
330
|
+
* we use ruby_current_node here to get the caller's file/line info,
|
331
|
+
* (as opposed to node, which points to the callee method being invoked)
|
332
|
+
*/
|
333
|
+
#ifndef RUBY_VM
|
334
|
+
NODE *caller_node = ruby_frame->node;
|
335
|
+
if (!caller_node) return;
|
336
|
+
|
337
|
+
file = caller_node->nd_file;
|
338
|
+
line = nd_line(caller_node);
|
339
|
+
#else
|
340
|
+
rb_thread_t *th = ruby_current_thread;
|
341
|
+
rb_control_frame_t *cfp = rb_vm_get_caller(th, th->cfp, mid);
|
342
|
+
if (!cfp) return;
|
343
|
+
|
344
|
+
#ifdef HAVE_TYPE_RB_ISEQ_LOCATION_T
|
345
|
+
if (RTEST(cfp->iseq->location.absolute_path))
|
346
|
+
file = StringValueCStr(cfp->iseq->location.absolute_path);
|
347
|
+
else
|
348
|
+
file = StringValueCStr(cfp->iseq->location.path);
|
349
|
+
#else
|
350
|
+
if (RTEST(cfp->iseq->filepath))
|
351
|
+
file = StringValueCStr(cfp->iseq->filepath);
|
352
|
+
else
|
353
|
+
file = StringValueCStr(cfp->iseq->filename);
|
354
|
+
#endif
|
355
|
+
line = rb_vm_get_sourceline(cfp);
|
356
|
+
#endif
|
357
|
+
|
358
|
+
if (!file) return;
|
359
|
+
if (line <= 0) return;
|
360
|
+
|
361
|
+
/* find the srcfile entry for the current file.
|
362
|
+
*
|
363
|
+
* first check the cache, in case this is the same file as
|
364
|
+
* the previous invocation.
|
365
|
+
*
|
366
|
+
* if no record is found, we don't care about profiling this
|
367
|
+
* file and return early.
|
368
|
+
*/
|
369
|
+
if (rblineprof.cache.file == file)
|
370
|
+
srcfile = rblineprof.cache.srcfile;
|
371
|
+
else
|
372
|
+
srcfile = sourcefile_lookup(file);
|
373
|
+
rblineprof.cache.file = file;
|
374
|
+
rblineprof.cache.srcfile = srcfile;
|
375
|
+
if (!srcfile) return; /* skip line profiling for this file */
|
376
|
+
|
377
|
+
snapshot_t now = {
|
378
|
+
.wall_time = walltime_usec(),
|
379
|
+
.cpu_time = cputime_usec(),
|
380
|
+
};
|
381
|
+
|
382
|
+
switch (event) {
|
383
|
+
case RUBY_EVENT_CALL:
|
384
|
+
case RUBY_EVENT_C_CALL:
|
385
|
+
/* Create a stack frame entry with this event,
|
386
|
+
* the current file, and a snapshot of metrics.
|
387
|
+
*
|
388
|
+
* On a corresponding RETURN event later, we can
|
389
|
+
* pop this stack frame and accumulate metrics to the
|
390
|
+
* associated file and line.
|
391
|
+
*/
|
392
|
+
rblineprof.stack_depth++;
|
393
|
+
if (rblineprof.stack_depth > 0 && rblineprof.stack_depth < MAX_STACK_DEPTH) {
|
394
|
+
frame = &rblineprof.stack[rblineprof.stack_depth-1];
|
395
|
+
frame->event = event;
|
396
|
+
frame->self = self;
|
397
|
+
frame->mid = mid;
|
398
|
+
frame->klass = klass;
|
399
|
+
frame->line = line;
|
400
|
+
frame->start = now;
|
401
|
+
frame->srcfile = srcfile;
|
402
|
+
#ifdef RUBY_VM
|
403
|
+
frame->thread = th;
|
404
|
+
#else
|
405
|
+
frame->node = node;
|
406
|
+
#endif
|
407
|
+
}
|
408
|
+
|
409
|
+
/* Record when we entered this file for the first time.
|
410
|
+
* The difference is later accumulated into exclusive_time,
|
411
|
+
* e.g. on the next event if the file changes.
|
412
|
+
*/
|
413
|
+
if (srcfile->depth == 0)
|
414
|
+
srcfile->exclusive_start = now;
|
415
|
+
srcfile->depth++;
|
416
|
+
|
417
|
+
if (rblineprof.stack_depth > 1) { // skip if outermost frame
|
418
|
+
prev = &rblineprof.stack[rblineprof.stack_depth-2];
|
419
|
+
|
420
|
+
/* If we just switched files, record time that was spent in
|
421
|
+
* the previous file.
|
422
|
+
*/
|
423
|
+
if (prev->srcfile != frame->srcfile) {
|
424
|
+
snapshot_t diff = snapshot_diff(&now, &prev->srcfile->exclusive_start);
|
425
|
+
snapshot_increment(&prev->srcfile->exclusive, &diff);
|
426
|
+
prev->srcfile->exclusive_start = now;
|
427
|
+
}
|
428
|
+
}
|
429
|
+
break;
|
430
|
+
|
431
|
+
case RUBY_EVENT_RETURN:
|
432
|
+
case RUBY_EVENT_C_RETURN:
|
433
|
+
/* Find the corresponding CALL for this event.
|
434
|
+
*
|
435
|
+
* We loop here instead of a simple pop, because in the event of a
|
436
|
+
* raise/rescue several stack frames could have disappeared.
|
437
|
+
*/
|
438
|
+
do {
|
439
|
+
if (rblineprof.stack_depth > 0 && rblineprof.stack_depth < MAX_STACK_DEPTH) {
|
440
|
+
frame = &rblineprof.stack[rblineprof.stack_depth-1];
|
441
|
+
if (frame->srcfile->depth > 0)
|
442
|
+
frame->srcfile->depth--;
|
443
|
+
} else
|
444
|
+
frame = NULL;
|
445
|
+
|
446
|
+
rblineprof.stack_depth--;
|
447
|
+
} while (frame &&
|
448
|
+
#ifdef RUBY_VM
|
449
|
+
frame->thread != th &&
|
450
|
+
#endif
|
451
|
+
/* Break when we find a matching CALL/C_CALL.
|
452
|
+
*/
|
453
|
+
frame->event != (event == RUBY_EVENT_CALL ? RUBY_EVENT_RETURN : RUBY_EVENT_C_RETURN) &&
|
454
|
+
frame->self != self &&
|
455
|
+
frame->mid != mid &&
|
456
|
+
frame->klass != klass);
|
457
|
+
|
458
|
+
if (rblineprof.stack_depth > 0) {
|
459
|
+
// The new top of the stack (that we're returning to)
|
460
|
+
prev = &rblineprof.stack[rblineprof.stack_depth-1];
|
461
|
+
|
462
|
+
/* If we're leaving this frame to go back to a different file,
|
463
|
+
* accumulate time we spent in this file.
|
464
|
+
*
|
465
|
+
* Note that we do this both when entering a new file and leaving to
|
466
|
+
* a new file to ensure we only count time spent exclusively in that file.
|
467
|
+
* Consider the following scenario:
|
468
|
+
*
|
469
|
+
* call (a.rb:1)
|
470
|
+
* call (b.rb:1) <-- leaving a.rb, increment into exclusive_time
|
471
|
+
* call (a.rb:5)
|
472
|
+
* return <-- leaving a.rb, increment into exclusive_time
|
473
|
+
* return
|
474
|
+
* return
|
475
|
+
*/
|
476
|
+
if (frame->srcfile != prev->srcfile) {
|
477
|
+
snapshot_t diff = snapshot_diff(&now, &frame->srcfile->exclusive_start);
|
478
|
+
snapshot_increment(&frame->srcfile->exclusive, &diff);
|
479
|
+
frame->srcfile->exclusive_start = now;
|
480
|
+
prev->srcfile->exclusive_start = now;
|
481
|
+
}
|
482
|
+
}
|
483
|
+
|
484
|
+
if (frame)
|
485
|
+
stackframe_record(frame, now, prev);
|
486
|
+
|
487
|
+
break;
|
488
|
+
}
|
489
|
+
}
|
490
|
+
|
491
|
+
static int
|
492
|
+
cleanup_files(st_data_t key, st_data_t record, st_data_t arg)
|
493
|
+
{
|
494
|
+
xfree((char *)key);
|
495
|
+
|
496
|
+
sourcefile_t *sourcefile = (sourcefile_t*)record;
|
497
|
+
if (!sourcefile || (VALUE)sourcefile == Qnil) return ST_DELETE;
|
498
|
+
|
499
|
+
if (sourcefile->lines)
|
500
|
+
xfree(sourcefile->lines);
|
501
|
+
xfree(sourcefile);
|
502
|
+
|
503
|
+
return ST_DELETE;
|
504
|
+
}
|
505
|
+
|
506
|
+
static int
|
507
|
+
summarize_files(st_data_t key, st_data_t record, st_data_t arg)
|
508
|
+
{
|
509
|
+
sourcefile_t *srcfile = (sourcefile_t*)record;
|
510
|
+
if (!srcfile || (VALUE)srcfile == Qnil) return ST_CONTINUE;
|
511
|
+
|
512
|
+
VALUE ret = (VALUE)arg;
|
513
|
+
VALUE ary = rb_ary_new();
|
514
|
+
long i;
|
515
|
+
|
516
|
+
rb_ary_store(ary, 0, rb_ary_new3(6,
|
517
|
+
ULL2NUM(srcfile->total.wall_time),
|
518
|
+
ULL2NUM(srcfile->child.wall_time),
|
519
|
+
ULL2NUM(srcfile->exclusive.wall_time),
|
520
|
+
ULL2NUM(srcfile->total.cpu_time),
|
521
|
+
ULL2NUM(srcfile->child.cpu_time),
|
522
|
+
ULL2NUM(srcfile->exclusive.cpu_time)
|
523
|
+
));
|
524
|
+
|
525
|
+
for (i=1; i<srcfile->nlines; i++)
|
526
|
+
rb_ary_store(ary, i, rb_ary_new3(3,
|
527
|
+
ULL2NUM(srcfile->lines[i].total.wall_time),
|
528
|
+
ULL2NUM(srcfile->lines[i].total.cpu_time),
|
529
|
+
ULL2NUM(srcfile->lines[i].calls)
|
530
|
+
));
|
531
|
+
rb_hash_aset(ret, rb_str_new2(srcfile->filename), ary);
|
532
|
+
|
533
|
+
return ST_CONTINUE;
|
534
|
+
}
|
535
|
+
|
536
|
+
static VALUE
|
537
|
+
lineprof_ensure(VALUE self)
|
538
|
+
{
|
539
|
+
rb_remove_event_hook((rb_event_hook_func_t) profiler_hook);
|
540
|
+
rblineprof.enabled = false;
|
541
|
+
return self;
|
542
|
+
}
|
543
|
+
|
544
|
+
VALUE
|
545
|
+
lineprof(VALUE self, VALUE filename)
|
546
|
+
{
|
547
|
+
if (!rb_block_given_p())
|
548
|
+
rb_raise(rb_eArgError, "block required");
|
549
|
+
|
550
|
+
if (rblineprof.enabled)
|
551
|
+
rb_raise(rb_eArgError, "profiler is already enabled");
|
552
|
+
|
553
|
+
VALUE filename_class = rb_obj_class(filename);
|
554
|
+
|
555
|
+
if (filename_class == rb_cString) {
|
556
|
+
#ifdef RUBY_VM
|
557
|
+
rblineprof.source_filename = (char *) (StringValuePtr(filename));
|
558
|
+
#else
|
559
|
+
/* rb_source_filename will return a string we can compare directly against
|
560
|
+
* node->file, without a strcmp()
|
561
|
+
*/
|
562
|
+
rblineprof.source_filename = rb_source_filename(StringValuePtr(filename));
|
563
|
+
#endif
|
564
|
+
} else if (filename_class == rb_cRegexp) {
|
565
|
+
rblineprof.source_regex = filename;
|
566
|
+
rblineprof.source_filename = NULL;
|
567
|
+
} else {
|
568
|
+
rb_raise(rb_eArgError, "argument must be String or Regexp");
|
569
|
+
}
|
570
|
+
|
571
|
+
// reset state
|
572
|
+
st_foreach(rblineprof.files, cleanup_files, 0);
|
573
|
+
if (rblineprof.file.lines) {
|
574
|
+
xfree(rblineprof.file.lines);
|
575
|
+
rblineprof.file.lines = NULL;
|
576
|
+
rblineprof.file.nlines = 0;
|
577
|
+
}
|
578
|
+
rblineprof.cache.file = NULL;
|
579
|
+
rblineprof.cache.srcfile = NULL;
|
580
|
+
|
581
|
+
rblineprof.enabled = true;
|
582
|
+
#ifndef RUBY_VM
|
583
|
+
rb_add_event_hook((rb_event_hook_func_t) profiler_hook, RUBY_EVENT_CALL|RUBY_EVENT_RETURN|RUBY_EVENT_C_CALL|RUBY_EVENT_C_RETURN);
|
584
|
+
#else
|
585
|
+
rb_add_event_hook((rb_event_hook_func_t) profiler_hook, RUBY_EVENT_CALL|RUBY_EVENT_RETURN|RUBY_EVENT_C_CALL|RUBY_EVENT_C_RETURN, Qnil);
|
586
|
+
#endif
|
587
|
+
|
588
|
+
rb_ensure(rb_yield, Qnil, lineprof_ensure, self);
|
589
|
+
|
590
|
+
VALUE ret = rb_hash_new();
|
591
|
+
VALUE ary = Qnil;
|
592
|
+
|
593
|
+
if (rblineprof.source_filename) {
|
594
|
+
summarize_files(Qnil, (st_data_t)&rblineprof.file, ret);
|
595
|
+
} else {
|
596
|
+
st_foreach(rblineprof.files, summarize_files, ret);
|
597
|
+
}
|
598
|
+
|
599
|
+
return ret;
|
600
|
+
}
|
601
|
+
|
602
|
+
static void
|
603
|
+
rblineprof_gc_mark()
|
604
|
+
{
|
605
|
+
if (rblineprof.enabled)
|
606
|
+
rb_gc_mark_maybe(rblineprof.source_regex);
|
607
|
+
}
|
608
|
+
|
609
|
+
void
|
610
|
+
Init_rblineprof()
|
611
|
+
{
|
612
|
+
gc_hook = Data_Wrap_Struct(rb_cObject, rblineprof_gc_mark, NULL, NULL);
|
613
|
+
rb_global_variable(&gc_hook);
|
614
|
+
|
615
|
+
rblineprof.files = st_init_strtable();
|
616
|
+
rb_define_global_function("lineprof", lineprof, 1);
|
617
|
+
}
|
618
|
+
|
619
|
+
/* vim: set ts=2 sw=2 expandtab: */
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'pilfer/profile'
|
2
|
+
|
3
|
+
module Pilfer
|
4
|
+
class Logger
|
5
|
+
attr_reader :path, :app_root
|
6
|
+
|
7
|
+
def initialize(path, options = {})
|
8
|
+
@path = path
|
9
|
+
if (app_root = options[:app_root])
|
10
|
+
app_root += '/' unless app_root[-1] == '/'
|
11
|
+
@app_root = %r{^#{Regexp.escape(app_root)}}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(profile_data, profile_start)
|
16
|
+
profile = Pilfer::Profile.new(profile_data, profile_start)
|
17
|
+
File.open(path, 'w') do |file|
|
18
|
+
print_report_banner file, profile_start
|
19
|
+
profile.each do |path, data|
|
20
|
+
print_file_banner file, path, data
|
21
|
+
print_file_source_with_profile file, path, data
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def print_report_banner(file, profile_start)
|
29
|
+
file.puts '#' * 50
|
30
|
+
file.puts "# #{profile_start.utc.to_s}"
|
31
|
+
file.puts '#' * 50
|
32
|
+
file.puts
|
33
|
+
end
|
34
|
+
|
35
|
+
def print_file_banner(file, path, data)
|
36
|
+
wall = data['wall_time'] / 1000.0
|
37
|
+
cpu = data['cpu_time'] / 1000.0
|
38
|
+
file.puts sprintf("%s wall_time=%.1fms cpu_time=%.1fms",
|
39
|
+
strip_app_root(path), wall, cpu)
|
40
|
+
end
|
41
|
+
|
42
|
+
def print_file_source_with_profile(file, path, data)
|
43
|
+
File.readlines(path).each_with_index do |line_source, index|
|
44
|
+
line_profile = data['lines'][index]
|
45
|
+
if line_profile && line_profile['calls'] > 0
|
46
|
+
total = line_profile['wall_time']
|
47
|
+
file.puts sprintf("% 8.1fms (% 5d) | %s",
|
48
|
+
total/1000.0,
|
49
|
+
line_profile['calls'],
|
50
|
+
line_source)
|
51
|
+
else
|
52
|
+
file.puts sprintf(" | %s", line_source)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
file.puts
|
56
|
+
end
|
57
|
+
|
58
|
+
def strip_app_root(path)
|
59
|
+
return path unless app_root
|
60
|
+
path.gsub(app_root, '')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Pilfer
|
2
|
+
class Profile
|
3
|
+
include Enumerable
|
4
|
+
attr_accessor :data, :start
|
5
|
+
|
6
|
+
def initialize(data, start)
|
7
|
+
@data = data
|
8
|
+
@start = start
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
files.each(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def files
|
16
|
+
data.each_with_object({}) do |(file, lines), files|
|
17
|
+
profile_lines = lines[1..-1].
|
18
|
+
each_with_index.
|
19
|
+
each_with_object({}) do |(data, number), lines|
|
20
|
+
next unless data.any? {|datum| datum > 0 }
|
21
|
+
wall_time, cpu_time, calls = data
|
22
|
+
lines[number] = { 'wall_time' => wall_time,
|
23
|
+
'cpu_time' => cpu_time,
|
24
|
+
'calls' => calls }
|
25
|
+
end
|
26
|
+
|
27
|
+
total_wall, child_wall, exclusive_wall,
|
28
|
+
total_cpu, child_cpu, exclusive_cpu = lines[0]
|
29
|
+
|
30
|
+
wall = total_wall
|
31
|
+
cpu = total_cpu
|
32
|
+
|
33
|
+
files[file] = { 'wall_time' => wall,
|
34
|
+
'cpu_time' => cpu,
|
35
|
+
'lines' => profile_lines }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rblineprof'
|
2
|
+
|
3
|
+
module Pilfer
|
4
|
+
class Profiler
|
5
|
+
attr_reader :reporter
|
6
|
+
|
7
|
+
def initialize(reporter)
|
8
|
+
@reporter = reporter
|
9
|
+
end
|
10
|
+
|
11
|
+
def profile(profiler = method(:lineprof), start = Time.now, &app)
|
12
|
+
profile_files_matching(/./, profiler, start, &app)
|
13
|
+
end
|
14
|
+
|
15
|
+
def profile_files_matching(matcher, profiler = method(:lineprof),
|
16
|
+
start = Time.now, &app)
|
17
|
+
app_response = nil
|
18
|
+
profile = profiler.call(matcher) do
|
19
|
+
app_response = app.call
|
20
|
+
end
|
21
|
+
reporter.write profile, start
|
22
|
+
app_response
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/pilfer.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'pilfer/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'pilfer'
|
7
|
+
spec.version = Pilfer::VERSION
|
8
|
+
spec.summary = 'Look into your ruby with rblineprof'
|
9
|
+
spec.authors = ['Eric Lindvall', 'Larry Marburger']
|
10
|
+
spec.email = ['eric@sevenscale.com', 'larry@marburger.cc']
|
11
|
+
spec.homepage = 'https://github.com/eric/pilfer'
|
12
|
+
spec.license = 'MIT'
|
13
|
+
|
14
|
+
spec.files = %w(Gemfile LICENSE README.md)
|
15
|
+
spec.files << 'pilfer.gemspec'
|
16
|
+
spec.files += Dir.glob('lib/**/*.rb')
|
17
|
+
spec.files += Dir.glob('test/**/*.rb')
|
18
|
+
spec.files += Dir.glob('script/*')
|
19
|
+
spec.test_files = Dir.glob('test/**/*.rb')
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.0'
|
22
|
+
spec.add_development_dependency 'rspec'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rack', '~> 1.5.2'
|
25
|
+
|
26
|
+
spec.required_rubygems_version = '>= 1.3.6'
|
27
|
+
|
28
|
+
# Bundled latest rblineprof
|
29
|
+
spec.extensions = 'ext/extconf.rb'
|
30
|
+
spec.files += Dir.glob('ext/*')
|
31
|
+
spec.add_dependency 'debugger-ruby_core_source', '~> 1.2'
|
32
|
+
end
|
data/script/package
ADDED
data/script/release
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
# Usage: script/release
|
3
|
+
# Build the package, tag a commit, push it to origin, and then release the
|
4
|
+
# package publicly.
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
version="$(script/package | grep Version: | awk '{print $2}')"
|
9
|
+
[ -n "$version" ] || exit 1
|
10
|
+
|
11
|
+
git commit --allow-empty -a -m "Release $version"
|
12
|
+
git tag "v$version"
|
13
|
+
git push origin
|
14
|
+
git push origin "v$version"
|
15
|
+
gem push pkg/*-${version}.gem
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pilfer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.pre
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eric Lindvall
|
8
|
+
- Larry Marburger
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ~>
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.0'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rake
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rack
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ~>
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.5.2
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.5.2
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: debugger-ruby_core_source
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '1.2'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ~>
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '1.2'
|
84
|
+
description:
|
85
|
+
email:
|
86
|
+
- eric@sevenscale.com
|
87
|
+
- larry@marburger.cc
|
88
|
+
executables: []
|
89
|
+
extensions:
|
90
|
+
- ext/extconf.rb
|
91
|
+
extra_rdoc_files: []
|
92
|
+
files:
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE
|
95
|
+
- README.md
|
96
|
+
- pilfer.gemspec
|
97
|
+
- lib/pilfer/logger.rb
|
98
|
+
- lib/pilfer/profile.rb
|
99
|
+
- lib/pilfer/profiler.rb
|
100
|
+
- lib/pilfer/version.rb
|
101
|
+
- script/package
|
102
|
+
- script/release
|
103
|
+
- ext/extconf.rb
|
104
|
+
- ext/rblineprof.c
|
105
|
+
homepage: https://github.com/eric/pilfer
|
106
|
+
licenses:
|
107
|
+
- MIT
|
108
|
+
metadata: {}
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ! '>='
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 1.3.6
|
123
|
+
requirements: []
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 2.0.3
|
126
|
+
signing_key:
|
127
|
+
specification_version: 4
|
128
|
+
summary: Look into your ruby with rblineprof
|
129
|
+
test_files: []
|