Skip to content

Commit 5f89d55

Browse files
authored
Implement readAsBytes for POSIX (#203)
1 parent a36923b commit 5f89d55

File tree

7 files changed

+440
-1
lines changed

7 files changed

+440
-1
lines changed

.github/workflows/io_file.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ jobs:
4848

4949
- run: dart test --test-randomize-ordering-seed=random --platform vm
5050

51+
desktop-vm-benchmark:
52+
strategy:
53+
fail-fast: false
54+
matrix:
55+
sdk: [stable]
56+
# TODO(brianquinlan): Run benchmarks on Windows.
57+
os: [ubuntu-latest, macos-latest]
58+
runs-on: ${{ matrix.os }}
59+
steps:
60+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
61+
- uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c
62+
with:
63+
sdk: ${{ matrix.sdk }}
64+
- run: dart pub get
65+
- name: 🪑 Run benchmarks
66+
run: dart run benchmarks/read_as_bytes.dart
67+
5168
web-test:
5269
runs-on: ubuntu-latest
5370
steps:
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
import 'dart:typed_data';
7+
8+
import 'package:benchmark_harness/benchmark_harness.dart';
9+
import 'package:io_file/io_file.dart';
10+
11+
class ReadAsBytesBenchmark extends BenchmarkBase {
12+
late Directory dir;
13+
late String path;
14+
final int size;
15+
16+
ReadAsBytesBenchmark(super.name, this.size);
17+
18+
@override
19+
void setup() {
20+
dir = Directory.systemTemp.createTempSync('bench');
21+
path = '${dir.path}/file';
22+
File(path).writeAsBytesSync(Uint8List(size));
23+
}
24+
25+
@override
26+
void teardown() {
27+
dir.deleteSync(recursive: true);
28+
}
29+
}
30+
31+
class IOFilesReadAsBytesBenchmark extends ReadAsBytesBenchmark {
32+
IOFilesReadAsBytesBenchmark(int size)
33+
: super('IOFilesReadAsBytesBenchmark($size)', size);
34+
35+
static void main(int size) {
36+
IOFilesReadAsBytesBenchmark(size).report();
37+
}
38+
39+
@override
40+
void run() {
41+
fileSystem.readAsBytes(path);
42+
}
43+
}
44+
45+
class DartIOReadAsBytesBenchmark extends ReadAsBytesBenchmark {
46+
DartIOReadAsBytesBenchmark(int size)
47+
: super('DartIOReadAsBytesBenchmark($size)', size);
48+
49+
static void main(int size) {
50+
DartIOReadAsBytesBenchmark(size).report();
51+
}
52+
53+
@override
54+
void run() {
55+
File(path).readAsBytesSync();
56+
}
57+
}
58+
59+
void main() {
60+
for (var size in [0, 1024, 64 * 1024, 1024 * 1024, 64 * 1024 * 1024]) {
61+
DartIOReadAsBytesBenchmark.main(size);
62+
IOFilesReadAsBytesBenchmark.main(size);
63+
}
64+
}

pkgs/io_file/lib/src/file_system.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:typed_data';
6+
57
/// An abstract representation of a file system.
68
base class FileSystem {
79
/// Renames, and possibly moves a file system object from one path to another.
@@ -23,4 +25,9 @@ base class FileSystem {
2325
void rename(String oldPath, String newPath) {
2426
throw UnsupportedError('rename');
2527
}
28+
29+
/// Reads the entire file contents as a list of bytes.
30+
Uint8List readAsBytes(String path) {
31+
throw UnsupportedError('readAsBytes');
32+
}
2633
}

pkgs/io_file/lib/src/vm_posix_file_system.dart

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,38 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:ffi';
56
import 'dart:io' as io;
7+
import 'dart:math';
8+
import 'dart:typed_data';
69

10+
import 'package:ffi/ffi.dart' as ffi;
11+
import 'package:meta/meta.dart';
712
import 'package:stdlibc/stdlibc.dart' as stdlibc;
813

914
import 'file_system.dart';
1015

16+
// The maximum number of bytes to read in a single call to `read`.
17+
//
18+
// On macOS, it is an error to call
19+
// `read/_read(fildes, buf, nbyte)` with `nbyte >= INT_MAX`.
20+
//
21+
// The POSIX specification states that the behavior of `read` is
22+
// implementation-defined if `nbyte > SSIZE_MAX`. On Linux, the `read` will
23+
// transfer at most 0x7ffff000 bytes and return the number of bytes actually.
24+
// transfered.
25+
//
26+
// A smaller value has the advantage of making vm-service clients
27+
// (e.g. debugger) more responsive.
28+
//
29+
// A bigger value reduces the number of system calls.
30+
@visibleForTesting
31+
const int maxReadSize = 16 * 1024 * 1024; // 16MB.
32+
33+
// If the size of a file is unknown, read in blocks of this size.
34+
@visibleForTesting
35+
const int blockSize = 64 * 1024;
36+
1137
Exception _getError(int errno, String message, String path) {
1238
//TODO(brianquinlan): In the long-term, do we need to avoid exceptions that
1339
// are part of `dart:io`? Can we move those exceptions into a different
@@ -25,6 +51,21 @@ Exception _getError(int errno, String message, String path) {
2551
}
2652
}
2753

54+
/// Return the given function until the result is not `EINTR`.
55+
int _tempFailureRetry(int Function() f) {
56+
int result;
57+
do {
58+
result = f();
59+
} while (result == -1 && stdlibc.errno == stdlibc.EINTR);
60+
return result;
61+
}
62+
63+
/// The POSIX `read` function.
64+
///
65+
/// See https://pubs.opengroup.org/onlinepubs/9699919799/functions/read.html
66+
@Native<Int Function(Int, Pointer<Uint8>, Int)>(isLeaf: false)
67+
external int read(int fd, Pointer<Uint8> buf, int count);
68+
2869
/// A [FileSystem] implementation for POSIX systems (e.g. Android, iOS, Linux,
2970
/// macOS).
3071
base class PosixFileSystem extends FileSystem {
@@ -36,4 +77,77 @@ base class PosixFileSystem extends FileSystem {
3677
throw _getError(errno, 'rename failed', oldPath);
3778
}
3879
}
80+
81+
@override
82+
Uint8List readAsBytes(String path) {
83+
final fd = stdlibc.open(path, flags: stdlibc.O_RDONLY | stdlibc.O_CLOEXEC);
84+
if (fd == -1) {
85+
final errno = stdlibc.errno;
86+
throw _getError(errno, 'open failed', path);
87+
}
88+
try {
89+
final stat = stdlibc.fstat(fd);
90+
if (stat != null &&
91+
stat.st_size == 0 &&
92+
stat.st_mode & stdlibc.S_IFREG != 0) {
93+
return Uint8List(0);
94+
}
95+
final length = stat?.st_size ?? 0;
96+
if (length == 0) {
97+
return _readUnknownLength(path, fd);
98+
} else {
99+
return _readKnownLength(path, fd, length);
100+
}
101+
} finally {
102+
stdlibc.close(fd);
103+
}
104+
}
105+
106+
Uint8List _readUnknownLength(String path, int fd) => ffi.using((arena) {
107+
final buf = ffi.malloc<Uint8>(blockSize);
108+
final builder = BytesBuilder(copy: true);
109+
110+
while (true) {
111+
final r = _tempFailureRetry(() => read(fd, buf, blockSize));
112+
switch (r) {
113+
case -1:
114+
final errno = stdlibc.errno;
115+
throw _getError(errno, 'read failed', path);
116+
case 0:
117+
return builder.takeBytes();
118+
default:
119+
final typed = buf.asTypedList(r);
120+
builder.add(typed);
121+
}
122+
}
123+
});
124+
125+
Uint8List _readKnownLength(String path, int fd, int length) {
126+
final buffer = ffi.malloc<Uint8>(length);
127+
var bufferOffset = 0;
128+
129+
while (bufferOffset < length) {
130+
final r = _tempFailureRetry(
131+
() => read(
132+
fd,
133+
(buffer + bufferOffset).cast(),
134+
min(length - bufferOffset, maxReadSize),
135+
),
136+
);
137+
switch (r) {
138+
case -1:
139+
final errno = stdlibc.errno;
140+
ffi.calloc.free(buffer);
141+
throw _getError(errno, 'read failed', path);
142+
case 0:
143+
return buffer.asTypedList(
144+
bufferOffset,
145+
finalizer: ffi.calloc.nativeFree,
146+
);
147+
default:
148+
bufferOffset += r;
149+
}
150+
}
151+
return buffer.asTypedList(length, finalizer: ffi.calloc.nativeFree);
152+
}
39153
}

pkgs/io_file/pubspec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ environment:
99

1010
dependencies:
1111
ffi: ^2.1.4
12+
meta: ^1.16.0
1213
stdlibc:
1314
git:
1415
# Change this to a released version.
1516
url: https://github.com/canonical/stdlibc.dart.git
1617
win32: ^5.11.0
1718

1819
dev_dependencies:
20+
args: ^2.7.0
21+
benchmark_harness: ^2.3.1
1922
dart_flutter_team_lints: ^3.4.0
2023
test: ^1.24.0

0 commit comments

Comments
 (0)