profit 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +28 -0
- data/bin/profit_server +5 -0
- data/lib/profit/client.rb +40 -0
- data/lib/profit/message_handler.rb +26 -0
- data/lib/profit/server.rb +67 -0
- data/lib/profit/version.rb +3 -0
- data/lib/profit.rb +11 -0
- data/spec/lib/client_spec.rb +130 -0
- data/spec/lib/server_spec.rb +37 -0
- data/spec/spec_helper.rb +24 -0
- metadata +162 -0
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,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
|
data/lib/profit.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|