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 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: []