diff options
Diffstat (limited to 'test/ruby/test_yjit.rb')
| -rw-r--r-- | test/ruby/test_yjit.rb | 851 |
1 files changed, 823 insertions, 28 deletions
diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index 6b6ea6619e..d6b9b75648 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -10,8 +10,10 @@ require_relative '../lib/jit_support' return unless JITSupport.yjit_supported? +require 'stringio' + # Tests for YJIT with assertions on compilation and side exits -# insipired by the MJIT tests in test/ruby/test_mjit.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? @@ -51,8 +53,127 @@ class TestYJIT < Test::Unit::TestCase #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') + refute_predicate RubyVM::YJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+YJIT" + + RubyVM::YJIT.enable + + assert_predicate RubyVM::YJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+YJIT" + RUBY + end + + def test_yjit_disable + assert_separately(["--yjit", "--yjit-disable"], <<~'RUBY') + refute_predicate RubyVM::YJIT, :enabled? + refute_includes RUBY_DESCRIPTION, "+YJIT" + + RubyVM::YJIT.enable + + assert_predicate RubyVM::YJIT, :enabled? + assert_includes RUBY_DESCRIPTION, "+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_ruby_status(%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_enable_with_valid_runtime_call_threshold_option + assert_in_out_err(['--yjit-disable', '-e', + 'RubyVM::YJIT.enable(call_threshold: 1); puts RubyVM::YJIT.enabled?']) do |stdout, stderr, _status| + assert_empty stderr + assert_include stdout.join, "true" + end + end + + def test_yjit_enable_with_invalid_runtime_call_threshold_option + assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(mem_size: 0)']) do |stdout, stderr, status| + assert_not_empty stderr + assert_match(/ArgumentError/, stderr.join) + assert_equal 1, status.exitstatus + end + end + + def test_yjit_enable_with_invalid_runtime_mem_size_option + assert_in_out_err(['--yjit-disable', '-e', 'RubyVM::YJIT.enable(mem_size: 0)']) do |stdout, stderr, status| + assert_not_empty stderr + assert_match(/ArgumentError/, stderr.join) + assert_equal 1, status.exitstatus + end + end + + if JITSupport.zjit_supported? + def test_yjit_enable_with_zjit_enabled + assert_in_out_err(['--zjit'], 'puts RubyVM::YJIT.enable', ['false'], ['Only one JIT can be enabled at the same time.']) + end + 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 @@ -241,10 +362,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 @@ -453,6 +574,32 @@ 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", call_threshold: 2) def make_str(foo, bar) @@ -510,6 +657,26 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_struct_aset_guards_recv_is_not_frozen + assert_compiles(<<~RUBY, result: :ok, exits: { opt_send_without_block: 1 }) + def foo(obj) + obj.foo = 123 + end + + Foo = Struct.new(:foo) + obj = Foo.new(123) + 100.times do + foo(obj) + end + obj.freeze + begin + foo(obj) + rescue FrozenError + :ok + end + RUBY + end + def test_getblockparam assert_compiles(<<~'RUBY', insns: [:getblockparam]) def foo &blk @@ -524,8 +691,7 @@ class TestYJIT < Test::Unit::TestCase end def test_getblockparamproxy - # Currently two side exits as OPTIMIZED_METHOD_TYPE_CALL is unimplemented - assert_compiles(<<~'RUBY', insns: [:getblockparamproxy], exits: { opt_send_without_block: 2 }) + assert_compiles(<<~'RUBY', insns: [:getblockparamproxy], exits: {}) def foo &blk p blk.call p blk.call @@ -536,6 +702,24 @@ class TestYJIT < Test::Unit::TestCase 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 @@ -584,7 +768,7 @@ class TestYJIT < Test::Unit::TestCase def test_send_kwargs # For now, this side-exits when calls include keyword args - assert_compiles(<<~'RUBY', result: "2#a:1,b:2/A", exits: {opt_send_without_block: 1}) + 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 @@ -624,7 +808,7 @@ class TestYJIT < Test::Unit::TestCase 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", exits: {opt_send_without_block: 1}) + 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 @@ -638,7 +822,7 @@ class TestYJIT < Test::Unit::TestCase 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", exits: { :setlocal_WC_0 => 0..1 }) + assert_compiles(<<~'RUBY', result: "b:n/b:y/b:y/b:n") def internal_method(&b) "b:#{block_given? ? "y" : "n"}" end @@ -754,6 +938,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 @@ -952,8 +1155,57 @@ class TestYJIT < Test::Unit::TestCase 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) + 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 + omit "compaction is not supported on this platform" unless GC.respond_to?(: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 @@ -974,6 +1226,25 @@ class TestYJIT < Test::Unit::TestCase 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 @@ -1045,7 +1316,7 @@ class TestYJIT < Test::Unit::TestCase def test_bug_19316 n = 2 ** 64 # foo's extra param and the splats are relevant - assert_compiles(<<~'RUBY', result: [[n, -n], [n, -n]]) + assert_compiles(<<~'RUBY', result: [[n, -n], [n, -n]], exits: :any) def foo(_, a, b, c) [a & b, ~c] end @@ -1058,6 +1329,493 @@ class TestYJIT < Test::Unit::TestCase RUBY end + def test_gc_compact_cyclic_branch + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + 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 }) + 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_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], no_send_fallbacks: true) + def id(x) = x + def kw_fw(arg, **) = id(arg, **) + def use = [kw_fw(:ok), :ok.itself(**nil)] + + use + RUBY + end + + def test_empty_splat + assert_compiles(<<~'RUBY', result: :ok, no_send_fallbacks: true) + def foo = :ok + def use(empty) = foo(*empty) + + 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 + + def test_runtime_stats_types + assert_compiles(<<~'RUBY', exits: :any, result: true) + def test = :ok + 3.times { test } + + stats = RubyVM::YJIT.runtime_stats + return true unless stats[:all_stats] + + [ + stats[:object_shape_count].is_a?(Integer), + stats[:ratio_in_yjit].nil? || stats[:ratio_in_yjit].is_a?(Float), + ].all? + RUBY + end + + def test_runtime_stats_key_arg + assert_compiles(<<~'RUBY', exits: :any, result: true) + def test = :ok + 3.times { test } + + # Collect single stat. + stat = RubyVM::YJIT.runtime_stats(:yjit_alloc_size) + + # Ensure this invocation had stats. + return true unless RubyVM::YJIT.runtime_stats[:all_stats] + + stat > 0.0 + RUBY + end + + def test_runtime_stats_arg_error + assert_compiles(<<~'RUBY', exits: :any, result: true) + begin + RubyVM::YJIT.runtime_stats(Object.new) + :no_error + rescue TypeError => e + e.message == "non-symbol given" + end + RUBY + end + + def test_runtime_stats_unknown_key + assert_compiles(<<~'RUBY', exits: :any, result: true) + def test = :ok + 3.times { test } + + RubyVM::YJIT.runtime_stats(:some_key_unlikely_to_exist).nil? + RUBY + end + + def test_yjit_option_uses_array_each_in_ruby + assert_separately(["--yjit"], <<~'RUBY') + # Array#each should be implemented in Ruby for YJIT + assert_equal "<internal:array>", Array.instance_method(:each).source_location.first + + # The backtrace, however, should not be `from <internal:array>:XX:in 'Array#each'` + begin + [nil].each { raise } + rescue => e + assert_equal "-:11:in 'Array#each'", e.backtrace[1] + end + RUBY + end + + def test_yjit_enable_replaces_array_each + assert_separately([*("--disable=yjit" if RubyVM::YJIT.enabled?)], <<~'RUBY') + # Array#each should be implemented in C for the interpreter + assert_nil Array.instance_method(:each).source_location + + # The backtrace should not be `from <internal:array>:XX:in 'Array#each'` + begin + [nil].each { raise } + rescue => e + assert_equal "-:11:in 'Array#each'", e.backtrace[1] + end + + RubyVM::YJIT.enable + + # Array#each should be implemented in Ruby for YJIT + assert_equal "<internal:array>", Array.instance_method(:each).source_location.first + + # However, the backtrace should still not be `from <internal:array>:XX:in 'Array#each'` + begin + [nil].each { raise } + rescue => e + assert_equal "-:23:in 'Array#each'", e.backtrace[1] + end + RUBY + end + + def test_yjit_enable_preserves_array_each_monkey_patch + assert_separately([*("--disable=yjit" if RubyVM::YJIT.enabled?)], <<~'RUBY') + # Array#each should be implemented in C initially + assert_nil Array.instance_method(:each).source_location + + # Override Array#each + $called = false + Array.prepend(Module.new { + def each + $called = true + super + end + }) + + RubyVM::YJIT.enable + + # The monkey-patch should still be alive + [].each {} + assert_true $called + + # YJIT should not replace Array#each with the "<internal:array>" one + assert_equal "-", Array.instance_method(:each).source_location.first + RUBY + end + + def test_yield_kwargs + assert_compiles(<<~RUBY, result: 3, no_send_fallbacks: true) + def req2kws = yield a: 1, b: 2 + + req2kws { |a:, b:| a + b } + RUBY + end + + def test_proc_block_with_kwrest + # When the bug was present this required --yjit-stats to trigger. + assert_compiles(<<~RUBY, result: {extra: 5}) + def foo = bar(w: 1, x: 2, y: 3, z: 4, extra: 5, &proc { _1 }) + def bar(w:, x:, y:, z:, **kwrest) = yield kwrest + + GC.stress = true + foo + foo + RUBY + end + + def test_yjit_dump_insns + # Testing that this undocumented debugging feature doesn't crash + args = [ + '--yjit-call-threshold=1', + '--yjit-dump-insns', + '-e def foo(case:) = {case:}[:case]', + '-e foo(case:0)', + ] + _out, _err, status = invoke_ruby(args, '', true, true) + assert_not_predicate(status, :signaled?) + end + private def code_gc_helpers @@ -1069,9 +1827,9 @@ class TestYJIT < Test::Unit::TestCase end def add_pages(num_jits) - pages = RubyVM::YJIT.runtime_stats[:compiled_page_count] + 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[:compiled_page_count] + pages.nil? || pages < RubyVM::YJIT.runtime_stats[:live_page_count] end RUBY end @@ -1081,7 +1839,17 @@ class TestYJIT < Test::Unit::TestCase end ANY = Object.new - def assert_compiles(test_script, insns: [], call_threshold: 1, stdout: nil, exits: {}, result: ANY, frozen_string_literal: nil, mem_size: 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! @@ -1106,7 +1874,7 @@ class TestYJIT < Test::Unit::TestCase 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} } @@ -1115,7 +1883,7 @@ class TestYJIT < Test::Unit::TestCase #{write_results} RUBY - status, out, err, stats = eval_with_jit(script, call_threshold:, mem_size:) + 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}" @@ -1141,12 +1909,23 @@ class TestYJIT < Test::Unit::TestCase # barriers, cache misses.) if exits != :any && exits != recorded_exits && - !exits.all? { |k, v| v === recorded_exits[k] } # triple-equal checks range membership or integer equality - flunk "Expected #{exits.empty? ? "no" : exits.inspect} exits" \ - ", but got\n#{recorded_exits.inspect}" + (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 + 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 @@ -1169,22 +1948,38 @@ class TestYJIT < Test::Unit::TestCase 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) + def eval_with_jit(script, call_threshold: 1, timeout: 1000, mem_size: nil, code_gc: false) args = [ "--disable-gems", "--yjit-call-threshold=#{call_threshold}", - "--yjit-stats" + "--yjit-stats=quiet" ] 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 |
