diff options
| -rw-r--r-- | .github/actions/make-snapshot/action.yml | 6 | ||||
| -rw-r--r-- | .github/workflows/tarball-macos.yml | 70 | ||||
| -rw-r--r-- | .github/workflows/tarball-test-schedule.yml | 2 | ||||
| -rw-r--r-- | .github/workflows/tarball-test.yml | 13 | ||||
| -rw-r--r-- | .github/workflows/tarball-ubuntu.yml | 174 | ||||
| -rw-r--r-- | .github/workflows/tarball-windows.yml | 38 | ||||
| -rw-r--r-- | bootstraptest/test_ractor.rb | 21 | ||||
| -rw-r--r-- | common.mk | 4 | ||||
| -rw-r--r-- | ext/strscan/strscan.c | 8 | ||||
| -rw-r--r-- | gc.c | 28 | ||||
| -rw-r--r-- | gc/default/default.c | 275 | ||||
| -rw-r--r-- | gc/gc.h | 30 | ||||
| -rw-r--r-- | imemo.c | 4 | ||||
| -rw-r--r-- | pathname_builtin.rb | 126 | ||||
| -rw-r--r-- | ruby_atomic.h | 40 | ||||
| -rw-r--r-- | template/configure-ext.mk.tmpl | 27 | ||||
| -rw-r--r-- | test/fiber/test_thread.rb | 6 | ||||
| -rw-r--r-- | test/ruby/test_defined.rb | 28 | ||||
| -rw-r--r-- | vm_callinfo.h | 2 | ||||
| -rw-r--r-- | vm_insnhelper.c | 10 | ||||
| -rw-r--r-- | vm_method.c | 2 | ||||
| -rw-r--r-- | yjit/src/cruby_bindings.inc.rs | 3 | ||||
| -rw-r--r-- | zjit/src/cruby_bindings.inc.rs | 3 | ||||
| -rw-r--r-- | zjit/src/hir.rs | 41 | ||||
| -rw-r--r-- | zjit/src/profile.rs | 15 |
25 files changed, 616 insertions, 360 deletions
diff --git a/.github/actions/make-snapshot/action.yml b/.github/actions/make-snapshot/action.yml index f8d26dd989..4552f0e067 100644 --- a/.github/actions/make-snapshot/action.yml +++ b/.github/actions/make-snapshot/action.yml @@ -18,6 +18,10 @@ inputs: description: 'srcdir for tool/make-snapshot. Empty = clone ruby/ruby into ./ruby.' required: false default: '' + upload-artifact: + description: 'Upload Packages and Info as workflow artifacts. Pass "false" when callers run in a matrix that would collide on artifact names.' + required: false + default: 'true' runs: using: "composite" @@ -65,7 +69,9 @@ runs: with: name: Packages path: pkg + if: ${{ inputs.upload-artifact == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: Info path: pkg/info + if: ${{ inputs.upload-artifact == 'true' }} diff --git a/.github/workflows/tarball-macos.yml b/.github/workflows/tarball-macos.yml index f0bb22f0c0..e1d4d4ba2a 100644 --- a/.github/workflows/tarball-macos.yml +++ b/.github/workflows/tarball-macos.yml @@ -7,18 +7,8 @@ on: description: 'archname (e.g. snapshot-master, snapshot-ruby_3_3)' required: true type: string - allow-failures: - description: 'TEST_BUNDLED_GEMS_ALLOW_FAILURES value' - required: false - type: string - default: '' - patch-url: - description: 'Patch URL forwarded from workflow_dispatch' - required: false - type: string - default: '' - rebuild-homebrew-ruby: - description: 'Uninstall and reinstall Homebrew Ruby on macos-15-intel' + notify-release-channel: + description: 'Also send failure notifications to SNAPSHOT_SLACK_WEBHOOK_URL (schedule/release builds).' required: false type: boolean default: false @@ -35,7 +25,7 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} env: - TEST_BUNDLED_GEMS_ALLOW_FAILURES: ${{ inputs.allow-failures }} + ARCHNAME: ${{ inputs.archname }} steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -43,16 +33,6 @@ jobs: path: pkg - name: Extract run: tar xf pkg/*.tar.xz - - name: Apply patch - run: | - set -x - curl -sSL "${RUBY_PATCH_URL}" -o ruby.patch - cd "${{ inputs.archname }}/" - git apply ../ruby.patch - shell: bash - env: - RUBY_PATCH_URL: ${{ inputs.patch-url }} - if: inputs.patch-url != '' - name: Install libraries run: | with_retry () { @@ -60,45 +40,51 @@ jobs: } set -x with_retry brew install gmp libffi openssl@1.1 zlib autoconf automake libtool readline libyaml - - name: Uninstall Homebrew Ruby - run: | - for formula in $(brew list 2>/dev/null | grep '^ruby'); do - brew uninstall --force "$formula" - done - sudo rm -rf /usr/local/lib/ruby - if: inputs.rebuild-homebrew-ruby && matrix.os == 'macos-15-intel' - name: Set ENV run: | echo "JOBS=-j$((1 + $(sysctl -n hw.activecpu)))" >> $GITHUB_ENV - name: configure - run: cd "${{ inputs.archname }}/" && ./configure --with-openssl-dir=$(brew --prefix openssl@1.1) --with-readline-dir=$(brew --prefix readline) --with-libyaml-dir=$(brew --prefix libyaml) + run: cd "$ARCHNAME/" && ./configure --with-openssl-dir=$(brew --prefix openssl@1.1) --with-readline-dir=$(brew --prefix readline) --with-libyaml-dir=$(brew --prefix libyaml) - name: make - run: cd "${{ inputs.archname }}/" && make $JOBS - - name: Reinstall Homebrew Ruby - run: brew install ruby - if: inputs.rebuild-homebrew-ruby && matrix.os == 'macos-15-intel' && (matrix.test_task == 'test-bundled-gems' || matrix.test_task == 'test-bundler-parallel') + run: cd "$ARCHNAME/" && make $JOBS - name: Tests - run: cd "${{ inputs.archname }}/" && make $JOBS -s ${{ matrix.test_task }} + run: cd "$ARCHNAME/" && make $JOBS -s ${{ matrix.test_task }} env: RUBY_TESTOPTS: "-q --tty=no" RUBY_DEBUG_TEST_NO_REMOTE: "1" # leaked-globals since 2.7 - name: Leaked Globals - run: cd "${{ inputs.archname }}/" && make -s leaked-globals + run: cd "$ARCHNAME/" && make -s leaked-globals if: matrix.test_task == 'check' - name: make install without root privilege - run: cd "${{ inputs.archname }}/" && make $JOBS install DESTDIR="/tmp/destdir" + run: cd "$ARCHNAME/" && make $JOBS install DESTDIR="/tmp/destdir" if: matrix.test_task == 'check' - name: make install - run: cd "${{ inputs.archname }}/" && sudo make $JOBS install + run: cd "$ARCHNAME/" && sudo make $JOBS install if: matrix.test_task == 'check' - - name: ruby -v - run: /usr/local/bin/ruby -v + - name: Verify installed binaries + run: | + /usr/local/bin/ruby -v + /usr/local/bin/gem -v + /usr/local/bin/bundle -v if: matrix.test_task == 'check' - uses: ruby/action-slack@54175162371f1f7c8eb94d7c8644ee2479fcd375 # v3.2.2 with: payload: | { + "ci": "GitHub Actions", + "env": "snapshot: ${{ matrix.os }} / ${{ matrix.test_task }}", + "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "commit": "${{ github.sha }}", + "branch": "${{ github.ref_name }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} + if: failure() + - uses: ruby/action-slack@54175162371f1f7c8eb94d7c8644ee2479fcd375 # v3.2.2 + with: + payload: | + { "attachments": [{ "text": "${{ job.status }}: ${{ matrix.os }} / ${{ matrix.test_task }} <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.workflow }}>", "color": "danger" @@ -106,4 +92,4 @@ jobs: } env: SLACK_WEBHOOK_URL: ${{ secrets.SNAPSHOT_SLACK_WEBHOOK_URL }} - if: failure() && github.event_name == 'schedule' + if: failure() && inputs.notify-release-channel diff --git a/.github/workflows/tarball-test-schedule.yml b/.github/workflows/tarball-test-schedule.yml index 225aeed633..abf80edf81 100644 --- a/.github/workflows/tarball-test-schedule.yml +++ b/.github/workflows/tarball-test-schedule.yml @@ -19,7 +19,7 @@ jobs: - ruby_3_3 steps: - name: Trigger tarball-test on ${{ matrix.branch }} - run: gh workflow run tarball-test.yml --ref "$BRANCH" --repo "$GITHUB_REPOSITORY" + run: gh workflow run tarball-test.yml --ref "$BRANCH" --repo "$GITHUB_REPOSITORY" -f notify-release-channel=true env: BRANCH: ${{ matrix.branch }} GH_TOKEN: ${{ secrets.MATZBOT_GITHUB_ACTION_TOKEN }} diff --git a/.github/workflows/tarball-test.yml b/.github/workflows/tarball-test.yml index 5b06b466fe..db49977cbf 100644 --- a/.github/workflows/tarball-test.yml +++ b/.github/workflows/tarball-test.yml @@ -13,6 +13,11 @@ on: # https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks merge_group: workflow_dispatch: + inputs: + notify-release-channel: + description: 'Also send failure notifications to SNAPSHOT_SLACK_WEBHOOK_URL (set by tarball-test-schedule).' + type: boolean + default: false concurrency: group: ${{ github.workflow }} / ${{ startsWith(github.event_name, 'pull') && github.ref_name || github.sha }} @@ -53,10 +58,7 @@ jobs: uses: ./.github/workflows/tarball-ubuntu.yml with: archname: snapshot-${{ needs.tarball.outputs.branch }} - allow-failures: power_assert - remove-gnupg: true - notify-ruby-sha: true - branch-label: ${{ needs.tarball.outputs.branch }} + notify-release-channel: ${{ github.event_name == 'workflow_dispatch' && inputs.notify-release-channel || false }} secrets: inherit macos: @@ -64,7 +66,7 @@ jobs: uses: ./.github/workflows/tarball-macos.yml with: archname: snapshot-${{ needs.tarball.outputs.branch }} - allow-failures: power_assert + notify-release-channel: ${{ github.event_name == 'workflow_dispatch' && inputs.notify-release-channel || false }} secrets: inherit windows: @@ -72,6 +74,7 @@ jobs: uses: ./.github/workflows/tarball-windows.yml with: archname: snapshot-${{ needs.tarball.outputs.branch }} + notify-release-channel: ${{ github.event_name == 'workflow_dispatch' && inputs.notify-release-channel || false }} secrets: inherit non_development: diff --git a/.github/workflows/tarball-ubuntu.yml b/.github/workflows/tarball-ubuntu.yml index d1c7a9bb78..4e4b2cf6d0 100644 --- a/.github/workflows/tarball-ubuntu.yml +++ b/.github/workflows/tarball-ubuntu.yml @@ -7,51 +7,11 @@ on: description: 'archname (e.g. snapshot-master, snapshot-ruby_3_3)' required: true type: string - allow-failures: - description: 'TEST_BUNDLED_GEMS_ALLOW_FAILURES value' - required: false - type: string - default: '' - patch-url: - description: 'Patch URL forwarded from workflow_dispatch' - required: false - type: string - default: '' - apt-mode: - description: '"none" | "git-only" | "ruby-and-git" — controls per-test_task apt install handling' - required: false - type: string - default: 'none' - setup-host-ruby: - description: 'Install Ruby 3.2 via ruby/setup-ruby for test-bundled-gems' - required: false - type: boolean - default: true - remove-gnupg: - description: 'Forcibly remove ~/.gnupg after tests' + notify-release-channel: + description: 'Also send failure notifications to SNAPSHOT_SLACK_WEBHOOK_URL (schedule/release builds).' required: false type: boolean default: false - fixed-dirs-extra: - description: 'Create $HOME/.local/share, $HOME/.ssh on top of minimal fixed dirs' - required: false - type: boolean - default: true - ruby-bin: - description: 'Path to ruby for post-install version check' - required: false - type: string - default: '/usr/local/bin/ruby' - notify-ruby-sha: - description: 'Post ruby SHA failure notification to SIMPLER_ALERTS_URL' - required: false - type: boolean - default: false - branch-label: - description: 'Branch label for SIMPLER_ALERTS_URL notification. Defaults to "master".' - required: false - type: string - default: 'master' jobs: ubuntu: @@ -62,7 +22,7 @@ jobs: fail-fast: false runs-on: ${{ matrix.os }} env: - TEST_BUNDLED_GEMS_ALLOW_FAILURES: ${{ inputs.allow-failures }} + ARCHNAME: ${{ inputs.archname }} steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -70,97 +30,39 @@ jobs: path: pkg - name: Extract run: tar xf pkg/*.tar.xz - - name: Apply patch - run: | - set -x - curl -sSL "${RUBY_PATCH_URL}" -o ruby.patch - cd "${{ inputs.archname }}/" - git apply ../ruby.patch - shell: bash - env: - RUBY_PATCH_URL: ${{ inputs.patch-url }} - if: inputs.patch-url != '' - - name: Install libraries (apt-mode=none) + - name: Install libraries run: | set -x sudo apt-get update -q sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev bison- autoconf- - if: inputs.apt-mode == 'none' - - name: Install libraries (apt-mode=git-only) - run: | - set -x - sudo apt-get update -q || : - # postfix `-` means `uninstall` - APT_INSTALL_GIT=git- - case "${{ matrix.test_task }}" in - test-bundled-gems) - # test-bundled-gems-fetch requires git - unset APT_INSTALL_GIT - ;; - test-bundler*) - # avoid Bundler::Source::Git::GitNotInstalledError - unset APT_INSTALL_GIT - ;; - *) - ;; - esac - sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev bison- autoconf- $APT_INSTALL_GIT - if: inputs.apt-mode == 'git-only' - - name: Install libraries (apt-mode=ruby-and-git) - run: | - set -x - sudo apt-get update -q || : - # postfix `-` means `uninstall` - APT_INSTALL_RUBY=ruby- - APT_INSTALL_GIT=git- - case "${{ matrix.test_task }}" in - test-bundled-gems) - # test-bundled-gems requires executable host ruby - APT_INSTALL_RUBY=ruby - # test-bundled-gems-fetch requires git - unset APT_INSTALL_GIT - ;; - test-bundler*) - # avoid Bundler::Source::Git::GitNotInstalledError - unset APT_INSTALL_GIT - ;; - *) - ;; - esac - sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev bison- autoconf- $APT_INSTALL_RUBY $APT_INSTALL_GIT - if: inputs.apt-mode == 'ruby-and-git' - uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0 with: ruby-version: 3.2 # test-bundled-gems requires executable host ruby - if: inputs.setup-host-ruby && matrix.test_task == 'test-bundled-gems' + if: matrix.test_task == 'test-bundled-gems' - name: Fixed world writable dirs run: | - mkdir -p $HOME/.local + mkdir -p $HOME/.local/share mkdir -p $HOME/.cache/gem/specs mkdir -p $HOME/.bundle/cache + mkdir -p $HOME/.ssh chmod a-w $HOME/.bundle # chmod a-w $HOME # allow to write $HOME and check stats of HOME around tests (see below) chmod -v a-w $HOME/.config sudo chmod -R a-w /usr/share sudo bash -c 'IFS=:; for d in '"$PATH"'; do chmod -v a-w $d; done' || : - - name: Fixed world writable dirs (extra) - run: | - mkdir -p $HOME/.local/share - mkdir -p $HOME/.ssh - if: inputs.fixed-dirs-extra - name: Set ENV run: | echo "JOBS=-j$((1 + $(nproc --all)))" >> $GITHUB_ENV - name: configure - run: cd "${{ inputs.archname }}/" && ./configure + run: cd "$ARCHNAME/" && ./configure - name: make - run: cd "${{ inputs.archname }}/" && make $JOBS + run: cd "$ARCHNAME/" && make $JOBS - name: Save stats of HOME run: | set -euxo pipefail - cd "${{ inputs.archname }}/" + cd "$ARCHNAME/" cat >test.rb <<'EOF' require 'pathname' require 'digest' @@ -187,18 +89,17 @@ jobs: make runruby rm -f test.rb - name: Tests - run: cd "${{ inputs.archname }}/" && make $JOBS -s ${{ matrix.test_task }} + run: cd "$ARCHNAME/" && make $JOBS -s ${{ matrix.test_task }} env: RUBY_TESTOPTS: "-q --tty=no" - # not sure why ~/.gnupg is remained - # see tool/test/test_sync_default_gems.rb - - name: Forcibly remove test directory + # test_sync_default_gems triggers gpg, whose agent processes leave + # $HOME/.gnupg around even when GNUPGHOME points elsewhere. + - name: Forcibly remove ~/.gnupg run: rm -rf $HOME/.gnupg - if: inputs.remove-gnupg - name: Diff stats of HOME run: | set -euxo pipefail - cd "${{ inputs.archname }}/" + cd "$ARCHNAME/" cat >test.rb <<'EOF' require 'pathname' require 'digest' @@ -227,18 +128,19 @@ jobs: diff -u /tmp/stat-before-tests.txt /tmp/stat-after-tests.txt # leaked-globals since 2.7 - name: Leaked Globals - run: cd "${{ inputs.archname }}/" && make -s leaked-globals + run: cd "$ARCHNAME/" && make -s leaked-globals if: matrix.test_task == 'check' - name: make install without root privilege - run: cd "${{ inputs.archname }}/" && make $JOBS install DESTDIR="/tmp/destdir" + run: cd "$ARCHNAME/" && make $JOBS install DESTDIR="/tmp/destdir" if: matrix.test_task == 'check' - name: make install - run: cd "${{ inputs.archname }}/" && sudo make $JOBS install + run: cd "$ARCHNAME/" && sudo make $JOBS install if: matrix.test_task == 'check' - - name: ruby -v - env: - RUBY_BIN: ${{ inputs.ruby-bin }} - run: '"$RUBY_BIN" -v' + - name: Verify installed binaries + run: | + /usr/local/bin/ruby -v + /usr/local/bin/gem -v + /usr/local/bin/bundle -v if: matrix.test_task == 'check' - name: Show .local run: find $HOME/.local -ls @@ -246,28 +148,24 @@ jobs: with: payload: | { - "attachments": [{ - "text": "${{ job.status }}: ${{ matrix.os }} / ${{ matrix.test_task }} <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.workflow }}>", - "color": "danger" - }] + "ci": "GitHub Actions", + "env": "snapshot: ${{ matrix.os }} / ${{ matrix.test_task }}", + "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "commit": "${{ github.sha }}", + "branch": "${{ github.ref_name }}" } env: - SLACK_WEBHOOK_URL: ${{ secrets.SNAPSHOT_SLACK_WEBHOOK_URL }} - if: failure() && github.event_name == 'schedule' - - name: Get ruby/ruby sha - id: ruby_sha - run: cd "${{ inputs.archname }}/" && ./ruby -e 'puts "sha=#{RUBY_REVISION}"' >> $GITHUB_OUTPUT - if: failure() && inputs.notify-ruby-sha && github.event_name == 'schedule' + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} + if: failure() - uses: ruby/action-slack@54175162371f1f7c8eb94d7c8644ee2479fcd375 # v3.2.2 with: payload: | { - "ci": "GitHub Actions", - "env": "snapshot: ${{ matrix.os }} / ${{ matrix.test_task }}", - "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", - "commit": "${{ steps.ruby_sha.outputs.sha }}", - "branch": "${{ inputs.branch-label }}" + "attachments": [{ + "text": "${{ job.status }}: ${{ matrix.os }} / ${{ matrix.test_task }} <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.workflow }}>", + "color": "danger" + }] } env: - SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} - if: failure() && inputs.notify-ruby-sha && github.event_name == 'schedule' + SLACK_WEBHOOK_URL: ${{ secrets.SNAPSHOT_SLACK_WEBHOOK_URL }} + if: failure() && inputs.notify-release-channel diff --git a/.github/workflows/tarball-windows.yml b/.github/workflows/tarball-windows.yml index 52a28f006e..fb29253eae 100644 --- a/.github/workflows/tarball-windows.yml +++ b/.github/workflows/tarball-windows.yml @@ -7,11 +7,11 @@ on: description: 'archname (e.g. snapshot-master)' required: true type: string - patch-url: - description: 'Patch URL forwarded from workflow_dispatch' + notify-release-channel: + description: 'Also send failure notifications to SNAPSHOT_SLACK_WEBHOOK_URL (schedule/release builds).' required: false - type: string - default: '' + type: boolean + default: false jobs: windows: @@ -73,18 +73,6 @@ jobs: - name: Extract run: 7z x pkg/*.zip working-directory: - - name: Apply patch - run: | - set -x - curl -sSL "${RUBY_PATCH_URL}" -o ruby.patch - cd "$ARCHNAME" - git apply ../ruby.patch - shell: bash - env: - RUBY_PATCH_URL: ${{ inputs.patch-url }} - ARCHNAME: ${{ inputs.archname }} - if: inputs.patch-url != '' - working-directory: - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: @@ -131,8 +119,7 @@ jobs: YACC: win_bison - name: ruby -v - run: | - .\ruby -v + run: .\ruby -v - run: nmake test timeout-minutes: 5 @@ -147,6 +134,19 @@ jobs: with: payload: | { + "ci": "GitHub Actions", + "env": "snapshot: ${{ env.OS_VER }} / ${{ matrix.test_task }}", + "url": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}", + "commit": "${{ github.sha }}", + "branch": "${{ github.ref_name }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SIMPLER_ALERTS_URL }} + if: failure() + - uses: ruby/action-slack@54175162371f1f7c8eb94d7c8644ee2479fcd375 # v3.2.2 + with: + payload: | + { "attachments": [{ "text": "${{ job.status }}: ${{ env.OS_VER }} / ${{ matrix.test_task }} <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.workflow }}>", "color": "danger" @@ -154,4 +154,4 @@ jobs: } env: SLACK_WEBHOOK_URL: ${{ secrets.SNAPSHOT_SLACK_WEBHOOK_URL }} - if: failure() && github.event_name == 'schedule' + if: failure() && inputs.notify-release-channel diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index b40c908f6f..4fe90703fc 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -2643,3 +2643,24 @@ rescue ThreadError => e end end RUBY + +# Concurrent super calls with keyword arguments must not race on the +# callinfo kwarg reference count. [Bug #22075] +assert_equal 'ok', %q{ + class Base + def foo(a:, b:, c:) = a + end + + class Sub < Base + def foo(a:, b:, c:) = super(a: a, b: b, c: c) + end + + 4.times.map do + Ractor.new do + obj = Sub.new + 100_000.times { obj.foo(a: 1, b: 2, c: 3) } + end + end.each(&:join) + + :ok +} @@ -69,6 +69,7 @@ LIBRUBY_EXTS = ./.libruby-with-ext.time REVISION_H = ./.revision.time PLATFORM_D = $(TIMESTAMPDIR)/.$(PLATFORM_DIR).time ENC_TRANS_D = $(TIMESTAMPDIR)/.enc-trans.time +yes_cross_compiling = $(CROSS_COMPILING:no=) X_$(CROSS_COMPILING:yes=)BASERUBY = $(BASERUBY) X_$(CROSS_COMPILING:no=)BASERUBY = $(XRUBY) RDOC = $(X_BASERUBY) --enable-gems "$(tooldir)/rdoc-srcdir" @@ -352,7 +353,8 @@ ext/configure-ext.mk: $(PREP) all-incs $(MKFILES) $(RBCONFIG) $(LIBRUBY) \ $(Q)$(MAKEDIRS) $(@D) $(Q)$(MINIRUBY) $(tooldir)/generic_erb.rb -o $@ -c \ $(srcdir)/template/$(@F).tmpl --srcdir="$(srcdir)" \ - --miniruby="$(MINIRUBY)" --script-args='$(SCRIPT_ARGS)' + --miniruby="$(MINIRUBY)" --script-args='$(SCRIPT_ARGS)' \ + $(yes_cross_compiling:yes=--without-ext=-test-) configure-ext: $(EXTS_MK) diff --git a/ext/strscan/strscan.c b/ext/strscan/strscan.c index 7e21a21f7e..936fb0f81b 100644 --- a/ext/strscan/strscan.c +++ b/ext/strscan/strscan.c @@ -822,7 +822,7 @@ strscan_scan(VALUE self, VALUE re) * :include: strscan/link_refs.txt * * call-seq: - * match?(pattern) -> updated_position or nil + * match?(pattern) -> match_size or nil * * Attempts to [match][17] the given `pattern` * at the beginning of the [target substring][3]; @@ -882,7 +882,7 @@ strscan_match_p(VALUE self, VALUE re) /* * :markup: markdown * call-seq: - * skip(pattern) match_size or nil + * skip(pattern) -> match_size or nil * * :include: strscan/link_refs.txt * :include: strscan/methods/skip.md @@ -956,7 +956,7 @@ strscan_check(VALUE self, VALUE re) /* * call-seq: - * scan_full(pattern, advance_pointer_p, return_string_p) -> matched_substring or nil + * scan_full(pattern, advance_pointer_p, return_string_p) -> matched_substring or length or nil * * Equivalent to one of the following: * @@ -1202,7 +1202,7 @@ strscan_getch(VALUE self) /* * call-seq: - * scan_byte -> integer_byte + * scan_byte -> integer_byte or nil * * Scans one byte and returns it as an integer. * This method is not multibyte character sensitive. @@ -3927,10 +3927,34 @@ rb_gc_update_tbl_refs(st_table *ptr) gc_update_table_refs(ptr); } +static int +rb_gc_update_set_refs_i(st_data_t key, st_data_t value, st_data_t argp, int error) +{ + if (rb_gc_location((VALUE)key) != (VALUE)key) { + return ST_REPLACE; + } + + return ST_CONTINUE; +} + +static int +rb_gc_update_set_refs_replace_i(st_data_t *key, st_data_t *value, st_data_t argp, int existing) +{ + if (rb_gc_location((VALUE)*key) != (VALUE)*key) { + *key = rb_gc_location((VALUE)*key); + } + + return ST_CONTINUE; +} + void -rb_gc_update_set_refs(st_table *ptr) +rb_gc_update_set_refs(st_table *tbl) { - gc_update_set_refs(ptr); + if (!tbl || tbl->num_entries == 0) return; + + if (st_foreach_with_replace(tbl, rb_gc_update_set_refs_i, rb_gc_update_set_refs_replace_i, 0)) { + rb_raise(rb_eRuntimeError, "hash modified during iteration"); + } } static void diff --git a/gc/default/default.c b/gc/default/default.c index e47c31d08d..291ff91b81 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -23,6 +23,7 @@ #include "ruby/ruby.h" #include "ruby/atomic.h" +#include "ruby_atomic.h" #include "ruby/debug.h" #include "ruby/thread.h" #include "ruby/util.h" @@ -196,18 +197,6 @@ static RB_THREAD_LOCAL_SPECIFIER int malloc_increase_local; SLOT(32) SLOT(64) SLOT(128) SLOT(256) SLOT(512) #endif -/* Precomputed reciprocals for fast slot index calculation. - * For slot size d: reciprocal = ceil(2^48 / d). - * Then offset / d == (uint32_t)((offset * reciprocal) >> 48) - * for all offset < HEAP_PAGE_SIZE. */ -#define SLOT_RECIPROCAL_SHIFT 48 -#define SLOT_RECIPROCAL(size) (((1ULL << SLOT_RECIPROCAL_SHIFT) + (size) - 1) / (size)) - -static const uint64_t heap_slot_reciprocal_table[HEAP_COUNT] = { -#define SLOT(size) SLOT_RECIPROCAL(size), - EACH_POOL_SLOT_SIZE(SLOT) -#undef SLOT -}; typedef struct ractor_newobj_heap_cache { struct free_slot *freelist; struct heap_page *using_page; @@ -507,11 +496,30 @@ enum gc_mode { gc_mode_compacting, }; +typedef rbimpl_atomic_uint64_t gc_counter_t; + +#if !defined(HAVE_GCC_ATOMIC_BUILTINS_64) && !defined(_WIN32) && \ + !(defined(__sun) && defined(HAVE_ATOMIC_H) && (defined(_LP64) || defined(_I32LPx))) +# define MALLOC_COUNTERS_NEED_LOCK 1 +#endif + +struct gc_malloc_bytes { + gc_counter_t malloc; + gc_counter_t free; + + /* Snapshots of `malloc` / `free` taken at the end of the last GC */ + gc_counter_t malloc_at_last_gc; + gc_counter_t free_at_last_gc; +}; + typedef struct rb_objspace { struct { - size_t increase; + struct gc_malloc_bytes counters; #if RGENGC_ESTIMATE_OLDMALLOC - size_t oldmalloc_increase; + struct gc_malloc_bytes oldcounters; +#endif +#ifdef MALLOC_COUNTERS_NEED_LOCK + rb_nativethread_lock_t lock; #endif } malloc_counters; @@ -711,11 +719,23 @@ size_t rb_gc_impl_obj_slot_size(VALUE obj); #define RVALUE_SLOT_SIZE (sizeof(struct RBasic) + sizeof(VALUE[RBIMPL_RVALUE_EMBED_LEN_MAX]) + RVALUE_OVERHEAD) static const size_t pool_slot_sizes[HEAP_COUNT] = { -#define SLOT(size) size, +#define SLOT(size) ((size) + RVALUE_OVERHEAD), EACH_POOL_SLOT_SIZE(SLOT) #undef SLOT }; +/* Precomputed reciprocals for fast slot index calculation. + * For slot size d: reciprocal = ceil(2^48 / d). + * Then offset / d == (uint32_t)((offset * reciprocal) >> 48) + * for all offset < HEAP_PAGE_SIZE. */ +#define SLOT_RECIPROCAL_SHIFT 48 +#define SLOT_RECIPROCAL(size) (((1ULL << SLOT_RECIPROCAL_SHIFT) + (size) - 1) / (size)) + +static const uint64_t heap_slot_reciprocal_table[HEAP_COUNT] = { +#define SLOT(size) SLOT_RECIPROCAL((size) + RVALUE_OVERHEAD), + EACH_POOL_SLOT_SIZE(SLOT) +#undef SLOT +}; #if SIZEOF_VALUE >= 8 static uint8_t size_to_heap_idx[1024 / 8 + 1]; @@ -943,8 +963,112 @@ RVALUE_AGE_SET(VALUE obj, int age) } #define malloc_limit objspace->malloc_params.limit -#define malloc_increase objspace->malloc_counters.increase +#define malloc_increase gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.counters) #define malloc_allocated_size objspace->malloc_params.allocated_size + +#ifdef MALLOC_COUNTERS_NEED_LOCK +# define MALLOC_COUNTERS_LOCK(o) rb_native_mutex_lock(&(o)->malloc_counters.lock) +# define MALLOC_COUNTERS_UNLOCK(o) rb_native_mutex_unlock(&(o)->malloc_counters.lock) +#else +# define MALLOC_COUNTERS_LOCK(o) ((void)0) +# define MALLOC_COUNTERS_UNLOCK(o) ((void)0) +#endif + +static inline void +gc_counter_add(gc_counter_t *p, size_t delta) +{ +#ifdef MALLOC_COUNTERS_NEED_LOCK + *p += (gc_counter_t)delta; +#else + rbimpl_atomic_u64_fetch_add_relaxed(p, (uint64_t)delta); +#endif +} + +static inline gc_counter_t +gc_counter_load_relaxed(const gc_counter_t *p) +{ +#ifdef MALLOC_COUNTERS_NEED_LOCK + return *p; +#else + return rbimpl_atomic_u64_load_relaxed(p); +#endif +} + +static inline gc_counter_t +gc_counter_load_acquire(const gc_counter_t *p) +{ +#ifdef MALLOC_COUNTERS_NEED_LOCK + return *p; +#else + return rbimpl_atomic_u64_load_acquire(p); +#endif +} + +static inline void +gc_counter_store_release(gc_counter_t *p, gc_counter_t v) +{ +#ifdef MALLOC_COUNTERS_NEED_LOCK + *p = v; +#else + rbimpl_atomic_u64_set_release(p, v); +#endif +} + +static inline int64_t +gc_malloc_counters_increase(rb_objspace_t *objspace, const struct gc_malloc_bytes *c) +{ + MALLOC_COUNTERS_LOCK(objspace); + gc_counter_t malloc_at = gc_counter_load_acquire(&c->malloc_at_last_gc); + gc_counter_t free_at = gc_counter_load_acquire(&c->free_at_last_gc); + gc_counter_t malloc_now = gc_counter_load_relaxed(&c->malloc); + gc_counter_t free_now = gc_counter_load_relaxed(&c->free); + MALLOC_COUNTERS_UNLOCK(objspace); + + gc_counter_t malloc_delta = malloc_now - malloc_at; + gc_counter_t free_delta = free_now - free_at; + + if (malloc_delta >= free_delta) { + return (int64_t)(malloc_delta - free_delta); + } + else { + return -(int64_t)(free_delta - malloc_delta); + } +} + +static inline size_t +gc_malloc_counters_increase_unsigned(rb_objspace_t *objspace, const struct gc_malloc_bytes *c) +{ + int64_t inc = gc_malloc_counters_increase(objspace, c); + if (inc <= 0) return 0; +#if SIZEOF_SIZE_T < 8 + if ((uint64_t)inc > SIZE_MAX) return SIZE_MAX; +#endif + return (size_t)inc; +} + +static inline int64_t +gc_malloc_counters_snapshot(rb_objspace_t *objspace, struct gc_malloc_bytes *c) +{ + MALLOC_COUNTERS_LOCK(objspace); + gc_counter_t malloc_now = gc_counter_load_relaxed(&c->malloc); + gc_counter_t free_now = gc_counter_load_relaxed(&c->free); + gc_counter_t malloc_at = gc_counter_load_relaxed(&c->malloc_at_last_gc); + gc_counter_t free_at = gc_counter_load_relaxed(&c->free_at_last_gc); + gc_counter_store_release(&c->malloc_at_last_gc, malloc_now); + gc_counter_store_release(&c->free_at_last_gc, free_now); + MALLOC_COUNTERS_UNLOCK(objspace); + + gc_counter_t malloc_delta = malloc_now - malloc_at; + gc_counter_t free_delta = free_now - free_at; + + if (malloc_delta >= free_delta) { + return (int64_t)(malloc_delta - free_delta); + } + else { + return -(int64_t)(free_delta - malloc_delta); + } +} + #define heap_pages_lomem objspace->heap_pages.range[0] #define heap_pages_himem objspace->heap_pages.range[1] #define heap_pages_freeable_pages objspace->heap_pages.freeable_pages @@ -3897,6 +4021,13 @@ gc_sweep_finish(rb_objspace_t *objspace) } } + (void)gc_malloc_counters_snapshot(objspace, &objspace->malloc_counters.counters); +#if RGENGC_ESTIMATE_OLDMALLOC + if (objspace->profile.latest_gc_info & GPR_FLAG_MAJOR_MASK) { + (void)gc_malloc_counters_snapshot(objspace, &objspace->malloc_counters.oldcounters); + } +#endif + rb_gc_event_hook(0, RUBY_INTERNAL_EVENT_GC_END_SWEEP); gc_mode_transition(objspace, gc_mode_none); @@ -4924,10 +5055,22 @@ gc_check_after_marks_i(st_data_t k, st_data_t v, st_data_t ptr) static void gc_marks_check(rb_objspace_t *objspace, st_foreach_callback_func *checker_func, const char *checker_name) { - size_t saved_malloc_increase = objspace->malloc_params.increase; + MALLOC_COUNTERS_LOCK(objspace); + struct gc_malloc_bytes saved_malloc = { + .malloc = gc_counter_load_relaxed(&objspace->malloc_counters.counters.malloc), + .free = gc_counter_load_relaxed(&objspace->malloc_counters.counters.free), + .malloc_at_last_gc = gc_counter_load_relaxed(&objspace->malloc_counters.counters.malloc_at_last_gc), + .free_at_last_gc = gc_counter_load_relaxed(&objspace->malloc_counters.counters.free_at_last_gc), + }; #if RGENGC_ESTIMATE_OLDMALLOC - size_t saved_oldmalloc_increase = objspace->malloc_counters.oldmalloc_increase; + struct gc_malloc_bytes saved_oldmalloc = { + .malloc = gc_counter_load_relaxed(&objspace->malloc_counters.oldcounters.malloc), + .free = gc_counter_load_relaxed(&objspace->malloc_counters.oldcounters.free), + .malloc_at_last_gc = gc_counter_load_relaxed(&objspace->malloc_counters.oldcounters.malloc_at_last_gc), + .free_at_last_gc = gc_counter_load_relaxed(&objspace->malloc_counters.oldcounters.free_at_last_gc), + }; #endif + MALLOC_COUNTERS_UNLOCK(objspace); VALUE already_disabled = rb_objspace_gc_disable(objspace); objspace->rgengc.allrefs_table = objspace_allrefs(objspace); @@ -4947,10 +5090,18 @@ gc_marks_check(rb_objspace_t *objspace, st_foreach_callback_func *checker_func, objspace->rgengc.allrefs_table = 0; if (already_disabled == Qfalse) rb_objspace_gc_enable(objspace); - objspace->malloc_params.increase = saved_malloc_increase; + MALLOC_COUNTERS_LOCK(objspace); + gc_counter_store_release(&objspace->malloc_counters.counters.malloc, saved_malloc.malloc); + gc_counter_store_release(&objspace->malloc_counters.counters.free, saved_malloc.free); + gc_counter_store_release(&objspace->malloc_counters.counters.malloc_at_last_gc, saved_malloc.malloc_at_last_gc); + gc_counter_store_release(&objspace->malloc_counters.counters.free_at_last_gc, saved_malloc.free_at_last_gc); #if RGENGC_ESTIMATE_OLDMALLOC - objspace->malloc_counters.oldmalloc_increase = saved_oldmalloc_increase; + gc_counter_store_release(&objspace->malloc_counters.oldcounters.malloc, saved_oldmalloc.malloc); + gc_counter_store_release(&objspace->malloc_counters.oldcounters.free, saved_oldmalloc.free); + gc_counter_store_release(&objspace->malloc_counters.oldcounters.malloc_at_last_gc, saved_oldmalloc.malloc_at_last_gc); + gc_counter_store_release(&objspace->malloc_counters.oldcounters.free_at_last_gc, saved_oldmalloc.free_at_last_gc); #endif + MALLOC_COUNTERS_UNLOCK(objspace); } #endif /* RGENGC_CHECK_MODE >= 4 */ @@ -6310,11 +6461,14 @@ gc_reset_malloc_info(rb_objspace_t *objspace, bool full_mark) { gc_prof_set_malloc_info(objspace); { - size_t inc = RUBY_ATOMIC_SIZE_EXCHANGE(malloc_increase, 0); + int64_t inc = gc_malloc_counters_increase(objspace, &objspace->malloc_counters.counters); size_t old_limit = malloc_limit; - if (inc > malloc_limit) { - malloc_limit = (size_t)(inc * gc_params.malloc_limit_growth_factor); + /* A net-negative `inc` (more freed than malloc'd since last GC) is + * treated the same as "allocated less than malloc_limit". + * This matches what we were doing pre-monotonic counters, but is it right? */ + if (inc > 0 && (size_t)inc > malloc_limit) { + malloc_limit = (size_t)((size_t)inc * gc_params.malloc_limit_growth_factor); if (malloc_limit > gc_params.malloc_limit_max) { malloc_limit = gc_params.malloc_limit_max; } @@ -6341,7 +6495,11 @@ gc_reset_malloc_info(rb_objspace_t *objspace, bool full_mark) /* reset oldmalloc info */ #if RGENGC_ESTIMATE_OLDMALLOC if (!full_mark) { - if (objspace->malloc_counters.oldmalloc_increase > objspace->rgengc.oldmalloc_increase_limit) { + /* Don't snapshot on minor GC: oldmalloc_increase is meant to + * accumulate across minor GCs and only reset at major GC. */ + int64_t oldmalloc_increase = gc_malloc_counters_increase(objspace, &objspace->malloc_counters.oldcounters); + if (oldmalloc_increase > 0 && + (uint64_t)oldmalloc_increase > objspace->rgengc.oldmalloc_increase_limit) { gc_needs_major_flags |= GPR_FLAG_MAJOR_BY_OLDMALLOC; objspace->rgengc.oldmalloc_increase_limit = (size_t)(objspace->rgengc.oldmalloc_increase_limit * gc_params.oldmalloc_limit_growth_factor); @@ -6351,17 +6509,14 @@ gc_reset_malloc_info(rb_objspace_t *objspace, bool full_mark) } } - if (0) fprintf(stderr, "%"PRIdSIZE"\t%d\t%"PRIuSIZE"\t%"PRIuSIZE"\t%"PRIdSIZE"\n", + if (0) fprintf(stderr, "%"PRIdSIZE"\t%d\t%"PRId64"\t%"PRIuSIZE"\t%"PRIdSIZE"\n", rb_gc_count(), gc_needs_major_flags, - objspace->malloc_counters.oldmalloc_increase, + oldmalloc_increase, objspace->rgengc.oldmalloc_increase_limit, gc_params.oldmalloc_limit_max); } else { - /* major GC */ - objspace->malloc_counters.oldmalloc_increase = 0; - if ((objspace->profile.latest_gc_info & GPR_FLAG_MAJOR_BY_OLDMALLOC) == 0) { objspace->rgengc.oldmalloc_increase_limit = (size_t)(objspace->rgengc.oldmalloc_increase_limit / ((gc_params.oldmalloc_limit_growth_factor - 1)/10 + 1)); @@ -7460,6 +7615,8 @@ enum gc_stat_sym { gc_stat_sym_total_freed_pages, gc_stat_sym_total_allocated_objects, gc_stat_sym_total_freed_objects, + gc_stat_sym_total_malloc_bytes, + gc_stat_sym_total_free_bytes, gc_stat_sym_malloc_increase_bytes, gc_stat_sym_malloc_increase_bytes_limit, gc_stat_sym_minor_gc_count, @@ -7510,6 +7667,8 @@ setup_gc_stat_symbols(void) S(total_freed_pages); S(total_allocated_objects); S(total_freed_objects); + S(total_malloc_bytes); + S(total_free_bytes); S(malloc_increase_bytes); S(malloc_increase_bytes_limit); S(minor_gc_count); @@ -7571,27 +7730,32 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) return SIZET2NUM(attr); \ else if (hash != Qnil) \ rb_hash_aset(hash, gc_stat_symbols[gc_stat_sym_##name], SIZET2NUM(attr)); +#define SET64(name, attr) \ + if (key == gc_stat_symbols[gc_stat_sym_##name]) \ + return ULL2NUM(attr); \ + else if (hash != Qnil) \ + rb_hash_aset(hash, gc_stat_symbols[gc_stat_sym_##name], ULL2NUM(attr)); SET(count, objspace->profile.count); SET(time, (size_t)ns_to_ms(objspace->profile.marking_time_ns + objspace->profile.sweeping_time_ns)); // TODO: UINT64T2NUM SET(marking_time, (size_t)ns_to_ms(objspace->profile.marking_time_ns)); SET(sweeping_time, (size_t)ns_to_ms(objspace->profile.sweeping_time_ns)); - /* implementation dependent counters */ + { + uint64_t total_malloc = (uint64_t)gc_counter_load_relaxed(&objspace->malloc_counters.counters.malloc); + uint64_t total_free = (uint64_t)gc_counter_load_relaxed(&objspace->malloc_counters.counters.free); + SET64(total_malloc_bytes, total_malloc); + SET64(total_free_bytes, total_free); + } + + /* implementation dependent counters (small / fixnum-safe) */ SET(heap_allocated_pages, rb_darray_size(objspace->heap_pages.sorted)); SET(heap_empty_pages, objspace->empty_pages_count) SET(heap_allocatable_bytes, objspace->heap_pages.allocatable_bytes); - SET(heap_available_slots, objspace_available_slots(objspace)); - SET(heap_live_slots, objspace_live_slots(objspace)); - SET(heap_free_slots, objspace_free_slots(objspace)); - SET(heap_final_slots, total_final_slots_count(objspace)); - SET(heap_marked_slots, objspace->marked_slots); SET(heap_eden_pages, heap_eden_total_pages(objspace)); SET(total_allocated_pages, objspace->heap_pages.allocated_pages); SET(total_freed_pages, objspace->heap_pages.freed_pages); - SET(total_allocated_objects, total_allocated_objects(objspace)); - SET(total_freed_objects, total_freed_objects(objspace)); - SET(malloc_increase_bytes, malloc_increase); + SET(malloc_increase_bytes, gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.counters)); SET(malloc_increase_bytes_limit, malloc_limit); SET(minor_gc_count, objspace->profile.minor_gc_count); SET(major_gc_count, objspace->profile.major_gc_count); @@ -7603,10 +7767,19 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(old_objects, objspace->rgengc.old_objects); SET(old_objects_limit, objspace->rgengc.old_objects_limit); #if RGENGC_ESTIMATE_OLDMALLOC - SET(oldmalloc_increase_bytes, objspace->malloc_counters.oldmalloc_increase); + SET(oldmalloc_increase_bytes, gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.oldcounters)); SET(oldmalloc_increase_bytes_limit, objspace->rgengc.oldmalloc_increase_limit); #endif + ractor_cache_flush_count(objspace, rb_gc_get_ractor_newobj_cache()); + SET(total_allocated_objects, total_allocated_objects(objspace)); + SET(total_freed_objects, total_freed_objects(objspace)); + SET(heap_available_slots, objspace_available_slots(objspace)); + SET(heap_live_slots, objspace_live_slots(objspace)); + SET(heap_free_slots, objspace_free_slots(objspace)); + SET(heap_final_slots, total_final_slots_count(objspace)); + SET(heap_marked_slots, objspace->marked_slots); + #if RGENGC_PROFILE SET(total_generated_normal_object_count, objspace->profile.total_generated_normal_object_count); SET(total_generated_shady_object_count, objspace->profile.total_generated_shady_object_count); @@ -7616,6 +7789,7 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(total_remembered_shady_object_count, objspace->profile.total_remembered_shady_object_count); #endif /* RGENGC_PROFILE */ #undef SET +#undef SET64 if (!NIL_P(key)) { // Matched key should return above @@ -8044,16 +8218,22 @@ static void malloc_increase_commit(rb_objspace_t *objspace, size_t new_size, size_t old_size) { if (new_size > old_size) { - RUBY_ATOMIC_SIZE_ADD(malloc_increase, new_size - old_size); + size_t delta = new_size - old_size; + MALLOC_COUNTERS_LOCK(objspace); + gc_counter_add(&objspace->malloc_counters.counters.malloc, delta); #if RGENGC_ESTIMATE_OLDMALLOC - RUBY_ATOMIC_SIZE_ADD(objspace->malloc_counters.oldmalloc_increase, new_size - old_size); + gc_counter_add(&objspace->malloc_counters.oldcounters.malloc, delta); #endif + MALLOC_COUNTERS_UNLOCK(objspace); } - else { - atomic_sub_nounderflow(&malloc_increase, old_size - new_size); + else if (old_size > new_size) { + size_t delta = old_size - new_size; + MALLOC_COUNTERS_LOCK(objspace); + gc_counter_add(&objspace->malloc_counters.counters.free, delta); #if RGENGC_ESTIMATE_OLDMALLOC - atomic_sub_nounderflow(&objspace->malloc_counters.oldmalloc_increase, old_size - new_size); + gc_counter_add(&objspace->malloc_counters.oldcounters.free, delta); #endif + MALLOC_COUNTERS_UNLOCK(objspace); } } @@ -9390,6 +9570,10 @@ rb_gc_impl_objspace_free(void *objspace_ptr) rb_darray_free_without_gc(objspace->weak_references); +#ifdef MALLOC_COUNTERS_NEED_LOCK + rb_native_mutex_destroy(&objspace->malloc_counters.lock); +#endif + free(objspace); } @@ -9520,6 +9704,9 @@ rb_gc_impl_objspace_init(void *objspace_ptr) objspace->flags.measure_gc = true; malloc_limit = gc_params.malloc_limit_min; +#ifdef MALLOC_COUNTERS_NEED_LOCK + rb_native_mutex_initialize(&objspace->malloc_counters.lock); +#endif objspace->finalize_deferred_pjob = rb_postponed_job_preregister(0, gc_finalize_deferred, objspace); if (objspace->finalize_deferred_pjob == POSTPONED_JOB_HANDLE_INVALID) { rb_bug("Could not preregister postponed job for GC"); @@ -240,36 +240,6 @@ gc_update_table_refs(st_table *tbl) } } -static int -rb_set_foreach_replace(st_data_t key, st_data_t value, st_data_t argp, int error) -{ - if (rb_gc_location((VALUE)key) != (VALUE)key) { - return ST_REPLACE; - } - - return ST_CONTINUE; -} - -static int -rb_set_replace_ref(st_data_t *key, st_data_t *value, st_data_t argp, int existing) -{ - if (rb_gc_location((VALUE)*key) != (VALUE)*key) { - *key = rb_gc_location((VALUE)*key); - } - - return ST_CONTINUE; -} - -static void -gc_update_set_refs(st_table *tbl) -{ - if (!tbl || tbl->num_entries == 0) return; - - if (st_foreach_with_replace(tbl, rb_set_foreach_replace, rb_set_replace_ref, 0)) { - rb_raise(rb_eRuntimeError, "hash modified during iteration"); - } -} - static inline size_t xmalloc2_size(const size_t count, const size_t elsize) { @@ -129,6 +129,7 @@ VALUE rb_imemo_cdhash_new(size_t size, const struct st_hash_type *type) { struct rb_imemo_cdhash *memo = IMEMO_NEW(struct rb_imemo_cdhash, imemo_cdhash, 0); + memo->tbl.num_entries = 0; st_init_existing_table_with_size(&memo->tbl, type, size); return (VALUE)memo; } @@ -648,8 +649,7 @@ rb_imemo_free(VALUE obj) const struct rb_callinfo *ci = ((const struct rb_callinfo *)obj); if (ci->kwarg) { - ((struct rb_callinfo_kwarg *)ci->kwarg)->references--; - if (ci->kwarg->references == 0) { + if (RUBY_ATOMIC_FETCH_SUB(((struct rb_callinfo_kwarg *)ci->kwarg)->references, 1) == 1) { ruby_xfree_sized((void *)ci->kwarg, rb_callinfo_kwarg_bytes(ci->kwarg->keyword_len)); } } diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 7a7befd664..51b6fd6a07 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -577,31 +577,32 @@ class Pathname nil end - # Iterates over and yields a new Pathname object - # for each element in the given path in descending order. - # - # Pathname.new('/path/to/some/file.rb').descend {|v| p v} - # #<Pathname:/> - # #<Pathname:/path> - # #<Pathname:/path/to> - # #<Pathname:/path/to/some> - # #<Pathname:/path/to/some/file.rb> - # - # Pathname.new('path/to/some/file.rb').descend {|v| p v} - # #<Pathname:path> - # #<Pathname:path/to> - # #<Pathname:path/to/some> - # #<Pathname:path/to/some/file.rb> + # :markup: markdown # - # Returns an Enumerator if no block was given. + # call-seq: + # descend {|entry| ... } -> nil + # descend -> new_enumerator # - # enum = Pathname.new("/usr/bin/ruby").descend - # # ... do stuff ... - # enum.each { |e| ... } - # # yields Pathnames /, /usr, /usr/bin, and /usr/bin/ruby. + # With a block given, yields a new pathname for each successive dirname + # in the stored path; see File.dirname: # - # It doesn't access the filesystem. + # ```ruby + # # Absolute path. + # Pathname('/path/to/some/file.rb').descend {|pn| p pn } + # #<Pathname:/> + # #<Pathname:/path> + # #<Pathname:/path/to> + # #<Pathname:/path/to/some> + # #<Pathname:/path/to/some/file.rb> + # # Relative path. + # Pathname('path/to/some/file.rb').descend {|pn| p pn } + # #<Pathname:path> + # #<Pathname:path/to> + # #<Pathname:path/to/some> + # #<Pathname:path/to/some/file.rb> + # ``` # + # With no block given, returns a new Enumerator. def descend return to_enum(__method__) unless block_given? vs = [] @@ -1034,7 +1035,8 @@ class Pathname # * File * # atime -> new_time # # Returns a new Time object containing the time of the most recent - # access (read or write) to the entry represented by +self+: + # access (read or write) to the entry represented by `self`; + # see {File System Timestamps}[rdoc-ref:file/timestamps.md]: # # # Work in a temporary directory. # require 'tmpdir' @@ -1072,7 +1074,6 @@ class Pathname # * File * # File atime: 2026-05-14 14:36:45 +0100 # Directory atime: 2026-05-14 14:36:45 +0100 # - # See {File System Timestamps}[rdoc-ref:file/timestamps.md]. def atime() File.atime(@path) end # :markup: markdown @@ -1081,7 +1082,8 @@ class Pathname # * File * # birthtime -> new_time # # Returns a new Time object containing the create time of the entry - # represented by +self+: + # represented by `self`; + # see [File System Timestamps](rdoc-ref:file/timestamps.md): # # ```ruby # # Work in a temporary directory. @@ -1122,10 +1124,67 @@ class Pathname # * File * # Directory birthtime: 2026-05-14 23:41:12 +0100 # ``` # - # See [File System Timestamps](rdoc-ref:file/timestamps.md). def birthtime() File.birthtime(@path) end - # See <tt>File.ctime</tt>. Returns last (directory entry, not file) change time. + # :markup: markdown + # + # call-seq: + # ctime -> new_time + # + # On Windows, returns the #birthtime. + # + # On other systems, + # returns a new Time object containing the time of the most recent + # metadata change to the entry represented by `self`; + # see {File System Timestamps}[rdoc-ref:file/timestamps.md]: + # + # ```ruby + # # Work in a temporary directory. + # Pathname.mktmpdir do |tmpdirpath| + # # A subdirectory therein, and its Pathname. + # dirpath = File.join(tmpdirpath, 'subdir') + # dir_pn = Pathname(dirpath) + # puts "Create directory; directory ctime established." + # dir_pn.mkdir + # puts " Directory ctime: #{dir_pn.ctime}" + # sleep(1) + # + # # A file in the subdirectory, and its Pathname. + # filepath = File.join(dirpath, 't.txt') + # file_pn = Pathname(filepath) + # puts "Create file; file ctime established; directory ctime updated." + # file_pn.write('foo') + # puts " File ctime: #{file_pn.ctime}" + # puts " Directory ctime: #{dir_pn.ctime}" + # sleep(1) + # puts "Write file; file ctime updated; directory ctime not updated." + # file_pn.write('bar') + # puts " File ctime: #{file_pn.ctime}" + # puts " Directory ctime: #{dir_pn.ctime}" + # sleep(1) + # puts "Read file; neither ctime not updated." + # file_pn.read + # puts " File ctime: #{file_pn.ctime}" + # puts " Directory ctime: #{dir_pn.ctime}" + # end + # ``` + # + # Output: + # + # ```text + # Create directory; directory ctime established. + # Directory ctime: 2026-05-20 14:05:05 -0500 + # Create file; file ctime established; directory ctime updated. + # File ctime: 2026-05-20 14:05:06 -0500 + # Directory ctime: 2026-05-20 14:05:06 -0500 + # Write file; file ctime updated; directory ctime not updated. + # File ctime: 2026-05-20 14:05:07 -0500 + # Directory ctime: 2026-05-20 14:05:06 -0500 + # Read file; neither ctime not updated. + # File ctime: 2026-05-20 14:05:07 -0500 + # Directory ctime: 2026-05-20 14:05:06 -0500 + # ``` + # def ctime() File.ctime(@path) end # See <tt>File.mtime</tt>. Returns last modification time. @@ -1430,7 +1489,20 @@ class Pathname # * FileTest * # See <tt>FileTest.grpowned?</tt>. def grpowned?() FileTest.grpowned?(@path) end - # See <tt>FileTest.directory?</tt>. + # :markup: markdown + # + # call-seq: + # directory? -> true or false + # + # Returns whether the entry represented by `self` is a directory: + # + # ```ruby + # Pathname('/etc').directory? # => true + # Pathname('lib').directory? # => true + # Pathname('README.md').directory? # => false + # Pathname('nosuch').directory? # => false + # ``` + # def directory?() FileTest.directory?(@path) end # See <tt>FileTest.file?</tt>. diff --git a/ruby_atomic.h b/ruby_atomic.h index cbcfe682ce..409b9bcfd2 100644 --- a/ruby_atomic.h +++ b/ruby_atomic.h @@ -70,4 +70,44 @@ rbimpl_atomic_u64_set_relaxed(volatile rbimpl_atomic_uint64_t *address, uint64_t } #define ATOMIC_U64_SET_RELAXED(var, val) rbimpl_atomic_u64_set_relaxed(&(var), val) +static inline uint64_t +rbimpl_atomic_u64_fetch_add_relaxed(volatile rbimpl_atomic_uint64_t *value, uint64_t addend) +{ +#if defined(HAVE_GCC_ATOMIC_BUILTINS_64) + return __atomic_fetch_add(value, addend, __ATOMIC_RELAXED); +#elif defined(_WIN32) + return (uint64_t)InterlockedExchangeAdd64((LONG64 *)value, (LONG64)addend); +#elif defined(__sun) && defined(HAVE_ATOMIC_H) && (defined(_LP64) || defined(_I32LPx)) + return atomic_add_64_nv(value, addend) - addend; +#else + // TODO: stdatomic + uint64_t prev = *value; + *value = prev + addend; + return prev; +#endif +} +#define ATOMIC_U64_FETCH_ADD_RELAXED(var, val) rbimpl_atomic_u64_fetch_add_relaxed(&(var), val) + +static inline uint64_t +rbimpl_atomic_u64_load_acquire(const volatile rbimpl_atomic_uint64_t *value) +{ +#if defined(HAVE_GCC_ATOMIC_BUILTINS_64) + return __atomic_load_n(value, __ATOMIC_ACQUIRE); +#else + return rbimpl_atomic_u64_load_relaxed(value); +#endif +} +#define ATOMIC_U64_LOAD_ACQUIRE(var) rbimpl_atomic_u64_load_acquire(&(var)) + +static inline void +rbimpl_atomic_u64_set_release(volatile rbimpl_atomic_uint64_t *address, uint64_t value) +{ +#if defined(HAVE_GCC_ATOMIC_BUILTINS_64) + __atomic_store_n(address, value, __ATOMIC_RELEASE); +#else + rbimpl_atomic_u64_set_relaxed(address, value); +#endif +} +#define ATOMIC_U64_SET_RELEASE(var, val) rbimpl_atomic_u64_set_release(&(var), val) + #endif diff --git a/template/configure-ext.mk.tmpl b/template/configure-ext.mk.tmpl index fff75b1f2e..b4e999fe8e 100644 --- a/template/configure-ext.mk.tmpl +++ b/template/configure-ext.mk.tmpl @@ -7,24 +7,34 @@ ECHO = $(ECHO1:0=@echo) <% srcdir = miniruby = script_args = nil +with_exts = [] +without_exts = [] opt = OptionParser.new do |o| o.on('--srcdir=SRCDIR') {|v| srcdir = v} o.on('--miniruby=MINIRUBY') {|v| miniruby = v} o.on('--script-args=MINIRUBY') {|v| script_args = v} + o.on('--with-ext=ext[,...]', Array) {|v| with_exts.concat(v)} + o.on('--without-ext=ext[,...]', Array) {|v| without_exts.concat(v)} o.order!(ARGV) end srcdir ||= File.dirname(File.dirname(__FILE__)) +script_args ||= "" + +filter = proc do |ext| + next false unless with_exts.empty? or !with_exts.any? {|w| File.fnmatch?(w, ext)} + without_exts.empty? or !without_exts.any? {|w| File.fnmatch?(w, ext)} +end exts = { exts: [ "--extstatic $(EXTSTATIC)", - Dir.glob("ext/**/extconf.rb", base: srcdir).map do |d| - d[%r[\Aext/[^/]+]] + Dir.glob("ext/**/extconf.rb", base: srcdir).filter_map do |d| + d if filter[File.basename(d = d[%r[\Aext/[^/]+]])] end.uniq ], gems: [ "--no-extstatic", - Dir.glob(".bundle/gems/**/extconf.rb", base: srcdir).grep_v(/test/) do |d| - d[%r[\A\.bundle/gems/[^/]+]] + Dir.glob(".bundle/gems/**/extconf.rb", base: srcdir).grep_v(/test/).filter_map do |d| + d if filter[File.basename(d = d[%r[\A\.bundle/gems/[^/]+]])] end.uniq ], } @@ -32,7 +42,14 @@ exts = { MINIRUBY = <%=miniruby%> SCRIPT_ARGS = <%=script_args.gsub("#", "\\#").gsub(/\A|[\s"']\K--jobserver-auth=[^\s'"]*/, "")%> EXTMK_ARGS = $(SCRIPT_ARGS) --gnumake=$(gnumake) --extflags="$(EXTLDFLAGS)" \ - --make-flags="MINIRUBY='$(MINIRUBY)'" + --make-flags="MINIRUBY='$(MINIRUBY)'" \ +<% unless with_exts.empty? -%> + --with-ext=<%= with_exts.join(',') %> \ +<% end -%> +<% unless without_exts.empty? -%> + --without-ext=<%= without_exts.join(',') %> \ +<% end -%> + # EXTMK_ARGS all: exts gems exts: diff --git a/test/fiber/test_thread.rb b/test/fiber/test_thread.rb index 0247f330d9..4d2fbde9ed 100644 --- a/test/fiber/test_thread.rb +++ b/test/fiber/test_thread.rb @@ -156,16 +156,20 @@ class TestFiberThread < Test::Unit::TestCase end def test_thread_join_hang + inner = nil thread = Thread.new do scheduler = SleepingUnblockScheduler.new Fiber.set_scheduler scheduler Fiber.schedule do - Thread.new{sleep(0.01)}.value + inner = Thread.new{sleep(0.01)} + inner.value end end thread.join + ensure + inner&.join end end diff --git a/test/ruby/test_defined.rb b/test/ruby/test_defined.rb index db1fdc8e25..75ed1a7534 100644 --- a/test/ruby/test_defined.rb +++ b/test/ruby/test_defined.rb @@ -62,6 +62,34 @@ class TestDefined < Test::Unit::TestCase f.bar(Class.new(Foo).new) { |v| assert(v, "inherited protected method") } end + module ProtectedInModule + def m + :m + end + protected :m + def call_m(o) + o.m + end + def defined_m(o) + defined?(o.m) + end + end + class ProtectedIncluderA + include ProtectedInModule + end + class ProtectedIncluderB + include ProtectedInModule + end + + def test_defined_protected_method_in_included_module + a = ProtectedIncluderA.new + b = ProtectedIncluderB.new + assert_equal(:m, a.call_m(a)) + assert_equal(:m, a.call_m(b)) + assert_equal("method", a.defined_m(a)) + assert_equal("method", a.defined_m(b)) + end + def test_defined_undefined_method f = Foo.new assert_nil(defined?(f.quux)) # undefined method diff --git a/vm_callinfo.h b/vm_callinfo.h index 06215978d6..9a6c69deae 100644 --- a/vm_callinfo.h +++ b/vm_callinfo.h @@ -47,7 +47,7 @@ enum vm_call_flag_bits { struct rb_callinfo_kwarg { int keyword_len; - int references; + rb_atomic_t references; VALUE keywords[]; }; diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 783246a7ef..75d5023566 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -5485,21 +5485,21 @@ vm_defined(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, rb_num_t op_ break; case DEFINED_METHOD:{ VALUE klass = CLASS_OF(v); - const rb_method_entry_t *me = rb_method_entry_with_refinements(klass, SYM2ID(obj), NULL); + const rb_callable_method_entry_t *cme = rb_callable_method_entry_with_refinements(klass, SYM2ID(obj), NULL); - if (me) { - switch (METHOD_ENTRY_VISI(me)) { + if (cme) { + switch (METHOD_ENTRY_VISI(cme)) { case METHOD_VISI_PRIVATE: break; case METHOD_VISI_PROTECTED: - if (!rb_obj_is_kind_of(GET_SELF(), rb_class_real(me->defined_class))) { + if (!rb_obj_is_kind_of(GET_SELF(), vm_defined_class_for_protected_call(cme))) { break; } case METHOD_VISI_PUBLIC: return true; break; default: - rb_bug("vm_defined: unreachable: %u", (unsigned int)METHOD_ENTRY_VISI(me)); + rb_bug("vm_defined: unreachable: %u", (unsigned int)METHOD_ENTRY_VISI(cme)); } } else { diff --git a/vm_method.c b/vm_method.c index 5ef47e97ac..5a99f684c0 100644 --- a/vm_method.c +++ b/vm_method.c @@ -668,7 +668,7 @@ rb_vm_ci_lookup(ID mid, unsigned int flag, unsigned int argc, const struct rb_ca const struct rb_callinfo *ci = NULL; if (kwarg) { - ((struct rb_callinfo_kwarg *)kwarg)->references++; + RUBY_ATOMIC_FETCH_ADD(((struct rb_callinfo_kwarg *)kwarg)->references, 1); } struct rb_callinfo *new_ci = SHAREABLE_IMEMO_NEW(struct rb_callinfo, imemo_callinfo, (VALUE)kwarg); diff --git a/yjit/src/cruby_bindings.inc.rs b/yjit/src/cruby_bindings.inc.rs index 392e393378..4ad88bb7d0 100644 --- a/yjit/src/cruby_bindings.inc.rs +++ b/yjit/src/cruby_bindings.inc.rs @@ -371,6 +371,7 @@ pub struct vm_ifunc { pub data: *const ::std::os::raw::c_void, pub argc: vm_ifunc_argc, } +pub type rb_atomic_t = ::std::os::raw::c_uint; pub const METHOD_VISI_UNDEF: rb_method_visibility_t = 0; pub const METHOD_VISI_PUBLIC: rb_method_visibility_t = 1; pub const METHOD_VISI_PRIVATE: rb_method_visibility_t = 2; @@ -660,7 +661,7 @@ pub type vm_call_flag_bits = u32; #[repr(C)] pub struct rb_callinfo_kwarg { pub keyword_len: ::std::os::raw::c_int, - pub references: ::std::os::raw::c_int, + pub references: rb_atomic_t, pub keywords: __IncompleteArrayField<VALUE>, } #[repr(C)] diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 860e1726db..c61e61edd1 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -462,6 +462,7 @@ pub struct vm_ifunc { pub data: *const ::std::os::raw::c_void, pub argc: vm_ifunc_argc, } +pub type rb_atomic_t = ::std::os::raw::c_uint; pub const METHOD_VISI_UNDEF: rb_method_visibility_t = 0; pub const METHOD_VISI_PUBLIC: rb_method_visibility_t = 1; pub const METHOD_VISI_PRIVATE: rb_method_visibility_t = 2; @@ -1535,7 +1536,7 @@ pub type vm_call_flag_bits = u32; #[repr(C)] pub struct rb_callinfo_kwarg { pub keyword_len: ::std::os::raw::c_int, - pub references: ::std::os::raw::c_int, + pub references: rb_atomic_t, pub keywords: __IncompleteArrayField<VALUE>, } #[repr(C)] diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index e5976a4045..fdc82c9552 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6572,16 +6572,14 @@ impl ProfileOracle { } /// Map the interpreter-recorded types of the stack onto the HIR operands on our compile-time virtual stack. - /// `stack_offset` is the number of extra stack entries above the profiled operands (e.g. 1 for - /// sends with ARGS_BLOCKARG, where the block arg sits on top of the regular args). - fn profile_stack(&mut self, state: &FrameState, stack_offset: usize) { + fn profile_stack(&mut self, state: &FrameState) { let iseq_insn_idx = state.insn_idx; let Some(operand_types) = self.payload.profile.get_operand_types(iseq_insn_idx) else { return }; let entry = self.types.entry(iseq_insn_idx).or_default(); // operand_types is always going to be <= stack size (otherwise it would have an underflow // at run-time) so use that to drive iteration. for (idx, insn_type_distribution) in operand_types.iter().rev().enumerate() { - let insn = state.stack_topn(idx + stack_offset).expect("Unexpected stack underflow in profiling"); + let insn = state.stack_topn(idx).expect("Unexpected stack underflow in profiling"); entry.push((insn, TypeDistributionSummary::new(insn_type_distribution))) } } @@ -6784,17 +6782,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { } } else { - // For sends with ARGS_BLOCKARG, the block arg sits on the stack above - // the profiled operands (receiver + regular args). Skip it so that the - // profile types map onto the correct HIR operands. - let stack_offset = if opcode == YARVINSN_send || opcode == YARVINSN_opt_send_without_block { - let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); - let flags = unsafe { vm_ci_flag(rb_get_call_data_ci(cd)) }; - usize::from(flags & VM_CALL_ARGS_BLOCKARG != 0) - } else { - 0 - }; - profiles.profile_stack(&exit_state, stack_offset); + profiles.profile_stack(&exit_state); } // Flag a future getlocal/setlocal to add a patch point if this instruction is not leaf. @@ -7220,7 +7208,10 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { // Check if #new resolves to rb_class_new_instance_pass_kw. // TODO: Guard on a profiled class and add a patch point for #new redefinition - let argc = unsafe { vm_ci_argc((*cd).ci) } as usize; + let argc = crate::profile::num_arguments_on_stack(cd); + let ci = unsafe { get_call_data_ci(cd) }; + let flags = unsafe { rb_vm_ci_flag(ci) }; + assert_eq!(flags & VM_CALL_ARGS_BLOCKARG, 0); let val = state.stack_topn(argc)?; let test_id = fun.push_insn(block, Insn::IsMethodCfunc { val, cd, cfunc: rb_class_new_instance_pass_kw as *const u8, state: exit_id }); @@ -7646,7 +7637,8 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type), recompile: None }); break; // End the block } - let argc = unsafe { vm_ci_argc((*cd).ci) }; + let argc = crate::profile::num_arguments_on_stack(cd); + assert_eq!(flags & VM_CALL_ARGS_BLOCKARG, 0); // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { @@ -7751,7 +7743,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type), recompile: None }); break; // End the block } - let argc = unsafe { vm_ci_argc((*cd).ci) }; + let argc = crate::profile::num_arguments_on_stack(cd); let mid = unsafe { rb_vm_ci_mid(call_info) }; // Check for calls to directives @@ -7885,10 +7877,9 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } - let argc = unsafe { vm_ci_argc((*cd).ci) }; let block_arg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; - let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; + let args = state.stack_pop_n(crate::profile::num_arguments_on_stack(cd))?; let recv = state.stack_pop()?; let block_handler = if !blockiseq.is_null() { Some(BlockHandler::BlockIseq(blockiseq)) @@ -7985,9 +7976,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } - let argc = unsafe { vm_ci_argc((*cd).ci) }; - let block_arg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; - let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; + let args = state.stack_pop_n(crate::profile::num_arguments_on_stack(cd))?; let recv = state.stack_pop()?; let blockiseq: IseqPtr = get_arg(pc, 1).as_ptr(); let result = fun.push_insn(block, Insn::InvokeSuper { recv, cd, blockiseq, args, state: exit_id, reason: Uncategorized(opcode) }); @@ -8079,9 +8068,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing, recompile: None }); break; } - let argc = unsafe { vm_ci_argc((*cd).ci) }; - let block_arg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; - let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; + let args = state.stack_pop_n(crate::profile::num_arguments_on_stack(cd))?; // Check if this is a monomorphic IFUNC block handler we can specialize let block_handler_types = profiles.payload.profile.get_operand_types(exit_state.insn_idx); @@ -8327,7 +8314,7 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> { } YARVINSN_objtostring => { let cd: *const rb_call_data = get_arg(pc, 0).as_ptr(); - let argc = unsafe { vm_ci_argc((*cd).ci) }; + let argc = crate::profile::num_arguments_on_stack(cd); assert_eq!(0, argc, "objtostring should not have args"); let recv = state.stack_pop()?; diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index d8bcd50d96..08803c4570 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -95,9 +95,9 @@ fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { YARVINSN_invokesuper => profile_invokesuper(profiler, profile), YARVINSN_opt_send_without_block | YARVINSN_send => { let cd: *const rb_call_data = profiler.insn_opnd(0).as_ptr(); - let argc = unsafe { vm_ci_argc((*cd).ci) }; + let argc = num_arguments_on_stack(cd); // Profile all the arguments and self (+1). - profile_operands(profiler, profile, (argc + 1) as usize); + profile_operands(profiler, profile, argc + 1); } YARVINSN_splatkw => profile_operands(profiler, profile, 2), _ => {} @@ -111,6 +111,15 @@ fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { } } +/// Return the argc as stated in the calldata plus: +/// * 1 if there is an explicit blockarg, since that will be passed on the stack +pub fn num_arguments_on_stack(cd: *const rb_call_data) -> usize { + let ci = unsafe { rb_get_call_data_ci(cd) }; + let flags = unsafe { rb_vm_ci_flag(ci) }; + let has_blockarg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; + (unsafe { vm_ci_argc(ci) }) as usize + has_blockarg as usize +} + const DISTRIBUTION_SIZE: usize = 4; pub type TypeDistribution = Distribution<ProfiledType, DISTRIBUTION_SIZE>; @@ -184,7 +193,7 @@ fn profile_invokesuper(profiler: &mut Profiler, profile: &mut IseqProfile) { unsafe { rb_gc_writebarrier(profiler.iseq.into(), cme_value) }; let cd: *const rb_call_data = profiler.insn_opnd(0).as_ptr(); - let argc = unsafe { vm_ci_argc((*cd).ci) }; + let argc = num_arguments_on_stack(cd); // Profile all the arguments and self (+1). profile_operands(profiler, profile, (argc + 1) as usize); |
