@ai-ecoverse/slicc-kernel 1.124.0 → 1.124.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -735,7 +735,7 @@ Programs mount and unmount with `mount(2)` and `umount2(2)`, so a `mount`/`umoun
735
735
 
736
736
  ### Exec
737
737
 
738
- An exec'd program takes over the pid of the process it replaces, as on Linux: `getpid()` and `$$` in the new image, `/proc`, `ps`, `kill` and the parent's `waitpid` all agree on that pid, and the image's parent pid is the replaced process's. Its own children see that pid as their parent. A file removed or renamed over while programs hold it open keeps its bytes for them, as on Linux, whether the descriptors live in the kernel or a program opened the file in its own worker: every open, read-only ones included, is known to the kernel, and all holders share the one file (writes through one are seen through the others). The kernel does not copy it: as NFS does with its "silly rename", it moves the file into a hidden directory, `.slicc-unlinked/`, at the root of the mount it lives on (or of the root file system), and serves the holders' reads and writes from there (a worker's I/O on the old path while the path is gone). No file data is held in memory for it. The file is deleted when the last holder closes it or exits, and the directory goes as soon as it is empty. A file created at the path afterwards is the new file for everyone, also for the holder; a holder whose file another process renames (rather than removes) still uses the old path. A rename over a held file stays atomic for everyone else, as `rename(2)` is: moving the held file aside and the new one into its place are two steps, so a lookup of that name (`open`, `stat`, a read) that comes while they run waits for both, and one that started before them and missed the name in between looks again (also when it spelled the name through a symlinked parent, which the wait doesn't match by path: a miss is retried whenever any replacement finished meanwhile, up to three tries). Every open finds the old file or the new one, never no file (#903). The OPFS root does the same for its own two steps: a rename onto an existing name replaces the file with `move()` (or, in a browser without it, a copy into the name and a removal of the source), and a lookup that loses that race to a stale handle looks again. A `tmpfs` mount replaces the name in one step; on `fsa` and `hostfs` mounts it is the real file system's `rename`. Listings in the kernel leave `.slicc-unlinked` out, but on an `fsa` or `hostfs` mount it is visible on the real machine while a removed file is held (add `.slicc-unlinked/` to a `.gitignore` there). Each kernel names its entries after itself and holds a Web Lock for that name while it runs; when a mount is set up (and the root at start), the kernel removes the entries whose owner holds no lock, so a tab that died while holding a file leaves nothing behind for long, and entries of other live tabs on the same origin are left alone. Under the Node entry, which has no Web Locks, entries are named after the kernel's process id instead, and an entry is removed when that process is gone (`kill(pid, 0)` fails with `ESRCH`); a reused pid leaves the entry alone. Signals a program ignores stay ignored in the image it execs and in the children it spawns or forks, as POSIX has it, so `nohup cmd` and `trap '' TERM; exec cmd` keep `cmd` safe from them; caught signals go back to their default. The kernel holds what was handed down, so a new image that sets one of those signals back to the default with `signal(SIG_DFL)` still ignores it, until it installs a handler.
738
+ An exec'd program takes over the pid of the process it replaces, as on Linux: `getpid()` and `$$` in the new image, `/proc`, `ps`, `kill` and the parent's `waitpid` all agree on that pid, and the image's parent pid is the replaced process's. Its own children see that pid as their parent. A file removed or renamed over while programs hold it open keeps its bytes for them, as on Linux, whether the descriptors live in the kernel or a program opened the file in its own worker: every open, read-only ones included, is known to the kernel, and all holders share the one file (writes through one are seen through the others). The kernel does not copy it: as NFS does with its "silly rename", it moves the file into a hidden directory, `.slicc-unlinked/`, at the root of the mount it lives on (or of the root file system), and serves the holders' reads and writes from there (a worker's I/O on the old path while the path is gone). No file data is held in memory for it. The file is deleted when the last holder closes it or exits, and the directory goes as soon as it is empty. A file created at the path afterwards is the new file for everyone, also for the holder; a holder whose file another process renames (rather than removes) still uses the old path. A rename over a held file stays atomic for everyone else, as `rename(2)` is: moving the held file aside and the new one into its place are two steps, so a lookup of that name (`open`, `stat`, a read) that comes while they run waits for both, and one that started before them and missed the name in between looks again (also when it spelled the name through a symlinked parent, which the wait doesn't match by path: a miss is retried whenever any replacement finished meanwhile, up to three tries). Every open finds the old file or the new one, never no file (#903). A read through a held descriptor and the rename take the file's turn in order: lookups start to wait for the rename only once it has its turn on every held file it moves aside, so a read that came first finishes, and a rename over a file other processes keep reading never stalls them or itself (#980). A process that opens the file while the rename waits for its turn is a holder too: the rename looks for holders again once it has its turn, so that process keeps the old file as well, and a holder that closed meanwhile leaves nothing to move aside. The OPFS root does the same for its own two steps: a rename onto an existing name replaces the file with `move()` (or, in a browser without it, a copy into the name and a removal of the source), and a lookup that loses that race to a stale handle looks again. A `tmpfs` mount replaces the name in one step; on `fsa` and `hostfs` mounts it is the real file system's `rename`. Listings in the kernel leave `.slicc-unlinked` out, but on an `fsa` or `hostfs` mount it is visible on the real machine while a removed file is held (add `.slicc-unlinked/` to a `.gitignore` there). Each kernel names its entries after itself and holds a Web Lock for that name while it runs; when a mount is set up (and the root at start), the kernel removes the entries whose owner holds no lock, so a tab that died while holding a file leaves nothing behind for long, and entries of other live tabs on the same origin are left alone. Under the Node entry, which has no Web Locks, entries are named after the kernel's process id instead, and an entry is removed when that process is gone (`kill(pid, 0)` fails with `ESRCH`); a reused pid leaves the entry alone. Signals a program ignores stay ignored in the image it execs and in the children it spawns or forks, as POSIX has it, so `nohup cmd` and `trap '' TERM; exec cmd` keep `cmd` safe from them; caught signals go back to their default. The kernel holds what was handed down, so a new image that sets one of those signals back to the default with `signal(SIG_DFL)` still ignores it, until it installs a handler.
739
739
 
740
740
  - **Emscripten:** `Module.sliccKernel.execve(file, argv, env, cwd)` runs `file` as this process's new image on its fds 0, 1 and 2, waits for it, and returns its wait status, or a negative errno when it cannot start. `env` and `cwd` may be `null` for this process's own. An exec shim detects it with `typeof Module.sliccKernel.execve === 'function'` and exits with the status it returns, as `execve` never returns on success. Older shims spawn the program and then call `Module.sliccKernel.execWait(pid)`; that works the same way: a new program asks the kernel for its pid before `main`, and the kernel answers once its parent has made its next call, so an image an older shim execs also reports the pid it replaced (the wait starts once the program is loaded; should the parent make no call within 100 ms after that, the kernel answers anyway, and the image may then report its own pid; `launcher.identities` counts the programs that asked and those answered by that timeout).
741
741
  - **WASIX:** `proc_exec` does this by itself.
@@ -942,11 +942,14 @@ The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#us
942
942
  ### How many processes at once
943
943
 
944
944
  In a browser every process worker of the page shares one renderer. V8 reserves about 8 GiB of address space for each 32-bit wasm memory, whatever its declared maximum, and its sandbox holds about 1 TiB. So a page runs about 125 wasm processes (or programs that own a wasm memory) at once, counting everything in the renderer (#970). Node has no such ceiling, and neither does Linux. The kernel keeps processes inside that ceiling:
945
- - **The ceiling:** 112 by default in a browser and none in Node. It's the kernel's count of live wasm processes, with the kernel's other wasm memories in the page left outside it. When an instantiation runs out of memory anyway, the kernel learns the real ceiling at that moment (the live count minus one), and that learned value wins over the default.
945
+ - **The ceiling:** 112 by default in a browser and none in Node. It's the kernel's count of live wasm processes, with the kernel's other wasm memories in the page left outside it. When an instantiation runs out of memory anyway, the kernel learns the real ceiling at that moment (the live count minus one), and that learned value wins over the default. A learned ceiling is never below 16, since an OOM at a low count means something outside it (another tab in the same renderer) filled the renderer. It's raised by 8 for each minute without another OOM, back to the default.
946
946
  - **Admission:** a spawn, fork, exec or `kernel.run` at the ceiling waits for another process to exit. It keeps waiting as long as processes exit or are admitted, and fails with `EAGAIN` only after 10 s with neither. A long `make -j` or `xargs -P` therefore queues, and a pipeline that needs more live processes than the ceiling fails cleanly. bash's own retry on a fork's `EAGAIN` (`fork: retry: Resource temporarily unavailable`) handles the rest.
947
947
  - **Close to the ceiling** (within 16 of it), the kernel waits for each new process to finish instantiating before the spawn returns. If it ran out of memory, the kernel starts it again, quietly, once there's room, so the caller sees no failure.
948
948
  - **The rare residual case:** a process that fails to instantiate with nobody waiting on it, which happens only when something outside the count fills the renderer. It ends with status 126 and `<program>: cannot allocate memory for a new process (too many processes at once)` on its stderr, never a JS `RangeError`.
949
- - **`/proc/sys/kernel/slicc_wasm_memories`** reads `<live> <ceiling>` (the ceiling 0 for none).
949
+ - **Below a learned ceiling,** a process that still runs out of memory is started again once another process exits, and the spawn fails with `EAGAIN` after 10 s with none.
950
+ - **`/proc/sys/kernel/slicc_wasm_memories`** reads `<live> <ceiling> <source>`. The source is `default`, `learned`, or `none` (with the ceiling 0).
951
+
952
+ `/proc/sys/kernel/slicc_shared_root` reads `1` while another kernel shares the root (a tab, or a second kernel in the page) and `0` otherwise. A peer's tab that closes is noticed when this kernel next writes to a shared file, at most every 30 s, so the value can stay `1` for a while after a peer has gone.
950
953
 
951
954
  ### Metadata
952
955
 
@@ -6565,7 +6565,7 @@ function satisfies(version, range2) {
6565
6565
  }
6566
6566
 
6567
6567
  // src/version.ts
6568
- var KERNEL_VERSION = true ? "1.124.0" : "0.0.0-dev";
6568
+ var KERNEL_VERSION = true ? "1.124.2" : "0.0.0-dev";
6569
6569
  var OS_RELEASE = "6.0.0-slicc";
6570
6570
  function procVersion() {
6571
6571
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -7468,18 +7468,19 @@ var UnlinkedKeeper = class {
7468
7468
  const keeper = this;
7469
7469
  const { replacing } = this;
7470
7470
  const replace = async (target, op) => {
7471
- const before = replacing.get(target);
7472
7471
  const { promise, resolve: resolve2 } = Promise.withResolvers();
7473
- const mine = {
7474
- done: Promise.all([before?.done, promise]).then(() => void 0)
7472
+ let mine;
7473
+ const gated = () => {
7474
+ const before = replacing.get(target);
7475
+ mine = { done: Promise.all([before?.done, promise]).then(() => void 0) };
7476
+ replacing.set(target, mine);
7475
7477
  };
7476
- replacing.set(target, mine);
7477
7478
  try {
7478
- await op();
7479
+ await op(gated);
7479
7480
  } finally {
7480
7481
  keeper.replaced += 1;
7481
7482
  resolve2();
7482
- if (replacing.get(target) === mine) replacing.delete(target);
7483
+ if (mine && replacing.get(target) === mine) replacing.delete(target);
7483
7484
  }
7484
7485
  };
7485
7486
  const unreplaced = async (path, op) => {
@@ -7524,8 +7525,8 @@ var UnlinkedKeeper = class {
7524
7525
  async rename(from, to) {
7525
7526
  const target = fs.resolvePath("/", to);
7526
7527
  if (target === fs.resolvePath("/", from)) return fs.rename(from, to);
7527
- await replace(target, async () => {
7528
- const pinned = await keeper.pin(target);
7528
+ await replace(target, async (gated) => {
7529
+ const pinned = await keeper.pin(target, gated);
7529
7530
  await keeper.settle(pinned, () => fs.rename(from, to));
7530
7531
  });
7531
7532
  },
@@ -7542,7 +7543,7 @@ var UnlinkedKeeper = class {
7542
7543
  }
7543
7544
  pinned.commit();
7544
7545
  }
7545
- async pin(target) {
7546
+ heldWithin(target) {
7546
7547
  const held = /* @__PURE__ */ new Map();
7547
7548
  for (const holder of this.holders) {
7548
7549
  if (holder.owns(target)) continue;
@@ -7551,11 +7552,19 @@ var UnlinkedKeeper = class {
7551
7552
  held.set(path, [...held.get(path) ?? [], holder]);
7552
7553
  }
7553
7554
  }
7554
- const paths = [...held.keys()].sort();
7555
+ return held;
7556
+ }
7557
+ async pin(target, gated = () => void 0) {
7558
+ const paths = [...this.heldWithin(target).keys()].sort();
7555
7559
  const releases = [];
7556
7560
  for (const path of paths) releases.push(await this.gate.acquire(path));
7561
+ gated();
7562
+ const now = this.heldWithin(target);
7563
+ const held = new Map(
7564
+ paths.flatMap((path) => now.has(path) ? [[path, now.get(path)]] : [])
7565
+ );
7557
7566
  const hidden = /* @__PURE__ */ new Map();
7558
- for (const path of paths) {
7567
+ for (const path of held.keys()) {
7559
7568
  const at = await this.hide(path);
7560
7569
  if (at !== void 0) hidden.set(path, at);
7561
7570
  }
@@ -12712,7 +12721,9 @@ var SYS_TREE = {
12712
12721
  osrelease: `${OS_RELEASE}
12713
12722
  `,
12714
12723
  ostype: "Linux\n",
12715
- slicc_wasm_memories: (listing) => `${listing.wasm?.live ?? 0} ${listing.wasm?.ceiling ?? 0}
12724
+ slicc_wasm_memories: (listing) => `${listing.wasm?.live ?? 0} ${listing.wasm?.ceiling ?? 0} ${listing.wasm?.source ?? "none"}
12725
+ `,
12726
+ slicc_shared_root: (listing) => `${listing.peers ?? 0}
12716
12727
  `
12717
12728
  }
12718
12729
  };
@@ -21917,37 +21928,65 @@ var SettlingChildren = class {
21917
21928
 
21918
21929
  // src/kernel/wasm-memories.ts
21919
21930
  var DEFAULT_CEILING = 112;
21931
+ var CEILING_FLOOR = 16;
21920
21932
  var IDLE_MS = 1e4;
21921
21933
  var MARGIN = 16;
21934
+ var DECAY_MS = 6e4;
21935
+ var DECAY_STEP = 8;
21922
21936
  var WasmMemories = class {
21923
21937
  live = 0;
21924
- ceiling;
21938
+ learned;
21925
21939
  waiters = /* @__PURE__ */ new Set();
21940
+ base;
21926
21941
  idleMs;
21927
- constructor(ceiling, idleMs = IDLE_MS) {
21928
- this.ceiling = ceiling;
21942
+ now;
21943
+ constructor(ceiling, idleMs = IDLE_MS, now = Date.now) {
21944
+ this.base = ceiling;
21929
21945
  this.idleMs = idleMs;
21946
+ this.now = now;
21947
+ }
21948
+ ceiling() {
21949
+ const learned = this.learned;
21950
+ if (!learned) return this.base;
21951
+ const steps = Math.floor((this.now() - learned.at) / DECAY_MS);
21952
+ const raised = learned.ceiling + steps * DECAY_STEP;
21953
+ if (raised < (this.base ?? DEFAULT_CEILING)) return raised;
21954
+ this.learned = void 0;
21955
+ return this.base;
21930
21956
  }
21931
21957
  count() {
21932
- return { live: this.live, ceiling: this.ceiling ?? 0 };
21958
+ const ceiling = this.ceiling();
21959
+ const source = this.learned ? "learned" : ceiling === void 0 ? "none" : "default";
21960
+ return { live: this.live, ceiling: ceiling ?? 0, source };
21933
21961
  }
21934
21962
  ended() {
21935
21963
  this.live = Math.max(0, this.live - 1);
21936
21964
  this.progress();
21937
21965
  }
21938
21966
  learn() {
21939
- this.ceiling = Math.max(1, this.live - 1);
21967
+ const top = this.base ?? DEFAULT_CEILING;
21968
+ this.learned = {
21969
+ ceiling: Math.min(top, Math.max(CEILING_FLOOR, this.live - 1)),
21970
+ at: this.now()
21971
+ };
21940
21972
  }
21941
21973
  near() {
21942
- return this.ceiling !== void 0 && this.live + MARGIN >= this.ceiling;
21974
+ const ceiling = this.ceiling();
21975
+ return ceiling !== void 0 && this.live + MARGIN >= ceiling;
21943
21976
  }
21944
21977
  async admit() {
21945
- while (this.ceiling !== void 0 && this.live >= this.ceiling) {
21978
+ for (let ceiling = this.ceiling(); ceiling !== void 0 && this.live >= ceiling; ) {
21946
21979
  if (!await this.changed()) throw new SpawnError("EAGAIN");
21980
+ ceiling = this.ceiling();
21947
21981
  }
21948
21982
  this.live += 1;
21949
21983
  this.progress();
21950
21984
  }
21985
+ async refused() {
21986
+ const ceiling = this.ceiling();
21987
+ if (ceiling === void 0 || this.live >= ceiling) return;
21988
+ if (!await this.changed()) throw new SpawnError("EAGAIN");
21989
+ }
21951
21990
  progress() {
21952
21991
  const waiters = [...this.waiters];
21953
21992
  this.waiters.clear();
@@ -25208,6 +25247,10 @@ var Launcher = class {
25208
25247
  return handle;
25209
25248
  }
25210
25249
  await handle.exited;
25250
+ await this.memories.refused().catch(async (err) => {
25251
+ await fds.closeAll();
25252
+ throw err;
25253
+ });
25211
25254
  }
25212
25255
  }
25213
25256
  start(req) {
@@ -25273,7 +25316,8 @@ var Launcher = class {
25273
25316
  processes: this.list(this.credOf(pid)),
25274
25317
  load: this.loadAverage(),
25275
25318
  cpu: this.cpuTotal(),
25276
- wasm: this.memories.count()
25319
+ wasm: this.memories.count(),
25320
+ peers: Number(this.locks.shared?.() === true)
25277
25321
  }),
25278
25322
  sudo: (call, signal) => this.sudo(pid, call, signal),
25279
25323
  crontab: (call) => this.crontab(pid, call),