pilfer 0.0.1.pre3 → 0.0.1.pre4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NzkwNzViNDg0NTFlMDIyNmU5YWMwYTYxNmY3NmE1YjJlYjgyY2IxOQ==
4
+ NzFkODBmMDU3MzMxMmQ2MGRiYjZhMmI2Y2I2OWQ4NmE0YzVlMmJiOQ==
5
5
  data.tar.gz: !binary |-
6
- ZjcwZjRkNzZiNzI0MjllYTE0NzUzYWQyNWZlOTM2MzQ4MzA3MGNkNA==
6
+ ZjVlOGMyYWFiNjM5ZWIyODI5NGU5MDI3YWYxZmJmNGM3YzFlNWQ4Nw==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- YTlhYzBiMGRjYTY5OTdiYzM4MTlmODdkZWI5MTBhZmRiN2Q1ZGYxOGNmZjVl
10
- Yjk5ZDVhMGFmOWEyYTUwNTFmM2MzZTYwZDZhMzQ0MzExNDVhNTE0ODZlNGYy
11
- OWM3NTY3N2NmZWQ2ZTUzM2Q3YmU2MzhjOTA1NzIxMmM3Y2U1OWY=
9
+ NTI5NjVlNGNlNGU0NzlkNzgwYWI0YTYyMjhmNmMzYWQyOWQwOWFkYmJiN2Ey
10
+ NTY4NWUxOTRkOTRjNjlhZjA3ZmEwNWFmOWQ2YmRlYjhmNjEyNjJiMDg2OWY2
11
+ NTA0MTZhYWI3YzYzOWE5OWZkODRmMWI4MGI3MjhkYjEwYzk0NGU=
12
12
  data.tar.gz: !binary |-
13
- NTE4OTU1OGQwYWExYjI0ZjE4YTFjN2RmNDExZGUxZTA5ZDUzYjZjYjE1MGNh
14
- MWY3NjkxYjY2N2Q3YjYyNzJlODFlZjQ2NjVmODU5ZThkMWE1OWRjNDkzNDNj
15
- MmUxYjAxMzZlNTcxMmRmYzZiZWIzNWU0Zjk0ZWVkMGI3ZTA2Njg=
13
+ YzY3Mzk5N2VlNzBiNzdmMWU2ZDBhNDMxMGEwNDVhODQwMTI1MTRhMWQxODJk
14
+ OGM1MDIzYjVlYTVkYTFkYTU5MjMwNzE2ZDA5YjkxNzEzZWU5OWYwYzc5OTU3
15
+ OWVhZmQ0YTc5NGNmNTY3MWU0Zjk0ZDc1NDA5ZmVmODg0ZmE4YTU=
data/Gemfile CHANGED
@@ -1,4 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'rblineprof', github: 'tmm1/rblineprof'
3
+ gem 'json', :platforms => [:ruby_18]
4
+ gem 'rspec', '~> 2.13'
5
+ gem 'rack', '~> 1.5.2'
6
+
4
7
  gemspec
data/README.md CHANGED
@@ -77,9 +77,9 @@ reporter = Pilfer::Server.new('https://pilfer.com', 'abc123')
77
77
  Profile your Rack or Rails app using `Pilfer::Middleware`.
78
78
 
79
79
  ```ruby
80
- reporter = Pilfer::Server.new('https://pilfer.com', 'abc123'
81
- :app_root => Rails.root)
82
- use Pilfer::Middleware, reporter
80
+ reporter = Pilfer::Logger.new($stdout, :app_root => Rails.root)
81
+ profiler = Pilfer::Profiler.new(reporter)
82
+ use Pilfer::Middleware :profiler => profiler
83
83
  ```
84
84
 
85
85
  Restrict the files profiled by passing a regular expression with
@@ -87,14 +87,15 @@ Restrict the files profiled by passing a regular expression with
87
87
 
88
88
  ```ruby
89
89
  matcher = %r{^#{Regexp.escape(Rails.root.to_s)}/(app|config|lib|vendor/plugin)}
90
- use Pilfer::Middleware, reporter, :files_matching => matcher
90
+ use Pilfer::Middleware, :files_matching => matcher,
91
+ :profiler => profiler
91
92
  ```
92
93
 
93
94
  You probably don't want to profile _every_ request. The given block will be
94
95
  evaluated on each request to determine if a profile should be run.
95
96
 
96
97
  ```ruby
97
- use Pilfer::Middleware, reporter do
98
+ use Pilfer::Middleware, :profiler => profiler do
98
99
  # Profile 1% of requests.
99
100
  rand(100) == 1
100
101
  end
@@ -103,7 +104,7 @@ end
103
104
  The Rack environment is available to allow profiling on demand.
104
105
 
105
106
  ```ruby
106
- use Pilfer::Middleware, reporter do |env|
107
+ use Pilfer::Middleware, :profiler => profiler do |env|
107
108
  env.query_string.include? 'profile=true'
108
109
  end
109
110
  ```
@@ -8,7 +8,7 @@ module Pilfer
8
8
  def initialize(path_or_io, options = {})
9
9
  @logger = ::Logger.new(path_or_io)
10
10
  if (app_root = options[:app_root])
11
- app_root += '/' unless app_root[-1] == '/'
11
+ app_root += '/' unless app_root[-1, 1] == '/'
12
12
  @app_root = %r{^#{Regexp.escape(app_root)}}
13
13
  end
14
14
  end
@@ -25,7 +25,8 @@ module Pilfer
25
25
  private
26
26
 
27
27
  def print_report_banner(profile_start)
28
- logger.info "Profile start=#{profile_start.utc.to_s}"
28
+ formatted_start = profile_start.utc.strftime('%Y-%m-%d %H:%M:%S UTC')
29
+ logger.info "Profile start=#{formatted_start}"
29
30
  end
30
31
 
31
32
  def print_file_banner(path, data)
@@ -0,0 +1,36 @@
1
+ require 'pilfer/logger'
2
+ require 'pilfer/profiler'
3
+
4
+ module Pilfer
5
+ class Middleware
6
+ attr_accessor :app, :profiler, :file_matcher, :profile_guard
7
+
8
+ def initialize(app, options = {}, &profile_guard)
9
+ @app = app
10
+ @profiler = options[:profiler] || default_profiler
11
+ @file_matcher = options[:file_matcher]
12
+ @profile_guard = profile_guard
13
+ end
14
+
15
+ def call(env)
16
+ if profile_guard.call(env)
17
+ run_profiler { app.call(env) }
18
+ else
19
+ app.call(env)
20
+ end
21
+ end
22
+
23
+ def run_profiler(&downstream)
24
+ if file_matcher
25
+ profiler.profile_files_matching(file_matcher, &downstream)
26
+ else
27
+ profiler.profile(&downstream)
28
+ end
29
+ end
30
+
31
+ def default_profiler
32
+ reporter = Pilfer::Logger.new($stdout, :app_root => ENV['PWD'])
33
+ Pilfer::Profiler.new(reporter)
34
+ end
35
+ end
36
+ end
@@ -9,19 +9,18 @@ module Pilfer
9
9
  end
10
10
 
11
11
  def each(&block)
12
- files.each(&block)
12
+ files.to_a.sort_by(&:first).each(&block)
13
13
  end
14
14
 
15
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|
16
+ data.inject({}) do |files, (file, lines)|
17
+ profile_lines = {}
18
+ lines[1..-1].each_with_index do |data, number|
20
19
  next unless data.any? {|datum| datum > 0 }
21
20
  wall_time, cpu_time, calls = data
22
- lines[number] = { 'wall_time' => wall_time,
23
- 'cpu_time' => cpu_time,
24
- 'calls' => calls }
21
+ profile_lines[number] = { 'wall_time' => wall_time,
22
+ 'cpu_time' => cpu_time,
23
+ 'calls' => calls }
25
24
  end
26
25
 
27
26
  total_wall, child_wall, exclusive_wall,
@@ -33,6 +32,7 @@ module Pilfer
33
32
  files[file] = { 'wall_time' => wall,
34
33
  'cpu_time' => cpu,
35
34
  'lines' => profile_lines }
35
+ files
36
36
  end
37
37
  end
38
38
  end
@@ -1,3 +1,3 @@
1
1
  module Pilfer
2
- VERSION = '0.0.1.pre3'
2
+ VERSION = '0.0.1.pre4'
3
3
  end
@@ -14,19 +14,12 @@ Gem::Specification.new do |spec|
14
14
  spec.files = %w(Gemfile LICENSE README.md)
15
15
  spec.files << 'pilfer.gemspec'
16
16
  spec.files += Dir.glob('lib/**/*.rb')
17
- spec.files += Dir.glob('test/**/*.rb')
17
+ spec.files += Dir.glob('spec/**/*.rb')
18
18
  spec.files += Dir.glob('script/*')
19
- spec.test_files = Dir.glob('test/**/*.rb')
19
+ spec.test_files = Dir.glob('spec/**/*.rb')
20
20
 
21
+ spec.add_dependency 'rblineprof', '~> 0.3.2'
21
22
  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
23
 
26
24
  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
25
  end
@@ -0,0 +1 @@
1
+ print 'Hello '
@@ -0,0 +1,17 @@
1
+ require 'bundler/setup'
2
+ require 'pilfer/logger'
3
+ require 'pilfer/profiler'
4
+ require 'stringio'
5
+
6
+ output = StringIO.new
7
+ reporter = Pilfer::Logger.new(output)
8
+ profiler = Pilfer::Profiler.new(reporter)
9
+ path = File.expand_path(File.dirname(__FILE__))
10
+
11
+ profiler.profile_files_matching(%r{^#{Regexp.escape(path)}}) do
12
+ require 'hello'
13
+ puts 'world!'
14
+ 10.times do
15
+ sleep 0.01
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ require 'bundler/setup'
2
+
3
+ RSpec.configure do |config|
4
+ config.filter_run :focused => true
5
+ config.run_all_when_everything_filtered = true
6
+ config.alias_example_to :fit, :focused => true
7
+ end
@@ -0,0 +1,88 @@
1
+ require 'helper'
2
+ require 'json'
3
+ require 'tempfile'
4
+ require 'pilfer/logger'
5
+
6
+ describe Pilfer::Logger do
7
+ let(:spec_root) { File.expand_path('..', File.dirname(__FILE__)) }
8
+ let(:profile) {
9
+ profile_file = File.join(spec_root, 'files', 'profile.json')
10
+ profile_content = File.read(profile_file).gsub('SPEC_ROOT', spec_root)
11
+ JSON.parse(profile_content)
12
+ }
13
+ let(:reporter) { StringIO.new }
14
+ let(:start) { Time.at(42) }
15
+ let(:output) {
16
+ reporter.string.each_line.map {|line|
17
+ line.sub(/I, \[[^\]]+\] INFO -- : /, '')
18
+ }.join
19
+ }
20
+ let(:first_file) {
21
+ output.
22
+ split("\n")[1].
23
+ split(' ').
24
+ first
25
+ }
26
+
27
+ describe '#write' do
28
+ it 'writes profile to reporter' do
29
+ expected = <<-EOS
30
+ Profile start=1970-01-01 00:00:42 UTC
31
+ #{spec_root}/files/hello.rb wall_time=0.0ms cpu_time=0.0ms
32
+ 0.0ms ( 2) | print 'Hello '
33
+ #{spec_root}/files/test.rb wall_time=113.7ms cpu_time=5.3ms
34
+ | require 'bundler/setup'
35
+ | require 'pilfer/logger'
36
+ | require 'pilfer/profiler'
37
+ | require 'stringio'
38
+ |
39
+ | output = StringIO.new
40
+ | reporter = Pilfer::Logger.new(output)
41
+ | profiler = Pilfer::Profiler.new(reporter)
42
+ | path = File.expand_path(File.dirname(__FILE__))
43
+ |
44
+ | profiler.profile_files_matching(%r{^\#{Regexp.escape(path)}}) do
45
+ 5.1ms ( 3) | require 'hello'
46
+ 0.0ms ( 4) | puts 'world!'
47
+ 108.6ms ( 1) | 10.times do
48
+ 108.4ms ( 10) | sleep 0.01
49
+ | end
50
+ | end
51
+ EOS
52
+ Pilfer::Logger.new(reporter).write(profile, start)
53
+ output.should eq(expected)
54
+ end
55
+
56
+ it 'omits app root' do
57
+ Pilfer::Logger.new(reporter, :app_root => spec_root).
58
+ write(profile, start)
59
+ first_file.should eq('files/hello.rb')
60
+ end
61
+
62
+ it 'omits app root with trailing separator' do
63
+ Pilfer::Logger.new(reporter, :app_root => spec_root + '/').
64
+ write(profile, start)
65
+ first_file.should eq('files/hello.rb')
66
+ end
67
+
68
+ context 'with a nonexistent file' do
69
+ let(:profile) {{
70
+ "(eval)" => [[113692, 31, 5026, 5313, 18, 4868], [0, 0, 0]]
71
+ }}
72
+
73
+ it 'omits the source of the nonexistent file' do
74
+ expected = <<-EOS
75
+ Profile start=1970-01-01 00:00:42 UTC
76
+ (eval) wall_time=113.7ms cpu_time=5.3ms
77
+ EOS
78
+ Pilfer::Logger.new(reporter).write(profile, start)
79
+ output.should eq(expected)
80
+ end
81
+ end
82
+
83
+ it 'appends to the log file' do
84
+ 3.times { Pilfer::Logger.new(reporter).write(profile, start) }
85
+ output.scan('Profile start=').size.should eq(3)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,55 @@
1
+ require 'helper'
2
+ require 'rack/mock'
3
+ require 'pilfer/middleware'
4
+
5
+ describe Pilfer::Middleware do
6
+ let(:env) { Rack::MockRequest.env_for }
7
+ let(:app) { stub(:app, :call => nil) }
8
+ let(:profiler) { stub(:profiler, :profile => nil) }
9
+ let(:options) {{ :profiler => profiler }}
10
+ let(:guard) { Proc.new do true end }
11
+ subject { Pilfer::Middleware.new(app, options, &guard) }
12
+
13
+ it 'profiles and calls the downstream app' do
14
+ app.should_receive(:call).with(env).once
15
+ profiler.should_receive(:profile).with(no_args).and_yield
16
+ subject.call(env)
17
+ end
18
+
19
+ it 'passes rack env to guard' do
20
+ guard_args = nil
21
+ guard = lambda do |*args| guard_args = args end
22
+
23
+ Pilfer::Middleware.new(app, options, &guard).call(env)
24
+ guard_args.should_not be_nil
25
+ guard_args.size.should eq(1)
26
+ guard_args.first.should eq(env)
27
+ end
28
+
29
+ context 'with file matcher' do
30
+ let(:file_matcher) { stub(:file_matcher) }
31
+ let(:options) {{ :file_matcher => file_matcher,
32
+ :profiler => profiler }}
33
+
34
+ it 'passes file matcher to profiler and calls the downstream app' do
35
+ app.should_receive(:call).with(env).once
36
+ profiler.should_receive(:profile_files_matching).with(file_matcher).
37
+ and_yield
38
+ subject.call(env)
39
+ end
40
+ end
41
+
42
+ context 'when guard returns false' do
43
+ let(:guard) { Proc.new do false end }
44
+
45
+ it 'calls the downstream app' do
46
+ app.should_receive(:call).with(env).once
47
+ subject.call(env)
48
+ end
49
+
50
+ it 'skips profiling' do
51
+ profiler.should_not_receive(:profile)
52
+ subject.call(env)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,61 @@
1
+ require 'helper'
2
+ require 'json'
3
+ require 'pilfer/profile'
4
+
5
+ describe Pilfer::Profile do
6
+ let(:spec_root) { File.expand_path('..', File.dirname(__FILE__)) }
7
+ let(:data) {
8
+ profile_file = File.join(spec_root, 'files', 'profile.json')
9
+ profile_content = File.read(profile_file).gsub('SPEC_ROOT', spec_root)
10
+ JSON.parse(profile_content)
11
+ }
12
+ let(:start) { Time.at(42) }
13
+ subject { Pilfer::Profile.new(data, start) }
14
+
15
+ it 'should be an Enumerable' do
16
+ subject.should be_kind_of(Enumerable)
17
+ end
18
+
19
+ describe '#start' do
20
+ it 'returns the given start time' do
21
+ subject.start.should eq(start)
22
+ end
23
+ end
24
+
25
+ describe '#each' do
26
+ it 'yields each file and data' do
27
+ actual = {}
28
+ subject.each do |file, data|
29
+ actual[file] = data
30
+ end
31
+ expected = {
32
+ "#{spec_root}/files/hello.rb" => {
33
+ "wall_time" => 31,
34
+ "cpu_time" => 18,
35
+ "lines" => {
36
+ 0 => { "wall_time" => 31, "cpu_time" => 18, "calls" => 2} }
37
+ },
38
+ "#{spec_root}/files/test.rb" => {
39
+ "wall_time" => 113692,
40
+ "cpu_time" => 5313,
41
+ "lines" => {
42
+ 11 => { "wall_time" => 5062, "cpu_time" => 4890, "calls" => 3 },
43
+ 12 => { "wall_time" => 23, "cpu_time" => 14, "calls" => 4 },
44
+ 13 => { "wall_time" => 108607, "cpu_time" => 409, "calls" => 1 },
45
+ 14 => { "wall_time" => 108404, "cpu_time" => 310, "calls" => 10 }
46
+ }
47
+ }
48
+ }
49
+ actual.should eq(expected)
50
+ end
51
+
52
+ it 'yields each file alphabetically' do
53
+ actual = []
54
+ subject.each do |file, data|
55
+ actual << file
56
+ end
57
+ expected = %W(#{spec_root}/files/hello.rb #{spec_root}/files/test.rb)
58
+ actual.should eq(expected)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,52 @@
1
+ require 'helper'
2
+ require 'pilfer/profiler'
3
+
4
+ describe Pilfer::Profiler do
5
+ let(:reporter) { stub(:reporter, :write => nil) }
6
+ let(:start) { stub(:start) }
7
+
8
+ describe '#profile' do
9
+ it 'profiles all files by default' do
10
+ profiler = stub(:profiler)
11
+ profiler.should_receive(:call).with(/./)
12
+ Pilfer::Profiler.new(reporter).profile(profiler) { }
13
+ end
14
+
15
+ it 'returns value of app' do
16
+ profiler = lambda {|*args, &app|
17
+ app.call
18
+ :profiler_response
19
+ }
20
+
21
+ response = Pilfer::Profiler.new(reporter).profile(profiler) {
22
+ :app_response
23
+ }
24
+
25
+ response.should eq(:app_response)
26
+ end
27
+
28
+ it 'writes profile to reporter' do
29
+ profiler = stub(:profiler, :call => :profiler_response)
30
+ reporter.should_receive(:write).with(:profiler_response, start)
31
+ Pilfer::Profiler.new(reporter).profile(profiler, start) { }
32
+ end
33
+ end
34
+
35
+ describe '#profile_files_matching' do
36
+ let(:matcher) { stub(:matcher) }
37
+
38
+ it 'passes file matcher to profiler' do
39
+ profiler = stub(:profiler)
40
+ profiler.should_receive(:call).with(matcher)
41
+ Pilfer::Profiler.new(reporter).
42
+ profile_files_matching(matcher, profiler) { }
43
+ end
44
+
45
+ it 'writes profile to reporter' do
46
+ profiler = stub(:profiler, :call => :profiler_response)
47
+ reporter.should_receive(:write).with(:profiler_response, start)
48
+ Pilfer::Profiler.new(reporter).
49
+ profile_files_matching(matcher, profiler, start) { }
50
+ end
51
+ end
52
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pilfer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.pre3
4
+ version: 0.0.1.pre4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Lindvall
@@ -9,85 +9,42 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-17 00:00:00.000000000 Z
12
+ date: 2013-04-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: bundler
15
+ name: rblineprof
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
18
  - - ~>
19
19
  - !ruby/object:Gem::Version
20
- version: '1.0'
21
- type: :development
20
+ version: 0.3.2
21
+ type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - ~>
26
26
  - !ruby/object:Gem::Version
27
- version: '1.0'
27
+ version: 0.3.2
28
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
29
+ name: bundler
58
30
  requirement: !ruby/object:Gem::Requirement
59
31
  requirements:
60
32
  - - ~>
61
33
  - !ruby/object:Gem::Version
62
- version: 1.5.2
34
+ version: '1.0'
63
35
  type: :development
64
36
  prerelease: false
65
37
  version_requirements: !ruby/object:Gem::Requirement
66
38
  requirements:
67
39
  - - ~>
68
40
  - !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'
41
+ version: '1.0'
84
42
  description:
85
43
  email:
86
44
  - eric@sevenscale.com
87
45
  - larry@marburger.cc
88
46
  executables: []
89
- extensions:
90
- - ext/extconf.rb
47
+ extensions: []
91
48
  extra_rdoc_files: []
92
49
  files:
93
50
  - Gemfile
@@ -95,13 +52,19 @@ files:
95
52
  - README.md
96
53
  - pilfer.gemspec
97
54
  - lib/pilfer/logger.rb
55
+ - lib/pilfer/middleware.rb
98
56
  - lib/pilfer/profile.rb
99
57
  - lib/pilfer/profiler.rb
100
58
  - lib/pilfer/version.rb
59
+ - spec/files/hello.rb
60
+ - spec/files/test.rb
61
+ - spec/helper.rb
62
+ - spec/pilfer/logger_spec.rb
63
+ - spec/pilfer/middleware_spec.rb
64
+ - spec/pilfer/profile_spec.rb
65
+ - spec/pilfer/profiler_spec.rb
101
66
  - script/package
102
67
  - script/release
103
- - ext/extconf.rb
104
- - ext/rblineprof.c
105
68
  homepage: https://github.com/eric/pilfer
106
69
  licenses:
107
70
  - MIT
@@ -126,4 +89,11 @@ rubygems_version: 2.0.3
126
89
  signing_key:
127
90
  specification_version: 4
128
91
  summary: Look into your ruby with rblineprof
129
- test_files: []
92
+ test_files:
93
+ - spec/files/hello.rb
94
+ - spec/files/test.rb
95
+ - spec/helper.rb
96
+ - spec/pilfer/logger_spec.rb
97
+ - spec/pilfer/middleware_spec.rb
98
+ - spec/pilfer/profile_spec.rb
99
+ - spec/pilfer/profiler_spec.rb
@@ -1,19 +0,0 @@
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
@@ -1,619 +0,0 @@
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: */