Skip to content

Commit 297189e

Browse files
committed
Lazily initialize ChangeSet collections to reduce GC pressure
ChangeSet#initialize was allocating 7 empty Hashes on every instantiation, accounting for 13.2% of CPU samples. Most of these collections are never used in a given ChangeSet instance. Remove the eager Hash allocations from initialize and use lazy accessor methods (e.g. def edges = @edges ||= {}) to allocate on first use. Array fields (@diagnostics, @depended_*) and the if-target guard are kept as-is to preserve the existing contract. Benchmark on Redmine v6.1.1 (app + sig): - Before: ~19.0s, 62 GCs, 34.1M allocations - After: ~14.9s, 47 GCs, 31.7M allocations (~22% faster)
1 parent a7177ac commit 297189e

1 file changed

Lines changed: 36 additions & 34 deletions

File tree

lib/typeprof/core/graph/change_set.rb

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,6 @@ class ChangeSet
33
def initialize(node, target)
44
@node = node
55
@target = target
6-
@new_vertexes = {}
7-
@covariant_types = {}
8-
@contravariant_types = {}
9-
@edges = {}
10-
@new_edges = {}
11-
@boxes = {}
12-
@new_boxes = {}
136
@diagnostics = []
147
@new_diagnostics = []
158
if target
@@ -24,11 +17,19 @@ def initialize(node, target)
2417
end
2518
end
2619

27-
attr_reader :node, :target, :covariant_types, :contravariant_types, :edges, :boxes, :diagnostics
20+
attr_reader :node, :target, :diagnostics
21+
22+
def covariant_types = @covariant_types ||= {}
23+
def contravariant_types = @contravariant_types ||= {}
24+
def edges = @edges ||= {}
25+
def boxes = @boxes ||= {}
26+
27+
private def new_edges = @new_edges ||= {}
28+
private def new_boxes = @new_boxes ||= {}
2829

2930
def reuse(new_node)
3031
@node = new_node
31-
@boxes.each_value do |box|
32+
boxes.each_value do |box|
3233
box.reuse(new_node)
3334
end
3435
end
@@ -48,95 +49,96 @@ def copy_from(other)
4849
end
4950

5051
def new_vertex(genv, origin, base_vtx)
52+
@new_vertexes ||= {}
5153
new_vtx = @new_vertexes[base_vtx] ||= Vertex.new(origin)
5254
add_edge(genv, base_vtx, new_vtx)
5355
new_vtx
5456
end
5557

5658
def new_covariant_vertex(genv, sig_type_node)
5759
# This is used to avoid duplicated vertex generation for the same sig node
58-
@covariant_types[sig_type_node] ||= Vertex.new(sig_type_node)
60+
covariant_types[sig_type_node] ||= Vertex.new(sig_type_node)
5961
end
6062

6163
def new_contravariant_vertex(genv, sig_type_node)
6264
# This is used to avoid duplicated vertex generation for the same sig node
63-
@contravariant_types[sig_type_node] ||= Vertex.new(sig_type_node)
65+
contravariant_types[sig_type_node] ||= Vertex.new(sig_type_node)
6466
end
6567

6668
def add_edge(genv, src, dst)
67-
(@new_edges[src] ||= {})[dst] = true
69+
(new_edges[src] ||= {})[dst] = true
6870
end
6971

7072
# TODO: if an edge is removed during one analysis, we may need to remove sub-boxes?
7173

7274
def add_method_call_box(genv, recv, mid, a_args, subclasses, suppress_errors: false)
7375
key = [:mcall, recv, mid, a_args, subclasses, suppress_errors]
74-
@new_boxes[key] ||= MethodCallBox.new(@node, genv, recv, mid, a_args, subclasses, suppress_errors: suppress_errors)
76+
new_boxes[key] ||= MethodCallBox.new(@node, genv, recv, mid, a_args, subclasses, suppress_errors: suppress_errors)
7577
end
7678

7779
def add_escape_box(genv, a_ret)
7880
key = [:return, a_ret]
79-
@new_boxes[key] ||= EscapeBox.new(@node, genv, a_ret)
81+
new_boxes[key] ||= EscapeBox.new(@node, genv, a_ret)
8082
end
8183

8284
def add_splat_box(genv, arg, idx = nil, orig = nil)
8385
key = [:splat, arg, idx, orig]
84-
@new_boxes[key] ||= SplatBox.new(@node, genv, arg, idx, orig)
86+
new_boxes[key] ||= SplatBox.new(@node, genv, arg, idx, orig)
8587
end
8688

8789
def add_hash_splat_box(genv, arg, unified_key, unified_val)
8890
key = [:hash_splat, arg, unified_key, unified_val]
89-
@new_boxes[key] ||= HashSplatBox.new(@node, genv, arg, unified_key, unified_val)
91+
new_boxes[key] ||= HashSplatBox.new(@node, genv, arg, unified_key, unified_val)
9092
end
9193

9294
def add_masgn_box(genv, value, lefts, rest_elem, rights)
9395
key = [:masgn, value, lefts, rest_elem, rights]
94-
@new_boxes[key] ||= MAsgnBox.new(@node, genv, value, lefts, rest_elem, rights)
96+
new_boxes[key] ||= MAsgnBox.new(@node, genv, value, lefts, rest_elem, rights)
9597
end
9698

9799
def add_method_def_box(genv, cpath, singleton, mid, f_args, ret_boxes)
98100
key = [:mdef, cpath, singleton, mid, f_args, ret_boxes]
99-
@new_boxes[key] ||= MethodDefBox.new(@node, genv, cpath, singleton, mid, f_args, ret_boxes)
101+
new_boxes[key] ||= MethodDefBox.new(@node, genv, cpath, singleton, mid, f_args, ret_boxes)
100102
end
101103

102104
def add_method_decl_box(genv, cpath, singleton, mid, method_types, overloading)
103105
key = [:mdecl, cpath, singleton, mid, method_types, overloading]
104-
@new_boxes[key] ||= MethodDeclBox.new(@node, genv, cpath, singleton, mid, method_types, overloading)
106+
new_boxes[key] ||= MethodDeclBox.new(@node, genv, cpath, singleton, mid, method_types, overloading)
105107
end
106108

107109
def add_method_alias_box(genv, cpath, singleton, new_mid, old_mid)
108110
key = [:mdecl, cpath, singleton, new_mid, old_mid]
109-
@new_boxes[key] ||= MethodAliasBox.new(@node, genv, cpath, singleton, new_mid, old_mid)
111+
new_boxes[key] ||= MethodAliasBox.new(@node, genv, cpath, singleton, new_mid, old_mid)
110112
end
111113

112114
def add_const_read_box(genv, static_ret)
113115
key = [:cread, static_ret]
114-
@new_boxes[key] ||= ConstReadBox.new(@node, genv, static_ret)
116+
new_boxes[key] ||= ConstReadBox.new(@node, genv, static_ret)
115117
end
116118

117119
def add_gvar_read_box(genv, var)
118120
key = [:gvar_read, var]
119-
@new_boxes[key] ||= GVarReadBox.new(@node, genv, var)
121+
new_boxes[key] ||= GVarReadBox.new(@node, genv, var)
120122
end
121123

122124
def add_ivar_read_box(genv, cpath, singleton, name)
123125
key = [:ivar_read, cpath, singleton, name]
124-
@new_boxes[key] ||= IVarReadBox.new(@node, genv, cpath, singleton, name)
126+
new_boxes[key] ||= IVarReadBox.new(@node, genv, cpath, singleton, name)
125127
end
126128

127129
def add_cvar_read_box(genv, cpath, name)
128130
key = [:cvar_read, cpath, name]
129-
@new_boxes[key] ||= CVarReadBox.new(@node, genv, cpath, name)
131+
new_boxes[key] ||= CVarReadBox.new(@node, genv, cpath, name)
130132
end
131133

132134
def add_type_read_box(genv, type)
133135
key = [:type_read, type]
134-
@new_boxes[key] ||= TypeReadBox.new(@node, genv, type)
136+
new_boxes[key] ||= TypeReadBox.new(@node, genv, type)
135137
end
136138

137139
def add_instance_type_box(genv, singleton_ty_vtx)
138140
key = [:instance_type, singleton_ty_vtx]
139-
@new_boxes[key] ||= InstanceTypeBox.new(@node, genv, singleton_ty_vtx)
141+
new_boxes[key] ||= InstanceTypeBox.new(@node, genv, singleton_ty_vtx)
140142
end
141143

142144
def add_diagnostic(meth, msg, node = @node)
@@ -161,27 +163,27 @@ def add_depended_superclass(mod)
161163

162164
def reinstall(genv)
163165
# Edges stored as nested hashes: {src => {dst => true}}
164-
@new_edges.each do |src, new_dsts|
165-
old_dsts = @edges[src]
166+
new_edges.each do |src, new_dsts|
167+
old_dsts = edges[src]
166168
new_dsts.each_key do |dst|
167169
src.add_edge(genv, dst) unless old_dsts&.key?(dst)
168170
end
169171
end
170-
@edges.each do |src, old_dsts|
171-
new_dsts = @new_edges[src]
172+
edges.each do |src, old_dsts|
173+
new_dsts = new_edges[src]
172174
old_dsts.each_key do |dst|
173175
src.remove_edge(genv, dst) unless new_dsts&.key?(dst)
174176
end
175177
end
176178
@edges, @new_edges = @new_edges, @edges
177-
@new_edges.each_value(&:clear)
178-
@new_edges.clear
179+
new_edges.each_value(&:clear)
180+
new_edges.clear
179181

180-
@boxes.each do |key, box|
182+
boxes.each do |key, box|
181183
box.destroy(genv)
182184
end
183185
@boxes, @new_boxes = @new_boxes, @boxes
184-
@new_boxes.clear
186+
new_boxes.clear
185187

186188
@diagnostics.each do |diag|
187189
genv.add_diagnostic_path(diag.node.lenv.path)

0 commit comments

Comments
 (0)