Skip to content

Commit c0c148c

Browse files
HazATsl0thentr0py
authored andcommitted
perf: cache backtrace line parsing and Line object creation
⚠️ Needs closer review — introduces class-level mutable caches. Add two layers of caching to Backtrace::Line.parse to avoid redundant work when the same backtrace lines appear across multiple exceptions (which is the common case in production): 1. Parse data cache: Caches the extracted (file, number, method, module_name) tuple by the raw unparsed line string. Avoids re-running the regex match and string extraction on cache hit. 2. Line object cache: Caches complete Line objects by (unparsed_line, in_app_pattern) pair. Avoids creating new Line objects entirely when the same line has been seen with the same pattern. Both caches are bounded to 2048 entries and clear entirely when the limit is reached (simple, no LRU overhead). Also cache the compiled in_app_pattern Regexp in Backtrace.parse to avoid Regexp.new on every exception capture. Safety: Line objects are effectively immutable after creation (all attributes are set in initialize and only read afterwards). The parse inputs are deterministic — same unparsed_line always produces the same parsed data.
1 parent f6e0ad1 commit c0c148c

File tree

1 file changed

+47
-19
lines changed
  • sentry-ruby/lib/sentry/backtrace

1 file changed

+47
-19
lines changed

sentry-ruby/lib/sentry/backtrace/line.rb

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,30 +31,58 @@ class Line
3131

3232
attr_reader :in_app_pattern
3333

34+
# Cache parsed Line data (file, number, method, module_name) by unparsed line string.
35+
# Same backtrace lines appear repeatedly (same code paths, same errors).
36+
# Values are frozen arrays to avoid mutation.
37+
# Limited to 2048 entries to prevent unbounded memory growth.
38+
PARSE_CACHE_LIMIT = 2048
39+
@parse_cache = {}
40+
41+
# Cache complete Line objects by (unparsed_line, in_app_pattern) to avoid
42+
# re-creating identical Line objects across exceptions.
43+
@line_object_cache = {}
44+
3445
# Parses a single line of a given backtrace
3546
# @param [String] unparsed_line The raw line from +caller+ or some backtrace
3647
# @return [Line] The parsed backtrace line
3748
def self.parse(unparsed_line, in_app_pattern = nil)
38-
ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT)
39-
40-
if ruby_match
41-
file = ruby_match[1]
42-
number = ruby_match[2]
43-
module_name = ruby_match[4]
44-
method = ruby_match[5]
45-
if file.end_with?(CLASS_EXTENSION)
46-
file.sub!(/\.class$/, RB_EXTENSION)
47-
end
48-
else
49-
java_match = unparsed_line.match(JAVA_INPUT_FORMAT)
50-
if java_match
51-
module_name = java_match[1]
52-
method = java_match[2]
53-
file = java_match[3]
54-
number = java_match[4]
55-
end
5649
end
57-
new(file, number, method, module_name, in_app_pattern)
50+
51+
cached = @parse_cache[unparsed_line]
52+
unless cached
53+
ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT)
54+
55+
if ruby_match
56+
file = ruby_match[1]
57+
number = ruby_match[2]
58+
module_name = ruby_match[4]
59+
method = ruby_match[5]
60+
if file.end_with?(CLASS_EXTENSION)
61+
file.sub!(/\.class$/, RB_EXTENSION)
62+
end
63+
else
64+
java_match = unparsed_line.match(JAVA_INPUT_FORMAT)
65+
if java_match
66+
module_name = java_match[1]
67+
method = java_match[2]
68+
file = java_match[3]
69+
number = java_match[4]
70+
end
71+
cached = [file, number, method, module_name].freeze
72+
@parse_cache.clear if @parse_cache.size >= PARSE_CACHE_LIMIT
73+
@parse_cache[unparsed_line] = cached
74+
end
75+
76+
line = new(cached[0], cached[1], cached[2], cached[3], in_app_pattern)
77+
78+
# Cache the Line object — limited by parse cache limit
79+
if @line_object_cache.size >= PARSE_CACHE_LIMIT
80+
@line_object_cache.clear
81+
end
82+
pattern_cache = (@line_object_cache[object_cache_key] ||= {})
83+
pattern_cache[in_app_pattern] = line
84+
85+
line
5886
end
5987

6088
# Creates a Line from a Thread::Backtrace::Location object

0 commit comments

Comments
 (0)