2021/11/05 今回の気になった bugs.ruby のチケット

今週は Proc#bind_call(obj) を追加する提案などがありました。

[Bug #18282] Rails CI raises Segmentation fault with ruby 3.1.0dev supporting Class#descendants

  • 開発版 Ruby と最新版の Rails でクラッシュするというバグ報告
  • 最終的には以下のコードで再現できるようになったらしい
    • よく見つけるなあ…
class C
end

100000.times { Class.new(C) }

p C.descendants

[Bug #18283] Creating a subclass in Ractor dumps core

  • 上の [Bug #18282] 関連のバグ報告
  • 以下のコードでも Ruby がクラッシュする
class C
end
(1..10).map {
  Ractor.new { 100000.times { Class.new(C) } }
}.each {|r| r.take }

[Bug #18243] Ractor.make_shareable does not freeze the receiver of a Proc but allows accessing ivars of it

  • 次のように Ractor 内で別の Ractor のオブジェクトが書き換えられてしまうバグ報告
class C
  attr_accessor :foo
  def setter_proc
    Ractor.make_shareable(-> v { @foo = v })
  end
end

c = C.new
c.foo = 1
p c
# => #<C:0x0000559bf1df5880 @foo=1>

# インスタンス変数を書き換える proc を生成
# c 自体は freeze されてない
proc = c.setter_proc
p c.frozen?
# => false

# Ractor 内で setter_proc を呼び出すと c のオブジェクトが書き換えら得てしまう
# これがバグ
Ractor.new(proc) { |s| s.call(42) }.take
p c
# => #<C:0x0000559bf1df5880 @foo=42>

[Feature #18276] Proc#bind_call(obj) same as obj.instance_exec(..., &proc_obj)

  • obj.instance_exec(..., &proc_obj) と同じ意味の Proc#bind_call(obj) を追加する提案
    • proc_obj.bind_call(...)obj.instance_exec(..., &proc_obj) と同じ意味になる
  • [Bug #18243] への対応として Ractor の共有可能オブジェクトの Proc#call を禁止して obj.instance_exec(..., &proc_obj) で呼び出すようにすることも考慮しているとのこと
    • そのショートカットとして Proc#bind_call を使用する想定
  • Proc#bind_call を利用すると以下のように block オブジェクトも渡せるようようになるので個人的には普通にほしい
pr = proc { |*args, &block|
  # ...
  block.call(something)
}

# pr に対してブロック引数を渡すことができない
obj.instance_exec(arg, &pr)

# bind_call だと pr に対してブロック引数を渡すことができる
pr.bind_call(obj, arg) do |something|
  # ...
end
require "proc/unbind"

using Proc::Unbind

class X
  attr_accessor :value
end

x = X.new

set = proc { |x|
  @value = x
}
# Proc#rebind で特定のオブジェクトに bind する
set.rebind(x).call(42)

p x.value
# => 42

[Misc #18285] NoMethodError#message uses a lot of CPU/is really expensive to call

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'

  gem 'benchmark-ips'
end

puts RUBY_DESCRIPTION

class GemInformation
  def get_no_method_error
    method_does_not_exist
  rescue => e
    e
  end

  def get_runtime_error
    raise 'Another Error'
  rescue => e
    e
  end

  # NoMethodError#message でこのメソッドが呼び出される
  # Rails の Controller だと複雑な #inspect が実装されておりそれがボトルネックになっているらしい
  def inspect
    Gem::Specification._all.inspect
  end
end

NO_METHOD_ERROR_INSTANCE = GemInformation.new.get_no_method_error
RUNTIME_ERROR_INSTANCE = GemInformation.new.get_runtime_error

Benchmark.ips do |x|
  x.config(:time => 5, :warmup => 2)

  x.report("no method error message cost") { NO_METHOD_ERROR_INSTANCE.message }
  x.report("runtime error message cost") { RUNTIME_ERROR_INSTANCE.message }

  x.compare!
end

__END__

output:
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
Warming up --------------------------------------
no method error message cost
                        20.000  i/100ms
runtime error message cost
                         1.620M i/100ms
Calculating -------------------------------------
no method error message cost
                        249.716  (± 6.0%) i/s -      1.260k in   5.068273s
runtime error message cost
                         16.046M (± 1.1%) i/s -     81.020M in   5.049800s

Comparison:
runtime error message cost: 16045983.5 i/s
no method error message cost:      249.7 i/s - 64256.99x  (± 0.00) slower
  • この例だとめっちゃ遅いですね
  • これは NoMethodError#message#inspect を呼び出しているのが原因らしい
class NoInspect
end

begin
  NoInspect.new.hoge
rescue => e
  pp e.message
  # => "undefined method `hoge' for #<NoInspect:0x00005644abd35978>"
end

class WithInspect
  def inspect
    "My WithInspect"
  end
end

begin
  WithInspect.new.hoge
rescue => e
  # メッセージに WithInspect#inspect も含められる
  # error: undefined method `hoge' for My WithInspect:WithInspect (NoMethodError)
  pp e.message
  # => "undefined method `hoge' for My WithInspect:WithInspect"
end
class WithInspect
  def inspect
    "A" * 66
  end
end

begin
  WithInspect.new.hoge
rescue => e
  pp e.message
  # Ruby 2.7 => "undefined method `hoge' for #<WithInspect:0x000055a784c8a560>"
  # Ruby 3.0 => "undefined method `hoge' for AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:WithInspect"
end

[Feature #18273] Class#subclasses

  • Class#descendants に関連して『自身の子クラスのみ』を返す Class#subclasses を追加する提案
class Class
  def subclasses
    descendants.select { |descendant| descendant.superclass == self }
  end
end


class A; end
class B < A; end
class C < B; end
class D < A; end

# 継承リストに A が含まれているクラスをすべて返す
pp A.descendants  # => [B, C]

# A を直接継承しているクラスのみ返す
pp A.subclasses   # => [D, B]
  • 確かにほしくなりそう