steep-relaxed 2.0.0.0 → 2.1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +2 -2
- data/README.md +3 -3
- data/Rakefile +352 -143
- data/Steepfile +2 -0
- data/bin/shape_warmup_bench.rb +402 -0
- data/bin/steep-check.rb +4 -4
- data/bin/subtyping_cache_bench.rb +436 -0
- data/bin/typecheck_bench.rb +145 -0
- data/doc/release.md +264 -0
- data/lib/steep/ast/types/helper.rb +0 -1
- data/lib/steep/ast/types/literal.rb +1 -1
- data/lib/steep/ast/types/name.rb +1 -1
- data/lib/steep/cli.rb +32 -5
- data/lib/steep/daemon/server.rb +1 -3
- data/lib/steep/daemon.rb +49 -9
- data/lib/steep/diagnostic/ruby.rb +4 -4
- data/lib/steep/drivers/check.rb +0 -1
- data/lib/steep/drivers/checkfile.rb +4 -12
- data/lib/steep/drivers/init.rb +4 -3
- data/lib/steep/drivers/langserver.rb +34 -5
- data/lib/steep/drivers/query.rb +40 -2
- data/lib/steep/drivers/stats.rb +0 -1
- data/lib/steep/drivers/worker.rb +0 -7
- data/lib/steep/interface/builder.rb +194 -28
- data/lib/steep/interface/shape.rb +28 -14
- data/lib/steep/node_helper.rb +2 -2
- data/lib/steep/server/base_worker.rb +27 -30
- data/lib/steep/server/change_buffer.rb +0 -32
- data/lib/steep/server/command_socket.rb +181 -0
- data/lib/steep/server/custom_methods.rb +22 -22
- data/lib/steep/server/goto_resolver.rb +200 -0
- data/lib/steep/server/interaction_worker.rb +32 -12
- data/lib/steep/server/master.rb +709 -261
- data/lib/steep/server/rbs_entry_builder.rb +237 -0
- data/lib/steep/server/type_check_controller.rb +73 -27
- data/lib/steep/server/type_check_database.rb +406 -0
- data/lib/steep/server/type_check_worker.rb +118 -352
- data/lib/steep/server/worker_process.rb +8 -22
- data/lib/steep/services/hover_provider/content.rb +1 -1
- data/lib/steep/services/hover_provider/ruby.rb +1 -1
- data/lib/steep/services/signature_help_provider.rb +1 -1
- data/lib/steep/services/stats_calculator.rb +16 -35
- data/lib/steep/services/symbol_provider.rb +191 -0
- data/lib/steep/services/type_check_service.rb +10 -21
- data/lib/steep/source.rb +9 -10
- data/lib/steep/subtyping/cache.rb +16 -4
- data/lib/steep/subtyping/check.rb +83 -16
- data/lib/steep/subtyping/stats.rb +356 -0
- data/lib/steep/tagged_logging.rb +4 -1
- data/lib/steep/type_construction.rb +39 -17
- data/lib/steep/type_inference/case_when.rb +2 -2
- data/lib/steep/type_inference/logic_type_interpreter.rb +6 -3
- data/lib/steep/type_inference/send_args.rb +9 -5
- data/lib/steep/typing.rb +3 -3
- data/lib/steep/version.rb +1 -1
- data/lib/steep.rb +6 -1
- data/steep-relaxed.gemspec +2 -8
- metadata +15 -6
- data/lib/steep/services/goto_service.rb +0 -644
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# Benchmark harness for Steep's subtyping cache (Steep::Subtyping::Cache).
|
|
3
|
+
#
|
|
4
|
+
# Runs the same type checking pipeline as `steep check` (signature validation +
|
|
5
|
+
# source type checking) in a single process, so that one Subtyping::Check /
|
|
6
|
+
# Cache instance is shared per target, and measures wall-clock time per phase.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# bundle exec ruby bin/subtyping_cache_bench.rb
|
|
10
|
+
#
|
|
11
|
+
# Env vars:
|
|
12
|
+
# CACHE_MODE=on|off|none
|
|
13
|
+
# "off" disables the cache (lookups always miss, stores are
|
|
14
|
+
# dropped, but check_type still pays the cache bookkeeping);
|
|
15
|
+
# "none" removes the caching logic from check_type entirely
|
|
16
|
+
# CACHE_STATS=1 collect Steep::Subtyping::Stats and include it in the JSON
|
|
17
|
+
# (adds overhead; do not trust timing from a stats run)
|
|
18
|
+
# KIND=1 (with CACHE_STATS=1) include per-relation-kind statistics
|
|
19
|
+
# and the most frequent relations
|
|
20
|
+
# TIME_SHARE=1 measure wall-clock time spent inside top-level check_type
|
|
21
|
+
# calls, to see the share of subtyping in type checking
|
|
22
|
+
# MEM=1 analyze cache memory: entry composition, reachable size,
|
|
23
|
+
# and memory freed by clearing the cache
|
|
24
|
+
# CLEAR_PER_FILE=1 clear the context-keyed cache table after each file, to
|
|
25
|
+
# evaluate bounding memory by per-file eviction
|
|
26
|
+
# TARGETS=app,test Steepfile targets to check (default: app)
|
|
27
|
+
# FILES=n limit the number of source files (for quick runs)
|
|
28
|
+
# DIAG_DUMP=path write all diagnostics to a file, for comparing modes
|
|
29
|
+
|
|
30
|
+
Encoding.default_external = Encoding::UTF_8
|
|
31
|
+
|
|
32
|
+
require "steep"
|
|
33
|
+
require "benchmark"
|
|
34
|
+
require "json"
|
|
35
|
+
|
|
36
|
+
MODE = ENV.fetch("CACHE_MODE", "on")
|
|
37
|
+
STATS = ENV["CACHE_STATS"] == "1"
|
|
38
|
+
TARGET_NAMES = (ENV["TARGETS"] || "app").split(",").map(&:to_sym)
|
|
39
|
+
FILE_LIMIT = ENV["FILES"]&.to_i
|
|
40
|
+
|
|
41
|
+
case MODE
|
|
42
|
+
when "off"
|
|
43
|
+
# Cache lookups always miss and stores are dropped, but check_type still pays
|
|
44
|
+
# the bookkeeping cost (cache_bounds, free_variables, lookup call).
|
|
45
|
+
module CacheOff
|
|
46
|
+
def [](relation, self_type, instance_type, class_type, bounds)
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def []=(relation, self_type, instance_type, class_type, bounds, value)
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ground(relation)
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def store_ground(relation, value)
|
|
59
|
+
value
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
Steep::Subtyping::Cache.prepend(CacheOff)
|
|
63
|
+
when "none"
|
|
64
|
+
# Remove the caching logic entirely: no cache_bounds, no free_variables, no
|
|
65
|
+
# lookup, no store. Shows what check_type would cost with no cache code at all.
|
|
66
|
+
module NoCacheCheckType
|
|
67
|
+
def check_type(relation)
|
|
68
|
+
if assumptions.size > Steep::Subtyping::Check::ABORT_LIMIT
|
|
69
|
+
return Failure(relation, Steep::Subtyping::Result::Failure::LoopAbort.new)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
relation.type!
|
|
73
|
+
|
|
74
|
+
Steep.logger.tagged(-> { "#{relation.sub_type} <: #{relation.super_type}" }) do
|
|
75
|
+
if assumptions.member?(relation)
|
|
76
|
+
success(relation)
|
|
77
|
+
else
|
|
78
|
+
push_assumption(relation) do
|
|
79
|
+
check_type0(relation)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
Steep::Subtyping::Check.prepend(NoCacheCheckType)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if ENV["TIME_SHARE"] == "1"
|
|
89
|
+
# Accumulates wall-clock time spent inside top-level check_type calls, to
|
|
90
|
+
# estimate the share of type checking spent on subtyping at all.
|
|
91
|
+
module SubtypingTimeShare
|
|
92
|
+
STATE = { depth: 0, time: 0.0, calls: 0 }
|
|
93
|
+
|
|
94
|
+
def check_type(relation)
|
|
95
|
+
state = STATE
|
|
96
|
+
if state[:depth].zero?
|
|
97
|
+
state[:calls] += 1
|
|
98
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
99
|
+
begin
|
|
100
|
+
state[:depth] += 1
|
|
101
|
+
super
|
|
102
|
+
ensure
|
|
103
|
+
state[:depth] -= 1
|
|
104
|
+
state[:time] += Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
super
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
Steep::Subtyping::Check.prepend(SubtypingTimeShare)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
FILE_IDX = [0]
|
|
115
|
+
|
|
116
|
+
def type_kind(type)
|
|
117
|
+
case type
|
|
118
|
+
when Steep::AST::Types::Name::Instance then "Instance"
|
|
119
|
+
when Steep::AST::Types::Name::Singleton then "Singleton"
|
|
120
|
+
when Steep::AST::Types::Name::Interface then "Interface"
|
|
121
|
+
when Steep::AST::Types::Name::Alias then "Alias"
|
|
122
|
+
when Steep::AST::Types::Union then "Union"
|
|
123
|
+
when Steep::AST::Types::Intersection then "Intersection"
|
|
124
|
+
when Steep::AST::Types::Tuple then "Tuple"
|
|
125
|
+
when Steep::AST::Types::Record then "Record"
|
|
126
|
+
when Steep::AST::Types::Proc then "Proc"
|
|
127
|
+
when Steep::AST::Types::Literal then "Literal"
|
|
128
|
+
when Steep::AST::Types::Boolean then "bool"
|
|
129
|
+
when Steep::AST::Types::Var then "Var"
|
|
130
|
+
when Steep::AST::Types::Any then "untyped"
|
|
131
|
+
when Steep::AST::Types::Top then "top"
|
|
132
|
+
when Steep::AST::Types::Bot then "bot"
|
|
133
|
+
when Steep::AST::Types::Void then "void"
|
|
134
|
+
when Steep::AST::Types::Nil then "nil"
|
|
135
|
+
when Steep::AST::Types::Self then "self"
|
|
136
|
+
when Steep::AST::Types::Instance then "instance"
|
|
137
|
+
when Steep::AST::Types::Class then "class"
|
|
138
|
+
when Steep::AST::Types::Logic::Base then "logic"
|
|
139
|
+
else type.class.name || "?"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def relation_kind(relation)
|
|
144
|
+
"#{type_kind(relation.sub_type)} <: #{type_kind(relation.super_type)}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if STATS
|
|
148
|
+
# Use the collector built into Steep (see Steep::Subtyping::Stats)
|
|
149
|
+
Steep::Subtyping::Stats.active = Steep::Subtyping::Stats.new()
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
steepfile = Pathname.pwd + "Steepfile"
|
|
153
|
+
project = Steep::Project.new(steepfile_path: steepfile)
|
|
154
|
+
Steep::Project::DSL.parse(project, steepfile.read, filename: steepfile.to_s)
|
|
155
|
+
|
|
156
|
+
loader = Steep::Services::FileLoader.new(base_dir: project.base_dir)
|
|
157
|
+
|
|
158
|
+
service = nil
|
|
159
|
+
boot_time = Benchmark.realtime do
|
|
160
|
+
service = Steep::Services::TypeCheckService.new(project: project)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
targets = project.targets.select {|t| TARGET_NAMES.include?(t.name) }
|
|
164
|
+
raise "no matching targets: #{TARGET_NAMES}" if targets.empty?
|
|
165
|
+
|
|
166
|
+
# Load files of all targets: signature files of non-unreferenced targets are
|
|
167
|
+
# shared with the other targets' environments (see TypeCheckService#update_signature).
|
|
168
|
+
changes = {}
|
|
169
|
+
project.targets.each do |target|
|
|
170
|
+
loader.each_path_in_target(target) do |path|
|
|
171
|
+
changes[path] ||= [Steep::Services::ContentChange.string((project.base_dir + path).read)]
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
update_time = Benchmark.realtime { service.update(changes: changes) }
|
|
176
|
+
|
|
177
|
+
result = {
|
|
178
|
+
mode: MODE,
|
|
179
|
+
stats: STATS,
|
|
180
|
+
targets: TARGET_NAMES,
|
|
181
|
+
boot: boot_time.round(2),
|
|
182
|
+
update: update_time.round(2),
|
|
183
|
+
phases: {},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
targets.each do |target|
|
|
187
|
+
sig_paths = loader.each_path_in_patterns(target.signature_pattern).to_a
|
|
188
|
+
src_paths = loader.each_path_in_patterns(target.source_pattern).to_a
|
|
189
|
+
src_paths = src_paths.first(FILE_LIMIT) if FILE_LIMIT
|
|
190
|
+
|
|
191
|
+
# Clears only the context-keyed table: ground entries stay valid across files,
|
|
192
|
+
# while context-keyed entries are bound to the file's classes.
|
|
193
|
+
clear_per_file = ENV["CLEAR_PER_FILE"] == "1"
|
|
194
|
+
clear_cache = -> do
|
|
195
|
+
check = service.signature_services.fetch(target.name).current_subtyping
|
|
196
|
+
check.cache.subtypes.clear if check
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
sig_time = Benchmark.realtime do
|
|
200
|
+
sig_paths.each do |path|
|
|
201
|
+
FILE_IDX[0] += 1
|
|
202
|
+
service.validate_signature(path: path, target: target)
|
|
203
|
+
clear_cache.() if clear_per_file
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
src_time = Benchmark.realtime do
|
|
208
|
+
src_paths.each do |path|
|
|
209
|
+
FILE_IDX[0] += 1
|
|
210
|
+
service.typecheck_source(path: path, target: target)
|
|
211
|
+
clear_cache.() if clear_per_file
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
check = service.signature_services.fetch(target.name).current_subtyping
|
|
216
|
+
result[:phases][target.name] = {
|
|
217
|
+
sig_files: sig_paths.size,
|
|
218
|
+
sig_time: sig_time.round(2),
|
|
219
|
+
src_files: src_paths.size,
|
|
220
|
+
src_time: src_time.round(2),
|
|
221
|
+
cache_entries: check && (check.cache.subtypes.size + check.cache.ground_subtypes.size),
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
diagnostics_count = service.diagnostics.sum {|_, diags| diags.size }
|
|
226
|
+
result[:diagnostics] = diagnostics_count
|
|
227
|
+
|
|
228
|
+
if ENV["MEM"] == "1"
|
|
229
|
+
require "objspace"
|
|
230
|
+
|
|
231
|
+
rss_kb = -> { File.read("/proc/self/status")[/VmRSS:\s+(\d+)/, 1].to_i }
|
|
232
|
+
|
|
233
|
+
checks = targets.filter_map {|t| service.signature_services.fetch(t.name).current_subtyping }
|
|
234
|
+
|
|
235
|
+
# Flatten entries as [context, relation, value] triples; ground entries have no context
|
|
236
|
+
entries = []
|
|
237
|
+
checks.each do |check|
|
|
238
|
+
check.cache.subtypes.each do |key, val|
|
|
239
|
+
rel, self_type, instance_type, class_type, bounds = key
|
|
240
|
+
entries << [[self_type, instance_type, class_type, bounds], rel, val]
|
|
241
|
+
end
|
|
242
|
+
check.cache.ground_subtypes.each do |rel, val|
|
|
243
|
+
entries << [nil, rel, val]
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Composition of cache entries
|
|
248
|
+
contexts = Hash.new(0)
|
|
249
|
+
value_classes = Hash.new(0)
|
|
250
|
+
empty_bounds = 0
|
|
251
|
+
success_values = 0
|
|
252
|
+
reflexive_entries = 0
|
|
253
|
+
ground_entries = 0
|
|
254
|
+
entries.each do |context, rel, value|
|
|
255
|
+
if context
|
|
256
|
+
contexts[context] += 1
|
|
257
|
+
empty_bounds += 1 if context[3].empty?
|
|
258
|
+
else
|
|
259
|
+
ground_entries += 1
|
|
260
|
+
end
|
|
261
|
+
value_classes[value.class.name.sub("Steep::Subtyping::Result::", "")] += 1
|
|
262
|
+
success_values += 1 if value.success?
|
|
263
|
+
reflexive_entries += 1 if rel.sub_type == rel.super_type
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Count result-tree nodes retained by cached values, split by success/failure.
|
|
267
|
+
# Cached sub-results are shared between parents, so count unique objects
|
|
268
|
+
# (memory-relevant) and total visits (tree size as seen by each entry).
|
|
269
|
+
seen_results = {}
|
|
270
|
+
count_tree = ->(result) do
|
|
271
|
+
unique = 0
|
|
272
|
+
visits = 0
|
|
273
|
+
stack = [result]
|
|
274
|
+
while r = stack.pop
|
|
275
|
+
visits += 1
|
|
276
|
+
unless seen_results[r.object_id]
|
|
277
|
+
seen_results[r.object_id] = true
|
|
278
|
+
unique += 1
|
|
279
|
+
end
|
|
280
|
+
case r
|
|
281
|
+
when Steep::Subtyping::Result::Expand
|
|
282
|
+
stack << r.child
|
|
283
|
+
when Steep::Subtyping::Result::All, Steep::Subtyping::Result::Any
|
|
284
|
+
stack.concat(r.branches)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
[unique, visits]
|
|
288
|
+
end
|
|
289
|
+
success_nodes = 0
|
|
290
|
+
failure_nodes = 0
|
|
291
|
+
success_visits = 0
|
|
292
|
+
failure_visits = 0
|
|
293
|
+
entries.each do |_, _, value|
|
|
294
|
+
unique, visits = count_tree[value]
|
|
295
|
+
if value.success?
|
|
296
|
+
success_nodes += unique
|
|
297
|
+
success_visits += visits
|
|
298
|
+
else
|
|
299
|
+
failure_nodes += unique
|
|
300
|
+
failure_visits += visits
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Memory reachable from the cache (over-counts objects shared with the env)
|
|
305
|
+
reachable_bytes = 0
|
|
306
|
+
reachable_count = 0
|
|
307
|
+
seen = {}
|
|
308
|
+
stack = checks.map {|c| c.cache.subtypes } + checks.map {|c| c.cache.ground_subtypes }
|
|
309
|
+
while obj = stack.pop
|
|
310
|
+
next if seen[obj.object_id]
|
|
311
|
+
seen[obj.object_id] = true
|
|
312
|
+
reachable_bytes += ObjectSpace.memsize_of(obj)
|
|
313
|
+
reachable_count += 1
|
|
314
|
+
ObjectSpace.reachable_objects_from(obj)&.each do |o|
|
|
315
|
+
case o
|
|
316
|
+
when Module, Symbol, Integer, Float, ObjectSpace::InternalObjectWrapper, nil, true, false
|
|
317
|
+
# skip
|
|
318
|
+
else
|
|
319
|
+
stack << o unless seen[o.object_id]
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
entries_by_kind = Hash.new {|h, k| h[k] = [0, 0] }
|
|
325
|
+
entries.each do |_, rel, value|
|
|
326
|
+
pair = entries_by_kind[relation_kind(rel)]
|
|
327
|
+
pair[0] += 1
|
|
328
|
+
pair[1] += 1 if value.success?
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
mem_summary = {
|
|
332
|
+
entries_total: contexts.values.sum + ground_entries,
|
|
333
|
+
ground_entries: ground_entries,
|
|
334
|
+
distinct_contexts: contexts.size,
|
|
335
|
+
top_context_entries: contexts.values.max,
|
|
336
|
+
empty_bounds_entries: empty_bounds,
|
|
337
|
+
reflexive_entries: reflexive_entries,
|
|
338
|
+
success_values: success_values,
|
|
339
|
+
value_classes: value_classes,
|
|
340
|
+
success_tree_nodes_unique: success_nodes,
|
|
341
|
+
success_tree_visits: success_visits,
|
|
342
|
+
failure_tree_nodes_unique: failure_nodes,
|
|
343
|
+
failure_tree_visits: failure_visits,
|
|
344
|
+
reachable_from_cache_mb: (reachable_bytes / 1024.0 / 1024).round(1),
|
|
345
|
+
reachable_objects: reachable_count,
|
|
346
|
+
entries_by_kind: entries_by_kind.sort_by {|_, (n, _)| -n }.first(20).to_h,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# Drop analysis structures so they don't retain cache contents through the GC below
|
|
350
|
+
entries = nil
|
|
351
|
+
contexts = nil
|
|
352
|
+
entries_by_kind = nil
|
|
353
|
+
seen = nil
|
|
354
|
+
stack = nil
|
|
355
|
+
|
|
356
|
+
3.times { GC.start }
|
|
357
|
+
rss_before = rss_kb.()
|
|
358
|
+
live_before = GC.stat(:heap_live_slots)
|
|
359
|
+
memsize_before = ObjectSpace.memsize_of_all
|
|
360
|
+
|
|
361
|
+
checks.each do |c|
|
|
362
|
+
c.cache.subtypes.clear
|
|
363
|
+
c.cache.ground_subtypes.clear
|
|
364
|
+
end
|
|
365
|
+
3.times { GC.start }
|
|
366
|
+
rss_after = rss_kb.()
|
|
367
|
+
live_after = GC.stat(:heap_live_slots)
|
|
368
|
+
memsize_after = ObjectSpace.memsize_of_all
|
|
369
|
+
|
|
370
|
+
mem_summary.merge!(
|
|
371
|
+
rss_before_clear_mb: (rss_before / 1024.0).round(1),
|
|
372
|
+
rss_after_clear_mb: (rss_after / 1024.0).round(1),
|
|
373
|
+
rss_freed_by_clear_mb: ((rss_before - rss_after) / 1024.0).round(1),
|
|
374
|
+
memsize_freed_by_clear_mb: ((memsize_before - memsize_after) / 1024.0 / 1024).round(1),
|
|
375
|
+
live_slots_freed_by_clear: live_before - live_after,
|
|
376
|
+
)
|
|
377
|
+
result[:memory] = mem_summary
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
if dump_path = ENV["DIAG_DUMP"]
|
|
381
|
+
File.open(dump_path, "w") do |io|
|
|
382
|
+
service.diagnostics.sort_by {|path, _| path.to_s }.each do |path, diags|
|
|
383
|
+
diags.each do |diag|
|
|
384
|
+
loc = diag.location
|
|
385
|
+
line = loc ? (loc.respond_to?(:line) ? loc.line : loc.start_line) : "-"
|
|
386
|
+
io.puts "#{path}:#{line}:#{diag.class}: #{diag.header_line}"
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
if STATS
|
|
393
|
+
stats = Steep::Subtyping::Stats.active or raise
|
|
394
|
+
result[:counts] = {
|
|
395
|
+
calls: stats.calls,
|
|
396
|
+
reflexive_calls: stats.reflexive_calls,
|
|
397
|
+
hits: stats.hits,
|
|
398
|
+
toplevel_hits: stats.toplevel_hits,
|
|
399
|
+
computes: stats.computes,
|
|
400
|
+
toplevel_computes: stats.toplevel_computes,
|
|
401
|
+
shortcuts: stats.shortcuts,
|
|
402
|
+
context_misses: stats.context_misses,
|
|
403
|
+
assumption_successes: stats.assumption_successes,
|
|
404
|
+
toplevel_compute_time: stats.toplevel_compute_time.round(2),
|
|
405
|
+
}
|
|
406
|
+
cached_lookups = stats.calls - stats.shortcuts
|
|
407
|
+
result[:hit_rate] = (stats.hits.to_f / cached_lookups).round(4) if cached_lookups > 0
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
if ENV["TIME_SHARE"] == "1"
|
|
411
|
+
result[:subtyping_toplevel_calls] = SubtypingTimeShare::STATE[:calls]
|
|
412
|
+
result[:subtyping_time] = SubtypingTimeShare::STATE[:time].round(2)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
if STATS && ENV["KIND"] == "1"
|
|
416
|
+
stats = Steep::Subtyping::Stats.active or raise
|
|
417
|
+
result[:kinds] = stats.kinds.sort_by {|_, k| -k.calls }.first(30).map do |kind, k|
|
|
418
|
+
{
|
|
419
|
+
kind: kind,
|
|
420
|
+
calls: k.calls,
|
|
421
|
+
hits: k.hits,
|
|
422
|
+
hit_rate: (k.hits.to_f / k.calls).round(3),
|
|
423
|
+
computes: k.computes,
|
|
424
|
+
tl_computes: k.toplevel_computes,
|
|
425
|
+
tl_compute_time: k.toplevel_compute_time.round(3),
|
|
426
|
+
}
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
result[:top_relations_by_calls] = stats.relations.sort_by {|_, r| -r.calls }.first(25).map do |rel, r|
|
|
430
|
+
{ relation: rel.to_s, calls: r.calls, hits: r.hits }
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
result[:distinct_relations] = stats.relations.size
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
puts JSON.pretty_generate(result)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# Measures the type checking of the source files of Steepfile targets in one
|
|
3
|
+
# process, after the signatures are loaded, to compare the type checking
|
|
4
|
+
# itself between versions of Steep -- the wall-clock time of `steep check`
|
|
5
|
+
# also includes the boot and the scheduling of the workers.
|
|
6
|
+
#
|
|
7
|
+
# Run it in the project directory to measure (the Steepfile is loaded from the
|
|
8
|
+
# current directory):
|
|
9
|
+
#
|
|
10
|
+
# bundle exec ruby "$(bundle info steep --path)/bin/typecheck_bench.rb"
|
|
11
|
+
#
|
|
12
|
+
# The script needs nothing but the `steep` gem in the bundle, so a copy of it
|
|
13
|
+
# can measure an older Steep that doesn't have it: save a copy, check out the
|
|
14
|
+
# older revision in the path source of the bundle, and run the copy.
|
|
15
|
+
#
|
|
16
|
+
# Env vars:
|
|
17
|
+
# TARGETS=app,test Steepfile targets to check (default: app)
|
|
18
|
+
# FILES=N Check only the first N source files of each target
|
|
19
|
+
# STACKPROF=cpu Profile the type checking with stackprof (cpu, wall, or object)
|
|
20
|
+
# and print the report to stderr; STACKPROF_INTERVAL overrides the
|
|
21
|
+
# sampling interval, STACKPROF_OUT saves the raw dump to a file
|
|
22
|
+
#
|
|
23
|
+
# The report contains no file or type names, so it is safe to share. The
|
|
24
|
+
# stackprof report contains only Steep/RBS method names.
|
|
25
|
+
|
|
26
|
+
Encoding.default_external = Encoding::UTF_8
|
|
27
|
+
|
|
28
|
+
require "steep"
|
|
29
|
+
require "benchmark"
|
|
30
|
+
require "json"
|
|
31
|
+
require "objspace"
|
|
32
|
+
|
|
33
|
+
TARGET_NAMES = (ENV["TARGETS"] || "app").split(",").map(&:to_sym)
|
|
34
|
+
file_limit = ENV["FILES"]&.to_i
|
|
35
|
+
stackprof_mode = ENV["STACKPROF"]&.to_sym
|
|
36
|
+
|
|
37
|
+
if stackprof_mode
|
|
38
|
+
begin
|
|
39
|
+
require "stackprof"
|
|
40
|
+
rescue LoadError
|
|
41
|
+
abort "STACKPROF needs the stackprof gem in the bundle"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
GC.measure_total_time = true
|
|
46
|
+
gc_snap = -> { [GC.stat(:time), GC.stat(:major_gc_count), GC.stat(:minor_gc_count)] }
|
|
47
|
+
gc_delta = ->(snap) { gc_snap.().zip(snap).map {|now, was| now - was } }
|
|
48
|
+
gc_memsize = -> { 2.times { GC.start }; ObjectSpace.memsize_of_all }
|
|
49
|
+
rss_mb = -> { File.read("/proc/self/status")[/VmRSS:\s+(\d+)/, 1].to_i / 1024.0 }
|
|
50
|
+
|
|
51
|
+
steepfile = Pathname.pwd + "Steepfile"
|
|
52
|
+
project = Steep::Project.new(steepfile_path: steepfile)
|
|
53
|
+
Steep::Project::DSL.parse(project, steepfile.read, filename: steepfile.to_s)
|
|
54
|
+
|
|
55
|
+
loader = Steep::Services::FileLoader.new(base_dir: project.base_dir)
|
|
56
|
+
service = Steep::Services::TypeCheckService.new(project: project)
|
|
57
|
+
|
|
58
|
+
changes = {}
|
|
59
|
+
project.targets.each do |target|
|
|
60
|
+
loader.each_path_in_target(target) do |path|
|
|
61
|
+
changes[path] ||= [Steep::Services::ContentChange.string((project.base_dir + path).read)]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
update_time = Benchmark.realtime { service.update(changes: changes) }
|
|
65
|
+
|
|
66
|
+
result = {
|
|
67
|
+
update: update_time.round(2),
|
|
68
|
+
targets: {},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
targets = project.targets.select {|t| TARGET_NAMES.include?(t.name) }
|
|
72
|
+
raise "no matching targets: #{TARGET_NAMES}" if targets.empty?
|
|
73
|
+
|
|
74
|
+
profile = ->(callable) do
|
|
75
|
+
if stackprof_mode
|
|
76
|
+
interval = (ENV["STACKPROF_INTERVAL"] || (stackprof_mode == :object ? 100 : 1000)).to_i
|
|
77
|
+
StackProf.start(mode: stackprof_mode, raw: false, interval: interval)
|
|
78
|
+
begin
|
|
79
|
+
callable.call
|
|
80
|
+
ensure
|
|
81
|
+
StackProf.stop
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
callable.call
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
targets.each do |target|
|
|
89
|
+
unless service.signature_services.fetch(target.name).current_subtyping
|
|
90
|
+
result[:targets][target.name] = { error: "signature has errors; no subtyping available" }
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
source_paths = [] #: Array[Pathname]
|
|
95
|
+
loader.each_path_in_target(target) do |path|
|
|
96
|
+
source_paths << path if target.possible_source_file?(path)
|
|
97
|
+
end
|
|
98
|
+
source_paths = source_paths.first(file_limit) if file_limit
|
|
99
|
+
|
|
100
|
+
before = gc_memsize.()
|
|
101
|
+
gc0 = gc_snap.()
|
|
102
|
+
allocated0 = GC.stat(:total_allocated_objects)
|
|
103
|
+
errors = 0
|
|
104
|
+
|
|
105
|
+
check_time = Benchmark.realtime do
|
|
106
|
+
profile.(-> do
|
|
107
|
+
source_paths.each do |path|
|
|
108
|
+
begin
|
|
109
|
+
service.typecheck_source(path: path, target: target)
|
|
110
|
+
rescue => _
|
|
111
|
+
errors += 1
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
allocated = GC.stat(:total_allocated_objects) - allocated0
|
|
118
|
+
gc_ms, major_gc, minor_gc = gc_delta.(gc0)
|
|
119
|
+
after = gc_memsize.()
|
|
120
|
+
|
|
121
|
+
diagnostics = source_paths.sum {|path| service.diagnostics.fetch(path, []).size }
|
|
122
|
+
|
|
123
|
+
result[:targets][target.name] = {
|
|
124
|
+
files: source_paths.size,
|
|
125
|
+
check_time: check_time.round(2),
|
|
126
|
+
diagnostics: diagnostics,
|
|
127
|
+
errors: errors,
|
|
128
|
+
allocated_objects: allocated,
|
|
129
|
+
gc_ms: gc_ms,
|
|
130
|
+
major_gc: major_gc,
|
|
131
|
+
minor_gc: minor_gc,
|
|
132
|
+
retained_mb: ((after - before) / 1024.0 / 1024).round(1),
|
|
133
|
+
rss_mb: rss_mb.().round(0),
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if stackprof_mode
|
|
138
|
+
profile = StackProf.results
|
|
139
|
+
if profile
|
|
140
|
+
File.binwrite(ENV["STACKPROF_OUT"], Marshal.dump(profile)) if ENV["STACKPROF_OUT"]
|
|
141
|
+
StackProf::Report.new(profile).print_text(false, 30, nil, nil, nil, nil, $stderr)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
puts JSON.pretty_generate(result)
|