├── ext ├── .gitignore ├── extconf.rb └── rblineprof.c ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── rblineprof.gemspec ├── Rakefile ├── LICENSE ├── test └── test_lineprof.rb ├── test.rb └── README.md /ext/.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | *.bundle 3 | *.o 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | /lib/*.bundle 3 | /lib/*.so 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | rblineprof (0.3.5) 5 | debugger-ruby_core_source (~> 1.3) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | debugger-ruby_core_source (1.3.1) 11 | rake (10.1.1) 12 | rake-compiler (0.9.2) 13 | rake 14 | 15 | PLATFORMS 16 | ruby 17 | 18 | DEPENDENCIES 19 | rake-compiler 20 | rblineprof! 21 | -------------------------------------------------------------------------------- /rblineprof.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'rblineprof' 3 | s.version = '0.3.7' 4 | s.homepage = 'http://github.com/tmm1/rblineprof' 5 | 6 | s.authors = 'Aman Gupta' 7 | s.email = 'aman@tmm1.net' 8 | 9 | s.files = `git ls-files`.split("\n") 10 | s.extensions = 'ext/extconf.rb' 11 | 12 | s.summary = 'line-profiler for ruby' 13 | s.description = 'rblineprof shows you lines of code that are slow.' 14 | 15 | s.license = 'MIT' 16 | 17 | s.add_dependency 'debugger-ruby_core_source', '~> 1.3' 18 | s.add_development_dependency 'rake-compiler' 19 | end 20 | -------------------------------------------------------------------------------- /ext/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | have_func('rb_os_allocated_objects') 4 | 5 | if RUBY_VERSION >= "2.1" 6 | have_func('rb_gc_stat') 7 | have_func('rb_profile_frames') 8 | have_func('rb_tracepoint_new') 9 | create_makefile 'rblineprof' 10 | elsif RUBY_VERSION >= "1.9" 11 | require "debugger/ruby_core_source" 12 | 13 | hdrs = proc { 14 | have_type("rb_iseq_location_t", "vm_core.h") 15 | 16 | have_header("vm_core.h") and 17 | (have_header("iseq.h") or have_header("iseq.h", ["vm_core.h"])) 18 | } 19 | 20 | unless Debugger::RubyCoreSource::create_makefile_with_core(hdrs, "rblineprof") 21 | STDERR.puts "\nDebugger::RubyCoreSource::create_makefile failed" 22 | exit(1) 23 | end 24 | else 25 | create_makefile 'rblineprof' 26 | end 27 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => :test 2 | 3 | # ========================================================== 4 | # Packaging 5 | # ========================================================== 6 | 7 | GEMSPEC = eval(File.read('rblineprof.gemspec')) 8 | 9 | require 'rubygems/package_task' 10 | Gem::PackageTask.new(GEMSPEC) do |pkg| 11 | end 12 | 13 | # ========================================================== 14 | # Ruby Extension 15 | # ========================================================== 16 | 17 | require 'rake/extensiontask' 18 | Rake::ExtensionTask.new('rblineprof', GEMSPEC) do |ext| 19 | ext.ext_dir = 'ext' 20 | end 21 | task :build => :compile 22 | 23 | # ========================================================== 24 | # Testing 25 | # ========================================================== 26 | 27 | require 'rake/testtask' 28 | Rake::TestTask.new 'test' do |t| 29 | t.test_files = FileList['test/test_*.rb'] 30 | end 31 | task :test => :build 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012 Aman Gupta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test_lineprof.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../../lib', __FILE__) 2 | require 'rblineprof' 3 | require 'test/unit' 4 | 5 | class LineProfTest < Test::Unit::TestCase 6 | def test_real 7 | profile = lineprof(/./) do 8 | sleep 0.001 9 | end 10 | 11 | line = profile[__FILE__][__LINE__-3] 12 | assert_in_delta 1000, line[0], 600 13 | assert_equal 1, line[2] 14 | end 15 | 16 | def test_cpu 17 | profile = lineprof(/./) do 18 | (fibonacci = Hash.new{ |h,k| h[k] = k < 2 ? k : h[k-1] + h[k-2] })[500] 19 | end 20 | 21 | line = profile[__FILE__][__LINE__-3] 22 | assert_operator line[1], :>=, 800 23 | end 24 | 25 | def test_objects 26 | profile = lineprof(/./) do 27 | 100.times{ "str" } 28 | end 29 | 30 | line = profile[__FILE__][__LINE__-3] 31 | assert_equal 100, line[3] 32 | end 33 | 34 | def test_method 35 | profile = lineprof(/./) do 36 | 100.times{ helper_method } 37 | end 38 | 39 | m = method(:helper_method) 40 | line = profile[__FILE__][m.source_location.last] 41 | assert_equal 0, line[0] 42 | end 43 | 44 | def helper_method 45 | sleep 0.001 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | $:.unshift 'lib' 2 | require 'rblineprof' 3 | 4 | class Obj 5 | define_method(:inner_block) do 6 | sleep 0.001 7 | end 8 | 9 | def another(options={}) 10 | sleep 0.001 11 | end 12 | 13 | def out=(*) 14 | end 15 | 16 | def with_defaults(arg=self.object_id.to_s) 17 | another 18 | list = [1,2,3] 19 | # for cookie in list 20 | # self.out=( 21 | dummy( 22 | 1, "str 23 | ing") 24 | dummy <<-EOS 25 | hi 26 | EOS 27 | dummy \ 28 | 1234 29 | dummy :a => 'b', 30 | :c => 'd', 31 | :e => 1024**1024, 32 | 'something' => dummy(:ok) 33 | # ) 34 | # end 35 | end 36 | 37 | def dummy(*args) 38 | args.inspect 39 | end 40 | 41 | class_eval <<-RUBY, 'otherfile.rb', 1 42 | def other_file 43 | another 44 | end 45 | RUBY 46 | end 47 | 48 | def inner 49 | sleep 0.001 50 | 1*2*3 51 | 4*5*6 52 | 7*8*9*10*11*12*13*14*15 53 | 2**32 54 | 2**128 55 | 56 | o = Obj.new 57 | o.inner_block 58 | o.another 59 | o.other_file 60 | o.with_defaults 61 | end 62 | 63 | def outer 64 | sleep 0.01 65 | 66 | 3000.times{ 2**1024 } 67 | for i in 1..3000 do 2**1024 end 68 | 69 | for i in 1..3000 70 | 2**1024 71 | end 72 | 73 | (fibonacci = Hash.new{ |h,k| h[k] = k < 2 ? k : h[k-1] + h[k-2] })[500] 74 | 75 | (fibonacci = Hash.new{ |h,k| 76 | h[k] = k < 2 ? 77 | k : 78 | h[k-1] + 79 | h[k-2] 80 | }) 81 | fibonacci[500] 82 | 83 | 100.times do 84 | inner 85 | end 86 | 87 | inner 88 | 89 | (0..10).map do |i| 90 | Thread.new(i) do 91 | inner 92 | end 93 | end.each(&:join) 94 | end 95 | 96 | file = RUBY_VERSION > '1.9' ? File.expand_path(__FILE__) : __FILE__ 97 | 98 | # profile = lineprof(file) do 99 | profile = lineprof(/./) do 100 | outer 101 | 102 | 100.times{ 1 } 103 | 100.times{ 1 + 1 } 104 | 100.times{ 1.1 } 105 | 100.times{ 1.1 + 1 } 106 | 100.times{ 1.1 + 1.1 } 107 | 100.times{ "str" } 108 | ('a'..'z').to_a 109 | end 110 | 111 | allocation_mode = false 112 | 113 | File.readlines(file).each_with_index do |line, num| 114 | wall, cpu, calls, allocations = profile[file][num+1] 115 | 116 | if allocation_mode 117 | if allocations > 0 118 | printf "% 10d objs | %s", allocations, line 119 | else 120 | printf " | %s", line 121 | end 122 | 123 | next 124 | end 125 | 126 | if calls && calls > 0 127 | printf "% 8.1fms + % 8.1fms (% 5d) | %s", cpu/1000.0, (wall-cpu)/1000.0, calls, line 128 | # printf "% 8.1fms (% 5d) | %s", wall/1000.0, calls, line 129 | else 130 | printf " | %s", line 131 | # printf " | %s", line 132 | end 133 | end 134 | 135 | puts 136 | profile.each do |file, data| 137 | total, child, exclusive, allocations = data[0] 138 | puts file 139 | printf " % 10.1fms in this file\n", exclusive/1000.0 140 | printf " % 10.1fms in this file + children\n", total/1000.0 141 | printf " % 10.1fms in children\n", child/1000.0 142 | puts 143 | end 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rblineprof 2 | 3 | ## Installation 4 | 5 | `gem install rblineprof` 6 | 7 | Or in your Gemfile: 8 | 9 | `gem 'rblineprof'` 10 | 11 | ## Usage 12 | 13 | ``` 14 | require 'rblineprof' 15 | 16 | profile = lineprof(/./) do 17 | sleep 0.001 18 | 19 | 100.times do 20 | sleep 0.001 21 | 22 | 1*2*3 23 | 4*5*6 24 | 7*8*9*10*11*12*13*14*15 25 | 2**32 26 | 2**128 27 | end 28 | end 29 | 30 | file = profile.keys.first 31 | 32 | File.readlines(file).each_with_index do |line, num| 33 | wall, cpu, calls, allocations = profile[file][num + 1] 34 | 35 | if wall > 0 || cpu > 0 || calls > 0 36 | printf( 37 | "% 5.1fms + % 6.1fms (% 4d) | %s", 38 | cpu / 1000.0, 39 | (wall - cpu) / 1000.0, 40 | calls, 41 | line 42 | ) 43 | else 44 | printf " | %s", line 45 | end 46 | end 47 | ``` 48 | 49 | Will give you: 50 | 51 | ``` 52 | | require 'rblineprof' 53 | | 54 | | profile = lineprof(/./) do 55 | 0.1ms + 1.4ms ( 1) | sleep 0.001 56 | | 57 | 2.7ms + 132.2ms ( 1) | 100.times do 58 | 1.3ms + 131.7ms ( 100) | sleep 0.001 59 | | 60 | | 1*2*3 61 | | 4*5*6 62 | | 7*8*9*10*11*12*13*14*15 63 | 0.1ms + 0.1ms ( 100) | 2**32 64 | 0.6ms + 0.1ms ( 100) | 2**128 65 | | end 66 | | end 67 | | 68 | | file = profile.keys.first 69 | | 70 | | File.readlines(file).each_with_index do |line, num| 71 | | wall, cpu, calls, allocations = profile[file][num + 1] 72 | | 73 | | if wall > 0 || cpu > 0 || calls > 0 74 | | printf( 75 | | "% 5.1fms + % 6.1fms (% 4d) | %s", 76 | | cpu / 1000.0, 77 | | (wall - cpu) / 1000.0, 78 | | calls, 79 | | line 80 | | ) 81 | | else 82 | | printf " | %s", line 83 | | end 84 | | end 85 | 86 | ``` 87 | 88 | ### Rails integration 89 | 90 | * [peek-rblineprof](https://github.com/peek/peek-rblineprof#peekrblineprof) 91 | 92 | ## Other profilers 93 | 94 | * [PLine](https://github.com/soba1104/PLine) line-profiler for ruby 1.9 95 | * pure-ruby [LineProfiler](http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/18997?help-en) for ruby 1.6 96 | * [method_profiler](https://github.com/change/method_profiler) 97 | * [ruby-prof](https://github.com/rdp/ruby-prof) 98 | * [perftools.rb](https://github.com/tmm1/perftools.rb) 99 | * [zenprofile](https://github.com/seattlerb/zenprofile) 100 | 101 | ## License 102 | 103 | rblineprof is released under the [MIT License](http://www.opensource.org/licenses/MIT). 104 | -------------------------------------------------------------------------------- /ext/rblineprof.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #if defined(RUBY_VM) 9 | #include 10 | #include 11 | 12 | #if defined(HAVE_RB_PROFILE_FRAMES) 13 | #include 14 | #else 15 | #include 16 | #include 17 | 18 | // There's a compile error on 1.9.3. So: 19 | #ifdef RTYPEDDATA_DATA 20 | #define ruby_current_thread ((rb_thread_t *)RTYPEDDATA_DATA(rb_thread_current())) 21 | #endif 22 | #endif 23 | #else 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | typedef rb_event_t rb_event_flag_t; 30 | #endif 31 | 32 | #if defined(HAVE_RB_OS_ALLOCATED_OBJECTS) && defined(RUBY_VM) 33 | size_t rb_os_allocated_objects(void); 34 | #endif 35 | 36 | static VALUE gc_hook; 37 | static VALUE sym_total_allocated_objects; 38 | 39 | /* 40 | * Time in microseconds 41 | */ 42 | typedef uint64_t prof_time_t; 43 | 44 | /* 45 | * Profiling snapshot 46 | */ 47 | typedef struct snapshot { 48 | prof_time_t wall_time; 49 | prof_time_t cpu_time; 50 | #if defined(HAVE_RB_OS_ALLOCATED_OBJECTS) || defined(HAVE_RB_GC_STAT) 51 | size_t allocated_objects; 52 | #endif 53 | } snapshot_t; 54 | 55 | /* 56 | * A line of Ruby source code 57 | */ 58 | typedef struct sourceline { 59 | uint64_t calls; // total number of call/c_call events 60 | snapshot_t total; 61 | } sourceline_t; 62 | 63 | /* 64 | * Struct representing a single Ruby file. 65 | */ 66 | typedef struct sourcefile { 67 | char *filename; 68 | 69 | /* per line timing */ 70 | long nlines; 71 | sourceline_t *lines; 72 | 73 | /* overall file timing */ 74 | snapshot_t total; 75 | snapshot_t child; 76 | uint64_t depth; 77 | snapshot_t exclusive_start; 78 | snapshot_t exclusive; 79 | } sourcefile_t; 80 | 81 | /* 82 | * An individual stack frame used to track 83 | * calls and returns from Ruby methods 84 | */ 85 | typedef struct stackframe { 86 | // data emitted from Ruby to our profiler hook 87 | rb_event_flag_t event; 88 | #if defined(HAVE_RB_PROFILE_FRAMES) 89 | VALUE thread; 90 | #elif defined(RUBY_VM) 91 | rb_thread_t *thread; 92 | #else 93 | NODE *node; 94 | #endif 95 | VALUE self; 96 | ID mid; 97 | VALUE klass; 98 | 99 | char *filename; 100 | long line; 101 | 102 | snapshot_t start; 103 | sourcefile_t *srcfile; 104 | } stackframe_t; 105 | 106 | /* 107 | * Static properties and rbineprof configuration 108 | */ 109 | static struct { 110 | bool enabled; 111 | 112 | // stack 113 | #define MAX_STACK_DEPTH 32768 114 | stackframe_t stack[MAX_STACK_DEPTH]; 115 | uint64_t stack_depth; 116 | 117 | // single file mode, store filename and line data directly 118 | char *source_filename; 119 | sourcefile_t file; 120 | 121 | // regex mode, store file data in hash table 122 | VALUE source_regex; 123 | st_table *files; 124 | 125 | // cache 126 | struct { 127 | char *file; 128 | sourcefile_t *srcfile; 129 | } cache; 130 | } 131 | rblineprof = { 132 | .enabled = false, 133 | .source_regex = Qfalse 134 | }; 135 | 136 | static prof_time_t 137 | cputime_usec() 138 | { 139 | #if defined(__linux__) 140 | struct timespec ts; 141 | 142 | if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts) == 0) { 143 | return (prof_time_t)ts.tv_sec*1e6 + 144 | (prof_time_t)ts.tv_nsec*1e-3; 145 | } 146 | #endif 147 | 148 | #if defined(RUSAGE_SELF) 149 | struct rusage usage; 150 | 151 | getrusage(RUSAGE_SELF, &usage); 152 | return (prof_time_t)usage.ru_utime.tv_sec*1e6 + 153 | (prof_time_t)usage.ru_utime.tv_usec; 154 | #endif 155 | 156 | return 0; 157 | } 158 | 159 | static prof_time_t 160 | walltime_usec() 161 | { 162 | struct timeval tv; 163 | gettimeofday(&tv, NULL); 164 | return (prof_time_t)tv.tv_sec*1e6 + 165 | (prof_time_t)tv.tv_usec; 166 | } 167 | 168 | static inline snapshot_t 169 | snapshot_diff(snapshot_t *t1, snapshot_t *t2) 170 | { 171 | snapshot_t diff = { 172 | .wall_time = t1->wall_time - t2->wall_time, 173 | .cpu_time = t1->cpu_time - t2->cpu_time, 174 | #if defined(HAVE_RB_OS_ALLOCATED_OBJECTS) || defined(HAVE_RB_GC_STAT) 175 | .allocated_objects = t1->allocated_objects - t2->allocated_objects 176 | #endif 177 | }; 178 | 179 | return diff; 180 | } 181 | 182 | static inline void 183 | snapshot_increment(snapshot_t *s, snapshot_t *inc) 184 | { 185 | s->wall_time += inc->wall_time; 186 | s->cpu_time += inc->cpu_time; 187 | #if defined(HAVE_RB_OS_ALLOCATED_OBJECTS) || defined(HAVE_RB_GC_STAT) 188 | s->allocated_objects += inc->allocated_objects; 189 | #endif 190 | } 191 | 192 | static inline void 193 | stackframe_record(stackframe_t *frame, snapshot_t now, stackframe_t *caller_frame) 194 | { 195 | sourcefile_t *srcfile = frame->srcfile; 196 | long line = frame->line; 197 | 198 | /* allocate space for per-line data the first time */ 199 | if (srcfile->lines == NULL) { 200 | srcfile->nlines = line + 100; 201 | srcfile->lines = ALLOC_N(sourceline_t, srcfile->nlines); 202 | MEMZERO(srcfile->lines, sourceline_t, srcfile->nlines); 203 | } 204 | 205 | /* grow the per-line array if necessary */ 206 | if (line >= srcfile->nlines) { 207 | long prev_nlines = srcfile->nlines; 208 | srcfile->nlines = line + 100; 209 | 210 | REALLOC_N(srcfile->lines, sourceline_t, srcfile->nlines); 211 | MEMZERO(srcfile->lines + prev_nlines, sourceline_t, srcfile->nlines - prev_nlines); 212 | } 213 | 214 | snapshot_t diff = snapshot_diff(&now, &frame->start); 215 | sourceline_t *srcline = &(srcfile->lines[line]); 216 | 217 | /* Line profiler metrics. 218 | */ 219 | 220 | srcline->calls++; 221 | 222 | /* Increment current line's total_time. 223 | * 224 | * Skip the special case where the stack frame we're returning to 225 | * had the same file/line. This fixes double counting on crazy one-liners. 226 | */ 227 | if (!(caller_frame && caller_frame->srcfile == frame->srcfile && caller_frame->line == frame->line)) 228 | snapshot_increment(&srcline->total, &diff); 229 | 230 | /* File profiler metrics. 231 | */ 232 | 233 | /* Increment the caller file's child_time. 234 | */ 235 | if (caller_frame && caller_frame->srcfile != srcfile) 236 | snapshot_increment(&caller_frame->srcfile->child, &diff); 237 | 238 | /* Increment current file's total_time, but only when we return 239 | * to the outermost stack frame when we first entered the file. 240 | */ 241 | if (srcfile->depth == 0) 242 | snapshot_increment(&srcfile->total, &diff); 243 | } 244 | 245 | static inline sourcefile_t* 246 | sourcefile_lookup(char *filename) 247 | { 248 | sourcefile_t *srcfile = NULL; 249 | 250 | if (rblineprof.source_filename) { // single file mode 251 | #ifdef RUBY_VM 252 | if (strcmp(rblineprof.source_filename, filename) == 0) { 253 | #else 254 | if (rblineprof.source_filename == filename) { // compare char*, not contents 255 | #endif 256 | srcfile = &rblineprof.file; 257 | srcfile->filename = filename; 258 | } else { 259 | return NULL; 260 | } 261 | 262 | } else { // regex mode 263 | st_lookup(rblineprof.files, (st_data_t)filename, (st_data_t *)&srcfile); 264 | 265 | if ((VALUE)srcfile == Qnil) // known negative match, skip 266 | return NULL; 267 | 268 | if (!srcfile) { // unknown file, check against regex 269 | VALUE backref = rb_backref_get(); 270 | rb_match_busy(backref); 271 | long rc = rb_reg_search(rblineprof.source_regex, rb_str_new2(filename), 0, 0); 272 | rb_backref_set(backref); 273 | 274 | if (rc >= 0) { 275 | srcfile = ALLOC_N(sourcefile_t, 1); 276 | MEMZERO(srcfile, sourcefile_t, 1); 277 | srcfile->filename = strdup(filename); 278 | st_insert(rblineprof.files, (st_data_t)srcfile->filename, (st_data_t)srcfile); 279 | } else { // no match, insert Qnil to prevent regex next time 280 | st_insert(rblineprof.files, (st_data_t)strdup(filename), (st_data_t)Qnil); 281 | return NULL; 282 | } 283 | } 284 | } 285 | 286 | return srcfile; 287 | } 288 | 289 | #if defined(RUBY_VM) && !defined(HAVE_RB_PROFILE_FRAMES) 290 | /* Find the source of the current method call. This is based on rb_f_caller 291 | * in vm_eval.c, and replicates the behavior of `caller.first` from ruby. 292 | * 293 | * On method calls, ruby 1.9 sends an extra RUBY_EVENT_CALL event with mid=0. The 294 | * top-most cfp on the stack in these cases points to the 'def method' line, so we skip 295 | * these and grab the second caller instead. 296 | */ 297 | static inline 298 | rb_control_frame_t * 299 | rb_vm_get_caller(rb_thread_t *th, rb_control_frame_t *cfp, ID mid) 300 | { 301 | int level = 0; 302 | 303 | while (!RUBY_VM_CONTROL_FRAME_STACK_OVERFLOW_P(th, cfp)) { 304 | if (++level == 1 && mid == 0) { 305 | // skip method definition line 306 | } else if (cfp->iseq != 0 && cfp->pc != 0) { 307 | return cfp; 308 | } 309 | 310 | cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp); 311 | } 312 | 313 | return 0; 314 | } 315 | 316 | #ifdef HAVE_TYPE_RB_ISEQ_LOCATION_T 317 | inline static int 318 | calc_lineno(const rb_iseq_t *iseq, const VALUE *pc) 319 | { 320 | return rb_iseq_line_no(iseq, pc - iseq->iseq_encoded); 321 | } 322 | 323 | int 324 | rb_vm_get_sourceline(const rb_control_frame_t *cfp) 325 | { 326 | int lineno = 0; 327 | const rb_iseq_t *iseq = cfp->iseq; 328 | 329 | if (RUBY_VM_NORMAL_ISEQ_P(iseq)) { 330 | lineno = calc_lineno(cfp->iseq, cfp->pc); 331 | } 332 | return lineno; 333 | } 334 | #endif 335 | #endif 336 | 337 | static void 338 | #ifdef RUBY_VM 339 | profiler_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass) 340 | #else 341 | profiler_hook(rb_event_flag_t event, NODE *node, VALUE self, ID mid, VALUE klass) 342 | #endif 343 | { 344 | char *file; 345 | long line; 346 | stackframe_t *frame = NULL, *prev = NULL; 347 | sourcefile_t *srcfile; 348 | 349 | /* line profiler: maintain a stack of CALL events with timestamps. for 350 | * each corresponding RETURN, account elapsed time to the calling line. 351 | * 352 | * we use ruby_current_node here to get the caller's file/line info, 353 | * (as opposed to node, which points to the callee method being invoked) 354 | */ 355 | #if defined(HAVE_RB_PROFILE_FRAMES) 356 | VALUE path, iseq; 357 | VALUE iseqs[2]; 358 | int lines[2]; 359 | int i = 0, l, n = rb_profile_frames(0, 2, iseqs, lines); 360 | 361 | if (n == 0) return; 362 | if (mid == 0 && n == 2) /* skip empty frame on method definition line */ 363 | i = 1; 364 | 365 | l = lines[i]; 366 | iseq = iseqs[i]; 367 | 368 | /* TODO: use fstring VALUE directly */ 369 | path = rb_profile_frame_absolute_path(iseq); 370 | if (!RTEST(path)) path = rb_profile_frame_path(iseq); 371 | file = RSTRING_PTR(path); 372 | line = l; 373 | #elif !defined(RUBY_VM) 374 | NODE *caller_node = ruby_frame->node; 375 | if (!caller_node) return; 376 | 377 | file = caller_node->nd_file; 378 | line = nd_line(caller_node); 379 | #else 380 | rb_thread_t *th = ruby_current_thread; 381 | rb_control_frame_t *cfp = rb_vm_get_caller(th, th->cfp, mid); 382 | if (!cfp) return; 383 | 384 | #ifdef HAVE_TYPE_RB_ISEQ_LOCATION_T 385 | if (RTEST(cfp->iseq->location.absolute_path)) 386 | file = StringValueCStr(cfp->iseq->location.absolute_path); 387 | else 388 | file = StringValueCStr(cfp->iseq->location.path); 389 | #else 390 | if (RTEST(cfp->iseq->filepath)) 391 | file = StringValueCStr(cfp->iseq->filepath); 392 | else 393 | file = StringValueCStr(cfp->iseq->filename); 394 | #endif 395 | line = rb_vm_get_sourceline(cfp); 396 | #endif 397 | 398 | if (!file) return; 399 | if (line <= 0) return; 400 | 401 | /* find the srcfile entry for the current file. 402 | * 403 | * first check the cache, in case this is the same file as 404 | * the previous invocation. 405 | * 406 | * if no record is found, we don't care about profiling this 407 | * file and return early. 408 | */ 409 | if (rblineprof.cache.file == file) 410 | srcfile = rblineprof.cache.srcfile; 411 | else 412 | srcfile = sourcefile_lookup(file); 413 | rblineprof.cache.file = file; 414 | rblineprof.cache.srcfile = srcfile; 415 | if (!srcfile) return; /* skip line profiling for this file */ 416 | 417 | snapshot_t now = { 418 | .wall_time = walltime_usec(), 419 | .cpu_time = cputime_usec(), 420 | #if defined(HAVE_RB_OS_ALLOCATED_OBJECTS) 421 | .allocated_objects = rb_os_allocated_objects() 422 | #elif defined(HAVE_RB_GC_STAT) 423 | .allocated_objects = rb_gc_stat(sym_total_allocated_objects) 424 | #endif 425 | }; 426 | 427 | switch (event) { 428 | case RUBY_EVENT_CALL: 429 | case RUBY_EVENT_C_CALL: 430 | /* Create a stack frame entry with this event, 431 | * the current file, and a snapshot of metrics. 432 | * 433 | * On a corresponding RETURN event later, we can 434 | * pop this stack frame and accumulate metrics to the 435 | * associated file and line. 436 | */ 437 | rblineprof.stack_depth++; 438 | if (rblineprof.stack_depth > 0 && rblineprof.stack_depth < MAX_STACK_DEPTH) { 439 | frame = &rblineprof.stack[rblineprof.stack_depth-1]; 440 | frame->event = event; 441 | frame->self = self; 442 | frame->mid = mid; 443 | frame->klass = klass; 444 | frame->line = line; 445 | frame->start = now; 446 | frame->srcfile = srcfile; 447 | #if defined(HAVE_RB_PROFILE_FRAMES) 448 | frame->thread = rb_thread_current(); 449 | #elif defined(RUBY_VM) 450 | frame->thread = th; 451 | #else 452 | frame->node = node; 453 | #endif 454 | } 455 | 456 | /* Record when we entered this file for the first time. 457 | * The difference is later accumulated into exclusive_time, 458 | * e.g. on the next event if the file changes. 459 | */ 460 | if (srcfile->depth == 0) 461 | srcfile->exclusive_start = now; 462 | srcfile->depth++; 463 | 464 | if (rblineprof.stack_depth > 1) { // skip if outermost frame 465 | prev = &rblineprof.stack[rblineprof.stack_depth-2]; 466 | 467 | /* If we just switched files, record time that was spent in 468 | * the previous file. 469 | */ 470 | if (prev->srcfile != frame->srcfile) { 471 | snapshot_t diff = snapshot_diff(&now, &prev->srcfile->exclusive_start); 472 | snapshot_increment(&prev->srcfile->exclusive, &diff); 473 | prev->srcfile->exclusive_start = now; 474 | } 475 | } 476 | break; 477 | 478 | case RUBY_EVENT_RETURN: 479 | case RUBY_EVENT_C_RETURN: 480 | /* Find the corresponding CALL for this event. 481 | * 482 | * We loop here instead of a simple pop, because in the event of a 483 | * raise/rescue several stack frames could have disappeared. 484 | */ 485 | do { 486 | if (rblineprof.stack_depth > 0 && rblineprof.stack_depth < MAX_STACK_DEPTH) { 487 | frame = &rblineprof.stack[rblineprof.stack_depth-1]; 488 | if (frame->srcfile->depth > 0) 489 | frame->srcfile->depth--; 490 | } else 491 | frame = NULL; 492 | 493 | if (rblineprof.stack_depth > 0) 494 | rblineprof.stack_depth--; 495 | } while (frame && 496 | #if defined(HAVE_RB_PROFILE_FRAMES) 497 | frame->thread != rb_thread_current() && 498 | #elif defined(RUBY_VM) 499 | frame->thread != th && 500 | #endif 501 | /* Break when we find a matching CALL/C_CALL. 502 | */ 503 | frame->event != (event == RUBY_EVENT_CALL ? RUBY_EVENT_RETURN : RUBY_EVENT_C_RETURN) && 504 | frame->self != self && 505 | frame->mid != mid && 506 | frame->klass != klass); 507 | 508 | if (rblineprof.stack_depth > 0) { 509 | // The new top of the stack (that we're returning to) 510 | prev = &rblineprof.stack[rblineprof.stack_depth-1]; 511 | 512 | /* If we're leaving this frame to go back to a different file, 513 | * accumulate time we spent in this file. 514 | * 515 | * Note that we do this both when entering a new file and leaving to 516 | * a new file to ensure we only count time spent exclusively in that file. 517 | * Consider the following scenario: 518 | * 519 | * call (a.rb:1) 520 | * call (b.rb:1) <-- leaving a.rb, increment into exclusive_time 521 | * call (a.rb:5) 522 | * return <-- leaving a.rb, increment into exclusive_time 523 | * return 524 | * return 525 | */ 526 | if (frame->srcfile != prev->srcfile) { 527 | snapshot_t diff = snapshot_diff(&now, &frame->srcfile->exclusive_start); 528 | snapshot_increment(&frame->srcfile->exclusive, &diff); 529 | frame->srcfile->exclusive_start = now; 530 | prev->srcfile->exclusive_start = now; 531 | } 532 | } 533 | 534 | if (frame) 535 | stackframe_record(frame, now, prev); 536 | 537 | break; 538 | } 539 | } 540 | 541 | static int 542 | cleanup_files(st_data_t key, st_data_t record, st_data_t arg) 543 | { 544 | xfree((char *)key); 545 | 546 | sourcefile_t *sourcefile = (sourcefile_t*)record; 547 | if (!sourcefile || (VALUE)sourcefile == Qnil) return ST_DELETE; 548 | 549 | if (sourcefile->lines) 550 | xfree(sourcefile->lines); 551 | xfree(sourcefile); 552 | 553 | return ST_DELETE; 554 | } 555 | 556 | static int 557 | summarize_files(st_data_t key, st_data_t record, st_data_t arg) 558 | { 559 | sourcefile_t *srcfile = (sourcefile_t*)record; 560 | if (!srcfile || (VALUE)srcfile == Qnil) return ST_CONTINUE; 561 | 562 | VALUE ret = (VALUE)arg; 563 | VALUE ary = rb_ary_new(); 564 | long i; 565 | 566 | rb_ary_store(ary, 0, rb_ary_new3( 567 | #if defined(HAVE_RB_OS_ALLOCATED_OBJECTS) || defined(HAVE_RB_GC_STAT) 568 | 7, 569 | #else 570 | 6, 571 | #endif 572 | ULL2NUM(srcfile->total.wall_time), 573 | ULL2NUM(srcfile->child.wall_time), 574 | ULL2NUM(srcfile->exclusive.wall_time), 575 | ULL2NUM(srcfile->total.cpu_time), 576 | ULL2NUM(srcfile->child.cpu_time), 577 | ULL2NUM(srcfile->exclusive.cpu_time) 578 | #if defined(HAVE_RB_OS_ALLOCATED_OBJECTS) || defined(HAVE_RB_GC_STAT) 579 | , ULL2NUM(srcfile->total.allocated_objects) 580 | #endif 581 | )); 582 | 583 | for (i=1; inlines; i++) 584 | rb_ary_store(ary, i, rb_ary_new3( 585 | #if defined(HAVE_RB_OS_ALLOCATED_OBJECTS) || defined(HAVE_RB_GC_STAT) 586 | 4, 587 | #else 588 | 3, 589 | #endif 590 | ULL2NUM(srcfile->lines[i].total.wall_time), 591 | ULL2NUM(srcfile->lines[i].total.cpu_time), 592 | ULL2NUM(srcfile->lines[i].calls) 593 | #if defined(HAVE_RB_OS_ALLOCATED_OBJECTS) || defined(HAVE_RB_GC_STAT) 594 | , ULL2NUM(srcfile->lines[i].total.allocated_objects) 595 | #endif 596 | )); 597 | rb_hash_aset(ret, rb_str_new2(srcfile->filename), ary); 598 | 599 | return ST_CONTINUE; 600 | } 601 | 602 | static VALUE 603 | lineprof_ensure(VALUE self) 604 | { 605 | rb_remove_event_hook((rb_event_hook_func_t) profiler_hook); 606 | rblineprof.enabled = false; 607 | return self; 608 | } 609 | 610 | VALUE 611 | lineprof(VALUE self, VALUE filename) 612 | { 613 | if (!rb_block_given_p()) 614 | rb_raise(rb_eArgError, "block required"); 615 | 616 | if (rblineprof.enabled) 617 | rb_raise(rb_eArgError, "profiler is already enabled"); 618 | 619 | VALUE filename_class = rb_obj_class(filename); 620 | 621 | if (filename_class == rb_cString) { 622 | #ifdef RUBY_VM 623 | rblineprof.source_filename = (char *) (StringValuePtr(filename)); 624 | #else 625 | /* rb_source_filename will return a string we can compare directly against 626 | * node->file, without a strcmp() 627 | */ 628 | rblineprof.source_filename = rb_source_filename(StringValuePtr(filename)); 629 | #endif 630 | } else if (filename_class == rb_cRegexp) { 631 | rblineprof.source_regex = filename; 632 | rblineprof.source_filename = NULL; 633 | } else { 634 | rb_raise(rb_eArgError, "argument must be String or Regexp"); 635 | } 636 | 637 | // reset state 638 | st_foreach(rblineprof.files, cleanup_files, 0); 639 | if (rblineprof.file.lines) { 640 | xfree(rblineprof.file.lines); 641 | rblineprof.file.lines = NULL; 642 | rblineprof.file.nlines = 0; 643 | } 644 | rblineprof.cache.file = NULL; 645 | rblineprof.cache.srcfile = NULL; 646 | 647 | rblineprof.enabled = true; 648 | #ifndef RUBY_VM 649 | 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); 650 | #else 651 | 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); 652 | #endif 653 | 654 | rb_ensure(rb_yield, Qnil, lineprof_ensure, self); 655 | 656 | VALUE ret = rb_hash_new(); 657 | 658 | if (rblineprof.source_filename) { 659 | summarize_files(Qnil, (st_data_t)&rblineprof.file, ret); 660 | } else { 661 | st_foreach(rblineprof.files, summarize_files, ret); 662 | } 663 | 664 | return ret; 665 | } 666 | 667 | static void 668 | rblineprof_gc_mark() 669 | { 670 | if (rblineprof.enabled) 671 | rb_gc_mark_maybe(rblineprof.source_regex); 672 | } 673 | 674 | void 675 | Init_rblineprof() 676 | { 677 | #if RUBY_API_VERSION_MAJOR == 2 && RUBY_API_VERSION_MINOR < 2 678 | sym_total_allocated_objects = ID2SYM(rb_intern("total_allocated_object")); 679 | #else 680 | sym_total_allocated_objects = ID2SYM(rb_intern("total_allocated_objects")); 681 | #endif 682 | gc_hook = Data_Wrap_Struct(rb_cObject, rblineprof_gc_mark, NULL, NULL); 683 | rb_global_variable(&gc_hook); 684 | 685 | rblineprof.files = st_init_strtable(); 686 | rb_define_global_function("lineprof", lineprof, 1); 687 | } 688 | 689 | /* vim: set ts=2 sw=2 expandtab: */ 690 | --------------------------------------------------------------------------------