@amritk/nish-x86_64-linux 0.29.0 → 0.30.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 +15 -0
- package/package.json +1 -1
- package/runtime/nish.h +22 -2
- package/runtime/runtime-region.c +231 -0
- package/runtime/runtime.c +12 -2
- package/scripts/build.sh +19 -6
- package/std/README.md +2 -0
- package/std/net/http1-client.ts +270 -41
- package/std/net/http1-server.ts +68 -9
- package/std/net/http1.ts +250 -47
- package/std/net/postgres-carrier.ts +985 -0
- package/std/net/postgres.ts +297 -4
- package/std/net/upstream.ts +759 -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
|
@@ -2743,6 +2743,21 @@ request. It reads no clock: when to take is the loop's choice, or a `PgBatchCloc
|
|
|
2743
2743
|
guide's [PostgreSQL page](guide/using-nish/postgres.md#batching-keyed-reads)
|
|
2744
2744
|
its walk-through.)
|
|
2745
2745
|
|
|
2746
|
+
**Call another service from a route handler** — there is no `await fetch()`
|
|
2747
|
+
in a handler and no event-loop callback: `nish/net/upstream`'s `Upstream(h,
|
|
2748
|
+
origin, limits)` is a pool of keep-alive connections to one `http:` origin on
|
|
2749
|
+
the `HttpHandler`'s own loop. `users.get(req, path)` returns at once — test it
|
|
2750
|
+
for `UPSTREAM_E_FULL`, which holds nothing — and keeps `req` held; after each
|
|
2751
|
+
`h.wait(ms)`, `while (users.ready())` `take()`s every finished call, answers
|
|
2752
|
+
`r.req` (`r.ok`, `r.status` and `r.body[0 .. r.length)`, or `r.error`: the
|
|
2753
|
+
codec's `HTTP1_E_` refusal or an `UPSTREAM_E_` code such as `TIMEOUT`) and
|
|
2754
|
+
`release`s it once. The origin is looked up once at start-up; set
|
|
2755
|
+
`UpstreamLimits.outbound` when it comes from input. Each connection takes a
|
|
2756
|
+
handler watch id from `firstId` (8) up, so keep your own `h.watch` ids below
|
|
2757
|
+
it. (No block, as for `nish/text`; `tests/link/net_upstream` is its program,
|
|
2758
|
+
and the user guide's [calling-services page](guide/using-nish/calling-services.md)
|
|
2759
|
+
its walk-through.)
|
|
2760
|
+
|
|
2746
2761
|
**Draw to the whole terminal** (a game, a dashboard) — there is no curses and
|
|
2747
2762
|
no `process.stdout.on("resize")`: `runTerminal(options, state, update, render)`
|
|
2748
2763
|
from `nish/term-io` opens the terminal, probes what it can draw, steps
|
package/package.json
CHANGED
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);
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* Nish runtime, the region half: arenas a program makes, enters, leaves and
|
|
2
|
+
* frees one at a time, so a long-running program can give back the memory of
|
|
3
|
+
* one session, one cache entry or one connection without a garbage collector
|
|
4
|
+
* (docs/wp46-long-running-memory.md, section 2.1, "Runtime and ABI").
|
|
5
|
+
*
|
|
6
|
+
* A translation unit of its own, paired with runtime.c in scripts/build.sh's
|
|
7
|
+
* `halves` and src/run-cache.ts's `runtimeUnits()` like the others, so it has
|
|
8
|
+
* its own measured `.text*` ceiling in tests/runtime-budgets.js and a program
|
|
9
|
+
* that never makes a region links none of it. The wasm profile has no malloc
|
|
10
|
+
* to give a region its chunks, so `scripts/build.sh --profile wasm` refuses IR
|
|
11
|
+
* that names `@nish_region_`; the wasi profile links this file.
|
|
12
|
+
*
|
|
13
|
+
* **A handle is a number.** `nish_region_new` answers an `i64`: the slot's
|
|
14
|
+
* index in the low 24 bits and the slot's generation in the 40 above them.
|
|
15
|
+
* Generations start at 1, so 0 -- what a fresh `Region<T>[]` holds -- is never
|
|
16
|
+
* a handle. `nish_region_free` moves the slot's generation on, so a copy of a
|
|
17
|
+
* freed handle is stale; a slot whose generation would pass 40 bits is
|
|
18
|
+
* retired rather than reused, so no handle ever names a slot twice. Every
|
|
19
|
+
* call that takes a handle checks it first and panics on a zero handle, a
|
|
20
|
+
* stale one, and one made on another thread. The 24-bit index caps a process
|
|
21
|
+
* at 16,777,216 live regions; `nish_region_new` past that panics.
|
|
22
|
+
*
|
|
23
|
+
* **The table** is one per process: a directory of pages, each page 1,024
|
|
24
|
+
* slots allocated (zeroed) the first time a slot in it is handed out, so a
|
|
25
|
+
* program that makes ten regions holds one page. Taking a slot and giving one
|
|
26
|
+
* back take the table's lock, once per `new` and once per `free`; `enter` and
|
|
27
|
+
* `leave` take none, because a slot is only ever entered by the thread that
|
|
28
|
+
* made it and the check reads the two fields another thread's `free` could
|
|
29
|
+
* write (generation and thread) with atomic loads. Only a threads build
|
|
30
|
+
* (`-DNISH_THREADS`) has a second thread to lock against, so only it pays for
|
|
31
|
+
* the lock.
|
|
32
|
+
*
|
|
33
|
+
* **Entering swaps arenas.** A slot holds a whole `struct nish_arena`.
|
|
34
|
+
* `nish_region_enter` swaps it with the thread's `nish_arena`, so every
|
|
35
|
+
* allocation the program makes until `nish_region_leave` -- the inlined bump
|
|
36
|
+
* as well as the runtime's own -- lands in the region; `leave` swaps them
|
|
37
|
+
* back. While a region is entered its slot holds the arena it displaced, so
|
|
38
|
+
* entering a second region inside the first nests: each leave restores what
|
|
39
|
+
* its enter took. A slot that is entered cannot be entered again (it would
|
|
40
|
+
* swap its own arena with itself) or freed, and `leave` must name the region
|
|
41
|
+
* entered last on this thread.
|
|
42
|
+
*
|
|
43
|
+
* **Chunk size.** A thread's own arena grows by 64 KiB chunks, which is right
|
|
44
|
+
* for one arena a thread and wrong for a hundred thousand of them. A region's
|
|
45
|
+
* first chunk is the size of its first allocation, and each later chunk
|
|
46
|
+
* doubles, or takes the allocation's own size when that is more. The size of
|
|
47
|
+
* the next chunk is runtime.c's thread-local `nish_region_chunk`, which
|
|
48
|
+
* `nish_arena_grow` reads on its slow path and which `enter` and `leave` swap
|
|
49
|
+
* with the slot's as they swap the arena; 0 there means the thread's own
|
|
50
|
+
* arena and the 64 KiB floor. A region starts it at 8, the smallest block.
|
|
51
|
+
*
|
|
52
|
+
* **The root.** `Region<T>` is only the number, so the slot keeps the root
|
|
53
|
+
* object `region`'s function answered: `nish_region_leave` records it and
|
|
54
|
+
* `nish_region_enter` answers it.
|
|
55
|
+
*
|
|
56
|
+
* Freeing a region frees its chunks through runtime.c's `nish_free_arena`,
|
|
57
|
+
* with the region's arena swapped in for the call, so an `-DNISH_ASAN` build
|
|
58
|
+
* forgets the freed blocks as it does for the thread's arena. */
|
|
59
|
+
|
|
60
|
+
#include <stdint.h>
|
|
61
|
+
#include <stdlib.h>
|
|
62
|
+
|
|
63
|
+
#include "nish.h"
|
|
64
|
+
|
|
65
|
+
#define NISH_COLD __attribute__((noreturn, cold, noinline))
|
|
66
|
+
|
|
67
|
+
/* runtime.c's: the size of the next chunk `nish_arena_grow` makes for the
|
|
68
|
+
arena in `nish_arena`, or 0 for a thread's own, which takes 64 KiB. */
|
|
69
|
+
extern NISH_TLS uint64_t nish_region_chunk;
|
|
70
|
+
|
|
71
|
+
#define NISH_REGION_SLOT_BITS 24
|
|
72
|
+
#define NISH_REGION_SLOTS ((uint64_t)1 << NISH_REGION_SLOT_BITS)
|
|
73
|
+
#define NISH_REGION_GEN_END ((uint64_t)1 << 40)
|
|
74
|
+
#define NISH_REGION_PAGE 1024
|
|
75
|
+
|
|
76
|
+
typedef struct nish_region_slot {
|
|
77
|
+
struct nish_arena arena; /* the region's arena, or, while entered, the one it displaced */
|
|
78
|
+
uint64_t gen; /* the live handle's generation; NISH_REGION_GEN_END once retired */
|
|
79
|
+
uint64_t thread; /* the thread that made it, 0 while the slot is free */
|
|
80
|
+
uint64_t chunk; /* the region's next chunk size, or, while entered, the displaced one */
|
|
81
|
+
uint64_t outer; /* while entered: the handle entered before it; while free: the next free slot + 1 */
|
|
82
|
+
void *root; /* what `leave` recorded, answered by `enter` */
|
|
83
|
+
uint64_t entered;
|
|
84
|
+
} nish_region_slot;
|
|
85
|
+
|
|
86
|
+
static nish_region_slot *nish_region_pages[NISH_REGION_SLOTS / NISH_REGION_PAGE];
|
|
87
|
+
static uint64_t nish_region_used; /* slots ever handed out: the next never-used index */
|
|
88
|
+
static uint64_t nish_region_free_list; /* a free slot's index + 1, or 0 */
|
|
89
|
+
/* The handle entered last on this thread, or 0. */
|
|
90
|
+
static NISH_TLS uint64_t nish_region_current;
|
|
91
|
+
|
|
92
|
+
#ifdef NISH_THREADS
|
|
93
|
+
static int nish_region_lock_word;
|
|
94
|
+
static uint64_t nish_region_threads;
|
|
95
|
+
static NISH_TLS uint64_t nish_region_thread_id;
|
|
96
|
+
static void nish_region_lock(void) {
|
|
97
|
+
while (__atomic_exchange_n(&nish_region_lock_word, 1, __ATOMIC_ACQUIRE))
|
|
98
|
+
while (__atomic_load_n(&nish_region_lock_word, __ATOMIC_RELAXED)) {}
|
|
99
|
+
}
|
|
100
|
+
static void nish_region_unlock(void) { __atomic_store_n(&nish_region_lock_word, 0, __ATOMIC_RELEASE); }
|
|
101
|
+
/* A number no other thread of the process has: 64 bits, so it never wraps
|
|
102
|
+
back onto a live thread's. */
|
|
103
|
+
static uint64_t nish_region_me(void) {
|
|
104
|
+
if (!nish_region_thread_id) nish_region_thread_id = __atomic_add_fetch(&nish_region_threads, 1, __ATOMIC_RELAXED);
|
|
105
|
+
return nish_region_thread_id;
|
|
106
|
+
}
|
|
107
|
+
#else
|
|
108
|
+
static void nish_region_lock(void) {}
|
|
109
|
+
static void nish_region_unlock(void) {}
|
|
110
|
+
static uint64_t nish_region_me(void) { return 1; }
|
|
111
|
+
#endif
|
|
112
|
+
|
|
113
|
+
/* The panics, by number: one cold function and one table, rather than a
|
|
114
|
+
message's address loaded at each call site. Each ends in `nish_panic`, so
|
|
115
|
+
only the first panic of a process prints and atexit handlers run. */
|
|
116
|
+
enum {
|
|
117
|
+
NISH_REGION_ZERO,
|
|
118
|
+
NISH_REGION_STALE,
|
|
119
|
+
NISH_REGION_THREAD,
|
|
120
|
+
NISH_REGION_ENTERED,
|
|
121
|
+
NISH_REGION_FREE_ENTERED,
|
|
122
|
+
NISH_REGION_LEAVE,
|
|
123
|
+
NISH_REGION_FULL,
|
|
124
|
+
NISH_REGION_OOM,
|
|
125
|
+
};
|
|
126
|
+
#define NISH_REGION_MSG(text) { sizeof text - 1, text }
|
|
127
|
+
static const struct { uint64_t len; char data[72]; } nish_region_messages[] = {
|
|
128
|
+
NISH_REGION_MSG("nish: a zero region handle: no region was made for it"),
|
|
129
|
+
NISH_REGION_MSG("nish: a stale region handle: the region was freed"),
|
|
130
|
+
NISH_REGION_MSG("nish: a region handle made on another thread"),
|
|
131
|
+
NISH_REGION_MSG("nish: enter of a region that is already entered"),
|
|
132
|
+
NISH_REGION_MSG("nish: free of a region that is entered"),
|
|
133
|
+
NISH_REGION_MSG("nish: leave of a region that is not the one entered last"),
|
|
134
|
+
NISH_REGION_MSG("nish: more than 16777216 live regions"),
|
|
135
|
+
NISH_REGION_MSG("nish: out of memory"),
|
|
136
|
+
};
|
|
137
|
+
static NISH_COLD void nish_region_die(int why) {
|
|
138
|
+
nish_panic((const nish_str *)&nish_region_messages[why]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* The slot `r` names, or a panic: zero, stale, another thread's. */
|
|
142
|
+
static nish_region_slot *nish_region_slot_of(uint64_t r) {
|
|
143
|
+
if (!r) nish_region_die(NISH_REGION_ZERO);
|
|
144
|
+
uint64_t index = r & (NISH_REGION_SLOTS - 1);
|
|
145
|
+
nish_region_slot *page = __atomic_load_n(&nish_region_pages[index / NISH_REGION_PAGE], __ATOMIC_ACQUIRE);
|
|
146
|
+
if (!page) nish_region_die(NISH_REGION_STALE);
|
|
147
|
+
nish_region_slot *s = &page[index % NISH_REGION_PAGE];
|
|
148
|
+
uint64_t thread = __atomic_load_n(&s->thread, __ATOMIC_RELAXED);
|
|
149
|
+
if (!thread || __atomic_load_n(&s->gen, __ATOMIC_RELAXED) != r >> NISH_REGION_SLOT_BITS)
|
|
150
|
+
nish_region_die(NISH_REGION_STALE);
|
|
151
|
+
if (thread != nish_region_me()) nish_region_die(NISH_REGION_THREAD);
|
|
152
|
+
return s;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Swap the slot's arena and chunk size with the thread's: `enter` and `leave`
|
|
156
|
+
both, and `free` around its `nish_free_arena`. */
|
|
157
|
+
static void nish_region_swap(nish_region_slot *s) {
|
|
158
|
+
struct nish_arena a = nish_arena;
|
|
159
|
+
nish_arena = s->arena;
|
|
160
|
+
s->arena = a;
|
|
161
|
+
uint64_t c = nish_region_chunk;
|
|
162
|
+
nish_region_chunk = s->chunk;
|
|
163
|
+
s->chunk = c;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
uint64_t nish_region_new(void) {
|
|
167
|
+
uint64_t me = nish_region_me();
|
|
168
|
+
nish_region_lock();
|
|
169
|
+
uint64_t index;
|
|
170
|
+
nish_region_slot *s;
|
|
171
|
+
if (nish_region_free_list) {
|
|
172
|
+
index = nish_region_free_list - 1;
|
|
173
|
+
s = &nish_region_pages[index / NISH_REGION_PAGE][index % NISH_REGION_PAGE];
|
|
174
|
+
nish_region_free_list = s->outer;
|
|
175
|
+
} else {
|
|
176
|
+
if (nish_region_used == NISH_REGION_SLOTS) nish_region_die(NISH_REGION_FULL);
|
|
177
|
+
index = nish_region_used++;
|
|
178
|
+
nish_region_slot **page = &nish_region_pages[index / NISH_REGION_PAGE];
|
|
179
|
+
if (!*page) {
|
|
180
|
+
nish_region_slot *p = calloc(NISH_REGION_PAGE, sizeof *p);
|
|
181
|
+
if (!p) nish_region_die(NISH_REGION_OOM);
|
|
182
|
+
__atomic_store_n(page, p, __ATOMIC_RELEASE);
|
|
183
|
+
}
|
|
184
|
+
s = &(*page)[index % NISH_REGION_PAGE];
|
|
185
|
+
__atomic_store_n(&s->gen, 1, __ATOMIC_RELAXED);
|
|
186
|
+
}
|
|
187
|
+
s->chunk = 8;
|
|
188
|
+
s->outer = 0;
|
|
189
|
+
__atomic_store_n(&s->thread, me, __ATOMIC_RELAXED);
|
|
190
|
+
uint64_t r = s->gen << NISH_REGION_SLOT_BITS | index;
|
|
191
|
+
nish_region_unlock();
|
|
192
|
+
return r;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void *nish_region_enter(uint64_t r) {
|
|
196
|
+
nish_region_slot *s = nish_region_slot_of(r);
|
|
197
|
+
if (s->entered) nish_region_die(NISH_REGION_ENTERED);
|
|
198
|
+
s->entered = 1;
|
|
199
|
+
s->outer = nish_region_current;
|
|
200
|
+
nish_region_current = r;
|
|
201
|
+
nish_region_swap(s);
|
|
202
|
+
return s->root;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
void nish_region_leave(uint64_t r, void *root) {
|
|
206
|
+
nish_region_slot *s = nish_region_slot_of(r);
|
|
207
|
+
if (nish_region_current != r) nish_region_die(NISH_REGION_LEAVE);
|
|
208
|
+
nish_region_swap(s);
|
|
209
|
+
nish_region_current = s->outer;
|
|
210
|
+
s->entered = 0;
|
|
211
|
+
s->root = root;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
void nish_region_free(uint64_t r) {
|
|
215
|
+
nish_region_slot *s = nish_region_slot_of(r);
|
|
216
|
+
if (s->entered) nish_region_die(NISH_REGION_FREE_ENTERED);
|
|
217
|
+
nish_region_swap(s);
|
|
218
|
+
nish_free_arena();
|
|
219
|
+
nish_region_swap(s);
|
|
220
|
+
s->root = 0;
|
|
221
|
+
uint64_t index = r & (NISH_REGION_SLOTS - 1);
|
|
222
|
+
nish_region_lock();
|
|
223
|
+
__atomic_store_n(&s->thread, 0, __ATOMIC_RELAXED);
|
|
224
|
+
uint64_t gen = s->gen + 1;
|
|
225
|
+
__atomic_store_n(&s->gen, gen, __ATOMIC_RELAXED);
|
|
226
|
+
if (gen < NISH_REGION_GEN_END) {
|
|
227
|
+
s->outer = nish_region_free_list;
|
|
228
|
+
nish_region_free_list = index + 1;
|
|
229
|
+
}
|
|
230
|
+
nish_region_unlock();
|
|
231
|
+
}
|
package/runtime/runtime.c
CHANGED
|
@@ -174,14 +174,24 @@ static NISH_COLD void nish_oom(void) { nish_die("nish: out of memory\n"); }
|
|
|
174
174
|
`nish_array_grow` and `nish_str_concat`. */
|
|
175
175
|
#define NISH_ARRAY_MAX 2147483647u
|
|
176
176
|
|
|
177
|
-
/*
|
|
177
|
+
/* The size of the next chunk while a region is entered (runtime-region.c
|
|
178
|
+
swaps it in and out with the region's arena), or 0 for the thread's own
|
|
179
|
+
arena, whose chunks are at least 64 KB. Defined here, not there, so that
|
|
180
|
+
this file still links alone. */
|
|
181
|
+
NISH_TLS uint64_t nish_region_chunk;
|
|
182
|
+
|
|
183
|
+
/* Slow path (size 8-byte rounded): push a chunk (>= 64 KB, or a region's
|
|
184
|
+
next size), bump from it. A region's chunks double from its first. */
|
|
178
185
|
void *nish_arena_grow(uint64_t size) {
|
|
179
186
|
#ifdef NISH_ASAN
|
|
180
187
|
/* An exact chunk leaves off == cap, so the inline bump never fits again and
|
|
181
188
|
every block comes through here: one malloc, one redzone, per block. */
|
|
182
189
|
uint64_t cap = size;
|
|
183
190
|
#else
|
|
184
|
-
uint64_t
|
|
191
|
+
uint64_t next = nish_region_chunk;
|
|
192
|
+
uint64_t floor = next ? next : 65536;
|
|
193
|
+
uint64_t cap = size > floor ? size : floor;
|
|
194
|
+
if (next) nish_region_chunk = cap * 2;
|
|
185
195
|
#endif
|
|
186
196
|
/* A 64-bit request cannot always be asked for: wasm32's `size_t` is 32 bits,
|
|
187
197
|
and malloc would silently take the low half of a chunk nobody can address. */
|
package/scripts/build.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm|wasi|napi]
|
|
5
5
|
#
|
|
6
|
-
# The C runtime is
|
|
6
|
+
# The C runtime is fifteen translation units and is named as one: an input
|
|
7
7
|
# <dir>/runtime.c also compiles <dir>/runtime-os.c, the half that wraps the
|
|
8
8
|
# system calls (files, directories, subprocesses, the environment, the clock),
|
|
9
9
|
# <dir>/runtime-parallel.c, the half that divides a range of work across
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
# <dir>/runtime-checksum.c, CRC-32 on its carry-less multiply or CRC instructions,
|
|
18
18
|
# <dir>/runtime-term.c, stdin and the terminal of `nish:term`,
|
|
19
19
|
# <dir>/runtime-bytes.c, the byte-class runs of `nish/bytes`,
|
|
20
|
-
# <dir>/runtime-sandbox.c, the confinement `--sandbox` applies,
|
|
21
|
-
# <dir>/runtime-shm.c, the shared-memory regions of `nish/sync/shm
|
|
20
|
+
# <dir>/runtime-sandbox.c, the confinement `--sandbox` applies,
|
|
21
|
+
# <dir>/runtime-shm.c, the shared-memory regions of `nish/sync/shm`, and
|
|
22
|
+
# <dir>/runtime-region.c, the arenas a program makes and frees one at a time. Each of those
|
|
22
23
|
# files says why they are compiled and measured apart.
|
|
23
24
|
#
|
|
24
25
|
# Profiles:
|
|
@@ -222,11 +223,11 @@ if [ -n "$sqlite_to" ] || [ -n "$sqlite_from" ]; then
|
|
|
222
223
|
fi
|
|
223
224
|
[ -n "$out" ] || [ -n "$objects_to" ] || [ -n "$sqlite_to" ] || { echo "error: -o <out> is required" >&2; exit 2; }
|
|
224
225
|
|
|
225
|
-
# The runtime is
|
|
226
|
+
# The runtime is fifteen translation units, and a caller names one: whoever passes
|
|
226
227
|
# <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c,
|
|
227
228
|
# <dir>/runtime-cross.c, <dir>/runtime-host.c, <dir>/runtime-net.c, <dir>/runtime-simd.c,
|
|
228
229
|
# <dir>/runtime-aes.c, <dir>/runtime-sha256.c, <dir>/runtime-checksum.c, <dir>/runtime-term.c,
|
|
229
|
-
# <dir>/runtime-bytes.c, <dir>/runtime-sandbox.c and <dir>/runtime-
|
|
230
|
+
# <dir>/runtime-bytes.c, <dir>/runtime-sandbox.c, <dir>/runtime-shm.c and <dir>/runtime-region.c compiled beside it, on every profile,
|
|
230
231
|
# wasi included: runtime-simd.c and runtime-bytes.c take their scalar paths there
|
|
231
232
|
# unless the build adds -msimd128,
|
|
232
233
|
# runtime-aes.c, runtime-sha256.c and runtime-checksum.c compile to an answer of 0 and
|
|
@@ -243,7 +244,7 @@ fi
|
|
|
243
244
|
# here keeps every one of those correct, and keeps "the runtime" one thing to
|
|
244
245
|
# name from the outside. A caller that names one itself is left alone, because
|
|
245
246
|
# naming one object twice is a duplicate-symbol error.
|
|
246
|
-
halves="runtime-os.c runtime-parallel.c runtime-cross.c runtime-host.c runtime-net.c runtime-simd.c runtime-aes.c runtime-sha256.c runtime-checksum.c runtime-term.c runtime-bytes.c runtime-sandbox.c runtime-shm.c"
|
|
247
|
+
halves="runtime-os.c runtime-parallel.c runtime-cross.c runtime-host.c runtime-net.c runtime-simd.c runtime-aes.c runtime-sha256.c runtime-checksum.c runtime-term.c runtime-bytes.c runtime-sandbox.c runtime-shm.c runtime-region.c"
|
|
247
248
|
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
248
249
|
case "$i" in
|
|
249
250
|
*/runtime.c|runtime.c)
|
|
@@ -671,6 +672,18 @@ case "$profile" in
|
|
|
671
672
|
"$CC" "${common[@]}" "${optimise[@]}" ${lto_parts[@]+"${lto_parts[@]}"} \
|
|
672
673
|
"${gc[@]}" ${strip_flag[@]+"${strip_flag[@]}"} "${inputs[@]}" -lm -o "$out" ;;
|
|
673
674
|
wasm)
|
|
675
|
+
# A region's chunks come from malloc, and the freestanding wasm runtime
|
|
676
|
+
# (runtime-wasm.c) has none, so IR that makes or enters one is refused here
|
|
677
|
+
# rather than failing at the link on an undefined `nish_region_*`. The wasi
|
|
678
|
+
# profile links runtime-region.c with the rest of the runtime.
|
|
679
|
+
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
680
|
+
case "$i" in
|
|
681
|
+
*.ll) if grep -q "@nish_region_" "$i"; then
|
|
682
|
+
echo "error: $i uses regions, which the wasm profile cannot link (it has no malloc); build it with --profile wasi" >&2
|
|
683
|
+
exit 2
|
|
684
|
+
fi ;;
|
|
685
|
+
esac
|
|
686
|
+
done
|
|
674
687
|
# clang resolves wasm-ld next to its own binary first, then on PATH; ask it
|
|
675
688
|
# rather than probing PATH so a Homebrew llvm without a PATH entry still works.
|
|
676
689
|
wasm_ld=$("$CC" -print-prog-name=wasm-ld)
|
package/std/README.md
CHANGED
|
@@ -250,7 +250,9 @@ reads one, and it still takes its randomness as an argument.
|
|
|
250
250
|
| [`net/http-compress.ts`](./net/http-compress.ts) | Content coding for HTTP responses: `httpAcceptEncoding(buf, start, end, offered)` reads an `Accept-Encoding` value (q-values, `*`, `identity`, `x-gzip`) into `HTTP_CODING_GZIP`, `HTTP_CODING_DEFLATE`, `HTTP_CODING_IDENTITY` or `HTTP_CODING_NONE`, allocating nothing; `HttpCompressor(level, minLength, maxLength)` gzips a body into its own buffer when it is worth it (-1 to send it as it is) and builds the response fields (`addFields`: `Content-Encoding: gzip` and, while `vary`, `Vary: Accept-Encoding`). It sends gzip only when it pays: `minLength`, `minSaving` bytes or `minSavingPercent`, and `segmentMode` (fewer TCP segments of `mss`, head included, than as it is); `decision` says why (`HTTP_COMPRESS_SENT`, `_NOT_ACCEPTED`, `_TOO_SMALL`, `_TOO_LARGE`, `_TOO_LITTLE_SAVED`, `_NO_FEWER_SEGMENTS`). Bodies up to `fixedLimit` use the fixed-Huffman encoder (`encoder`: `HTTP_ENCODER_AUTO`, `_FIXED`, `_LEVEL`). On HTTP/1.1, `Http1Connection.acceptEncoding()` and `respondEncoded(status, names, values, body, off, len, enc, coding)` answer a request in one call with the compressed `Content-Length`, or nothing at all (`H1_AGAIN`, `H1_INVALID`); `identity;q=0` is honoured. Opt-in; BREACH is the program's to avoid (`docs/security/compress.md`) | RFC 9110 §8.4 and §12.5.3: the negotiation table, and sans-IO responses gunzipped back (`tests/link/std_http_compress`) |
|
|
251
251
|
| [`net/router.ts`](./net/router.ts) | `Router`, a method and path router that knows no carrier and allocates nothing per match: `add(method, pattern)` answers the route's index (0, 1, 2 in the order added) or a refusal — `ROUTER_BAD_PATTERN`, `ROUTER_BAD_METHOD`, `ROUTER_TOO_LONG`, `ROUTER_AMBIGUOUS` (`*` not last), `ROUTER_DUPLICATE` (the same shape and method, whatever the parameter names) or `ROUTER_FULL`, against `RouterLimits`; `match(method, mStart, mEnd, target, tStart, tEnd, params)` answers the index, `ROUTER_NOT_FOUND` or `ROUTER_METHOD_NOT_ALLOWED`, writing each `:name` and a final `*` as a `[start, end)` pair into the caller's `params` (`ROUTER_TOO_MANY_PARAMS` when it is short). Literal before parameter before `*`, segment by segment, backtracking; a lookup enters each node at most once. The query is left unparsed and nothing is decoded: `routerDecode` decodes one parameter (`ROUTER_BAD_ESCAPE`, `ROUTER_NO_ROOM`) (`docs/security/router.md`) | Every refusal code, precedence and backtracking, 405 against 404, `%2F` kept in one segment, the query, `*` and authority-form targets, a target deeper than any route, the lookup bound, params past storage, and the arena flat over matches (`tests/link/net_router`, `net_router_f64`, five `net_router_negative_*`) |
|
|
252
252
|
| [`net/handler.ts`](./net/handler.ts) | `HttpHandler`, complete requests and their answers over the HTTP/1.1 carrier: it owns the readiness loop, routes each request through a `Router` at its head, gathers its body up to `HandlerLimits.http.maxBody` (1 MiB; past it the carrier answers 413), and hands it over whole. The program pulls — `wait(ms)`, `ready()`, `take()` — and switches on `req.route`; `HttpRequest` reads parameters, form-decoded query values and fields as ranges into its own buffers. Each request is answered once, now or in a later turn, by `send`, `sendFields`, `empty`, `json()` + `sendJson`, `problem` (RFC 9457), `refuse` (404, or 405 with `Allow`) or `start`/`write`/`end`; an answer whose connection closed first is `HANDLER_DROPPED` and reaches no later peer. `HEAD` takes a `GET` route. `watch`/`fired` add the program's own descriptors (`signalFd()`), and `drain` stops gracefully. Nothing is allocated per request once warm (`docs/security/handler.md`) | Routing, parameters and form-decoded queries, `HEAD`, 404 and 405 problems, chunked bodies in and out, the body cap at and one past it, pipelining order, late answers dropped across a reused slot, the arena and pool flat over two thousand requests, and the drain (`tests/link/net_handler`, and `net_http_app` over `examples/http-app`) |
|
|
253
|
+
| [`net/upstream.ts`](./net/upstream.ts) | `Upstream`, outbound HTTP/1.1 calls from a route handler that never block it: a pool of keep-alive connections to one `http:` origin on the `HttpHandler`'s own loop (watch ids `UpstreamLimits.firstId` and up). `get(req, path)` returns at once, or `UPSTREAM_E_FULL` with nothing held; each call comes back once through `ready()`/`take()` tagged with `r.req`, `ok` with its status and `body[0 .. length)`, or failed with the codec's `HTTP1_E_` refusal or an `UPSTREAM_E_` code (timeout, closed, connect, origin, lookup, blocked, request), and goes back with `release`. The origin is looked up once at start-up and pinned; `UpstreamLimits.outbound` checks it against an `OutboundPolicy`. A failed call's connection is closed. Each connection keeps one codec in spans mode, so a warm pool allocates nothing per call | A slow upstream while other clients and a second upstream are answered, the timeout, keep-alive and tagging, five codec refusals as failures, the policy refusing loopback, origin and request-target refusals, three thousand warm calls on one connection with `Arena.used()` flat (`tests/link/net_upstream`), and a second `release` panicking (`net_upstream_release`) |
|
|
253
254
|
| [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindStartBinary` for a Bind whose result columns come in binary (read in place by `binaryInt`, `binaryBool`, `binaryFloat` and `PgBinary`'s `timestamp` and `iso`, and a one-dimensional `int2[]`, `int4[]` or `int8[]` by `PgBinaryArray`), `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first (read with `binaryInt` when `PgBatchConfig.binary` is set, so the batch's rows come in binary too), to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. `PgBatchClock` says when to take, from times the program passes in: once max(`minCount`, the moving average of batch sizes × `factor`) wait, the oldest has waited `maxWaitUs`, or, with the idle bypass, nothing is in flight and the load is light (its defaults are the fixed window); `PgBatcher.takeDue` takes only then, and `PgBatchTurns` lets one free connection collect while the others execute. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`); the clock's reasons, backlog and turns (`tests/link/std_postgres_batch_clock`) |
|
|
255
|
+
| [`net/postgres-carrier.ts`](./net/postgres-carrier.ts) | `PgCarrier`, a `PgPool`'s queries on an `HttpHandler`'s loop: its sockets sit in the handler's epoll set, a route handler hands a request to `run` (or `runInt` / `runText` for one of up to 8 attached `PgBatcher`s) and gets its number at once, and `next()` answers `PG_CARRIER_BUILD` to bind a plain query on `connection()`, `PG_CARRIER_ROW`, `PG_CARRIER_COMPLETE`, `PG_CARRIER_ERROR` and one final `PG_CARRIER_DONE`, `PG_CARRIER_MISSING` or `PG_CARRIER_FAILED` (`failure`, `socketError`) with `request()` the request to answer and `tag` the caller's. It connects the pool's slots itself (TCP or a Unix socket), times out a connect after `connectTimeoutMs`, sheds batched keys when no slot can connect, and sizes every table from `PgCarrierLimits` at construction: `run` answers `PG_CARRIER_FULL` rather than grow one, and once connections are up it allocates nothing per query. Kept apart from `net/postgres.ts` so that module reaches no capability (`docs/security/postgres.md`) | A loopback backend in the test itself: routing, a slow query not stalling another client, lost and refused connections, a connect timeout, and `Arena.used()` flat over thousands of queries (`tests/link/std_postgres_carrier`); a live server by `tests/postgres/live/run.mjs` |
|
|
254
256
|
| [`net/prefork.ts`](./net/prefork.ts) | A server on every core: `preforkWorkers(process.argv)` or `preforkWith(process.argv, config)` answers a worker's index in each worker it starts again from the same program, and in the supervisor runs them until SIGTERM or SIGINT, restarting one that exits after a backoff. `PreforkConfig.supervisorChannels` gives each worker a `unixSocketPair` to the supervisor, which `preforkSupervise(args, config, state, onChannel)` hands to a callback; `serviceWorker` gives every other worker a pair to that one, and makes the workers one group that restarts together on fresh pairs. The ends arrive from descriptor 3 through `spawnWithFds`, found with `preforkSupervisorChannel`, `preforkServiceChannel` and `preforkServiceChannelOf`. Native only | `tests/link/net_prefork` (the spread, a dead worker, SIGTERM, the backoff); `tests/link/net_prefork_channels` (each worker holds exactly its own ends, a restart, the supervisor's descriptors flat) |
|
|
255
257
|
|
|
256
258
|
**How a carrier drives `TlsServer`.** The server speaks in handshake
|