Issue #19246 has been reported by thomthom (Thomas Thomassen).
----------------------------------------
Bug #19246: Rebuilding the loaded feature index much slower in Ruby 3.1
https://bugs.ruby-lang.org/issues/19246
* Author: thomthom (Thomas Thomassen)
* Status: Open
* Priority: Normal
* Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN
----------------------------------------
Some background to this issue: (This is a case that is unconventional usage of Ruby, but I hope you bear with me.)
We ship the Ruby interpreter with our desktop applications for plugin support in our application (SketchUp).
One feature we have had since, at least 2006 (maybe earlier-hard to track history beyond that) is that we had a custom alternate `require` method: `Sketchup.require`. This allows the users of our API to load encrypted Ruby files.
This originally used `rb_provide` to add the path to the encrypted file into the list of loaded feature. However, somewhere between Ruby 2.2 and 2.5 there was some string optimisations made and the function `rb_provide` would not use a copy of the string passed to it. Instead it just held on to a pointer reference. In our case that string came from user-land, being passed in from `Sketchup.require` and would eventually be garbage collected and cause access violation crashes.
To work around that we changed our custom `Sketchup.require` to push to `$LOADED_FEATURES` directly. There was a small penalty to the index being rebuilt after that, but it was negligible.
Recently we tried to upgrade the Ruby interpreter in our application from 2.7 to 3.1 and found a major performance reduction when using our `Sketchup.require. As in, a plugin that would load in half a second would now spend 30 seconds.
From https://bugs.ruby-lang.org/issues/18452 it sounds like there is _some_ expected extra penalty due to changes in how the index is built. But should it really be this much?
Example minimal repro to simulate the issue:
```
# frozen_string_literal: true
require 'benchmark'
iterations = 200
foo_files = iterations.times.map { |i| "#{__dir__}/tmp/foo-#{i}.rb" }
foo_files.each { |f| File.write(f, "") }
bar_files = iterations.times.map { |i| "#{__dir__}/tmp/bar-#{i}.rb" }
bar_files.each { |f| File.write(f, "") }
biz_files = iterations.times.map { |i| "#{__dir__}/tmp/biz-#{i}.rb" }
biz_files.each { |f| File.write(f, "") }
Benchmark.bm do |x|
x.report('normal') {
foo_files.each { |file|
require file
}
}
x.report('loaded_features') {
foo_files.each { |file|
require file
$LOADED_FEATURES << "#{file}-fake.rb"
}
}
x.report('normal again') {
biz_files.each { |file|
require file
}
}
end
```
```
C:\Users\Thomas\SourceTree\ruby-perf>ruby27.bat
ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x64-mingw32]
C:\Users\Thomas\SourceTree\ruby-perf>ruby test-require.rb
user system total real
normal 0.000000 0.031000 0.031000 ( 0.078483)
loaded_features 0.015000 0.000000 0.015000 ( 0.038759)
normal again 0.016000 0.032000 0.048000 ( 0.076940)
```
```
C:\Users\Thomas\SourceTree\ruby-perf>ruby30.bat
ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x64-mingw32]
C:\Users\Thomas\SourceTree\ruby-perf>ruby test-require.rb
user system total real
normal 0.000000 0.031000 0.031000 ( 0.074733)
loaded_features 0.032000 0.000000 0.032000 ( 0.038898)
normal again 0.000000 0.047000 0.047000 ( 0.076343)
```
```
C:\Users\Thomas\SourceTree\ruby-perf>ruby31.bat
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x64-mingw-ucrt]
C:\Users\Thomas\SourceTree\ruby-perf>ruby test-require.rb
user system total real
normal 0.016000 0.031000 0.047000 ( 0.132633)
loaded_features 1.969000 11.500000 13.469000 ( 18.395761)
normal again 0.031000 0.125000 0.156000 ( 0.249130)
```
Right now we're exploring options to deal with this. Because the performance degradation is a blocker for us upgrading. We also have 16 years of plugins created by third party developer that makes it impossible for us to drop this feature.
Some options as-is, none of which are ideal:
1. We revert to using `rb_provide` but ensure the string passed in is not owned by Ruby, instead building a list of strings that we keep around for the duration of the application process. The problem is that some of our plugin developers have on occasion released plugins that will touch `$LOADED_FEATURES`, and if such a plugin is installed on a user machine it might cause the application to become unresponsive for minutes. The other non-ideal issue with using `rb_provide` is that we're also using that in ways it wasn't really intended (from that I understand). And it's not an official API?
2. We create a separate way for our `Sketchup.require` to keep track of it's loaded features, but then that would diverge even more from the behaviour of `require`. Replicating `require` functionality is not trivial and would be prone to subtle errors and possible diverge. It also doesn't address our issue that there is code out there in existing plugins that touches `$LOADED_FEATURES`. (And it's not something we can just ask people to clean up. From previous experience old versions stick around for a long time and is very hard to purge from circulation.)
I have two questions for the Ruby mantainers:
1. Would it be reasonable to see an API for adding/removing/checking `$LOADED_FEATURE` that would allow for a more ideal implementation of a custom `require` functionality?
2. Is the performance difference in rebuilding the loaded feature index really expected to be as high as what we're seeing? An increase of nearly 100 times? Is there something there that might be addressed to make the rebuild to be less expensive against? (This would really help to address our challenges with third party plugins occasionally touching the global.)
--
https://bugs.ruby-lang.org/
Issue #19160 has been reported by kaiquekandykoga (Kaíque Koga).
----------------------------------------
Bug #19160: cmp_clamp arguments
https://bugs.ruby-lang.org/issues/19160
* Author: kaiquekandykoga (Kaíque Koga)
* Status: Open
* Priority: Normal
* ruby -v: ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-freebsd13.1]
* Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN
----------------------------------------
If clamp receives min higher than max, it will raise an exception. The message says *min argument must be smaller than max argument* , but min can actually be equal to max too.
Patch https://github.com/ruby/ruby/pull/6802.
--
https://bugs.ruby-lang.org/
Issue #19231 has been reported by andrykonchin (Andrew Konchin).
----------------------------------------
Bug #19231: Integer#step and Float::INFINITY - inconsistent behaviour when called with and without a block
https://bugs.ruby-lang.org/issues/19231
* Author: andrykonchin (Andrew Konchin)
* Status: Open
* Priority: Normal
* ruby -v: 3.1.2
* Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN
----------------------------------------
The initial issue was reported here https://github.com/oracle/truffleruby/issues/2797.
`0.step(Float::INFINITY, 10)` returns:
- `Integers` when called with a block
- `Floats` when called without a block
I would expect `Floats` to be returned in both cases.
Examples:
```ruby
0.step(100.0, 10).take(1).map(&:class)
# => [Float]
```
```ruby
0.step(Float::INFINITY, 10) { |offset| p offset.class; break }
# Integer
```
When `to` argument is a finite `Float` value then calling with a block returns `Floats` as well:
```ruby
0.step(100.0, 10) { |offset| p offset.class; break }
# Float
```
Wondering whether it's intentional behaviour.
I've found a related issue https://bugs.ruby-lang.org/issues/15518.
--
https://bugs.ruby-lang.org/
Issue #19288 has been reported by maciej.mensfeld (Maciej Mensfeld).
----------------------------------------
Bug #19288: Ractor JSON parsing significantly slower than linear parsing
https://bugs.ruby-lang.org/issues/19288
* Author: maciej.mensfeld (Maciej Mensfeld)
* Status: Open
* Priority: Normal
* ruby -v: ruby 3.2.0 (2022-12-25 revision a528908271) [x86_64-linux]
* Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN, 3.2: UNKNOWN
----------------------------------------
a simple benchmark:
```ruby
require 'json'
require 'benchmark'
CONCURRENT = 5
RACTORS = true
ELEMENTS = 100_000
data = CONCURRENT.times.map do
ELEMENTS.times.map do
{
rand => rand,
rand => rand,
rand => rand,
rand => rand
}.to_json
end
end
ractors = CONCURRENT.times.map do
Ractor.new do
Ractor.receive.each { JSON.parse(_1) }
end
end
result = Benchmark.measure do
if RACTORS
CONCURRENT.times do |i|
ractors[i].send(data[i], move: false)
end
ractors.each(&:take)
else
# Linear without any threads
data.each do |piece|
piece.each { JSON.parse(_1) }
end
end
end
puts result
```
Gives following results on my 8 core machine:
```shell
# without ractors:
2.731748 0.003993 2.735741 ( 2.736349)
# with ractors
12.580452 5.089802 17.670254 ( 5.209755)
```
I would expect Ractors not to be two times slower on the CPU intense work.
--
https://bugs.ruby-lang.org/
Issue #19281 has been reported by tompng (tomoya ishida).
----------------------------------------
Bug #19281: SyntaxError if first argument of command call has semicolon inside parenthesis
https://bugs.ruby-lang.org/issues/19281
* Author: tompng (tomoya ishida)
* Status: Open
* Priority: Normal
* Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN, 3.2: UNKNOWN
----------------------------------------
These are syntax error
~~~ruby
p (1;2),(3),(4)
p (;),(),()
a.b (1;2),(3),(4)
a.b (;),(),()
~~~
I expect it to be syntax ok because the code below is syntax ok.
~~~ruby
p (1),(2;3),(4;5)
p (),(;),(;)
a.b (1),(2;3),(4;5)
a.b (),(;),(;)
~~~
It will be easy to traverse sexp if the sexp of first argument is same as others
~~~ruby
Ripper.sexp "p (),(),()"
# =>
[:program,
[[:command,
[:@ident, "p", [1, 0]],
[:args_add_block,
[[:paren, false], # [:paren, [[:void_stmt]]]
[:paren, [[:void_stmt]]],
[:paren, [[:void_stmt]]]],
false]]]]
Ripper.sexp "p (1),(2),(3)"
# =>
[:program,
[[:command,
[:@ident, "p", [1, 0]],
[:args_add_block,
[[:paren, [:@int, "1", [1, 3]]], # [:paren, [[:@int, "1", [1, 3]]]]
[:paren, [[:@int, "2", [1, 7]]]],
[:paren, [[:@int, "3", [1, 11]]]]],
false]]]]
~~~
--
https://bugs.ruby-lang.org/
Issue #19230 has been reported by mame (Yusuke Endoh).
----------------------------------------
Bug #19230: The openssl backend of securerandom is no longer needed
https://bugs.ruby-lang.org/issues/19230
* Author: mame (Yusuke Endoh)
* Status: Open
* Priority: Normal
* ruby -v: ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
* Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN
----------------------------------------
securerandom first checks if Random.urandom is available ([Line 77](https://github.com/ruby/securerandom/blob/5bfe7d6c163f7a8a45af8d2fc377f…), and if not available, it uses the openssl backend as a degeneration.
However, the openssl backend does not work because it internally uses Random.urandom ([Line 55](https://github.com/ruby/securerandom/blob/5bfe7d6c163f7a8a45af8d2fc377f…) to create a seed.
This issue is found by @hanachin.
```
$ ruby -ve 'def Random.urandom(*); raise; end; require "securerandom"; p SecureRandom.bytes(10)'
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]
-e:1: warning: method redefined; discarding old urandom
-e:1:in `urandom': unhandled exception
from /home/mame/local/lib/ruby/3.1.0/securerandom.rb:75:in `singleton class'
from /home/mame/local/lib/ruby/3.1.0/securerandom.rb:42:in `<module:SecureRandom>'
from /home/mame/local/lib/ruby/3.1.0/securerandom.rb:41:in `<top (required)>'
from <internal:/home/mame/local/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
from <internal:/home/mame/local/lib/ruby/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
from -e:1:in `<main>'
```
There has been this bug since commit:abae70d6ed63054d7d01bd6cd80c1b5b98b93ba3, which made the urandom backend as default and left the openssl backend just for degeneration. I think no one need the openssl anymore because no one has reported this bug for such a long time.
How about removing it?
```diff
diff --git a/lib/securerandom.rb b/lib/securerandom.rb
index 07ae048634..32b76a2137 100644
--- a/lib/securerandom.rb
+++ b/lib/securerandom.rb
@@ -14,7 +14,6 @@
#
# It supports the following secure random number generators:
#
-# * openssl
# * /dev/urandom
# * Win32
#
@@ -46,21 +45,6 @@ def bytes(n)
private
- def gen_random_openssl(n)
- @pid = 0 unless defined?(@pid)
- pid = $$
- unless @pid == pid
- now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
- OpenSSL::Random.random_add([now, @pid, pid].join(""), 0.0)
- seed = Random.urandom(16)
- if (seed)
- OpenSSL::Random.random_add(seed, 16)
- end
- @pid = pid
- end
- return OpenSSL::Random.random_bytes(n)
- end
-
def gen_random_urandom(n)
ret = Random.urandom(n)
unless ret
@@ -77,13 +61,7 @@ def gen_random_urandom(n)
Random.urandom(1)
alias gen_random gen_random_urandom
rescue RuntimeError
- begin
- require 'openssl'
- rescue NoMethodError
- raise NotImplementedError, "No random device"
- else
- alias gen_random gen_random_openssl
- end
+ raise NotImplementedError, "No random device"
end
public :gen_random
```
--
https://bugs.ruby-lang.org/
Issue #19197 has been reported by AMomchilov (Alexander Momchilov).
----------------------------------------
Feature #19197: Add Exception#root_cause
https://bugs.ruby-lang.org/issues/19197
* Author: AMomchilov (Alexander Momchilov)
* Status: Open
* Priority: Normal
----------------------------------------
## Description
I would like to add a `#root_cause` method to `Exception`.
It returns the last exception in linked-list of causality chain, that is, the original exception (whose own `cause` is `nil`).
## Example
```ruby
e = begin
raise 'a' # This is the root cause
rescue => a
begin
raise 'b'
rescue => b
begin
raise 'c' # This is the outermost cause assigned to `e`
rescue => c
c
end
end
end
p(e) # => #<RuntimeError: c>
p(e.cause) # => #<RuntimeError: b>
p(e.cause.cause) # => #<RuntimeError: a>
p(e.cause.cause.cause) # => nil
p(e.root_cause) # => #<RuntimeError: a>
p(e.root_cause.cause) # => nil
```
# Motivation
There are some kinds of exceptions that can occur all over the place (and might be wrapped by arbitrarily many middlemen), but are attributable to a singular global cause. For example, a database outage could raise exceptions in almost every line of business logic of an app that uses ActiveRecord models.
Fundamentally, you wouldn't want an error report for every one of these lines. You'd want to look at the root cause, and bucket all SQL-connection issues into a single report, regardless of where they surface.
### Implementation
Draft PR: https://github.com/ruby/ruby/pull/6913
--
https://bugs.ruby-lang.org/
Issue #19236 has been reported by byroot (Jean Boussier).
----------------------------------------
Feature #19236: Allow to create hashes with a specific capacity from Ruby
https://bugs.ruby-lang.org/issues/19236
* Author: byroot (Jean Boussier)
* Status: Open
* Priority: Normal
* Target version: 3.3
----------------------------------------
Followup on [Feature #18683] which added a C-API for this purpose.
Various protocol parsers such as Redis `RESP3` or `msgpack`, have to create hashes, and they know the size in advance.
For efficiency, it would be preferable if they could directly allocate a Hash of the necessary size, so that large hashes wouldn't cause many re-alloccations and re-hash.
`String` and `Array` both already offer similar APIs:
```ruby
String.new(capacity: XXX)
Array.new(XX) / rb_ary_new_capa(long)
```
However there's no such public API for Hashes in Ruby land.
### Proposal
I think `Hash` should have a way to create a new hash with a `capacity` parameter.
The logical signature of `Hash.new(capacity: 1000)` was deemed too incompatible in [Feature #18683].
@Eregon proposed to add `Hash.create(capacity: 1000)`.
--
https://bugs.ruby-lang.org/
Issue #19168 has been reported by masterleep2 (Bill Lipa).
----------------------------------------
Bug #19168: "such file" is bad grammar
https://bugs.ruby-lang.org/issues/19168
* Author: masterleep2 (Bill Lipa)
* Status: Open
* Priority: Normal
* ruby -v: ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [arm64-darwin22]
* Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN
----------------------------------------
The error message for a missing required file has bad grammar:
$ irb
irb(main):001:0> require 'wuxx'
<internal:/opt/local/lib/ruby3.1/3.1.0/rubygems/core_ext/kernel_require.rb>:85:in `require': cannot load such file -- wuxx (LoadError)
The "such" should be removed. "cannot load file" reads more normally in English.
--
https://bugs.ruby-lang.org/