pilfer 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|