Skip to content

Commit 7ce022e

Browse files
committed
perf: optimize LineCache to reduce allocations
Refactor LineCache internals to reduce memory allocations when reading source context for stacktrace frames: - Use Hash#fetch instead of ||= to avoid double hash lookup in getlines - Remove valid_path?/getline indirection — inline bounds checking via line_at helper - Build pre/post context arrays directly with Array.new instead of creating a single large array and slicing it - Add set_frame_context method that sets frame attributes directly, avoiding the intermediate [pre, context_line, post] array allocation - Cache context results per (filename, lineno) since the same frames repeat across exceptions — avoids recreating identical arrays The public get_file_context API is preserved for custom LineCache implementations.
1 parent ebb05d6 commit 7ce022e

File tree

1 file changed

+49
-20
lines changed

1 file changed

+49
-20
lines changed

sentry-ruby/lib/sentry/linecache.rb

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,72 @@ module Sentry
55
class LineCache
66
def initialize
77
@cache = {}
8+
@context_cache = {}
89
end
910

1011
# Any linecache you provide to Sentry must implement this method.
1112
# Returns an Array of Strings representing the lines in the source
1213
# file. The number of lines retrieved is (2 * context) + 1, the middle
1314
# line should be the line requested by lineno. See specs for more information.
1415
def get_file_context(filename, lineno, context)
15-
return nil, nil, nil unless valid_path?(filename)
16+
lines = getlines(filename)
17+
return nil, nil, nil unless lines
1618

17-
lines = Array.new(2 * context + 1) do |i|
18-
getline(filename, lineno - context + i)
19-
end
20-
[lines[0..(context - 1)], lines[context], lines[(context + 1)..-1]]
19+
first_line = lineno - context
20+
pre = Array.new(context) { |i| line_at(lines, first_line + i) }
21+
context_line = line_at(lines, lineno)
22+
post = Array.new(context) { |i| line_at(lines, lineno + 1 + i) }
23+
[pre, context_line, post]
2124
end
2225

23-
private
24-
25-
def valid_path?(path)
26-
lines = getlines(path)
27-
!lines.nil?
28-
end
26+
# Sets context directly on a frame, avoiding intermediate array allocation.
27+
# Caches results per (filename, lineno) since the same frames repeat across exceptions.
28+
def set_frame_context(frame, filename, lineno, context)
29+
cache_key = lineno
30+
file_contexts = @context_cache[filename]
2931

30-
def getlines(path)
31-
@cache[path] ||= begin
32-
File.open(path, "r", &:readlines)
33-
rescue
34-
nil
32+
if file_contexts
33+
cached = file_contexts[cache_key]
34+
if cached
35+
frame.pre_context = cached[0]
36+
frame.context_line = cached[1]
37+
frame.post_context = cached[2]
38+
return
39+
end
3540
end
41+
42+
lines = getlines(filename)
43+
return unless lines
44+
45+
first_line = lineno - context
46+
pre = Array.new(context) { |i| line_at(lines, first_line + i) }
47+
ctx_line = line_at(lines, lineno)
48+
post = Array.new(context) { |i| line_at(lines, lineno + 1 + i) }
49+
50+
file_contexts = (@context_cache[filename] ||= {})
51+
file_contexts[cache_key] = [pre, ctx_line, post].freeze
52+
53+
frame.pre_context = pre
54+
frame.context_line = ctx_line
55+
frame.post_context = post
3656
end
3757

38-
def getline(path, n)
39-
return nil if n < 1
58+
private
4059

41-
lines = getlines(path)
42-
return nil if lines.nil?
60+
def line_at(lines, n)
61+
return nil if n < 1
4362

4463
lines[n - 1]
4564
end
65+
66+
def getlines(path)
67+
@cache.fetch(path) do
68+
@cache[path] = begin
69+
File.open(path, "r", &:readlines)
70+
rescue
71+
nil
72+
end
73+
end
74+
end
4675
end
4776
end

0 commit comments

Comments
 (0)