profit 0.1.0

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.
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