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

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