Skip to content

[pull] master from ruby:master#885

Merged
pull[bot] merged 18 commits intoturkdevops:masterfrom
ruby:master
Mar 26, 2026
Merged

[pull] master from ruby:master#885
pull[bot] merged 18 commits intoturkdevops:masterfrom
ruby:master

Conversation

@pull
Copy link
Copy Markdown

@pull pull bot commented Mar 26, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

Edouard-chin and others added 18 commits March 25, 2026 18:53
- During resolution, Gem::Version are compared against each other.
  Since comparing versions is a very hot path we can micro optimize
  it to check the happy path first.

  The speed gain on the overall resolution isn't significant but the
  ips gain is quite substantial. The diff chunk is small so I figure
  it's worth it anyway.

  ```ruby
  a = Gem::Version.new("5.3.1")
  b = Gem::Version.new("5.3.1")

  Benchmark.ips do |x|
    x.report("equal regular:") { a <=> c }
    x.report("equal optimized:") { a <=> c }
    x.hold!("equal_temp_results")
    x.compare!(order: :baseline)
  end
  ```

  ```
  Warming up --------------------------------------
      equal optimized:     1.268M i/100ms
  Calculating -------------------------------------
      equal optimized:     12.738M (± 1.5%) i/s   (78.50 ns/i) -     64.680M in   5.078754s

  Comparison:
        equal regular::  9866605.0 i/s
      equal optimized:: 12738310.3 i/s - 1.29x  faster
  ```

ruby 3.4.8 (2025-12-17 revision ruby/rubygems@995b59f666) +PRISM [arm64-darwin25]

ruby/rubygems@c940f7a547
We should never initialize a class with an existing singleton class
(singleton classes definitionally should not be shared).

The only cases this happened in Ruby itself is methods, which exposes a
bug that dup did not behave correctly.
Copy the allocator from superclass to subclass at class creation time,
so rb_get_alloc_func no longer needs to walk the ancestor chain.

This expands on 68ffc8d, which did this
copy, but only for classes defined in Ruby (ex. `class Foo`). And allows
us to simple read the allocator field for any class to get the correct
value.

To keep this correct when rb_define_alloc_func is called after
subclasses already exist, propagate the new allocator down to all
subclasses. An RCLASS_ALLOCATOR_DEFINED flag distinguishes classes with
an explicitly set allocator from those that inherited one, so
propagation stops at subclasses that define their own.
When a send instruction has no profile data (e.g., the code path was
not reached during interpreter profiling), the first JIT compilation
converts it to a SideExit that profiles operands on exit. After
--zjit-num-profiles exits (default 5), the exit triggers ISEQ
invalidation and recompilation with the newly gathered profile data,
allowing the send to be optimized (e.g., as SendDirect) in the
second version.

This approach avoids adding Profile instructions to the HIR (which
could interfere with optimization passes) and keeps profile_time
low by only profiling on side exits rather than during normal
execution.
Extract the duplicated invalidation + JIT-to-JIT call recompilation
logic from both PatchPoint handling and no-profile-send recompilation
into a single invalidate_iseq_version() function. This is a starting
point toward a cohesive "lifecycle of a JITed method" state machine
where all compile/recompile decisions flow through one place.
Count down from --zjit-num-profiles to 0 instead of counting up. This
avoids reading options state in done_profiling_at and profile_send_at
at check time. Also simplify the stack offset arithmetic in
profile_send_at: -1 - (n - i - 1) simplifies to (i - n).
ObjectSpace.each_object(Tempfile) can find partially-constructed
Tempfile objects where @delegate_dc_obj is not yet set (between
ivar initialization and the super() call in Tempfile#initialize).
Calling closed? on such objects raises ArgumentError ("not delegated")
from DelegateClass's __getobj__. Under rare conditions (observed in
ZJIT CI with --zjit-call-threshold=1), this can also trigger
SystemStackError.

Use `rescue true` to treat such tempfiles as closed, avoiding
test worker crashes in the leak checker.
- ### Problem

  Selecting a version for a given package is a extremelly hot path.
  In a very large gemfile and a deep dependency tree (like the one
  we have in our monolith at Shopify), this codepath is hit around 3.2
  million times in total during the resolution phase.

  ### Context

  When the resolution starts, Bundler fetch and turn every possible
  versions of a gem that was ever released on Rubygems.org into a
  possible candidate. We end up with a massive matrix of possibilites
  that PubGrub has to go through. In the case of a large Gemfile
  like we have, we end up with ~55,000 candidates.

  Many of this candidate have conflicting dependencies requirements
  and as pubgrub progress, it will continously ask over and over the
  same things: "Return the possible candidates given this version
  constraint" (`range.select_versions(@sorted_versions[package])`).

  Since this path is called so frequently (sometimes more than 8000
  times for a single candidate and the same constraint), the returned
  value can be cached for faster access.

  ### Solution

  Cache the selected versions for a given constraint in a hash.
  The key being an array where the first element is the package we want
  to resolve and the second element is the constraint. The associated
  value is all possible candidates matching.

  If the resolver end up not finding a candidate (in example you run
  `bundle install --prefer-local`) then Bundler will allow finding
  candidates on the remote. In this case we need to invalidate the
  cache as otherwise the candidates from the remotes will not
  be considered.

  ### Benchmark

  This change has a huge impact on resolution time. Those measures
  were taken on Shopify monolith by removing the lockfile and measuring
  only resolution time (no network or external factors affect these
  results.)

  ┌─────┬──────────┬───────────┬─────────┐
  │ Run │ Original │ Optimized │ Speedup │
  ├─────┼──────────┼───────────┼─────────┤
  │ 1   │ 19.20s   │ 10.32s    │ 46.3%   │
  ├─────┼──────────┼───────────┼─────────┤
  │ 2   │ 19.17s   │ 10.46s    │ 45.4%   │
  ├─────┼──────────┼───────────┼─────────┤
  │ 3   │ 18.95s   │ 10.29s    │ 45.7%   │
  ├─────┼──────────┼───────────┼─────────┤
  │ 4   │ 19.17s   │ 10.37s    │ 45.9%   │
  ├─────┼──────────┼───────────┼─────────┤
  │ 5   │ 19.26s   │ 10.30s    │ 46.5%   │
  ├─────┼──────────┼───────────┼─────────┤
  │ Avg │ 19.15s   │ 10.35s    │ 46.0%   │
  └─────┴──────────┴───────────┴─────────┘

ruby/rubygems@a4f5973f95
Some tests migrated from test_zjit.rb retained 4-space indentation
for Ruby code inside string literals. Normalize to 2-space indentation
to match the convention used by most tests (e.g. test_getglobal_with_warning,
test_duphash).
…ilation

In the old Ruby-based tests, assert_compiles asserted that every
compilation triggered by the test script succeeds, while assert_runs
(now inspect) allowed compilation failures. This distinction was lost
during the migration to Rust tests.

Introduce assert_compiles() in Rust that temporarily enables
ZJITState::assert_compiles during program evaluation, causing a panic
if any compilation fails. Replace inspect() with assert_compiles() in
146 tests that compile successfully, keeping inspect() for 224 tests
that intentionally test graceful compilation failures or side exits.
(ruby/rubygems#9296)

Symlinks are not permitted by default for a Windows user. To use them, a switch called "Development Mode" in the system settings has to be enabled.

## What was the end-user or developer problem that led to this PR?

Ordinary users as well as administrators are unable per default to install gems using symlinks.
One such problematical gem is `haml-rails-3.0.0`.
It uses symlinks for [files and directories](https://github.com/haml/haml-rails/tree/9f4703ddff0644ba52529c5cf41c1624829b16a7/lib/generators/haml/scaffold/templates).
The resulting error message is not very helpful:

```
$ gem inst haml-rails
Fetching haml-rails-3.0.0.gem
ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the directory. (Gem::FilePermissionError)
        C:/ruby/lib/ruby/4.0.0/rubygems/installer.rb:308:in 'Gem::Installer#install'
        C:/ruby/lib/ruby/4.0.0/rubygems/resolver/specification.rb:105:in 'Gem::Resolver::Specification#install'
        C:/ruby/lib/ruby/4.0.0/rubygems/request_set.rb:192:in 'block in Gem::RequestSet#install'
        C:/ruby/lib/ruby/4.0.0/rubygems/request_set.rb:183:in 'Array#each'
        C:/ruby/lib/ruby/4.0.0/rubygems/request_set.rb:183:in 'Gem::RequestSet#install'
        C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:207:in 'Gem::Commands::InstallCommand#install_gem'
        C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:223:in 'block in Gem::Commands::InstallCommand#install_gems'
        C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:216:in 'Array#each'
        C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:216:in 'Gem::Commands::InstallCommand#install_gems'
        C:/ruby/lib/ruby/4.0.0/rubygems/commands/install_command.rb:162:in 'Gem::Commands::InstallCommand#execute'
        C:/ruby/lib/ruby/4.0.0/rubygems/command.rb:326:in 'Gem::Command#invoke_with_build_args'
        C:/ruby/lib/ruby/4.0.0/rubygems/command_manager.rb:252:in 'Gem::CommandManager#invoke_command'
        C:/ruby/lib/ruby/4.0.0/rubygems/command_manager.rb:193:in 'Gem::CommandManager#process_args'
        C:/ruby/lib/ruby/4.0.0/rubygems/command_manager.rb:151:in 'Gem::CommandManager#run'
        C:/ruby/lib/ruby/4.0.0/rubygems/gem_runner.rb:56:in 'Gem::GemRunner#run'
        C:/ruby/bin/gem.cmd:20:in '<main>'
```

## What is your fix for the problem, implemented in this PR?

Instead of working around the situation in the affected gem or to skip symlinks completely, I think the better solution would be to make copies of the files in question. This would allow Windows users to install and use the gem smoothly.

The switch for the "Developer Mode" is available in the Windows registry under
    `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock`
entry
    `AllowDevelopmentWithoutDevLicense`

ruby/rubygems@ca6c5791fe
When optimizing sends to C function calls, ZJIT was calling
rust_str_to_id(qualified_method_name(...)) to create display names like
"ObjectSpace.allocation_generation". This calls rb_intern() which
creates a dynamic symbol and allocates a Ruby String on the heap.

These allocations interfered with ObjectSpace.trace_object_allocations,
causing test_string_memory.rb to fail intermittently when run with ZJIT
enabled (--zjit-call-threshold=1).

Fix by using the CME's called_id directly (already interned, no new
allocation) and reconstructing qualified names lazily in Display impls.
This reverts commit 1596853.
Expressions that run when the local is not given can observe the initial
state of the optional parameter locals.
Make an ArrayMin opcode and call the fallback for now.
The server thread's msg_src.reply can raise Errno::ENOBUFS
("No buffer space available") transiently on macOS CI. This
exception was not caught, propagating through th.join in the
ensure block and failing the test.

Rescue it in the server thread so the client side times out on
IO.select, raises RuntimeError, and hits the existing omit path.
@pull pull bot locked and limited conversation to collaborators Mar 26, 2026
@pull pull bot added the ⤵️ pull label Mar 26, 2026
@pull pull bot merged commit eb80511 into turkdevops:master Mar 26, 2026
1 of 2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants