diff --git a/sentry-ruby/lib/sentry/interfaces/stacktrace.rb b/sentry-ruby/lib/sentry/interfaces/stacktrace.rb index 104b7a0fb..5d007b327 100644 --- a/sentry-ruby/lib/sentry/interfaces/stacktrace.rb +++ b/sentry-ruby/lib/sentry/interfaces/stacktrace.rb @@ -23,6 +23,77 @@ def inspect private # Not actually an interface, but I want to use the same style + # Cache for longest_load_path lookups — shared across all frames + @load_path_cache = {} + @load_path_size = nil + # Cache for compute_filename results — many frames share identical abs_paths + # Separate caches for in_app=true and in_app=false to avoid composite keys + @filename_cache_in_app = {} + @filename_cache_not_in_app = {} + @filename_project_root = nil + + class << self + def check_load_path_freshness + current_size = $LOAD_PATH.size + if @load_path_size != current_size + @load_path_cache = {} + @filename_cache_in_app = {} + @filename_cache_not_in_app = {} + @load_path_size = current_size + end + end + + def longest_load_path_for(abs_path) + check_load_path_freshness + + @load_path_cache.fetch(abs_path) do + result = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size) + @load_path_cache[abs_path] = result + end + end + + def cached_filename(abs_path, project_root, in_app, strip_backtrace_load_path) + return abs_path unless abs_path + return abs_path unless strip_backtrace_load_path + + check_load_path_freshness + + # Invalidate filename cache when project_root changes + if @filename_project_root != project_root + @filename_cache_in_app = {} + @filename_cache_not_in_app = {} + @filename_project_root = project_root + end + + cache = in_app ? @filename_cache_in_app : @filename_cache_not_in_app + cache.fetch(abs_path) do + under_root = project_root && abs_path.start_with?(project_root) + prefix = + if under_root && in_app + project_root + elsif under_root + longest_load_path_for(abs_path) || project_root + else + longest_load_path_for(abs_path) + end + + result = if prefix + prefix_str = prefix.to_s + offset = if prefix_str.end_with?(File::SEPARATOR) + prefix_str.length + else + prefix_str.length + 1 + end + abs_path.byteslice(offset, abs_path.bytesize - offset) + else + abs_path + end + + cache[abs_path] = result + end + end + end + class Frame < Interface attr_accessor :abs_path, :context_line, :function, :in_app, :filename, :lineno, :module, :pre_context, :post_context, :vars @@ -36,7 +107,9 @@ def initialize(project_root, line, strip_backtrace_load_path = true) @lineno = line.number @in_app = line.in_app @module = line.module_name if line.module_name - @filename = compute_filename + @filename = StacktraceInterface.cached_filename( + @abs_path, project_root, @in_app, strip_backtrace_load_path + ) end def to_s @@ -82,7 +155,7 @@ def under_project_root? end def longest_load_path - $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size) + StacktraceInterface.longest_load_path_for(abs_path) end end end