authorgravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2026-01-10 15:34:36-05:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-30 12:10:01-08:00
logdea653fdb97c62968138c28496f25510b5d50024
tree037776338497fd9675c83c327ba38316cfedb66f
parent5456f953fab3254bd87a1be8d7c42b15d4bb8138

Io: add ring to `Batch` API


5 files changed, 258 insertions(+), 158 deletions(-)

lib/std/Io.zig+121-48
...@@ -149,9 +149,8 @@ pub const VTable = struct {...@@ -149,9 +149,8 @@ pub const VTable = struct {
149 futexWaitUncancelable: *const fn (?*anyopaque, ptr: *const u32, expected: u32) void,149 futexWaitUncancelable: *const fn (?*anyopaque, ptr: *const u32, expected: u32) void,
150 futexWake: *const fn (?*anyopaque, ptr: *const u32, max_waiters: u32) void,150 futexWake: *const fn (?*anyopaque, ptr: *const u32, max_waiters: u32) void,
151151
152 batch: *const fn (?*anyopaque, []Operation) ConcurrentError!void,152 operate: *const fn (?*anyopaque, *Operation) Cancelable!void,
153 batchSubmit: *const fn (?*anyopaque, *Batch) void,153 batchWait: *const fn (?*anyopaque, *Batch, Timeout) Batch.WaitError!void,
154 batchWait: *const fn (?*anyopaque, *Batch, resubmissions: []const usize, Timeout) Batch.WaitError!usize,
155 batchCancel: *const fn (?*anyopaque, *Batch) void,154 batchCancel: *const fn (?*anyopaque, *Batch) void,
156155
157 dirCreateDir: *const fn (?*anyopaque, Dir, []const u8, Dir.Permissions) Dir.CreateDirError!void,156 dirCreateDir: *const fn (?*anyopaque, Dir, []const u8, Dir.Permissions) Dir.CreateDirError!void,
...@@ -261,48 +260,50 @@ pub const Operation = union(enum) {...@@ -261,48 +260,50 @@ pub const Operation = union(enum) {
261260
262 pub const Noop = struct {261 pub const Noop = struct {
263 reserved: [2]usize = .{ 0, 0 },262 reserved: [2]usize = .{ 0, 0 },
264 status: Status(void) = .{ .result = {} },263 status: Status(void) = .{ .unstarted = {} },
265 };264 };
266265
267 /// Returns 0 on end of stream.266 /// Returns 0 on end of stream.
268 pub const FileReadStreaming = struct {267 pub const FileReadStreaming = struct {
269 file: File,268 file: File,
270 data: []const []u8,269 data: []const []u8,
271 status: Status(File.Reader.Error!usize) = .{ .unstarted = {} },270 status: Status(Error!usize) = .{ .unstarted = {} },
271
272 pub const Error = error{
273 InputOutput,
274 SystemResources,
275 /// Trying to read a directory file descriptor as if it were a file.
276 IsDir,
277 BrokenPipe,
278 ConnectionResetByPeer,
279 /// File was not opened with read capability.
280 NotOpenForReading,
281 SocketUnconnected,
282 /// Non-blocking has been enabled, and reading from the file descriptor
283 /// would block.
284 WouldBlock,
285 /// In WASI, this error occurs when the file descriptor does
286 /// not hold the required rights to read from it.
287 AccessDenied,
288 /// Unable to read file due to lock. Depending on the `Io` implementation,
289 /// reading from a locked file may return this error, or may ignore the
290 /// lock.
291 LockViolation,
292 } || Io.UnexpectedError;
272 };293 };
273294
274 pub fn Status(Result: type) type {295 pub fn Status(Result: type) type {
275 return union {296 return union {
276 unstarted: void,297 unstarted: void,
277 pending: usize,298 pending: *Batch,
278 result: Result,299 result: Result,
279 };300 };
280 }301 }
281};302};
282303
283/// Performs all `operations` in an unspecified order, concurrently.
284///
285/// Returns after all `operations` have been completed. If the operations could
286/// not be completed concurrently, returns `error.ConcurrencyUnavailable`.
287///
288/// With this API, it is rare for concurrency to not be available. Even a
289/// single-threaded `Io` implementation can, for example, take advantage of
290/// poll() to implement this. Note that poll() is fallible however.
291///
292/// If `operations.len` is one, `error.ConcurrencyUnavailable` is unreachable.
293///
294/// On entry, all operations must already have `.status = .unstarted` except
295/// noops must have `.status = .{ .result = {} }`, to safety check the state
296/// transitions.
297///
298/// On return, all operations have `.status = .{ .result = ... }`.
299pub fn batch(io: Io, operations: []Operation) ConcurrentError!void {
300 return io.vtable.batch(io.userdata, operations);
301}
302
303/// Performs one `Operation`.304/// Performs one `Operation`.
304pub fn operate(io: Io, operation: *Operation) void {305pub fn operate(io: Io, operation: *Operation) Cancelable!void {
305 return io.vtable.batch(io.userdata, (operation)[0..1]) catch unreachable;306 return io.vtable.operate(io.userdata, operation) catch unreachable;
306}307}
307308
308/// Submits many operations together without waiting for all of them to309/// Submits many operations together without waiting for all of them to
...@@ -312,35 +313,107 @@ pub fn operate(io: Io, operation: *Operation) void {...@@ -312,35 +313,107 @@ pub fn operate(io: Io, operation: *Operation) void {
312/// level API that operates on `Future`, see `Select`.313/// level API that operates on `Future`, see `Select`.
313pub const Batch = struct {314pub const Batch = struct {
314 operations: []Operation,315 operations: []Operation,
315 index: usize,316 ring: [*]u32,
316 reserved: ?*anyopaque,317 user: struct {
318 submit_tail: RingIndex,
319 complete_head: RingIndex,
320 complete_tail: RingIndex,
321 },
322 impl: struct {
323 submit_head: RingIndex,
324 submit_tail: RingIndex,
325 complete_tail: RingIndex,
326 reserved: ?*anyopaque,
327 },
328
329 pub const RingIndex = enum(u32) {
330 _,
331
332 pub fn index(ri: RingIndex, len: u31) u31 {
333 const i = @intFromEnum(ri);
334 assert(i < @as(u32, len) * 2);
335 return @intCast(if (i < len) i else i - len);
336 }
337
338 pub fn prev(ri: RingIndex, len: u31) RingIndex {
339 const i = @intFromEnum(ri);
340 const double_len = @as(u32, len) * 2;
341 assert(i <= double_len);
342 return @enumFromInt((if (i > 0) i else double_len) - 1);
343 }
344
345 pub fn next(ri: RingIndex, len: u31) RingIndex {
346 const i = @intFromEnum(ri) + 1;
347 const double_len = @as(u32, len) * 2;
348 assert(i <= double_len);
349 return @enumFromInt(if (i < double_len) i else 0);
350 }
351 };
352
353 pub const WaitError = ConcurrentError || Cancelable || Timeout.Error;
317354
318 pub fn init(operations: []Operation) Batch {355 pub fn init(operations: []Operation, ring: []u32) Batch {
319 return .{ .operations = operations, .index = 0, .reserved = null };356 const len: u31 = @intCast(operations.len);
357 assert(ring.len == len);
358 return .{
359 .operations = operations,
360 .ring = ring.ptr,
361 .user = .{
362 .submit_tail = @enumFromInt(0),
363 .complete_head = @enumFromInt(0),
364 .complete_tail = @enumFromInt(0),
365 },
366 .impl = .{
367 .submit_head = @enumFromInt(0),
368 .submit_tail = @enumFromInt(0),
369 .complete_tail = @enumFromInt(0),
370 .reserved = null,
371 },
372 };
320 }373 }
321374
322 /// Submits all non-noop `operations`.375 /// Adds `b.operations[operation]` to the list of submitted operations
323 pub fn submit(b: *Batch, io: Io) void {376 /// that will be performed when `wait` is called.
324 return io.vtable.batchSubmit(io.userdata, b);377 pub fn add(b: *Batch, operation: usize) void {
378 const tail = b.user.submit_tail;
379 const len: u31 = @intCast(b.operations.len);
380 b.user.submit_tail = tail.next(len);
381 b.ring[0..len][tail.index(len)] = @intCast(operation);
325 }382 }
326383
327 pub const WaitError = ConcurrentError || Cancelable || Timeout.Error;384 fn flush(b: *Batch) void {
385 @atomicStore(RingIndex, &b.impl.submit_tail, b.user.submit_tail, .release);
386 }
328387
329 /// Resubmits the previously completed or noop-initialized `operations` at388 /// Returns `operation` such that `b.operations[operation]` has completed.
330 /// indexes given by `resubmissions`. This set of indexes typically will be empty389 /// Returns `null` when `wait` should be called.
331 /// on the first call to `await` since all operations have already been390 pub fn next(b: *Batch) ?u32 {
332 /// submitted via `async`.391 const head = b.user.complete_head;
333 ///392 if (head == b.user.complete_tail) {
334 /// Returns the index of a completed `Operation`, or `operations.len` if393 @branchHint(.unlikely);
335 /// all operations are completed.394 b.flush();
395 const tail = @atomicLoad(RingIndex, &b.impl.complete_tail, .acquire);
396 if (head == tail) {
397 @branchHint(.unlikely);
398 return null;
399 }
400 assert(head != tail);
401 b.user.complete_tail = tail;
402 }
403 const len: u31 = @intCast(b.operations.len);
404 b.user.complete_head = head.next(len);
405 return b.ring[0..len][head.index(len)];
406 }
407
408 /// Starts work on any submitted operations and returns when at least one has completeed.
336 ///409 ///
337 /// When `error.Canceled` is returned, all operations have already completed.410 /// Returns `error.Timeout` if `timeout` expires first.
338 pub fn wait(b: *Batch, io: Io, resubmissions: []const usize, timeout: Timeout) WaitError!usize {411 pub fn wait(b: *Batch, io: Io, timeout: Timeout) WaitError!void {
339 return io.vtable.batchWait(io.userdata, b, resubmissions, timeout);412 return io.vtable.batchWait(io.userdata, b, timeout);
340 }413 }
341414
342 /// Returns after all `operations` have completed. Each operation415 /// Returns after all `operations` have completed. Operations which have not completed
343 /// independently may or may not have been canceled.416 /// after this function returns were successfully dropped and had no side effects.
344 pub fn cancel(b: *Batch, io: Io) void {417 pub fn cancel(b: *Batch, io: Io) void {
345 return io.vtable.batchCancel(io.userdata, b);418 return io.vtable.batchCancel(io.userdata, b);
346 }419 }
lib/std/Io/File.zig+1-1
...@@ -558,7 +558,7 @@ pub fn readStreaming(file: File, io: Io, buffer: []const []u8) Reader.Error!usiz...@@ -558,7 +558,7 @@ pub fn readStreaming(file: File, io: Io, buffer: []const []u8) Reader.Error!usiz
558 .file = file,558 .file = file,
559 .data = buffer,559 .data = buffer,
560 } };560 } };
561 io.operate(&operation);561 try io.operate(&operation);
562 return operation.file_read_streaming.status.result;562 return operation.file_read_streaming.status.result;
563}563}
564564
lib/std/Io/File/Reader.zig+1-21
...@@ -26,27 +26,7 @@ size_err: ?SizeError = null,...@@ -26,27 +26,7 @@ size_err: ?SizeError = null,
26seek_err: ?SeekError = null,26seek_err: ?SeekError = null,
27interface: Io.Reader,27interface: Io.Reader,
2828
29pub const Error = error{29pub const Error = Io.Operation.FileReadStreaming.Error || Io.Cancelable;
30 InputOutput,
31 SystemResources,
32 /// Trying to read a directory file descriptor as if it were a file.
33 IsDir,
34 BrokenPipe,
35 ConnectionResetByPeer,
36 /// File was not opened with read capability.
37 NotOpenForReading,
38 SocketUnconnected,
39 /// Non-blocking has been enabled, and reading from the file descriptor
40 /// would block.
41 WouldBlock,
42 /// In WASI, this error occurs when the file descriptor does
43 /// not hold the required rights to read from it.
44 AccessDenied,
45 /// Unable to read file due to lock. Depending on the `Io` implementation,
46 /// reading from a locked file may return this error, or may ignore the
47 /// lock.
48 LockViolation,
49} || Io.Cancelable || Io.UnexpectedError;
5030
51pub const SizeError = File.StatError || error{31pub const SizeError = File.StatError || error{
52 /// Occurs if, for example, the file handle is a network socket and therefore does not have a size.32 /// Occurs if, for example, the file handle is a network socket and therefore does not have a size.
lib/std/Io/Threaded.zig+102-49
...@@ -1587,8 +1587,7 @@ pub fn io(t: *Threaded) Io {...@@ -1587,8 +1587,7 @@ pub fn io(t: *Threaded) Io {
1587 .futexWaitUncancelable = futexWaitUncancelable,1587 .futexWaitUncancelable = futexWaitUncancelable,
1588 .futexWake = futexWake,1588 .futexWake = futexWake,
15891589
1590 .batch = batch,1590 .operate = operate,
1591 .batchSubmit = batchSubmit,
1592 .batchWait = batchWait,1591 .batchWait = batchWait,
1593 .batchCancel = batchCancel,1592 .batchCancel = batchCancel,
15941593
...@@ -1751,8 +1750,7 @@ pub fn ioBasic(t: *Threaded) Io {...@@ -1751,8 +1750,7 @@ pub fn ioBasic(t: *Threaded) Io {
1751 .futexWaitUncancelable = futexWaitUncancelable,1750 .futexWaitUncancelable = futexWaitUncancelable,
1752 .futexWake = futexWake,1751 .futexWake = futexWake,
17531752
1754 .batch = batch,1753 .operate = operate,
1755 .batchSubmit = batchSubmit,
1756 .batchWait = batchWait,1754 .batchWait = batchWait,
1757 .batchCancel = batchCancel,1755 .batchCancel = batchCancel,
17581756
...@@ -2456,59 +2454,82 @@ fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {...@@ -2456,59 +2454,82 @@ fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {
2456 Thread.futexWake(ptr, max_waiters);2454 Thread.futexWake(ptr, max_waiters);
2457}2455}
24582456
2459fn batchSubmit(userdata: ?*anyopaque, b: *Io.Batch) void {2457fn operate(userdata: ?*anyopaque, op: *Io.Operation) Io.Cancelable!void {
2460 const t: *Threaded = @ptrCast(@alignCast(userdata));2458 const t: *Threaded = @ptrCast(@alignCast(userdata));
2461 _ = t;2459 _ = t;
2462 _ = b;
2463 return;
2464}
2465
2466fn operate(op: *Io.Operation) void {
2467 switch (op.*) {2460 switch (op.*) {
2468 .noop => {},2461 .noop => |*o| {
2469 .file_read_streaming => |*o| o.status = .{ .result = fileReadStreaming(o.file, o.data) },2462 _ = o.status.unstarted;
2463 o.status = .{ .result = {} };
2464 },
2465 .file_read_streaming => |*o| {
2466 _ = o.status.unstarted;
2467 o.status = .{ .result = fileReadStreaming(o.file, o.data) catch |err| switch (err) {
2468 error.Canceled => return error.Canceled,
2469 else => |e| e,
2470 } };
2471 },
2470 }2472 }
2471}2473}
24722474
2473fn batchWait(2475fn batchWait(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout) Io.Batch.WaitError!void {
2474 userdata: ?*anyopaque,
2475 b: *Io.Batch,
2476 resubmissions: []const usize,
2477 timeout: Io.Timeout,
2478) Io.Batch.WaitError!usize {
2479 _ = resubmissions;
2480 const t: *Threaded = @ptrCast(@alignCast(userdata));2476 const t: *Threaded = @ptrCast(@alignCast(userdata));
2481 const operations = b.operations;2477 const operations = b.operations;
2482 if (operations.len == 1) {2478 const len: u31 = @intCast(operations.len);
2483 operate(&operations[0]);2479 const ring = b.ring[0..len];
2484 return b.operations.len;2480 var submit_head = b.impl.submit_head;
2481 const submit_tail = b.user.submit_tail;
2482 b.impl.submit_tail = submit_tail;
2483 var complete_tail = b.impl.complete_tail;
2484 var map_buffer: [poll_buffer_len]u32 = undefined; // poll_buffer index to operations index
2485 var poll_i: usize = 0;
2486 defer {
2487 for (map_buffer[0..poll_i]) |op| {
2488 submit_head = submit_head.prev(len);
2489 ring[submit_head.index(len)] = op;
2490 }
2491 b.impl.submit_head = submit_head;
2492 b.impl.complete_tail = complete_tail;
2493 b.user.complete_tail = complete_tail;
2485 }2494 }
2486 if (is_windows) @panic("TODO");2495 if (is_windows) @panic("TODO");
2487
2488 var poll_buffer: [poll_buffer_len]posix.pollfd = undefined;2496 var poll_buffer: [poll_buffer_len]posix.pollfd = undefined;
2489 var map_buffer: [poll_buffer_len]u8 = undefined; // poll_buffer index to operations index2497 while (submit_head != submit_tail) : (submit_head = submit_head.next(len)) {
2490 var poll_i: usize = 0;2498 const op = ring[submit_head.index(len)];
24912499 const operation = &operations[op];
2492 for (operations, 0..) |*op, operation_index| switch (op.*) {2500 switch (operation.*) {
2493 .noop => continue,2501 else => {
2494 .file_read_streaming => |*o| {2502 try operate(t, operation);
2495 if (poll_buffer.len - poll_i == 0) return error.ConcurrencyUnavailable;2503 ring[complete_tail.index(len)] = op;
2496 poll_buffer[poll_i] = .{2504 complete_tail = complete_tail.next(len);
2497 .fd = o.file.handle,2505 },
2498 .events = posix.POLL.IN,2506 .file_read_streaming => |*o| {
2499 .revents = 0,2507 _ = o.status.unstarted;
2500 };2508 if (poll_buffer.len - poll_i == 0) return error.ConcurrencyUnavailable;
2501 map_buffer[poll_i] = @intCast(operation_index);2509 poll_buffer[poll_i] = .{
2502 poll_i += 1;2510 .fd = o.file.handle,
2511 .events = posix.POLL.IN,
2512 .revents = 0,
2513 };
2514 map_buffer[poll_i] = op;
2515 poll_i += 1;
2516 },
2517 }
2518 }
2519 switch (poll_i) {
2520 0 => return,
2521 1 => if (timeout == .none) {
2522 const op = map_buffer[0];
2523 try operate(t, &operations[op]);
2524 ring[complete_tail.index(len)] = op;
2525 complete_tail = complete_tail.next(len);
2526 return;
2503 },2527 },
2504 };2528 else => {},
25052529 }
2506 if (poll_i == 0) return operations.len;
2507
2508 const t_io = ioBasic(t);2530 const t_io = ioBasic(t);
2509 const deadline = timeout.toDeadline(t_io) catch return error.UnsupportedClock;2531 const deadline = timeout.toDeadline(t_io) catch return error.UnsupportedClock;
2510 const max_poll_ms = std.math.maxInt(i32);2532 const max_poll_ms = std.math.maxInt(i32);
2511
2512 while (true) {2533 while (true) {
2513 const timeout_ms: i32 = if (deadline) |d| t: {2534 const timeout_ms: i32 = if (deadline) |d| t: {
2514 const duration = d.durationFromNow(t_io) catch return error.UnsupportedClock;2535 const duration = d.durationFromNow(t_io) catch return error.UnsupportedClock;
...@@ -2526,11 +2547,24 @@ fn batchWait(...@@ -2526,11 +2547,24 @@ fn batchWait(
2526 if (deadline == null) continue;2547 if (deadline == null) continue;
2527 return error.Timeout;2548 return error.Timeout;
2528 }2549 }
2529 for (poll_buffer[0..poll_i], map_buffer[0..poll_i]) |*poll_fd, i| {2550 var canceled = false;
2530 if (poll_fd.revents == 0) continue;2551 for (poll_buffer[0..poll_i], map_buffer[0..poll_i]) |*poll_fd, op| {
2531 operate(&operations[i]);2552 if (poll_fd.revents == 0) {
2532 return i;2553 submit_head = submit_head.prev(len);
2554 ring[submit_head.index(len)] = op;
2555 } else {
2556 operate(t, &operations[op]) catch |err| switch (err) {
2557 error.Canceled => {
2558 canceled = true;
2559 continue;
2560 },
2561 };
2562 ring[complete_tail.index(len)] = op;
2563 complete_tail = complete_tail.next(len);
2564 }
2533 }2565 }
2566 poll_i = 0;
2567 return if (canceled) error.Canceled;
2534 },2568 },
2535 .INTR => continue,2569 .INTR => continue,
2536 else => return error.ConcurrencyUnavailable,2570 else => return error.ConcurrencyUnavailable,
...@@ -2540,9 +2574,27 @@ fn batchWait(...@@ -2540,9 +2574,27 @@ fn batchWait(
25402574
2541fn batchCancel(userdata: ?*anyopaque, b: *Io.Batch) void {2575fn batchCancel(userdata: ?*anyopaque, b: *Io.Batch) void {
2542 const t: *Threaded = @ptrCast(@alignCast(userdata));2576 const t: *Threaded = @ptrCast(@alignCast(userdata));
2543 _ = t;2577 const operations = b.operations;
2544 _ = b;2578 const len: u31 = @intCast(operations.len);
2545 return;2579 const ring = b.ring[0..len];
2580 var submit_head = b.impl.submit_head;
2581 const submit_tail = b.user.submit_tail;
2582 b.impl.submit_tail = submit_tail;
2583 var complete_tail = b.impl.complete_tail;
2584 while (submit_head != submit_tail) : (submit_head = submit_head.next(len)) {
2585 const op = ring[submit_head.index(len)];
2586 switch (operations[op]) {
2587 .noop => {
2588 operate(t, &operations[op]) catch unreachable;
2589 ring[complete_tail.index(len)] = op;
2590 complete_tail = complete_tail.next(len);
2591 },
2592 .file_read_streaming => |*o| _ = o.status.unstarted,
2593 }
2594 }
2595 b.impl.submit_head = submit_tail;
2596 b.impl.complete_tail = complete_tail;
2597 b.user.complete_tail = complete_tail;
2546}2598}
25472599
2548fn batch(userdata: ?*anyopaque, operations: []Io.Operation) Io.ConcurrentError!void {2600fn batch(userdata: ?*anyopaque, operations: []Io.Operation) Io.ConcurrentError!void {
...@@ -10352,6 +10404,7 @@ fn nowWasi(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {...@@ -10352,6 +10404,7 @@ fn nowWasi(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
1035210404
10353fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {10405fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
10354 const t: *Threaded = @ptrCast(@alignCast(userdata));10406 const t: *Threaded = @ptrCast(@alignCast(userdata));
10407 if (timeout == .none) return;
10355 if (use_parking_sleep) return parking_sleep.sleep(try timeout.toDeadline(ioBasic(t)));10408 if (use_parking_sleep) return parking_sleep.sleep(try timeout.toDeadline(ioBasic(t)));
10356 if (native_os == .wasi) return sleepWasi(t, timeout);10409 if (native_os == .wasi) return sleepWasi(t, timeout);
10357 if (@TypeOf(posix.system.clock_nanosleep) != void) return sleepPosix(timeout);10410 if (@TypeOf(posix.system.clock_nanosleep) != void) return sleepPosix(timeout);
lib/std/process/Child.zig+33-39
...@@ -149,51 +149,45 @@ pub fn collectOutput(child: *const Child, io: Io, options: CollectOutputOptions)...@@ -149,51 +149,45 @@ pub fn collectOutput(child: *const Child, io: Io, options: CollectOutputOptions)
149 const files: [2]Io.File = .{ child.stdout.?, child.stderr.? };149 const files: [2]Io.File = .{ child.stdout.?, child.stderr.? };
150 const lists: [2]*std.ArrayList(u8) = .{ options.stdout, options.stderr };150 const lists: [2]*std.ArrayList(u8) = .{ options.stdout, options.stderr };
151 const limits: [2]Io.Limit = .{ options.stdout_limit, options.stderr_limit };151 const limits: [2]Io.Limit = .{ options.stdout_limit, options.stderr_limit };
152 var dones: [2]bool = .{ false, false };
153 var reads: [2]Io.Operation = undefined;152 var reads: [2]Io.Operation = undefined;
154 var vecs: [2][1][]u8 = undefined;153 var vecs: [2][1][]u8 = undefined;
155 while (true) {154 var ring: [2]u32 = undefined;
156 for (&reads, &lists, &files, dones, &vecs) |*read, list, file, done, *vec| {155 var batch: Io.Batch = .init(&reads, &ring);
157 if (done) {156 defer {
158 read.* = .{ .noop = .{} };157 batch.cancel(io);
159 continue;158 while (batch.next()) |op| {
160 }159 lists[op].items.len += reads[op].file_read_streaming.status.result catch continue;
161 if (options.allocator) |gpa| try list.ensureUnusedCapacity(gpa, 1);
162 const cap = list.unusedCapacitySlice();
163 if (cap.len == 0) return error.StreamTooLong;
164 vec[0] = cap;
165 read.* = .{ .file_read_streaming = .{
166 .file = file,
167 .data = vec,
168 } };
169 }160 }
170 var all_done = true;161 }
171 var any_canceled = false;162 var remaining: usize = 0;
172 var other_err: (error{StreamTooLong} || Io.File.Reader.Error)!void = {};163 for (0.., &reads, &lists, &files, &vecs) |op, *read, list, file, *vec| {
173 try io.vtable.batch(io.userdata, &reads);164 if (options.allocator) |gpa| try list.ensureUnusedCapacity(gpa, 1);
174 for (&reads, &lists, &limits, &dones) |*read, list, limit, *done| {165 const cap = list.unusedCapacitySlice();
175 if (done.*) continue;166 if (cap.len == 0) return error.StreamTooLong;
176 const n = read.file_read_streaming.status.result catch |err| switch (err) {167 vec[0] = cap;
177 error.Canceled => {168 read.* = .{ .file_read_streaming = .{
178 any_canceled = true;169 .file = file,
179 continue;170 .data = vec,
180 },171 } };
181 error.WouldBlock => continue,172 batch.add(op);
182 else => |e| {173 remaining += 1;
183 other_err = e;174 }
184 continue;175 while (remaining > 0) {
185 },176 try batch.wait(io, .none);
186 };177 while (batch.next()) |op| {
178 const n = try reads[op].file_read_streaming.status.result;
187 if (n == 0) {179 if (n == 0) {
188 done.* = true;180 remaining -= 1;
189 } else {181 } else {
190 all_done = false;182 lists[op].items.len += n;
183 if (lists[op].items.len > @intFromEnum(limits[op])) return error.StreamTooLong;
184 if (options.allocator) |gpa| try lists[op].ensureUnusedCapacity(gpa, 1);
185 const cap = lists[op].unusedCapacitySlice();
186 if (cap.len == 0) return error.StreamTooLong;
187 vecs[op][0] = cap;
188 reads[op].file_read_streaming.status = .{ .unstarted = {} };
189 batch.add(op);
191 }190 }
192 list.items.len += n;
193 if (list.items.len > @intFromEnum(limit)) other_err = error.StreamTooLong;
194 }191 }
195 if (any_canceled) return error.Canceled;
196 try other_err;
197 if (all_done) return;
198 }192 }
199}193}