pilfer 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rblineprof', github: 'tmm1/rblineprof'
4
+ gemspec
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
@@ -0,0 +1,3 @@
1
+ module Pilfer
2
+ VERSION = '0.0.1.pre'
3
+ 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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: script/package
3
+ # Updates the gemspec and builds a new gem in the pkg directory.
4
+
5
+ mkdir -p pkg
6
+ gem build *.gemspec
7
+ mv *.gem pkg
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: []