diff --git a/.github/workflows/annocheck.yml b/.github/workflows/annocheck.yml index a7363e3a9ab39f..2c1d35899fb722 100644 --- a/.github/workflows/annocheck.yml +++ b/.github/workflows/annocheck.yml @@ -73,7 +73,7 @@ jobs: builddir: build makeup: true - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/auto_review_pr.yml b/.github/workflows/auto_review_pr.yml index bcbdec54dae635..72611269a40eca 100644 --- a/.github/workflows/auto_review_pr.yml +++ b/.github/workflows/auto_review_pr.yml @@ -29,7 +29,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/baseruby.yml b/.github/workflows/baseruby.yml index 27f98d2d50479a..695c468153be36 100644 --- a/.github/workflows/baseruby.yml +++ b/.github/workflows/baseruby.yml @@ -48,7 +48,7 @@ jobs: - ruby-3.3 steps: - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml index fe485d2bfbfa10..464a44dfceb9dd 100644 --- a/.github/workflows/bundled_gems.yml +++ b/.github/workflows/bundled_gems.yml @@ -38,7 +38,7 @@ jobs: with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: 4.0 diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index 6bd21df6faa1aa..56eea4001fd274 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -42,7 +42,7 @@ jobs: - uses: ./.github/actions/setup/directories - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 0879d72b24ed39..55238ffba504d9 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -23,7 +23,7 @@ jobs: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: head diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 75b0c7f896a69d..537432bd407920 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -78,14 +78,14 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: languages: ${{ matrix.language }} build-mode: none config-file: .github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: category: '/language:${{ matrix.language }}' upload: False @@ -127,7 +127,7 @@ jobs: continue-on-error: true - name: Upload SARIF - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: sarif_file: sarif-results/${{ matrix.language }}.sarif continue-on-error: true diff --git a/.github/workflows/dependabot_automerge.yml b/.github/workflows/dependabot_automerge.yml index de408c97cbeb47..b331f888279bc0 100644 --- a/.github/workflows/dependabot_automerge.yml +++ b/.github/workflows/dependabot_automerge.yml @@ -17,7 +17,7 @@ jobs: id: metadata - name: Wait for status checks - uses: lewagon/wait-on-check-action@96d9100b431964d10e0136aff8b9ccb92470505e # v1.8.0 + uses: lewagon/wait-on-check-action@1d57e2c51a58d812d2765e036a028b6bdb5a6154 # v1.8.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/modgc.yml b/.github/workflows/modgc.yml index 8dce73ab16134f..b0fe1c827f51a8 100644 --- a/.github/workflows/modgc.yml +++ b/.github/workflows/modgc.yml @@ -67,7 +67,7 @@ jobs: uses: ./.github/actions/setup/ubuntu if: ${{ contains(matrix.os, 'ubuntu') }} - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/parse_y.yml b/.github/workflows/parse_y.yml index 026fb28c9fe30b..0e51c852335355 100644 --- a/.github/workflows/parse_y.yml +++ b/.github/workflows/parse_y.yml @@ -59,7 +59,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8dc18ca9f472d5..bea2aba1012685 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,7 +22,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: 3.3.4 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 86a39eb0ab0d89..e63a22e5d262b9 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: sarif_file: results.sarif diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index 8b4137f935404c..bf6b6f1d0155ec 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -49,7 +49,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml index f4f176b356c341..98957ab01acb29 100644 --- a/.github/workflows/sync_default_gems.yml +++ b/.github/workflows/sync_default_gems.yml @@ -39,7 +39,7 @@ jobs: with: token: ${{ github.repository == 'ruby/ruby' && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/tarball-ubuntu.yml b/.github/workflows/tarball-ubuntu.yml index 932ed4a8298b05..d0d4e5ba575d66 100644 --- a/.github/workflows/tarball-ubuntu.yml +++ b/.github/workflows/tarball-ubuntu.yml @@ -43,7 +43,7 @@ jobs: set -x sudo apt-get update -q sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev zlib1g-dev libffi-dev libgmp-dev bison- autoconf- - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.2' # test-bundled-gems requires executable host ruby diff --git a/.github/workflows/tarball-windows.yml b/.github/workflows/tarball-windows.yml index c13ba7b0d0cc45..85fe1c073f199a 100644 --- a/.github/workflows/tarball-windows.yml +++ b/.github/workflows/tarball-windows.yml @@ -47,7 +47,7 @@ jobs: - run: md build working-directory: - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.2' bundler: none diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 1b0371bb5f314d..6518e77d72b07b 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -79,7 +79,7 @@ jobs: with: arch: ${{ matrix.arch }} - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 458676137a79e2..75344395bb4155 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -65,7 +65,7 @@ jobs: sparse-checkout: /.github persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 90e2a93ebe37d2..9a9c2be282a000 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -58,7 +58,7 @@ jobs: - run: md build working-directory: - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: # windows-11-arm has only 3.4.1, 3.4.2, 3.4.3, head ruby-version: ${{ !endsWith(matrix.os, 'arm') && '3.1' || '3.4' }} diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml index 04740c03037ff3..793f93d6d10f44 100644 --- a/.github/workflows/yjit-ubuntu.yml +++ b/.github/workflows/yjit-ubuntu.yml @@ -138,7 +138,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 7dd8e252b79235..a38ad2f0ce5b7d 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -136,7 +136,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/common.mk b/common.mk index 29fd0f5bcf6655..07ece19ee98373 100644 --- a/common.mk +++ b/common.mk @@ -1716,10 +1716,13 @@ yes-test-bundler-parallel: $(PREPARE_BUNDLER) $(gnumake_recursive)$(XRUBY) \ -r./$(arch)-fake \ -r$(tooldir)/lib/_tmpdir \ + -r$(tooldir)/lib/bundler_runtime_grouping \ -I$(srcdir)/spec/bundler \ -e "ruby = ENV['RUBY']" \ -e "ARGV[-1] = File.expand_path(ARGV[-1])" \ -e "ENV['RSPEC_EXECUTABLE'] = ruby + ARGV.shift" \ + -e "require 'support/setup'" \ + -e "BundlerRuntimeGrouping.install!" \ -e "load ARGV.shift" \ -s -- -no-report-tmpdir -- \ " -C $(srcdir) -Ispec/bundler -Ispec/lib .bundle/bin/rspec -r spec_helper" \ diff --git a/configure.ac b/configure.ac index 8d020771c6d505..562d9262f837f1 100644 --- a/configure.ac +++ b/configure.ac @@ -3820,9 +3820,6 @@ AS_CASE(["${enable_dtrace}"], ], [ rb_cv_dtrace_available=no ]) -AS_CASE(["$target_os"],[freebsd*],[ - rb_cv_dtrace_available=no - ]) AS_IF([test "${enable_dtrace}" = yes], [dnl AS_IF([test -z "$DTRACE"], [dnl AC_MSG_ERROR([dtrace(1) is missing]) diff --git a/object.c b/object.c index cbafd558f9fc9d..0552e7cf44019a 100644 --- a/object.c +++ b/object.c @@ -453,13 +453,13 @@ rb_immutable_obj_clone(int argc, VALUE *argv, VALUE obj) VALUE rb_get_freeze_opt(int argc, VALUE *argv) { - static ID keyword_ids[1]; + /* idFreeze (== :freeze) is preinterned before any Ruby code runs, so use it + * directly instead of lazily initializing a shared static, which races when + * Ractors run this concurrently. */ + const ID keyword_ids[1] = { idFreeze }; VALUE opt; VALUE kwfreeze = Qnil; - if (!keyword_ids[0]) { - CONST_ID(keyword_ids[0], "freeze"); - } rb_scan_args(argc, argv, "0:", &opt); if (!NIL_P(opt)) { rb_get_kwargs(opt, keyword_ids, 0, 1, &kwfreeze); @@ -478,6 +478,29 @@ immutable_obj_clone(VALUE obj, VALUE kwfreeze) return obj; } +/* Cache of the `{freeze: true/false}` keyword hash passed to #initialize_clone. + * Ractors may reach this concurrently, so build a fully populated, frozen and + * pinned hash locally and publish it with a single atomic CAS: any value another + * thread can observe in the static is already complete, and a builder that loses + * the CAS just discards its hash. (The old lazy init published an empty hash that + * a second thread could read and freeze before the first finished filling it.) */ +static VALUE freeze_true_hash, freeze_false_hash; + +static VALUE +clone_freeze_kwarg_hash(VALUE *cache, VALUE freeze_value) +{ + VALUE h = RUBY_ATOMIC_VALUE_LOAD(*cache); + if (!h) { + h = rb_hash_new(); + rb_hash_aset(h, ID2SYM(idFreeze), freeze_value); + rb_obj_freeze(h); + rb_vm_register_global_object(h); /* pin before publishing */ + VALUE prev = RUBY_ATOMIC_VALUE_CAS(*cache, 0, h); + if (prev) h = prev; /* lost the race; our h becomes garbage */ + } + return h; +} + VALUE rb_obj_clone_setup(VALUE obj, VALUE clone, VALUE kwfreeze) { @@ -506,31 +529,15 @@ rb_obj_clone_setup(VALUE obj, VALUE clone, VALUE kwfreeze) } break; case Qtrue: { - static VALUE freeze_true_hash; - if (!freeze_true_hash) { - freeze_true_hash = rb_hash_new(); - rb_vm_register_global_object(freeze_true_hash); - rb_hash_aset(freeze_true_hash, ID2SYM(idFreeze), Qtrue); - rb_obj_freeze(freeze_true_hash); - } - argv[0] = obj; - argv[1] = freeze_true_hash; + argv[1] = clone_freeze_kwarg_hash(&freeze_true_hash, Qtrue); rb_funcallv_kw(clone, id_init_clone, 2, argv, RB_PASS_KEYWORDS); OBJ_FREEZE(clone); break; } case Qfalse: { - static VALUE freeze_false_hash; - if (!freeze_false_hash) { - freeze_false_hash = rb_hash_new(); - rb_vm_register_global_object(freeze_false_hash); - rb_hash_aset(freeze_false_hash, ID2SYM(idFreeze), Qfalse); - rb_obj_freeze(freeze_false_hash); - } - argv[0] = obj; - argv[1] = freeze_false_hash; + argv[1] = clone_freeze_kwarg_hash(&freeze_false_hash, Qfalse); rb_funcallv_kw(clone, id_init_clone, 2, argv, RB_PASS_KEYWORDS); break; } diff --git a/pathname_builtin.rb b/pathname_builtin.rb index beaff62de2acfa..ade839e2bc94bf 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1895,7 +1895,26 @@ def pipe?() FileTest.pipe?(@path) end # See FileTest.socket?. def socket?() FileTest.socket?(@path) end - # See FileTest.owned?. + # :markup: markdown + # + # call-seq: + # owned? -> true or false + # + # Returns whether the entry at the path represented by `self` + # exists and is owned by the user of the current process: + # + # ```ruby + # pn = Pathname('doc/t.tmp') + # pn.write('foo') + # pn.owned? # => true + # pn.delete + # pn = Pathname('doc/tmp') + # pn.mkdir + # pn.owned? # => true + # pn.rmdir + # Pathname('/etc').owned? # => false + # ``` + # def owned?() FileTest.owned?(@path) end # See FileTest.readable?. diff --git a/ractor_sync.c b/ractor_sync.c index 5eeb6e20db9f28..5dfb86e3bff130 100644 --- a/ractor_sync.c +++ b/ractor_sync.c @@ -144,13 +144,27 @@ static VALUE ractor_port_closed_p(rb_execution_context_t *ec, VALUE self) { const struct ractor_port *rp = RACTOR_PORT_PTR(self); + rb_ractor_t *r = rp->r; + bool closed; - if (ractor_closed_port_p(ec, rp->r, rp)) { - return Qtrue; + if (rb_ec_ractor_ptr(ec) == r) { + /* The owner's threads are serialized by the ractor GVL, so the ports + * table can't change under this lookup. */ + closed = ractor_closed_port_p(ec, r, rp); } else { - return Qfalse; + /* A foreign Ractor races the owner's st_insert/st_delete on the ports + * table; take the lock like every other foreign reader. ractor_closed_port_p + * asserts the lock is held for foreign access, and Port#closed? was the + * only path reaching it without the lock. */ + RACTOR_LOCK(r); + { + closed = ractor_closed_port_p(ec, r, rp); + } + RACTOR_UNLOCK(r); } + + return closed ? Qtrue : Qfalse; } static VALUE diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index 24b0e71020ccdd..27ddc6a77125a2 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -175,26 +175,6 @@ def self.ruby=(ruby) reset! end - # Opt-in per-example runtime log (set BUNDLER_SPEC_RUNTIME_LOG to a file path). - # Each parallel worker appends one "\t" line per example to its - # own "." file (a single shared file would hit Windows - # cross-process sharing violations), so the heaviest specs can be found after a - # full run. turbo_tests only writes its own --runtime-log when invoked with the - # bare "spec" path, which the build never does, so it produces no log otherwise. - if (runtime_log = ENV["BUNDLER_SPEC_RUNTIME_LOG"]) - worker = ENV["TEST_ENV_NUMBER"].to_s - worker = "1" if worker.empty? - runtime_log = "#{runtime_log}.#{worker}" - config.before(:each) { @__runtime_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) } - config.after(:each) do |example| - next unless @__runtime_start - dt = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @__runtime_start - File.write(runtime_log, "#{format("%.4f", dt)}\t#{example.metadata[:file_path]}\n", mode: "a") - rescue StandardError - # never let runtime logging break a test run - end - end - Spec::Shards::EXAMPLE_MAPPINGS.each do |tag, file_paths| file_pattern = Regexp.union(file_paths.map {|path| Regexp.new(Regexp.escape(path) + "$") }) diff --git a/thread.c b/thread.c index 2a89582a274e15..22f0fe8e0f73de 100644 --- a/thread.c +++ b/thread.c @@ -680,6 +680,13 @@ thread_start_func_2(rb_thread_t *th, VALUE *stack_start) r->r_stdin = rb_io_prep_stdin(); r->r_stdout = rb_io_prep_stdout(); r->r_stderr = rb_io_prep_stderr(); + + /* Build the interrupt queue and mask stack here, on the new Ractor's + * own main thread, instead of carrying over the ones the creating + * thread made. The mask stack starts empty so a new Ractor does not + * inherit the creating thread's Thread.handle_interrupt state. */ + th->pending_interrupt_queue = rb_ary_hidden_new(0); + th->pending_interrupt_mask_stack = rb_ary_hidden_new(0); } RB_VM_UNLOCK(); } @@ -845,7 +852,12 @@ thread_create_core(VALUE thval, struct thread_create_params *params) "can't start a new thread (frozen ThreadGroup)"); } - rb_fiber_inherit_storage(ec, th->ec->fiber_ptr); + /* A new Ractor must not inherit the creating thread's fiber storage: its + * entries may be objects owned by the creating Ractor. Only threads created + * within the same Ractor inherit it. */ + if (params->type != thread_invoke_type_ractor_proc) { + rb_fiber_inherit_storage(ec, th->ec->fiber_ptr); + } switch (params->type) { case thread_invoke_type_proc: @@ -882,10 +894,19 @@ thread_create_core(VALUE thval, struct thread_create_params *params) th->priority = current_th->priority; th->thgroup = current_th->thgroup; - th->pending_interrupt_queue = rb_ary_hidden_new(0); - th->pending_interrupt_queue_checked = 0; - th->pending_interrupt_mask_stack = rb_ary_dup(current_th->pending_interrupt_mask_stack); - RBASIC_CLEAR_CLASS(th->pending_interrupt_mask_stack); + if (th->invoke_type == thread_invoke_type_ractor_proc) { + /* A new Ractor's main thread builds these on start + * (thread_start_func_2); leave them unset until then. */ + th->pending_interrupt_queue = 0; + th->pending_interrupt_mask_stack = 0; + th->pending_interrupt_queue_checked = 0; + } + else { + th->pending_interrupt_queue = rb_ary_hidden_new(0); + th->pending_interrupt_queue_checked = 0; + th->pending_interrupt_mask_stack = rb_ary_dup(current_th->pending_interrupt_mask_stack); + RBASIC_CLEAR_CLASS(th->pending_interrupt_mask_stack); + } rb_native_mutex_initialize(&th->interrupt_lock); diff --git a/tool/lib/bundler_runtime_grouping.rb b/tool/lib/bundler_runtime_grouping.rb new file mode 100644 index 00000000000000..c1c55810146e76 --- /dev/null +++ b/tool/lib/bundler_runtime_grouping.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# Distribute the parallel bundler-spec workers by measured per-file runtime +# instead of file size, with no change to turbo_tests or parallel_tests and +# without touching the synced spec/bin/parallel_rspec. The test-bundler-parallel +# recipe requires this file and calls install! just before loading the stock +# parallel_rspec; install! prepends the grouper (parent) and appends the recorder +# to RSPEC_EXECUTABLE so each worker records (see bundler_runtime_recorder.rb). +# The grouping/recording logic lives under tool/, which sync-default-gems never +# copies, so the rubygems side stays untouched. +# +# turbo_tests only feeds a runtime log to the grouper when invoked with the bare +# "spec" path (TurboTests::CLI passes files: ["spec"] only when given no +# positional arg), which the build never does: the recipe passes an absolute +# spec/bundler path, so grouping falls back to file size and the single heaviest +# file becomes the long pole. We reopen the one grouper hook that size path uses, +# ParallelTests::RSpec::Runner.sort_by_filesize, and return runtime weights when +# the previous run left us data. Its argument is the already-expanded file list, +# so nothing here re-globs or re-reads it. + +module BundlerRuntimeGrouping + WORKER_LOGS = ".[0-9]*" + + module_function + + # Per-worker log base. Under the (gitignored) source tmp so it survives between + # runs; overridable via BUNDLER_SPEC_RUNTIME_LOG. + def log_base + ENV["BUNDLER_SPEC_RUNTIME_LOG"] || + File.expand_path("../../tmp/bundler_runtime_rspec.log", __dir__) + end + + # Append one "\t" line for a finished example to this worker's + # own log ("."); a single shared file would hit Windows + # cross-process sharing violations. Called from the worker (see recorder). + def record(example, seconds) + worker = ENV["TEST_ENV_NUMBER"].to_s + worker = "1" if worker.empty? + File.write("#{log_base}.#{worker}", + "#{format("%.4f", seconds)}\t#{example.metadata[:file_path]}\n", mode: "a") + rescue StandardError + # never let runtime logging break a test run + end + + # {canonical_path => seconds} summed across the previous run's worker logs, + # memoized before this run overwrites them. Empty on the first run. + def runtimes + @runtimes ||= Dir.glob(log_base + WORKER_LOGS).each_with_object(Hash.new(0.0)) do |path, sums| + File.foreach(path) do |line| + seconds, tab, file = line.chomp.partition("\t") + sums[canonical(file)] += seconds.to_f unless tab.empty? + end + rescue StandardError + sums + end + end + + # cwd-independent key: from "spec/bundler/" on, forward slashes. Recorded paths + # are rspec-relative ("./spec/bundler/..."); grouped paths are absolute. + def canonical(path) + normalized = path.tr("\\", "/") + i = normalized.index("spec/bundler/") + i ? normalized[i..] : normalized + end + + # Called once in the parent before parallel_rspec loads (spec/bundler/support + # must already be required so parallel_tests is on the load path). Reads the + # previous run, clears the logs, prepends the grouper, and arranges for each + # worker to record by appending the recorder to RSPEC_EXECUTABLE. + def install! + require "fileutils" + require "parallel_tests/rspec/runner" + FileUtils.mkdir_p(File.dirname(log_base)) + runtimes # capture the previous run before clearing + Dir.glob(log_base + WORKER_LOGS).each { |f| File.delete(f) rescue nil } + ParallelTests::RSpec::Runner.singleton_class.prepend(SortHook) + if ENV["RSPEC_EXECUTABLE"] + recorder = File.expand_path("bundler_runtime_recorder.rb", __dir__) + ENV["RSPEC_EXECUTABLE"] = "#{ENV["RSPEC_EXECUTABLE"]} -r#{recorder}" + end + rescue StandardError, LoadError => e + warn "parallel_rspec: runtime-based grouping disabled (#{e.class}: #{e.message})" + end + + module SortHook + def sort_by_filesize(tests) + rt = BundlerRuntimeGrouping.runtimes + return super if rt.empty? + tests.sort! + known = tests.map { |t| rt[BundlerRuntimeGrouping.canonical(t)] }.select(&:positive?) + return super unless known.size * 1.5 > tests.size # parallel_tests' own threshold + average = known.sum / known.size + tests.map! do |t| + seconds = rt[BundlerRuntimeGrouping.canonical(t)] + [t, seconds.positive? ? seconds : average] + end + end + end +end diff --git a/tool/lib/bundler_runtime_recorder.rb b/tool/lib/bundler_runtime_recorder.rb new file mode 100644 index 00000000000000..1c5b110beeb7e0 --- /dev/null +++ b/tool/lib/bundler_runtime_recorder.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Loaded into each parallel worker (via -r appended to RSPEC_EXECUTABLE by +# BundlerRuntimeGrouping.install!) to record per-example runtimes that the +# next run groups by. Kept in tool/ so recording does not depend on +# spec/bundler/spec_helper.rb (which is synced from rubygems/rubygems). + +require_relative "bundler_runtime_grouping" + +RSpec.configure do |config| + config.before(:each) { @__runtime_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) } + config.after(:each) do |example| + next unless @__runtime_start + BundlerRuntimeGrouping.record(example, Process.clock_gettime(Process::CLOCK_MONOTONIC) - @__runtime_start) + end +end diff --git a/vm.c b/vm.c index a5ab2a153013e1..c30e750d9fee00 100644 --- a/vm.c +++ b/vm.c @@ -2292,7 +2292,7 @@ short ruby_vm_redefined_flag[BOP_LAST_]; static st_table *vm_opt_method_def_table = 0; static st_table *vm_opt_mid_table = 0; -void +static void rb_free_vm_opt_tables(void) { st_free_table(vm_opt_method_def_table);