diff --git a/Gemfile b/Gemfile index ff77e12ef..cd6478cdb 100644 --- a/Gemfile +++ b/Gemfile @@ -6,9 +6,14 @@ gemspec gem "rake" gem "minitest", "~> 5.21" gem "minitest-hooks" -group :stackprof, optional: true do +gem 'minitest-slow_test' + +group :development, optional: true do gem "stackprof" + gem "debug", require: false, platform: :mri + gem "vernier", "~> 1.0", require: false, platform: :mri + gem "memory_profiler" + gem "majo" end -gem 'minitest-slow_test' -gem "debug", require: false, platform: :mri +gem "rbs" diff --git a/Gemfile.lock b/Gemfile.lock index d75a5e46e..db9bcf255 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,8 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.6.0) + majo (1.0.0) + memory_profiler (1.0.2) minitest (5.24.1) minitest-hooks (1.5.1) minitest (> 5.3) @@ -86,18 +88,23 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) + vernier (1.1.1) PLATFORMS ruby DEPENDENCIES debug + majo + memory_profiler minitest (~> 5.21) minitest-hooks minitest-slow_test rake + rbs stackprof steep! + vernier (~> 1.0) BUNDLED WITH 2.3.8 diff --git a/bin/mem_graph.rb b/bin/mem_graph.rb new file mode 100644 index 000000000..087c0b8e7 --- /dev/null +++ b/bin/mem_graph.rb @@ -0,0 +1,67 @@ +class MemGraph + attr_reader :edges + + attr_reader :checked + + attr_reader :generation + + def initialize(generation) + @generation = generation + @edges = [] + @checked = Set.new.compare_by_identity + @checked << self + @checked << edges + @checked << checked + end + + IVARS = Object.instance_method(:instance_variables) + IVGET = Object.instance_method(:instance_variable_get) + + def traverse(object) + return if checked.include?(object) + checked << object + + case object + when Array + object.each do |value| + insert_edge(object, value) + traverse(value) + end + when Hash + object.each do |key, value| + insert_edge(object, key) + insert_edge(object, value) + + traverse(key) + traverse(value) + end + else + IVARS.bind_call(object).each do |name| + if name.is_a?(Symbol) + value = IVGET.bind_call(object, name) + traverse(value) + insert_edge(object, value) + else + STDERR.puts "Unexpected instance variable name: #{name} in #{object.class}" + end + end + end + end + + def insert_edge(source, dest) + case dest + when Integer, Symbol, nil, true, false, Float + else + edges << [ + "#{source.class}(#{source.__id__})", + "#{dest.class}(#{dest.__id__})", + ] + end + end + + def dot + "digraph G {\n" + edges.uniq.map do |source, dest| + " #{source} -> #{dest};" + end.join("\n") + "}" + end +end diff --git a/bin/mem_prof.rb b/bin/mem_prof.rb new file mode 100644 index 000000000..7d918f6b9 --- /dev/null +++ b/bin/mem_prof.rb @@ -0,0 +1,102 @@ +require "objspace" + +class MemProf + attr_reader :generation + + def initialize + end + + def self.trace(io: STDOUT, &block) + profiler = MemProf.new + profiler.start + + begin + ret = yield + rescue + ObjectSpace.trace_object_allocations_stop + ObjectSpace.trace_object_allocations_clear + raise + end + + allocated, retained, collected = profiler.stop + + counts = {} + collected.each do |id, entry| + counts[entry] ||= 0 + counts[entry] += 1 + end + + counts.keys.sort_by {|entry| -counts[entry] }.take(200).each do |entry| + count = counts.fetch(entry) + io.puts "#{entry[0]},#{entry[1]},#{entry[2]},#{count}" + end + + STDERR.puts "Total allocated: #{allocated.size}" + STDERR.puts "Total retained: #{retained.size}" + STDERR.puts "Total collected: #{collected.size}" + + ret + end + + def start + GC.disable + 3.times { GC.start } + GC.start + + @generation = GC.count + ObjectSpace.trace_object_allocations_start + end + + def stop + ObjectSpace.trace_object_allocations_stop + + allocated = objects() + retained = {} + + GC.enable + GC.start + GC.start + GC.start + + ObjectSpace.each_object do |obj| + next unless ObjectSpace.allocation_generation(obj) == generation + if o = allocated[obj.__id__] + retained[obj.__id__] = o + end + end + + # ObjectSpace.trace_object_allocations_clear + + collected = {} + allocated.each do |id, state| + collected[id] = state unless retained.key?(id) + end + + [allocated, retained, collected] + end + + def objects(hash = {}) + ObjectSpace.each_object do |obj| + next unless ObjectSpace.allocation_generation(obj) == generation + + file = ObjectSpace.allocation_sourcefile(obj) || "(no name)" + line = ObjectSpace.allocation_sourceline(obj) + klass = object_class(obj) + + hash[obj.__id__] = [file, line, klass] + end + + hash + end + + KERNEL_CLASS_METHOD = Kernel.instance_method(:class) + def object_class(obj) + klass = obj.class rescue nil + + unless Class === klass + # attempt to determine the true Class when .class returns something other than a Class + klass = KERNEL_CLASS_METHOD.bind_call(obj) + end + klass + end +end diff --git a/bin/steep-check.rb b/bin/steep-check.rb new file mode 100755 index 000000000..157e0a41d --- /dev/null +++ b/bin/steep-check.rb @@ -0,0 +1,237 @@ +#!/usr/bin/env ruby + +require 'pathname' + +$LOAD_PATH << Pathname(__dir__) + "../lib" + +require 'steep' +require "fileutils" +require "optparse" + +puts <> Loading RBS files..." +env = command.load_signatures() + +puts ">> Type checking files with #{profile_mode}..." + +typings = nil + +GC.start(immediate_sweep: true, immediate_mark: true, full_mark: true) +# GC.config[:rgengc_allow_major_gc] = false + +case profile_mode +when :vernier + require "vernier" + out = Pathname.pwd + "tmp/typecheck-#{Process.pid}.vernier.json" + puts ">> Profiling with vernier: #{out}" + Vernier.profile(out: out.to_s) do + typings = command.type_check_files(command_line_args, env) + end + +when :memory + require 'memory_profiler' + out = Pathname.pwd + "tmp/typecheck-#{Process.pid}.memory.txt" + puts ">> Profiling with memory_profiler: #{out}" + classes = nil + report = MemoryProfiler.report(trace: classes) do + typings = command.type_check_files(command_line_args, env) + end + report.pretty_print(to_file: out, detailed_report: true, scale_bytes: true, retained_strings: false, allocated_strings: false) + +when :memory2 + require_relative 'mem_prof' + out = Pathname.pwd + "tmp/typecheck-#{Process.pid}.memory2.csv" + puts ">> Profiling with mem_prof: #{out}" + generation = nil + out.open("w") do |io| + MemProf.trace(io: io) do + generation = GC.count + typings = command.type_check_files(command_line_args, env) + end + end + + require_relative 'mem_graph' + graph = MemGraph.new(generation) + ObjectSpace.each_object do |obj| + if ObjectSpace.allocation_generation(obj) == generation + graph.traverse(obj) + end + end + (Pathname.pwd + "objects-#{Process.pid}.dot").write(graph.dot) + +when :stackprof + require "stackprof" + out = Pathname.pwd + "tmp/typecheck-#{Process.pid}.stackprof" + puts ">> Profiling with stackprof: #{out}" + StackProf.run(mode: :cpu, out: out, raw: true, interval: 1000) do + typings = command.type_check_files(command_line_args, env) + end + +when :majo + require "majo" + out = Pathname.pwd + "tmp/typecheck-#{Process.pid}.majo.csv" + puts ">> Profiling with majo: #{out}" + + result = Majo.run do + typings = command.type_check_files(command_line_args, env) + end + + out.open("w") do |io| + result.report(out: io, formatter: :csv) + end + +when :dumpall + require "objspace" + out = Pathname.pwd + "tmp/dumpall-#{Process.pid}.json" + puts ">> Profiling with dumpall: #{out}" + ObjectSpace.trace_object_allocations_start + typings = command.type_check_files(command_line_args, env) + out.open('w+') do |io| + ObjectSpace.dump_all(output: io) + end + +when :none + Steep.measure("type check", level: :fatal) do + typings = command.type_check_files(command_line_args, env) + end +end + +typings.size diff --git a/lib/steep.rb b/lib/steep.rb index 90e7f3691..df7c386d9 100644 --- a/lib/steep.rb +++ b/lib/steep.rb @@ -339,3 +339,15 @@ def count_objects(title, regexp = /^Steep/, skip: false) end end end + + + + +# klasses = [Set] +# klasses.each do |klass| +# # steep:ignore:start +# def klass.new(...) +# super +# end +# # steep:ignore:end +# end diff --git a/lib/steep/interface/substitution.rb b/lib/steep/interface/substitution.rb index 5f7ccc3db..553043744 100644 --- a/lib/steep/interface/substitution.rb +++ b/lib/steep/interface/substitution.rb @@ -82,6 +82,8 @@ def apply?(type) !instance_type.is_a?(AST::Types::Instance) when AST::Types::Class !module_type.is_a?(AST::Types::Class) + when AST::Types::Name::Applying + type.args.any? {|ty| apply?(ty) } else type.each_child.any? {|t| apply?(t) } end diff --git a/lib/steep/services/signature_service.rb b/lib/steep/services/signature_service.rb index c17920e78..b4c7332bc 100644 --- a/lib/steep/services/signature_service.rb +++ b/lib/steep/services/signature_service.rb @@ -228,7 +228,6 @@ def update(changes) end def update_env(updated_files, paths:) - Steep.logger.tagged "#update_env" do errors = [] #: Array[RBS::BaseError] new_decls = Set[].compare_by_identity #: Set[RBS::AST::Declarations::t] diff --git a/sig/steep/drivers/stats.rbs b/sig/steep/drivers/stats.rbs index a755558f3..0be3a5f27 100644 --- a/sig/steep/drivers/stats.rbs +++ b/sig/steep/drivers/stats.rbs @@ -5,7 +5,6 @@ module Steep type: String, target: String, path: String, - type: String, typed_calls: Integer, untyped_calls: Integer, total_calls: Integer