diff --git a/sentry-ruby/lib/sentry/backtrace.rb b/sentry-ruby/lib/sentry/backtrace.rb index 73a77acfd..9c7803465 100644 --- a/sentry-ruby/lib/sentry/backtrace.rb +++ b/sentry-ruby/lib/sentry/backtrace.rb @@ -10,13 +10,16 @@ class Backtrace # holder for an Array of Backtrace::Line instances attr_reader :lines + @in_app_pattern_cache = {} + def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback) ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/) ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback - in_app_pattern ||= begin - Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}") + cache_key = app_dirs_pattern + in_app_pattern = @in_app_pattern_cache.fetch(cache_key) do + @in_app_pattern_cache[cache_key] = Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}") end lines = ruby_lines.to_a.map do |unparsed_line| diff --git a/sentry-ruby/lib/sentry/backtrace/line.rb b/sentry-ruby/lib/sentry/backtrace/line.rb index 2a9f6c96e..a30066ad8 100644 --- a/sentry-ruby/lib/sentry/backtrace/line.rb +++ b/sentry-ruby/lib/sentry/backtrace/line.rb @@ -30,21 +30,55 @@ 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) + # Try full Line object cache first (avoids creating new objects entirely) + object_cache_key = unparsed_line + pattern_cache = @line_object_cache[object_cache_key] + if pattern_cache + cached_line = pattern_cache[in_app_pattern] + return cached_line if cached_line + end - if ruby_match - _, file, number, _, module_name, method = ruby_match.to_a - file.sub!(/\.class$/, RB_EXTENSION) - module_name = module_name - else - java_match = unparsed_line.match(JAVA_INPUT_FORMAT) - _, module_name, method, file, number = java_match.to_a + cached = @parse_cache[unparsed_line] + unless cached + ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT) + + if ruby_match + _, file, number, _, module_name, method = ruby_match.to_a + file.sub!(/\.class$/, RB_EXTENSION) + else + java_match = unparsed_line.match(JAVA_INPUT_FORMAT) + _, module_name, method, file, number = java_match.to_a + 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