summaryrefslogtreecommitdiff
path: root/test/ruby/test_yjit.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/ruby/test_yjit.rb')
-rw-r--r--test/ruby/test_yjit.rb1264
1 files changed, 1192 insertions, 72 deletions
diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb
index 0106a09166..796787e355 100644
--- a/test/ruby/test_yjit.rb
+++ b/test/ruby/test_yjit.rb
@@ -1,17 +1,27 @@
# frozen_string_literal: true
+#
+# This set of tests can be run with:
+# make test-all TESTS='test/ruby/test_yjit.rb'
+
require 'test/unit'
require 'envutil'
require 'tmpdir'
+require_relative '../lib/jit_support'
+
+return unless JITSupport.yjit_supported?
-return unless defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
+require 'stringio'
# Tests for YJIT with assertions on compilation and side exits
-# insipired by the MJIT tests in test/ruby/test_jit.rb
+# insipired by the RJIT tests in test/ruby/test_rjit.rb
class TestYJIT < Test::Unit::TestCase
+ running_with_yjit = defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
+
def test_yjit_in_ruby_description
assert_includes(RUBY_DESCRIPTION, '+YJIT')
- end
+ end if running_with_yjit
+ # Check that YJIT is in the version string
def test_yjit_in_version
[
%w(--version --yjit),
@@ -21,24 +31,107 @@ class TestYJIT < Test::Unit::TestCase
%w(--version --disable=yjit --yjit),
%w(--version --disable=yjit --enable-yjit),
%w(--version --disable=yjit --enable=yjit),
+ %w(--version --jit),
+ %w(--version --disable-jit --jit),
+ %w(--version --disable-jit --enable-jit),
+ %w(--version --disable-jit --enable=jit),
+ %w(--version --disable=jit --yjit),
+ %w(--version --disable=jit --enable-jit),
+ %w(--version --disable=jit --enable=jit),
].each do |version_args|
assert_in_out_err(version_args) do |stdout, stderr|
assert_equal(RUBY_DESCRIPTION, stdout.first)
assert_equal([], stderr)
end
end
- end
+ end if running_with_yjit
def test_command_line_switches
assert_in_out_err('--yjit-', '', [], /invalid option --yjit-/)
assert_in_out_err('--yjithello', '', [], /invalid option --yjithello/)
- assert_in_out_err('--yjit-call-threshold', '', [], /--yjit-call-threshold needs an argument/)
- assert_in_out_err('--yjit-call-threshold=', '', [], /--yjit-call-threshold needs an argument/)
- assert_in_out_err('--yjit-greedy-versioning=1', '', [], /warning: argument to --yjit-greedy-versioning is ignored/)
+ #assert_in_out_err('--yjit-call-threshold', '', [], /--yjit-call-threshold needs an argument/)
+ #assert_in_out_err('--yjit-call-threshold=', '', [], /--yjit-call-threshold needs an argument/)
+ end
+
+ def test_yjit_enable
+ args = []
+ args << "--disable=yjit" if RubyVM::YJIT.enabled?
+ assert_separately(args, <<~RUBY)
+ assert_false RubyVM::YJIT.enabled?
+ assert_false RUBY_DESCRIPTION.include?("+YJIT")
+
+ RubyVM::YJIT.enable
+
+ assert_true RubyVM::YJIT.enabled?
+ assert_true RUBY_DESCRIPTION.include?("+YJIT")
+ RUBY
+ end
+
+ def test_yjit_enable_stats_false
+ assert_separately(["--yjit-disable", "--yjit-stats"], <<~RUBY, ignore_stderr: true)
+ assert_false RubyVM::YJIT.enabled?
+ assert_nil RubyVM::YJIT.runtime_stats
+
+ RubyVM::YJIT.enable
+
+ assert_true RubyVM::YJIT.enabled?
+ assert_true RubyVM::YJIT.runtime_stats[:all_stats]
+ RUBY
+ end
+
+ def test_yjit_enable_stats_true
+ args = []
+ args << "--disable=yjit" if RubyVM::YJIT.enabled?
+ assert_separately(args, <<~RUBY, ignore_stderr: true)
+ assert_false RubyVM::YJIT.enabled?
+ assert_nil RubyVM::YJIT.runtime_stats
+
+ RubyVM::YJIT.enable(stats: true)
+
+ assert_true RubyVM::YJIT.enabled?
+ assert_true RubyVM::YJIT.runtime_stats[:all_stats]
+ RUBY
+ end
+
+ def test_yjit_enable_stats_quiet
+ assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(stats: true)']) do |_stdout, stderr, _status|
+ assert_not_empty stderr
+ end
+ assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(stats: :quiet)']) do |_stdout, stderr, _status|
+ assert_empty stderr
+ end
+ end
+
+ def test_yjit_enable_with_call_threshold
+ assert_separately(%w[--yjit-disable --yjit-call-threshold=1], <<~RUBY)
+ def not_compiled = nil
+ def will_compile = nil
+ def compiled_counts = RubyVM::YJIT.runtime_stats&.dig(:compiled_iseq_count)
+
+ not_compiled
+ assert_nil compiled_counts
+ assert_false RubyVM::YJIT.enabled?
+
+ RubyVM::YJIT.enable
+
+ will_compile
+ assert compiled_counts > 0
+ assert_true RubyVM::YJIT.enabled?
+ RUBY
+ end
+
+ def test_yjit_enable_with_monkey_patch
+ assert_separately(%w[--yjit-disable], <<~RUBY)
+ # This lets rb_method_entry_at(rb_mKernel, ...) return NULL
+ Kernel.prepend(Module.new)
+
+ # This must not crash with "undefined optimized method!"
+ RubyVM::YJIT.enable
+ RUBY
end
def test_yjit_stats_and_v_no_error
- _stdout, stderr, _status = EnvUtil.invoke_ruby(%w(-v --yjit-stats), '', true, true)
+ _stdout, stderr, _status = invoke_ruby(%w(-v --yjit-stats), '', true, true)
refute_includes(stderr, "NoMethodError")
end
@@ -50,7 +143,7 @@ class TestYJIT < Test::Unit::TestCase
end
assert_in_out_err([yjit_child_env, '-e puts RUBY_DESCRIPTION'], '', [RUBY_DESCRIPTION])
assert_in_out_err([yjit_child_env, '-e p RubyVM::YJIT.enabled?'], '', ['true'])
- end
+ end if running_with_yjit
def test_compile_setclassvariable
script = 'class Foo; def self.foo; @@foo = 1; end; end; Foo.foo'
@@ -72,6 +165,10 @@ class TestYJIT < Test::Unit::TestCase
assert_compiles(':foo', insns: %i[putobject], result: :foo)
end
+ def test_compile_opt_succ
+ assert_compiles('1.succ', insns: %i[opt_succ], result: 2)
+ end
+
def test_compile_opt_not
assert_compiles('!false', insns: %i[opt_not], result: true)
assert_compiles('!nil', insns: %i[opt_not], result: true)
@@ -223,10 +320,10 @@ class TestYJIT < Test::Unit::TestCase
end
def test_compile_opt_aset
- assert_compiles('[1,2,3][2] = 4', insns: %i[opt_aset])
- assert_compiles('{}[:foo] = :bar', insns: %i[opt_aset])
- assert_compiles('[1,2,3][0..-1] = []', insns: %i[opt_aset])
- assert_compiles('"foo"[3] = "d"', insns: %i[opt_aset])
+ assert_compiles('[1,2,3][2] = 4', insns: %i[opt_aset], frozen_string_literal: false)
+ assert_compiles('{}[:foo] = :bar', insns: %i[opt_aset], frozen_string_literal: false)
+ assert_compiles('[1,2,3][0..-1] = []', insns: %i[opt_aset], frozen_string_literal: false)
+ assert_compiles('"foo"[3] = "d"', insns: %i[opt_aset], frozen_string_literal: false)
end
def test_compile_attr_set
@@ -247,6 +344,11 @@ class TestYJIT < Test::Unit::TestCase
assert_no_exits('/#{true}/')
end
+ def test_compile_dynamic_symbol
+ assert_compiles(':"#{"foo"}"', insns: %i[intern])
+ assert_compiles('s = "bar"; :"foo#{s}"', insns: %i[intern])
+ end
+
def test_getlocal_with_level
assert_compiles(<<~RUBY, insns: %i[getlocal opt_plus], result: [[7]])
def foo(foo, bar)
@@ -297,6 +399,32 @@ class TestYJIT < Test::Unit::TestCase
RUBY
end
+ def test_string_concat_utf8
+ assert_compiles(<<~RUBY, frozen_string_literal: true, result: true)
+ def str_cat_utf8
+ s = String.new
+ 10.times { s << "✅" }
+ s
+ end
+
+ str_cat_utf8 == "✅" * 10
+ RUBY
+ end
+
+ def test_string_concat_ascii
+ # Constant-get for classes (e.g. String, Encoding) can cause a side-exit in getinlinecache. For now, ignore exits.
+ assert_compiles(<<~RUBY, exits: :any)
+ str_arg = "b".encode(Encoding::ASCII)
+ def str_cat_ascii(arg)
+ s = String.new(encoding: Encoding::ASCII)
+ 10.times { s << arg }
+ s
+ end
+
+ str_cat_ascii(str_arg) == str_arg * 10
+ RUBY
+ end
+
def test_opt_length_in_method
assert_compiles(<<~RUBY, insns: %i[opt_length], result: 5)
def foo(str)
@@ -340,8 +468,31 @@ class TestYJIT < Test::Unit::TestCase
assert_compiles("'foo' =~ /(o)./; $2", insns: %i[getspecial], result: nil)
end
- def test_compile_opt_getinlinecache
- assert_compiles(<<~RUBY, insns: %i[opt_getinlinecache], result: 123, min_calls: 2)
+ def test_compile_getconstant
+ assert_compiles(<<~RUBY, insns: %i[getconstant], result: [], call_threshold: 1)
+ def get_argv(klass)
+ klass::ARGV
+ end
+
+ get_argv(Object)
+ RUBY
+ end
+
+ def test_compile_getconstant_with_sp_offset
+ assert_compiles(<<~RUBY, insns: %i[getconstant], result: 2, call_threshold: 1)
+ class Foo
+ Bar = 1
+ end
+
+ 2.times do
+ s = Foo # this opt_getconstant_path needs warmup, so 2.times is needed
+ Class.new(Foo).const_set(:Bar, s::Bar)
+ end
+ RUBY
+ end
+
+ def test_compile_opt_getconstant_path
+ assert_compiles(<<~RUBY, insns: %i[opt_getconstant_path], result: 123, call_threshold: 2)
def get_foo
FOO
end
@@ -353,8 +504,8 @@ class TestYJIT < Test::Unit::TestCase
RUBY
end
- def test_opt_getinlinecache_slowpath
- assert_compiles(<<~RUBY, exits: { opt_getinlinecache: 1 }, result: [42, 42, 1, 1], min_calls: 2)
+ def test_opt_getconstant_path_slowpath
+ assert_compiles(<<~RUBY, exits: { opt_getconstant_path: 1 }, result: [42, 42, 1, 1], call_threshold: 2)
class A
FOO = 42
class << self
@@ -381,8 +532,34 @@ class TestYJIT < Test::Unit::TestCase
RUBY
end
+ def test_opt_getconstant_path_general
+ assert_compiles(<<~RUBY, result: [1, 1])
+ module Base
+ Const = 1
+ end
+
+ class Sub
+ def const
+ _const = nil # make a non-entry block for opt_getconstant_path
+ Const
+ end
+
+ def self.const_missing(n)
+ Base.const_get(n)
+ end
+ end
+
+
+ sub = Sub.new
+ result = []
+ result << sub.const # generate the general case
+ result << sub.const # const_missing does not invalidate the block
+ result
+ RUBY
+ end
+
def test_string_interpolation
- assert_compiles(<<~'RUBY', insns: %i[objtostring anytostring concatstrings], result: "foobar", min_calls: 2)
+ assert_compiles(<<~'RUBY', insns: %i[objtostring anytostring concatstrings], result: "foobar", call_threshold: 2)
def make_str(foo, bar)
"#{foo}#{bar}"
end
@@ -438,6 +615,249 @@ class TestYJIT < Test::Unit::TestCase
RUBY
end
+ def test_getblockparam
+ assert_compiles(<<~'RUBY', insns: [:getblockparam])
+ def foo &blk
+ 2.times do
+ blk
+ end
+ end
+
+ foo {}
+ foo {}
+ RUBY
+ end
+
+ def test_getblockparamproxy
+ assert_compiles(<<~'RUBY', insns: [:getblockparamproxy], exits: {})
+ def foo &blk
+ p blk.call
+ p blk.call
+ end
+
+ foo { 1 }
+ foo { 2 }
+ RUBY
+ end
+
+ def test_ifunc_getblockparamproxy
+ assert_compiles(<<~'RUBY', insns: [:getblockparamproxy], exits: {})
+ class Foo
+ include Enumerable
+
+ def each(&block)
+ block.call 1
+ block.call 2
+ block.call 3
+ end
+ end
+
+ foo = Foo.new
+ foo.map { _1 * 2 }
+ foo.map { _1 * 2 }
+ RUBY
+ end
+
+ def test_send_blockarg
+ assert_compiles(<<~'RUBY', insns: [:getblockparamproxy, :send], exits: {})
+ def bar
+ end
+
+ def foo &blk
+ bar(&blk)
+ bar(&blk)
+ end
+
+ foo
+ foo
+
+ foo { }
+ foo { }
+ RUBY
+ end
+
+ def test_send_splat
+ assert_compiles(<<~'RUBY', result: "3#1,2,3/P", exits: {})
+ def internal_method(*args)
+ "#{args.size}##{args.join(",")}"
+ end
+
+ def jit_method
+ send(:internal_method, *[1, 2, 3]) + "/P"
+ end
+
+ jit_method
+ RUBY
+ end
+
+ def test_send_multiarg
+ assert_compiles(<<~'RUBY', result: "3#1,2,3/Q")
+ def internal_method(*args)
+ "#{args.size}##{args.join(",")}"
+ end
+
+ def jit_method
+ send(:internal_method, 1, 2, 3) + "/Q"
+ end
+
+ jit_method
+ RUBY
+ end
+
+ def test_send_kwargs
+ # For now, this side-exits when calls include keyword args
+ assert_compiles(<<~'RUBY', result: "2#a:1,b:2/A")
+ def internal_method(**kw)
+ "#{kw.size}##{kw.keys.map { |k| "#{k}:#{kw[k]}" }.join(",")}"
+ end
+
+ def jit_method
+ send(:internal_method, a: 1, b: 2) + "/A"
+ end
+ jit_method
+ RUBY
+ end
+
+ def test_send_kwargs_in_receiver_only
+ assert_compiles(<<~'RUBY', result: "0/RK", exits: {})
+ def internal_method(**kw)
+ "#{kw.size}"
+ end
+
+ def jit_method
+ send(:internal_method) + "/RK"
+ end
+ jit_method
+ RUBY
+ end
+
+ def test_send_with_underscores
+ assert_compiles(<<~'RUBY', result: "0/RK", exits: {})
+ def internal_method(**kw)
+ "#{kw.size}"
+ end
+
+ def jit_method
+ __send__(:internal_method) + "/RK"
+ end
+ jit_method
+ RUBY
+ end
+
+ def test_send_kwargs_splat
+ # For now, this side-exits when calling with a splat
+ assert_compiles(<<~'RUBY', result: "2#a:1,b:2/B")
+ def internal_method(**kw)
+ "#{kw.size}##{kw.keys.map { |k| "#{k}:#{kw[k]}" }.join(",")}"
+ end
+
+ def jit_method
+ send(:internal_method, **{ a: 1, b: 2 }) + "/B"
+ end
+ jit_method
+ RUBY
+ end
+
+ def test_send_block
+ # Setlocal_wc_0 sometimes side-exits on write barrier
+ assert_compiles(<<~'RUBY', result: "b:n/b:y/b:y/b:n")
+ def internal_method(&b)
+ "b:#{block_given? ? "y" : "n"}"
+ end
+
+ def jit_method
+ b7 = proc { 7 }
+ [
+ send(:internal_method),
+ send(:internal_method, &b7),
+ send(:internal_method) { 7 },
+ send(:internal_method, &nil),
+ ].join("/")
+ end
+ jit_method
+ RUBY
+ end
+
+ def test_send_block_calling
+ assert_compiles(<<~'RUBY', result: "1a2", exits: {})
+ def internal_method
+ out = yield
+ "1" + out + "2"
+ end
+
+ def jit_method
+ __send__(:internal_method) { "a" }
+ end
+ jit_method
+ RUBY
+ end
+
+ def test_send_block_only_receiver
+ assert_compiles(<<~'RUBY', result: "b:n", exits: {})
+ def internal_method(&b)
+ "b:#{block_given? ? "y" : "n"}"
+ end
+
+ def jit_method
+ send(:internal_method)
+ end
+ jit_method
+ RUBY
+ end
+
+ def test_send_block_only_sender
+ assert_compiles(<<~'RUBY', result: "Y/Y/Y/Y", exits: {})
+ def internal_method
+ "Y"
+ end
+
+ def jit_method
+ b7 = proc { 7 }
+ [
+ send(:internal_method),
+ send(:internal_method, &b7),
+ send(:internal_method) { 7 },
+ send(:internal_method, &nil),
+ ].join("/")
+ end
+ jit_method
+ RUBY
+ end
+
+ def test_multisend
+ assert_compiles(<<~'RUBY', result: "77")
+ def internal_method
+ "7"
+ end
+
+ def jit_method
+ send(:send, :internal_method) + send(:send, :send, :internal_method)
+ end
+ jit_method
+ RUBY
+ end
+
+ def test_getivar_opt_plus
+ assert_no_exits(<<~RUBY)
+ class TheClass
+ def initialize
+ @levar = 1
+ end
+
+ def get_sum
+ sum = 0
+ # The type of levar is unknown,
+ # but this still should not exit
+ sum += @levar
+ sum
+ end
+ end
+
+ obj = TheClass.new
+ obj.get_sum
+ RUBY
+ end
+
def test_super_iseq
assert_compiles(<<~'RUBY', insns: %i[invokesuper opt_plus opt_mult], result: 15)
class A
@@ -456,6 +876,25 @@ class TestYJIT < Test::Unit::TestCase
RUBY
end
+ def test_super_with_alias
+ assert_compiles(<<~'RUBY', insns: %i[invokesuper opt_plus opt_mult], result: 15)
+ class A
+ def foo = 1 + 2
+ end
+
+ module M
+ def foo = super() * 5
+ alias bar foo
+
+ def foo = :bad
+ end
+
+ A.prepend M
+
+ A.new.bar
+ RUBY
+ end
+
def test_super_cfunc
assert_compiles(<<~'RUBY', insns: %i[invokesuper], result: "Hello")
class Gnirts < String
@@ -474,7 +913,7 @@ class TestYJIT < Test::Unit::TestCase
# Tests calling a variadic cfunc with many args
def test_build_large_struct
- assert_compiles(<<~RUBY, insns: %i[opt_send_without_block], min_calls: 2)
+ assert_compiles(<<~RUBY, insns: %i[opt_send_without_block], call_threshold: 2)
::Foo = Struct.new(:a, :b, :c, :d, :e, :f, :g, :h)
def build_foo
@@ -497,8 +936,26 @@ class TestYJIT < Test::Unit::TestCase
RUBY
end
+ def test_optarg_and_kwarg
+ assert_no_exits(<<~'RUBY')
+ def opt_and_kwarg(a, b=nil, c: nil)
+ end
+
+ 2.times do
+ opt_and_kwarg(1, 2, c: 3)
+ end
+ RUBY
+ end
+
+ def test_cfunc_kwarg
+ assert_no_exits('{}.store(:value, foo: 123)')
+ assert_no_exits('{}.store(:value, foo: 123, bar: 456, baz: 789)')
+ assert_no_exits('{}.merge(foo: 123)')
+ assert_no_exits('{}.merge(foo: 123, bar: 456, baz: 789)')
+ end
+
+ # regression test simplified from URI::Generic#hostname=
def test_ctx_different_mappings
- # regression test simplified from URI::Generic#hostname=
assert_compiles(<<~'RUBY', frozen_string_literal: true)
def foo(v)
!(v&.start_with?('[')) && v&.index(':')
@@ -534,12 +991,662 @@ class TestYJIT < Test::Unit::TestCase
RUBY
end
+ def test_int_equal
+ assert_compiles(<<~'RUBY', exits: :any, result: [true, false, true, false, true, false, true, false])
+ def eq(a, b)
+ a == b
+ end
+
+ def eqq(a, b)
+ a === b
+ end
+
+ big1 = 2 ** 65
+ big2 = big1 + 1
+ [eq(1, 1), eq(1, 2), eq(big1, big1), eq(big1, big2), eqq(1, 1), eqq(1, 2), eqq(big1, big1), eqq(big1, big2)]
+ RUBY
+ end
+
+ def test_opt_case_dispatch
+ assert_compiles(<<~'RUBY', exits: :any, result: [:"1", "2", 3])
+ def case_dispatch(val)
+ case val
+ when 1
+ :"#{val}"
+ when 2
+ "#{val}"
+ else
+ val
+ end
+ end
+
+ [case_dispatch(1), case_dispatch(2), case_dispatch(3)]
+ RUBY
+ end
+
+ def test_code_gc
+ assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok)
+ return :not_paged unless add_pages(100) # prepare freeable pages
+ RubyVM::YJIT.code_gc # first code GC
+ return :not_compiled1 unless compiles { nil } # should be JITable again
+
+ RubyVM::YJIT.code_gc # second code GC
+ return :not_compiled2 unless compiles { nil } # should be JITable again
+
+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
+ return :"code_gc_#{code_gc_count}" if code_gc_count != 2
+
+ :ok
+ RUBY
+ end
+
+ def test_on_stack_code_gc_call
+ assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok)
+ fiber = Fiber.new {
+ # Loop to call the same basic block again after Fiber.yield
+ while true
+ Fiber.yield(nil.to_i)
+ end
+ }
+
+ return :not_paged1 unless add_pages(400) # go to a page without initial ocb code
+ return :broken_resume1 if fiber.resume != 0 # JIT the fiber
+ RubyVM::YJIT.code_gc # first code GC, which should not free the fiber page
+ return :broken_resume2 if fiber.resume != 0 # The code should be still callable
+
+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
+ return :"code_gc_#{code_gc_count}" if code_gc_count != 1
+
+ :ok
+ RUBY
+ end
+
+ def test_on_stack_code_gc_twice
+ assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok)
+ fiber = Fiber.new {
+ # Loop to call the same basic block again after Fiber.yield
+ while Fiber.yield(nil.to_i); end
+ }
+
+ return :not_paged1 unless add_pages(400) # go to a page without initial ocb code
+ return :broken_resume1 if fiber.resume(true) != 0 # JIT the fiber
+ RubyVM::YJIT.code_gc # first code GC, which should not free the fiber page
+
+ return :not_paged2 unless add_pages(300) # add some stuff to be freed
+ # Not calling fiber.resume here to test the case that the YJIT payload loses some
+ # information at the previous code GC. The payload should still be there, and
+ # thus we could know the fiber ISEQ is still on stack on this second code GC.
+ RubyVM::YJIT.code_gc # second code GC, which should still not free the fiber page
+
+ return :not_paged3 unless add_pages(200) # attempt to overwrite the fiber page (it shouldn't)
+ return :broken_resume2 if fiber.resume(true) != 0 # The fiber code should be still fine
+
+ return :broken_resume3 if fiber.resume(false) != nil # terminate the fiber
+ RubyVM::YJIT.code_gc # third code GC, freeing a page that used to be on stack
+
+ return :not_paged4 unless add_pages(100) # check everything still works
+
+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
+ return :"code_gc_#{code_gc_count}" if code_gc_count != 3
+
+ :ok
+ RUBY
+ end
+
+ def test_disable_code_gc_with_many_iseqs
+ assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok, mem_size: 1, code_gc: false)
+ fiber = Fiber.new {
+ # Loop to call the same basic block again after Fiber.yield
+ while true
+ Fiber.yield(nil.to_i)
+ end
+ }
+
+ return :not_paged1 unless add_pages(250) # use some pages
+ return :broken_resume1 if fiber.resume != 0 # leave an on-stack code as well
+
+ add_pages(2000) # use a whole lot of pages to run out of 1MiB
+ return :broken_resume2 if fiber.resume != 0 # on-stack code should be callable
+
+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
+ return :"code_gc_#{code_gc_count}" if code_gc_count != 0
+
+ :ok
+ RUBY
+ end
+
+ def test_code_gc_with_many_iseqs
+ assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: :ok, mem_size: 1, code_gc: true)
+ fiber = Fiber.new {
+ # Loop to call the same basic block again after Fiber.yield
+ while true
+ Fiber.yield(nil.to_i)
+ end
+ }
+
+ return :not_paged1 unless add_pages(250) # use some pages
+ return :broken_resume1 if fiber.resume != 0 # leave an on-stack code as well
+
+ add_pages(2000) # use a whole lot of pages to run out of 1MiB
+ return :broken_resume2 if fiber.resume != 0 # on-stack code should be callable
+
+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
+ return :"code_gc_#{code_gc_count}" if code_gc_count == 0
+
+ :ok
+ RUBY
+ end
+
+ def test_code_gc_with_auto_compact
+ assert_compiles((code_gc_helpers + <<~'RUBY'), exits: :any, result: :ok, mem_size: 1, code_gc: true)
+ # Test ISEQ moves in the middle of code GC
+ GC.auto_compact = true
+
+ fiber = Fiber.new {
+ # Loop to call the same basic block again after Fiber.yield
+ while true
+ Fiber.yield(nil.to_i)
+ end
+ }
+
+ return :not_paged1 unless add_pages(250) # use some pages
+ return :broken_resume1 if fiber.resume != 0 # leave an on-stack code as well
+
+ add_pages(2000) # use a whole lot of pages to run out of 1MiB
+ return :broken_resume2 if fiber.resume != 0 # on-stack code should be callable
+
+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
+ return :"code_gc_#{code_gc_count}" if code_gc_count == 0
+
+ :ok
+ RUBY
+ end
+
+ def test_code_gc_partial_last_page
+ # call_threshold: 2 to avoid JIT-ing code_gc itself. If code_gc were JITed right before
+ # code_gc is called, the last page would be on stack.
+ assert_compiles(<<~'RUBY', exits: :any, result: :ok, call_threshold: 2)
+ # Leave a bunch of off-stack pages
+ i = 0
+ while i < 1000
+ eval("x = proc { 1.to_s }; x.call; x.call")
+ i += 1
+ end
+
+ # On Linux, memory page size != code page size. So the last code page could be partially
+ # mapped. This call tests that assertions and other things work fine under the situation.
+ RubyVM::YJIT.code_gc
+
+ :ok
+ RUBY
+ end
+
+ def test_trace_script_compiled # not ISEQ_TRACE_EVENTS
+ assert_compiles(<<~'RUBY', exits: :any, result: :ok)
+ @eval_counter = 0
+ def eval_script
+ eval('@eval_counter += 1')
+ end
+
+ @trace_counter = 0
+ trace = TracePoint.new(:script_compiled) do |t|
+ @trace_counter += 1
+ end
+
+ eval_script # JIT without TracePoint
+ trace.enable
+ eval_script # call with TracePoint
+ trace.disable
+
+ return :"eval_#{@eval_counter}" if @eval_counter != 2
+ return :"trace_#{@trace_counter}" if @trace_counter != 1
+
+ :ok
+ RUBY
+ end
+
+ def test_trace_b_call # ISEQ_TRACE_EVENTS
+ assert_compiles(<<~'RUBY', exits: :any, result: :ok)
+ @call_counter = 0
+ def block_call
+ 1.times { @call_counter += 1 }
+ end
+
+ @trace_counter = 0
+ trace = TracePoint.new(:b_call) do |t|
+ @trace_counter += 1
+ end
+
+ block_call # JIT without TracePoint
+ trace.enable
+ block_call # call with TracePoint
+ trace.disable
+
+ return :"call_#{@call_counter}" if @call_counter != 2
+ return :"trace_#{@trace_counter}" if @trace_counter != 1
+
+ :ok
+ RUBY
+ end
+
+ def test_send_to_call
+ assert_compiles(<<~'RUBY', result: :ok)
+ ->{ :ok }.send(:call)
+ RUBY
+ end
+
+ def test_invokeblock_many_locals
+ # [Bug #19299]
+ assert_compiles(<<~'RUBY', result: :ok)
+ def foo
+ yield
+ end
+
+ foo do
+ a1=a2=a3=a4=a5=a6=a7=a8=a9=a10=a11=a12=a13=a14=a15=a16=a17=a18=a19=a20=a21=a22=a23=a24=a25=a26=a27=a28=a29=a30 = :ok
+ a30
+ end
+ RUBY
+ end
+
+ def test_bug_19316
+ n = 2 ** 64
+ # foo's extra param and the splats are relevant
+ assert_compiles(<<~'RUBY', result: [[n, -n], [n, -n]], exits: :any)
+ def foo(_, a, b, c)
+ [a & b, ~c]
+ end
+
+ n = 2 ** 64
+ args = [0, -n, n, n-1]
+
+ GC.stress = true
+ [foo(*args), foo(*args)]
+ RUBY
+ end
+
+ def test_gc_compact_cyclic_branch
+ assert_compiles(<<~'RUBY', result: 2)
+ def foo
+ i = 0
+ while i < 2
+ i += 1
+ end
+ i
+ end
+
+ foo
+ GC.compact
+ foo
+ RUBY
+ end
+
+ def test_invalidate_cyclic_branch
+ assert_compiles(<<~'RUBY', result: 2, exits: { opt_plus: 1 })
+ def foo
+ i = 0
+ while i < 2
+ i += 1
+ end
+ i
+ end
+
+ foo
+ class Integer
+ def +(x) = self - -x
+ end
+ foo
+ RUBY
+ end
+
+ def test_tracing_str_uplus
+ assert_compiles(<<~RUBY, frozen_string_literal: true, result: :ok, exits: { putspecialobject: 1, definemethod: 1 })
+ def str_uplus
+ _ = 1
+ _ = 2
+ ret = [+"frfr", __LINE__]
+ _ = 3
+ _ = 4
+
+ ret
+ end
+
+ str_uplus
+ require 'objspace'
+ ObjectSpace.trace_object_allocations_start
+
+ str, expected_line = str_uplus
+ alloc_line = ObjectSpace.allocation_sourceline(str)
+
+ if expected_line == alloc_line
+ :ok
+ else
+ [expected_line, alloc_line]
+ end
+ RUBY
+ end
+
+ def test_str_uplus_subclass
+ assert_compiles(<<~RUBY, frozen_string_literal: true, result: :subclass)
+ class S < String
+ def encoding
+ :subclass
+ end
+ end
+
+ def test(str)
+ (+str).encoding
+ end
+
+ test ""
+ test S.new
+ RUBY
+ end
+
+ def test_return_to_invalidated_block
+ # [Bug #19463]
+ assert_compiles(<<~RUBY, result: [1, 1, :ugokanai], exits: { definesmethod: 1, getlocal_WC_0: 1 })
+ klass = Class.new do
+ def self.lookup(hash, key) = hash[key]
+
+ def self.foo(a, b) = []
+
+ def self.test(hash, key)
+ [lookup(hash, key), key, "".freeze]
+ # 05 opt_send_without_block :lookup
+ # 07 getlocal_WC_0 :hash
+ # 09 opt_str_freeze ""
+ # 12 newarray 3
+ # 14 leave
+ #
+ # YJIT will put instructions (07..14) into a block.
+ # When String#freeze is redefined from within lookup(),
+ # the return address to the block is still on-stack. We rely
+ # on invalidation patching the code at the return address
+ # to service this situation correctly.
+ end
+ end
+
+ # get YJIT to compile test()
+ hash = { 1 => [] }
+ 31.times { klass.test(hash, 1) }
+
+ # inject invalidation into lookup()
+ evil_hash = Hash.new do |_, key|
+ class String
+ undef :freeze
+ def freeze = :ugokanai
+ end
+
+ key
+ end
+ klass.test(evil_hash, 1)
+ RUBY
+ end
+
+ def test_return_to_invalidated_frame
+ assert_compiles(code_gc_helpers + <<~RUBY, exits: :any, result: :ok)
+ def jump
+ [] # something not inlined
+ end
+
+ def entry(code_gc)
+ jit_exception(code_gc)
+ jump # faulty jump after code GC. #jit_exception should not come back.
+ end
+
+ def jit_exception(code_gc)
+ if code_gc
+ tap do
+ RubyVM::YJIT.code_gc
+ break # jit_exec_exception catches TAG_BREAK and re-enters JIT code
+ end
+ end
+ end
+
+ add_pages(100)
+ jump # Compile #jump in a non-first page
+ add_pages(100)
+ entry(false) # Compile #entry and its call to #jump in another page
+ entry(true) # Free #jump but not #entry
+
+ :ok
+ RUBY
+ end
+
+ def test_setivar_on_class
+ # Bug in https://github.com/ruby/ruby/pull/8152
+ assert_compiles(<<~RUBY, result: :ok)
+ class Base
+ def self.or_equal
+ @or_equal ||= Object.new
+ end
+ end
+
+ Base.or_equal # ensure compiled
+
+ class Child < Base
+ end
+
+ 200.times do |iv| # Need to be more than MAX_IVAR
+ Child.instance_variable_set("@_iv_\#{iv}", Object.new)
+ end
+
+ Child.or_equal
+ :ok
+ RUBY
+ end
+
+ def test_nested_send
+ #[Bug #19464]
+ assert_compiles(<<~RUBY, result: [:ok, :ok], exits: { defineclass: 1 })
+ klass = Class.new do
+ class << self
+ alias_method :my_send, :send
+
+ def bar = :ok
+
+ def foo = bar
+ end
+ end
+
+ with_break = -> { break klass.send(:my_send, :foo) }
+ wo_break = -> { klass.send(:my_send, :foo) }
+
+ [with_break[], wo_break[]]
+ RUBY
+ end
+
+ def test_str_concat_encoding_mismatch
+ assert_compiles(<<~'RUBY', result: "incompatible character encodings: BINARY (ASCII-8BIT) and EUC-JP")
+ def bar(a, b)
+ a << b
+ rescue => e
+ e.message
+ end
+
+ def foo(a, b, h)
+ h[nil]
+ bar(a, b) # Ruby call, not set cfp->pc
+ end
+
+ h = Hash.new { nil }
+ foo("\x80".b, "\xA1A1".dup.force_encoding("EUC-JP"), h)
+ foo("\x80".b, "\xA1A1".dup.force_encoding("EUC-JP"), h)
+ RUBY
+ end
+
+ def test_io_reopen_clobbering_singleton_class
+ assert_compiles(<<~RUBY, result: [:ok, :ok], exits: { definesmethod: 1, opt_eq: 2 })
+ def $stderr.to_i = :i
+
+ def test = $stderr.to_i
+
+ [test, test]
+ $stderr.reopen($stderr.dup)
+ [test, test].map { :ok unless _1 == :i }
+ RUBY
+ end
+
+ def test_opt_aref_with
+ assert_compiles(<<~RUBY, insns: %i[opt_aref_with], result: "bar", frozen_string_literal: false)
+ h = {"foo" => "bar"}
+
+ h["foo"]
+ RUBY
+ end
+
+ def test_proc_block_arg
+ assert_compiles(<<~RUBY, result: [:proc, :no_block])
+ def yield_if_given = block_given? ? yield : :no_block
+
+ def call(block_arg = nil) = yield_if_given(&block_arg)
+
+ [call(-> { :proc }), call]
+ RUBY
+ end
+
+ def test_opt_mult_overflow
+ assert_no_exits('0xfff_ffff_ffff_ffff * 0x10')
+ end
+
+ def test_disable_stats
+ assert_in_out_err(%w[--yjit-stats --yjit-disable])
+ end
+
+ def test_odd_calls_to_attr_reader
+ # Use of delegate from ActiveSupport use these kind of calls to getter methods.
+ assert_compiles(<<~RUBY, result: [1, 1, 1], no_send_fallbacks: true)
+ class One
+ attr_reader :one
+ def initialize
+ @one = 1
+ end
+ end
+
+ def calls(obj, empty, &)
+ [obj.one(*empty), obj.one(&), obj.one(*empty, &)]
+ end
+
+ calls(One.new, [])
+ RUBY
+ end
+
+ def test_kwrest
+ assert_compiles(<<~RUBY, result: true, no_send_fallbacks: true)
+ def req_rest(r1:, **kwrest) = [r1, kwrest]
+ def opt_rest(r1: 1.succ, **kwrest) = [r1, kwrest]
+ def kwrest(**kwrest) = kwrest
+
+ def calls
+ [
+ [1, {}] == req_rest(r1: 1),
+ [1, {:r2=>2, :r3=>3}] == req_rest(r1: 1, r2: 2, r3: 3),
+ [1, {:r2=>2, :r3=>3}] == req_rest(r2: 2, r1:1, r3: 3),
+ [1, {:r2=>2, :r3=>3}] == req_rest(r2: 2, r3: 3, r1: 1),
+
+ [2, {}] == opt_rest,
+ [2, { r2: 2, r3: 3 }] == opt_rest(r2: 2, r3: 3),
+ [0, { r2: 2, r3: 3 }] == opt_rest(r1: 0, r3: 3, r2: 2),
+ [0, { r2: 2, r3: 3 }] == opt_rest(r2: 2, r1: 0, r3: 3),
+ [1, { r2: 2, r3: 3 }] == opt_rest(r2: 2, r3: 3, r1: 1),
+
+ {} == kwrest,
+ { r0: 88, r1: 99 } == kwrest(r0: 88, r1: 99),
+ ]
+ end
+
+ calls.all?
+ RUBY
+ end
+
+ def test_send_polymorphic_method_name
+ assert_compiles(<<~'RUBY', result: %i[ok ok], no_send_fallbacks: true)
+ mid = "dynamic_mid_#{rand(100..200)}"
+ mid_dsym = mid.to_sym
+
+ define_method(mid) { :ok }
+
+ define_method(:send_site) { send(_1) }
+
+ [send_site(mid), send_site(mid_dsym)]
+ RUBY
+ end
+
+ def test_kw_splat_nil
+ assert_compiles(<<~'RUBY', result: %i[ok ok ok], no_send_fallbacks: true)
+ def id(x) = x
+ def kw_fw(arg, **) = id(arg, **)
+ def fw(...) = id(...)
+ def use = [fw(:ok), kw_fw(:ok), :ok.itself(**nil)]
+
+ use
+ RUBY
+ end
+
+ def test_empty_splat
+ assert_compiles(<<~'RUBY', result: %i[ok ok], no_send_fallbacks: true)
+ def foo = :ok
+ def fw(...) = foo(...)
+ def use(empty) = [foo(*empty), fw]
+
+ use([])
+ RUBY
+ end
+
+ def test_byteslice_sp_invalidation
+ assert_compiles(<<~'RUBY', result: 'ok', no_send_fallbacks: true)
+ "okng".itself.byteslice(0, 2)
+ RUBY
+ end
+
+ def test_leaf_builtin
+ assert_compiles(code_gc_helpers + <<~'RUBY', exits: :any, result: 1)
+ before = RubyVM::YJIT.runtime_stats[:num_send_iseq_leaf]
+ return 1 if before.nil?
+
+ def entry = self.class
+ entry
+
+ after = RubyVM::YJIT.runtime_stats[:num_send_iseq_leaf]
+ after - before
+ RUBY
+ end
+
+ private
+
+ def code_gc_helpers
+ <<~'RUBY'
+ def compiles(&block)
+ failures = RubyVM::YJIT.runtime_stats[:compilation_failure]
+ block.call
+ failures == RubyVM::YJIT.runtime_stats[:compilation_failure]
+ end
+
+ def add_pages(num_jits)
+ pages = RubyVM::YJIT.runtime_stats[:live_page_count]
+ num_jits.times { return false unless eval('compiles { nil.to_i }') }
+ pages.nil? || pages < RubyVM::YJIT.runtime_stats[:live_page_count]
+ end
+ RUBY
+ end
+
def assert_no_exits(script)
assert_compiles(script)
end
ANY = Object.new
- def assert_compiles(test_script, insns: [], min_calls: 1, stdout: nil, exits: {}, result: ANY, frozen_string_literal: nil)
+ def assert_compiles(
+ test_script, insns: [],
+ call_threshold: 1,
+ stdout: nil,
+ exits: {},
+ result: ANY,
+ frozen_string_literal: nil,
+ mem_size: nil,
+ code_gc: false,
+ no_send_fallbacks: false
+ )
reset_stats = <<~RUBY
RubyVM::YJIT.runtime_stats
RubyVM::YJIT.reset_stats!
@@ -548,35 +1655,23 @@ class TestYJIT < Test::Unit::TestCase
write_results = <<~RUBY
stats = RubyVM::YJIT.runtime_stats
- def collect_blocks(blocks)
- blocks.sort_by(&:address).map { |b| [b.iseq_start_index, b.iseq_end_index] }
- end
-
- def collect_iseqs(iseq)
- iseq_array = iseq.to_a
- insns = iseq_array.last.grep(Array)
- blocks = RubyVM::YJIT.blocks_for(iseq)
- h = {
- name: iseq_array[5],
- insns: insns,
- blocks: collect_blocks(blocks),
- }
- arr = [h]
- iseq.each_child { |c| arr.concat collect_iseqs(c) }
- arr
+ def collect_insns(iseq)
+ insns = RubyVM::YJIT.insns_compiled(iseq)
+ iseq.each_child { |c| insns.concat collect_insns(c) }
+ insns
end
iseq = RubyVM::InstructionSequence.of(_test_proc)
IO.open(3).write Marshal.dump({
result: #{result == ANY ? "nil" : "result"},
stats: stats,
- iseqs: collect_iseqs(iseq),
+ insns: collect_insns(iseq),
disasm: iseq.disasm
})
RUBY
script = <<~RUBY
- #{"# frozen_string_literal: true" if frozen_string_literal}
+ #{"# frozen_string_literal: " + frozen_string_literal.to_s unless frozen_string_literal.nil?}
_test_proc = -> {
#{test_script}
}
@@ -585,7 +1680,7 @@ class TestYJIT < Test::Unit::TestCase
#{write_results}
RUBY
- status, out, err, stats = eval_with_jit(script, min_calls: min_calls)
+ status, out, err, stats = eval_with_jit(script, call_threshold:, mem_size:, code_gc:)
assert status.success?, "exited with status #{status.to_i}, stderr:\n#{err}"
@@ -596,67 +1691,92 @@ class TestYJIT < Test::Unit::TestCase
end
runtime_stats = stats[:stats]
- iseqs = stats[:iseqs]
+ insns_compiled = stats[:insns]
disasm = stats[:disasm]
- # Only available when RUBY_DEBUG enabled
+ # Check that exit counts are as expected
+ # Full stats are only available when --enable-yjit=dev
if runtime_stats[:all_stats]
recorded_exits = runtime_stats.select { |k, v| k.to_s.start_with?("exit_") }
recorded_exits = recorded_exits.reject { |k, v| v == 0 }
recorded_exits.transform_keys! { |k| k.to_s.gsub("exit_", "").to_sym }
- if exits != :any && exits != recorded_exits
- flunk "Expected #{exits.empty? ? "no" : exits.inspect} exits" \
- ", but got\n#{recorded_exits.inspect}"
+ # Exits can be specified as a hash of stat-name symbol to integer for exact exits.
+ # or stat-name symbol to range if the number of side exits might vary (e.g. write
+ # barriers, cache misses.)
+ if exits != :any &&
+ exits != recorded_exits &&
+ (exits.keys != recorded_exits.keys || !exits.all? { |k, v| v === recorded_exits[k] }) # triple-equal checks range membership or integer equality
+ stats_reasons = StringIO.new
+ ::RubyVM::YJIT.send(:_print_stats_reasons, runtime_stats, stats_reasons)
+ stats_reasons = stats_reasons.string
+ flunk <<~EOM
+ Expected #{exits.empty? ? "no" : exits.inspect} exits, but got:
+ #{recorded_exits.inspect}
+ Reasons:
+ #{stats_reasons}
+ EOM
end
end
- # Only available when RUBY_DEBUG enabled
+ if no_send_fallbacks
+ assert_equal(0, runtime_stats[:num_send_dynamic], "Expected no use of fallback implementation")
+ end
+
+ # Only available when --enable-yjit=dev
if runtime_stats[:all_stats]
missed_insns = insns.dup
- all_compiled_blocks = {}
- iseqs.each do |iseq|
- compiled_blocks = iseq[:blocks].map { |from, to| (from...to) }
- all_compiled_blocks[iseq[:name]] = compiled_blocks
- compiled_insns = iseq[:insns]
- next_idx = 0
- compiled_insns.map! do |insn|
- # TODO: not sure this is accurate for determining insn size
- idx = next_idx
- next_idx += insn.length
- [idx, *insn]
- end
-
- compiled_insns.each do |idx, op, *arguments|
- next unless missed_insns.include?(op)
- next unless compiled_blocks.any? { |block| block === idx }
+ insns_compiled.each do |op|
+ if missed_insns.include?(op)
# This instruction was compiled
missed_insns.delete(op)
end
end
unless missed_insns.empty?
- flunk "Expected to compile instructions #{missed_insns.join(", ")} but didn't.\nCompiled ranges: #{all_compiled_blocks.inspect}\niseq:\n#{disasm}"
+ flunk "Expected to compile instructions #{missed_insns.join(", ")} but didn't.\niseq:\n#{disasm}"
end
end
end
- def eval_with_jit(script, min_calls: 1, timeout: 1000)
+ def script_shell_encode(s)
+ # We can't pass utf-8-encoded characters directly in a shell arg. But we can use Ruby \u constants.
+ s.chars.map { |c| c.ascii_only? ? c : "\\u%x" % c.codepoints[0] }.join
+ end
+
+ def eval_with_jit(script, call_threshold: 1, timeout: 1000, mem_size: nil, code_gc: false)
args = [
"--disable-gems",
- "--yjit-call-threshold=#{min_calls}",
- "--yjit-stats"
+ "--yjit-call-threshold=#{call_threshold}",
+ "--yjit-stats=quiet"
]
- args << "-e" << script
+ args << "--yjit-exec-mem-size=#{mem_size}" if mem_size
+ args << "--yjit-code-gc" if code_gc
+ args << "-e" << script_shell_encode(script)
stats_r, stats_w = IO.pipe
- out, err, status = EnvUtil.invoke_ruby(args,
- '', true, true, timeout: timeout, ios: {3 => stats_w}
- )
+ # Separate thread so we don't deadlock when
+ # the child ruby blocks writing the stats to fd 3
+ stats = ''
+ stats_reader = Thread.new do
+ stats = stats_r.read
+ stats_r.close
+ end
+ out, err, status = invoke_ruby(args, '', true, true, timeout: timeout, ios: { 3 => stats_w })
stats_w.close
- stats = stats_r.read
+ stats_reader.join(timeout)
stats = Marshal.load(stats) if !stats.empty?
- stats_r.close
[status, out, err, stats]
+ ensure
+ stats_reader&.kill
+ stats_reader&.join(timeout)
+ stats_r&.close
+ stats_w&.close
+ end
+
+ # A wrapper of EnvUtil.invoke_ruby that uses RbConfig.ruby instead of EnvUtil.ruby
+ # that might use a wrong Ruby depending on your environment.
+ def invoke_ruby(*args, **kwargs)
+ EnvUtil.invoke_ruby(*args, rubybin: RbConfig.ruby, **kwargs)
end
end