Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,19 @@ Two runs before this change used 1,549–1,793 MiB. All runs reached the same
117 tests and retained the same five existing failures; this measures resource
usage, not full FileUtils compatibility. The CI limits and test inputs are
unchanged.

## Positioned positive-length reads

Regular `File#read(length)` calls without universal newline conversion now
open the existing path, seek to the logical position, and read at most the
requested byte count in 64 KiB chunks. This avoids a whole-file copy for each
small read. It keeps the existing path-based behavior and does not cache file
contents, so changes between reads remain visible. Device reads and newline
conversion retain their existing path.

The `file_read_checks.rb` benchmark above also exercises this path through
`File.foreach`. One local O2 comparison against `23ce853`, using Mere
`991f299b` and the default 1 MiB input, reduced the foreach portion from
28.12 seconds to 24.47 seconds with identical output. Other validation ran
concurrently; this is indicative, not an isolated benchmark or a CI duration
claim. Per-line interpreter work still dominates much of this workload.
44 changes: 44 additions & 0 deletions corpus/502_positioned_file_reads.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'tmpdir'

Dir.mktmpdir do |dir|
path = File.join(dir, 'data')
data = (0..255).to_a.pack('C*') * 600
File.binwrite(path, data)
[1, 65535, 65536, 65537, 200000].each do |length|
Comment thread
284km marked this conversation as resolved.
Resolved
File.open(path, 'rb') do |f|
f.seek(7)
actual = f.read(length)
expected = data.byteslice(7, length)
p [length, actual == expected, actual.encoding, f.pos]
end
end
File.open(path, 'rb') do |f|
f.seek(data.bytesize - 3)
f.ungetbyte('xyz')
p [f.read(0), f.read(2), f.read(10).bytes, f.pos, f.read(1)]
f.seek(data.bytesize + 5)
p [f.read(1), f.pos, f.read(0)]
end
File.open(path, 'r+b') do |f|
p f.read(2).bytes
f.write('XY')
f.seek(1)
p f.read(4).bytes
f.truncate(3)
f.seek(2)
p [f.read(20), f.pos, f.read(1)]
end
File.open(path, 'rb') do |f|
p f.read(1).bytes
File.binwrite(path, 'updated')
p f.read(3)
File.truncate(path, 0)
p [f.read(1), f.read(0)]
end
File.binwrite(path, "first\r\nsecond\rlast\n")
File.open(path, 'rt', universal_newline: true) do |f|
p [f.read(3), f.gets, f.read]
end
File.open('/dev/zero', 'rb') { |f| p f.read(3).bytes }
File.open(File::NULL, 'rb') { |f| p [f.read(1), f.read(0), f.read] }
end
37 changes: 35 additions & 2 deletions main.mere
Original file line number Diff line number Diff line change
Expand Up @@ -22982,6 +22982,13 @@ and invoke_val_c0 = fn env -> fn world -> fn blk -> fn (recv0: Val) -> fn (name0
// check nor a read satisfied by pushback needs the file's bytes.
if n == 0 then mkstr_e "" "ASCII-8BIT"
else
(let uc9 = file_univ_code ivars k mode in
let (ranged9, bytes9) = (if n > 0 && uc9 == 0 then file_read_range path pos n else (false, "")) in
if ranged9 then
(if str_len bytes9 == 0 then VNil
else (let _ = map_set ivars (k ++ "@__f_pos") (VInt (pos + str_len bytes9)) in
mkstr_e bytes9 "ASCII-8BIT"))
else
(let content = (if file_exists path then read_file path else "") in
// ⚠ A FILE WITH NO BYTES MAY BE A DEVICE: /dev/zero reads as the
// empty whole-file string and has any number of bytes to give.
Expand All @@ -22993,7 +23000,6 @@ and invoke_val_c0 = fn env -> fn world -> fn blk -> fn (recv0: Val) -> fn (name0
let got9 = (if fd9 < 0 then 0 else fd_read fd9 p9 (if n > 65536 then 65536 else n)) in
if got9 > 0 then mem_to_str p9 got9 else "")
else "") in
let uc9 = file_univ_code ivars k mode in
if str_len dev9 > 0 then mkstr_e dev9 "ASCII-8BIT"
else if uc9 > 0 && (n < 0 || (n > 0 && pos < str_len content)) then
(let (t9, e9) = univ_take content (if pos > str_len content then str_len content else pos) n uc9 in
Expand All @@ -23008,7 +23014,7 @@ and invoke_val_c0 = fn env -> fn world -> fn blk -> fn (recv0: Val) -> fn (name0
else
(let stop = (if pos + n > str_len content then str_len content else pos + n) in
let _ = map_set ivars (k ++ "@__f_pos") (VInt stop) in
mkstr_e (substring content pos stop) "ASCII-8BIT")) in
mkstr_e (substring content pos stop) "ASCII-8BIT"))) in
// what #ungetc / #ungetbyte pushed back (the prelude keeps it in
// @__f_unget) comes out first, as ruby's read buffer hands it out
let ug9 = (match (if map_has ivars (k ++ "@__f_unget") then map_get ivars (k ++ "@__f_unget") else VNil) with
Expand Down Expand Up @@ -45604,6 +45610,33 @@ and file_univ_code = fn ivars -> fn (k: str) -> fn (mode: str) ->
let e9 = enc_canon_name (if c2 < 0 then rest9 else substring rest9 0 c2) in
if str_eq e9 "UTF-16LE" then 2 else if str_eq e9 "UTF-16BE" then 3
else if str_eq e9 "UTF-32LE" then 4 else if str_eq e9 "UTF-32BE" then 5 else 1))
// File still owns a path and a logical position. Read only the requested
// range of a regular file without moving a descriptor exposed to Ruby.
// Devices, newline conversion, and failures keep the existing read path.
and file_read_range = fn (path: str) -> fn (pos: int) -> fn (count: int) ->
let mode9 = fs_mode path true in
if pos < 0 || mode9 < 0 || bit_and mode9 61440 != 32768 then (false, "")
else
(let fd9 = fd_open path "r" 438 in
if fd9 < 0 then (false, "")
else
(let moved9 = fd_seek fd9 pos 0 in
if moved9 < 0 then (let _ = fd_close fd9 in (false, ""))
else
(let buf9 = strbuf_new () in
let arena9 = tcp_arena () in
let rec read9 = fn (left9: int) ->
if left9 <= 0 then true
else
(let want9 = (if left9 > 65536 then 65536 else left9) in
let got9 = fd_read fd9 arena9 want9 in
if got9 < 0 then false
else if got9 == 0 then true
else (let _ = strbuf_push buf9 (mem_to_str arena9 got9) in
Comment thread
284km marked this conversation as resolved.
Resolved
Comment thread
284km marked this conversation as resolved.
Resolved
read9 (left9 - got9))) in
let ok9 = read9 count in
let _ = fd_close fd9 in
(ok9, strbuf_to_str buf9))))
and file_write_at = fn world -> fn (id: int) -> fn (piece: str) ->
let (_, _, _, iv9) = world in
let k9 = str_of_int id in
Expand Down
Loading