pilfer 0.0.1.pre3 → 0.0.1.pre4

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 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: */