Skip to content

Commit

Permalink
Made the middleware store the objectspace to the data store specified
Browse files Browse the repository at this point in the history
  • Loading branch information
iainbeeston committed Jun 26, 2015
1 parent 8511a13 commit f6228be
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 9 deletions.
53 changes: 51 additions & 2 deletions lib/rack/profiler.rb
@@ -1,11 +1,60 @@
require 'objspace'
require 'yajl'
require 'securerandom'

module Rack
class Profiler
def initialize(app)
def initialize(app, store:, async: true, object_space: ObjectSpace)
@app = app
@store = store
@async = async
@object_space = object_space
@object_space.trace_object_allocations_start
end

def call(env)
@app.call(env)
result = @app.call(env)

dumper = Thread.new { dump_allocations }
dumper.join unless @async

result
end

def dump_allocations
read, write = IO.pipe

if pid = fork
# parent
read.close
@object_space.dump_all(output: write)
write.close
Process.wait(pid) unless @async
else
# child
write.close
parse_dump(input: read)
read.close
end
end

def parse_dump(input:, key: request_id, run: ->(id, obj) { store_object(id, obj) })
parser = Yajl::Parser.new
parser.on_parse_complete = lambda do |obj|
run.call(key, obj)
end
parser.parse(input)
end

def store_object(request_id, obj)
object_id = obj.delete('address')
@store["#{request_id}-#{object_id}"] = obj if object_id
end

def request_id
thread_id = Thread.current.object_id
random_salt = SecureRandom.hex(4)
"#{thread_id}-#{random_salt}"
end
end
end
1 change: 1 addition & 0 deletions rack-profiler.gemspec
Expand Up @@ -24,4 +24,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler", "~> 1.10"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.3"
spec.add_development_dependency "moneta", "~> 0.8"
end
85 changes: 80 additions & 5 deletions spec/rack/profiler_spec.rb
@@ -1,15 +1,90 @@
require 'spec_helper'
require 'support/rack_helpers'
require 'support/object_space_helpers'
require 'support/file_store_helpers'
require 'support/rack_matchers'
require 'rack/mock'
require 'securerandom'

describe Rack::Profiler do
include RackHelpers
include ObjectSpaceHelpers
include FileStoreHelpers

it 'does not modify the response from the app' do
env = [418, { 'Content-Type' => 'text/plain', 'Content-Length' => '12' }, ["I'm a teapot"]]
app = ->(_) { env }
profiler = profiler(app)
expect(request(profiler, '/')).to match_response(response(env))
let(:env) { [418, { 'Content-Type' => 'text/plain', 'Content-Length' => '12' }, ["I'm a teapot"]] }
let(:app) { ->(_) { env } }
let(:store) { Hash.new }
let(:middleware) { described_class.new(app, store: store, async: false, object_space: objects) }
let(:server) { rack(middleware) }

let(:object_data) do
<<-STR
{"address":"0x7f930a8203a8", "type":"ARRAY"}
STR
end
let(:objects) { object_space(object_data) }

describe '#call' do
it 'does not modify the response from the app' do
expect(request(server, '/')).to match_response(response(env))
end

context 'with real data' do
let(:object_data) do
<<-STR
{"address":"0x7f930a820650", "type":"ARRAY"}
{"address":"0x7f930a8206c8", "type":"OBJECT"}
STR
end
let(:store_id) { SecureRandom.uuid }
let(:store) { file_store(store_id) }

it 'saves every object in the object space to the store' do
expect {
request(server, '/')
}.to change {
file_store_keys(store_id).map { |k| store[k] }
}.from([]).to([{ 'type' => 'ARRAY' }, { 'type' => 'OBJECT' }])
end
end
end

describe '#request_id' do
it 'returns a unique string every time' do
expect(middleware.request_id).to_not eq(middleware.request_id)
end
end

describe '#store_object' do
it 'stores the hash by the address property' do
expect {
middleware.store_object('key', 'address' => '0x7f930a820010', 'type' => 'OBJECT')
}.to change {
store['key-0x7f930a820010']
}.from(nil).to('type' => 'OBJECT')
end

it 'stores nothing if there is no address property' do
expect {
middleware.store_object('key', 'type' => 'OBJECT')
}.to_not change {
store.keys
}.from([])
end
end

describe '#parse_dump' do
it 'parses the input io stream and calls run for each line (parsed as json)' do
json = <<-STR
{"address":"0x7f930a8203a8", "type":"ARRAY"}
{"address":"0x7f930a820420", "type":"ARRAY"}
STR
io = StringIO.new(json)
calls = []
processor = ->(id, obj) { calls << [id, obj] }
middleware.parse_dump(input: io, key: 'key', run: processor)
expect(calls).to eq([['key', { 'address' => '0x7f930a8203a8', 'type' => 'ARRAY' } ],
['key', { 'address' => '0x7f930a820420', 'type' => 'ARRAY' } ]])
end
end
end
17 changes: 17 additions & 0 deletions spec/support/file_store_helpers.rb
@@ -0,0 +1,17 @@
require 'moneta'

module FileStoreHelpers
def file_store(unique_id)
Moneta.new(:File, dir: path(unique_id))
end

def file_store_keys(unique_id)
Dir.glob(File.join(path(unique_id), '*')).map { |path| File.basename(path) }
end

private

def path(unique_id)
File.join('tmp', unique_id)
end
end
20 changes: 20 additions & 0 deletions spec/support/object_space_helpers.rb
@@ -0,0 +1,20 @@
module ObjectSpaceHelpers
def object_space(object_data)
FakeObjectSpace.new(object_data)
end

private

class FakeObjectSpace
def initialize(object_data)
@object_data = object_data
end

def trace_object_allocations_start
end

def dump_all(output:)
output << @object_data
end
end
end
4 changes: 2 additions & 2 deletions spec/support/rack_helpers.rb
@@ -1,6 +1,6 @@
module RackHelpers
def profiler(app)
Rack::Lint.new(Rack::Profiler.new(app))
def rack(middleware)
Rack::Lint.new(middleware)
end

def request(app, path, headers = {})
Expand Down

0 comments on commit f6228be

Please sign in to comment.