profit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Profit (Profile It)
2
+
3
+ ![Step 3](http://thetrichordist.files.wordpress.com/2013/05/gnomes_plan.png)
4
+
5
+ Profit is a client/server pair that lets you record timing data for your code.
6
+
7
+ Here's the client
8
+ ``` ruby
9
+ # my_ruby_app.rb
10
+ client = Profit::Client.new
11
+ client.start("some_suspect_code")
12
+ some_thing_is_not_right
13
+ client.stop("some_suspect_code")
14
+ ```
15
+
16
+ Here's the server
17
+ ``` ruby
18
+ server = Profit::Server.new
19
+ server.run
20
+ ```
21
+
22
+ And if you looked in Redis
23
+ ``` ruby
24
+ irb(main):001:0>Redis.new(host: "127.0.0.1", port: 6379).lrange("some_foo_measurement", 0, -1)
25
+ => ["{\"recorded_time\":1.001161,\"start_file\":\"/Users/me/dev/my_ruby_app.rb\",\"start_line\":27,\"stop_file\":\"/Users/me/dev/my_ruby_app.rb\",\"stop_line\":27}"]
26
+ ```
27
+
28
+ With this, you could track the data over time, see how some optimizations change the performance at runtime, make pretty graphs, you name it!
data/bin/profit_server ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'profit'
4
+
5
+ Profit::Server.new.run
@@ -0,0 +1,40 @@
1
+ module Profit
2
+ class Client
3
+
4
+ attr_accessor :ctx, :socket, :pending
5
+
6
+ def initialize(ctx = nil)
7
+ @ctx = ctx || ZMQ::Context.new
8
+ @socket = @ctx.connect(:PUSH, "tcp://127.0.0.1:5556")
9
+ @pending = {}
10
+ end
11
+
12
+ def start(metric_type)
13
+ now = Time.now
14
+ start_file = caller[0][/(.+):(.+):/,1]
15
+ start_line = caller[0][/(.+):(.+):/,2].to_i + 1
16
+
17
+ # TODO: wrap in a Mutex & make the key a combo of metric_type,
18
+ # pid, and/or thread object_id to make this thread safe and
19
+ # thread-robust.
20
+ @pending[metric_type] = { start_file: start_file,
21
+ start_line: start_line,
22
+ start_time: now }
23
+ end
24
+
25
+ def stop(metric_type)
26
+ now = Time.now
27
+ metric = @pending.delete(metric_type)
28
+ recorded_time = Time.now - metric[:start_time]
29
+ stop_file = caller[0][/(.+):(.+):/,1]
30
+ stop_line = caller[0][/(.+):(.+):/,2].to_i - 1
31
+
32
+ @socket.send({ metric_type: metric_type,
33
+ recorded_time: recorded_time,
34
+ start_file: metric[:start_file],
35
+ start_line: metric[:start_line],
36
+ stop_file: stop_file,
37
+ stop_line: stop_line }.to_json)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+
2
+ module Profit
3
+
4
+ class MessageHandler
5
+
6
+ include EM::Deferrable
7
+
8
+ attr_reader :text
9
+
10
+ def initialize(json, conn)
11
+ @json, @conn = json, conn
12
+ end
13
+
14
+ def run
15
+ return succeed("Starting") if @json.empty?
16
+ message_hash = JSON.parse(@json)
17
+ key = message_hash.delete("metric_type")
18
+ response = @conn.rpush key, message_hash.to_json
19
+ if response == "OK"
20
+ succeed response
21
+ else
22
+ fail response
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,67 @@
1
+ module Profit
2
+
3
+ class Server
4
+
5
+ attr_reader :ctx
6
+
7
+ def initialize
8
+ @ctx = ZMQ::Context.new
9
+ end
10
+
11
+ def shutdown!
12
+ @shutdown = true
13
+ end
14
+
15
+ def setup_trap_int
16
+ trap :INT do
17
+ puts "\nSIGINT received, quitting!"
18
+ EM.stop
19
+ end
20
+ end
21
+
22
+ def run
23
+ @run = true
24
+ EM.run do
25
+
26
+ @redis_pool = EM::Pool.new
27
+ spawn = lambda { @redis_pool.add Redis.new(host: "127.0.0.1", port: 6379) }
28
+ @redis_pool.on_error { |conn| spawn[] }
29
+ 10.times { spawn[] }
30
+
31
+ @puller = @ctx.bind(:PULL, "tcp://127.0.0.1:5556")
32
+
33
+ # gives us a graceful exit
34
+ setup_trap_int
35
+
36
+ if @shutdown
37
+ EM.next_tick do # change to add_timer(1) for more delay
38
+ @run = false
39
+ end
40
+ end
41
+
42
+ # allows us to break out of the loop
43
+ EM.add_periodic_timer do
44
+ unless @run
45
+ EM.stop unless @run
46
+ end
47
+ end
48
+
49
+ # ensures we can continue to run other specs
50
+ EM.add_shutdown_hook { puts("destroy"); @ctx.destroy }
51
+
52
+ # this is the entry to message handling
53
+ EM.add_periodic_timer do
54
+ @redis_pool.perform do |conn|
55
+ message_handler = MessageHandler.new @puller.recv, conn
56
+
57
+ message_handler.callback do |response|
58
+ puts response.inspect
59
+ end
60
+ message_handler.run
61
+ message_handler
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,3 @@
1
+ module Profit
2
+ VERSION = "0.1.0"
3
+ end
data/lib/profit.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'zmq'
2
+ require 'eventmachine'
3
+ require 'redis'
4
+ require 'json'
5
+
6
+ module Profit
7
+ end
8
+
9
+ require 'profit/message_handler'
10
+ require 'profit/server'
11
+ require 'profit/client'
@@ -0,0 +1,130 @@
1
+ require 'spec_helper'
2
+
3
+ describe Profit::Client do
4
+
5
+ let!(:redis) { Redis.new(host: "127.0.0.1", port: 6379) }
6
+ let!(:client) { Profit::Client.new(server.ctx) }
7
+ let!(:server) { TestServer.server }
8
+ let!(:server_thread) { TestServer.server_thread }
9
+
10
+ after do
11
+ redis.del("some_foo_measurement")
12
+ end
13
+
14
+ it "sends the amount of time it takes to run some code" do
15
+ metrics = redis.lrange("some_foo_measurement", 0, -1)
16
+ expect(metrics).to be_empty
17
+
18
+ client.start("some_foo_measurement")
19
+ sleep 1
20
+ client.stop("some_foo_measurement")
21
+
22
+ server_thread.join(0.1)
23
+
24
+ metrics = redis.lrange("some_foo_measurement", 0, -1)
25
+ metric = JSON.parse(metrics.first)
26
+ expect(metric['recorded_time']).to be_within(0.1).of(1)
27
+ end
28
+
29
+ describe "#ctx" do
30
+
31
+ it "is a ZMQ context" do
32
+ expect(client.ctx).to be_a(ZMQ::Context)
33
+ end
34
+ end
35
+
36
+ describe "#new" do
37
+
38
+ it "connects to an open socket on a Profit::Server" do
39
+ expect(client.ctx).to be_a(ZMQ::Context)
40
+
41
+ expect(client.socket).to be_a(ZMQ::Socket)
42
+ expect(client.socket.to_s).to eq "PUSH socket connected to tcp://127.0.0.1:5556"
43
+ end
44
+
45
+ it "sends an initial message to notify the server of itself"
46
+ end
47
+
48
+ describe "#pending" do
49
+
50
+ it "is a Hash" do
51
+ expect(client.pending).to be_a(Hash)
52
+ end
53
+ end
54
+
55
+ describe "#start" do
56
+
57
+ it "starts the timer" do
58
+ expect(client.pending).to be_empty
59
+
60
+ client.start("some_foo_measurement")
61
+ now = Time.now
62
+
63
+ pending_metric = client.pending["some_foo_measurement"]
64
+ expect(pending_metric[:start_time].to_i).to be_within(1).of(now.to_i)
65
+ end
66
+
67
+ it "records where the execution starts" do
68
+ expect(client.pending).to be_empty
69
+
70
+ client.start("some_foo_measurement")
71
+ start_line = __LINE__
72
+ start_file = __FILE__
73
+
74
+ pending_metric = client.pending["some_foo_measurement"]
75
+ expect(pending_metric[:start_file]).to eq start_file
76
+ expect(pending_metric[:start_line]).to eq start_line
77
+ end
78
+ end
79
+
80
+ describe "#stop" do
81
+
82
+ after do
83
+ redis.del("m_1")
84
+ redis.del("m_2")
85
+ end
86
+
87
+ it "matches up with the start marker" do
88
+ client.start("m_1")
89
+ expect(client.pending.keys.count).to eq 1
90
+ sleep 0.3
91
+ client.start("m_2")
92
+ expect(client.pending.keys.count).to eq 2
93
+ sleep 1
94
+ client.stop("m_1")
95
+ client.start("m_1")
96
+ expect(client.pending.keys.count).to eq 2
97
+ sleep 0.2
98
+ client.stop("m_2")
99
+ expect(client.pending.keys.count).to eq 1
100
+ client.stop("m_1")
101
+
102
+ server_thread.join(0.1)
103
+
104
+ first_measurement_list = redis.lrange("m_1", 0, -1)
105
+ second_measurement_list = redis.lrange("m_2", 0, -1)
106
+
107
+ expect(first_measurement_list.count).to eq 2
108
+ expect(second_measurement_list.count).to eq 1
109
+
110
+ expect(JSON.parse(first_measurement_list[0])['recorded_time']).to be_within(0.1).of(1.3)
111
+ expect(JSON.parse(first_measurement_list[1])['recorded_time']).to be_within(0.1).of(0.2)
112
+ expect(JSON.parse(second_measurement_list[0])['recorded_time']).to be_within(0.1).of(1.2)
113
+ end
114
+
115
+ it "records where the execution stops" do
116
+ client.start("m_1")
117
+ sleep 0.1
118
+ stop_file = __FILE__
119
+ stop_line = __LINE__
120
+ client.stop("m_1")
121
+
122
+ server_thread.join(0.1)
123
+
124
+ measurements = redis.lrange("m_1", 0, -1)
125
+ metric = JSON.parse(measurements[0])
126
+ expect(metric['stop_file']).to eq stop_file
127
+ expect(metric['stop_line']).to eq stop_line
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Profit::Server do
4
+
5
+ let!(:redis) { Redis.new(host: "127.0.0.1", port: 6379) }
6
+ let!(:server) { TestServer.server }
7
+ let!(:server_thread) { TestServer.server_thread }
8
+
9
+ after do
10
+ redis.del("some_slow_piece_of_code")
11
+ end
12
+
13
+ it "stores metrics messages" do
14
+ pusher = server.ctx.connect(:PUSH, "tcp://127.0.0.1:5556")
15
+
16
+ pusher.send({ recorded_time: (now = Time.now),
17
+ total_time: 12.012,
18
+ metric_type: "some_slow_piece_of_code",
19
+ start_line: 1,
20
+ end_line: 42,
21
+ start_file: "/foo/bar/baz.rb",
22
+ end_file: "/foo/bar/biz.rb" }.to_json)
23
+ server_thread.join(0.1)
24
+
25
+ list = redis.lrange("some_slow_piece_of_code", 0, -1)
26
+ expect(list.count).to eq 1
27
+ expect(redis.llen("some_slow_piece_of_code")).to eq 1
28
+
29
+ metric = JSON.parse(list[0])
30
+ expect(metric["total_time"]).to eq 12.012
31
+ expect(metric["start_line"]).to eq 1
32
+ expect(metric["end_line"]).to eq 42
33
+ expect(metric["start_file"]).to eq "/foo/bar/baz.rb"
34
+ expect(metric["end_file"]).to eq "/foo/bar/biz.rb"
35
+ expect(metric["recorded_time"]).to eq now.to_s
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ require './lib/profit'
2
+
3
+ class TestServer
4
+
5
+ def self.server
6
+ @@server ||= Profit::Server.new
7
+ end
8
+
9
+ def self.server_thread
10
+ @@server_thread ||= Thread.new { server.run }
11
+ end
12
+ end
13
+
14
+ RSpec.configure do |config|
15
+
16
+ config.before(:suite) do
17
+ TestServer.server
18
+ end
19
+
20
+ config.after(:suite) do
21
+ TestServer.server.shutdown!
22
+ TestServer.server_thread.join(1)
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: profit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dave Rogers
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rbczmq
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: redis
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: debugger
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rake
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: This is a client/server combination that allows you to profile code and
111
+ send the results to a remote Redis server.
112
+ email:
113
+ - david.t.rogers@gmail.com
114
+ executables:
115
+ - profit_server
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - lib/profit/client.rb
120
+ - lib/profit/message_handler.rb
121
+ - lib/profit/server.rb
122
+ - lib/profit/version.rb
123
+ - lib/profit.rb
124
+ - README.md
125
+ - spec/lib/client_spec.rb
126
+ - spec/lib/server_spec.rb
127
+ - spec/spec_helper.rb
128
+ - bin/profit_server
129
+ homepage: https://github.com/davidtrogers/profit
130
+ licenses: []
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ none: false
137
+ requirements:
138
+ - - ! '>='
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ segments:
142
+ - 0
143
+ hash: -3353567577735004538
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ segments:
151
+ - 0
152
+ hash: -3353567577735004538
153
+ requirements: []
154
+ rubyforge_project:
155
+ rubygems_version: 1.8.23
156
+ signing_key:
157
+ specification_version: 3
158
+ summary: A simple library to store profiling information for ruby code.
159
+ test_files:
160
+ - spec/lib/client_spec.rb
161
+ - spec/lib/server_spec.rb
162
+ - spec/spec_helper.rb