fibers: Scheduler.deinit + struct-literal init cleanup
Scheduler.deinit closes the bounded leaks B1 documented: it reaps any leftover
ready fibers, frees every heap Task from go (now tracked via a task_allocs
field), frees the timers/io_waiters/task_allocs List backings, and closes the
lazily-opened kqueue fd. Terminal + idempotent; the per-spawn/go closure env
remains unfreeable (language limitation). Locked by
examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx, which exercises
every freed resource under a tracking GPA (freed by deinit: 5, kq reset to -1).
Also converts plain-struct '= ---'+field-assign init to '.{ ... }' literal init
where '---' carries no meaning: Scheduler.init, Dock.make, and the fiber
examples 1811/1813/1814/1816 (partial literals zero-fill the index-filled array
fields). Unions, '---'-feature tests, the 0154 regression, documented
generic-pack gaps, and loop/conditional inits are intentionally left on '---'.
This commit is contained in:
@@ -32,6 +32,10 @@ kqb :: #import "modules/std/net/kqueue.sx";
|
||||
mmap :: (addr: *void, len: i64, prot: i32, flags: i32, fd: i32, off: i64) -> *void extern libc "mmap";
|
||||
mprotect :: (addr: *void, len: i64, prot: i32) -> i32 extern libc "mprotect";
|
||||
munmap :: (addr: *void, len: i64) -> i32 extern libc "munmap";
|
||||
// Canonical libc `close` signature `(i32) -> i32` — must match any other
|
||||
// binding in the program (the extern dedupe rejects a divergent one). Used by
|
||||
// `Scheduler.deinit` to close the lazily-opened kqueue fd.
|
||||
close :: (fd: i32) -> i32 extern libc "close";
|
||||
abort :: () -> noreturn extern libc "abort";
|
||||
|
||||
PROT_NONE :: 0;
|
||||
@@ -109,33 +113,36 @@ Scheduler :: struct {
|
||||
// `block_on_fd` opens it, so a pure-compute /
|
||||
// virtual-timer scheduler never opens a kqueue
|
||||
// fd (no leak for the common case). Once opened it
|
||||
// lives for the scheduler's lifetime; there is no
|
||||
// deinit yet, so it leaks one fd at program exit
|
||||
// (bounded, harmless — same class as the spawn
|
||||
// env / go Task leaks documented above).
|
||||
// lives for the scheduler's lifetime; `deinit`
|
||||
// closes it (and resets this back to -1).
|
||||
io_waiters: List(IoWaiter); // fibers parked on fd readiness, grown through
|
||||
// `own_allocator` (long-lived-container rule: a
|
||||
// waiter outlives the `block_on_fd` call's scope).
|
||||
|
||||
// --- deinit bookkeeping: heap Tasks allocated by `go` --------------------
|
||||
task_allocs: List(*void); // every heap `*Task` from `go`, recorded so
|
||||
// `deinit` can free them. The scheduler does not
|
||||
// otherwise know its Tasks (they are generic
|
||||
// `Task($R)` handed back to the caller); without
|
||||
// this list they would leak. Grown through
|
||||
// `own_allocator` (a Task outlives the `go` call).
|
||||
|
||||
// Construct a scheduler BY VALUE (allocator value-return convention).
|
||||
// Captures the current `context.allocator` into `own_allocator` — fibers and
|
||||
// their heap `Fiber` structs outlive their spawn scope, so all internal
|
||||
// allocation must go through this captured (long-lived) allocator, not
|
||||
// whatever transient one happens to be current at a later call.
|
||||
init :: () -> Scheduler {
|
||||
s : Scheduler = ---;
|
||||
s.current = null;
|
||||
s.ready_head = null;
|
||||
s.ready_tail = null;
|
||||
s.own_allocator = context.allocator;
|
||||
s.next_id = 0;
|
||||
s.n_spawned = 0;
|
||||
s.n_suspended = 0;
|
||||
s.clock_ms = 0;
|
||||
s.timers = .{};
|
||||
s.kq = -1; // lazy: opened by the first block_on_fd
|
||||
s.io_waiters = .{};
|
||||
return s;
|
||||
// Literal init (by value). `sched_ctx` is intentionally unnamed — the
|
||||
// partial literal zero-fills it, and it is written by the first
|
||||
// `swap_context` before ever being read. `kq = -1` is the lazy sentinel
|
||||
// (opened by the first `block_on_fd`).
|
||||
return Scheduler.{
|
||||
current = null, ready_head = null, ready_tail = null,
|
||||
own_allocator = context.allocator,
|
||||
next_id = 0, n_spawned = 0, n_suspended = 0,
|
||||
clock_ms = 0, timers = .{}, kq = -1, io_waiters = .{}, task_allocs = .{}
|
||||
};
|
||||
}
|
||||
|
||||
// Spawn a fiber running `body`. Heap-allocates the `Fiber` and a guarded
|
||||
@@ -445,6 +452,65 @@ Scheduler :: struct {
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
// Release the scheduler's owned resources. TERMINAL: the scheduler is dead
|
||||
// after this — no scheduler-owned handle (the `*Task`s returned by `go`, a
|
||||
// `*Fiber` from `spawn`, the scheduler itself) may be used afterward; doing
|
||||
// so is a use-after-free, the universal deinit contract. Idempotent: a
|
||||
// second `deinit` is a no-op (it rests on `List.deinit` nulling `items` +
|
||||
// zeroing `len`, and on `kq`/`ready_head` being reset below).
|
||||
//
|
||||
// Call AFTER `run()` has returned: a clean `run()` leaves the ready queue
|
||||
// empty and aborts loudly on any orphaned suspend, so nothing is mid-flight
|
||||
// and every `task_allocs` entry is a COMPLETED task (safe to free). Frees,
|
||||
// in order:
|
||||
// 1. any fibers still enqueued ready — a leak-SAFETY NET for the misuse
|
||||
// path (`spawn`/`go` without a following `run()`, or after it returned),
|
||||
// NOT a blessed reuse pattern: reaping a `go`'s fiber here while step (2)
|
||||
// frees its paired `*Task` is self-consistent ONLY because the contract
|
||||
// already forbade touching those handles post-`deinit`. A suspended
|
||||
// (off-queue) fiber is unreachable from here, but a clean `run()` never
|
||||
// leaves one (it aborts on an orphaned suspend);
|
||||
// 2. every heap `*Task` from `go` (recorded in `task_allocs`);
|
||||
// 3. the three `List` backings (`task_allocs`, `timers`, `io_waiters`),
|
||||
// each grown through `own_allocator`;
|
||||
// 4. the kqueue fd, if `block_on_fd` ever opened it (lazy `-1` otherwise).
|
||||
//
|
||||
// NOT freed (documented language limitation, unchanged): one closure env per
|
||||
// `spawn`/`go`. The env is heap-allocated at the closure-literal site and sx
|
||||
// exposes no way to free it (the scheduler cannot name the env pointer), so
|
||||
// it leaks until program exit — bounded by the spawn/go count, invisible
|
||||
// under the default GPA. Freeing it needs a closure-env-ownership affordance.
|
||||
deinit :: (self: *Scheduler) {
|
||||
// (1) Reap leftover ready fibers: unmap the stack, free the Fiber.
|
||||
f := self.ready_head;
|
||||
while f != null {
|
||||
nxt := f.next;
|
||||
munmap(f.stack_region, f.stack_len);
|
||||
self.own_allocator.dealloc_bytes(xx f);
|
||||
f = nxt;
|
||||
}
|
||||
self.ready_head = null;
|
||||
self.ready_tail = null;
|
||||
|
||||
// (2) Free every heap Task allocated by `go`.
|
||||
i := 0;
|
||||
while i < self.task_allocs.len {
|
||||
self.own_allocator.dealloc_bytes(self.task_allocs.items[i]);
|
||||
i = i + 1;
|
||||
}
|
||||
|
||||
// (3) Free the List backings (all grown through `own_allocator`).
|
||||
self.task_allocs.deinit(self.own_allocator);
|
||||
self.timers.deinit(self.own_allocator);
|
||||
self.io_waiters.deinit(self.own_allocator);
|
||||
|
||||
// (4) Close the kqueue fd if it was ever opened (lazy: -1 if never used).
|
||||
if self.kq >= 0 {
|
||||
close(self.kq);
|
||||
self.kq = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- the context switch (naked) + first-entry trampoline -------------------
|
||||
@@ -719,6 +785,9 @@ go :: ufcs (self: *Scheduler, work: Closure() -> $R) -> *Task($R) {
|
||||
t.waiter = null;
|
||||
t.sched = self;
|
||||
t.canceled = 0;
|
||||
// Record the heap Task so `deinit` can free it (the scheduler otherwise has
|
||||
// no handle on its generic Tasks). Long-lived: a Task outlives this call.
|
||||
self.task_allocs.append(xx t, self.own_allocator);
|
||||
self.spawn(() => {
|
||||
// Cooperative cancel: skip the work entirely if cancel already landed
|
||||
// before this fiber was scheduled (saves the compute + side effects). A
|
||||
|
||||
@@ -472,20 +472,20 @@ Dock :: struct {
|
||||
on_dock: ?Closure(i64, DockZone);
|
||||
|
||||
make :: (interaction: *DockInteraction, delta_time: *f32) -> Dock {
|
||||
d : Dock = ---;
|
||||
d.children = List(ViewChild).{};
|
||||
d.alignments = List(Alignment).{};
|
||||
d.interaction = interaction;
|
||||
d.delta_time = delta_time;
|
||||
d.background = null;
|
||||
d.corner_radius = 0.0;
|
||||
d.hint_size = 40.0;
|
||||
d.hint_color = Color.rgba(77, 153, 255, 153);
|
||||
d.hint_active_color = Color.rgba(77, 153, 255, 230);
|
||||
d.preview_color = Color.rgba(77, 153, 255, 64);
|
||||
d.enable_corners = true;
|
||||
d.on_dock = null;
|
||||
d
|
||||
Dock.{
|
||||
children = List(ViewChild).{},
|
||||
alignments = List(Alignment).{},
|
||||
interaction = interaction,
|
||||
delta_time = delta_time,
|
||||
background = null,
|
||||
corner_radius = 0.0,
|
||||
hint_size = 40.0,
|
||||
hint_color = Color.rgba(77, 153, 255, 153),
|
||||
hint_active_color = Color.rgba(77, 153, 255, 230),
|
||||
preview_color = Color.rgba(77, 153, 255, 64),
|
||||
enable_corners = true,
|
||||
on_dock = null
|
||||
}
|
||||
}
|
||||
|
||||
add_panel :: (self: *Dock, panel: DockPanel) {
|
||||
|
||||
Reference in New Issue
Block a user