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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/CLAUDE.md +2 -2
  4. data/README.md +3 -3
  5. data/Rakefile +352 -143
  6. data/Steepfile +2 -0
  7. data/bin/shape_warmup_bench.rb +402 -0
  8. data/bin/steep-check.rb +4 -4
  9. data/bin/subtyping_cache_bench.rb +436 -0
  10. data/bin/typecheck_bench.rb +145 -0
  11. data/doc/release.md +264 -0
  12. data/lib/steep/ast/types/helper.rb +0 -1
  13. data/lib/steep/ast/types/literal.rb +1 -1
  14. data/lib/steep/ast/types/name.rb +1 -1
  15. data/lib/steep/cli.rb +32 -5
  16. data/lib/steep/daemon/server.rb +1 -3
  17. data/lib/steep/daemon.rb +49 -9
  18. data/lib/steep/diagnostic/ruby.rb +4 -4
  19. data/lib/steep/drivers/check.rb +0 -1
  20. data/lib/steep/drivers/checkfile.rb +4 -12
  21. data/lib/steep/drivers/init.rb +4 -3
  22. data/lib/steep/drivers/langserver.rb +34 -5
  23. data/lib/steep/drivers/query.rb +40 -2
  24. data/lib/steep/drivers/stats.rb +0 -1
  25. data/lib/steep/drivers/worker.rb +0 -7
  26. data/lib/steep/interface/builder.rb +194 -28
  27. data/lib/steep/interface/shape.rb +28 -14
  28. data/lib/steep/node_helper.rb +2 -2
  29. data/lib/steep/server/base_worker.rb +27 -30
  30. data/lib/steep/server/change_buffer.rb +0 -32
  31. data/lib/steep/server/command_socket.rb +181 -0
  32. data/lib/steep/server/custom_methods.rb +22 -22
  33. data/lib/steep/server/goto_resolver.rb +200 -0
  34. data/lib/steep/server/interaction_worker.rb +32 -12
  35. data/lib/steep/server/master.rb +709 -261
  36. data/lib/steep/server/rbs_entry_builder.rb +237 -0
  37. data/lib/steep/server/type_check_controller.rb +73 -27
  38. data/lib/steep/server/type_check_database.rb +406 -0
  39. data/lib/steep/server/type_check_worker.rb +118 -352
  40. data/lib/steep/server/worker_process.rb +8 -22
  41. data/lib/steep/services/hover_provider/content.rb +1 -1
  42. data/lib/steep/services/hover_provider/ruby.rb +1 -1
  43. data/lib/steep/services/signature_help_provider.rb +1 -1
  44. data/lib/steep/services/stats_calculator.rb +16 -35
  45. data/lib/steep/services/symbol_provider.rb +191 -0
  46. data/lib/steep/services/type_check_service.rb +10 -21
  47. data/lib/steep/source.rb +9 -10
  48. data/lib/steep/subtyping/cache.rb +16 -4
  49. data/lib/steep/subtyping/check.rb +83 -16
  50. data/lib/steep/subtyping/stats.rb +356 -0
  51. data/lib/steep/tagged_logging.rb +4 -1
  52. data/lib/steep/type_construction.rb +39 -17
  53. data/lib/steep/type_inference/case_when.rb +2 -2
  54. data/lib/steep/type_inference/logic_type_interpreter.rb +6 -3
  55. data/lib/steep/type_inference/send_args.rb +9 -5
  56. data/lib/steep/typing.rb +3 -3
  57. data/lib/steep/version.rb +1 -1
  58. data/lib/steep.rb +6 -1
  59. data/steep-relaxed.gemspec +2 -8
  60. metadata +15 -6
  61. 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)