Skip to content

Commit 18089f0

Browse files
committed
Remove write buffering for IO
* The existing write buffering from Rubinius is buggy, complicated, and missing synchronization. Write buffering does not seem needed in Ruby (unlike read buffering). It also caused an extra copy of the bytes. * Also makes it easier to reason about RDWR IOs since now there cannot be both directions using the same buffer. * Ensure every usage of @ibuffer is guarded by #ensure_open_and_readable or handles @ibuffer being nil. * Fixes #2034
1 parent f05cb2b commit 18089f0

File tree

5 files changed

+26
-72
lines changed

5 files changed

+26
-72
lines changed

spec/tags/core/io/flush_tags.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fails:IO#flush on a pipe raises Errno::EPIPE if sync=false and the read end is closed

src/main/ruby/truffleruby/core/io.rb

+17-66
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,6 @@ class EINPROGRESSWaitWritable < Errno::EINPROGRESS
8787
# equals total, no additional bytes will be filled until the
8888
# buffer is emptied.
8989
#
90-
# As a write buffer, +empty_to+ removes bytes from +start+ up
91-
# to +used+. When +start+ equals +used+, no additional bytes
92-
# will be emptied until the buffer is filled.
93-
#
9490
# IO presents a stream of input. Buffer presents buckets of
9591
# input. IO's task is to chain the buckets so the user sees
9692
# a stream. IO explicitly requests that the buffer be filled
@@ -108,7 +104,6 @@ def initialize
108104
@start = 0
109105
@used = 0
110106
@total = SIZE
111-
@write_synced = true
112107
@eof = false
113108
end
114109

@@ -144,26 +139,6 @@ def reset!
144139
@start = 0
145140
@used = 0
146141
@eof = false
147-
@write_synced = true
148-
end
149-
150-
def write_synced?
151-
@write_synced
152-
end
153-
154-
# Returns the number of bytes that could be written to the buffer.
155-
# If the number is less then the expected, then we need to +empty_to+
156-
# the IO, and +unshift+ again beginning at +start_pos+.
157-
def unshift(str, start_pos)
158-
@write_synced = false
159-
free = unused
160-
written = str.bytesize - start_pos
161-
if written > free
162-
written = free
163-
end
164-
@storage.fill(@used, str, start_pos, written)
165-
@used += written
166-
written
167142
end
168143

169144
def fill(io, max = DEFAULT_READ_SIZE)
@@ -192,7 +167,6 @@ def fill(io, max = DEFAULT_READ_SIZE)
192167
# Returns the number of bytes in the buffer.
193168
def fill_from(io, skip = nil, max = DEFAULT_READ_SIZE)
194169
Truffle::System.synchronized(self) do
195-
empty_to io
196170
discard skip if skip
197171

198172
if empty?
@@ -207,16 +181,6 @@ def fill_from(io, skip = nil, max = DEFAULT_READ_SIZE)
207181
end
208182
end
209183

210-
def empty_to(io)
211-
return 0 if @write_synced or empty?
212-
213-
data = String.from_bytearray(@storage, @start, size, Encoding::ASCII_8BIT)
214-
Truffle::POSIX.write_string io, data, true
215-
reset!
216-
217-
size
218-
end
219-
220184
# Advances the beginning-of-buffer marker past any number
221185
# of contiguous characters == +skip+. For example, if +skip+
222186
# is ?\n and the buffer contents are "\n\n\nAbc...", the
@@ -238,8 +202,6 @@ def find(pattern, discard = nil)
238202

239203
def unseek!(io)
240204
Truffle::System.synchronized(self) do
241-
# Unseek the still buffered amount
242-
return unless write_synced?
243205
unless empty?
244206
amount = @start - @used
245207
r = Truffle::POSIX.lseek(io.fileno, amount, IO::SEEK_CUR)
@@ -1026,16 +988,19 @@ def self.setup(io, fd, mode=nil, sync=false)
1026988
if cur_mode and (cur_mode == RDONLY or cur_mode == WRONLY) and mode != cur_mode
1027989
raise Errno::EINVAL, "Invalid new mode for existing descriptor #{fd}"
1028990
end
991+
else
992+
mode = cur_mode or raise 'No mode given for IO'
1029993
end
1030994

1031995
# Close old descriptor if there was already one associated
1032996
io.close if Primitive.io_fd(io) != -1
1033997

1034998
Primitive.io_set_fd(io, fd)
1035-
io.instance_variable_set :@mode, mode || cur_mode
1036-
io.sync = !!sync
999+
io.instance_variable_set :@mode, mode
1000+
io.sync = !!sync
10371001
io.autoclose = true
1038-
io.instance_variable_set :@ibuffer, IO::InternalBuffer.new
1002+
ibuffer = mode != WRONLY ? IO::InternalBuffer.new : nil
1003+
io.instance_variable_set :@ibuffer, ibuffer
10391004
io.instance_variable_set :@lineno, 0
10401005

10411006
# Truffle: STDOUT isn't defined by the time this call is made during bootstrap, so we need to guard it.
@@ -1156,7 +1121,7 @@ def binmode?
11561121

11571122
# Used to find out if there is buffered data available.
11581123
private def buffer_empty?
1159-
@ibuffer.empty?
1124+
@ibuffer.nil? or @ibuffer.empty?
11601125
end
11611126

11621127
def close_on_exec=(value)
@@ -1640,7 +1605,6 @@ def fileno
16401605
# no newline
16411606
def flush
16421607
ensure_open
1643-
@ibuffer.empty_to self
16441608
self
16451609
end
16461610

@@ -1658,7 +1622,7 @@ def fsync
16581622
end
16591623

16601624
def getbyte
1661-
ensure_open
1625+
ensure_open_and_readable
16621626
@ibuffer.getbyte(self)
16631627
end
16641628

@@ -1670,7 +1634,7 @@ def getbyte
16701634
# f.getc #=> 84
16711635
# f.getc #=> 104
16721636
def getc
1673-
ensure_open
1637+
ensure_open_and_readable
16741638
@ibuffer.getchar(self)
16751639
end
16761640

@@ -1904,7 +1868,7 @@ def read(length=nil, buffer=nil)
19041868
# buffer like readpartial. In this case, read(2) is not called.
19051869
def read_nonblock(size, buffer = nil, exception: true)
19061870
raise ArgumentError, 'illegal read size' if size < 0
1907-
ensure_open
1871+
ensure_open_and_readable
19081872
self.nonblock = true
19091873

19101874
buffer = StringValue buffer if buffer
@@ -2026,7 +1990,7 @@ def readlines(sep_or_limit=$/, limit=nil)
20261990
# blocks on the situation IO#sysread causes Errno::EAGAIN as if the fd is blocking mode.
20271991
def readpartial(size, buffer=nil)
20281992
raise ArgumentError, 'negative string size' unless size >= 0
2029-
ensure_open
1993+
ensure_open_and_readable
20301994

20311995
if buffer
20321996
buffer = StringValue(buffer)
@@ -2091,6 +2055,7 @@ def reopen(other, mode=undefined)
20912055
# We need to use that mode of other here like MRI, and not fcntl(), because fcntl(fd, F_GETFL)
20922056
# gives O_RDWR for the 3 standard IOs, even though they are not bidirectional.
20932057
@mode = other.instance_variable_get :@mode
2058+
@ibuffer = (@mode & ACCMODE) != WRONLY ? IO::InternalBuffer.new : nil
20942059

20952060
if io.respond_to?(:path)
20962061
@path = io.path
@@ -2128,6 +2093,7 @@ def reopen(other, mode=undefined)
21282093
Errno.handle if mode < 0
21292094

21302095
@mode = mode
2096+
@ibuffer = (@mode & ACCMODE) != WRONLY ? IO::InternalBuffer.new : nil
21312097
end
21322098

21332099
self
@@ -2137,7 +2103,7 @@ def reopen(other, mode=undefined)
21372103
# Internal method used to reset the state of the buffer, including the
21382104
# physical position in the stream.
21392105
private def reset_buffering
2140-
@ibuffer.unseek! self
2106+
@ibuffer.unseek! self if @ibuffer
21412107
end
21422108

21432109
##
@@ -2347,6 +2313,7 @@ def sync=(v)
23472313
# @todo Improve reading into provided buffer.
23482314
#
23492315
def sysread(number_of_bytes, buffer=undefined)
2316+
ensure_open_and_readable
23502317
flush
23512318
raise IOError unless @ibuffer.empty?
23522319

@@ -2370,11 +2337,7 @@ def sysread(number_of_bytes, buffer=undefined)
23702337
# f.sysread(10) #=> "And so on."
23712338
def sysseek(amount, whence=SEEK_SET)
23722339
ensure_open
2373-
if @ibuffer.write_synced?
2374-
raise IOError unless @ibuffer.empty?
2375-
else
2376-
warn 'sysseek for buffered IO', uplevel: 1
2377-
end
2340+
raise IOError unless buffer_empty?
23782341

23792342
amount = Integer(amount)
23802343
r = Truffle::POSIX.lseek(Primitive.io_fd(self), amount, whence)
@@ -2457,19 +2420,7 @@ def write(data)
24572420
end
24582421
end
24592422

2460-
if @sync
2461-
Truffle::POSIX.write_string self, data, true
2462-
else
2463-
reset_buffering
2464-
bytes_to_write = data.bytesize
2465-
2466-
while bytes_to_write > 0
2467-
bytes_to_write -= @ibuffer.unshift(data, data.bytesize - bytes_to_write)
2468-
@ibuffer.empty_to self if @ibuffer.full?
2469-
end
2470-
end
2471-
2472-
data.bytesize
2423+
Truffle::POSIX.write_string self, data, true
24732424
end
24742425

24752426
def write_nonblock(data, exception: true)

test/mri/excludes/TestIO.rb

+1
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,4 @@
8282
exclude :test_write_with_many_arguments, "needs investigation"
8383
exclude :test_write_with_multiple_arguments, "needs investigation"
8484
exclude :test_write_with_multiple_arguments_and_buffer, "needs investigation"
85+
exclude :test_copy_stream_dst_rbuf, "seems to require write buffering"

test/truffle/compiler/can-we-fold-yet.sh

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ jt ruby --experimental-options --engine.IterativePartialEscape test/truffle/comp
66

77
if ! cmp test/truffle/compiler/can-we-fold-yet/expected.txt actual.txt
88
then
9+
set +x
910
echo Output not as expected
1011
echo Expected:
1112
cat test/truffle/compiler/can-we-fold-yet/expected.txt
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
> 14
2-
> 14 + 2
3-
> [1, 2, 3][1]
4-
> [1, -2, 3].map(&:abs)[1]
5-
> rand
6-
> exit
71
Can TruffleRuby constant fold yet?
2+
> 14
83
Yes! Truffle can constant fold this to 14
4+
> 14 + 2
95
Yes! Truffle can constant fold this to 16
6+
> [1, 2, 3][1]
107
Yes! Truffle can constant fold this to 2
8+
> [1, -2, 3].map(&:abs)[1]
119
Yes! Truffle can constant fold this to 2
10+
> rand
1211
No :( Truffle can't constant fold that
12+
> exit

0 commit comments

Comments
 (0)