Skip to content

perf: Cache parsed backtrace lines - #3202

Open
sentry-junior[bot] wants to merge 1 commit into
masterfrom
perf/backtrace-line-parse-cache
Open

sentry-junior[bot] wants to merge 1 commit into
masterfrom
perf/backtrace-line-parse-cache

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

A slimmed-down version of #2905. It keeps the part that actually saves work and drops the rest.

Backtrace::Line.parse runs a regex and allocates a MatchData plus substrings for every frame of every captured exception. A recurring error produces the same raw lines on every capture, so this caches the parsed [file, number, method, module_name] by raw line.

How this differs from #2905

  • No Line object cache. In perf: cache backtrace line parsing and Line object creation #2905 it was written but never read, and its key (object_cache_key) was undefined, so every parse raised. Allocating one Line per frame is cheap. The regex and substrings are where the cost is.
  • No in_app_pattern Regexp cache. StacktraceBuilder already passes in a precompiled pattern, and that cache's key left out project_root.
  • Concurrent::Map instead of {}. This matches Backtrace.line_cache and is safe on JRuby, where a plain Hash can be corrupted by concurrent writes.
  • Cached strings are frozen. Every Line and event frame built from the same raw line now shares them. Without freezing, a before_send that mutates frame.abs_path in place would change it for every later event.
  • Not keyed by in_app_pattern. Parsing doesn't depend on the pattern, and in_app is still computed for each Line.
  • Limit is 4096 instead of 2048. A Rails stack is often 100+ frames, so 2048 fills after about 15–20 distinct error paths and then keeps clearing. Measured, a full cache holds about 2 MiB (~520 B per entry for ~110-byte lines). The cache is cleared when full, with no LRU.

Numbers (benchmarks/backtrace_parse.rb: Ruby 3.2.8, 120-frame Rails-like backtrace, warm caches, a transport that serializes the envelope and then drops it)

master this branch
Backtrace.parse allocations 722 objects / 79.4 KB 122 objects / 10.6 KB
Backtrace.parse throughput ~2.7k i/s ~10–11k i/s
capture_exception allocations 3,658 objects / 353 KB 3,058 objects / 284 KB (−16% / −19%)
capture_exception throughput ~460–475 i/s ~525–555 i/s (noisy, ±10–15%)

On a cache miss, a line costs 8 objects instead of 6 (the key copy plus the frozen tuple). So an app that keeps cycling through more than 4096 distinct lines pays a little extra and gets no benefit.

Behavior change to call out: Line#file, #method and #module_name, and therefore Frame#abs_path and #function, are now frozen strings. Code that mutates them in place (e.g. abs_path.sub! in before_send) will raise FrozenError instead of working. Reassigning them (frame.abs_path = ...) still works. The whole sentry-ruby suite passes under --enable-frozen-string-literal --debug=frozen-string-literal, so the SDK itself doesn't mutate them. We should decide whether this needs a changelog note.

via @dingsdax.

--

View Session [Sentry]

Backtrace::Line.parse runs a regex match and allocates a MatchData plus
substrings for every frame of every captured exception. A recurring
error produces the same raw lines each time, so cache the parsed
[file, number, method, module_name] by raw line.

- Concurrent::Map, like the other backtrace caches, so it's safe on JRuby.
- The cached strings are frozen because every Line created from the same
  raw line now shares them.
- Not keyed by in_app_pattern: parsing doesn't depend on it, and in_app
  is still computed per Line.
- Bounded at 4096 entries (~2 MiB), cleared when full.

Slimmed-down take on #2905: drops its Line object cache and the
in_app_pattern Regexp cache.

Co-Authored-By: Johannes Daxböck <114897+dingsdax@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant