@@ -826,7 +826,100 @@ def foo
826826 end
827827
828828 def test_code_gc
829- assert_compiles ( <<~'RUBY' , exits : :any , result : :ok )
829+ assert_compiles ( code_gc_helpers + <<~'RUBY' , exits : :any , result : :ok )
830+ return :not_paged unless add_pages(100) # prepare freeable pages
831+ code_gc # first code GC
832+ return :not_compiled1 unless compiles { nil } # should be JITable again
833+
834+ code_gc # second code GC
835+ return :not_compiled2 unless compiles { nil } # should be JITable again
836+
837+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
838+ return :"code_gc_#{code_gc_count}" if code_gc_count && code_gc_count != 2
839+
840+ :ok
841+ RUBY
842+ end
843+
844+ def test_on_stack_code_gc_call
845+ assert_compiles ( code_gc_helpers + <<~'RUBY' , exits : :any , result : :ok )
846+ fiber = Fiber.new {
847+ # Loop to call the same basic block again after Fiber.yield
848+ while true
849+ Fiber.yield(nil.to_i)
850+ end
851+ }
852+
853+ return :not_paged1 unless add_pages(400) # go to a page without initial ocb code
854+ return :broken_resume1 if fiber.resume != 0 # JIT the fiber
855+ code_gc # first code GC, which should not free the fiber page
856+ return :broken_resume2 if fiber.resume != 0 # The code should be still callable
857+
858+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
859+ return :"code_gc_#{code_gc_count}" if code_gc_count && code_gc_count != 1
860+
861+ :ok
862+ RUBY
863+ end
864+
865+ def test_on_stack_code_gc_twice
866+ assert_compiles ( code_gc_helpers + <<~'RUBY' , exits : :any , result : :ok )
867+ fiber = Fiber.new {
868+ # Loop to call the same basic block again after Fiber.yield
869+ while Fiber.yield(nil.to_i); end
870+ }
871+
872+ return :not_paged1 unless add_pages(400) # go to a page without initial ocb code
873+ return :broken_resume1 if fiber.resume(true) != 0 # JIT the fiber
874+ code_gc # first code GC, which should not free the fiber page
875+
876+ return :not_paged2 unless add_pages(300) # add some stuff to be freed
877+ # Not calling fiber.resume here to test the case that the YJIT payload loses some
878+ # informatio n at the previous code GC. The payload should still be there, and
879+ # thus we could know the fiber ISEQ is still on stack on this second code GC.
880+ code_gc # second code GC, which should still not free the fiber page
881+
882+ return :not_paged3 unless add_pages(200) # attempt to overwrite the fiber page (it shouldn't)
883+ return :broken_resume2 if fiber.resume(true) != 0 # The fiber code should be still fine
884+
885+ return :broken_resume3 if fiber.resume(false) != nil # terminate the fiber
886+ code_gc # third code GC, freeing a page that used to be on stack
887+
888+ return :not_paged4 unless add_pages(100) # check everything still works
889+
890+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
891+ return :"code_gc_#{code_gc_count}" if code_gc_count && code_gc_count != 3
892+
893+ :ok
894+ RUBY
895+ end
896+
897+ def test_code_gc_with_many_iseqs
898+ assert_compiles ( code_gc_helpers + <<~'RUBY' , exits : :any , result : :ok , mem_size : 1 )
899+ fiber = Fiber.new {
900+ # Loop to call the same basic block again after Fiber.yield
901+ while true
902+ Fiber.yield(nil.to_i)
903+ end
904+ }
905+
906+ return :not_paged1 unless add_pages(500) # use some pages
907+ return :broken_resume1 if fiber.resume != 0 # leave an on-stack code as well
908+
909+ return :not_gc if add_pages(2000) # use a whole lot of pages to run out of 1MiB
910+ return :broken_resume2 if fiber.resume != 0 # on-stack code should be callable
911+
912+ code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
913+ return :"code_gc_#{code_gc_count}" if code_gc_count && code_gc_count == 0
914+
915+ :ok
916+ RUBY
917+ end
918+
919+ private
920+
921+ def code_gc_helpers
922+ <<~'RUBY'
830923 def compiles(&block)
831924 failures = RubyVM::YJIT.runtime_stats[:compilation_failure]
832925 block.call
@@ -835,37 +928,23 @@ def compiles(&block)
835928
836929 def add_pages(num_jits)
837930 pages = RubyVM::YJIT.runtime_stats[:compiled_page_count]
838- 100 .times { return false unless eval('compiles { nil.to_i }') }
931+ num_jits .times { return false unless eval('compiles { nil.to_i }') }
839932 pages.nil? || pages < RubyVM::YJIT.runtime_stats[:compiled_page_count]
840933 end
841934
842935 def code_gc
843936 RubyVM::YJIT.simulate_oom! # bump write_pos
844937 eval('proc { nil }.call') # trigger code GC
845938 end
846-
847- return :not_paged unless add_pages(100) # prepare freeable pages
848- code_gc # first code GC
849- return :not_compiled1 unless compiles { nil } # should be JITable again
850-
851- code_gc # second code GC
852- return :not_compiled2 unless compiles { nil } # should be JITable again
853-
854- code_gc_count = RubyVM::YJIT.runtime_stats[:code_gc_count]
855- return :"code_gc_#{code_gc_count}" if code_gc_count && code_gc_count != 2
856-
857- :ok
858939 RUBY
859940 end
860941
861- private
862-
863942 def assert_no_exits ( script )
864943 assert_compiles ( script )
865944 end
866945
867946 ANY = Object . new
868- def assert_compiles ( test_script , insns : [ ] , call_threshold : 1 , stdout : nil , exits : { } , result : ANY , frozen_string_literal : nil )
947+ def assert_compiles ( test_script , insns : [ ] , call_threshold : 1 , stdout : nil , exits : { } , result : ANY , frozen_string_literal : nil , mem_size : nil )
869948 reset_stats = <<~RUBY
870949 RubyVM::YJIT.runtime_stats
871950 RubyVM::YJIT.reset_stats!
@@ -899,7 +978,7 @@ def collect_insns(iseq)
899978 #{ write_results }
900979 RUBY
901980
902- status , out , err , stats = eval_with_jit ( script , call_threshold : call_threshold )
981+ status , out , err , stats = eval_with_jit ( script , call_threshold :, mem_size : )
903982
904983 assert status . success? , "exited with status #{ status . to_i } , stderr:\n #{ err } "
905984
@@ -953,12 +1032,13 @@ def script_shell_encode(s)
9531032 s . chars . map { |c | c . ascii_only? ? c : "\\ u%x" % c . codepoints [ 0 ] } . join
9541033 end
9551034
956- def eval_with_jit ( script , call_threshold : 1 , timeout : 1000 )
1035+ def eval_with_jit ( script , call_threshold : 1 , timeout : 1000 , mem_size : nil )
9571036 args = [
9581037 "--disable-gems" ,
9591038 "--yjit-call-threshold=#{ call_threshold } " ,
9601039 "--yjit-stats"
9611040 ]
1041+ args << "--yjit-exec-mem-size=#{ mem_size } " if mem_size
9621042 args << "-e" << script_shell_encode ( script )
9631043 stats_r , stats_w = IO . pipe
9641044 out , err , status = EnvUtil . invoke_ruby ( args ,
0 commit comments