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:
@@ -37,9 +37,7 @@ append :: (sh: *Shared, v: i64) {
|
||||
}
|
||||
|
||||
main :: () -> i64 {
|
||||
sh : Shared = ---;
|
||||
sh.n = 0;
|
||||
sh.done[0] = 0; sh.done[1] = 0; sh.done[2] = 0;
|
||||
sh : Shared = .{ n = 0 }; // seq[] + done[] zero-filled
|
||||
|
||||
s := sched.Scheduler.init();
|
||||
ps := @s;
|
||||
|
||||
@@ -32,8 +32,7 @@ Log :: struct { seq: [16]i64; n: i64; }
|
||||
rec :: (l: *Log, v: i64) { l.seq[l.n] = v; l.n = l.n + 1; }
|
||||
|
||||
main :: () -> i64 {
|
||||
lg : Log = ---;
|
||||
lg.n = 0;
|
||||
lg : Log = .{ n = 0 }; // seq[] zero-filled
|
||||
|
||||
s := sched.Scheduler.init();
|
||||
ps := @s;
|
||||
|
||||
@@ -43,8 +43,7 @@ Log :: struct { ids: [16]i64; ts: [16]i64; n: i64; }
|
||||
rec :: (l: *Log, id: i64, t: i64) { l.ids[l.n] = id; l.ts[l.n] = t; l.n = l.n + 1; }
|
||||
|
||||
main :: () -> i64 {
|
||||
lg : Log = ---;
|
||||
lg.n = 0;
|
||||
lg : Log = .{ n = 0 }; // ids[] + ts[] zero-filled
|
||||
|
||||
s := sched.Scheduler.init();
|
||||
ps := @s;
|
||||
|
||||
@@ -41,10 +41,7 @@ S :: struct {
|
||||
}
|
||||
|
||||
main :: () -> i64 {
|
||||
st : S = ---;
|
||||
st.wrote = false;
|
||||
st.read_n = 0;
|
||||
st.read_done = false;
|
||||
st : S = .{ wrote = false, read_n = 0, read_done = false }; // bytes[] zero-filled
|
||||
|
||||
fds : [2]i32 = ---;
|
||||
if pipe(@fds[0]) != 0 {
|
||||
|
||||
124
examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx
Normal file
124
examples/concurrency/1820-concurrency-fiber-scheduler-deinit.sx
Normal file
@@ -0,0 +1,124 @@
|
||||
// Stream B1 (fibers) — `Scheduler.deinit` releases the scheduler's owned heap
|
||||
// + fd resources, closing the documented bounded leaks (kq fd / heap Tasks /
|
||||
// List backings). Verified by a tracking `GPA`: deinit drives the live
|
||||
// allocation count DOWN, and resets the kqueue fd to -1.
|
||||
//
|
||||
// Scenario (one run that touches every freed resource):
|
||||
// - a SLEEPER fiber `sleep(5)`s → exercises the `timers` List
|
||||
// - a READER fiber `block_on_fd`s a pipe → exercises the kqueue fd + the
|
||||
// `io_waiters` List
|
||||
// - a WRITER fiber writes 3 bytes → makes the pipe readable
|
||||
// - two `go` tasks compute 42 / 7 → exercise the heap `Task`s +
|
||||
// the `task_allocs` List
|
||||
// After `run()` drains all of it, `deinit()` frees: the 2 heap Tasks, the
|
||||
// `timers` / `io_waiters` / `task_allocs` List backings, and CLOSES the kqueue
|
||||
// fd (resetting `kq` to -1). The Fibers were already reaped during `run()`.
|
||||
//
|
||||
// WHAT IT PROVES (the contract; numbers below are the snapshot):
|
||||
// - `freed by deinit: N` — live allocations reclaimed by `deinit` (> 0).
|
||||
// - `live after deinit` — the RESIDUAL. This is NOT zero and NOT a bug: it is
|
||||
// exactly the documented closure-env leak — one heap env per `spawn`/`go`
|
||||
// that sx cannot free (the runtime has no name for the env pointer). deinit
|
||||
// reclaims everything it CAN; the env residual is a language limitation.
|
||||
// - `kq open after run: 1` then `kq after deinit: -1` — the lazily-opened
|
||||
// kqueue fd was genuinely open after the fd round and is closed by deinit.
|
||||
// - `read: 3 [97 98 99]` — the fd path actually ran (reader blocked, woke via
|
||||
// kqueue, read 'a' 'b' 'c'), so the kq we close is a real, used fd.
|
||||
//
|
||||
// Counts are captured into locals BEFORE any `print` — `print` itself allocates
|
||||
// format temporaries through the same GPA, which would otherwise pollute the
|
||||
// reading.
|
||||
//
|
||||
// aarch64-macOS-pinned (`.build {"target":"macos"}`, matches host → runs
|
||||
// end-to-end): sched.sx's switch asm + the kqueue path are per-arch/Apple.
|
||||
#import "modules/std.sx";
|
||||
sched :: #import "modules/std/sched.sx";
|
||||
|
||||
// Raw libc fd primitives — canonical signatures (the extern dedupe rejects a
|
||||
// divergent re-binding of the same C symbol). `close` matches sched.sx's own.
|
||||
pipe :: (fds: *i32) -> i32 extern libc "pipe";
|
||||
read :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "read";
|
||||
write :: (fd: i32, buf: [*]u8, count: usize) -> isize extern libc "write";
|
||||
close :: (fd: i32) -> i32 extern libc "close";
|
||||
|
||||
S :: struct {
|
||||
read_n: i64;
|
||||
bytes: [8]u8;
|
||||
read_done: bool;
|
||||
}
|
||||
|
||||
main :: () -> i64 {
|
||||
st : S = .{ read_n = 0, read_done = false }; // bytes[] zero-filled; read() fills it
|
||||
|
||||
fds : [2]i32 = ---;
|
||||
if pipe(@fds[0]) != 0 {
|
||||
print("1820: pipe() failed\n");
|
||||
return 1;
|
||||
}
|
||||
read_fd := fds[0];
|
||||
write_fd := fds[1];
|
||||
|
||||
// Captured under the GPA scope; printed after it closes.
|
||||
after_run : i64 = 0;
|
||||
after_deinit : i64 = 0;
|
||||
kq_open_run : bool = false;
|
||||
kq_after : i32 = 0;
|
||||
|
||||
gpa := mem.GPA.init();
|
||||
push Context.{ allocator = xx gpa, data = null } {
|
||||
s := sched.Scheduler.init();
|
||||
ps := @s; pst := @st;
|
||||
|
||||
// SLEEPER — arms a virtual-time timer, then parks.
|
||||
ps.spawn(() => { ps.sleep(5); });
|
||||
|
||||
// READER — blocks on the empty pipe until kqueue reports it readable.
|
||||
mk_reader :: (ps: *sched.Scheduler, pst: *S, rfd: i32) {
|
||||
ps.spawn(() => {
|
||||
ps.block_on_fd(rfd, true);
|
||||
n := read(rfd, xx @pst.bytes[0], xx 3);
|
||||
pst.read_n = xx n;
|
||||
pst.read_done = true;
|
||||
});
|
||||
}
|
||||
// WRITER — writes 'a' 'b' 'c', making the pipe readable.
|
||||
mk_writer :: (ps: *sched.Scheduler, wfd: i32) {
|
||||
ps.spawn(() => {
|
||||
buf : [3]u8 = ---;
|
||||
buf[0] = xx 97; buf[1] = xx 98; buf[2] = xx 99;
|
||||
write(wfd, xx @buf[0], xx 3);
|
||||
});
|
||||
}
|
||||
mk_reader(ps, pst, read_fd);
|
||||
mk_writer(ps, write_fd);
|
||||
|
||||
// Two async tasks — heap Tasks tracked for deinit to free.
|
||||
ps.go(() -> i64 => 42);
|
||||
ps.go(() -> i64 => 7);
|
||||
|
||||
ps.run();
|
||||
|
||||
after_run = gpa.alloc_count;
|
||||
kq_open_run = s.kq >= 0;
|
||||
ps.deinit();
|
||||
after_deinit = gpa.alloc_count;
|
||||
kq_after = s.kq;
|
||||
}
|
||||
|
||||
print("read: {} [", st.read_n);
|
||||
i := 0;
|
||||
while i < st.read_n {
|
||||
if i > 0 { print(" "); }
|
||||
print("{}", st.bytes[i]);
|
||||
i = i + 1;
|
||||
}
|
||||
print("]\n");
|
||||
print("freed by deinit: {}\n", after_run - after_deinit);
|
||||
print("live after deinit: {}\n", after_deinit);
|
||||
print("kq open after run: {}\n", kq_open_run);
|
||||
print("kq after deinit: {}\n", kq_after);
|
||||
|
||||
close(read_fd);
|
||||
close(write_fd);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "target": "macos" }
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
read: 3 [97 98 99]
|
||||
freed by deinit: 5
|
||||
live after deinit: 5
|
||||
kq open after run: true
|
||||
kq after deinit: -1
|
||||
Reference in New Issue
Block a user