@amritk/nish-x86_64-linux 0.29.0 → 0.31.0
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/INSTALL.md +3 -3
- package/bin/nish +0 -0
- package/docs/AI.md +71 -4
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -0
- package/runtime/core/host.mjs +9 -0
- package/runtime/nish.d.ts +48 -2
- package/runtime/nish.h +31 -3
- package/runtime/nish.mjs +36 -0
- package/runtime/runtime-net.c +32 -3
- package/runtime/runtime-os.c +33 -1
- package/runtime/runtime-region.c +243 -0
- package/runtime/runtime.c +19 -2
- package/scripts/build.sh +19 -6
- package/std/README.md +10 -7
- package/std/net/{http-client.ts → fetch.ts} +17 -17
- package/std/net/handler.ts +1 -1
- package/std/net/{http1-client.ts → http1-client-codec.ts} +276 -47
- package/std/net/{http1.ts → http1-codec.ts} +252 -49
- package/std/net/http1-server.ts +72 -13
- package/std/net/outbound.ts +2 -2
- package/std/net/postgres-carrier.ts +985 -0
- package/std/net/postgres.ts +680 -53
- package/std/net/prefork.ts +149 -13
- package/std/net/router.ts +1 -1
- package/std/net/upstream.ts +759 -0
- package/std/net/websocket.ts +5 -5
- package/std/region.ts +95 -0
package/INSTALL.md
CHANGED
|
@@ -256,7 +256,7 @@ routes are
|
|
|
256
256
|
runtime/runtime-cross.c runtime/runtime-host.c runtime/runtime-net.c \
|
|
257
257
|
runtime/runtime-simd.c runtime/runtime-aes.c runtime/runtime-sha256.c runtime/runtime-term.c \
|
|
258
258
|
runtime/runtime-bytes.c runtime/runtime-checksum.c runtime/runtime-sandbox.c \
|
|
259
|
-
runtime/runtime-shm.c -lm -o app
|
|
259
|
+
runtime/runtime-shm.c runtime/runtime-region.c -lm -o app
|
|
260
260
|
```
|
|
261
261
|
|
|
262
262
|
`--link`ing inside the container is the mistake to avoid: it shells out to the
|
|
@@ -392,7 +392,7 @@ it into `build/seed/`; `NISH_BOOTSTRAP=<path>` names one you already have and
|
|
|
392
392
|
skips the download. §2a has what the build does with it.
|
|
393
393
|
|
|
394
394
|
The published package ships `bin/` (the `nish` command, which is a launcher),
|
|
395
|
-
`runtime/` (the C runtime —
|
|
395
|
+
`runtime/` (the C runtime — fifteen translation units, the wasm subset and their
|
|
396
396
|
header — and `runtime/sqlite/`, SQLite's vendored source),
|
|
397
397
|
`scripts/build.sh` (the link pipeline), `std/` (the standard library) and
|
|
398
398
|
`docs/AI.md` (the rules card). The
|
|
@@ -405,7 +405,7 @@ working directory — and the binary runs from inside its own platform package
|
|
|
405
405
|
for exactly that reason.
|
|
406
406
|
|
|
407
407
|
Building the IR yourself rather than through `--link` means naming the runtime
|
|
408
|
-
on the `clang` line, and it is
|
|
408
|
+
on the `clang` line, and it is fifteen files:
|
|
409
409
|
|
|
410
410
|
```bash
|
|
411
411
|
clang app.ll runtime/runtime.c runtime/runtime-os.c runtime/runtime-parallel.c \
|
package/bin/nish
CHANGED
|
Binary file
|
package/docs/AI.md
CHANGED
|
@@ -2222,6 +2222,13 @@ system call either way. `setIdlePriority()` moves the program to Linux's `SCHED_
|
|
|
2222
2222
|
`main`, never in a task. An unprivileged process can never leave `SCHED_IDLE`
|
|
2223
2223
|
and an idle one can starve on a saturated machine, so keep a normal-priority
|
|
2224
2224
|
fallback. Neither exists on wasm; both throw under Node.
|
|
2225
|
+
`setCpuAffinity(cpu)` pins the process to one CPU, narrow-only: a CPU outside
|
|
2226
|
+
the mask it already has is `-22`; `-95` on Darwin and under Node, refused on
|
|
2227
|
+
wasm. `nish/net/prefork`'s `PreforkConfig` takes `pin = true` to pin worker
|
|
2228
|
+
`k` to CPU `k` (more workers than CPUs panics), and `listenPort` (with
|
|
2229
|
+
`listenHost`, `listenBacklog`, `listenFlags` 0 or 2) to have the supervisor
|
|
2230
|
+
bind one `SO_REUSEPORT` listener per worker, steered on Linux by the receiving
|
|
2231
|
+
CPU when pinned; a worker takes its own with `preforkListener(process.argv)`.
|
|
2225
2232
|
|
|
2226
2233
|
**Sockets** (`nish:net`, or the same names as globals). `tcpListen(host, port,
|
|
2227
2234
|
backlog)`, `tcpAccept(fd, peer)`, `netRead(fd, buf, off, len)`,
|
|
@@ -2236,10 +2243,10 @@ wait for it to be writable, then `connectResult(fd)` is 0 or the failure. Buffer
|
|
|
2236
2243
|
are `u8[]` only; an address is 18 bytes of one (IPv4 as `::ffff:a.b.c.d`, then
|
|
2237
2244
|
the port big-endian); hosts are numeric literals; to reach a name, resolve it first with
|
|
2238
2245
|
`nish/net/dns` (`DnsClient` on your loop, or the blocking `resolveHost`).
|
|
2239
|
-
To fetch a URL from a CLI, skip all of this: `nish/net/
|
|
2240
|
-
`
|
|
2246
|
+
To fetch a URL from a CLI, skip all of this: `nish/net/fetch`'s
|
|
2247
|
+
`fetchSync(new HttpRequest("GET", url), new HttpOptions())` blocks until the
|
|
2241
2248
|
response's head is in and answers it with `body.readAll(limit)`, or null on a
|
|
2242
|
-
failure (
|
|
2249
|
+
failure (a `FetchSyncClient` keeps the connection and says why in `error`).
|
|
2243
2250
|
`netRead` / `netWrite` panic on a range outside the buffer.
|
|
2244
2251
|
`tcpListenFlags(host, port, backlog, flags)` is `tcpListen` with flag 1
|
|
2245
2252
|
`SO_REUSEPORT`, so several listeners, in one process or many, share a port (Linux
|
|
@@ -2596,6 +2603,46 @@ returns. To give memory back sooner, scope it with `using a = arena()`;
|
|
|
2596
2603
|
`Arena.release` and `Arena.reset` are gone, and a call to either is an unknown
|
|
2597
2604
|
builtin (NL2244).
|
|
2598
2605
|
|
|
2606
|
+
### Regions: `nish:region`
|
|
2607
|
+
|
|
2608
|
+
**For memory a long-running program frees itself — per connection, per cache
|
|
2609
|
+
entry — use a region.** `region(f)` / `regionWith(arg, f)` make one and run
|
|
2610
|
+
`f` in it, and what `f` answers is the root; `enter(r, f)` /
|
|
2611
|
+
`enterWith(r, arg, f)` run `f(root)` / `f(root, arg)` in it again; `free(r)`
|
|
2612
|
+
gives all of it back. `f` is a named function or an arrow that captures
|
|
2613
|
+
nothing. `Region<T>` is a number, not a pointer.
|
|
2614
|
+
|
|
2615
|
+
```ts nish:ok
|
|
2616
|
+
import { Region, enter, free, region } from "nish:region";
|
|
2617
|
+
|
|
2618
|
+
class Session {
|
|
2619
|
+
hits: i32 = 0;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
const open = (): Session => new Session();
|
|
2623
|
+
const touch = (s: Session): i32 => {
|
|
2624
|
+
s.hits = s.hits + 1;
|
|
2625
|
+
return s.hits;
|
|
2626
|
+
};
|
|
2627
|
+
|
|
2628
|
+
export const main = (): i32 => {
|
|
2629
|
+
const r: Region<Session> = region(open);
|
|
2630
|
+
console.log(enter(r, touch));
|
|
2631
|
+
free(r);
|
|
2632
|
+
return 0;
|
|
2633
|
+
};
|
|
2634
|
+
```
|
|
2635
|
+
|
|
2636
|
+
The checker refuses every way region memory could outlive `free`, and every way
|
|
2637
|
+
outer memory could be pointed at from inside: only numbers, booleans, enums and
|
|
2638
|
+
`Region` handles cross. `enter` answers a scalar or a handle (NL2611), `f` may
|
|
2639
|
+
not store region memory into `arg` or other outer memory (NL2609) nor outer
|
|
2640
|
+
memory into the region (NL2610), nor `push`/`set`/`add` to an outer collection
|
|
2641
|
+
(NL2615). A handle stays on its thread (NL2612, NL2613, NL2351), is never a
|
|
2642
|
+
`using` (NL2387) and is never `new Region` (NL2618). A bad handle, a double
|
|
2643
|
+
`free` or a nested `enter` of one region stops the program. `--emit-ts` refuses
|
|
2644
|
+
the module (NL4015).
|
|
2645
|
+
|
|
2599
2646
|
## Recipes for what is missing
|
|
2600
2647
|
|
|
2601
2648
|
**Iterate and transform** — there is no `map`, and no function to pass it:
|
|
@@ -2705,7 +2752,12 @@ parameter. `paramIntArray(values, start, count)` binds an `i64[]` window;
|
|
|
2705
2752
|
element, then `arrayEnd()`, builds a `text[]` straight into the Bind from
|
|
2706
2753
|
windows of a buffer. An element refused for room answers `false` and leaves
|
|
2707
2754
|
the array open: end it and send the rest in the next statement. Every other
|
|
2708
|
-
`param...` call and `bindEnd()` panic until `arrayEnd()`.
|
|
2755
|
+
`param...` call and `bindEnd()` panic until `arrayEnd()`. For a long
|
|
2756
|
+
`int4[]` or `int8[]`, `paramInt4Array` / `paramInt8Array` (same window) or
|
|
2757
|
+
`binaryArrayStart(23 or 20)`, `binaryArrayInt` / `binaryArrayNull`,
|
|
2758
|
+
`arrayEnd()` send it in binary, at a tenth of the client CPU; the element type
|
|
2759
|
+
must be the statement's (`int4[]` to an `int8[]` parameter is SQLSTATE
|
|
2760
|
+
`42804`), and an `int4` element out of range panics. (No block, as for
|
|
2709
2761
|
`nish/text`; `tests/postgres/live` runs it against a server, and the user
|
|
2710
2762
|
guide's [PostgreSQL page](guide/using-nish/postgres.md#array-parameters) its
|
|
2711
2763
|
walk-through.)
|
|
@@ -2743,6 +2795,21 @@ request. It reads no clock: when to take is the loop's choice, or a `PgBatchCloc
|
|
|
2743
2795
|
guide's [PostgreSQL page](guide/using-nish/postgres.md#batching-keyed-reads)
|
|
2744
2796
|
its walk-through.)
|
|
2745
2797
|
|
|
2798
|
+
**Call another service from a route handler** — there is no `await fetch()`
|
|
2799
|
+
in a handler and no event-loop callback: `nish/net/upstream`'s `Upstream(h,
|
|
2800
|
+
origin, limits)` is a pool of keep-alive connections to one `http:` origin on
|
|
2801
|
+
the `HttpHandler`'s own loop. `users.get(req, path)` returns at once — test it
|
|
2802
|
+
for `UPSTREAM_E_FULL`, which holds nothing — and keeps `req` held; after each
|
|
2803
|
+
`h.wait(ms)`, `while (users.ready())` `take()`s every finished call, answers
|
|
2804
|
+
`r.req` (`r.ok`, `r.status` and `r.body[0 .. r.length)`, or `r.error`: the
|
|
2805
|
+
codec's `HTTP1_E_` refusal or an `UPSTREAM_E_` code such as `TIMEOUT`) and
|
|
2806
|
+
`release`s it once. The origin is looked up once at start-up; set
|
|
2807
|
+
`UpstreamLimits.outbound` when it comes from input. Each connection takes a
|
|
2808
|
+
handler watch id from `firstId` (8) up, so keep your own `h.watch` ids below
|
|
2809
|
+
it. (No block, as for `nish/text`; `tests/link/net_upstream` is its program,
|
|
2810
|
+
and the user guide's [calling-services page](guide/using-nish/calling-services.md)
|
|
2811
|
+
its walk-through.)
|
|
2812
|
+
|
|
2746
2813
|
**Draw to the whole terminal** (a game, a dashboard) — there is no curses and
|
|
2747
2814
|
no `process.stdout.on("resize")`: `runTerminal(options, state, update, render)`
|
|
2748
2815
|
from `nish/term-io` opens the terminal, probes what it can draw, steps
|
package/package.json
CHANGED
package/runtime/core/host.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export declare function readSignal(fd: i32): i32;
|
|
|
82
82
|
/** These two have no reading under Node: each throws, naming itself. */
|
|
83
83
|
export declare function setNice(n: i32): i32;
|
|
84
84
|
export declare function setIdlePriority(): i32;
|
|
85
|
+
/** -95: there is no affinity call under Node. */
|
|
86
|
+
export declare function setCpuAffinity(cpu: i32): i32;
|
|
85
87
|
/** What each `nish:net` function is under the core: a throw naming `name`. */
|
|
86
88
|
export declare function noNetReading(name: string): never;
|
|
87
89
|
/** What `readStdin`, `waitStdin`, `setRawMode` and `setTerminalRestore` are under the core: a throw naming `name`. */
|
package/runtime/core/host.mjs
CHANGED
|
@@ -335,6 +335,15 @@ export function setIdlePriority() {
|
|
|
335
335
|
return noSchedulingReading("setIdlePriority");
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* `setCpuAffinity(cpu)` answers -95, the native build's answer on a host with
|
|
340
|
+
* no affinity call: unlike the two above it has a defined "not supported"
|
|
341
|
+
* value, so a program that pins where it can runs unpinned here.
|
|
342
|
+
*/
|
|
343
|
+
export function setCpuAffinity() {
|
|
344
|
+
return -95;
|
|
345
|
+
}
|
|
346
|
+
|
|
338
347
|
/**
|
|
339
348
|
* `signalFd()` and `readSignal(fd)` have no faithful reading under Node, and
|
|
340
349
|
* these say so rather than answer something else. Node delivers a signal to
|
package/runtime/nish.d.ts
CHANGED
|
@@ -378,6 +378,14 @@ declare function setNice(n: i32): i32;
|
|
|
378
378
|
* normal-priority fallback for work that must finish. Throws under Node.
|
|
379
379
|
*/
|
|
380
380
|
declare function setIdlePriority(): i32;
|
|
381
|
+
/**
|
|
382
|
+
* Pin this process to CPU `cpu` alone (`sched_setaffinity`): 0, or a negative
|
|
383
|
+
* errno. Narrow-only: a CPU outside the mask the process already has (an
|
|
384
|
+
* operator's `taskset` or cpuset), a negative one, or one past the kernel's
|
|
385
|
+
* set size is -22 with no system call. -95 where the host has no affinity
|
|
386
|
+
* call (Darwin, and under Node).
|
|
387
|
+
*/
|
|
388
|
+
declare function setCpuAffinity(cpu: i32): i32;
|
|
381
389
|
/** One environment variable, or `null` when it is unset (an empty value is a set variable). */
|
|
382
390
|
declare function getenv(name: string): string | null;
|
|
383
391
|
/**
|
|
@@ -422,7 +430,13 @@ declare function netAddress(out: u8[], host: string, port: i32): i32;
|
|
|
422
430
|
declare function netLocalPort(fd: i32): i32;
|
|
423
431
|
/** A listening TCP socket with `SO_REUSEADDR`; `"::"` hears IPv4 and IPv6. */
|
|
424
432
|
declare function tcpListen(host: string, port: i32, backlog: i32): i32;
|
|
425
|
-
/**
|
|
433
|
+
/**
|
|
434
|
+
* `tcpListen`'s socket, with `SO_REUSEPORT` for flag 1 and `TCP_NODELAY` on
|
|
435
|
+
* each accepted socket for flag 2. Flag 4, with flag 1, steers by CPU: a new
|
|
436
|
+
* connection goes to the group's listener whose bind order is the index of the
|
|
437
|
+
* CPU that received it (Linux; -95 elsewhere). Any other bit, or 4 without 1,
|
|
438
|
+
* is -22.
|
|
439
|
+
*/
|
|
426
440
|
declare function tcpListenFlags(host: string, port: i32, backlog: i32, flags: i32): i32;
|
|
427
441
|
/** The next connection's descriptor, its address in `peer`; -11 when none waits. */
|
|
428
442
|
declare function tcpAccept(fd: i32, peer: u8[]): i32;
|
|
@@ -667,6 +681,8 @@ declare module "nish:process" {
|
|
|
667
681
|
* on a saturated machine, so a caller keeps a normal-priority fallback.
|
|
668
682
|
*/
|
|
669
683
|
export function setIdlePriority(): i32;
|
|
684
|
+
/** Pin this process to CPU `cpu` alone: 0, -22 for a CPU outside the current mask (narrow-only), or -95 off Linux. */
|
|
685
|
+
export function setCpuAffinity(cpu: i32): i32;
|
|
670
686
|
/**
|
|
671
687
|
* A monotonic clock in nanoseconds. The origin is arbitrary, so only the
|
|
672
688
|
* difference between two reads is meaningful.
|
|
@@ -693,7 +709,13 @@ declare module "nish:net" {
|
|
|
693
709
|
export function netLocalPort(fd: i32): i32;
|
|
694
710
|
/** A listening TCP socket with `SO_REUSEADDR`; `"::"` hears IPv4 and IPv6. */
|
|
695
711
|
export function tcpListen(host: string, port: i32, backlog: i32): i32;
|
|
696
|
-
/**
|
|
712
|
+
/**
|
|
713
|
+
* `tcpListen`'s socket, with `SO_REUSEPORT` for flag 1 and `TCP_NODELAY` on
|
|
714
|
+
* each accepted socket for flag 2. Flag 4, with flag 1, steers by CPU: a new
|
|
715
|
+
* connection goes to the group's listener whose bind order is the index of the
|
|
716
|
+
* CPU that received it (Linux; -95 elsewhere). Any other bit, or 4 without 1,
|
|
717
|
+
* is -22.
|
|
718
|
+
*/
|
|
697
719
|
export function tcpListenFlags(host: string, port: i32, backlog: i32, flags: i32): i32;
|
|
698
720
|
/** The next connection's descriptor, its address in `peer`; -11 when none waits. */
|
|
699
721
|
export function tcpAccept(fd: i32, peer: u8[]): i32;
|
|
@@ -936,6 +958,30 @@ declare module "nish:secret" {
|
|
|
936
958
|
export function wipe(target: object): void;
|
|
937
959
|
}
|
|
938
960
|
|
|
961
|
+
// `nish:region` has source behind it, `std/region.ts`, but a program imports it
|
|
962
|
+
// by this name. The class has a private constructor, so `tsc` refuses
|
|
963
|
+
// `new Region()` as `nish` does (NL2618). What `nish` refuses about the
|
|
964
|
+
// functions a region runs — keeping its memory outside it, growing outer
|
|
965
|
+
// storage, reaching threads — is flow, which `tsc` cannot see
|
|
966
|
+
// (docs/LANGUAGE.md -> Regions).
|
|
967
|
+
declare module "nish:region" {
|
|
968
|
+
/** A handle to a region whose root is a `T`: 64 bits, not a pointer. */
|
|
969
|
+
export class Region<T> {
|
|
970
|
+
private constructor();
|
|
971
|
+
private readonly root: T;
|
|
972
|
+
}
|
|
973
|
+
/** A new region whose root is what `f` answers; everything `f` allocates is in it. */
|
|
974
|
+
export function region<T>(f: () => T): Region<T>;
|
|
975
|
+
/** `region` with one outside argument for `f`, which `f` may read but not keep. */
|
|
976
|
+
export function regionWith<A, T>(arg: A, f: (arg: A) => T): Region<T>;
|
|
977
|
+
/** `f(root)`, allocating in the region. What `f` answers is a scalar or a handle. */
|
|
978
|
+
export function enter<T, R>(r: Region<T>, f: (root: T) => R): R;
|
|
979
|
+
/** `f(root, arg)`, under `enter`'s rules; `f` may not keep or grow `arg`. */
|
|
980
|
+
export function enterWith<T, A, R>(r: Region<T>, arg: A, f: (root: T, arg: A) => R): R;
|
|
981
|
+
/** Give the region's memory back. A freed handle used again stops the program. */
|
|
982
|
+
export function free<T>(r: Region<T>): void;
|
|
983
|
+
}
|
|
984
|
+
|
|
939
985
|
// ---- Arena (docs/LANGUAGE.md -> Arena) ---------------------------------------
|
|
940
986
|
|
|
941
987
|
declare const Arena: {
|
package/runtime/nish.h
CHANGED
|
@@ -104,8 +104,9 @@ extern NISH_TLS struct nish_arena nish_arena;
|
|
|
104
104
|
|
|
105
105
|
/* Bump allocation: 8-byte rounded and aligned, uninitialised. */
|
|
106
106
|
void *nish_alloc_struct(uint64_t size);
|
|
107
|
-
/* Slow path: push a new chunk (at least 64 KB
|
|
108
|
-
* the inlined fast path when the current chunk is
|
|
107
|
+
/* Slow path: push a new chunk (at least 64 KB, or a region's next size) and
|
|
108
|
+
* bump from it. Called by the inlined fast path when the current chunk is
|
|
109
|
+
* full; rarely by hosts. */
|
|
109
110
|
void *nish_arena_grow(uint64_t size);
|
|
110
111
|
/* Recycle everything in O(1): keeps the newest chunk, frees the rest. Every
|
|
111
112
|
* arena string and object becomes invalid. Call it between batches. */
|
|
@@ -132,6 +133,25 @@ void *nish_arena_keep(uint64_t mark, void *p);
|
|
|
132
133
|
/* Bytes bumped in the current chunk (`Arena.used()`); a steady-state loop keeps it flat. */
|
|
133
134
|
uint64_t nish_arena_used(void);
|
|
134
135
|
|
|
136
|
+
/* ---- Regions (runtime-region.c) -----------------------------------------
|
|
137
|
+
* Arenas made, entered, left and freed one at a time
|
|
138
|
+
* (docs/wp46-long-running-memory.md). A handle is a nonzero `uint64_t`: a
|
|
139
|
+
* 24-bit slot and its 40-bit generation. Every call below panics on a zero
|
|
140
|
+
* handle, a freed (stale) one and one made on another thread.
|
|
141
|
+
* `nish_region_new` makes an empty region, owned by the calling thread.
|
|
142
|
+
* `nish_region_enter` makes the region the thread's arena until the matching
|
|
143
|
+
* `nish_region_leave`, so every allocation in between lands in it, and
|
|
144
|
+
* answers the root `leave` last recorded (null at first); entering a region
|
|
145
|
+
* already entered panics. `nish_region_leave` must name the region entered
|
|
146
|
+
* last on this thread, puts back the arena its enter displaced and records
|
|
147
|
+
* `root`. `nish_region_free` frees every chunk of a region that is not
|
|
148
|
+
* entered and makes its handle stale. The wasm profile has none of these
|
|
149
|
+
* (scripts/build.sh refuses IR that names them); wasi has. */
|
|
150
|
+
uint64_t nish_region_new(void);
|
|
151
|
+
void *nish_region_enter(uint64_t r);
|
|
152
|
+
void nish_region_leave(uint64_t r, void *root);
|
|
153
|
+
void nish_region_free(uint64_t r);
|
|
154
|
+
|
|
135
155
|
/* ---- String operations ------------------------------------------------- */
|
|
136
156
|
/* Copy `len` bytes into a new arena string (`bytes` need not be terminated). */
|
|
137
157
|
nish_str *nish_str_new(const char *bytes, uint64_t len);
|
|
@@ -557,6 +577,11 @@ int32_t nish_set_nice(int32_t n);
|
|
|
557
577
|
* negative errno; -95 (Linux's EOPNOTSUPP) elsewhere, -ENOSYS under WASI. An
|
|
558
578
|
* unprivileged process cannot leave the policy again. */
|
|
559
579
|
int32_t nish_set_idle_priority(void);
|
|
580
|
+
/* `setCpuAffinity(cpu)`: this process pinned to CPU `cpu` alone
|
|
581
|
+
* (`sched_setaffinity`, Linux), 0 or a negative errno. Narrow-only: a CPU not
|
|
582
|
+
* in the current mask, negative, or past `CPU_SETSIZE` is -22 (-EINVAL) with
|
|
583
|
+
* no system call. -95 (Linux's EOPNOTSUPP) elsewhere, -ENOSYS under WASI. */
|
|
584
|
+
int32_t nish_set_cpu_affinity(int32_t cpu);
|
|
560
585
|
|
|
561
586
|
/* ---- The environment (WP19 R1) ------------------------------------------
|
|
562
587
|
* `getenv(name)`: the value of environment variable `name`, copied into the
|
|
@@ -659,7 +684,10 @@ bool nish_is_executable(const nish_str *path);
|
|
|
659
684
|
* `nish_tcp_listen(host, port, backlog)`: `SO_REUSEADDR`, and `::` hears
|
|
660
685
|
* both families, or IPv4 alone on a kernel without IPv6.
|
|
661
686
|
* `nish_tcp_listen_flags(host, port, backlog, flags)`: the same listener,
|
|
662
|
-
* with `SO_REUSEPORT` for flag 1
|
|
687
|
+
* with `SO_REUSEPORT` for flag 1, `TCP_NODELAY` on each accepted socket for
|
|
688
|
+
* flag 2, and, for flag 4 with flag 1, a reuseport program steering each new
|
|
689
|
+
* connection to the listener bound at the index of the CPU that received it
|
|
690
|
+
* (Linux; -95 elsewhere). Any other bit, or 4 without 1, is -22.
|
|
663
691
|
* `nish_tcp_accept(fd, peer)`: a new descriptor and the peer's address.
|
|
664
692
|
* `nish_net_read` / `nish_net_write(fd, buf, off, len)`: bytes `[off, off +
|
|
665
693
|
* len)` of `buf`, which the compiled call has range-checked; a write to a
|
package/runtime/nish.mjs
CHANGED
|
@@ -121,6 +121,9 @@ for (const name of ["setNice", "setIdlePriority"]) {
|
|
|
121
121
|
throw new Error(`\`${name}\` has no reading under Node: it changes how the kernel schedules the native program`);
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
+
// `setCpuAffinity` answers -95, as on Darwin: Node has no affinity call, and
|
|
125
|
+
// "not supported" is a value a caller already branches on.
|
|
126
|
+
provide("setCpuAffinity", shim.setCpuAffinity);
|
|
124
127
|
provide("getenv", shim.getenv);
|
|
125
128
|
provide("statMtimeSync", shim.statMtimeSync);
|
|
126
129
|
provide("signalFd", shim.signalFd);
|
|
@@ -328,11 +331,41 @@ provide("arena", shim.arena);
|
|
|
328
331
|
// zero-fills it. `nish:unsafe` and `nish:term` are each answered by a module
|
|
329
332
|
// that hands back the globals installed above: its functions are globals of this module either
|
|
330
333
|
// way, and the import is what the compiler requires a program to write.
|
|
334
|
+
// `nish:region` is answered by a module of its own, below.
|
|
331
335
|
// `nish:simd` is `runtime/core.mjs`, whose vectors are typed arrays made once
|
|
332
336
|
// and never written after, so `--emit-ts` output and a program run as it was
|
|
333
337
|
// written share one implementation.
|
|
334
338
|
const UNSAFE_EXPORTS = ["uncheckedGet", "uncheckedSet", "wrappingAdd", "wrappingSub", "wrappingMul"];
|
|
335
339
|
const unsafeSource = UNSAFE_EXPORTS.map((name) => `export const ${name} = globalThis.${name};`).join("\n");
|
|
340
|
+
// `nish:region` has no allocator to swap under Node: a region is an entry in a
|
|
341
|
+
// table, keyed by its handle (a BigInt, as every `i64` is here), `enter` runs
|
|
342
|
+
// its function on the root, and `free` drops the entry. The handle checks the
|
|
343
|
+
// native runtime makes are made here too, so a freed handle or a second
|
|
344
|
+
// `enter` throws where the native program stops.
|
|
345
|
+
const regionSource = `
|
|
346
|
+
const roots = new Map();
|
|
347
|
+
const entered = new Set();
|
|
348
|
+
let next = 0n;
|
|
349
|
+
const rootOf = (r) => {
|
|
350
|
+
if (!roots.has(r)) throw new Error("nish:region: the region is freed or was never made");
|
|
351
|
+
if (entered.has(r)) throw new Error("nish:region: the region is entered already");
|
|
352
|
+
return roots.get(r);
|
|
353
|
+
};
|
|
354
|
+
const run = (r, f, arg, withArg) => {
|
|
355
|
+
const root = rootOf(r);
|
|
356
|
+
entered.add(r);
|
|
357
|
+
try { return withArg ? f(root, arg) : f(root); } finally { entered.delete(r); }
|
|
358
|
+
};
|
|
359
|
+
export class Region {}
|
|
360
|
+
export const region = (f) => { next += 1n; roots.set(next, f()); return next; };
|
|
361
|
+
export const regionWith = (arg, f) => { next += 1n; roots.set(next, f(arg)); return next; };
|
|
362
|
+
export const enter = (r, f) => run(r, f, undefined, false);
|
|
363
|
+
export const enterWith = (r, arg, f) => run(r, f, arg, true);
|
|
364
|
+
export const free = (r) => {
|
|
365
|
+
if (entered.has(r)) throw new Error("nish:region: the region is freed while it is entered");
|
|
366
|
+
if (!roots.delete(r)) throw new Error("nish:region: the region is freed or was never made");
|
|
367
|
+
};
|
|
368
|
+
`;
|
|
336
369
|
const TERM_EXPORTS = ["readStdin", "waitStdin", "isTerminal", "terminalSize", "setRawMode", "setTerminalRestore"];
|
|
337
370
|
const termSource = TERM_EXPORTS.map((name) => `export const ${name} = globalThis.${name};`).join("\n");
|
|
338
371
|
registerHooks({
|
|
@@ -346,6 +379,9 @@ registerHooks({
|
|
|
346
379
|
if (specifier === "nish:simd") {
|
|
347
380
|
return next(new URL("./core.mjs", import.meta.url).href, context);
|
|
348
381
|
}
|
|
382
|
+
if (specifier === "nish:region") {
|
|
383
|
+
return { url: `data:text/javascript,${encodeURIComponent(regionSource)}`, shortCircuit: true };
|
|
384
|
+
}
|
|
349
385
|
if (specifier === "nish:term") {
|
|
350
386
|
return { url: `data:text/javascript,${encodeURIComponent(termSource)}`, shortCircuit: true };
|
|
351
387
|
}
|
package/runtime/runtime-net.c
CHANGED
|
@@ -73,6 +73,7 @@ typedef int nish_net_unused;
|
|
|
73
73
|
#include <sys/un.h>
|
|
74
74
|
#include <unistd.h>
|
|
75
75
|
#if defined(__linux__)
|
|
76
|
+
#include <linux/filter.h>
|
|
76
77
|
#include <netinet/udp.h>
|
|
77
78
|
#include <sys/epoll.h>
|
|
78
79
|
/* glibc before 2.29 names neither; the numbers are the kernel's ABI. */
|
|
@@ -82,6 +83,10 @@ typedef int nish_net_unused;
|
|
|
82
83
|
#ifndef UDP_GRO
|
|
83
84
|
#define UDP_GRO 104
|
|
84
85
|
#endif
|
|
86
|
+
/* Linux 4.5's; a header older than the kernel may not name it. */
|
|
87
|
+
#ifndef SO_ATTACH_REUSEPORT_CBPF
|
|
88
|
+
#define SO_ATTACH_REUSEPORT_CBPF 51
|
|
89
|
+
#endif
|
|
85
90
|
#else
|
|
86
91
|
#include <sys/event.h>
|
|
87
92
|
#include <sys/time.h>
|
|
@@ -243,7 +248,23 @@ static int32_t nish_net_bound(const nish_str *host, int32_t port, int type, int3
|
|
|
243
248
|
}
|
|
244
249
|
nish_sockaddr s;
|
|
245
250
|
socklen_t n = nish_net_sockaddr(&s, a, port, v4);
|
|
246
|
-
|
|
251
|
+
#if defined(__linux__)
|
|
252
|
+
/* `tcpListenFlags` flag 4's program, two instructions, the kernel
|
|
253
|
+
selftest's: load the receiving CPU, return it. It is the reuseport
|
|
254
|
+
group's, so attaching it from every listener installs the same program
|
|
255
|
+
each time, and it goes on after `listen`, once the socket is in its
|
|
256
|
+
group. Only a stream listener's `opts` can hold 4. */
|
|
257
|
+
static struct sock_filter cpu[] = {
|
|
258
|
+
{BPF_LD | BPF_W | BPF_ABS, 0, 0, (uint32_t)(SKF_AD_OFF + SKF_AD_CPU)},
|
|
259
|
+
{BPF_RET | BPF_A, 0, 0, 0},
|
|
260
|
+
};
|
|
261
|
+
static const struct sock_fprog steer = {2, cpu};
|
|
262
|
+
#endif
|
|
263
|
+
if (bad != 0 || bind(fd, &s.sa, n) != 0 || (type == SOCK_STREAM && listen(fd, arg) != 0)
|
|
264
|
+
#if defined(__linux__)
|
|
265
|
+
|| ((opts & 4) && setsockopt(fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF, &steer, sizeof steer) != 0)
|
|
266
|
+
#endif
|
|
267
|
+
) {
|
|
247
268
|
int e = errno;
|
|
248
269
|
close(fd);
|
|
249
270
|
return nish_net_err(e);
|
|
@@ -262,9 +283,17 @@ int32_t nish_tcp_listen(const nish_str *host, int32_t port, int32_t backlog) {
|
|
|
262
283
|
`SO_REUSEPORT` for flag 1, so several listeners of one uid share the port
|
|
263
284
|
(Linux spreads new connections across them; Darwin hands them all to one),
|
|
264
285
|
and `TCP_NODELAY` for flag 2, which the listener carries so that
|
|
265
|
-
`tcpAccept` sets it on each connection it takes.
|
|
286
|
+
`tcpAccept` sets it on each connection it takes. Flag 4, which needs flag
|
|
287
|
+
1, steers by CPU: a reuseport program (`SO_ATTACH_REUSEPORT_CBPF`) hands a
|
|
288
|
+
new connection to the group's listener whose index is the CPU that received
|
|
289
|
+
it, the index being the order the group's sockets were bound in, and the
|
|
290
|
+
kernel falls back to its hash when there is no such listener. Linux alone
|
|
291
|
+
has it; elsewhere flag 4 is -95. Any other bit, or 4 without 1, is -22. */
|
|
266
292
|
int32_t nish_tcp_listen_flags(const nish_str *host, int32_t port, int32_t backlog, int32_t flags) {
|
|
267
|
-
if ((uint32_t)flags >
|
|
293
|
+
if ((uint32_t)flags > 7 || (flags & 5) == 4) return -22;
|
|
294
|
+
#if !defined(__linux__)
|
|
295
|
+
if (flags & 4) return -95;
|
|
296
|
+
#endif
|
|
268
297
|
return nish_net_bound(host, port, SOCK_STREAM, backlog, flags);
|
|
269
298
|
}
|
|
270
299
|
|
package/runtime/runtime-os.c
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* from drifting from the contract it is published under.
|
|
27
27
|
*/
|
|
28
28
|
#if defined(__linux__) && !defined(_GNU_SOURCE)
|
|
29
|
-
#define _GNU_SOURCE /* `sched_getaffinity` and `CPU_COUNT`, for
|
|
29
|
+
#define _GNU_SOURCE /* `sched_getaffinity`, `sched_setaffinity` and `CPU_COUNT`, for
|
|
30
|
+
`nish_cpu_online` and `nish_set_cpu_affinity` */
|
|
30
31
|
#endif
|
|
31
32
|
#define _POSIX_C_SOURCE 200809L
|
|
32
33
|
/* `realpath` is XSI rather than POSIX base, so _POSIX_C_SOURCE alone does not
|
|
@@ -860,6 +861,37 @@ int32_t nish_set_idle_priority(void) {
|
|
|
860
861
|
#endif
|
|
861
862
|
}
|
|
862
863
|
|
|
864
|
+
/* `setCpuAffinity(cpu)`: this process pinned to CPU `cpu` alone, 0 or a
|
|
865
|
+
negative errno. Narrow-only: `cpu` must be in the mask the process already
|
|
866
|
+
has (`sched_getaffinity`), so a pin never escapes an operator's `taskset` or
|
|
867
|
+
cpuset; a CPU outside it, a negative one, or one past `CPU_SETSIZE` is
|
|
868
|
+
-EINVAL with no `sched_setaffinity` call. Linux alone has the call; Darwin
|
|
869
|
+
answers -95 as `setIdlePriority` does, WASI -ENOSYS. Like the nice value,
|
|
870
|
+
the mask is the calling thread's and every thread started after it. */
|
|
871
|
+
int32_t nish_set_cpu_affinity(int32_t cpu) {
|
|
872
|
+
#if defined(__linux__)
|
|
873
|
+
/* The mask as words: the set to pin is the word holding `cpu` with only its
|
|
874
|
+
bit, the words below it zero, and the kernel zero-fills a shorter mask.
|
|
875
|
+
`sched_getaffinity` fails only on a bad pid or size, neither of which
|
|
876
|
+
this call can pass, so its failure shares the range check's answer. */
|
|
877
|
+
unsigned long m[CPU_SETSIZE / (8 * sizeof(unsigned long))];
|
|
878
|
+
uint32_t w = (uint32_t)cpu / (8 * sizeof m[0]);
|
|
879
|
+
unsigned long bit = 1UL << ((uint32_t)cpu % (8 * sizeof m[0]));
|
|
880
|
+
if ((uint32_t)cpu >= CPU_SETSIZE || sched_getaffinity(0, sizeof m, (cpu_set_t *)m) != 0 ||
|
|
881
|
+
!(m[w] & bit))
|
|
882
|
+
return -EINVAL;
|
|
883
|
+
for (uint32_t i = 0; i < w; i++) m[i] = 0;
|
|
884
|
+
m[w] = bit;
|
|
885
|
+
return sched_setaffinity(0, (w + 1) * sizeof m[0], (cpu_set_t *)m) == 0 ? 0 : -errno;
|
|
886
|
+
#elif defined(__wasi__)
|
|
887
|
+
(void)cpu;
|
|
888
|
+
return -ENOSYS;
|
|
889
|
+
#else
|
|
890
|
+
(void)cpu;
|
|
891
|
+
return -95;
|
|
892
|
+
#endif
|
|
893
|
+
}
|
|
894
|
+
|
|
863
895
|
/* `readdirSync(path)`: the listing a driver needs to discover its own inputs.
|
|
864
896
|
|
|
865
897
|
**Sorted by bytes**, which `readdir(3)` and Node are not, for two reasons
|