Skip to content

Commit 56c6d46

Browse files
committed
Reuse decls in resolve_type_names when no type names change
`resolve_type_names` previously rebuilt every declaration, member, and type even when type name resolution did not change anything. The first resolve must produce absolutized type names, so its cost is unavoidable; but the second and later resolves were re-allocating identical structures for no benefit, pressuring GC heavily. This change makes every `map_type_name` / `map_type` / `resolve_*` helper return its receiver when each child maps back to a value `equal?` to the original. Combined with the existing flyweight behavior of `TypeName` and `Namespace`, declarations whose type names were already absolute are now reused verbatim across resolves. The first resolve is therefore unchanged in both wall time and allocations. The numbers below compare the second-and-later resolves only, measured on conference-app (kaigionrails/conference-app): - allocated per resolve: 20.60 MB / 387,664 objects -> 3.52 MB / 64,293 objects (-83%) - retained over 10 resolves: 18.36 MB / 346,343 objects -> 1.25 MB / 22,973 objects (-93%) - resolve wall time, p99: 112.7 ms -> 98.3 ms (-12.8%) - GC major / 50 resolves: 4 -> 1 (-75%) Single-shot CLI usage (`rbs list` etc.) calls resolve_type_names only once, so it sees no change. Long-running clients such as Steep that re-resolve repeatedly are the primary beneficiaries.
1 parent fcc1685 commit 56c6d46

7 files changed

Lines changed: 350 additions & 225 deletions

File tree

lib/rbs.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,43 @@ def print_warning()
102102
logger.warn { message }
103103
end
104104
end
105+
106+
# Internal helper for `map_type_name` / `map_type` / `resolve_*` paths
107+
# in this gem. The given block is invoked for every element. Returns
108+
# the input array unchanged (the same object) when every mapped result
109+
# is `equal?` to its source; otherwise returns a fresh array with the
110+
# changed elements substituted in. Callers detect a no-op by comparing
111+
# the return value with the input via `equal?`, which avoids
112+
# allocating a `[mapped, changed]` tuple on every invocation.
113+
def map_if_changed(array, &)
114+
return array if array.empty?
115+
116+
result = nil
117+
array.each_with_index do |element, i|
118+
new_element = yield(element)
119+
next if new_element.equal?(element)
120+
121+
result ||= array.dup
122+
result[i] = new_element
123+
end
124+
result || array
125+
end
126+
127+
# Hash counterpart of `map_if_changed`: transforms values through the
128+
# block and returns the receiver unchanged when every value identity
129+
# is preserved.
130+
def transform_values_if_changed(hash, &)
131+
return hash if hash.empty?
132+
133+
result = nil
134+
hash.each do |key, value|
135+
new_value = yield(value)
136+
next if new_value.equal?(value)
137+
138+
result ||= hash.dup
139+
result[key] = new_value
140+
end
141+
result || hash
142+
end
105143
end
106144
end

lib/rbs/ast/type_param.rb

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,23 @@ def to_json(state = JSON::State.new)
6767
end
6868

6969
def map_type(&block)
70-
if b = upper_bound_type
71-
_upper_bound_type = yield(b)
72-
end
73-
74-
if b = lower_bound_type
75-
_lower_bound_type = yield(b)
76-
end
70+
new_upper_bound_type = upper_bound_type ? yield(upper_bound_type) : nil
71+
new_lower_bound_type = lower_bound_type ? yield(lower_bound_type) : nil
72+
new_default_type = default_type ? yield(default_type) : nil
7773

78-
if dt = default_type
79-
_default_type = yield(dt)
74+
if new_upper_bound_type.equal?(upper_bound_type) &&
75+
new_lower_bound_type.equal?(lower_bound_type) &&
76+
new_default_type.equal?(default_type)
77+
return self
8078
end
8179

8280
TypeParam.new(
8381
name: name,
8482
variance: variance,
85-
upper_bound: _upper_bound_type,
86-
lower_bound: _lower_bound_type,
83+
upper_bound: new_upper_bound_type,
84+
lower_bound: new_lower_bound_type,
8785
location: location,
88-
default_type: _default_type
86+
default_type: new_default_type
8987
).unchecked!(unchecked?)
9088
end
9189

0 commit comments

Comments
 (0)