165 Commits

Author SHA1 Message Date
Jean Boussier
53099633e2 Make ruby_xfree_sized and ruby_xrealloc_sized public
[Feature #21861]
2026-04-01 08:10:07 +01:00
Jean Boussier
d8b8a95af9 Make Monitor a core class
[Feature #21788]

It allows monitor to access internal routines and remove some overhead.

Before:

```
ruby 4.0.0dev (2025-12-13T04:52:13Z master 71dd272506) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
               Mutex     2.111M i/100ms
             Monitor     1.736M i/100ms
Calculating -------------------------------------
               Mutex     25.050M (± 0.4%) i/s   (39.92 ns/i) -    126.631M in   5.055208s
             Monitor     19.809M (± 0.1%) i/s   (50.48 ns/i) -    100.672M in   5.082015s
```

After:

```
ruby 4.0.0dev (2025-12-13T06:49:18Z core-monitor 6fabf389fd) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
               Mutex     2.144M i/100ms
             Monitor     1.859M i/100ms
Calculating -------------------------------------
               Mutex     24.771M (± 0.4%) i/s   (40.37 ns/i) -    124.342M in   5.019716s
             Monitor     23.722M (± 0.4%) i/s   (42.15 ns/i) -    118.998M in   5.016361s
```

Bench:

```ruby
require 'bundler/inline'

gemfile do
  gem "benchmark-ips"
end

mutex = Mutex.new
require "monitor"
monitor = Monitor.new

Benchmark.ips do |x|
  x.report("Mutex") { mutex.synchronize { } }
  x.report("Monitor") { monitor.synchronize { } }
end
```
2026-02-12 20:35:18 +01:00
Jean Boussier
66fdb15abf thread_sync.c: Fix leak in Queue and SizedQueue
This was introduced in 8ce61f90ba4aea3d52e92e258c3803b8b885726e
2026-01-31 19:59:40 +01:00
Luke Gruber
8d41e57efe
Revert "Prevent starvation when acquiring mutex over and over (#15877)" (#15990)
This reverts commit 994257ab06072df38de024e70a60aa9a87e36089.

I saw some failures in CI that are probably related to the change.
Example:

```
 1) Failure:
  TestMonitor#test_timedwait [/Users/runner/work/ruby/ruby/src/test/monitor/test_monitor.rb:282]:
```

This starvation problem has not been an issue in real apps afaik, so for now it's best to revert
it and think of a better solution.
2026-01-28 12:32:59 -05:00
Luke Gruber
52e71ad4b7
Fix test for mutex starvation as well as small fix in thread_sync.c (#15982)
Don't reset `th->running_time_us` when unlocking from `mutex_free` or
force unlocking during thread destruction. Follow-up to 994257ab06072d.
2026-01-27 12:36:55 -05:00
Luke Gruber
994257ab06
Prevent starvation when acquiring mutex over and over (#15877)
Continually locking a mutex m can lead to starvation if all other threads are on the waitq of m.

See https://bugs.ruby-lang.org/issues/21840 for more details.

Solution:

When a thread `T1` wakes up `T2` during mutex unlock but `T1` or any other thread successfully acquires it
before `T2`, then we record the `running_time` of the thread during mutex acquisition. Then during unlock, if
that thread's running_time is less than the saved running time, we set it back to the saved time.

Fixes [Bug #21840]
2026-01-26 14:34:37 -05:00
Jean Boussier
16feb46fa2 Convert Queue and SizedQueue to rb builtin
A large part of `thread_sync.c` was migrated already, might as well
go all the way. It also allow to remove a bunch of Rdoc commands.
2026-01-03 00:07:44 +01:00
Jean Boussier
177949c8b2 Speedup Queue initialization
Rather than to push items one by one we can directly memcpy.
2026-01-02 13:51:36 +01:00
Jean Boussier
8ce61f90ba Thread::Queue use a ring buffer
Thread::Queue spends a significant amount of time in array functions,
checking for invariants we know aren't a problem, and whether the backing
array need to reordered.

By using a ring buffer we can remove a lot of overhead (~23% faster).

```
$ hyperfine './miniruby --yjit /tmp/q.rb' './miniruby-qrb --yjit /tmp/q.rb'
Benchmark 1: ./miniruby --yjit /tmp/q.rb
  Time (mean ± σ):      1.050 s ±  0.191 s    [User: 0.988 s, System: 0.004 s]
  Range (min … max):    0.984 s …  1.595 s    10 runs

Benchmark 2: ./miniruby-qrb --yjit /tmp/q.rb
  Time (mean ± σ):     844.2 ms ±   3.1 ms    [User: 840.4 ms, System: 2.8 ms]
  Range (min … max):   838.6 ms … 848.9 ms    10 runs

Summary
  ./miniruby-qrb --yjit /tmp/q.rb ran
    1.24 ± 0.23 times faster than ./miniruby --yjit /tmp/q.rb
```

```
q = Queue.new([1, 2, 3, 4, 5, 6, 7, 8])
i = 2_000_000
while i > 0
  i -= 1
  q.push(q.pop)
  q.push(q.pop)
  q.push(q.pop)
  q.push(q.pop)
  q.push(q.pop)
  q.push(q.pop)
  q.push(q.pop)
  q.push(q.pop)
  q.push(q.pop)
  q.push(q.pop)
end
```
2026-01-01 10:20:08 +01:00
Jean Boussier
fb1dd92d30 thread_sync.c: rename mutex_trylock internal function
[Bug #21793]

To fix a naming conflict on solaris.
2025-12-18 21:07:55 +01:00
Jean Boussier
8cf4f373ff thread_sync.c: declare queue_data_type as parent of szqueue_data_type.
Allows to remove some duplicated code like szqueue_length, etc.
2025-12-18 20:57:05 +01:00
Jean Boussier
bbc684d830 thread_sync.c: simplify check_array
If the queue was allocated without calling initialize,
`ary` will be `0`.
2025-12-18 20:57:05 +01:00
Jean Boussier
57c4cd9a47 thread_sync.c: eliminate GET_EC() from queue_do_pop
We receive the ec as argument, it's much cheaper to pass it
around that to look it up again.
2025-12-18 20:57:05 +01:00
Jean Boussier
0e719239c2 thread_sync: Mutex keep rb_thread_t * instead of VALUE
We never need the actual thread object and this avoid any issue
if the thread object is ever moved.
2025-12-18 19:37:14 +01:00
Jean Boussier
e42bcd7ce7 Rename fiber_serial into ec_serial
Since it now live in the EC.
2025-12-16 09:51:07 +01:00
Jean Boussier
28b195fc67 Store the fiber_serial in the EC to allow inlining
Mutexes spend a significant amount of time in `rb_fiber_serial`
because it can't be inlined (except with LTO).
The fiber struct is opaque the so function can't be defined as inlineable.

Ideally the while fiber struct would not be opaque to the rest of
Ruby core, but it's tricky to do.

Instead we can store the fiber serial in the execution context
itself, and make its access cheaper:

```
$ hyperfine './miniruby-baseline --yjit /tmp/mut.rb' './miniruby-inline-serial --yjit /tmp/mut.rb'
Benchmark 1: ./miniruby-baseline --yjit /tmp/mut.rb
  Time (mean ± σ):      4.011 s ±  0.084 s    [User: 3.977 s, System: 0.011 s]
  Range (min … max):    3.950 s …  4.245 s    10 runs

Benchmark 2: ./miniruby-inline-serial --yjit /tmp/mut.rb
  Time (mean ± σ):      3.495 s ±  0.150 s    [User: 3.448 s, System: 0.009 s]
  Range (min … max):    3.340 s …  3.869 s    10 runs

Summary
  ./miniruby-inline-serial --yjit /tmp/mut.rb ran
    1.15 ± 0.05 times faster than ./miniruby-baseline --yjit /tmp/mut.rb
```

```ruby
i = 10_000_000
mut = Mutex.new
while i > 0
  i -= 1
  mut.synchronize { }
  mut.synchronize { }
  mut.synchronize { }
  mut.synchronize { }
  mut.synchronize { }
  mut.synchronize { }
  mut.synchronize { }
  mut.synchronize { }
  mut.synchronize { }
  mut.synchronize { }
end
```
2025-12-16 09:51:07 +01:00
Jean Boussier
ff831eb057 thead_sync.c: directly pass the execution context to yield
Saves one more call to GET_EC()
2025-12-12 10:08:05 +01:00
Jean Boussier
7e7a1db579 Define Thread::ConditionVariable in thread_sync.rb
It's more consistent with Mutex, but also the `#wait` method
benefits from receiving the execution context instead of having
to call `GET_EC`.
2025-12-12 10:07:39 +01:00
Jean Boussier
07b2356a6a Mutex: avoid repeated calls to GET_EC
That call is surprisingly expensive, so trying doing it once
in `#synchronize` and then passing the EC to lock and unlock
saves quite a few cycles.

Before:

```
ruby 4.0.0dev (2025-12-10T09:30:18Z master c5608ab4d7) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
               Mutex     1.888M i/100ms
             Monitor     1.633M i/100ms
Calculating -------------------------------------
               Mutex     22.610M (± 0.2%) i/s   (44.23 ns/i) -    113.258M in   5.009097s
             Monitor     19.148M (± 0.3%) i/s   (52.22 ns/i) -     96.366M in   5.032755s
```

After:
```
ruby 4.0.0dev (2025-12-10T10:40:07Z speedup-mutex 1c901cd4f8) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
               Mutex     2.095M i/100ms
             Monitor     1.578M i/100ms
Calculating -------------------------------------
               Mutex     24.456M (± 0.4%) i/s   (40.89 ns/i) -    123.584M in   5.053418s
             Monitor     19.176M (± 0.1%) i/s   (52.15 ns/i) -     96.243M in   5.018977s
```

Bench:

```
require 'bundler/inline'

gemfile do
  gem "benchmark-ips"
end

mutex = Mutex.new
require "monitor"
monitor = Monitor.new

Benchmark.ips do |x|
  x.report("Mutex") { mutex.synchronize { } }
  x.report("Monitor") { monitor.synchronize { } }
end
```
2025-12-11 23:25:57 +01:00
Jean Boussier
c5608ab4d7 Monitor: avoid repeated calls to rb_fiber_current()
That call is surprisingly expensive, so trying doing it once
in `#synchronize` and then passing the fiber to enter and exit
saves quite a few cycles.
2025-12-10 10:30:18 +01:00
Koichi Sasada
ced333677f fix SEGV on clang-16/18
Maybe because of TLS/coroutine problem, CI fails on clang-16/18

```
  1) Failure:
TestTimeout#test_ractor [/tmp/ruby/src/trunk_clang_18/test/test_timeout.rb:288]:
pid 307341 killed by SIGSEGV (signal 11) (core dumped)
| /tmp/ruby/src/trunk_clang_18/lib/timeout.rb:98: [BUG] Segmentation fault at 0x0000000000000030
| ruby 4.0.0dev (2025-12-07T16:51:02Z master 4f900c35bc) +PRISM [x86_64-linux]
|
| -- Control frame information -----------------------------------------------
| c:0006 p:---- s:0026 e:000025 l:y b:---- CFUNC  :sleep
| c:0005 p:---- s:0023 e:000022 l:y b:---- CFUNC  :wait
| c:0004 p:0020 s:0017 e:000016 l:n b:---- BLOCK  /tmp/ruby/src/trunk_clang_18/lib/timeout.rb:98 [FINISH]
| c:0003 p:---- s:0014 e:000013 l:y b:---- CFUNC  :synchronize
| c:0002 p:0072 s:0010 e:000009 l:n b:---- BLOCK  /tmp/ruby/src/trunk_clang_18/lib/timeout.rb:96 [FINISH]
| c:0001 p:---- s:0003 e:000002 l:y b:---- DUMMY  [FINISH]
|
| -- Ruby level backtrace information ----------------------------------------
| /tmp/ruby/src/trunk_clang_18/lib/timeout.rb:96:in 'block in create_timeout_thread'
| /tmp/ruby/src/trunk_clang_18/lib/timeout.rb:96:in 'synchronize'
| /tmp/ruby/src/trunk_clang_18/lib/timeout.rb:98:in 'block (2 levels) in create_timeout_thread'
| /tmp/ruby/src/trunk_clang_18/lib/timeout.rb:98:in 'wait'
| /tmp/ruby/src/trunk_clang_18/lib/timeout.rb:98:in 'sleep'
|
| -- Threading information ---------------------------------------------------
| Total ractor count: 3
| Ruby thread count for this ractor: 2
|
| -- Machine register context ------------------------------------------------
|  RIP: 0x0000602b1e08a5b5 RBP: 0x000071c65facd130 RSP: 0x000071c6258842e0
|  RAX: 0x0000000000000000 RBX: 0x000000006935f7c4 RCX: 0x0000000000000000
|  RDX: 0x0000602b1e520c20 RDI: 0x000071c620012480 RSI: 0x000071c620012480
|   R8: 0x0000000000000000  R9: 0x0000000000000000 R10: 0x0000000000000000
|  R11: 0x0000000000000000 R12: 0x000071c65fa2f640 R13: 0x000071c65fb66e48
|  R14: 0x0000000000000000 R15: 0xfdccaa3270000002 EFL: 0x0000000000010202
|
| -- C level backtrace information -------------------------------------------
| /tmp/ruby/build/trunk_clang_18/ruby(rb_print_backtrace+0x14) [0x602b1e31a6ea] /tmp/ruby/src/trunk_clang_18/vm_dump.c:1105
| /tmp/ruby/build/trunk_clang_18/ruby(rb_vm_bugreport) /tmp/ruby/src/trunk_clang_18/vm_dump.c:1450
| /tmp/ruby/build/trunk_clang_18/ruby(rb_bug_for_fatal_signal+0x15c) [0x602b1e2d960c] /tmp/ruby/src/trunk_clang_18/error.c:1131
| /tmp/ruby/build/trunk_clang_18/ruby(sigsegv+0x5a) [0x602b1e05528a] /tmp/ruby/src/trunk_clang_18/signal.c:948
| /lib/x86_64-linux-gnu/libc.so.6(0x71c65fd46320) [0x71c65fd46320]
| /tmp/ruby/build/trunk_clang_18/ruby(vm_check_ints_blocking+0x0) [0x602b1e08a5b5] /tmp/ruby/src/trunk_clang_18/vm_core.h:2097
| /tmp/ruby/build/trunk_clang_18/ruby(rb_current_execution_context) /tmp/ruby/src/trunk_clang_18/thread_sync.c:617
| /tmp/ruby/build/trunk_clang_18/ruby(rb_mutex_sleep) /tmp/ruby/src/trunk_clang_18/thread_sync.c:617
```

This patch introduces workaround by acquiring EC before swithcing coroutine.
2025-12-08 18:21:37 +09:00
John Hawthorn
ff1d23eccb Use a serial to keep track of Mutex-owning Fiber
Previously this held a pointer to the Fiber itself, which requires
marking it (which was only implemented recently, prior to that it was
buggy). Using a monotonically increasing integer instead allows us to
avoid having a free function and keeps everything simpler.

My main motivations in making this change are that the root fiber lazily
allocates self, which makes the writebarrier implementation challenging
to do correctly, and wanting to avoid sending Mutexes to the remembered
set when locked by a short-lived Fiber.
2025-11-20 14:06:33 -08:00
Luke Gruber
62430c19c9 Properly unlock locked mutexes on thread cleanup.
Mutexes were being improperly unlocked on thread cleanup. This bug was
introduced in 050a8954395.

We must keep a reference from the mutex to the thread, because if the fiber
is collected before the mutex is, then we cannot unlink it from the thread in
`mutex_free`. If it's not unlinked from the the thread when it's freed, it
causes bugs in `rb_thread_unlock_all_locking_mutexes`.

We now mark the fiber when a mutex is locked, and the thread is marked
as well. However, a fiber can still be freed in the same GC cycle as the
mutex, so the reference to the thread is still needed.

The reason we need to mark the fiber is that `mutex_owned_p()` has an ABA
issue where if the fiber is collected while it's locked, a new fiber could be
allocated at the same memory address and we could get false positives.

Fixes [Bug #21342]

Co-authored-by: John Hawthorn <john@hawthorn.email>
2025-09-25 15:29:47 -07:00
Peter Zhu
2edfc51acf Make Thread::Queue and SizedQueue support compaction 2025-08-28 09:05:49 -04:00
Peter Zhu
d025bc230c Remove duplicated line of code in thread_sync.c 2025-08-14 21:51:43 -04:00
Kunshan Wang
ce1dfe81c2 [DOC] Doc for Thread::ConditionVariable
Documented the necessity of calling `wait` in a loop.  We modified the
example to demonstrate the idiomatic use, and added a third thread `a2`
to demonstrate another reason that necessitates the loop.

Mentioned spurious wake-up in the doc.
2025-04-02 10:47:52 -04:00
Samuel Williams
a8c2d5e7be
Ensure fiber scheduler re-acquires mutex when interrupted from sleep. (#12158)
[Bug #20907]
2024-11-23 23:54:12 +00:00
Peter Zhu
80700f453e Replace assert with RUBY_ASSERT in thread_sync.c
assert does not print the bug report, only the file and line number of
the assertion that failed. RUBY_ASSERT prints the full bug report, which
makes it much easier to debug.
2024-02-12 15:07:47 -05:00
Jeremy Evans
c546ee3bb5 Remove SizedQueue#freeze
Queue#freeze uses the same C function, so SizedQueue#freeze can use
that via normal method lookup.
2024-01-17 13:17:25 -08:00
Victor Shepelev
570d7b2c3e
[DOC] Adjust some new features wording/examples. (#9183)
* Reword Range#overlap? docs last paragraph.

* Docs: add explanation about Queue#freeze

* Docs: Add :rescue event docs for TracePoint

* Docs: Enhance Module#set_temporary_name documentation

* Docs: Slightly expand Process::Status deprecations

* Fix MatchData#named_captures rendering glitch

* Improve Dir.fchdir examples

* Adjust Refinement#target docs
2023-12-14 23:01:48 +02:00
Koichi Sasada
be1bbd5b7d M:N thread scheduler for Ractors
This patch introduce M:N thread scheduler for Ractor system.

In general, M:N thread scheduler employs N native threads (OS threads)
to manage M user-level threads (Ruby threads in this case).
On the Ruby interpreter, 1 native thread is provided for 1 Ractor
and all Ruby threads are managed by the native thread.

From Ruby 1.9, the interpreter uses 1:1 thread scheduler which means
1 Ruby thread has 1 native thread. M:N scheduler change this strategy.

Because of compatibility issue (and stableness issue of the implementation)
main Ractor doesn't use M:N scheduler on default. On the other words,
threads on the main Ractor will be managed with 1:1 thread scheduler.

There are additional settings by environment variables:

`RUBY_MN_THREADS=1` enables M:N thread scheduler on the main ractor.
Note that non-main ractors use the M:N scheduler without this
configuration. With this configuration, single ractor applications
run threads on M:1 thread scheduler (green threads, user-level threads).

`RUBY_MAX_CPU=n` specifies maximum number of native threads for
M:N scheduler (default: 8).

This patch will be reverted soon if non-easy issues are found.

[Bug #19842]
2023-10-12 14:47:01 +09:00
Jeremy Evans
62181e17da Make {Queue,SizedQueue}#freeze raise TypeError
Fixes [Bug #17146]
2023-09-27 19:57:50 +01:00
Samuel Williams
be21a056d2
Try default gcc 9.4.0 to see if it exhibits the same compiler bugs. (#8394)
* Revert "Extract `do_mutex_lock_check_interrupts` to try and fix `ppc64le`. (#8393)"

This reverts commit 5184b40dd4dc446660cd35c3e53896324e95b317.

* .travis.yml: Try default gcc 9.4.0 instead of gcc-10 in ppc64le and s390x.

Use gcc 9.4.0 instead of gcc-10 to avoid the current failures by a possible GCC
10 compiler bug in the Travis ppc64le and s390x cases. And it also aligns with
RubyCI Ubuntu ppc64le and s390x where the default gcc is used.

---------

Co-authored-by: Jun Aruga <jaruga@ruby-lang.org>
2023-09-08 20:44:46 +12:00
Samuel Williams
5184b40dd4
Extract do_mutex_lock_check_interrupts to try and fix ppc64le. (#8393)
We found some tests were hanging in `do_mutex_lock`, specifically the
fiber scheduler autoload test. After much investigation, it may be a code
generation bug. Because we didn't change the code, but only extracted it
into a separate function, and it appears to fix the problem.
2023-09-08 00:32:54 +12:00
Shane Becker
fff4773085 [DOC] Add parenthetical to explain what FIFO abbrev means 2023-07-13 21:39:31 +09:00
Shane Becker
7524675330 [DOC] Fix example code indentation from 3 to 2 2023-07-13 14:22:47 +09:00
Samuel Williams
2c4b2053ca
Correctly clean up keeping_mutexes before resuming any other threads. (#7460)
It's possible (but very rare) to have a race condition between setting
`mutex->fiber = NULL` and `thread_mutex_remove(th, mutex)` which results
in the following bug:

```
[BUG] invalid keeping_mutexes: Attempt to unlock a mutex which is not locked
```

Fixes <https://bugs.ruby-lang.org/issues/19480>.
2023-03-07 20:23:00 +13:00
Nobuyoshi Nakada
a56d959ed5
Replace PACKED_STRUCT and PACKED_STRUCT_UNALIGNED 2023-02-08 12:35:27 +09:00
Nobuyoshi Nakada
f0f2535c4d Add queue_list and szqueue_list macros 2023-01-21 00:30:34 +09:00
Peter Zhu
8872ebec6a Fix compilation warnings in thread_sync.c
Fixes the following compilation warnings:

thread_sync.c:908:48: warning: taking address of packed member of `struct rb_queue` may result in an unaligned pointer value [-Waddress-of-packed-member]

thread_sync.c:1181:48: warning: taking address of packed member of `struct rb_queue` may result in an unaligned pointer value [-Waddress-of-packed-member]
2023-01-19 12:22:02 -05:00
Peter Zhu
4fa7d38324 Don't redefine RB_OBJ_WRITE
RB_OBJ_WRITE already exists in rgengc.h, so we shouldn't redefine it in
gc.h.
2023-01-18 08:49:32 -05:00
Jean byroot Boussier
eacedcfe44
mutex: Raise a ThreadError when detecting a fiber deadlock (#6680)
[Bug #19105]

If no fiber scheduler is registered and the fiber that
owns the lock and the one that try to acquire it
both belong to the same thread, we're in a deadlock case.

Co-authored-by: Jean Boussier <byroot@ruby-lang.org>
2022-11-09 00:43:16 +13:00
Samuel Williams
7f175e5648
Avoid missed wakeup with fiber scheduler and Fiber.blocking. (#6588)
* Ensure that blocked fibers don't prevent valid wakeups.
2022-10-20 13:38:52 +13:00
Nobuyoshi Nakada
637144b834
Adjust indents [ci skip] 2022-10-18 00:23:23 +09:00
Jean Boussier
60defe0a68 thread_sync.c: Clarify and document the behavior of timeout == 0
[Feature #18982]

Instead of introducing an `exception: false` argument to have `non_block`
return nil rather than raise, we can clearly document that a timeout of 0
immediately returns.

The code is refactored a bit to avoid doing a time calculation in
such case.
2022-10-17 16:56:00 +02:00
Jean Boussier
fe61cad749 Implement SizedQueue#push(timeout: sec)
[Feature #18944]

If both `non_block=true` and `timeout:` are supplied, ArgumentError
is raised.
2022-08-18 10:07:37 +02:00
Nobuyoshi Nakada
58c8b6e862
Adjust styles [ci skip] 2022-08-06 10:13:20 +09:00
Jean Boussier
6b2fc33ae2 thread_sync.c: pass proper argument to queue_sleep in rb_szqueue_push
When I removed the SizeQueue#push timeout from my PR, I forgot to
update the `queue_sleep` parameters to be a `queue_sleep_arg`.

Somehow this worked on most archs, but on Solaris/Sparc it would
legitimately crash when trying to access the `timeout` and `end`
members of the struct.
2022-08-04 11:48:31 +02:00
Jean Boussier
e3aabe93aa Implement Queue#pop(timeout: sec)
[Feature #18774]

As well as `SizedQueue#pop(timeout: sec)`

If both `non_block=true` and `timeout:` are supplied, ArgumentError
is raised.
2022-08-02 11:04:28 +02:00
Peter Zhu
efb91ff19b Rename rb_ary_tmp_new to rb_ary_hidden_new
rb_ary_tmp_new suggests that the array is temporary in some way, but
that's not true, it just creates an array that's hidden and not on the
transient heap. This commit renames it to rb_ary_hidden_new.
2022-07-26 09:12:09 -04:00