[pull] master from ruby:master#885
Merged
pull[bot] merged 18 commits intoturkdevops:masterfrom Mar 26, 2026
Merged
Conversation
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 : )