Skip to content

Commit ccb88bb

Browse files
committed
WIP: Improve capture performance
Currently 2-3x faster. TODO: Any frozen objects that may be bad? Should we provide some sort of fallback bath here? Reduce allocations in RemoveCircularReferences, if possible
1 parent 823c905 commit ccb88bb

14 files changed

+187
-82
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
coverage
22
pkg
33
ruby/
4+
log/
45
docs/_build
56
.bundle
67
*.gem

Gemfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ group :development do
1212
gem "pry-coolline"
1313
gem "benchmark-ips"
1414
gem "benchmark-ipsa"
15-
gem "yajl-ruby", :platforms => :mri
15+
gem "ruby-prof"
1616
end

benchmarks/allocation_report.rb

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
require 'benchmark/ips'
2+
require 'benchmark/ipsa'
3+
require 'raven'
4+
require 'raven/breadcrumbs/logger'
5+
require 'raven/transports/dummy'
6+
require_relative "../spec/support/test_rails_app/app"
7+
8+
TestApp.configure do |config|
9+
config.middleware.delete ActionDispatch::DebugExceptions
10+
config.middleware.delete ActionDispatch::ShowExceptions
11+
end
12+
13+
Raven.configure do |config|
14+
config.logger = Logger.new(nil)
15+
config.dsn = "dummy://12345:[email protected]:3000/sentry/42"
16+
end
17+
18+
TestApp.initialize!
19+
@app = Rack::MockRequest.new(TestApp)
20+
RAILS_EXC = begin
21+
@app.get("/exception")
22+
rescue => exc
23+
exc
24+
end
25+
26+
report = MemoryProfiler.report do
27+
Raven.capture_exception(RAILS_EXC)
28+
end
29+
30+
report.pretty_print

benchmarks/async_benchmark.rb

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
require 'benchmark/ips'
2+
require 'benchmark/ipsa'
3+
require 'raven'
4+
require 'raven/breadcrumbs/logger'
5+
require 'raven/transports/dummy'
6+
require 'rack/test'
7+
require_relative "../spec/support/test_rails_app/app"
8+
9+
def build_exception
10+
1 / 0
11+
rescue ZeroDivisionError => exception
12+
exception
13+
end
14+
15+
TestApp.configure do |config|
16+
config.middleware.delete ActionDispatch::DebugExceptions
17+
config.middleware.delete ActionDispatch::ShowExceptions
18+
end
19+
20+
Raven.configure do |config|
21+
config.logger = Logger.new(nil)
22+
config.dsn = "dummy://12345:[email protected]:3000/sentry/42"
23+
config.async = lambda { |event| event.to_hash } # Simulate throwing this into a Sidekiq job, etc
24+
end
25+
26+
TestApp.initialize!
27+
@app = Rack::MockRequest.new(TestApp)
28+
RAILS_EXC = begin
29+
@app.get("/exception")
30+
rescue => exc
31+
exc
32+
end
33+
34+
DIVIDE_BY_ZERO = build_exception
35+
36+
LOGGER = Logger.new(nil)
37+
38+
Benchmark.ipsa do |x|
39+
x.config(:time => 5, :warmup => 2)
40+
41+
x.report("simple") { Raven.capture_exception(DIVIDE_BY_ZERO) }
42+
x.report("rails") { Raven.capture_exception(RAILS_EXC) }
43+
x.report("lots o logs") do
44+
100.times { LOGGER.debug(rand(100_000).to_s) }
45+
Raven.capture_exception(DIVIDE_BY_ZERO)
46+
end
47+
48+
x.compare!
49+
end

benchmarks/json.rb

-22
This file was deleted.

benchmarks/profile.rb

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require 'raven'
2+
require 'raven/breadcrumbs/logger'
3+
require 'raven/transports/dummy'
4+
require_relative "../spec/support/test_rails_app/app"
5+
6+
TestApp.configure do |config|
7+
config.middleware.delete ActionDispatch::DebugExceptions
8+
config.middleware.delete ActionDispatch::ShowExceptions
9+
end
10+
11+
Raven.configure do |config|
12+
config.logger = Logger.new(nil)
13+
config.dsn = "dummy://12345:[email protected]:3000/sentry/42"
14+
end
15+
16+
TestApp.initialize!
17+
@app = Rack::MockRequest.new(TestApp)
18+
RAILS_EXC = begin
19+
@app.get("/exception")
20+
rescue => exc
21+
exc
22+
end
23+
24+
require 'ruby-prof'
25+
26+
RubyProf.measure_mode = RubyProf::PROCESS_TIME
27+
28+
# profile the code
29+
result = RubyProf.profile do
30+
100.times { Raven.capture_exception(RAILS_EXC) }
31+
end
32+
33+
# print a graph profile to text
34+
printer = RubyProf::MultiPrinter.new(result)
35+
printer.print(:path => "./tmp", :profile => "profile")

lib/raven/client.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def transport
6464

6565
def encode(event)
6666
hash = @processors.reduce(event.to_hash) { |a, e| e.process(a) }
67-
encoded = JSON.generate(hash)
67+
encoded = JSON.fast_generate(hash)
6868

6969
case configuration.encoding
7070
when 'gzip'

lib/raven/interfaces/stack_trace.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def initialize(*arguments)
3737
end
3838

3939
def filename
40-
return nil if abs_path.nil?
40+
return if abs_path.nil?
4141

4242
prefix =
4343
if under_project_root? && in_app
@@ -60,7 +60,7 @@ def project_root
6060
end
6161

6262
def longest_load_path
63-
$LOAD_PATH.select { |s| abs_path.start_with?(s.to_s) }.sort_by { |s| s.to_s.length }.last
63+
@longest_load_path ||= $LOAD_PATH.select { |s| abs_path.start_with?(s) }.max_by(&:length)
6464
end
6565

6666
def to_hash(*args)

lib/raven/processor/removecircularreferences.rb

+10-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
module Raven
22
class Processor::RemoveCircularReferences < Processor
3-
def process(v, visited = [])
4-
return "(...)" if visited.include?(v.__id__)
5-
visited += [v.__id__]
6-
if v.is_a?(Hash)
7-
v.each_with_object({}) { |(k, v_), memo| memo[k] = process(v_, visited) }
8-
elsif v.is_a?(Array)
9-
v.map { |v_| process(v_, visited) }
3+
def process(value, visited = [])
4+
return "(...)" if visited.include?(value.__id__)
5+
visited << value.__id__ if value.is_a?(Array) || value.is_a?(Hash)
6+
7+
case value
8+
when Hash
9+
!value.frozen? ? value.merge!(value) { |_, v| process v, visited } : value.merge(value) { |_, v| process v, visited }
10+
when Array
11+
!value.frozen? ? value.map! { |v| process v, visited } : value.map { |v| process v, visited }
1012
else
11-
v
13+
value
1214
end
1315
end
1416
end

lib/raven/processor/sanitizedata.rb

+19-22
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,27 @@ def initialize(client)
1414
self.sanitize_credit_cards = client.configuration.sanitize_credit_cards
1515
end
1616

17-
def process(value)
18-
return value if value.frozen?
19-
value.each_with_object(value) { |(k, v), memo| memo[k] = sanitize(k, v) }
20-
end
21-
22-
def sanitize(k, v)
23-
if v.is_a?(Hash)
24-
process(v)
25-
elsif v.is_a?(Array)
26-
v.map { |a| sanitize(k, a) }
27-
elsif k.to_s == 'query_string'
28-
sanitize_query_string(v)
29-
elsif v.is_a?(Integer) && matches_regexes?(k, v)
30-
INT_MASK
31-
elsif v.is_a?(String)
32-
if fields_re.match(v.to_s) && (json = parse_json_or_nil(v))
17+
def process(value, key = nil)
18+
case value
19+
when Hash
20+
!value.frozen? ? value.merge!(value) { |k, v| process v, k } : value.merge(value) { |k, v| process v, k }
21+
when Array
22+
!value.frozen? ? value.map! { |v| process v, key } : value.map { |v| process v, key }
23+
when Integer
24+
matches_regexes?(key, value.to_s) ? INT_MASK : value
25+
when String
26+
if value =~ fields_re && (json = parse_json_or_nil(value))
3327
# if this string is actually a json obj, convert and sanitize
34-
json.is_a?(Hash) ? process(json).to_json : v
35-
elsif matches_regexes?(k, v)
28+
process(json).to_json
29+
elsif matches_regexes?(key, value)
3630
STRING_MASK
31+
elsif key == 'query_string' || key == :query_string
32+
sanitize_query_string(value)
3733
else
38-
v
34+
value
3935
end
4036
else
41-
v
37+
value
4238
end
4339
end
4440

@@ -51,8 +47,8 @@ def sanitize_query_string(query_string)
5147
end
5248

5349
def matches_regexes?(k, v)
54-
(sanitize_credit_cards && CREDIT_CARD_RE.match(v.to_s)) ||
55-
fields_re.match(k.to_s)
50+
(sanitize_credit_cards && v =~ CREDIT_CARD_RE) ||
51+
k =~ fields_re
5652
end
5753

5854
def fields_re
@@ -70,6 +66,7 @@ def special_characters?(string)
7066
end
7167

7268
def parse_json_or_nil(string)
69+
return unless string.start_with?("[", "{")
7370
JSON.parse(string)
7471
rescue JSON::ParserError, NoMethodError
7572
nil

lib/raven/processor/utf8conversion.rb

+12-13
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
module Raven
22
class Processor::UTF8Conversion < Processor
33
def process(value)
4-
if value.is_a? Array
5-
value.map { |v| process v }
6-
elsif value.is_a? Hash
7-
value.merge(value) { |_, v| process v }
8-
elsif value.is_a?(Exception) && !value.message.valid_encoding?
4+
case value
5+
when Array
6+
!value.frozen? ? value.map! { |v| process v } : value.map { |v| process v }
7+
when Hash
8+
!value.frozen? ? value.merge!(value) { |_, v| process v } : value.merge(value) { |_, v| process v }
9+
when Exception
10+
return value if value.message.valid_encoding?
911
clean_exc = value.class.new(clean_invalid_utf8_bytes(value.message))
1012
clean_exc.set_backtrace(value.backtrace)
1113
clean_exc
12-
else
14+
when String
15+
return value if value.valid_encoding?
1316
clean_invalid_utf8_bytes(value)
17+
else
18+
value
1419
end
1520
end
1621

1722
private
1823

1924
def clean_invalid_utf8_bytes(obj)
20-
if obj.respond_to?(:to_utf8)
21-
obj.to_utf8
22-
elsif obj.respond_to?(:encoding) && obj.is_a?(String)
23-
obj.encode('UTF-16', :invalid => :replace, :undef => :replace, :replace => '').encode('UTF-8')
24-
else
25-
obj
26-
end
25+
obj.encode('UTF-16', :invalid => :replace, :undef => :replace, :replace => '').encode('UTF-8')
2726
end
2827
end
2928
end

spec/raven/json_spec.rb

+26-3
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,31 @@
4141
expect(JSON.parse("[123e090000000]")).to eq [+1.0 / 0.0]
4242
end
4343

44-
it 'it raises the correct error on strings that look like incomplete objects' do
45-
expect { JSON.parse("{") }.to raise_error(JSON::ParserError)
46-
expect { JSON.parse("[") }.to raise_error(JSON::ParserError)
44+
if RUBY_VERSION.to_f >= 2.0 # 1.9 just hangs on this.
45+
it 'it raises the correct error on strings that look like incomplete objects' do
46+
expect { JSON.parse("{") }.to raise_error(JSON::ParserError)
47+
expect { JSON.parse("[") }.to raise_error(JSON::ParserError)
48+
end
49+
50+
it "raises an error on bad UTF8 input" do
51+
expect do
52+
JSON.parse("invalid utf8 string goes here\255".force_encoding('UTF-8'))
53+
end.to raise_error(JSON::ParserError)
54+
end
55+
56+
it "blows up on circular references" do
57+
data = {}
58+
data['data'] = data
59+
data['ary'] = []
60+
data['ary'].push('x' => data['ary'])
61+
data['ary2'] = data['ary']
62+
data['leave intact'] = { 'not a circular reference' => true }
63+
64+
if RUBY_PLATFORM == 'java'
65+
expect { JSON.dump(data) }.to raise_error
66+
else
67+
expect { JSON.dump(data) }.to raise_error(SystemStackError)
68+
end
69+
end
4770
end
4871
end

spec/raven/processors/removecirculareferences_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
result = @processor.process(data)
2020
expect(result['data']).to eq('(...)')
2121
expect(result['ary'].first['x']).to eq('(...)')
22-
expect(result['ary2']).not_to eq('(...)')
22+
expect(result['ary2']).to eq("(...)")
2323
expect(result['leave intact']).to eq('not a circular reference' => true)
2424
end
2525
end

spec/raven/processors/sanitizedata_processor_spec.rb

-9
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,6 @@
111111
expect { JSON.parse(result["data"]["invalid"]) }.to raise_exception(JSON::ParserError)
112112
end
113113

114-
it 'should not sanitize frozen objects' do
115-
data = {
116-
'ccnumba' => '4242424242424242'
117-
}.freeze
118-
119-
result = @processor.process(data)
120-
expect(result["ccnumba"]).to eq('4242424242424242')
121-
end
122-
123114
it 'should filter credit card values' do
124115
data = {
125116
'ccnumba' => '4242424242424242',

0 commit comments

Comments
 (0)