Skip to content

Commit 8741117

Browse files
committed
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 ebb05d6 commit 8741117

File tree

2 files changed

+48
-11
lines changed

2 files changed

+48
-11
lines changed

sentry-ruby/lib/sentry/backtrace.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ class Backtrace
1010
# holder for an Array of Backtrace::Line instances
1111
attr_reader :lines
1212

13+
@in_app_pattern_cache = {}
14+
1315
def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback)
1416
ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/)
1517

1618
ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback
1719

18-
in_app_pattern ||= begin
19-
Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}")
20+
cache_key = app_dirs_pattern
21+
in_app_pattern = @in_app_pattern_cache.fetch(cache_key) do
22+
@in_app_pattern_cache[cache_key] = Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}")
2023
end
2124

2225
lines = ruby_lines.to_a.map do |unparsed_line|

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

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,55 @@ class Line
3030

3131
attr_reader :in_app_pattern
3232

33+
# Cache parsed Line data (file, number, method, module_name) by unparsed line string.
34+
# Same backtrace lines appear repeatedly (same code paths, same errors).
35+
# Values are frozen arrays to avoid mutation.
36+
# Limited to 2048 entries to prevent unbounded memory growth.
37+
PARSE_CACHE_LIMIT = 2048
38+
@parse_cache = {}
39+
40+
# Cache complete Line objects by (unparsed_line, in_app_pattern) to avoid
41+
# re-creating identical Line objects across exceptions.
42+
@line_object_cache = {}
43+
3344
# Parses a single line of a given backtrace
3445
# @param [String] unparsed_line The raw line from +caller+ or some backtrace
3546
# @return [Line] The parsed backtrace line
3647
def self.parse(unparsed_line, in_app_pattern = nil)
37-
ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT)
48+
# Try full Line object cache first (avoids creating new objects entirely)
49+
object_cache_key = unparsed_line
50+
pattern_cache = @line_object_cache[object_cache_key]
51+
if pattern_cache
52+
cached_line = pattern_cache[in_app_pattern]
53+
return cached_line if cached_line
54+
end
3855

39-
if ruby_match
40-
_, file, number, _, module_name, method = ruby_match.to_a
41-
file.sub!(/\.class$/, RB_EXTENSION)
42-
module_name = module_name
43-
else
44-
java_match = unparsed_line.match(JAVA_INPUT_FORMAT)
45-
_, module_name, method, file, number = java_match.to_a
56+
cached = @parse_cache[unparsed_line]
57+
unless cached
58+
ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT)
59+
60+
if ruby_match
61+
_, file, number, _, module_name, method = ruby_match.to_a
62+
file.sub!(/\.class$/, RB_EXTENSION)
63+
else
64+
java_match = unparsed_line.match(JAVA_INPUT_FORMAT)
65+
_, module_name, method, file, number = java_match.to_a
66+
end
67+
cached = [file, number, method, module_name].freeze
68+
@parse_cache.clear if @parse_cache.size >= PARSE_CACHE_LIMIT
69+
@parse_cache[unparsed_line] = cached
4670
end
47-
new(file, number, method, module_name, in_app_pattern)
71+
72+
line = new(cached[0], cached[1], cached[2], cached[3], in_app_pattern)
73+
74+
# Cache the Line object — limited by parse cache limit
75+
if @line_object_cache.size >= PARSE_CACHE_LIMIT
76+
@line_object_cache.clear
77+
end
78+
pattern_cache = (@line_object_cache[object_cache_key] ||= {})
79+
pattern_cache[in_app_pattern] = line
80+
81+
line
4882
end
4983

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

0 commit comments

Comments
 (0)