diff --git a/sentry-ruby/lib/sentry/backtrace/line.rb b/sentry-ruby/lib/sentry/backtrace/line.rb index 741038e41..772af9060 100644 --- a/sentry-ruby/lib/sentry/backtrace/line.rb +++ b/sentry-ruby/lib/sentry/backtrace/line.rb @@ -31,30 +31,57 @@ class Line attr_reader :in_app_pattern + # Cache parsed Line data (file, number, method, module_name) by unparsed line string. + # Same backtrace lines appear repeatedly (same code paths, same errors). + # Values are frozen arrays to avoid mutation. + # Limited to 2048 entries to prevent unbounded memory growth. + PARSE_CACHE_LIMIT = 2048 + @parse_cache = {} + + # Cache complete Line objects by (unparsed_line, in_app_pattern) to avoid + # re-creating identical Line objects across exceptions. + @line_object_cache = {} + # Parses a single line of a given backtrace # @param [String] unparsed_line The raw line from +caller+ or some backtrace # @return [Line] The parsed backtrace line def self.parse(unparsed_line, in_app_pattern = nil) - ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT) - - if ruby_match - file = ruby_match[1] - number = ruby_match[2] - module_name = ruby_match[4] - method = ruby_match[5] - if file.end_with?(CLASS_EXTENSION) - file.sub!(/\.class$/, RB_EXTENSION) - end - else - java_match = unparsed_line.match(JAVA_INPUT_FORMAT) - if java_match - module_name = java_match[1] - method = java_match[2] - file = java_match[3] - number = java_match[4] + cached = @parse_cache[unparsed_line] + unless cached + ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT) + + if ruby_match + file = ruby_match[1] + number = ruby_match[2] + module_name = ruby_match[4] + method = ruby_match[5] + if file.end_with?(CLASS_EXTENSION) + file.sub!(/\.class$/, RB_EXTENSION) + end + else + java_match = unparsed_line.match(JAVA_INPUT_FORMAT) + if java_match + module_name = java_match[1] + method = java_match[2] + file = java_match[3] + number = java_match[4] + end end + cached = [file, number, method, module_name].freeze + @parse_cache.clear if @parse_cache.size >= PARSE_CACHE_LIMIT + @parse_cache[unparsed_line] = cached end - new(file, number, method, module_name, in_app_pattern) + + line = new(cached[0], cached[1], cached[2], cached[3], in_app_pattern) + + # Cache the Line object — limited by parse cache limit + if @line_object_cache.size >= PARSE_CACHE_LIMIT + @line_object_cache.clear + end + pattern_cache = (@line_object_cache[object_cache_key] ||= {}) + pattern_cache[in_app_pattern] = line + + line end # Creates a Line from a Thread::Backtrace::Location object