summaryrefslogtreecommitdiff
path: root/test/ruby/test_zjit.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/ruby/test_zjit.rb')
-rw-r--r--test/ruby/test_zjit.rb826
1 files changed, 421 insertions, 405 deletions
diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb
index ff43827c5a..a56fea6d51 100644
--- a/test/ruby/test_zjit.rb
+++ b/test/ruby/test_zjit.rb
@@ -9,528 +9,544 @@ require_relative '../lib/jit_support'
return unless JITSupport.zjit_supported?
class TestZJIT < Test::Unit::TestCase
- def test_call_itself
- assert_compiles '42', <<~RUBY, call_threshold: 2
- def test = 42.itself
- test
- test
+ def test_enabled
+ assert_runs 'false', <<~RUBY, zjit: false
+ RubyVM::ZJIT.enabled?
+ RUBY
+ assert_runs 'true', <<~RUBY, zjit: true
+ RubyVM::ZJIT.enabled?
RUBY
end
- def test_nil
- assert_compiles 'nil', %q{
- def test = nil
- test
- }
+ def test_stats_enabled
+ assert_runs 'false', <<~RUBY, stats: false
+ RubyVM::ZJIT.stats_enabled?
+ RUBY
+ assert_runs 'true', <<~RUBY, stats: true
+ RubyVM::ZJIT.stats_enabled?
+ RUBY
end
- def test_putobject
- assert_compiles '1', %q{
- def test = 1
- test
- }
+ def test_stats_string_no_zjit
+ assert_runs 'nil', <<~RUBY, zjit: false
+ RubyVM::ZJIT.stats_string
+ RUBY
+ assert_runs 'true', <<~RUBY, stats: false
+ RubyVM::ZJIT.stats_string.is_a?(String)
+ RUBY
+ assert_runs 'true', <<~RUBY, stats: true
+ RubyVM::ZJIT.stats_string.is_a?(String)
+ RUBY
end
- def test_leave_param
- assert_compiles '5', %q{
- def test(n) = n
- test(5)
+ def test_stats_quiet
+ # Test that --zjit-stats-quiet collects stats but doesn't print them
+ script = <<~RUBY
+ def test = 42
+ test
+ test
+ puts RubyVM::ZJIT.stats_enabled?
+ RUBY
+
+ stats_header = "***ZJIT: Printing ZJIT statistics on exit***"
+
+ # With --zjit-stats, stats should be printed to stderr
+ out, err, status = eval_with_jit(script, stats: true)
+ assert_success(out, err, status)
+ assert_includes(err, stats_header)
+ assert_equal("true\n", out)
+
+ # With --zjit-stats-quiet, stats should NOT be printed but still enabled
+ out, err, status = eval_with_jit(script, stats: :quiet)
+ assert_success(out, err, status)
+ refute_includes(err, stats_header)
+ assert_equal("true\n", out)
+
+ # With --zjit-stats=<path>, stats should be printed to the path
+ Tempfile.create("zjit-stats-") {|tmp|
+ stats_file = tmp.path
+ tmp.puts("Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...")
+ tmp.close
+
+ out, err, status = eval_with_jit(script, stats: stats_file)
+ assert_success(out, err, status)
+ refute_includes(err, stats_header)
+ assert_equal("true\n", out)
+ assert_equal stats_header, File.open(stats_file) {|f| f.gets(chomp: true)}, "should be overwritten"
}
end
- def test_setlocal
- assert_compiles '3', %q{
- def test(n)
- m = n
- m
- end
- test(3)
- }
+ def test_enable_through_env
+ child_env = {'RUBY_YJIT_ENABLE' => nil, 'RUBY_ZJIT_ENABLE' => '1'}
+ assert_in_out_err([child_env, '-v'], '') do |stdout, stderr|
+ assert_includes(stdout.first, '+ZJIT')
+ assert_equal([], stderr)
+ end
end
- def test_send_without_block
- assert_compiles '[1, 2, 3]', %q{
- def foo = 1
- def bar(a) = a - 1
- def baz(a, b) = a - b
+ def test_zjit_enable
+ # --disable-all is important in case the build/environment has YJIT enabled by
+ # default through e.g. -DYJIT_FORCE_ENABLE. Can't enable ZJIT when YJIT is on.
+ assert_separately(["--disable-all"], <<~'RUBY')
+ refute_predicate RubyVM::ZJIT, :enabled?
+ refute_predicate RubyVM::ZJIT, :stats_enabled?
+ refute_includes RUBY_DESCRIPTION, "+ZJIT"
- def test1 = foo
- def test2 = bar(3)
- def test3 = baz(4, 1)
+ RubyVM::ZJIT.enable
- [test1, test2, test3]
- }
+ assert_predicate RubyVM::ZJIT, :enabled?
+ refute_predicate RubyVM::ZJIT, :stats_enabled?
+ assert_includes RUBY_DESCRIPTION, "+ZJIT"
+ RUBY
end
- def test_opt_plus_const
- assert_compiles '3', %q{
- def test = 1 + 2
- test # profile opt_plus
- test
- }, call_threshold: 2
- end
+ def test_zjit_disable
+ assert_separately(["--zjit", "--zjit-disable"], <<~'RUBY')
+ refute_predicate RubyVM::ZJIT, :enabled?
+ refute_includes RUBY_DESCRIPTION, "+ZJIT"
- def test_opt_plus_fixnum
- assert_compiles '3', %q{
- def test(a, b) = a + b
- test(0, 1) # profile opt_plus
- test(1, 2)
- }, call_threshold: 2
+ RubyVM::ZJIT.enable
+
+ assert_predicate RubyVM::ZJIT, :enabled?
+ assert_includes RUBY_DESCRIPTION, "+ZJIT"
+ RUBY
end
- def test_opt_plus_chain
- assert_compiles '6', %q{
- def test(a, b, c) = a + b + c
- test(0, 1, 2) # profile opt_plus
- test(1, 2, 3)
- }, call_threshold: 2
+ def test_zjit_prelude_kernel_prepend
+ # Simulate what bundler/setup can do: prepend a module to Kernel during
+ # the prelude via the BUNDLER_SETUP mechanism in rubygems.rb:
+ # require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler)
+ Tempfile.create(["kernel_prepend", ".rb"]) do |f|
+ f.write("Kernel.prepend(Module.new)\n")
+ f.flush
+ assert_separately([{ "BUNDLER_SETUP" => f.path }, "--enable=gems", "--zjit"], "", ignore_stderr: true)
+ end
end
- # Test argument ordering
- def test_opt_minus
- assert_compiles '2', %q{
- def test(a, b) = a - b
- test(2, 1) # profile opt_minus
- test(6, 4)
- }, call_threshold: 2
+ def test_zjit_enable_respects_existing_options
+ assert_separately(['--zjit-disable', '--zjit-stats-quiet'], <<~RUBY)
+ refute_predicate RubyVM::ZJIT, :enabled?
+ assert_predicate RubyVM::ZJIT, :stats_enabled?
+
+ RubyVM::ZJIT.enable
+
+ assert_predicate RubyVM::ZJIT, :enabled?
+ assert_predicate RubyVM::ZJIT, :stats_enabled?
+ RUBY
end
- def test_opt_mult
- assert_compiles '6', %q{
- def test(a, b) = a * b
- test(1, 2) # profile opt_mult
- test(2, 3)
- }, call_threshold: 2
+ def test_toplevel_binding
+ # Not using assert_compiles, which doesn't use the toplevel frame for `test_script`.
+ out, err, status = eval_with_jit(%q{
+ a = 1
+ b = 2
+ TOPLEVEL_BINDING.local_variable_set(:b, 3)
+ c = 4
+ print [a, b, c]
+ })
+ assert_success(out, err, status)
+ assert_equal "[1, 3, 4]", out
end
- def test_opt_mult_overflow
- omit 'side exits are not implemented yet'
- assert_compiles '[6, -6, 9671406556917033397649408, -9671406556917033397649408, 21267647932558653966460912964485513216]', %q{
- def test(a, b)
- a * b
+ def test_send_exit_with_uninitialized_locals
+ assert_runs 'nil', %q{
+ def entry(init)
+ function_stub_exit(init)
end
- test(1, 1) # profile opt_mult
- r1 = test(2, 3)
- r2 = test(2, -3)
- r3 = test(2 << 40, 2 << 41)
- r4 = test(2 << 40, -2 << 41)
- r5 = test(1 << 62, 1 << 62)
+ def function_stub_exit(init)
+ uninitialized_local = 1 if init
+ uninitialized_local
+ end
- [r1, r2, r3, r4, r5]
- }, call_threshold: 2
+ entry(true) # profile and set 1 to the local slot
+ entry(false)
+ }, call_threshold: 2, allowed_iseqs: 'entry@-e:2'
end
- def test_opt_eq
- assert_compiles '[true, false]', %q{
- def test(a, b) = a == b
- test(0, 2) # profile opt_eq
- [test(1, 1), test(0, 1)]
- }, call_threshold: 2
+ def test_opt_new_with_custom_allocator
+ assert_compiles '"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"', %q{
+ require "digest"
+ def test = Digest::SHA256.new.hexdigest
+ test; test
+ }, insns: [:opt_new], call_threshold: 2
end
- def test_opt_neq_dynamic
- # TODO(max): Don't split this test; instead, run all tests with and without
- # profiling.
- assert_compiles '[false, true]', %q{
- def test(a, b) = a != b
- test(0, 2) # profile opt_neq
- [test(1, 1), test(0, 1)]
- }, call_threshold: 1
+ def test_opt_new_with_custom_allocator_raises
+ assert_compiles '[42, 42]', %q{
+ require "digest"
+ class C < Digest::Base; end
+ def test
+ begin
+ Digest::Base.new
+ rescue NotImplementedError
+ 42
+ end
+ end
+ [test, test]
+ }, insns: [:opt_new], call_threshold: 2
end
- def test_opt_neq_fixnum
- assert_compiles '[false, true]', %q{
- def test(a, b) = a != b
- test(0, 2) # profile opt_neq
- [test(1, 1), test(0, 1)]
- }, call_threshold: 2
+ def test_uncached_getconstant_path
+ assert_compiles RUBY_COPYRIGHT.dump, %q{
+ def test = RUBY_COPYRIGHT
+ test
+ }, call_threshold: 1, insns: [:opt_getconstant_path]
end
- def test_opt_lt
- assert_compiles '[true, false, false]', %q{
- def test(a, b) = a < b
- test(2, 3) # profile opt_lt
- [test(0, 1), test(0, 0), test(1, 0)]
- }, call_threshold: 2
- end
+ def test_getconstant_path_autoload
+ # A constant-referencing expression can run arbitrary code through Kernel#autoload.
+ Dir.mktmpdir('autoload') do |tmpdir|
+ autoload_path = File.join(tmpdir, 'test_getconstant_path_autoload.rb')
+ File.write(autoload_path, 'X = RUBY_COPYRIGHT')
- def test_opt_lt_with_literal_lhs
- assert_compiles '[false, false, true]', %q{
- def test(n) = 2 < n
- test(2) # profile opt_lt
- [test(1), test(2), test(3)]
- }, call_threshold: 2
+ assert_compiles RUBY_COPYRIGHT.dump, %Q{
+ Object.autoload(:X, #{File.realpath(autoload_path).inspect})
+ def test = X
+ test
+ }, call_threshold: 1, insns: [:opt_getconstant_path]
+ end
end
- def test_opt_le
- assert_compiles '[true, true, false]', %q{
- def test(a, b) = a <= b
- test(2, 3) # profile opt_le
- [test(0, 1), test(0, 0), test(1, 0)]
+ def test_send_backtrace
+ backtrace = [
+ "-e:2:in 'Object#jit_frame1'",
+ "-e:3:in 'Object#entry'",
+ "-e:5:in 'block in <main>'",
+ "-e:6:in '<main>'",
+ ]
+ assert_compiles backtrace.inspect, %q{
+ def jit_frame2 = caller # 1
+ def jit_frame1 = jit_frame2 # 2
+ def entry = jit_frame1 # 3
+ entry # profile send # 4
+ entry # 5
}, call_threshold: 2
end
- def test_opt_gt
- assert_compiles '[false, false, true]', %q{
- def test(a, b) = a > b
- test(2, 3) # profile opt_gt
- [test(0, 1), test(0, 0), test(1, 0)]
- }, call_threshold: 2
+ # tool/ruby_vm/views/*.erb relies on the zjit instructions a) being contiguous and
+ # b) being reliably ordered after all the other instructions.
+ def test_instruction_order
+ insn_names = RubyVM::INSTRUCTION_NAMES
+ zjit, others = insn_names.map.with_index.partition { |name, _| name.start_with?('zjit_') }
+ zjit_indexes = zjit.map(&:last)
+ other_indexes = others.map(&:last)
+ zjit_indexes.product(other_indexes).each do |zjit_index, other_index|
+ assert zjit_index > other_index, "'#{insn_names[zjit_index]}' at #{zjit_index} "\
+ "must be defined after '#{insn_names[other_index]}' at #{other_index}"
+ end
end
- def test_opt_ge
- assert_compiles '[false, true, true]', %q{
- def test(a, b) = a >= b
- test(2, 3) # profile opt_ge
- [test(0, 1), test(0, 0), test(1, 0)]
+ def test_require_rubygems
+ assert_runs 'true', %q{
+ require 'rubygems'
}, call_threshold: 2
end
- def test_new_array_empty
- assert_compiles '[]', %q{
- def test = []
- test
- }
- end
-
- def test_new_array_nonempty
- assert_compiles '[5]', %q{
- def a = 5
- def test = [a]
- test
- }
+ def test_require_rubygems_with_auto_compact
+ omit("GC.auto_compact= support is required for this test") unless GC.respond_to?(:auto_compact=)
+ assert_runs 'true', %q{
+ GC.auto_compact = true
+ require 'rubygems'
+ }, call_threshold: 2
end
- def test_new_array_order
- assert_compiles '[3, 2, 1]', %q{
- def a = 3
- def b = 2
- def c = 1
- def test = [a, b, c]
+ def test_stats_availability
+ assert_runs '[true, true]', %q{
+ def test = 1
test
- }
+ [
+ RubyVM::ZJIT.stats[:zjit_insn_count] > 0,
+ RubyVM::ZJIT.stats(:zjit_insn_count) > 0,
+ ]
+ }, stats: true
end
- def test_array_dup
- assert_compiles '[1, 2, 3]', %q{
- def test = [1,2,3]
- test
- }
- end
+ def test_stats_consistency
+ assert_runs '[]', %q{
+ def test = 1
+ test # increment some counters
- def test_if
- assert_compiles '[0, nil]', %q{
- def test(n)
- if n < 5
- 0
+ RubyVM::ZJIT.stats.to_a.filter_map do |key, value|
+ # The value may be incremented, but the class should stay the same
+ other_value = RubyVM::ZJIT.stats(key)
+ if value.class != other_value.class
+ [key, value, other_value]
end
end
- [test(3), test(7)]
- }
+ }, stats: true
end
- def test_if_else
- assert_compiles '[0, 1]', %q{
- def test(n)
- if n < 5
- 0
- else
- 1
- end
- end
- [test(3), test(7)]
- }
- end
+ def test_reset_stats
+ assert_runs 'true', %q{
+ def test = 1
+ 100.times { test }
- def test_if_else_params
- assert_compiles '[1, 20]', %q{
- def test(n, a, b)
- if n < 5
- a
- else
- b
- end
- end
- [test(3, 1, 2), test(7, 10, 20)]
- }
- end
+ # Get initial stats and verify they're non-zero
+ initial_stats = RubyVM::ZJIT.stats
+
+ # Reset the stats
+ RubyVM::ZJIT.reset_stats!
+
+ # Get stats after reset
+ reset_stats = RubyVM::ZJIT.stats
- def test_if_else_nested
- assert_compiles '[3, 8, 9, 14]', %q{
- def test(a, b, c, d, e)
- if 2 < a
- if a < 4
- b
- else
- c
- end
- else
- if a < 0
- d
- else
- e
- end
- end
- end
[
- test(-1, 1, 2, 3, 4),
- test( 0, 5, 6, 7, 8),
- test( 3, 9, 10, 11, 12),
- test( 5, 13, 14, 15, 16),
- ]
+ # After reset, counters should be zero or at least much smaller
+ # (some instructions might execute between reset and reading stats)
+ :zjit_insn_count.then { |s| initial_stats[s] > 0 && reset_stats[s] < initial_stats[s] },
+ :compiled_iseq_count.then { |s| initial_stats[s] > 0 && reset_stats[s] < initial_stats[s] }
+ ].all?
+ }, stats: true
+ end
+
+ def test_zjit_option_uses_array_each_in_ruby
+ omit 'ZJIT wrongly compiles Array#each, so it is disabled for now'
+ assert_runs '"<internal:array>"', %q{
+ Array.instance_method(:each).source_location&.first
}
end
- def test_if_else_chained
- assert_compiles '[12, 11, 21]', %q{
- def test(a)
- (if 2 < a then 1 else 2 end) + (if a < 4 then 10 else 20 end)
+ def test_line_tracepoint_on_c_method
+ assert_compiles '"[[:line, true]]"', %q{
+ events = []
+ events.instance_variable_set(
+ :@tp,
+ TracePoint.new(:line) { |tp| events << [tp.event, tp.lineno] if tp.path == __FILE__ }
+ )
+ def events.to_str
+ @tp.enable; ''
end
- [test(0), test(3), test(5)]
- }
- end
- def test_if_elsif_else
- assert_compiles '[0, 2, 1]', %q{
- def test(n)
- if n < 5
- 0
- elsif 8 < n
- 1
- else
- 2
- end
+ # Stay in generated code while enabling tracing
+ def events.compiled(obj)
+ String(obj)
+ @tp.disable; __LINE__
end
- [test(3), test(7), test(9)]
- }
- end
- def test_ternary_operator
- assert_compiles '[1, 20]', %q{
- def test(n, a, b)
- n < 5 ? a : b
- end
- [test(3, 1, 2), test(7, 10, 20)]
- }
- end
+ line = events.compiled(events)
+ events[0][-1] = (events[0][-1] == line)
- def test_ternary_operator_nested
- assert_compiles '[2, 21]', %q{
- def test(n, a, b)
- (n < 5 ? a : b) + 1
- end
- [test(3, 1, 2), test(7, 10, 20)]
+ events.to_s # can't dump events as it's a singleton object AND it has a TracePoint instance variable, which also can't be dumped
}
end
- def test_while_loop
- assert_compiles '10', %q{
- def test(n)
- i = 0
- while i < n
- i = i + 1
- end
- i
+ def test_targeted_line_tracepoint_in_c_method_call
+ assert_compiles '"[true]"', %q{
+ events = []
+ events.instance_variable_set(:@tp, TracePoint.new(:line) { |tp| events << tp.lineno })
+ def events.to_str
+ @tp.enable(target: method(:compiled))
+ ''
end
- test(10)
- }
- end
- def test_while_loop_chain
- assert_compiles '[135, 270]', %q{
- def test(n)
- i = 0
- while i < n
- i = i + 1
- end
- while i < n * 10
- i = i * 3
- end
- i
+ # Stay in generated code while enabling tracing
+ def events.compiled(obj)
+ String(obj)
+ __LINE__
end
- [test(5), test(10)]
- }
- end
- def test_while_loop_nested
- assert_compiles '[0, 4, 12]', %q{
- def test(n, m)
- i = 0
- while i < n
- j = 0
- while j < m
- j += 2
- end
- i += j
- end
- i
- end
- [test(0, 0), test(1, 3), test(10, 5)]
- }
- end
+ line = events.compiled(events)
+ events[0] = (events[0] == line)
- def test_while_loop_if_else
- assert_compiles '[9, -1]', %q{
- def test(n)
- i = 0
- while i < n
- if n >= 10
- return -1
- else
- i = i + 1
- end
- end
- i
- end
- [test(9), test(10)]
+ events.to_s # can't dump events as it's a singleton object AND it has a TracePoint instance variable, which also can't be dumped
}
end
- def test_if_while_loop
- assert_compiles '[9, 12]', %q{
- def test(n)
- i = 0
- if n < 10
- while i < n
- i += 1
- end
- else
- while i < n
- i += 3
- end
- end
- i
+ def test_regression_cfp_sp_set_correctly_before_leaf_gc_call
+ assert_compiles ':ok', %q{
+ def check(l, r)
+ return 1 unless l
+ 1 + check(*l) + check(*r)
end
- [test(9), test(10)]
- }
- end
- def test_live_reg_past_ccall
- assert_compiles '2', %q{
- def callee = 1
- def test = callee + callee
- test
- }
- end
+ def tree(depth)
+ # This duparray is our leaf-gc target.
+ return [nil, nil] unless depth > 0
- def test_method_call
- assert_compiles '12', %q{
- def callee(a, b)
- a - b
+ # Modify the local and pass it to the following calls.
+ depth -= 1
+ [tree(depth), tree(depth)]
end
def test
- callee(4, 2) + 10
+ GC.stress = true
+ 2.times do
+ t = tree(11)
+ check(*t)
+ end
+ :ok
end
- test # profile test
test
- }, call_threshold: 2
+ }, call_threshold: 14, num_profiles: 5
end
- def test_recursive_fact
- assert_compiles '[1, 6, 720]', %q{
- def fact(n)
- if n == 0
- return 1
- end
- return n * fact(n-1)
- end
- [fact(0), fact(3), fact(6)]
- }
- end
+ def test_exit_tracing
+ # Smoke test: --zjit-trace-exits writes a Fuchsia trace (.fxt) file to /tmp
+ assert_compiles('true', <<~RUBY, extra_args: ['--zjit-trace-exits'])
+ def test(object) = object.itself
- def test_profiled_fact
- assert_compiles '[1, 6, 720]', %q{
- def fact(n)
- if n == 0
- return 1
- end
- return n * fact(n-1)
- end
- fact(1) # profile fact
- [fact(0), fact(3), fact(6)]
- }, call_threshold: 3, num_profiles: 2
+ # induce an exit just for good measure
+ array = []
+ test(array)
+ test(array)
+ def array.itself = :not_itself
+ test(array)
+
+ fxt_files = Dir.glob("/tmp/perfetto-\#{Process.pid}.fxt")
+ result = fxt_files.length == 1 && !File.empty?(fxt_files.first)
+ File.unlink(*fxt_files)
+ result
+ RUBY
end
- def test_recursive_fib
- assert_compiles '[0, 2, 3]', %q{
- def fib(n)
- if n < 2
- return n
- end
- return fib(n-1) + fib(n-2)
- end
- [fib(0), fib(3), fib(4)]
- }
+ def test_send_no_profiles_with_disabled_specialized_instruction
+ # Regression test: when specialized_instruction is disabled (as power_assert does),
+ # eval'd code uses `send` instead of `opt_send_without_block`, producing SendNoProfiles.
+ # The `times` call with a literal block is the SendNoProfiles send whose exit profiling
+ # triggers recompilation of `run`. After recompilation, `make`'s eval("proc { }") crashes
+ # in vm_make_env_each because the caller frame's EP[-1] (specval) has a stale value.
+ assert_runs ':ok', <<~RUBY
+ RubyVM::InstructionSequence.compile_option = { specialized_instruction: false }
+ eval <<~'INNERRUBY'
+ def make = eval("proc { }")
+ def run(n) = n.times { make }
+ INNERRUBY
+ run(6)
+ :ok
+ RUBY
end
- def test_profiled_fib
- assert_compiles '[0, 2, 3]', %q{
- def fib(n)
- if n < 2
- return n
- end
- return fib(n-1) + fib(n-2)
- end
- fib(3) # profile fib
- [fib(0), fib(3), fib(4)]
- }, call_threshold: 5, num_profiles: 3
+ def test_float_arithmetic
+ assert_compiles '4.0', 'def test = 1.5 + 2.5; test'
+ assert_compiles '6.0', 'def test = 2.0 * 3.0; test'
+ assert_compiles '1.5', 'def test = 3.5 - 2.0; test'
+ assert_compiles '2.5', 'def test = 5.0 / 2.0; test'
+ assert_compiles '4.5', 'def test = 1.5 * 3; test' # Float * Fixnum
+ assert_compiles 'true', 'def test = (Float::NAN + 1.0).nan?; test'
+ assert_compiles 'Infinity', 'def test = Float::INFINITY * 2.0; test'
+ assert_compiles '3', 'def test = 3.7.to_i; test'
+ assert_compiles '-2', 'def test = (-2.9).to_i; test'
end
private
# Assert that every method call in `test_script` can be compiled by ZJIT
# at a given call_threshold
- def assert_compiles(expected, test_script, **opts)
+ def assert_compiles(expected, test_script, insns: [], **opts)
+ assert_runs(expected, test_script, insns:, assert_compiles: true, **opts)
+ end
+
+ # Assert that `test_script` runs successfully with ZJIT enabled.
+ # Unlike `assert_compiles`, `assert_runs(assert_compiles: false)`
+ # allows ZJIT to skip compiling methods.
+ def assert_runs(expected, test_script, insns: [], assert_compiles: false, **opts)
pipe_fd = 3
+ disasm_method = :test
script = <<~RUBY
- _test_proc = -> {
- RubyVM::ZJIT.assert_compiles
- #{test_script}
+ ret_val = (_test_proc = -> { #{('RubyVM::ZJIT.assert_compiles; ' if assert_compiles)}#{test_script.lstrip} }).call
+ result = {
+ ret_val:,
+ #{ unless insns.empty?
+ "insns: RubyVM::InstructionSequence.of(method(#{disasm_method.inspect})).to_a"
+ end}
}
- result = _test_proc.call
- IO.open(#{pipe_fd}).write(result.inspect)
+ IO.open(#{pipe_fd}).write(Marshal.dump(result))
RUBY
- status, out, err, actual = eval_with_jit(script, pipe_fd:, **opts)
-
- message = "exited with status #{status.to_i}"
- message << "\nstdout:\n```\n#{out}```\n" unless out.empty?
- message << "\nstderr:\n```\n#{err}```\n" unless err.empty?
- assert status.success?, message
-
- assert_equal expected, actual
+ out, err, status, result = eval_with_jit(script, pipe_fd:, **opts)
+ assert_success(out, err, status)
+
+ result = Marshal.load(result)
+ assert_equal(expected, result.fetch(:ret_val).inspect)
+
+ unless insns.empty?
+ iseq = result.fetch(:insns)
+ assert_equal(
+ "YARVInstructionSequence/SimpleDataFormat",
+ iseq.first,
+ "Failed to get ISEQ disassembly. " \
+ "Make sure to put code directly under the '#{disasm_method}' method."
+ )
+ iseq_insns = iseq.last
+
+ expected_insns = Set.new(insns)
+ iseq_insns.each do
+ next unless it.is_a?(Array)
+ expected_insns.delete(it.first)
+ end
+ assert(expected_insns.empty?, -> { "Not present in ISeq: #{expected_insns.to_a}" })
+ end
end
# Run a Ruby process with ZJIT options and a pipe for writing test results
- def eval_with_jit(script, call_threshold: 1, num_profiles: 1, timeout: 1000, pipe_fd:, debug: true)
- args = [
- "--disable-gems",
- "--zjit-call-threshold=#{call_threshold}",
- "--zjit-num-profiles=#{num_profiles}",
- ]
- args << "--zjit-debug" if debug
+ def eval_with_jit(
+ script,
+ call_threshold: 1,
+ num_profiles: 1,
+ zjit: true,
+ stats: false,
+ debug: true,
+ allowed_iseqs: nil,
+ extra_args: nil,
+ timeout: 1000,
+ pipe_fd: nil
+ )
+ args = ["--disable-gems", *extra_args]
+ if zjit
+ args << "--zjit-call-threshold=#{call_threshold}"
+ args << "--zjit-num-profiles=#{num_profiles}"
+ case stats
+ when true
+ args << "--zjit-stats"
+ when :quiet
+ args << "--zjit-stats-quiet"
+ else
+ args << "--zjit-stats=#{stats}" if stats
+ end
+ args << "--zjit-debug" if debug
+ if allowed_iseqs
+ jitlist = Tempfile.new("jitlist")
+ jitlist.write(allowed_iseqs)
+ jitlist.close
+ args << "--zjit-allowed-iseqs=#{jitlist.path}"
+ end
+ end
args << "-e" << script_shell_encode(script)
- pipe_r, pipe_w = IO.pipe
- # Separate thread so we don't deadlock when
- # the child ruby blocks writing the output to pipe_fd
- pipe_out = nil
- pipe_reader = Thread.new do
- pipe_out = pipe_r.read
- pipe_r.close
+ ios = {}
+ if pipe_fd
+ pipe_r, pipe_w = IO.pipe
+ # Separate thread so we don't deadlock when
+ # the child ruby blocks writing the output to pipe_fd
+ pipe_out = nil
+ pipe_reader = Thread.new do
+ pipe_out = pipe_r.read
+ pipe_r.close
+ end
+ ios[pipe_fd] = pipe_w
+ end
+ result = EnvUtil.invoke_ruby(args, '', true, true, rubybin: RbConfig.ruby, timeout: timeout, ios:)
+ if pipe_fd
+ pipe_w.close
+ pipe_reader.join(timeout)
+ result << pipe_out
end
- out, err, status = EnvUtil.invoke_ruby(args, '', true, true, rubybin: RbConfig.ruby, timeout: timeout, ios: { pipe_fd => pipe_w })
- pipe_w.close
- pipe_reader.join(timeout)
- [status, out, err, pipe_out]
+ result
ensure
pipe_reader&.kill
pipe_reader&.join(timeout)
pipe_r&.close
pipe_w&.close
+ jitlist&.unlink
+ end
+
+ def assert_success(out, err, status)
+ message = "exited with status #{status.to_i}"
+ message << "\nstdout:\n```\n#{out}```\n" unless out.empty?
+ message << "\nstderr:\n```\n#{err}```\n" unless err.empty?
+ assert status.success?, message
end
def script_shell_encode(s)