[#108176] [Ruby master Bug#18679] Encoding::UndefinedConversionError: "\xE2" from ASCII-8BIT to UTF-8 — "taf2 (Todd Fisher)" <noreply@...>

Issue #18679 has been reported by taf2 (Todd Fisher).

8 messages 2022/04/05

[#108185] [Ruby master Feature#18683] Allow to create hashes with a specific capacity. — "byroot (Jean Boussier)" <noreply@...>

Issue #18683 has been reported by byroot (Jean Boussier).

13 messages 2022/04/06

[#108198] [Ruby master Feature#18685] Enumerator.product: Cartesian product of enumerators — "knu (Akinori MUSHA)" <noreply@...>

Issue #18685 has been reported by knu (Akinori MUSHA).

8 messages 2022/04/08

[#108201] [Ruby master Misc#18687] [ANN] Upgraded bugs.ruby-lang.org to Redmine 5.0 — "hsbt (Hiroshi SHIBATA)" <noreply@...>

Issue #18687 has been reported by hsbt (Hiroshi SHIBATA).

10 messages 2022/04/09

[#108216] [Ruby master Misc#18691] An option to run `make rbconfig.rb` in a different directory — "jaruga (Jun Aruga)" <noreply@...>

Issue #18691 has been reported by jaruga (Jun Aruga).

14 messages 2022/04/12

[#108225] [Ruby master Misc#18726] CI Error on c99 and c2x — "znz (Kazuhiro NISHIYAMA)" <noreply@...>

Issue #18726 has been reported by znz (Kazuhiro NISHIYAMA).

11 messages 2022/04/14

[#108235] [Ruby master Bug#18729] Method#owner and UnboundMethod#owner are incorrect after using Module#public/protected/private — "Eregon (Benoit Daloze)" <noreply@...>

Issue #18729 has been reported by Eregon (Benoit Daloze).

28 messages 2022/04/14

[#108237] [Ruby master Bug#18730] Double `return` event handling with different tracepoints — "hurricup (Alexandr Evstigneev)" <noreply@...>

Issue #18730 has been reported by hurricup (Alexandr Evstigneev).

8 messages 2022/04/14

[#108294] [Ruby master Bug#18743] Enumerator#next / peek re-use each others stacktraces — sos4nt <noreply@...>

Issue #18743 has been reported by sos4nt (Stefan Schテシテ殕er).

20 messages 2022/04/19

[#108301] [Ruby master Bug#18744] I used Jazzy to generate the doc for my iOS library, but it showed me a bug — "zhaoxinqiang (marc steven)" <noreply@...>

Issue #18744 has been reported by zhaoxinqiang (marc steven).

8 messages 2022/04/20

[ruby-core:108168] [Ruby master Bug#18455] `IO#close` has poor performance and difficult to understand semantics.

From: "shyouhei (Shyouhei Urabe)" <noreply@...>
Date: 2022-04-04 02:02:49 UTC
List: ruby-core #108168
Issue #18455 has been updated by shyouhei (Shyouhei Urabe).


ioquatix (Samuel Williams) wrote in #note-4:
> @shyouhei what was the point of https://github.com/ioquatix/ruby/commit/b121cfde5fbc8cd20684a5fd94047f40323a8353 - Performance? Consistency? Something else?

Hello, `rb_io_fptr_finalize_internal` this is a very tiny peephole optimisation to omit return value when possible.  I didn't indent to make it public.  If it is now that isn't what I want.

----------------------------------------
Bug #18455: `IO#close` has poor performance and difficult to understand semantics.
https://bugs.ruby-lang.org/issues/18455#change-97142

* Author: ioquatix (Samuel Williams)
* Status: Open
* Priority: Normal
* Backport: 2.6: UNKNOWN, 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN
----------------------------------------
`IO#close` should be responsible for closing the file descriptor referred to by the IO instance. When dealing with buffered IO, one can also expect this to flush the internal buffers if possible.

Currently, all blocking IO operations release the GVL and perform the blocking system call using `rb_thread_io_blocking_region`. The current implementation takes a file descriptor and adds an entry to the VM global `waiting_fds` list. When the operation is completed, the entry is removed from `waiting_fds`.

When calling `IO#close`, this list is traversed and any threads performing blocking operations with a matching file descriptor are interrupted. The performance of this is O(number of blocking IO operations) which in practice the performance of `IO#close` can take milliseconds with 10,000 threads performing blocking IO. This performance is unacceptable.

``` ruby
#!/usr/bin/env ruby

require 'benchmark'

class Reading
  def initialize
    @r, @w = IO.pipe

    @thread = Thread.new do
      @r.read
    rescue IOError
      # Ignore.
    end
  end

  attr :r
  attr :w

  attr :thread

  def join
    @thread.join
  end
end

def measure(count = 10)
  readings = count.times.map do
    Reading.new
  end

  sleep 10

  duration = Benchmark.measure do
    readings.each do |reading|
      reading.r.close
      reading.w.close
    end
  end

  average = (duration.total / count) * 1000.0
  pp count: count, average: sprintf("%0.2fms", average)

  readings.each(&:join)
end

measure(   10)
measure(  100)
measure( 1000)
measure(10000)
```

In addition, the semantics of this operation are confusing at best. While Ruby programs are dealing with IO instances, the VM is dealing with file descriptors, in effect performing some internal de-duplication of IO state. In practice, this leads to strange behaviour:

``` ruby
#!/usr/bin/env ruby

r, w = IO.pipe
r2 = IO.for_fd(r.to_i)
pp r: r, r2: r2

t = Thread.new do
  r2.read rescue nil
  r2.read # EBADF
end

sleep 0.5
r.close
t.join rescue nil

pp r: r, r2: r2
# r is closed, r2 is valid but will raise EBADF on any operation.
```

In addition, this confusing behaviour extends to Ractor and state is leaked between the two:

``` ruby
r, w = IO.pipe

ractor = Ractor.new(r.to_i) do |fd|
  r2 = IO.for_fd(fd)
  r2.read
  # r2.read # EBADF
end

sleep 0.5
r.close

pp take: ractor.take
```

I propose the following changes to simplify the semantics and improve performance:

- Move the semantics of `waiting_fds` from per-fd to per-IO. This means that `IO#close` only interrupts blocking operations performed on the same IO instance rather than ANY IO which refers to the same file descriptor. I think this behaviour is easier to understand and still protects against the vast majority of incorrect usage.
- Move the details of `struct rb_io_t` to `internal/io.h` so that the implementation details are not part of the public interface.

## Benchmarks

Before:

```
{:count=>10, :average=>"0.19ms"}
{:count=>100, :average=>"0.11ms"}
{:count=>1000, :average=>"0.18ms"}
{:count=>10000, :average=>"1.16ms"}
```

After:

```
{:count=>10, :average=>"0.20ms"}
{:count=>100, :average=>"0.11ms"}
{:count=>1000, :average=>"0.15ms"}
{:count=>10000, :average=>"0.68ms"}
```

After investigating this further I found that the `rb_thread_io_blocking_region` using `ubf_select` can be incredibly slow, proportional to the number of threads. I don't know whether it's advisable but:

``` c
        BLOCKING_REGION(blocking_node.thread, {
            val = func(data1);
            saved_errno = errno;
        }, NULL /* ubf_select */, blocking_node.thread, FALSE);
```

Disabling the UBF function and relying on `read(fd, ...)`/`write(fd, ...)` blocking operations to fail when `close(fd)` is invoked might be sufficient? This needs more investigation but after making this change, we have constant-time IO#close.

```
{:count=>10, :average=>"0.13ms"}
{:count=>100, :average=>"0.06ms"}
{:count=>1000, :average=>"0.04ms"}
{:count=>10000, :average=>"0.09ms"}
```

Which is ideally what we want.



-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>

In This Thread