authorgravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2026-01-10 15:34:36-05:00
committergravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2026-01-12 20:05:15-05:00
logf633a4845aeea5f390d4ea1b05e5d957efb02966
treebcc9fb652281816f12f6aae1bc7b72d3cc7addbb
parent078a19cf3111726f4a3861da9d4e297eac7dc585

Io: add ring to `Batch` API


5 files changed, 259 insertions(+), 159 deletions(-)

lib/std/Io.zig+122-48
...@@ -148,9 +148,8 @@ pub const VTable = struct {...@@ -148,9 +148,8 @@ pub const VTable = struct {
148 futexWaitUncancelable: *const fn (?*anyopaque, ptr: *const u32, expected: u32) void,148 futexWaitUncancelable: *const fn (?*anyopaque, ptr: *const u32, expected: u32) void,
149 futexWake: *const fn (?*anyopaque, ptr: *const u32, max_waiters: u32) void,149 futexWake: *const fn (?*anyopaque, ptr: *const u32, max_waiters: u32) void,
150150
151 batch: *const fn (?*anyopaque, []Operation) ConcurrentError!void,151 operate: *const fn (?*anyopaque, *Operation) Cancelable!void,
152 batchSubmit: *const fn (?*anyopaque, *Batch) void,152 batchWait: *const fn (?*anyopaque, *Batch, Timeout) Batch.WaitError!void,
153 batchWait: *const fn (?*anyopaque, *Batch, resubmissions: []const usize, Timeout) Batch.WaitError!usize,
154 batchCancel: *const fn (?*anyopaque, *Batch) void,153 batchCancel: *const fn (?*anyopaque, *Batch) void,
155154
156 dirCreateDir: *const fn (?*anyopaque, Dir, []const u8, Dir.Permissions) Dir.CreateDirError!void,155 dirCreateDir: *const fn (?*anyopaque, Dir, []const u8, Dir.Permissions) Dir.CreateDirError!void,
...@@ -252,48 +251,51 @@ pub const Operation = union(enum) {...@@ -252,48 +251,51 @@ pub const Operation = union(enum) {
252251
253 pub const Noop = struct {252 pub const Noop = struct {
254 reserved: [2]usize = .{ 0, 0 },253 reserved: [2]usize = .{ 0, 0 },
255 status: Status(void) = .{ .result = {} },254 status: Status(void) = .{ .unstarted = {} },
256 };255 };
257256
258 /// Returns 0 on end of stream.257 /// Returns 0 on end of stream.
259 pub const FileReadStreaming = struct {258 pub const FileReadStreaming = struct {
260 file: File,259 file: File,
261 data: []const []u8,260 data: []const []u8,
262 status: Status(File.Reader.Error!usize) = .{ .unstarted = {} },261 status: Status(Error!usize) = .{ .unstarted = {} },
262
263 pub const Error = error{
264 InputOutput,
265 SystemResources,
266 IsDir,
267 BrokenPipe,
268 ConnectionResetByPeer,
269 Timeout,
270 /// In WASI, EBADF is mapped to this error because it is returned when
271 /// trying to read a directory file descriptor as if it were a file.
272 NotOpenForReading,
273 SocketUnconnected,
274 /// Non-blocking has been enabled, and reading from the file descriptor
275 /// would block.
276 WouldBlock,
277 /// In WASI, this error occurs when the file descriptor does
278 /// not hold the required rights to read from it.
279 AccessDenied,
280 /// Unable to read file due to lock. Depending on the `Io` implementation,
281 /// reading from a locked file may return this error, or may ignore the
282 /// lock.
283 LockViolation,
284 } || Io.UnexpectedError;
263 };285 };
264286
265 pub fn Status(Result: type) type {287 pub fn Status(Result: type) type {
266 return union {288 return union {
267 unstarted: void,289 unstarted: void,
268 pending: usize,290 pending: *Batch,
269 result: Result,291 result: Result,
270 };292 };
271 }293 }
272};294};
273295
274/// Performs all `operations` in an unspecified order, concurrently.
275///
276/// Returns after all `operations` have been completed. If the operations could
277/// not be completed concurrently, returns `error.ConcurrencyUnavailable`.
278///
279/// With this API, it is rare for concurrency to not be available. Even a
280/// single-threaded `Io` implementation can, for example, take advantage of
281/// poll() to implement this. Note that poll() is fallible however.
282///
283/// If `operations.len` is one, `error.ConcurrencyUnavailable` is unreachable.
284///
285/// On entry, all operations must already have `.status = .unstarted` except
286/// noops must have `.status = .{ .result = {} }`, to safety check the state
287/// transitions.
288///
289/// On return, all operations have `.status = .{ .result = ... }`.
290pub fn batch(io: Io, operations: []Operation) ConcurrentError!void {
291 return io.vtable.batch(io.userdata, operations);
292}
293
294/// Performs one `Operation`.296/// Performs one `Operation`.
295pub fn operate(io: Io, operation: *Operation) void {297pub fn operate(io: Io, operation: *Operation) Cancelable!void {
296 return io.vtable.batch(io.userdata, (operation)[0..1]) catch unreachable;298 return io.vtable.operate(io.userdata, operation) catch unreachable;
297}299}
298300
299/// Submits many operations together without waiting for all of them to301/// Submits many operations together without waiting for all of them to
...@@ -303,35 +305,107 @@ pub fn operate(io: Io, operation: *Operation) void {...@@ -303,35 +305,107 @@ pub fn operate(io: Io, operation: *Operation) void {
303/// level API that operates on `Future`, see `Select`.305/// level API that operates on `Future`, see `Select`.
304pub const Batch = struct {306pub const Batch = struct {
305 operations: []Operation,307 operations: []Operation,
306 index: usize,308 ring: [*]u32,
307 reserved: ?*anyopaque,309 user: struct {
310 submit_tail: RingIndex,
311 complete_head: RingIndex,
312 complete_tail: RingIndex,
313 },
314 impl: struct {
315 submit_head: RingIndex,
316 submit_tail: RingIndex,
317 complete_tail: RingIndex,
318 reserved: ?*anyopaque,
319 },
320
321 pub const RingIndex = enum(u32) {
322 _,
323
324 pub fn index(ri: RingIndex, len: u31) u31 {
325 const i = @intFromEnum(ri);
326 assert(i < @as(u32, len) * 2);
327 return @intCast(if (i < len) i else i - len);
328 }
329
330 pub fn prev(ri: RingIndex, len: u31) RingIndex {
331 const i = @intFromEnum(ri);
332 const double_len = @as(u32, len) * 2;
333 assert(i <= double_len);
334 return @enumFromInt((if (i > 0) i else double_len) - 1);
335 }
336
337 pub fn next(ri: RingIndex, len: u31) RingIndex {
338 const i = @intFromEnum(ri) + 1;
339 const double_len = @as(u32, len) * 2;
340 assert(i <= double_len);
341 return @enumFromInt(if (i < double_len) i else 0);
342 }
343 };
344
345 pub const WaitError = ConcurrentError || Cancelable || Timeout.Error;
308346
309 pub fn init(operations: []Operation) Batch {347 pub fn init(operations: []Operation, ring: []u32) Batch {
310 return .{ .operations = operations, .index = 0, .reserved = null };348 const len: u31 = @intCast(operations.len);
349 assert(ring.len == len);
350 return .{
351 .operations = operations,
352 .ring = ring.ptr,
353 .user = .{
354 .submit_tail = @enumFromInt(0),
355 .complete_head = @enumFromInt(0),
356 .complete_tail = @enumFromInt(0),
357 },
358 .impl = .{
359 .submit_head = @enumFromInt(0),
360 .submit_tail = @enumFromInt(0),
361 .complete_tail = @enumFromInt(0),
362 .reserved = null,
363 },
364 };
311 }365 }
312366
313 /// Submits all non-noop `operations`.367 /// Adds `b.operations[operation]` to the list of submitted operations
314 pub fn submit(b: *Batch, io: Io) void {368 /// that will be performed when `wait` is called.
315 return io.vtable.batchSubmit(io.userdata, b);369 pub fn add(b: *Batch, operation: usize) void {
370 const tail = b.user.submit_tail;
371 const len: u31 = @intCast(b.operations.len);
372 b.user.submit_tail = tail.next(len);
373 b.ring[0..len][tail.index(len)] = @intCast(operation);
316 }374 }
317375
318 pub const WaitError = ConcurrentError || Cancelable || Timeout.Error;376 fn flush(b: *Batch) void {
377 @atomicStore(RingIndex, &b.impl.submit_tail, b.user.submit_tail, .release);
378 }
319379
320 /// Resubmits the previously completed or noop-initialized `operations` at380 /// Returns `operation` such that `b.operations[operation]` has completed.
321 /// indexes given by `resubmissions`. This set of indexes typically will be empty381 /// Returns `null` when `wait` should be called.
322 /// on the first call to `await` since all operations have already been382 pub fn next(b: *Batch) ?u32 {
323 /// submitted via `async`.383 const head = b.user.complete_head;
324 ///384 if (head == b.user.complete_tail) {
325 /// Returns the index of a completed `Operation`, or `operations.len` if385 @branchHint(.unlikely);
326 /// all operations are completed.386 b.flush();
387 const tail = @atomicLoad(RingIndex, &b.impl.complete_tail, .acquire);
388 if (head == tail) {
389 @branchHint(.unlikely);
390 return null;
391 }
392 assert(head != tail);
393 b.user.complete_tail = tail;
394 }
395 const len: u31 = @intCast(b.operations.len);
396 b.user.complete_head = head.next(len);
397 return b.ring[0..len][head.index(len)];
398 }
399
400 /// Starts work on any submitted operations and returns when at least one has completeed.
327 ///401 ///
328 /// When `error.Canceled` is returned, all operations have already completed.402 /// Returns `error.Timeout` if `timeout` expires first.
329 pub fn wait(b: *Batch, io: Io, resubmissions: []const usize, timeout: Timeout) WaitError!usize {403 pub fn wait(b: *Batch, io: Io, timeout: Timeout) WaitError!void {
330 return io.vtable.batchWait(io.userdata, b, resubmissions, timeout);404 return io.vtable.batchWait(io.userdata, b, timeout);
331 }405 }
332406
333 /// Returns after all `operations` have completed. Each operation407 /// Returns after all `operations` have completed. Operations which have not completed
334 /// independently may or may not have been canceled.408 /// after this function returns were successfully dropped and had no side effects.
335 pub fn cancel(b: *Batch, io: Io) void {409 pub fn cancel(b: *Batch, io: Io) void {
336 return io.vtable.batchCancel(io.userdata, b);410 return io.vtable.batchCancel(io.userdata, b);
337 }411 }
lib/std/Io/File.zig+1-1
...@@ -530,7 +530,7 @@ pub fn readStreaming(file: File, io: Io, buffer: []const []u8) Reader.Error!usiz...@@ -530,7 +530,7 @@ pub fn readStreaming(file: File, io: Io, buffer: []const []u8) Reader.Error!usiz
530 .file = file,530 .file = file,
531 .data = buffer,531 .data = buffer,
532 } };532 } };
533 io.operate(&operation);533 try io.operate(&operation);
534 return operation.file_read_streaming.status.result;534 return operation.file_read_streaming.status.result;
535}535}
536536
lib/std/Io/File/Reader.zig+1-22
...@@ -26,28 +26,7 @@ size_err: ?SizeError = null,...@@ -26,28 +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 IsDir,
33 BrokenPipe,
34 ConnectionResetByPeer,
35 Timeout,
36 /// In WASI, EBADF is mapped to this error because it is returned when
37 /// trying to read a directory file descriptor as if it were a file.
38 NotOpenForReading,
39 SocketUnconnected,
40 /// Non-blocking has been enabled, and reading from the file descriptor
41 /// would block.
42 WouldBlock,
43 /// In WASI, this error occurs when the file descriptor does
44 /// not hold the required rights to read from it.
45 AccessDenied,
46 /// Unable to read file due to lock. Depending on the `Io` implementation,
47 /// reading from a locked file may return this error, or may ignore the
48 /// lock.
49 LockViolation,
50} || Io.Cancelable || Io.UnexpectedError;
5130
52pub const SizeError = std.os.windows.GetFileSizeError || File.StatError || error{31pub const SizeError = std.os.windows.GetFileSizeError || File.StatError || error{
53 /// 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
...@@ -1438,8 +1438,7 @@ pub fn io(t: *Threaded) Io {...@@ -1438,8 +1438,7 @@ pub fn io(t: *Threaded) Io {
1438 .futexWaitUncancelable = futexWaitUncancelable,1438 .futexWaitUncancelable = futexWaitUncancelable,
1439 .futexWake = futexWake,1439 .futexWake = futexWake,
14401440
1441 .batch = batch,1441 .operate = operate,
1442 .batchSubmit = batchSubmit,
1443 .batchWait = batchWait,1442 .batchWait = batchWait,
1444 .batchCancel = batchCancel,1443 .batchCancel = batchCancel,
14451444
...@@ -1594,8 +1593,7 @@ pub fn ioBasic(t: *Threaded) Io {...@@ -1594,8 +1593,7 @@ pub fn ioBasic(t: *Threaded) Io {
1594 .futexWaitUncancelable = futexWaitUncancelable,1593 .futexWaitUncancelable = futexWaitUncancelable,
1595 .futexWake = futexWake,1594 .futexWake = futexWake,
15961595
1597 .batch = batch,1596 .operate = operate,
1598 .batchSubmit = batchSubmit,
1599 .batchWait = batchWait,1597 .batchWait = batchWait,
1600 .batchCancel = batchCancel,1598 .batchCancel = batchCancel,
16011599
...@@ -2275,59 +2273,82 @@ fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {...@@ -2275,59 +2273,82 @@ fn futexWake(userdata: ?*anyopaque, ptr: *const u32, max_waiters: u32) void {
2275 Thread.futexWake(ptr, max_waiters);2273 Thread.futexWake(ptr, max_waiters);
2276}2274}
22772275
2278fn batchSubmit(userdata: ?*anyopaque, b: *Io.Batch) void {2276fn operate(userdata: ?*anyopaque, op: *Io.Operation) Io.Cancelable!void {
2279 const t: *Threaded = @ptrCast(@alignCast(userdata));2277 const t: *Threaded = @ptrCast(@alignCast(userdata));
2280 _ = t;2278 _ = t;
2281 _ = b;
2282 return;
2283}
2284
2285fn operate(op: *Io.Operation) void {
2286 switch (op.*) {2279 switch (op.*) {
2287 .noop => {},2280 .noop => |*o| {
2288 .file_read_streaming => |*o| o.status = .{ .result = fileReadStreaming(o.file, o.data) },2281 _ = o.status.unstarted;
2282 o.status = .{ .result = {} };
2283 },
2284 .file_read_streaming => |*o| {
2285 _ = o.status.unstarted;
2286 o.status = .{ .result = fileReadStreaming(o.file, o.data) catch |err| switch (err) {
2287 error.Canceled => return error.Canceled,
2288 else => |e| e,
2289 } };
2290 },
2289 }2291 }
2290}2292}
22912293
2292fn batchWait(2294fn batchWait(userdata: ?*anyopaque, b: *Io.Batch, timeout: Io.Timeout) Io.Batch.WaitError!void {
2293 userdata: ?*anyopaque,
2294 b: *Io.Batch,
2295 resubmissions: []const usize,
2296 timeout: Io.Timeout,
2297) Io.Batch.WaitError!usize {
2298 _ = resubmissions;
2299 const t: *Threaded = @ptrCast(@alignCast(userdata));2295 const t: *Threaded = @ptrCast(@alignCast(userdata));
2300 const operations = b.operations;2296 const operations = b.operations;
2301 if (operations.len == 1) {2297 const len: u31 = @intCast(operations.len);
2302 operate(&operations[0]);2298 const ring = b.ring[0..len];
2303 return b.operations.len;2299 var submit_head = b.impl.submit_head;
2300 const submit_tail = b.user.submit_tail;
2301 b.impl.submit_tail = submit_tail;
2302 var complete_tail = b.impl.complete_tail;
2303 var map_buffer: [poll_buffer_len]u32 = undefined; // poll_buffer index to operations index
2304 var poll_i: usize = 0;
2305 defer {
2306 for (map_buffer[0..poll_i]) |op| {
2307 submit_head = submit_head.prev(len);
2308 ring[submit_head.index(len)] = op;
2309 }
2310 b.impl.submit_head = submit_head;
2311 b.impl.complete_tail = complete_tail;
2312 b.user.complete_tail = complete_tail;
2304 }2313 }
2305 if (is_windows) @panic("TODO");2314 if (is_windows) @panic("TODO");
2306
2307 var poll_buffer: [poll_buffer_len]posix.pollfd = undefined;2315 var poll_buffer: [poll_buffer_len]posix.pollfd = undefined;
2308 var map_buffer: [poll_buffer_len]u8 = undefined; // poll_buffer index to operations index2316 while (submit_head != submit_tail) : (submit_head = submit_head.next(len)) {
2309 var poll_i: usize = 0;2317 const op = ring[submit_head.index(len)];
23102318 const operation = &operations[op];
2311 for (operations, 0..) |*op, operation_index| switch (op.*) {2319 switch (operation.*) {
2312 .noop => continue,2320 else => {
2313 .file_read_streaming => |*o| {2321 try operate(t, operation);
2314 if (poll_buffer.len - poll_i == 0) return error.ConcurrencyUnavailable;2322 ring[complete_tail.index(len)] = op;
2315 poll_buffer[poll_i] = .{2323 complete_tail = complete_tail.next(len);
2316 .fd = o.file.handle,2324 },
2317 .events = posix.POLL.IN,2325 .file_read_streaming => |*o| {
2318 .revents = 0,2326 _ = o.status.unstarted;
2319 };2327 if (poll_buffer.len - poll_i == 0) return error.ConcurrencyUnavailable;
2320 map_buffer[poll_i] = @intCast(operation_index);2328 poll_buffer[poll_i] = .{
2321 poll_i += 1;2329 .fd = o.file.handle,
2330 .events = posix.POLL.IN,
2331 .revents = 0,
2332 };
2333 map_buffer[poll_i] = op;
2334 poll_i += 1;
2335 },
2336 }
2337 }
2338 switch (poll_i) {
2339 0 => return,
2340 1 => if (timeout == .none) {
2341 const op = map_buffer[0];
2342 try operate(t, &operations[op]);
2343 ring[complete_tail.index(len)] = op;
2344 complete_tail = complete_tail.next(len);
2345 return;
2322 },2346 },
2323 };2347 else => {},
23242348 }
2325 if (poll_i == 0) return operations.len;
2326
2327 const t_io = ioBasic(t);2349 const t_io = ioBasic(t);
2328 const deadline = timeout.toDeadline(t_io) catch return error.UnsupportedClock;2350 const deadline = timeout.toDeadline(t_io) catch return error.UnsupportedClock;
2329 const max_poll_ms = std.math.maxInt(i32);2351 const max_poll_ms = std.math.maxInt(i32);
2330
2331 while (true) {2352 while (true) {
2332 const timeout_ms: i32 = if (deadline) |d| t: {2353 const timeout_ms: i32 = if (deadline) |d| t: {
2333 const duration = d.durationFromNow(t_io) catch return error.UnsupportedClock;2354 const duration = d.durationFromNow(t_io) catch return error.UnsupportedClock;
...@@ -2345,11 +2366,24 @@ fn batchWait(...@@ -2345,11 +2366,24 @@ fn batchWait(
2345 if (deadline == null) continue;2366 if (deadline == null) continue;
2346 return error.Timeout;2367 return error.Timeout;
2347 }2368 }
2348 for (poll_buffer[0..poll_i], map_buffer[0..poll_i]) |*poll_fd, i| {2369 var canceled = false;
2349 if (poll_fd.revents == 0) continue;2370 for (poll_buffer[0..poll_i], map_buffer[0..poll_i]) |*poll_fd, op| {
2350 operate(&operations[i]);2371 if (poll_fd.revents == 0) {
2351 return i;2372 submit_head = submit_head.prev(len);
2373 ring[submit_head.index(len)] = op;
2374 } else {
2375 operate(t, &operations[op]) catch |err| switch (err) {
2376 error.Canceled => {
2377 canceled = true;
2378 continue;
2379 },
2380 };
2381 ring[complete_tail.index(len)] = op;
2382 complete_tail = complete_tail.next(len);
2383 }
2352 }2384 }
2385 poll_i = 0;
2386 return if (canceled) error.Canceled;
2353 },2387 },
2354 .INTR => continue,2388 .INTR => continue,
2355 else => return error.ConcurrencyUnavailable,2389 else => return error.ConcurrencyUnavailable,
...@@ -2359,9 +2393,27 @@ fn batchWait(...@@ -2359,9 +2393,27 @@ fn batchWait(
23592393
2360fn batchCancel(userdata: ?*anyopaque, b: *Io.Batch) void {2394fn batchCancel(userdata: ?*anyopaque, b: *Io.Batch) void {
2361 const t: *Threaded = @ptrCast(@alignCast(userdata));2395 const t: *Threaded = @ptrCast(@alignCast(userdata));
2362 _ = t;2396 const operations = b.operations;
2363 _ = b;2397 const len: u31 = @intCast(operations.len);
2364 return;2398 const ring = b.ring[0..len];
2399 var submit_head = b.impl.submit_head;
2400 const submit_tail = b.user.submit_tail;
2401 b.impl.submit_tail = submit_tail;
2402 var complete_tail = b.impl.complete_tail;
2403 while (submit_head != submit_tail) : (submit_head = submit_head.next(len)) {
2404 const op = ring[submit_head.index(len)];
2405 switch (operations[op]) {
2406 .noop => {
2407 operate(t, &operations[op]) catch unreachable;
2408 ring[complete_tail.index(len)] = op;
2409 complete_tail = complete_tail.next(len);
2410 },
2411 .file_read_streaming => |*o| _ = o.status.unstarted,
2412 }
2413 }
2414 b.impl.submit_head = submit_tail;
2415 b.impl.complete_tail = complete_tail;
2416 b.user.complete_tail = complete_tail;
2365}2417}
23662418
2367fn batch(userdata: ?*anyopaque, operations: []Io.Operation) Io.ConcurrentError!void {2419fn batch(userdata: ?*anyopaque, operations: []Io.Operation) Io.ConcurrentError!void {
...@@ -9910,6 +9962,7 @@ fn nowWasi(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {...@@ -9910,6 +9962,7 @@ fn nowWasi(clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
99109962
9911fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {9963fn sleep(userdata: ?*anyopaque, timeout: Io.Timeout) Io.SleepError!void {
9912 const t: *Threaded = @ptrCast(@alignCast(userdata));9964 const t: *Threaded = @ptrCast(@alignCast(userdata));
9965 if (timeout == .none) return;
9913 if (use_parking_sleep) return parking_sleep.sleep(timeout);9966 if (use_parking_sleep) return parking_sleep.sleep(timeout);
9914 if (native_os == .wasi) return sleepWasi(t, timeout);9967 if (native_os == .wasi) return sleepWasi(t, timeout);
9915 if (@TypeOf(posix.system.clock_nanosleep) != void) return sleepPosix(timeout);9968 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}