From 2a45546ed9e9533ce468bc8391f399193186182f Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Wed, 18 Mar 2026 15:53:36 +0100 Subject: [PATCH] perf: cache longest_load_path and compute_filename results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add class-level caches to StacktraceInterface for two expensive per-frame operations that repeat with identical inputs: longest_load_path: Previously iterated $LOAD_PATH for every frame, creating many intermediate strings. Now cached by abs_path with automatic invalidation when $LOAD_PATH.size changes (e.g. after Bundler.require). compute_filename: Many frames share identical abs_paths (same gem files appear in every exception). Results are cached in separate in_app/ not_in_app hashes keyed by abs_path only, avoiding composite array keys. Cache invalidates on project_root or $LOAD_PATH changes. Both caches are deterministic — same inputs always produce the same filename. The caches grow proportionally to the number of unique source files seen, which is naturally bounded in any application. --- .../lib/sentry/interfaces/stacktrace.rb | 77 ++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) 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