Skip to content

Commit 79ac920

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 79ac920

7 files changed

Lines changed: 358 additions & 225 deletions

File tree

lib/rbs.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,51 @@ 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 = array
117+
changed = false
118+
array.each_with_index do |element, i|
119+
new_element = yield(element)
120+
next if new_element.equal?(element)
121+
122+
unless changed
123+
result = array.dup
124+
changed = true
125+
end
126+
result[i] = new_element
127+
end
128+
result
129+
end
130+
131+
# Hash counterpart of `map_if_changed`: transforms values through the
132+
# block and returns the receiver unchanged when every value identity
133+
# is preserved.
134+
def transform_values_if_changed(hash, &)
135+
return hash if hash.empty?
136+
137+
result = hash
138+
changed = false
139+
hash.each do |key, value|
140+
new_value = yield(value)
141+
next if new_value.equal?(value)
142+
143+
unless changed
144+
result = hash.dup
145+
changed = true
146+
end
147+
result[key] = new_value
148+
end
149+
result
150+
end
105151
end
106152
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)