| ... | ... | @@ -11,7 +11,7 @@ const windows = std.os.windows; |
| 11 | 11 | const testing = std.testing; |
| 12 | 12 | const assert = std.debug.assert; |
| 13 | 13 | const posix = std.posix; |
| 14 | | const Writer = std.Io.Writer; |
| 14 | const Writer = Io.Writer; |
| 15 | 15 | |
| 16 | 16 | /// Currently this API only supports this value being set to stderr, which |
| 17 | 17 | /// happens automatically inside `start`. |
| ... | ... | @@ -21,13 +21,10 @@ io: Io, |
| 21 | 21 | |
| 22 | 22 | terminal_mode: TerminalMode, |
| 23 | 23 | |
| 24 | | update_worker: ?Io.Future(void), |
| 24 | update_worker: ?Io.Future(WorkerError!void), |
| 25 | 25 | |
| 26 | 26 | /// Atomically set by SIGWINCH as well as the root done() function. |
| 27 | 27 | redraw_event: Io.Event, |
| 28 | | /// Indicates a request to shut down and reset global state. |
| 29 | | /// Accessed atomically. |
| 30 | | done: bool, |
| 31 | 28 | need_clear: bool, |
| 32 | 29 | status: Status, |
| 33 | 30 | |
| ... | ... | @@ -43,15 +40,19 @@ draw_buffer: []u8, |
| 43 | 40 | /// This is in a separate array from `node_storage` but with the same length so |
| 44 | 41 | /// that it can be iterated over efficiently without trashing too much of the |
| 45 | 42 | /// CPU cache. |
| 46 | | node_parents: []Node.Parent, |
| 47 | | node_storage: []Node.Storage, |
| 48 | | node_freelist_next: []Node.OptionalIndex, |
| 43 | node_parents: [node_storage_buffer_len]Node.Parent, |
| 44 | node_storage: [node_storage_buffer_len]Node.Storage, |
| 45 | node_freelist_next: [node_storage_buffer_len]Node.OptionalIndex, |
| 49 | 46 | node_freelist: Freelist, |
| 50 | 47 | /// This is the number of elements in node arrays which have been used so far. Nodes before this |
| 51 | 48 | /// index are either active, or on the freelist. The remaining nodes are implicitly free. This |
| 52 | 49 | /// value may at times temporarily exceed the node count. |
| 53 | 50 | node_end_index: u32, |
| 54 | 51 | |
| 52 | ipc_next: Ipc.SlotAtomic, |
| 53 | ipc: [ipc_storage_buffer_len]Ipc, |
| 54 | ipc_files: [ipc_storage_buffer_len]Io.File, |
| 55 | |
| 55 | 56 | start_failure: StartFailure, |
| 56 | 57 | |
| 57 | 58 | pub const Status = enum { |
| ... | ... | @@ -77,6 +78,80 @@ const Freelist = packed struct(u32) { |
| 77 | 78 | generation: u24, |
| 78 | 79 | }; |
| 79 | 80 | |
| 81 | pub const Ipc = packed struct(u32) { |
| 82 | /// mutex protecting `file` use, only locked by `serializeIpc` |
| 83 | locked: bool, |
| 84 | /// when unlocked: whether `file` is defined |
| 85 | /// when locked: whether `file` does not need to be closed |
| 86 | valid: bool, |
| 87 | unused: @Int(.unsigned, 32 - 2 - @bitSizeOf(Generation)) = 0, |
| 88 | generation: Generation, |
| 89 | |
| 90 | pub const Slot = std.math.IntFittingRange(0, ipc_storage_buffer_len - 1); |
| 91 | pub const Generation = @Int(.unsigned, 32 - @bitSizeOf(Slot)); |
| 92 | |
| 93 | const SlotAtomic = @Int(.unsigned, std.math.ceilPowerOfTwoAssert(usize, @min(@bitSizeOf(Slot), 8))); |
| 94 | |
| 95 | pub const Index = packed struct(u32) { |
| 96 | slot: Slot, |
| 97 | generation: Generation, |
| 98 | }; |
| 99 | |
| 100 | const Data = struct { |
| 101 | state: State, |
| 102 | bytes_read: u16, |
| 103 | main_index: u8, |
| 104 | start_index: u8, |
| 105 | nodes_len: u8, |
| 106 | |
| 107 | const State = enum { unused, pending, ready }; |
| 108 | |
| 109 | /// No operations have been started on this file. |
| 110 | const unused: Data = .{ |
| 111 | .state = .unused, |
| 112 | .bytes_read = 0, |
| 113 | .main_index = 0, |
| 114 | .start_index = 0, |
| 115 | .nodes_len = 0, |
| 116 | }; |
| 117 | |
| 118 | fn findLastPacket(data: *const Data, buffer: *const [max_packet_len]u8) struct { u16, u16 } { |
| 119 | assert(data.state == .ready); |
| 120 | var packet_start: u16 = 0; |
| 121 | var packet_end: u16 = 0; |
| 122 | const bytes_read = data.bytes_read; |
| 123 | while (bytes_read - packet_end >= 1) { |
| 124 | const nodes_len: u16 = buffer[packet_end]; |
| 125 | const packet_len = 1 + nodes_len * (@sizeOf(Node.Storage) + @sizeOf(Node.Parent)); |
| 126 | if (packet_end + packet_len > bytes_read) break; |
| 127 | packet_start = packet_end; |
| 128 | packet_end += packet_len; |
| 129 | } |
| 130 | return .{ packet_start, packet_end }; |
| 131 | } |
| 132 | |
| 133 | fn rebase( |
| 134 | data: *Data, |
| 135 | buffer: *[max_packet_len]u8, |
| 136 | vec: *[1][]u8, |
| 137 | batch: *std.Io.Batch, |
| 138 | slot: Slot, |
| 139 | packet_end: u16, |
| 140 | ) void { |
| 141 | assert(data.state == .ready); |
| 142 | const remaining = buffer[packet_end..data.bytes_read]; |
| 143 | @memmove(buffer[0..remaining.len], remaining); |
| 144 | vec.* = .{buffer[remaining.len..]}; |
| 145 | batch.addAt(slot, .{ .file_read_streaming = .{ |
| 146 | .file = global_progress.ipc_files[slot], |
| 147 | .data = vec, |
| 148 | } }); |
| 149 | data.state = .pending; |
| 150 | data.bytes_read = @intCast(remaining.len); |
| 151 | } |
| 152 | }; |
| 153 | }; |
| 154 | |
| 80 | 155 | pub const TerminalMode = union(enum) { |
| 81 | 156 | off, |
| 82 | 157 | ansi_escape_codes, |
| ... | ... | @@ -116,7 +191,7 @@ pub const Node = struct { |
| 116 | 191 | |
| 117 | 192 | pub const none: Node = .{ .index = .none }; |
| 118 | 193 | |
| 119 | | pub const max_name_len = 40; |
| 194 | pub const max_name_len = 120; |
| 120 | 195 | |
| 121 | 196 | const Storage = extern struct { |
| 122 | 197 | /// Little endian. |
| ... | ... | @@ -127,25 +202,16 @@ pub const Node = struct { |
| 127 | 202 | name: [max_name_len]u8 align(@alignOf(usize)), |
| 128 | 203 | |
| 129 | 204 | /// Not thread-safe. |
| 130 | | fn getIpcFd(s: Storage) ?Io.File.Handle { |
| 131 | | return if (s.estimated_total_count == std.math.maxInt(u32)) switch (@typeInfo(Io.File.Handle)) { |
| 132 | | .int => @bitCast(s.completed_count), |
| 133 | | .pointer => @ptrFromInt(s.completed_count), |
| 134 | | else => @compileError("unsupported fd_t of " ++ @typeName(Io.File.Handle)), |
| 135 | | } else null; |
| 205 | fn getIpcIndex(s: Storage) ?Ipc.Index { |
| 206 | return if (s.estimated_total_count == std.math.maxInt(u32)) @bitCast(s.completed_count) else null; |
| 136 | 207 | } |
| 137 | 208 | |
| 138 | 209 | /// Thread-safe. |
| 139 | | fn setIpcFd(s: *Storage, fd: Io.File.Handle) void { |
| 140 | | const integer: u32 = switch (@typeInfo(Io.File.Handle)) { |
| 141 | | .int => @bitCast(fd), |
| 142 | | .pointer => @intCast(@intFromPtr(fd)), |
| 143 | | else => @compileError("unsupported fd_t of " ++ @typeName(Io.File.Handle)), |
| 144 | | }; |
| 210 | fn setIpcIndex(s: *Storage, ipc_index: Ipc.Index) void { |
| 145 | 211 | // `estimated_total_count` max int indicates the special state that |
| 146 | 212 | // causes `completed_count` to be treated as a file descriptor, so |
| 147 | 213 | // the order here matters. |
| 148 | | @atomicStore(u32, &s.completed_count, integer, .monotonic); |
| 214 | @atomicStore(u32, &s.completed_count, @bitCast(ipc_index), .monotonic); |
| 149 | 215 | @atomicStore(u32, &s.estimated_total_count, std.math.maxInt(u32), .release); // synchronizes with acquire in `serialize` |
| 150 | 216 | } |
| 151 | 217 | |
| ... | ... | @@ -155,6 +221,14 @@ pub const Node = struct { |
| 155 | 221 | s.estimated_total_count = @byteSwap(s.estimated_total_count); |
| 156 | 222 | } |
| 157 | 223 | |
| 224 | fn copyRoot(dest: *Node.Storage, src: *align(1) const Node.Storage) void { |
| 225 | dest.* = .{ |
| 226 | .completed_count = src.completed_count, |
| 227 | .estimated_total_count = src.estimated_total_count, |
| 228 | .name = if (src.name[0] == 0) dest.name else src.name, |
| 229 | }; |
| 230 | } |
| 231 | |
| 158 | 232 | comptime { |
| 159 | 233 | assert((@sizeOf(Storage) % 4) == 0); |
| 160 | 234 | } |
| ... | ... | @@ -242,7 +316,7 @@ pub const Node = struct { |
| 242 | 316 | } |
| 243 | 317 | |
| 244 | 318 | const free_index = @atomicRmw(u32, &global_progress.node_end_index, .Add, 1, .monotonic); |
| 245 | | if (free_index >= global_progress.node_storage.len) { |
| 319 | if (free_index >= node_storage_buffer_len) { |
| 246 | 320 | // Ran out of node storage memory. Progress for this node will not be tracked. |
| 247 | 321 | _ = @atomicRmw(u32, &global_progress.node_end_index, .Sub, 1, .monotonic); |
| 248 | 322 | return Node.none; |
| ... | ... | @@ -292,15 +366,17 @@ pub const Node = struct { |
| 292 | 366 | const index = n.index.unwrap() orelse return; |
| 293 | 367 | const storage = storageByIndex(index); |
| 294 | 368 | // Avoid u32 max int which is used to indicate a special state. |
| 295 | | const saturated = @min(std.math.maxInt(u32) - 1, count); |
| 296 | | @atomicStore(u32, &storage.estimated_total_count, saturated, .monotonic); |
| 369 | const saturated_total_count = @min(std.math.maxInt(u32) - 1, count); |
| 370 | @atomicStore(u32, &storage.estimated_total_count, saturated_total_count, .monotonic); |
| 297 | 371 | } |
| 298 | 372 | |
| 299 | 373 | /// Thread-safe. |
| 300 | 374 | pub fn increaseEstimatedTotalItems(n: Node, count: usize) void { |
| 301 | 375 | const index = n.index.unwrap() orelse return; |
| 302 | 376 | const storage = storageByIndex(index); |
| 303 | | _ = @atomicRmw(u32, &storage.estimated_total_count, .Add, std.math.lossyCast(u32, count), .monotonic); |
| 377 | // Avoid u32 max int which is used to indicate a special state. |
| 378 | const saturated_total_count = @min(std.math.maxInt(u32) - 1, count); |
| 379 | _ = @atomicRmw(u32, &storage.estimated_total_count, .Add, saturated_total_count, .monotonic); |
| 304 | 380 | } |
| 305 | 381 | |
| 306 | 382 | /// Finish a started `Node`. Thread-safe. |
| ... | ... | @@ -310,11 +386,25 @@ pub const Node = struct { |
| 310 | 386 | return; |
| 311 | 387 | } |
| 312 | 388 | const index = n.index.unwrap() orelse return; |
| 389 | const io = global_progress.io; |
| 313 | 390 | const parent_ptr = parentByIndex(index); |
| 314 | 391 | if (@atomicLoad(Node.Parent, parent_ptr, .monotonic).unwrap()) |parent_index| { |
| 315 | 392 | _ = @atomicRmw(u32, &storageByIndex(parent_index).completed_count, .Add, 1, .monotonic); |
| 316 | 393 | @atomicStore(Node.Parent, parent_ptr, .unused, .monotonic); |
| 317 | 394 | |
| 395 | if (storageByIndex(index).getIpcIndex()) |ipc_index| { |
| 396 | const file = global_progress.ipc_files[ipc_index.slot]; |
| 397 | const ipc = @atomicRmw( |
| 398 | Ipc, |
| 399 | &global_progress.ipc[ipc_index.slot], |
| 400 | .And, |
| 401 | .{ .locked = true, .valid = false, .generation = std.math.maxInt(Ipc.Generation) }, |
| 402 | .release, |
| 403 | ); |
| 404 | assert(ipc.valid and ipc.generation == ipc_index.generation); |
| 405 | if (!ipc.locked) file.close(io); |
| 406 | } |
| 407 | |
| 318 | 408 | const freelist = &global_progress.node_freelist; |
| 319 | 409 | var old_freelist = @atomicLoad(Freelist, freelist, .monotonic); |
| 320 | 410 | while (true) { |
| ... | ... | @@ -332,42 +422,52 @@ pub const Node = struct { |
| 332 | 422 | }; |
| 333 | 423 | } |
| 334 | 424 | } else { |
| 335 | | @atomicStore(bool, &global_progress.done, true, .monotonic); |
| 336 | | const io = global_progress.io; |
| 337 | | global_progress.redraw_event.set(io); |
| 338 | | if (global_progress.update_worker) |*worker| worker.await(io); |
| 425 | if (global_progress.update_worker) |*worker| worker.cancel(io) catch {}; |
| 426 | for (&global_progress.ipc, &global_progress.ipc_files) |ipc, ipc_file| { |
| 427 | assert(!ipc.locked or !ipc.valid); // missing call to end() |
| 428 | if (ipc.locked or ipc.valid) ipc_file.close(io); |
| 429 | } |
| 339 | 430 | } |
| 340 | 431 | } |
| 341 | 432 | |
| 342 | | /// Posix-only. Used by `std.process.Child`. Thread-safe. |
| 343 | | pub fn setIpcFd(node: Node, fd: Io.File.Handle) void { |
| 433 | /// Used by `std.process.Child`. Thread-safe. |
| 434 | pub fn setIpcFile(node: Node, expected_io_userdata: ?*anyopaque, file: Io.File) void { |
| 344 | 435 | const index = node.index.unwrap() orelse return; |
| 345 | | switch (@typeInfo(Io.File.Handle)) { |
| 346 | | .int => { |
| 347 | | assert(fd >= 0); |
| 348 | | assert(fd != posix.STDOUT_FILENO); |
| 349 | | assert(fd != posix.STDIN_FILENO); |
| 350 | | assert(fd != posix.STDERR_FILENO); |
| 351 | | }, |
| 352 | | .pointer => { |
| 353 | | assert(fd != windows.INVALID_HANDLE_VALUE); |
| 354 | | }, |
| 355 | | else => @compileError("unsupported fd_t of " ++ @typeName(Io.File.Handle)), |
| 356 | | } |
| 357 | | storageByIndex(index).setIpcFd(fd); |
| 436 | const io = global_progress.io; |
| 437 | assert(io.userdata == expected_io_userdata); |
| 438 | for (0..ipc_storage_buffer_len) |_| { |
| 439 | const slot: Ipc.Slot = @truncate( |
| 440 | @atomicRmw(Ipc.SlotAtomic, &global_progress.ipc_next, .Add, 1, .monotonic), |
| 441 | ); |
| 442 | if (slot >= ipc_storage_buffer_len) continue; |
| 443 | const ipc_ptr = &global_progress.ipc[slot]; |
| 444 | const ipc = @atomicLoad(Ipc, ipc_ptr, .monotonic); |
| 445 | if (ipc.locked or ipc.valid) continue; |
| 446 | const generation = ipc.generation +% 1; |
| 447 | if (@cmpxchgWeak( |
| 448 | Ipc, |
| 449 | ipc_ptr, |
| 450 | ipc, |
| 451 | .{ .locked = false, .valid = true, .generation = generation }, |
| 452 | .acquire, |
| 453 | .monotonic, |
| 454 | )) |_| continue; |
| 455 | global_progress.ipc_files[slot] = file; |
| 456 | storageByIndex(index).setIpcIndex(.{ .slot = slot, .generation = generation }); |
| 457 | break; |
| 458 | } else file.close(io); |
| 358 | 459 | } |
| 359 | 460 | |
| 360 | | /// Posix-only. Thread-safe. Assumes the node is storing an IPC file |
| 361 | | /// descriptor. |
| 362 | | pub fn getIpcFd(node: Node) ?Io.File.Handle { |
| 363 | | const index = node.index.unwrap() orelse return null; |
| 364 | | const storage = storageByIndex(index); |
| 365 | | const int = @atomicLoad(u32, &storage.completed_count, .monotonic); |
| 366 | | return switch (@typeInfo(Io.File.Handle)) { |
| 367 | | .int => @bitCast(int), |
| 368 | | .pointer => @ptrFromInt(int), |
| 369 | | else => @compileError("unsupported fd_t of " ++ @typeName(Io.File.Handle)), |
| 370 | | }; |
| 461 | pub fn setIpcIndex(node: Node, ipc_index: Ipc.Index) void { |
| 462 | storageByIndex(node.index.unwrap() orelse return).setIpcIndex(ipc_index); |
| 463 | } |
| 464 | |
| 465 | /// Not thread-safe. |
| 466 | pub fn takeIpcIndex(node: Node) ?Ipc.Index { |
| 467 | const storage = storageByIndex(node.index.unwrap() orelse return null); |
| 468 | assert(storage.estimated_total_count == std.math.maxInt(u32)); |
| 469 | @atomicStore(u32, &storage.estimated_total_count, 0, .monotonic); |
| 470 | return @bitCast(storage.completed_count); |
| 371 | 471 | } |
| 372 | 472 | |
| 373 | 473 | fn storageByIndex(index: Node.Index) *Node.Storage { |
| ... | ... | @@ -387,7 +487,9 @@ pub const Node = struct { |
| 387 | 487 | |
| 388 | 488 | const storage = storageByIndex(free_index); |
| 389 | 489 | @atomicStore(u32, &storage.completed_count, 0, .monotonic); |
| 390 | | @atomicStore(u32, &storage.estimated_total_count, std.math.lossyCast(u32, estimated_total_items), .monotonic); |
| 490 | // Avoid u32 max int which is used to indicate a special state. |
| 491 | const saturated_total_count = @min(std.math.maxInt(u32) - 1, estimated_total_items); |
| 492 | @atomicStore(u32, &storage.estimated_total_count, saturated_total_count, .monotonic); |
| 391 | 493 | const name_len = @min(max_name_len, name.len); |
| 392 | 494 | copyAtomicStore(storage.name[0..name_len], name[0..name_len]); |
| 393 | 495 | if (name_len < storage.name.len) |
| ... | ... | @@ -414,16 +516,20 @@ var global_progress: Progress = .{ |
| 414 | 516 | .rows = 0, |
| 415 | 517 | .cols = 0, |
| 416 | 518 | .draw_buffer = undefined, |
| 417 | | .done = false, |
| 418 | 519 | .need_clear = false, |
| 419 | 520 | .status = .working, |
| 420 | | .start_failure = .unstarted, |
| 421 | 521 | |
| 422 | | .node_parents = &node_parents_buffer, |
| 423 | | .node_storage = &node_storage_buffer, |
| 424 | | .node_freelist_next = &node_freelist_next_buffer, |
| 522 | .node_parents = undefined, |
| 523 | .node_storage = undefined, |
| 524 | .node_freelist_next = undefined, |
| 425 | 525 | .node_freelist = .{ .head = .none, .generation = 0 }, |
| 426 | 526 | .node_end_index = 0, |
| 527 | |
| 528 | .ipc_next = 0, |
| 529 | .ipc = undefined, |
| 530 | .ipc_files = undefined, |
| 531 | |
| 532 | .start_failure = .unstarted, |
| 427 | 533 | }; |
| 428 | 534 | |
| 429 | 535 | pub const StartFailure = union(enum) { |
| ... | ... | @@ -433,17 +539,23 @@ pub const StartFailure = union(enum) { |
| 433 | 539 | parent_ipc: error{ UnsupportedOperation, UnrecognizedFormat }, |
| 434 | 540 | }; |
| 435 | 541 | |
| 436 | | const node_storage_buffer_len = 83; |
| 437 | | var node_parents_buffer: [node_storage_buffer_len]Node.Parent = undefined; |
| 438 | | var node_storage_buffer: [node_storage_buffer_len]Node.Storage = undefined; |
| 439 | | var node_freelist_next_buffer: [node_storage_buffer_len]Node.OptionalIndex = undefined; |
| 542 | /// One less than a power of two ensures `max_packet_len` is already a power of two. |
| 543 | const node_storage_buffer_len = ipc_storage_buffer_len - 1; |
| 544 | |
| 545 | /// Power of two to avoid wasted `ipc_next` increments. |
| 546 | const ipc_storage_buffer_len = 128; |
| 547 | |
| 548 | pub const max_packet_len = std.math.ceilPowerOfTwoAssert( |
| 549 | usize, |
| 550 | 1 + node_storage_buffer_len * (@sizeOf(Node.Storage) + @sizeOf(Node.OptionalIndex)), |
| 551 | ); |
| 440 | 552 | |
| 441 | 553 | var default_draw_buffer: [4096]u8 = undefined; |
| 442 | 554 | |
| 443 | 555 | var debug_start_trace = std.debug.Trace.init; |
| 444 | 556 | |
| 445 | 557 | pub const have_ipc = switch (builtin.os.tag) { |
| 446 | | .wasi, .freestanding, .windows => false, |
| 558 | .wasi, .freestanding => false, |
| 447 | 559 | else => true, |
| 448 | 560 | }; |
| 449 | 561 | |
| ... | ... | @@ -475,9 +587,9 @@ pub fn start(io: Io, options: Options) Node { |
| 475 | 587 | } |
| 476 | 588 | debug_start_trace.add("first initialized here"); |
| 477 | 589 | |
| 478 | | @memset(global_progress.node_parents, .unused); |
| 590 | @memset(&global_progress.node_parents, .unused); |
| 591 | @memset(&global_progress.ipc, .{ .locked = false, .valid = false, .generation = 0 }); |
| 479 | 592 | const root_node = Node.init(@enumFromInt(0), .none, options.root_name, options.estimated_total_items); |
| 480 | | global_progress.done = false; |
| 481 | 593 | global_progress.node_end_index = 1; |
| 482 | 594 | |
| 483 | 595 | assert(options.draw_buffer.len >= 200); |
| ... | ... | @@ -551,58 +663,55 @@ pub fn setStatus(new_status: Status) void { |
| 551 | 663 | } |
| 552 | 664 | |
| 553 | 665 | /// Returns whether a resize is needed to learn the terminal size. |
| 554 | | fn wait(io: Io, timeout_ns: u64) bool { |
| 666 | fn wait(io: Io, timeout_ns: u64) Io.Cancelable!bool { |
| 555 | 667 | const timeout: Io.Timeout = .{ .duration = .{ |
| 556 | 668 | .clock = .awake, |
| 557 | 669 | .raw = .fromNanoseconds(timeout_ns), |
| 558 | 670 | } }; |
| 559 | 671 | const resize_flag = if (global_progress.redraw_event.waitTimeout(io, timeout)) |_| true else |err| switch (err) { |
| 560 | | error.Timeout, error.Canceled => false, |
| 672 | error.Timeout => false, |
| 673 | error.Canceled => |e| return e, |
| 561 | 674 | }; |
| 562 | 675 | global_progress.redraw_event.reset(); |
| 563 | 676 | return resize_flag or (global_progress.cols == 0); |
| 564 | 677 | } |
| 565 | 678 | |
| 566 | | fn updateTask(io: Io) void { |
| 679 | const WorkerError = error{WindowTooSmall} || Io.ConcurrentError || Io.Cancelable || |
| 680 | Io.File.Writer.Error || Io.Operation.FileReadStreaming.Error; |
| 681 | |
| 682 | fn updateTask(io: Io) WorkerError!void { |
| 567 | 683 | // Store this data in the thread so that it does not need to be part of the |
| 568 | 684 | // linker data of the main executable. |
| 569 | 685 | var serialized_buffer: Serialized.Buffer = undefined; |
| 686 | serialized_buffer.init(); |
| 687 | defer serialized_buffer.batch.cancel(io); |
| 570 | 688 | |
| 571 | 689 | // In this function we bypass the wrapper code inside `Io.lockStderr` / |
| 572 | 690 | // `Io.tryLockStderr` in order to avoid clearing the terminal twice. |
| 573 | 691 | // We still want to go through the `Io` instance however in case it uses a |
| 574 | 692 | // task-switching mutex. |
| 575 | 693 | |
| 576 | | { |
| 577 | | const resize_flag = wait(io, global_progress.initial_delay_ns); |
| 578 | | if (@atomicLoad(bool, &global_progress.done, .monotonic)) return; |
| 579 | | maybeUpdateSize(io, resize_flag) catch return; |
| 580 | | |
| 581 | | const buffer, _ = computeRedraw(&serialized_buffer); |
| 582 | | if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { |
| 583 | | defer io.unlockStderr(); |
| 584 | | global_progress.need_clear = true; |
| 585 | | locked_stderr.file_writer.interface.writeAll(buffer) catch return; |
| 586 | | } |
| 694 | try maybeUpdateSize(io, try wait(io, global_progress.initial_delay_ns)); |
| 695 | errdefer { |
| 696 | const cancel_protection = io.swapCancelProtection(.blocked); |
| 697 | defer _ = io.swapCancelProtection(cancel_protection); |
| 698 | const stderr = io.vtable.lockStderr(io.userdata, null) catch |err| switch (err) { |
| 699 | error.Canceled => unreachable, // blocked |
| 700 | }; |
| 701 | defer io.unlockStderr(); |
| 702 | clearWrittenWithEscapeCodes(stderr.file_writer) catch {}; |
| 587 | 703 | } |
| 588 | | |
| 589 | 704 | while (true) { |
| 590 | | const resize_flag = wait(io, global_progress.refresh_rate_ns); |
| 591 | | |
| 592 | | if (@atomicLoad(bool, &global_progress.done, .monotonic)) { |
| 593 | | const stderr = io.vtable.lockStderr(io.userdata, null) catch return; |
| 594 | | defer io.unlockStderr(); |
| 595 | | return clearWrittenWithEscapeCodes(stderr.file_writer) catch {}; |
| 596 | | } |
| 597 | | |
| 598 | | maybeUpdateSize(io, resize_flag) catch return; |
| 599 | | |
| 600 | | const buffer, _ = computeRedraw(&serialized_buffer); |
| 601 | | if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { |
| 705 | const buffer, _ = try computeRedraw(io, &serialized_buffer); |
| 706 | if (try io.vtable.tryLockStderr(io.userdata, null)) |locked_stderr| { |
| 602 | 707 | defer io.unlockStderr(); |
| 603 | 708 | global_progress.need_clear = true; |
| 604 | | locked_stderr.file_writer.interface.writeAll(buffer) catch return; |
| 709 | locked_stderr.file_writer.interface.writeAll(buffer) catch |err| switch (err) { |
| 710 | error.WriteFailed => return locked_stderr.file_writer.err.?, |
| 711 | }; |
| 605 | 712 | } |
| 713 | |
| 714 | try maybeUpdateSize(io, try wait(io, global_progress.refresh_rate_ns)); |
| 606 | 715 | } |
| 607 | 716 | } |
| 608 | 717 | |
| ... | ... | @@ -614,79 +723,60 @@ fn windowsApiWriteMarker() void { |
| 614 | 723 | _ = windows.kernel32.WriteConsoleW(handle, &[_]u16{windows_api_start_marker}, 1, &num_chars_written, null); |
| 615 | 724 | } |
| 616 | 725 | |
| 617 | | fn windowsApiUpdateTask(io: Io) void { |
| 726 | fn windowsApiUpdateTask(io: Io) WorkerError!void { |
| 727 | // Store this data in the thread so that it does not need to be part of the |
| 728 | // linker data of the main executable. |
| 618 | 729 | var serialized_buffer: Serialized.Buffer = undefined; |
| 730 | serialized_buffer.init(); |
| 731 | defer serialized_buffer.batch.cancel(io); |
| 619 | 732 | |
| 620 | 733 | // In this function we bypass the wrapper code inside `Io.lockStderr` / |
| 621 | 734 | // `Io.tryLockStderr` in order to avoid clearing the terminal twice. |
| 622 | 735 | // We still want to go through the `Io` instance however in case it uses a |
| 623 | 736 | // task-switching mutex. |
| 624 | 737 | |
| 625 | | { |
| 626 | | const resize_flag = wait(io, global_progress.initial_delay_ns); |
| 627 | | if (@atomicLoad(bool, &global_progress.done, .monotonic)) return; |
| 628 | | maybeUpdateSize(io, resize_flag) catch return; |
| 629 | | |
| 630 | | const buffer, const nl_n = computeRedraw(&serialized_buffer); |
| 631 | | if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { |
| 632 | | defer io.unlockStderr(); |
| 633 | | windowsApiWriteMarker(); |
| 634 | | global_progress.need_clear = true; |
| 635 | | locked_stderr.file_writer.interface.writeAll(buffer) catch return; |
| 636 | | windowsApiMoveToMarker(nl_n) catch return; |
| 637 | | } |
| 738 | try maybeUpdateSize(io, try wait(io, global_progress.initial_delay_ns)); |
| 739 | errdefer { |
| 740 | const cancel_protection = io.swapCancelProtection(.blocked); |
| 741 | defer _ = io.swapCancelProtection(cancel_protection); |
| 742 | _ = io.vtable.lockStderr(io.userdata, null) catch |err| switch (err) { |
| 743 | error.Canceled => unreachable, // blocked |
| 744 | }; |
| 745 | defer io.unlockStderr(); |
| 746 | clearWrittenWindowsApi() catch {}; |
| 638 | 747 | } |
| 639 | | |
| 640 | 748 | while (true) { |
| 641 | | const resize_flag = wait(io, global_progress.refresh_rate_ns); |
| 642 | | |
| 643 | | if (@atomicLoad(bool, &global_progress.done, .monotonic)) { |
| 644 | | _ = io.vtable.lockStderr(io.userdata, null) catch return; |
| 645 | | defer io.unlockStderr(); |
| 646 | | return clearWrittenWindowsApi() catch {}; |
| 647 | | } |
| 648 | | |
| 649 | | maybeUpdateSize(io, resize_flag) catch return; |
| 650 | | |
| 651 | | const buffer, const nl_n = computeRedraw(&serialized_buffer); |
| 749 | const buffer, const nl_n = try computeRedraw(io, &serialized_buffer); |
| 652 | 750 | if (io.vtable.tryLockStderr(io.userdata, null) catch return) |locked_stderr| { |
| 653 | 751 | defer io.unlockStderr(); |
| 654 | | clearWrittenWindowsApi() catch return; |
| 752 | try clearWrittenWindowsApi(); |
| 655 | 753 | windowsApiWriteMarker(); |
| 656 | 754 | global_progress.need_clear = true; |
| 657 | | locked_stderr.file_writer.interface.writeAll(buffer) catch return; |
| 755 | locked_stderr.file_writer.interface.writeAll(buffer) catch |err| switch (err) { |
| 756 | error.WriteFailed => return locked_stderr.file_writer.err.?, |
| 757 | }; |
| 658 | 758 | windowsApiMoveToMarker(nl_n) catch return; |
| 659 | 759 | } |
| 760 | |
| 761 | try maybeUpdateSize(io, try wait(io, global_progress.refresh_rate_ns)); |
| 660 | 762 | } |
| 661 | 763 | } |
| 662 | 764 | |
| 663 | | fn ipcThreadRun(io: Io, file: Io.File) void { |
| 765 | fn ipcThreadRun(io: Io, file: Io.File) WorkerError!void { |
| 664 | 766 | // Store this data in the thread so that it does not need to be part of the |
| 665 | 767 | // linker data of the main executable. |
| 666 | 768 | var serialized_buffer: Serialized.Buffer = undefined; |
| 769 | serialized_buffer.init(); |
| 770 | defer serialized_buffer.batch.cancel(io); |
| 771 | var fw = file.writerStreaming(io, &.{}); |
| 667 | 772 | |
| 668 | | { |
| 669 | | _ = wait(io, global_progress.initial_delay_ns); |
| 670 | | |
| 671 | | if (@atomicLoad(bool, &global_progress.done, .monotonic)) |
| 672 | | return; |
| 673 | | |
| 674 | | const serialized = serialize(&serialized_buffer); |
| 675 | | writeIpc(io, file, serialized) catch |err| switch (err) { |
| 676 | | error.BrokenPipe => return, |
| 677 | | }; |
| 678 | | } |
| 679 | | |
| 773 | _ = try io.sleep(.fromNanoseconds(global_progress.initial_delay_ns), .awake); |
| 680 | 774 | while (true) { |
| 681 | | _ = wait(io, global_progress.refresh_rate_ns); |
| 682 | | |
| 683 | | if (@atomicLoad(bool, &global_progress.done, .monotonic)) |
| 684 | | return; |
| 685 | | |
| 686 | | const serialized = serialize(&serialized_buffer); |
| 687 | | writeIpc(io, file, serialized) catch |err| switch (err) { |
| 688 | | error.BrokenPipe => return, |
| 775 | writeIpc(&fw.interface, try serialize(io, &serialized_buffer)) catch |err| switch (err) { |
| 776 | error.WriteFailed => return fw.err.?, |
| 689 | 777 | }; |
| 778 | |
| 779 | _ = try io.sleep(.fromNanoseconds(global_progress.refresh_rate_ns), .awake); |
| 690 | 780 | } |
| 691 | 781 | } |
| 692 | 782 | |
| ... | ... | @@ -865,31 +955,49 @@ const Serialized = struct { |
| 865 | 955 | const Buffer = struct { |
| 866 | 956 | parents: [node_storage_buffer_len]Node.Parent, |
| 867 | 957 | storage: [node_storage_buffer_len]Node.Storage, |
| 868 | | map: [node_storage_buffer_len]Node.OptionalIndex, |
| 869 | 958 | |
| 870 | | parents_copy: [node_storage_buffer_len]Node.Parent, |
| 871 | | storage_copy: [node_storage_buffer_len]Node.Storage, |
| 872 | | ipc_metadata_fds_copy: [node_storage_buffer_len]Fd, |
| 873 | | ipc_metadata_copy: [node_storage_buffer_len]SavedMetadata, |
| 874 | | |
| 875 | | ipc_metadata_fds: [node_storage_buffer_len]Fd, |
| 876 | | ipc_metadata: [node_storage_buffer_len]SavedMetadata, |
| 959 | ipc_start: u8, |
| 960 | ipc_end: u8, |
| 961 | ipc_data: [ipc_storage_buffer_len]Ipc.Data, |
| 962 | ipc_buffers: [ipc_storage_buffer_len][max_packet_len]u8, |
| 963 | ipc_vecs: [ipc_storage_buffer_len][1][]u8, |
| 964 | batch_storage: [ipc_storage_buffer_len]Io.Operation.Storage, |
| 965 | batch: Io.Batch, |
| 966 | |
| 967 | fn init(buffer: *Buffer) void { |
| 968 | buffer.ipc_start = 0; |
| 969 | buffer.ipc_end = 0; |
| 970 | @memset(&buffer.ipc_data, .unused); |
| 971 | buffer.batch = .init(&buffer.batch_storage); |
| 972 | } |
| 877 | 973 | }; |
| 878 | 974 | }; |
| 879 | 975 | |
| 880 | | fn serialize(serialized_buffer: *Serialized.Buffer) Serialized { |
| 881 | | var serialized_len: usize = 0; |
| 882 | | var any_ipc = false; |
| 976 | fn serialize(io: Io, serialized_buffer: *Serialized.Buffer) !Serialized { |
| 977 | var prev_parents: [node_storage_buffer_len]Node.Parent = undefined; |
| 978 | var prev_storage: [node_storage_buffer_len]Node.Storage = undefined; |
| 979 | { |
| 980 | const ipc_start = serialized_buffer.ipc_start; |
| 981 | const ipc_end = serialized_buffer.ipc_end; |
| 982 | @memcpy(prev_parents[ipc_start..ipc_end], serialized_buffer.parents[ipc_start..ipc_end]); |
| 983 | @memcpy(prev_storage[ipc_start..ipc_end], serialized_buffer.storage[ipc_start..ipc_end]); |
| 984 | } |
| 883 | 985 | |
| 884 | 986 | // Iterate all of the nodes and construct a serializable copy of the state that can be examined |
| 885 | 987 | // without atomics. The `@min` call is here because `node_end_index` might briefly exceed the |
| 886 | 988 | // node count sometimes. |
| 887 | | const end_index = @min(@atomicLoad(u32, &global_progress.node_end_index, .monotonic), global_progress.node_storage.len); |
| 989 | const end_index = @min( |
| 990 | @atomicLoad(u32, &global_progress.node_end_index, .monotonic), |
| 991 | node_storage_buffer_len, |
| 992 | ); |
| 993 | var map: [node_storage_buffer_len]Node.OptionalIndex = undefined; |
| 994 | var serialized_len: u8 = 0; |
| 995 | var maybe_ipc_start: ?u8 = null; |
| 888 | 996 | for ( |
| 889 | 997 | global_progress.node_parents[0..end_index], |
| 890 | 998 | global_progress.node_storage[0..end_index], |
| 891 | | serialized_buffer.map[0..end_index], |
| 892 | | ) |*parent_ptr, *storage_ptr, *map| { |
| 999 | map[0..end_index], |
| 1000 | ) |*parent_ptr, *storage_ptr, *map_entry| { |
| 893 | 1001 | const parent = @atomicLoad(Node.Parent, parent_ptr, .monotonic); |
| 894 | 1002 | if (parent == .unused) { |
| 895 | 1003 | // We might read "mixed" node data in this loop, due to weird atomic things |
| ... | ... | @@ -903,17 +1011,17 @@ fn serialize(serialized_buffer: *Serialized.Buffer) Serialized { |
| 903 | 1011 | // parent, it will just not be printed at all. The general idea here is that performance |
| 904 | 1012 | // is more important than 100% correct output every frame, given that this API is likely |
| 905 | 1013 | // to be used in hot paths! |
| 906 | | map.* = .none; |
| 1014 | map_entry.* = .none; |
| 907 | 1015 | continue; |
| 908 | 1016 | } |
| 909 | 1017 | const dest_storage = &serialized_buffer.storage[serialized_len]; |
| 910 | 1018 | copyAtomicLoad(&dest_storage.name, &storage_ptr.name); |
| 911 | | dest_storage.estimated_total_count = @atomicLoad(u32, &storage_ptr.estimated_total_count, .acquire); // sychronizes with release in `setIpcFd` |
| 1019 | dest_storage.estimated_total_count = @atomicLoad(u32, &storage_ptr.estimated_total_count, .acquire); // sychronizes with release in `setIpcIndex` |
| 912 | 1020 | dest_storage.completed_count = @atomicLoad(u32, &storage_ptr.completed_count, .monotonic); |
| 913 | 1021 | |
| 914 | | any_ipc = any_ipc or (dest_storage.getIpcFd() != null); |
| 915 | 1022 | serialized_buffer.parents[serialized_len] = parent; |
| 916 | | map.* = @enumFromInt(serialized_len); |
| 1023 | map_entry.* = @enumFromInt(serialized_len); |
| 1024 | if (maybe_ipc_start == null and dest_storage.getIpcIndex() != null) maybe_ipc_start = serialized_len; |
| 917 | 1025 | serialized_len += 1; |
| 918 | 1026 | } |
| 919 | 1027 | |
| ... | ... | @@ -922,266 +1030,212 @@ fn serialize(serialized_buffer: *Serialized.Buffer) Serialized { |
| 922 | 1030 | parent.* = switch (parent.*) { |
| 923 | 1031 | .unused => unreachable, |
| 924 | 1032 | .none => .none, |
| 925 | | _ => |p| serialized_buffer.map[@intFromEnum(p)].toParent(), |
| 1033 | _ => |p| map[@intFromEnum(p)].toParent(), |
| 926 | 1034 | }; |
| 927 | 1035 | } |
| 928 | 1036 | |
| 929 | | // Find nodes which correspond to child processes. |
| 930 | | if (any_ipc) |
| 931 | | serialized_len = serializeIpc(serialized_len, serialized_buffer); |
| 932 | | |
| 933 | | return .{ |
| 934 | | .parents = serialized_buffer.parents[0..serialized_len], |
| 935 | | .storage = serialized_buffer.storage[0..serialized_len], |
| 1037 | // Fill pipe buffers. |
| 1038 | const batch = &serialized_buffer.batch; |
| 1039 | batch.awaitConcurrent(io, .{ |
| 1040 | .duration = .{ .raw = .zero, .clock = .awake }, |
| 1041 | }) catch |err| switch (err) { |
| 1042 | error.Timeout => {}, |
| 1043 | else => |e| return e, |
| 1044 | }; |
| 1045 | var ready_len: u8 = 0; |
| 1046 | while (batch.next()) |operation| switch (operation.index) { |
| 1047 | 0...ipc_storage_buffer_len - 1 => { |
| 1048 | const ipc_data = &serialized_buffer.ipc_data[operation.index]; |
| 1049 | ipc_data.bytes_read += @intCast( |
| 1050 | operation.result.file_read_streaming catch |err| switch (err) { |
| 1051 | error.EndOfStream => { |
| 1052 | const file = global_progress.ipc_files[operation.index]; |
| 1053 | const ipc = @atomicRmw( |
| 1054 | Ipc, |
| 1055 | &global_progress.ipc[operation.index], |
| 1056 | .And, |
| 1057 | .{ |
| 1058 | .locked = false, |
| 1059 | .valid = true, |
| 1060 | .generation = std.math.maxInt(Ipc.Generation), |
| 1061 | }, |
| 1062 | .release, |
| 1063 | ); |
| 1064 | assert(ipc.locked); |
| 1065 | if (!ipc.valid) file.close(io); |
| 1066 | ipc_data.* = .unused; |
| 1067 | continue; |
| 1068 | }, |
| 1069 | else => |e| return e, |
| 1070 | }, |
| 1071 | ); |
| 1072 | assert(ipc_data.state == .pending); |
| 1073 | ipc_data.state = .ready; |
| 1074 | ready_len += 1; |
| 1075 | }, |
| 1076 | else => unreachable, |
| 936 | 1077 | }; |
| 937 | | } |
| 938 | | |
| 939 | | const SavedMetadata = struct { |
| 940 | | remaining_read_trash_bytes: u16, |
| 941 | | main_index: u8, |
| 942 | | start_index: u8, |
| 943 | | nodes_len: u8, |
| 944 | | }; |
| 945 | | |
| 946 | | const Fd = enum(i32) { |
| 947 | | _, |
| 948 | | |
| 949 | | fn init(fd: Io.File.Handle) Fd { |
| 950 | | return @enumFromInt(if (is_windows) @as(isize, @bitCast(@intFromPtr(fd))) else fd); |
| 951 | | } |
| 952 | | |
| 953 | | fn get(fd: Fd) Io.File.Handle { |
| 954 | | return if (is_windows) |
| 955 | | @ptrFromInt(@as(usize, @bitCast(@as(isize, @intFromEnum(fd))))) |
| 956 | | else |
| 957 | | @intFromEnum(fd); |
| 958 | | } |
| 959 | | }; |
| 960 | | |
| 961 | | var ipc_metadata_len: u8 = 0; |
| 962 | | |
| 963 | | fn serializeIpc(start_serialized_len: usize, serialized_buffer: *Serialized.Buffer) usize { |
| 964 | | const io = global_progress.io; |
| 965 | | const ipc_metadata_fds_copy = &serialized_buffer.ipc_metadata_fds_copy; |
| 966 | | const ipc_metadata_copy = &serialized_buffer.ipc_metadata_copy; |
| 967 | | const ipc_metadata_fds = &serialized_buffer.ipc_metadata_fds; |
| 968 | | const ipc_metadata = &serialized_buffer.ipc_metadata; |
| 969 | | |
| 970 | | var serialized_len = start_serialized_len; |
| 971 | | var pipe_buf: [2 * 4096]u8 = undefined; |
| 972 | | |
| 973 | | const old_ipc_metadata_fds = ipc_metadata_fds_copy[0..ipc_metadata_len]; |
| 974 | | const old_ipc_metadata = ipc_metadata_copy[0..ipc_metadata_len]; |
| 975 | | ipc_metadata_len = 0; |
| 976 | 1078 | |
| 977 | | main_loop: for ( |
| 978 | | serialized_buffer.parents[0..serialized_len], |
| 979 | | serialized_buffer.storage[0..serialized_len], |
| 980 | | 0.., |
| 1079 | // Find nodes which correspond to child processes. |
| 1080 | const ipc_start = maybe_ipc_start orelse serialized_len; |
| 1081 | serialized_buffer.ipc_start = ipc_start; |
| 1082 | for ( |
| 1083 | serialized_buffer.parents[ipc_start..serialized_len], |
| 1084 | serialized_buffer.storage[ipc_start..serialized_len], |
| 1085 | ipc_start.., |
| 981 | 1086 | ) |main_parent, *main_storage, main_index| { |
| 982 | 1087 | if (main_parent == .unused) continue; |
| 983 | | const file: Io.File = .{ |
| 984 | | .handle = main_storage.getIpcFd() orelse continue, |
| 985 | | .flags = .{ .nonblocking = true }, |
| 986 | | }; |
| 987 | | const opt_saved_metadata = findOld(file.handle, old_ipc_metadata_fds, old_ipc_metadata); |
| 988 | | var bytes_read: usize = 0; |
| 989 | | while (true) { |
| 990 | | const n = file.readStreaming(io, &.{pipe_buf[bytes_read..]}) catch |err| switch (err) { |
| 991 | | error.WouldBlock, error.EndOfStream => break, |
| 992 | | else => |e| { |
| 993 | | std.log.debug("failed to read child progress data: {t}", .{e}); |
| 994 | | main_storage.completed_count = 0; |
| 995 | | main_storage.estimated_total_count = 0; |
| 996 | | continue :main_loop; |
| 997 | | }, |
| 998 | | }; |
| 999 | | if (opt_saved_metadata) |m| { |
| 1000 | | if (m.remaining_read_trash_bytes > 0) { |
| 1001 | | assert(bytes_read == 0); |
| 1002 | | if (m.remaining_read_trash_bytes >= n) { |
| 1003 | | m.remaining_read_trash_bytes = @intCast(m.remaining_read_trash_bytes - n); |
| 1004 | | continue; |
| 1005 | | } |
| 1006 | | const src = pipe_buf[m.remaining_read_trash_bytes..n]; |
| 1007 | | @memmove(pipe_buf[0..src.len], src); |
| 1008 | | m.remaining_read_trash_bytes = 0; |
| 1009 | | bytes_read = src.len; |
| 1010 | | continue; |
| 1011 | | } |
| 1012 | | } |
| 1013 | | bytes_read += n; |
| 1014 | | } |
| 1015 | | // Ignore all but the last message on the pipe. |
| 1016 | | var input: []u8 = pipe_buf[0..bytes_read]; |
| 1017 | | if (input.len == 0) { |
| 1018 | | serialized_len = useSavedIpcData(serialized_len, serialized_buffer, main_storage, main_index, opt_saved_metadata, 0, file.handle); |
| 1019 | | continue; |
| 1020 | | } |
| 1021 | | |
| 1022 | | const storage, const parents = while (true) { |
| 1023 | | const subtree_len: usize = input[0]; |
| 1024 | | const expected_bytes = 1 + subtree_len * (@sizeOf(Node.Storage) + @sizeOf(Node.Parent)); |
| 1025 | | if (input.len < expected_bytes) { |
| 1026 | | // Ignore short reads. We'll handle the next full message when it comes instead. |
| 1027 | | const remaining_read_trash_bytes: u16 = @intCast(expected_bytes - input.len); |
| 1028 | | serialized_len = useSavedIpcData(serialized_len, serialized_buffer, main_storage, main_index, opt_saved_metadata, remaining_read_trash_bytes, file.handle); |
| 1029 | | continue :main_loop; |
| 1030 | | } |
| 1031 | | if (input.len > expected_bytes) { |
| 1032 | | input = input[expected_bytes..]; |
| 1033 | | continue; |
| 1034 | | } |
| 1035 | | const storage_bytes = input[1..][0 .. subtree_len * @sizeOf(Node.Storage)]; |
| 1036 | | const parents_bytes = input[1 + storage_bytes.len ..][0 .. subtree_len * @sizeOf(Node.Parent)]; |
| 1037 | | break .{ |
| 1038 | | std.mem.bytesAsSlice(Node.Storage, storage_bytes), |
| 1039 | | std.mem.bytesAsSlice(Node.Parent, parents_bytes), |
| 1040 | | }; |
| 1041 | | }; |
| 1042 | | |
| 1043 | | const nodes_len: u8 = @intCast(@min(parents.len - 1, serialized_buffer.storage.len - serialized_len)); |
| 1088 | const ipc_index = main_storage.getIpcIndex() orelse continue; |
| 1089 | const ipc = &global_progress.ipc[ipc_index.slot]; |
| 1090 | const ipc_data = &serialized_buffer.ipc_data[ipc_index.slot]; |
| 1091 | state: switch (ipc_data.state) { |
| 1092 | .unused => { |
| 1093 | if (@cmpxchgWeak( |
| 1094 | Ipc, |
| 1095 | ipc, |
| 1096 | .{ .locked = false, .valid = true, .generation = ipc_index.generation }, |
| 1097 | .{ .locked = true, .valid = true, .generation = ipc_index.generation }, |
| 1098 | .acquire, |
| 1099 | .monotonic, |
| 1100 | )) |_| continue; |
| 1101 | |
| 1102 | const ipc_vec = &serialized_buffer.ipc_vecs[ipc_index.slot]; |
| 1103 | ipc_vec.* = .{&serialized_buffer.ipc_buffers[ipc_index.slot]}; |
| 1104 | batch.addAt(ipc_index.slot, .{ .file_read_streaming = .{ |
| 1105 | .file = global_progress.ipc_files[ipc_index.slot], |
| 1106 | .data = ipc_vec, |
| 1107 | } }); |
| 1108 | |
| 1109 | ipc_data.* = .{ |
| 1110 | .state = .pending, |
| 1111 | .bytes_read = 0, |
| 1112 | .main_index = @intCast(main_index), |
| 1113 | .start_index = serialized_len, |
| 1114 | .nodes_len = 0, |
| 1115 | }; |
| 1116 | main_storage.completed_count = 0; |
| 1117 | main_storage.estimated_total_count = 0; |
| 1118 | }, |
| 1119 | .pending => { |
| 1120 | const start_index = ipc_data.start_index; |
| 1121 | const nodes_len = @min(ipc_data.nodes_len, node_storage_buffer_len - serialized_len); |
| 1122 | |
| 1123 | main_storage.copyRoot(&prev_storage[ipc_data.main_index]); |
| 1124 | @memcpy( |
| 1125 | serialized_buffer.storage[serialized_len..][0..nodes_len], |
| 1126 | prev_storage[start_index..][0..nodes_len], |
| 1127 | ); |
| 1128 | for ( |
| 1129 | serialized_buffer.parents[serialized_len..][0..nodes_len], |
| 1130 | prev_parents[serialized_len..][0..nodes_len], |
| 1131 | ) |*parent, prev_parent| parent.* = switch (prev_parent) { |
| 1132 | .none, .unused => .none, |
| 1133 | _ => if (@intFromEnum(prev_parent) == ipc_data.main_index) |
| 1134 | @enumFromInt(main_index) |
| 1135 | else if (@intFromEnum(prev_parent) >= start_index and |
| 1136 | @intFromEnum(prev_parent) < start_index + nodes_len) |
| 1137 | @enumFromInt(@intFromEnum(prev_parent) - start_index + serialized_len) |
| 1138 | else |
| 1139 | .none, |
| 1140 | }; |
| 1044 | 1141 | |
| 1045 | | // Remember in case the pipe is empty on next update. |
| 1046 | | ipc_metadata_fds[ipc_metadata_len] = Fd.init(file.handle); |
| 1047 | | ipc_metadata[ipc_metadata_len] = .{ |
| 1048 | | .remaining_read_trash_bytes = 0, |
| 1049 | | .start_index = @intCast(serialized_len), |
| 1050 | | .nodes_len = nodes_len, |
| 1051 | | .main_index = @intCast(main_index), |
| 1052 | | }; |
| 1053 | | ipc_metadata_len += 1; |
| 1054 | | |
| 1055 | | // Mount the root here. |
| 1056 | | copyRoot(main_storage, &storage[0]); |
| 1057 | | if (is_big_endian) main_storage.byteSwap(); |
| 1058 | | |
| 1059 | | // Copy the rest of the tree to the end. |
| 1060 | | const storage_dest = serialized_buffer.storage[serialized_len..][0..nodes_len]; |
| 1061 | | @memcpy(storage_dest, storage[1..][0..nodes_len]); |
| 1062 | | |
| 1063 | | // Always little-endian over the pipe. |
| 1064 | | if (is_big_endian) for (storage_dest) |*s| s.byteSwap(); |
| 1065 | | |
| 1066 | | // Patch up parent pointers taking into account how the subtree is mounted. |
| 1067 | | for (serialized_buffer.parents[serialized_len..][0..nodes_len], parents[1..][0..nodes_len]) |*dest, p| { |
| 1068 | | dest.* = switch (p) { |
| 1069 | | // Fix bad data so the rest of the code does not see `unused`. |
| 1070 | | .none, .unused => .none, |
| 1071 | | // Root node is being mounted here. |
| 1072 | | @as(Node.Parent, @enumFromInt(0)) => @enumFromInt(main_index), |
| 1073 | | // Other nodes mounted at the end. |
| 1074 | | // Don't trust child data; if the data is outside the expected range, ignore the data. |
| 1075 | | // This also handles the case when data was truncated. |
| 1076 | | _ => |off| if (@intFromEnum(off) > nodes_len) |
| 1077 | | .none |
| 1078 | | else |
| 1079 | | @enumFromInt(serialized_len + @intFromEnum(off) - 1), |
| 1080 | | }; |
| 1142 | ipc_data.main_index = @intCast(main_index); |
| 1143 | ipc_data.start_index = serialized_len; |
| 1144 | ipc_data.nodes_len = nodes_len; |
| 1145 | serialized_len += nodes_len; |
| 1146 | }, |
| 1147 | .ready => { |
| 1148 | const ipc_buffer = &serialized_buffer.ipc_buffers[ipc_index.slot]; |
| 1149 | const packet_start, const packet_end = ipc_data.findLastPacket(ipc_buffer); |
| 1150 | const packet_is_empty = packet_end - packet_start <= 1; |
| 1151 | if (!packet_is_empty) { |
| 1152 | const storage, const parents, const nodes_len = packet_contents: { |
| 1153 | var packet_index: usize = packet_start; |
| 1154 | const nodes_len: u16 = ipc_buffer[packet_index]; |
| 1155 | packet_index += 1; |
| 1156 | const storage_bytes = |
| 1157 | ipc_buffer[packet_index..][0 .. nodes_len * @sizeOf(Node.Storage)]; |
| 1158 | packet_index += storage_bytes.len; |
| 1159 | const parents_bytes = |
| 1160 | ipc_buffer[packet_index..][0 .. nodes_len * @sizeOf(Node.Parent)]; |
| 1161 | packet_index += parents_bytes.len; |
| 1162 | assert(packet_index == packet_end); |
| 1163 | const storage: []align(1) const Node.Storage = @ptrCast(storage_bytes); |
| 1164 | const parents: []align(1) const Node.Parent = @ptrCast(parents_bytes); |
| 1165 | const children_nodes_len = |
| 1166 | @min(nodes_len - 1, node_storage_buffer_len - serialized_len); |
| 1167 | break :packet_contents .{ storage, parents, children_nodes_len }; |
| 1168 | }; |
| 1169 | |
| 1170 | // Mount the root here. |
| 1171 | main_storage.copyRoot(&storage[0]); |
| 1172 | if (is_big_endian) main_storage.byteSwap(); |
| 1173 | |
| 1174 | // Copy the rest of the tree to the end. |
| 1175 | const serialized_storage = |
| 1176 | serialized_buffer.storage[serialized_len..][0..nodes_len]; |
| 1177 | @memcpy(serialized_storage, storage[1..][0..nodes_len]); |
| 1178 | if (is_big_endian) for (serialized_storage) |*s| s.byteSwap(); |
| 1179 | |
| 1180 | // Patch up parent pointers taking into account how the subtree is mounted. |
| 1181 | for ( |
| 1182 | serialized_buffer.parents[serialized_len..][0..nodes_len], |
| 1183 | parents[1..][0..nodes_len], |
| 1184 | ) |*parent, prev_parent| parent.* = switch (prev_parent) { |
| 1185 | // Fix bad data so the rest of the code does not see `unused`. |
| 1186 | .none, .unused => .none, |
| 1187 | // Root node is being mounted here. |
| 1188 | @as(Node.Parent, @enumFromInt(0)) => @enumFromInt(main_index), |
| 1189 | // Other nodes mounted at the end. |
| 1190 | // Don't trust child data; if the data is outside the expected range, |
| 1191 | // ignore the data. This also handles the case when data was truncated. |
| 1192 | _ => if (@intFromEnum(prev_parent) <= nodes_len) |
| 1193 | @enumFromInt(@intFromEnum(prev_parent) - 1 + serialized_len) |
| 1194 | else |
| 1195 | .none, |
| 1196 | }; |
| 1197 | |
| 1198 | ipc_data.main_index = @intCast(main_index); |
| 1199 | ipc_data.start_index = serialized_len; |
| 1200 | ipc_data.nodes_len = nodes_len; |
| 1201 | serialized_len += nodes_len; |
| 1202 | } |
| 1203 | const ipc_vec = &serialized_buffer.ipc_vecs[ipc_index.slot]; |
| 1204 | ipc_data.rebase(ipc_buffer, ipc_vec, batch, ipc_index.slot, packet_end); |
| 1205 | ready_len -= 1; |
| 1206 | if (packet_is_empty) continue :state .pending; |
| 1207 | }, |
| 1081 | 1208 | } |
| 1082 | | |
| 1083 | | serialized_len += nodes_len; |
| 1084 | | } |
| 1085 | | |
| 1086 | | // Save a copy in case any pipes are empty on the next update. |
| 1087 | | @memcpy(serialized_buffer.parents_copy[0..serialized_len], serialized_buffer.parents[0..serialized_len]); |
| 1088 | | @memcpy(serialized_buffer.storage_copy[0..serialized_len], serialized_buffer.storage[0..serialized_len]); |
| 1089 | | @memcpy(ipc_metadata_fds_copy[0..ipc_metadata_len], ipc_metadata_fds[0..ipc_metadata_len]); |
| 1090 | | @memcpy(ipc_metadata_copy[0..ipc_metadata_len], ipc_metadata[0..ipc_metadata_len]); |
| 1091 | | |
| 1092 | | return serialized_len; |
| 1093 | | } |
| 1094 | | |
| 1095 | | fn copyRoot(dest: *Node.Storage, src: *align(1) Node.Storage) void { |
| 1096 | | dest.* = .{ |
| 1097 | | .completed_count = src.completed_count, |
| 1098 | | .estimated_total_count = src.estimated_total_count, |
| 1099 | | .name = if (src.name[0] == 0) dest.name else src.name, |
| 1100 | | }; |
| 1101 | | } |
| 1102 | | |
| 1103 | | fn findOld( |
| 1104 | | ipc_fd: Io.File.Handle, |
| 1105 | | old_metadata_fds: []Fd, |
| 1106 | | old_metadata: []SavedMetadata, |
| 1107 | | ) ?*SavedMetadata { |
| 1108 | | for (old_metadata_fds, old_metadata) |fd, *m| { |
| 1109 | | if (fd.get() == ipc_fd) |
| 1110 | | return m; |
| 1111 | 1209 | } |
| 1112 | | return null; |
| 1113 | | } |
| 1114 | | |
| 1115 | | fn useSavedIpcData( |
| 1116 | | start_serialized_len: usize, |
| 1117 | | serialized_buffer: *Serialized.Buffer, |
| 1118 | | main_storage: *Node.Storage, |
| 1119 | | main_index: usize, |
| 1120 | | opt_saved_metadata: ?*SavedMetadata, |
| 1121 | | remaining_read_trash_bytes: u16, |
| 1122 | | fd: Io.File.Handle, |
| 1123 | | ) usize { |
| 1124 | | const parents_copy = &serialized_buffer.parents_copy; |
| 1125 | | const storage_copy = &serialized_buffer.storage_copy; |
| 1126 | | const ipc_metadata_fds = &serialized_buffer.ipc_metadata_fds; |
| 1127 | | const ipc_metadata = &serialized_buffer.ipc_metadata; |
| 1128 | | |
| 1129 | | const saved_metadata = opt_saved_metadata orelse { |
| 1130 | | main_storage.completed_count = 0; |
| 1131 | | main_storage.estimated_total_count = 0; |
| 1132 | | if (remaining_read_trash_bytes > 0) { |
| 1133 | | ipc_metadata_fds[ipc_metadata_len] = Fd.init(fd); |
| 1134 | | ipc_metadata[ipc_metadata_len] = .{ |
| 1135 | | .remaining_read_trash_bytes = remaining_read_trash_bytes, |
| 1136 | | .start_index = @intCast(start_serialized_len), |
| 1137 | | .nodes_len = 0, |
| 1138 | | .main_index = @intCast(main_index), |
| 1139 | | }; |
| 1140 | | ipc_metadata_len += 1; |
| 1141 | | } |
| 1142 | | return start_serialized_len; |
| 1210 | serialized_buffer.ipc_end = serialized_len; |
| 1211 | |
| 1212 | // Ignore data from unused pipes. This ensures that if a child process exists we will |
| 1213 | // eventually see `EndOfStream` and close the pipe. |
| 1214 | if (ready_len > 0) for ( |
| 1215 | &serialized_buffer.ipc_data, |
| 1216 | &serialized_buffer.ipc_buffers, |
| 1217 | &serialized_buffer.ipc_vecs, |
| 1218 | 0.., |
| 1219 | ) |*ipc_data, *ipc_buffer, *ipc_vec, ipc_slot| switch (ipc_data.state) { |
| 1220 | .unused, .pending => {}, |
| 1221 | .ready => { |
| 1222 | _, const packet_end = ipc_data.findLastPacket(ipc_buffer); |
| 1223 | ipc_data.rebase(ipc_buffer, ipc_vec, batch, @intCast(ipc_slot), packet_end); |
| 1224 | ready_len -= 1; |
| 1225 | }, |
| 1143 | 1226 | }; |
| 1227 | assert(ready_len == 0); |
| 1144 | 1228 | |
| 1145 | | const start_index = saved_metadata.start_index; |
| 1146 | | const nodes_len = @min(saved_metadata.nodes_len, serialized_buffer.storage.len - start_serialized_len); |
| 1147 | | const old_main_index = saved_metadata.main_index; |
| 1148 | | |
| 1149 | | ipc_metadata_fds[ipc_metadata_len] = Fd.init(fd); |
| 1150 | | ipc_metadata[ipc_metadata_len] = .{ |
| 1151 | | .remaining_read_trash_bytes = remaining_read_trash_bytes, |
| 1152 | | .start_index = @intCast(start_serialized_len), |
| 1153 | | .nodes_len = nodes_len, |
| 1154 | | .main_index = @intCast(main_index), |
| 1229 | return .{ |
| 1230 | .parents = serialized_buffer.parents[0..serialized_len], |
| 1231 | .storage = serialized_buffer.storage[0..serialized_len], |
| 1155 | 1232 | }; |
| 1156 | | ipc_metadata_len += 1; |
| 1157 | | |
| 1158 | | const parents = parents_copy[start_index..][0..nodes_len]; |
| 1159 | | const storage = storage_copy[start_index..][0..nodes_len]; |
| 1160 | | |
| 1161 | | copyRoot(main_storage, &storage_copy[old_main_index]); |
| 1162 | | |
| 1163 | | @memcpy(serialized_buffer.storage[start_serialized_len..][0..storage.len], storage); |
| 1164 | | |
| 1165 | | for (serialized_buffer.parents[start_serialized_len..][0..parents.len], parents) |*dest, p| { |
| 1166 | | dest.* = switch (p) { |
| 1167 | | .none, .unused => .none, |
| 1168 | | _ => |prev| d: { |
| 1169 | | if (@intFromEnum(prev) == old_main_index) { |
| 1170 | | break :d @enumFromInt(main_index); |
| 1171 | | } else if (@intFromEnum(prev) > nodes_len) { |
| 1172 | | break :d .none; |
| 1173 | | } else { |
| 1174 | | break :d @enumFromInt(@intFromEnum(prev) - start_index + start_serialized_len); |
| 1175 | | } |
| 1176 | | }, |
| 1177 | | }; |
| 1178 | | } |
| 1179 | | |
| 1180 | | return start_serialized_len + storage.len; |
| 1181 | 1233 | } |
| 1182 | 1234 | |
| 1183 | | fn computeRedraw(serialized_buffer: *Serialized.Buffer) struct { []u8, usize } { |
| 1184 | | const serialized = serialize(serialized_buffer); |
| 1235 | fn computeRedraw(io: Io, serialized_buffer: *Serialized.Buffer) !struct { []u8, usize } { |
| 1236 | if (global_progress.rows == 0 or global_progress.cols == 0) return error.WindowTooSmall; |
| 1237 | |
| 1238 | const serialized = try serialize(io, serialized_buffer); |
| 1185 | 1239 | |
| 1186 | 1240 | // Now we can analyze our copy of the graph without atomics, reconstructing |
| 1187 | 1241 | // children lists which do not exist in the canonical data. These are |
| ... | ... | @@ -1416,9 +1470,7 @@ fn withinRowLimit(p: *Progress, nl_n: usize) bool { |
| 1416 | 1470 | return nl_n + 2 < p.rows; |
| 1417 | 1471 | } |
| 1418 | 1472 | |
| 1419 | | var remaining_write_trash_bytes: usize = 0; |
| 1420 | | |
| 1421 | | fn writeIpc(io: Io, file: Io.File, serialized: Serialized) error{BrokenPipe}!void { |
| 1473 | fn writeIpc(writer: *Io.Writer, serialized: Serialized) Io.Writer.Error!void { |
| 1422 | 1474 | // Byteswap if necessary to ensure little endian over the pipe. This is |
| 1423 | 1475 | // needed because the parent or child process might be running in qemu. |
| 1424 | 1476 | if (is_big_endian) for (serialized.storage) |*s| s.byteSwap(); |
| ... | ... | @@ -1429,62 +1481,8 @@ fn writeIpc(io: Io, file: Io.File, serialized: Serialized) error{BrokenPipe}!voi |
| 1429 | 1481 | const storage = std.mem.sliceAsBytes(serialized.storage); |
| 1430 | 1482 | const parents = std.mem.sliceAsBytes(serialized.parents); |
| 1431 | 1483 | |
| 1432 | | var vecs: [3][]const u8 = .{ header, storage, parents }; |
| 1433 | | |
| 1434 | | // Ensures the packet can fit in the pipe buffer. |
| 1435 | | const upper_bound_msg_len = 1 + node_storage_buffer_len * @sizeOf(Node.Storage) + |
| 1436 | | node_storage_buffer_len * @sizeOf(Node.OptionalIndex); |
| 1437 | | comptime assert(upper_bound_msg_len <= 4096); |
| 1438 | | |
| 1439 | | while (remaining_write_trash_bytes > 0) { |
| 1440 | | // We do this in a separate write call to give a better chance for the |
| 1441 | | // writev below to be in a single packet. |
| 1442 | | const n = @min(parents.len, remaining_write_trash_bytes); |
| 1443 | | if (file.writeStreaming(io, &.{}, &.{parents[0..n]}, 1)) |written| { |
| 1444 | | remaining_write_trash_bytes -= written; |
| 1445 | | continue; |
| 1446 | | } else |err| switch (err) { |
| 1447 | | error.WouldBlock => return, |
| 1448 | | error.BrokenPipe => return error.BrokenPipe, |
| 1449 | | else => |e| { |
| 1450 | | std.log.debug("failed to send progress to parent process: {t}", .{e}); |
| 1451 | | return error.BrokenPipe; |
| 1452 | | }, |
| 1453 | | } |
| 1454 | | } |
| 1455 | | |
| 1456 | | // If this write would block we do not want to keep trying, but we need to |
| 1457 | | // know if a partial message was written. |
| 1458 | | if (writevNonblock(io, file, &vecs)) |written| { |
| 1459 | | const total = header.len + storage.len + parents.len; |
| 1460 | | if (written < total) { |
| 1461 | | remaining_write_trash_bytes = total - written; |
| 1462 | | } |
| 1463 | | } else |err| switch (err) { |
| 1464 | | error.WouldBlock => {}, |
| 1465 | | error.BrokenPipe => return error.BrokenPipe, |
| 1466 | | else => |e| { |
| 1467 | | std.log.debug("failed to send progress to parent process: {t}", .{e}); |
| 1468 | | return error.BrokenPipe; |
| 1469 | | }, |
| 1470 | | } |
| 1471 | | } |
| 1472 | | |
| 1473 | | fn writevNonblock(io: Io, file: Io.File, iov: [][]const u8) Io.File.Writer.Error!usize { |
| 1474 | | var iov_index: usize = 0; |
| 1475 | | var written: usize = 0; |
| 1476 | | var total_written: usize = 0; |
| 1477 | | while (true) { |
| 1478 | | while (if (iov_index < iov.len) |
| 1479 | | written >= iov[iov_index].len |
| 1480 | | else |
| 1481 | | return total_written) : (iov_index += 1) written -= iov[iov_index].len; |
| 1482 | | iov[iov_index].ptr += written; |
| 1483 | | iov[iov_index].len -= written; |
| 1484 | | written = try file.writeStreaming(io, &.{}, iov, 1); |
| 1485 | | if (written == 0) return total_written; |
| 1486 | | total_written += written; |
| 1487 | | } |
| 1484 | var vec = [3][]const u8{ header, storage, parents }; |
| 1485 | try writer.writeVecAll(&vec); |
| 1488 | 1486 | } |
| 1489 | 1487 | |
| 1490 | 1488 | fn maybeUpdateSize(io: Io, resize_flag: bool) !void { |