| ... | ... | @@ -4,28 +4,23 @@ const assert = std.debug.assert; |
| 4 | 4 | const Allocator = std.mem.Allocator; |
| 5 | 5 | const Io = std.Io; |
| 6 | 6 | const EventLoop = @This(); |
| 7 | const Alignment = std.mem.Alignment; |
| 7 | 8 | |
| 8 | 9 | gpa: Allocator, |
| 9 | 10 | mutex: std.Thread.Mutex, |
| 10 | 11 | cond: std.Thread.Condition, |
| 11 | 12 | queue: std.DoublyLinkedList(void), |
| 12 | 13 | free: std.DoublyLinkedList(void), |
| 13 | | main_fiber_buffer: [@sizeOf(Fiber) + max_result_len]u8 align(@alignOf(Fiber)), |
| 14 | main_context: Context, |
| 14 | 15 | exit_awaiter: ?*Fiber, |
| 15 | 16 | idle_count: usize, |
| 16 | 17 | threads: std.ArrayListUnmanaged(Thread), |
| 17 | 18 | |
| 18 | 19 | threadlocal var current_idle_context: *Context = undefined; |
| 19 | | threadlocal var current_fiber_context: *Context = undefined; |
| 20 | threadlocal var current_context: *Context = undefined; |
| 20 | 21 | |
| 21 | | /// Also used for context. |
| 22 | | const max_result_len = 64; |
| 23 | | /// Also used for context. |
| 24 | | const max_result_align: std.mem.Alignment = .@"16"; |
| 25 | | |
| 26 | | const min_stack_size = 4 * 1024 * 1024; |
| 22 | /// Empirically saw 10KB being used by the self-hosted backend for logging. |
| 27 | 23 | const idle_stack_size = 32 * 1024; |
| 28 | | const stack_align = 16; |
| 29 | 24 | |
| 30 | 25 | const Thread = struct { |
| 31 | 26 | thread: std.Thread, |
| ... | ... | @@ -33,45 +28,53 @@ const Thread = struct { |
| 33 | 28 | }; |
| 34 | 29 | |
| 35 | 30 | const Fiber = struct { |
| 36 | | _: void align(max_result_align.toByteUnits()) = {}, |
| 37 | | |
| 38 | 31 | context: Context, |
| 39 | 32 | awaiter: ?*Fiber, |
| 40 | 33 | queue_node: std.DoublyLinkedList(void).Node, |
| 34 | result_align: Alignment, |
| 41 | 35 | |
| 42 | 36 | const finished: ?*Fiber = @ptrFromInt(std.mem.alignBackward(usize, std.math.maxInt(usize), @alignOf(Fiber))); |
| 43 | 37 | |
| 44 | | fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 { |
| 45 | | const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); |
| 46 | | return base[0..std.mem.alignForward( |
| 38 | const max_result_align: Alignment = .@"16"; |
| 39 | const max_result_size = max_result_align.forward(64); |
| 40 | /// This includes any stack realignments that need to happen, and also the |
| 41 | /// initial frame return address slot and argument frame, depending on target. |
| 42 | const min_stack_size = 4 * 1024 * 1024; |
| 43 | const max_context_align: Alignment = .@"16"; |
| 44 | const max_context_size = max_context_align.forward(1024); |
| 45 | const allocation_size = std.mem.alignForward( |
| 46 | usize, |
| 47 | std.mem.alignForward( |
| 47 | 48 | usize, |
| 48 | | resultOffset() + max_result_len + min_stack_size, |
| 49 | | std.heap.page_size_max, |
| 50 | | )]; |
| 51 | | } |
| 52 | | |
| 53 | | fn argsOffset() usize { |
| 54 | | return max_result_align.forward(@sizeOf(Fiber)); |
| 55 | | } |
| 56 | | |
| 57 | | fn resultOffset() usize { |
| 58 | | return max_result_align.forward(argsOffset() + max_result_len); |
| 59 | | } |
| 60 | | |
| 61 | | fn argsSlice(f: *Fiber) []u8 { |
| 62 | | const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); |
| 63 | | return base[argsOffset()..][0..max_result_len]; |
| 49 | max_result_align.forward(@sizeOf(Fiber)) + max_result_size + min_stack_size, |
| 50 | @max(@alignOf(AsyncClosure), max_context_align.toByteUnits()), |
| 51 | ) + @sizeOf(AsyncClosure) + max_context_size, |
| 52 | std.heap.page_size_max, |
| 53 | ); |
| 54 | |
| 55 | fn allocate(el: *EventLoop) error{OutOfMemory}!*Fiber { |
| 56 | return if (free_node: { |
| 57 | el.mutex.lock(); |
| 58 | defer el.mutex.unlock(); |
| 59 | break :free_node el.free.pop(); |
| 60 | }) |free_node| |
| 61 | @alignCast(@fieldParentPtr("queue_node", free_node)) |
| 62 | else |
| 63 | @ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), allocation_size)); |
| 64 | 64 | } |
| 65 | 65 | |
| 66 | | fn resultSlice(f: *Fiber) []u8 { |
| 67 | | const base: [*]align(@alignOf(Fiber)) u8 = @ptrCast(f); |
| 68 | | return base[resultOffset()..][0..max_result_len]; |
| 66 | fn allocatedSlice(f: *Fiber) []align(@alignOf(Fiber)) u8 { |
| 67 | return @as([*]align(@alignOf(Fiber)) u8, @ptrCast(f))[0..allocation_size]; |
| 69 | 68 | } |
| 70 | 69 | |
| 71 | | fn stackEndPointer(f: *Fiber) [*]u8 { |
| 70 | fn allocatedEnd(f: *Fiber) [*]u8 { |
| 72 | 71 | const allocated_slice = f.allocatedSlice(); |
| 73 | 72 | return allocated_slice[allocated_slice.len..].ptr; |
| 74 | 73 | } |
| 74 | |
| 75 | fn resultPointer(f: *Fiber) [*]u8 { |
| 76 | return @ptrFromInt(f.result_align.forward(@intFromPtr(f) + @sizeOf(Fiber))); |
| 77 | } |
| 75 | 78 | }; |
| 76 | 79 | |
| 77 | 80 | pub fn io(el: *EventLoop) Io { |
| ... | ... | @@ -88,7 +91,7 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { |
| 88 | 91 | const threads_bytes = ((std.Thread.getCpuCount() catch 1) -| 1) * @sizeOf(Thread); |
| 89 | 92 | const idle_context_offset = std.mem.alignForward(usize, threads_bytes, @alignOf(Context)); |
| 90 | 93 | const idle_stack_end_offset = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); |
| 91 | | const allocated_slice = try gpa.alignedAlloc(u8, @max(@alignOf(Thread), @alignOf(Context), stack_align), idle_stack_end_offset); |
| 94 | const allocated_slice = try gpa.alignedAlloc(u8, @max(@alignOf(Thread), @alignOf(Context)), idle_stack_end_offset); |
| 92 | 95 | errdefer gpa.free(allocated_slice); |
| 93 | 96 | el.* = .{ |
| 94 | 97 | .gpa = gpa, |
| ... | ... | @@ -96,13 +99,13 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { |
| 96 | 99 | .cond = .{}, |
| 97 | 100 | .queue = .{}, |
| 98 | 101 | .free = .{}, |
| 99 | | .main_fiber_buffer = undefined, |
| 102 | .main_context = undefined, |
| 100 | 103 | .exit_awaiter = null, |
| 101 | 104 | .idle_count = 0, |
| 102 | 105 | .threads = .initBuffer(@ptrCast(allocated_slice[0..threads_bytes])), |
| 103 | 106 | }; |
| 104 | 107 | const main_idle_context: *Context = @alignCast(std.mem.bytesAsValue(Context, allocated_slice[idle_context_offset..][0..@sizeOf(Context)])); |
| 105 | | const idle_stack_end: [*]align(stack_align) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); |
| 108 | const idle_stack_end: [*]align(@max(@alignOf(Thread), @alignOf(Context))) usize = @alignCast(@ptrCast(allocated_slice[idle_stack_end_offset..].ptr)); |
| 106 | 109 | (idle_stack_end - 1)[0..1].* = .{@intFromPtr(el)}; |
| 107 | 110 | main_idle_context.* = .{ |
| 108 | 111 | .rsp = @intFromPtr(idle_stack_end - 1), |
| ... | ... | @@ -111,9 +114,8 @@ pub fn init(el: *EventLoop, gpa: Allocator) error{OutOfMemory}!void { |
| 111 | 114 | }; |
| 112 | 115 | std.log.debug("created main idle {*}", .{main_idle_context}); |
| 113 | 116 | current_idle_context = main_idle_context; |
| 114 | | const current_fiber: *Fiber = @ptrCast(&el.main_fiber_buffer); |
| 115 | | std.log.debug("created main fiber {*}", .{current_fiber}); |
| 116 | | current_fiber_context = &current_fiber.context; |
| 117 | std.log.debug("created main {*}", .{&el.main_context}); |
| 118 | current_context = &el.main_context; |
| 117 | 119 | } |
| 118 | 120 | |
| 119 | 121 | pub fn deinit(el: *EventLoop) void { |
| ... | ... | @@ -125,27 +127,11 @@ pub fn deinit(el: *EventLoop) void { |
| 125 | 127 | } |
| 126 | 128 | const idle_context_offset = std.mem.alignForward(usize, el.threads.items.len * @sizeOf(Thread), @alignOf(Context)); |
| 127 | 129 | const idle_stack_end = std.mem.alignForward(usize, idle_context_offset + idle_stack_size, std.heap.page_size_max); |
| 128 | | const allocated_ptr: [*]align(@max(@alignOf(Thread), @alignOf(Context), stack_align)) u8 = @alignCast(@ptrCast(el.threads.items.ptr)); |
| 130 | const allocated_ptr: [*]align(@max(@alignOf(Thread), @alignOf(Context))) u8 = @alignCast(@ptrCast(el.threads.items.ptr)); |
| 129 | 131 | for (el.threads.items) |*thread| thread.thread.join(); |
| 130 | 132 | el.gpa.free(allocated_ptr[0..idle_stack_end]); |
| 131 | 133 | } |
| 132 | 134 | |
| 133 | | fn allocateFiber(el: *EventLoop) error{OutOfMemory}!*Fiber { |
| 134 | | const free_node = free_node: { |
| 135 | | el.mutex.lock(); |
| 136 | | defer el.mutex.unlock(); |
| 137 | | break :free_node el.free.pop(); |
| 138 | | } orelse { |
| 139 | | const n = std.mem.alignForward( |
| 140 | | usize, |
| 141 | | Fiber.resultOffset() + max_result_len + min_stack_size, |
| 142 | | std.heap.page_size_max, |
| 143 | | ); |
| 144 | | return @alignCast(@ptrCast(try el.gpa.alignedAlloc(u8, @alignOf(Fiber), n))); |
| 145 | | }; |
| 146 | | return @alignCast(@fieldParentPtr("queue_node", free_node)); |
| 147 | | } |
| 148 | | |
| 149 | 135 | fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) void { |
| 150 | 136 | const ready_context: *Context = ready_context: { |
| 151 | 137 | const ready_fiber: *Fiber = optional_fiber orelse if (ready_node: { |
| ... | ... | @@ -159,7 +145,7 @@ fn yield(el: *EventLoop, optional_fiber: ?*Fiber, register_awaiter: ?*?*Fiber) v |
| 159 | 145 | break :ready_context &ready_fiber.context; |
| 160 | 146 | }; |
| 161 | 147 | const message: SwitchMessage = .{ |
| 162 | | .prev_context = current_fiber_context, |
| 148 | .prev_context = current_context, |
| 163 | 149 | .ready_context = ready_context, |
| 164 | 150 | .register_awaiter = register_awaiter, |
| 165 | 151 | }; |
| ... | ... | @@ -189,14 +175,13 @@ fn schedule(el: *EventLoop, fiber: *Fiber) void { |
| 189 | 175 | |
| 190 | 176 | fn recycle(el: *EventLoop, fiber: *Fiber) void { |
| 191 | 177 | std.log.debug("recyling {*}", .{fiber}); |
| 192 | | fiber.awaiter = undefined; |
| 193 | | @memset(fiber.resultSlice(), undefined); |
| 178 | @memset(fiber.allocatedSlice(), undefined); |
| 194 | 179 | el.mutex.lock(); |
| 195 | 180 | defer el.mutex.unlock(); |
| 196 | 181 | el.free.append(&fiber.queue_node); |
| 197 | 182 | } |
| 198 | 183 | |
| 199 | | fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.c) noreturn { |
| 184 | fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.withStackAlign(.c, @max(@alignOf(Thread), @alignOf(Context)))) noreturn { |
| 200 | 185 | message.handle(el); |
| 201 | 186 | el.yield(el.idle(), null); |
| 202 | 187 | unreachable; // switched to dead fiber |
| ... | ... | @@ -205,7 +190,7 @@ fn mainIdle(el: *EventLoop, message: *const SwitchMessage) callconv(.c) noreturn |
| 205 | 190 | fn threadEntry(el: *EventLoop, thread: *Thread) void { |
| 206 | 191 | std.log.debug("created thread idle {*}", .{&thread.idle_context}); |
| 207 | 192 | current_idle_context = &thread.idle_context; |
| 208 | | current_fiber_context = &thread.idle_context; |
| 193 | current_context = &thread.idle_context; |
| 209 | 194 | _ = el.idle(); |
| 210 | 195 | } |
| 211 | 196 | |
| ... | ... | @@ -230,7 +215,7 @@ const SwitchMessage = extern struct { |
| 230 | 215 | register_awaiter: ?*?*Fiber, |
| 231 | 216 | |
| 232 | 217 | fn handle(message: *const SwitchMessage, el: *EventLoop) void { |
| 233 | | current_fiber_context = message.ready_context; |
| 218 | current_context = message.ready_context; |
| 234 | 219 | if (message.register_awaiter) |awaiter| { |
| 235 | 220 | const prev_fiber: *Fiber = @alignCast(@fieldParentPtr("context", message.prev_context)); |
| 236 | 221 | if (@atomicRmw(?*Fiber, awaiter, .Xchg, prev_fiber, .acq_rel) == Fiber.finished) el.schedule(prev_fiber); |
| ... | ... | @@ -238,10 +223,13 @@ const SwitchMessage = extern struct { |
| 238 | 223 | } |
| 239 | 224 | }; |
| 240 | 225 | |
| 241 | | const Context = extern struct { |
| 242 | | rsp: usize, |
| 243 | | rbp: usize, |
| 244 | | rip: usize, |
| 226 | const Context = switch (builtin.cpu.arch) { |
| 227 | .x86_64 => extern struct { |
| 228 | rsp: u64, |
| 229 | rbp: u64, |
| 230 | rip: u64, |
| 231 | }, |
| 232 | else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), |
| 245 | 233 | }; |
| 246 | 234 | |
| 247 | 235 | inline fn contextSwitch(message: *const SwitchMessage) *const SwitchMessage { |
| ... | ... | @@ -299,42 +287,45 @@ fn fiberEntry() callconv(.naked) void { |
| 299 | 287 | pub fn @"async"( |
| 300 | 288 | userdata: ?*anyopaque, |
| 301 | 289 | result: []u8, |
| 302 | | result_alignment: std.mem.Alignment, |
| 290 | result_alignment: Alignment, |
| 303 | 291 | context: []const u8, |
| 304 | | context_alignment: std.mem.Alignment, |
| 292 | context_alignment: Alignment, |
| 305 | 293 | start: *const fn (context: *const anyopaque, result: *anyopaque) void, |
| 306 | 294 | ) ?*std.Io.AnyFuture { |
| 307 | | assert(result_alignment.compare(.lte, max_result_align)); // TODO |
| 308 | | assert(context_alignment.compare(.lte, max_result_align)); // TODO |
| 309 | | assert(result.len <= max_result_len); // TODO |
| 310 | | assert(context.len <= max_result_len); // TODO |
| 295 | assert(result_alignment.compare(.lte, Fiber.max_result_align)); // TODO |
| 296 | assert(context_alignment.compare(.lte, Fiber.max_context_align)); // TODO |
| 297 | assert(result.len <= Fiber.max_result_size); // TODO |
| 298 | assert(context.len <= Fiber.max_context_size); // TODO |
| 311 | 299 | |
| 312 | 300 | const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); |
| 313 | | const fiber = event_loop.allocateFiber() catch { |
| 301 | const fiber = Fiber.allocate(event_loop) catch { |
| 314 | 302 | start(context.ptr, result.ptr); |
| 315 | 303 | return null; |
| 316 | 304 | }; |
| 317 | | fiber.awaiter = null; |
| 318 | | fiber.queue_node = .{ .data = {} }; |
| 319 | | @memcpy(fiber.argsSlice()[0..context.len], context); |
| 320 | 305 | std.log.debug("allocated {*}", .{fiber}); |
| 321 | 306 | |
| 322 | | const closure: *AsyncClosure = @ptrFromInt(std.mem.alignBackward( |
| 323 | | usize, |
| 324 | | @intFromPtr(fiber.stackEndPointer() - @sizeOf(AsyncClosure)), |
| 325 | | @max(@alignOf(AsyncClosure), stack_align), |
| 326 | | )); |
| 307 | const closure: *AsyncClosure = @ptrFromInt(Fiber.max_context_align.max(.of(AsyncClosure)).backward( |
| 308 | @intFromPtr(fiber.allocatedEnd()) - Fiber.max_context_size, |
| 309 | ) - @sizeOf(AsyncClosure)); |
| 310 | fiber.* = .{ |
| 311 | .context = switch (builtin.cpu.arch) { |
| 312 | .x86_64 => .{ |
| 313 | .rsp = @intFromPtr(closure) - @sizeOf(usize), |
| 314 | .rbp = 0, |
| 315 | .rip = @intFromPtr(&fiberEntry), |
| 316 | }, |
| 317 | else => |arch| @compileError("unimplemented architecture: " ++ @tagName(arch)), |
| 318 | }, |
| 319 | .awaiter = null, |
| 320 | .queue_node = undefined, |
| 321 | .result_align = result_alignment, |
| 322 | }; |
| 327 | 323 | closure.* = .{ |
| 328 | 324 | .event_loop = event_loop, |
| 329 | 325 | .fiber = fiber, |
| 330 | 326 | .start = start, |
| 331 | 327 | }; |
| 332 | | const stack_end: [*]align(stack_align) usize = @alignCast(@ptrCast(closure)); |
| 333 | | fiber.context = .{ |
| 334 | | .rsp = @intFromPtr(stack_end - 1), |
| 335 | | .rbp = 0, |
| 336 | | .rip = @intFromPtr(&fiberEntry), |
| 337 | | }; |
| 328 | @memcpy(closure.contextPointer(), context); |
| 338 | 329 | |
| 339 | 330 | event_loop.schedule(fiber); |
| 340 | 331 | return @ptrCast(fiber); |
| ... | ... | @@ -345,10 +336,14 @@ const AsyncClosure = struct { |
| 345 | 336 | fiber: *Fiber, |
| 346 | 337 | start: *const fn (context: *const anyopaque, result: *anyopaque) void, |
| 347 | 338 | |
| 348 | | fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.c) noreturn { |
| 339 | fn contextPointer(closure: *AsyncClosure) [*]align(Fiber.max_context_align.toByteUnits()) u8 { |
| 340 | return @alignCast(@as([*]u8, @ptrCast(closure)) + @sizeOf(AsyncClosure)); |
| 341 | } |
| 342 | |
| 343 | fn call(closure: *AsyncClosure, message: *const SwitchMessage) callconv(.withStackAlign(.c, @alignOf(AsyncClosure))) noreturn { |
| 349 | 344 | message.handle(closure.event_loop); |
| 350 | 345 | std.log.debug("{*} performing async", .{closure.fiber}); |
| 351 | | closure.start(closure.fiber.argsSlice().ptr, closure.fiber.resultSlice().ptr); |
| 346 | closure.start(closure.contextPointer(), closure.fiber.resultPointer()); |
| 352 | 347 | const awaiter = @atomicRmw(?*Fiber, &closure.fiber.awaiter, .Xchg, Fiber.finished, .acq_rel); |
| 353 | 348 | closure.event_loop.yield(awaiter, null); |
| 354 | 349 | unreachable; // switched to dead fiber |
| ... | ... | @@ -358,8 +353,7 @@ const AsyncClosure = struct { |
| 358 | 353 | pub fn @"await"(userdata: ?*anyopaque, any_future: *std.Io.AnyFuture, result: []u8) void { |
| 359 | 354 | const event_loop: *EventLoop = @alignCast(@ptrCast(userdata)); |
| 360 | 355 | const future_fiber: *Fiber = @alignCast(@ptrCast(any_future)); |
| 361 | | const result_src = future_fiber.resultSlice()[0..result.len]; |
| 362 | 356 | if (@atomicLoad(?*Fiber, &future_fiber.awaiter, .acquire) != Fiber.finished) event_loop.yield(null, &future_fiber.awaiter); |
| 363 | | @memcpy(result, result_src); |
| 357 | @memcpy(result, future_fiber.resultPointer()); |
| 364 | 358 | event_loop.recycle(future_fiber); |
| 365 | 359 | } |