Skip to content

perf: fix performance when applying large numbers of type unions - #2329

Open
HoneyryderChuck wants to merge 1 commit into
soutaro:masterfrom
HoneyryderChuck:fix-record-unions-perf
Open

HoneyryderChuck wants to merge 1 commit into
soutaro:masterfrom
HoneyryderChuck:fix-record-unions-perf

Conversation

@HoneyryderChuck

Copy link
Copy Markdown

the handling of union types, particularly bad when used with records, creates transitive interfaces with the union of all methods from each option; a lot of these methods are common across records, but they wouldn't be deduped, thereby being union'ed several times while joined, to the point where a 12 member union could take +10m to load.

This fixes the behaviour by:

1: removing method overload dead pairs for [] calls for combinations
which cannot be called.
2: removing duplicates from the same overload, which weren't identified
due to pair ordering.

To reproduce this in the master branch, you can use the following script:

# Usage: N=8 ruby repro_union.rb [path/to/steep/checkout]
# Generates a union of N record types sharing a base record, then type-checks `frame[:stream]`.
require "tmpdir"
require "fileutils"

n = Integer(ENV.fetch("N", "6"))
steep = File.expand_path(ARGV[0] || "~/dev/steep")

Dir.mktmpdir("steep-union") do |dir|
  FileUtils.mkdir_p(%W[#{dir}/sig #{dir}/lib])
  members = (1..n).map { |i| "common & { type: :t#{i}, p#{i}: Integer, ?o#{i}: String }" }
  File.write("#{dir}/sig/a.rbs", <<~RBS)
    type common = { stream: Integer, flags: Integer }
    type frame = #{members.map { |m| "(#{m})" }.join(" | ")}
    class Foo
      def go: (frame) -> Integer
    end
  RBS
  File.write("#{dir}/lib/a.rb", "class Foo\n  def go(frame)\n    frame[:stream]\n  end\nend\n")
  File.write("#{dir}/Steepfile", %(target(:t) { signature "sig"; check "lib" }\n))

  t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  system({ "BUNDLE_GEMFILE" => "#{steep}/Gemfile" }, "bundle", "exec", "#{steep}/exe/steep", "check",
         "--jobs=1", "--steepfile=#{dir}/Steepfile", chdir: dir, out: File::NULL)
  puts "N=#{n}: #{(Process.clock_gettime(Process::CLOCK_MONOTONIC) - t).round(1)}s"
end

the handling of union types, particularly bad when used with records,
creates transitive interfaces with the union of all methods from each
option; a lot of these methods are common across records, but they
wouldn't be deduped, thereby being union'ed several times while joined,
to the point where a 12 member union could take +10m to load.

This fixes the behaviour by:

1: removing method overload dead pairs for `[]` calls for combinations
   which cannot be called.
2: removing duplicates from the same overload, which weren't identified
   due to pair ordering.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant