perf: Add FilenameCache to cache compute_filename results#2904
Merged
sl0thentr0py merged 1 commit intomasterfrom Mar 30, 2026
Merged
perf: Add FilenameCache to cache compute_filename results#2904sl0thentr0py merged 1 commit intomasterfrom
sl0thentr0py merged 1 commit intomasterfrom
Conversation
HazAT
added a commit
that referenced
this pull request
Mar 18, 2026
Reduce total allocated memory from 442k to 206k bytes (-53.5%) and
objects from 3305 to 1538 (-53.5%) per Rails exception capture.
All changes are internal optimizations with zero behavior changes.
Key optimizations:
- Cache longest_load_path and compute_filename results (class-level,
invalidated on $LOAD_PATH changes)
- Cache backtrace line parsing and Line/Frame object creation (bounded
at 2048 entries)
- Optimize LineCache with Hash#fetch, direct context setting, and
per-(filename, lineno) caching
- Avoid unnecessary allocations: indexed regex captures, match? instead
of =~, byteslice, single-pass iteration in StacktraceBuilder
- RequestInterface: avoid env.dup, cache header name transforms, ASCII
fast-path for encoding
- Scope/BreadcrumbBuffer: shallow dup instead of deep_dup where inner
values are not mutated after duplication
- Hub#add_breadcrumb: hint default nil instead of {} to avoid empty
hash allocation
See sub-PRs for detailed review by risk level:
- #2902 (low risk) — hot path allocation avoidance
- #2903 (low risk) — LineCache optimization
- #2904 (medium risk) — load path and filename caching
- #2905 (needs review) — backtrace parse caching
- #2906 (needs review) — Frame object caching
- #2907 (needs review) — Scope/BreadcrumbBuffer shallow dup
- #2908 (medium risk) — RequestInterface optimizations
2a45546 to
398da01
Compare
398da01 to
33bb925
Compare
430233c to
c33e3c0
Compare
Changed below autoresearch implementation to just have one universal FilenameCache --- 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.
c33e3c0 to
b86ccae
Compare
sl0thentr0py
approved these changes
Mar 27, 2026
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Frame silently produces nil filename without cache
- Made filename_cache a required keyword argument instead of defaulting to nil, so callers must provide a valid cache and will get an ArgumentError if they don't.
- ✅ Fixed: Dead
project_rootparameter in Frame constructor- Removed the unused project_root positional parameter from Frame.initialize since all filename computation is now delegated to FilenameCache.
Or push these changes by commenting:
@cursor push b9a3b54088
Preview (b9a3b54088)
diff --git a/sentry-ruby/lib/sentry/interfaces/stacktrace.rb b/sentry-ruby/lib/sentry/interfaces/stacktrace.rb
--- a/sentry-ruby/lib/sentry/interfaces/stacktrace.rb
+++ b/sentry-ruby/lib/sentry/interfaces/stacktrace.rb
@@ -27,7 +27,7 @@
attr_accessor :abs_path, :context_line, :function, :in_app, :filename,
:lineno, :module, :pre_context, :post_context, :vars
- def initialize(project_root, line, strip_backtrace_load_path = true, filename_cache: nil)
+ def initialize(line, strip_backtrace_load_path: true, filename_cache:)
@strip_backtrace_load_path = strip_backtrace_load_path
@filename_cache = filename_cache
diff --git a/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb b/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb
--- a/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb
+++ b/sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb
@@ -90,7 +90,7 @@
private
def convert_parsed_line_into_frame(line)
- frame = StacktraceInterface::Frame.new(project_root, line, strip_backtrace_load_path, filename_cache: @filename_cache)
+ frame = StacktraceInterface::Frame.new(line, strip_backtrace_load_path: strip_backtrace_load_path, filename_cache: @filename_cache)
frame.set_context(linecache, context_lines) if context_lines
frame
end
diff --git a/sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb b/sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb
--- a/sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb
+++ b/sentry-ruby/spec/sentry/interfaces/stacktrace_spec.rb
@@ -15,14 +15,14 @@
let(:filename_cache) { Sentry::FilenameCache.new(configuration.project_root) }
it "initializes a Frame with the correct info from the given Backtrace::Line object" do
- first_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.first, true, filename_cache: filename_cache)
+ first_frame = Sentry::StacktraceInterface::Frame.new(lines.first, strip_backtrace_load_path: true, filename_cache: filename_cache)
expect(first_frame.filename).to match(/base.rb/)
expect(first_frame.in_app).to eq(false)
expect(first_frame.function).to eq("save")
expect(first_frame.lineno).to eq(10)
- second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last, true, filename_cache: filename_cache)
+ second_frame = Sentry::StacktraceInterface::Frame.new(lines.last, strip_backtrace_load_path: true, filename_cache: filename_cache)
expect(second_frame.filename).to match(/post.rb/)
expect(second_frame.in_app).to eq(true)
@@ -31,11 +31,11 @@
end
it "does not strip load path when strip_backtrace_load_path is false" do
- first_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.first, false, filename_cache: filename_cache)
+ first_frame = Sentry::StacktraceInterface::Frame.new(lines.first, strip_backtrace_load_path: false, filename_cache: filename_cache)
expect(first_frame.filename).to eq(first_frame.abs_path)
expect(first_frame.filename).to eq(raw_lines.first.split(':').first)
- second_frame = Sentry::StacktraceInterface::Frame.new(configuration.project_root, lines.last, false, filename_cache: filename_cache)
+ second_frame = Sentry::StacktraceInterface::Frame.new(lines.last, strip_backtrace_load_path: false, filename_cache: filename_cache)
expect(second_frame.filename).to eq(second_frame.abs_path)
expect(second_frame.filename).to eq(raw_lines.last.split(':').first)
end
diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb
--- a/sentry-ruby/spec/sentry/transport_spec.rb
+++ b/sentry-ruby/spec/sentry/transport_spec.rb
@@ -341,9 +341,8 @@
new_stacktrace = Sentry::StacktraceInterface.new(
frames: frame_list_size.times.map do |zero_based_index|
Sentry::StacktraceInterface::Frame.new(
- "/fake/path",
Sentry::Backtrace::Line.parse("app.rb:#{zero_based_index + 1}:in `/'", in_app_pattern),
- true,
+ strip_backtrace_load_path: true,
filename_cache: Sentry::FilenameCache.new("/fake/path")
)
end,This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Changed below autoresearch implementation to just have one universal
FilenameCache.Below is what autoresearch initially did.
⚡ Medium risk — class-level caches with invalidation
Part of #2901 (reduce memory allocations by ~53%)
Changes
Add class-level caches to
StacktraceInterfacefor two expensive per-frame operations that repeat with identical inputs:longest_load_path: Previously iterated$LOAD_PATHfor every frame, creating many intermediate strings. Now cached byabs_pathwith automatic invalidation when$LOAD_PATH.sizechanges (e.g. afterBundler.require).compute_filename: Many frames share identicalabs_paths (same gem files appear in every exception). Results are cached in separatein_app/not_in_apphashes keyed byabs_pathonly, avoiding composite array keys. Cache invalidates onproject_rootor$LOAD_PATHchanges.Safety
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.