castrum 0.7.0 → 0.9.1
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/LICENSE +21 -0
- package/README.md +154 -368
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +2763 -0
- package/dist/index.js +8707 -0
- package/docs/API.md +337 -0
- package/docs/ARCHITECTURE.md +507 -0
- package/docs/BENCHMARKS.md +343 -0
- package/docs/CASE_STUDY.md +240 -0
- package/docs/ENVIRONMENT.md +81 -0
- package/docs/FFI_BUN_GUIDE.md +327 -0
- package/docs/GETTING_STARTED.md +192 -0
- package/docs/INGRESS-ROUTER.md +81 -0
- package/docs/INGRESS.md +746 -0
- package/docs/NATIVE-ROUTE.md +53 -0
- package/docs/REPO_MAP.md +279 -0
- package/docs/adr/0001-two-wire-formats.md +39 -0
- package/docs/adr/0002-zero-dom-fast-schema.md +38 -0
- package/docs/adr/0003-bun-builtins-delegation.md +43 -0
- package/docs/bun-builtins-decision-matrix.md +115 -0
- package/index.ts +44 -8
- package/package.json +98 -33
- package/src/ingress/body.ts +289 -0
- package/src/ingress/constants.ts +220 -48
- package/src/ingress/context.ts +136 -0
- package/src/ingress/decode/baked-result.ts +121 -0
- package/src/ingress/decode/fast-result.ts +160 -0
- package/src/ingress/decode/packed-sections.ts +56 -0
- package/src/ingress/decode/result-base.ts +172 -0
- package/src/ingress/errors.ts +72 -0
- package/src/ingress/fast.ts +152 -1021
- package/src/ingress/handlers.ts +639 -1001
- package/src/ingress/headers/baked-templates.ts +102 -0
- package/src/ingress/headers/cors.ts +44 -0
- package/src/ingress/headers/fast-templates.ts +238 -0
- package/src/ingress/headers/hsts.ts +83 -0
- package/src/ingress/health.ts +68 -0
- package/src/ingress/index.ts +142 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +139 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +164 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +102 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/router.ts +237 -0
- package/src/ingress/routes/common.ts +79 -0
- package/src/ingress/routes/delete.ts +14 -0
- package/src/ingress/routes/echo.ts +107 -0
- package/src/ingress/routes/fallback.ts +23 -0
- package/src/ingress/routes/head.ts +27 -0
- package/src/ingress/routes/index.ts +11 -0
- package/src/ingress/routes/json-write.ts +129 -0
- package/src/ingress/routes/options.ts +26 -0
- package/src/ingress/routes/read.ts +35 -0
- package/src/ingress/routes/responder.ts +159 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +475 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +191 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +220 -0
- package/src/integration/streaming.ts +78 -0
- package/src/integration/websocket.ts +93 -0
- package/src/loader/batch.ts +137 -0
- package/src/loader/cost.ts +105 -0
- package/src/loader/index.ts +745 -0
- package/src/loader/ops.ts +657 -0
- package/src/loader/state.ts +84 -0
- package/src/loader/types.ts +145 -0
- package/src/native/ffi/constants.ts +58 -0
- package/src/native/ffi/selftest.ts +729 -0
- package/src/native/ffi/types.ts +474 -0
- package/src/native/ffi.ts +1775 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +240 -0
- package/src/native/types/instances.ts +274 -0
- package/src/native/types.ts +401 -0
- package/src/runtime/builtins.ts +131 -0
- package/src/runtime/codec.ts +147 -0
- package/src/runtime/detect.ts +36 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/native.ts +35 -0
- package/src/runtime/server.ts +31 -0
- package/src/runtime/transport.ts +64 -0
- package/src/runtime/types.ts +173 -0
- package/src/runtime/uuid.ts +45 -0
- package/src/runtime/websocket.ts +30 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch/build.ts +155 -0
- package/src/rust-ffi/batch/index.ts +8 -0
- package/src/rust-ffi/batch/types.ts +92 -0
- package/src/rust-ffi/client.ts +87 -0
- package/src/rust-ffi/context.ts +193 -0
- package/src/rust-ffi/index.ts +51 -541
- package/src/rust-ffi/into.ts +21 -0
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +28 -0
- package/src/rust-ffi/scalar/crypto.ts +346 -0
- package/src/rust-ffi/scalar/factories.ts +459 -0
- package/src/rust-ffi/scalar/hashing.ts +74 -0
- package/src/rust-ffi/scalar/http.ts +337 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +325 -0
- package/src/rust-ffi/scalar/json-packed.ts +175 -0
- package/src/rust-ffi/scalar/json.ts +61 -0
- package/src/rust-ffi/scalar/payload.ts +241 -0
- package/src/rust-ffi/text.ts +121 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +73 -0
- package/src/shared/adaptive.ts +67 -0
- package/src/shared/buffer-pool.ts +264 -0
- package/src/shared/bytes.ts +108 -9
- package/src/shared/codec.ts +78 -0
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +75 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed/index.ts +10 -0
- package/src/shared/packed/parsers.ts +31 -0
- package/src/shared/packed/schema.ts +23 -0
- package/src/shared/packed/wire.ts +504 -0
- package/src/shared/proven.ts +289 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +29 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +15 -0
- package/Cargo.toml +0 -73
- package/bench.ts +0 -3
- package/rust/batch.rs +0 -195
- package/rust/cookie_parser.rs +0 -70
- package/rust/cors.rs +0 -263
- package/rust/hashing.rs +0 -164
- package/rust/headers.rs +0 -234
- package/rust/hmac_sha256.rs +0 -113
- package/rust/http_parser.rs +0 -132
- package/rust/ingress.rs +0 -616
- package/rust/ingress_constants.rs +0 -89
- package/rust/ip_trust.rs +0 -185
- package/rust/json_ops.rs +0 -66
- package/rust/json_patch_ops.rs +0 -25
- package/rust/json_schema.rs +0 -151
- package/rust/json_ser.rs +0 -429
- package/rust/lib.rs +0 -48
- package/rust/method.rs +0 -141
- package/rust/mime_lookup.rs +0 -76
- package/rust/output.rs +0 -238
- package/rust/proxy.rs +0 -90
- package/rust/query_parser.rs +0 -141
- package/rust/random_token.rs +0 -52
- package/rust/rate_limit.rs +0 -203
- package/rust/terminal.rs +0 -95
- package/rust/unit_tests.rs +0 -843
- package/rust/url_codec.rs +0 -133
- package/rust/util.rs +0 -537
- package/rust/validation.rs +0 -107
- package/rust/websocket.rs +0 -23
- package/src/baseline/index.ts +0 -12
- package/src/baseline/tasks/cookie.ts +0 -22
- package/src/baseline/tasks/hashing.ts +0 -18
- package/src/baseline/tasks/hmac.ts +0 -38
- package/src/baseline/tasks/http.ts +0 -62
- package/src/baseline/tasks/json-patch.ts +0 -12
- package/src/baseline/tasks/json.ts +0 -50
- package/src/baseline/tasks/mime.ts +0 -5
- package/src/baseline/tasks/query.ts +0 -14
- package/src/baseline/tasks/token.ts +0 -15
- package/src/baseline/tasks/url.ts +0 -11
- package/src/baseline/tasks/validation.ts +0 -19
- package/src/baseline/tasks/websocket.ts +0 -14
- package/src/bench/assert.ts +0 -70
- package/src/bench/checks.ts +0 -227
- package/src/bench/checksum.ts +0 -27
- package/src/bench/comparisons.ts +0 -70
- package/src/bench/concurrent-worker.ts +0 -135
- package/src/bench/fixtures.ts +0 -233
- package/src/bench/index.ts +0 -2
- package/src/bench/measure.ts +0 -177
- package/src/bench/now.ts +0 -3
- package/src/bench/report.ts +0 -79
- package/src/bench/run.ts +0 -57
- package/src/bench/tasks/complex.ts +0 -232
- package/src/bench/tasks/concurrent.ts +0 -213
- package/src/bench/tasks/cookie.ts +0 -44
- package/src/bench/tasks/hashing.ts +0 -33
- package/src/bench/tasks/hmac.ts +0 -36
- package/src/bench/tasks/http.ts +0 -50
- package/src/bench/tasks/index.ts +0 -52
- package/src/bench/tasks/json-patch.ts +0 -21
- package/src/bench/tasks/json.ts +0 -33
- package/src/bench/tasks/mime.ts +0 -21
- package/src/bench/tasks/query.ts +0 -44
- package/src/bench/tasks/stress.ts +0 -142
- package/src/bench/tasks/token.ts +0 -20
- package/src/bench/tasks/url.ts +0 -34
- package/src/bench/tasks/validation.ts +0 -57
- package/src/bench/tasks/websocket.ts +0 -21
- package/src/bench/types.ts +0 -60
- package/src/data/json-rows.ts +0 -33
- package/src/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
- package/src/shared/packed.ts +0 -316
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generatedAt": "2026-08-13T09:13:26.107Z",
|
|
3
|
+
"note": "Auto-selected by `bun scripts/select-native.ts --write` from measurements of the castrum addon vs representative pure-TS implementations. Do NOT hand-edit — regenerate with the benchmark script. The public API reads this via `opImpl(op)` and binds each op to a fixed impl (native or js) at load time.",
|
|
4
|
+
"ops": {
|
|
5
|
+
"fnv1a64": {
|
|
6
|
+
"impl": "native",
|
|
7
|
+
"nativeRatio": 12.603
|
|
8
|
+
},
|
|
9
|
+
"crc32": {
|
|
10
|
+
"impl": "js",
|
|
11
|
+
"nativeRatio": 0.659
|
|
12
|
+
},
|
|
13
|
+
"hmacSha256": {
|
|
14
|
+
"impl": "native",
|
|
15
|
+
"nativeRatio": 1.212
|
|
16
|
+
},
|
|
17
|
+
"hmacSha256Verify": {
|
|
18
|
+
"impl": "native",
|
|
19
|
+
"nativeRatio": 9.028
|
|
20
|
+
},
|
|
21
|
+
"signCookie": {
|
|
22
|
+
"impl": "native",
|
|
23
|
+
"nativeRatio": 3.64
|
|
24
|
+
},
|
|
25
|
+
"verifyCookie": {
|
|
26
|
+
"impl": "native",
|
|
27
|
+
"nativeRatio": 1.772
|
|
28
|
+
},
|
|
29
|
+
"csrfToken": {
|
|
30
|
+
"impl": "native",
|
|
31
|
+
"nativeRatio": 12.671
|
|
32
|
+
},
|
|
33
|
+
"csrfVerify": {
|
|
34
|
+
"impl": "native",
|
|
35
|
+
"nativeRatio": 12.458
|
|
36
|
+
},
|
|
37
|
+
"randomToken": {
|
|
38
|
+
"impl": "native",
|
|
39
|
+
"nativeRatio": 9.546
|
|
40
|
+
},
|
|
41
|
+
"passwordHash": {
|
|
42
|
+
"impl": "native",
|
|
43
|
+
"nativeRatio": 1.657
|
|
44
|
+
},
|
|
45
|
+
"aeadEncrypt": {
|
|
46
|
+
"impl": "native",
|
|
47
|
+
"nativeRatio": 1.288
|
|
48
|
+
},
|
|
49
|
+
"aeadDecrypt": {
|
|
50
|
+
"impl": "native",
|
|
51
|
+
"nativeRatio": 1.108
|
|
52
|
+
},
|
|
53
|
+
"queryPairs": {
|
|
54
|
+
"impl": "js",
|
|
55
|
+
"nativeRatio": 0.279
|
|
56
|
+
},
|
|
57
|
+
"cookiePairs": {
|
|
58
|
+
"impl": "js",
|
|
59
|
+
"nativeRatio": 0.105
|
|
60
|
+
},
|
|
61
|
+
"formPairs": {
|
|
62
|
+
"impl": "js",
|
|
63
|
+
"nativeRatio": 0.328
|
|
64
|
+
},
|
|
65
|
+
"etag": {
|
|
66
|
+
"impl": "js",
|
|
67
|
+
"nativeRatio": 0.187
|
|
68
|
+
},
|
|
69
|
+
"parseMediaType": {
|
|
70
|
+
"impl": "js",
|
|
71
|
+
"nativeRatio": 0.378
|
|
72
|
+
},
|
|
73
|
+
"parseAcceptEncoding": {
|
|
74
|
+
"impl": "js",
|
|
75
|
+
"nativeRatio": 0.367
|
|
76
|
+
},
|
|
77
|
+
"createConditionalRequest": {
|
|
78
|
+
"impl": "js",
|
|
79
|
+
"nativeRatio": 0.275
|
|
80
|
+
},
|
|
81
|
+
"createAcceptNegotiator": {
|
|
82
|
+
"impl": "native",
|
|
83
|
+
"nativeRatio": 2.01
|
|
84
|
+
},
|
|
85
|
+
"multipartParse": {
|
|
86
|
+
"impl": "js",
|
|
87
|
+
"nativeRatio": null
|
|
88
|
+
},
|
|
89
|
+
"jsonValid": {
|
|
90
|
+
"impl": "js",
|
|
91
|
+
"nativeRatio": 0.749
|
|
92
|
+
},
|
|
93
|
+
"jsonPatch": {
|
|
94
|
+
"impl": "js",
|
|
95
|
+
"nativeRatio": 0.014
|
|
96
|
+
},
|
|
97
|
+
"gzipCompress": {
|
|
98
|
+
"impl": "native",
|
|
99
|
+
"nativeRatio": 22.132
|
|
100
|
+
},
|
|
101
|
+
"gzipDecompress": {
|
|
102
|
+
"impl": "native",
|
|
103
|
+
"nativeRatio": 61.039
|
|
104
|
+
},
|
|
105
|
+
"brotliCompress": {
|
|
106
|
+
"impl": "native",
|
|
107
|
+
"nativeRatio": 9.983
|
|
108
|
+
},
|
|
109
|
+
"brotliDecompress": {
|
|
110
|
+
"impl": "native",
|
|
111
|
+
"nativeRatio": 33.327
|
|
112
|
+
},
|
|
113
|
+
"sseEncode": {
|
|
114
|
+
"impl": "js",
|
|
115
|
+
"nativeRatio": 0.586
|
|
116
|
+
},
|
|
117
|
+
"wsFrameEncode": {
|
|
118
|
+
"impl": "js",
|
|
119
|
+
"nativeRatio": 0.152
|
|
120
|
+
},
|
|
121
|
+
"wsFrameDecode": {
|
|
122
|
+
"impl": "js",
|
|
123
|
+
"nativeRatio": 0.988
|
|
124
|
+
},
|
|
125
|
+
"wsAcceptKey": {
|
|
126
|
+
"impl": "js",
|
|
127
|
+
"nativeRatio": 0.939
|
|
128
|
+
},
|
|
129
|
+
"validateEmail": {
|
|
130
|
+
"impl": "js",
|
|
131
|
+
"nativeRatio": 0.007
|
|
132
|
+
},
|
|
133
|
+
"validateUuid": {
|
|
134
|
+
"impl": "js",
|
|
135
|
+
"nativeRatio": 0.008
|
|
136
|
+
},
|
|
137
|
+
"validateIpv4": {
|
|
138
|
+
"impl": "js",
|
|
139
|
+
"nativeRatio": 0.012
|
|
140
|
+
},
|
|
141
|
+
"validateIpv6": {
|
|
142
|
+
"impl": "js",
|
|
143
|
+
"nativeRatio": 0.078
|
|
144
|
+
},
|
|
145
|
+
"createRateLimiter": {
|
|
146
|
+
"impl": "js",
|
|
147
|
+
"nativeRatio": 0.061
|
|
148
|
+
},
|
|
149
|
+
"renderTemplate": {
|
|
150
|
+
"impl": "js",
|
|
151
|
+
"nativeRatio": 0.141
|
|
152
|
+
},
|
|
153
|
+
"xxh3": {
|
|
154
|
+
"impl": "native",
|
|
155
|
+
"nativeRatio": null
|
|
156
|
+
},
|
|
157
|
+
"urlEncode": {
|
|
158
|
+
"impl": "js",
|
|
159
|
+
"nativeRatio": 0.214
|
|
160
|
+
},
|
|
161
|
+
"urlDecode": {
|
|
162
|
+
"impl": "js",
|
|
163
|
+
"nativeRatio": 0.092
|
|
164
|
+
},
|
|
165
|
+
"base64Encode": {
|
|
166
|
+
"impl": "js",
|
|
167
|
+
"nativeRatio": 0.376
|
|
168
|
+
},
|
|
169
|
+
"httpDate": {
|
|
170
|
+
"impl": "js",
|
|
171
|
+
"nativeRatio": 0.401
|
|
172
|
+
},
|
|
173
|
+
"jwtSign": {
|
|
174
|
+
"impl": "native",
|
|
175
|
+
"nativeRatio": null
|
|
176
|
+
},
|
|
177
|
+
"jwtVerify": {
|
|
178
|
+
"impl": "native",
|
|
179
|
+
"nativeRatio": null
|
|
180
|
+
},
|
|
181
|
+
"passwordVerify": {
|
|
182
|
+
"impl": "native",
|
|
183
|
+
"nativeRatio": null
|
|
184
|
+
},
|
|
185
|
+
"createTemplate": {
|
|
186
|
+
"impl": "native",
|
|
187
|
+
"nativeRatio": null
|
|
188
|
+
},
|
|
189
|
+
"createSchemaValidator": {
|
|
190
|
+
"impl": "native",
|
|
191
|
+
"nativeRatio": null
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/selection.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/selection.ts — native-vs-JS selection surface (OWNED BY CASTRUM).
|
|
2
|
+
//
|
|
3
|
+
// The decision of whether an operation should use the Rust addon ("native")
|
|
4
|
+
// or a pure-JS implementation ("js") is a property of this library, not of
|
|
5
|
+
// any consumer. The single source of truth is `rust/selection.rs`
|
|
6
|
+
// (benchmark-driven, audited by `scripts/select-native.ts --check`); this
|
|
7
|
+
// module is its TS projection and the API consumers call ONCE at load time.
|
|
8
|
+
//
|
|
9
|
+
// Consumers bind each operation to a fixed implementation at startup by
|
|
10
|
+
// reading `opImpl(op)` here — they do NOT swap native↔js per call.
|
|
11
|
+
//
|
|
12
|
+
// The baked default comes from the PROVEN SELECTION registry
|
|
13
|
+
// (`src/shared/proven.ts`, `PROVEN_SELECTION`): for each op the
|
|
14
|
+
// benchmark-proven winner is 'native' | 'js' | 'bun' (Bun built-in
|
|
15
|
+
// delegation). Bun built-in delegation is runtime-aware via the adapter's
|
|
16
|
+
// `builtins` registry (`runtimeNative.builtins.has(op)` — empty under Node).
|
|
17
|
+
// `test/unit/shared/proven.test.ts` verifies this baked registry matches the
|
|
18
|
+
// live wiring, so it can't drift.
|
|
19
|
+
|
|
20
|
+
import { getAddon, lazyAddon } from './native'
|
|
21
|
+
import { provenImpl } from './shared/proven'
|
|
22
|
+
import { runtimeNative } from './runtime/native'
|
|
23
|
+
|
|
24
|
+
/** Recommended implementation for an operation. */
|
|
25
|
+
export type OpImpl = 'native' | 'js'
|
|
26
|
+
|
|
27
|
+
/** A selection decision: recommended implementation + rationale note. */
|
|
28
|
+
export interface OpDecision {
|
|
29
|
+
/** The recommended implementation when the addon is available. */
|
|
30
|
+
readonly impl: OpImpl
|
|
31
|
+
/** Why this decision (measured ratio / rationale). */
|
|
32
|
+
readonly note?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Lazy: importing this module does NOT dlopen the addon; `opImpl` reads the
|
|
36
|
+
// baked decision from Rust on first call.
|
|
37
|
+
const addon = lazyAddon(getAddon)
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The benchmark-driven native-vs-JS decision for `op` — runtime-aware.
|
|
41
|
+
*
|
|
42
|
+
* Returns `"native"` when the Rust addon is the recommended implementation,
|
|
43
|
+
* `"js"` when a pure-JS (or Bun built-in) implementation wins, or `null` for
|
|
44
|
+
* unknown ops (or when the addon is unavailable — with no addon there is no
|
|
45
|
+
* "native" choice). Under Bun, ops in the adapter's builtins registry resolve
|
|
46
|
+
* to `"js"` so the consumer's JS path delegates to the faster Bun built-in.
|
|
47
|
+
*
|
|
48
|
+
* The default is the BAKED proven selection (`src/shared/proven.ts`):
|
|
49
|
+
* `native` / `js` directly, `bun` resolves to `"js"` via the builtins check
|
|
50
|
+
* above. Unknown ops (and `bun` entries on a non-Bun runtime, where no
|
|
51
|
+
* built-in exists) fall back to the addon's embedded decision
|
|
52
|
+
* (`rust/selection.rs` ← `src/selection.json`).
|
|
53
|
+
*/
|
|
54
|
+
export const opImpl = (op: string): OpImpl | null => {
|
|
55
|
+
// Bun built-in delegation (runtime-aware — the registry is empty under Node).
|
|
56
|
+
if (runtimeNative.builtins.has(op)) return 'js'
|
|
57
|
+
// Baked proven winners are the default selection.
|
|
58
|
+
const proven = provenImpl(op)
|
|
59
|
+
if (proven === 'native') return 'native'
|
|
60
|
+
if (proven === 'js') return 'js'
|
|
61
|
+
// 'bun' on a non-Bun runtime (no built-in) or unknown ops → addon decision.
|
|
62
|
+
return addon.opImpl(op)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `true` when the Rust addon is the recommended impl for `op`. */
|
|
66
|
+
export const isNativeOp = (op: string): boolean => opImpl(op) === 'native'
|
|
67
|
+
|
|
68
|
+
/** The decision record for `op`, or `null` when unknown. */
|
|
69
|
+
export const opDecision = (op: string): OpDecision | null => {
|
|
70
|
+
const impl = opImpl(op)
|
|
71
|
+
if (impl === null) return null
|
|
72
|
+
return { impl }
|
|
73
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/shared/adaptive.ts — Reusable EWMA adaptive-estimate utility.
|
|
2
|
+
//
|
|
3
|
+
// The loader's cost model (src/loader/cost.ts) learns per-op dispatch costs at
|
|
4
|
+
// runtime with an exponential moving average (EWMA). This is that same
|
|
5
|
+
// machinery, extracted as a tiny reusable class so OTHER subsystems can
|
|
6
|
+
// self-optimize without re-implementing the math — e.g. BufferPool learns the
|
|
7
|
+
// observed request size to pre-size future buffers (`adaptive` option), and
|
|
8
|
+
// consumers can drive their own runtime decisions (batch-vs-loop, delegate
|
|
9
|
+
// vs-native, cache-size) with a bounded, noise-smoothed estimate.
|
|
10
|
+
/** Options for an `AdaptiveEstimate`: EWMA smoothing, bounds, starting value. */ export interface AdaptiveEstimateOptions {
|
|
11
|
+
/**
|
|
12
|
+
* EWMA smoothing factor in (0, 1]. Higher reacts faster to new samples;
|
|
13
|
+
* lower smooths more. Default: 0.25.
|
|
14
|
+
*/
|
|
15
|
+
alpha?: number
|
|
16
|
+
/** Starting estimate. Default 0 (the first sample wins). */
|
|
17
|
+
initial?: number
|
|
18
|
+
/** Clamp the estimate to at least this. Default 0. */
|
|
19
|
+
min?: number
|
|
20
|
+
/** Clamp the estimate to at most this (bounds memory / resources). Default Infinity. */
|
|
21
|
+
max?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A bounded exponential-moving-average estimate of an observed quantity.
|
|
26
|
+
*
|
|
27
|
+
* `sample(x)` folds a new observation into the running estimate; `estimate`
|
|
28
|
+
* reads the current value. The estimate is clamped to `[min, max]`, so it is
|
|
29
|
+
* safe to drive resource sizing (buffer pools, batch thresholds) with it —
|
|
30
|
+
* a single outlier cannot blow up the estimate, and it decays toward recent
|
|
31
|
+
* observations.
|
|
32
|
+
*/
|
|
33
|
+
export class AdaptiveEstimate {
|
|
34
|
+
private readonly alpha: number
|
|
35
|
+
private readonly min: number
|
|
36
|
+
private readonly max: number
|
|
37
|
+
private readonly initial: number
|
|
38
|
+
private value: number
|
|
39
|
+
|
|
40
|
+
constructor(options: AdaptiveEstimateOptions = {}) {
|
|
41
|
+
const alpha = options.alpha ?? 0.25
|
|
42
|
+
if (!Number.isFinite(alpha) || alpha <= 0 || alpha > 1) {
|
|
43
|
+
throw new RangeError(`AdaptiveEstimate: alpha must be in (0,1], got ${alpha}`)
|
|
44
|
+
}
|
|
45
|
+
this.alpha = alpha
|
|
46
|
+
this.min = Math.max(0, options.min ?? 0)
|
|
47
|
+
this.max = options.max ?? Number.POSITIVE_INFINITY
|
|
48
|
+
this.initial = options.initial ?? 0
|
|
49
|
+
this.value = this.initial
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Fold a new observation into the running estimate. */
|
|
53
|
+
sample(x: number): void {
|
|
54
|
+
const v = this.value === 0 ? x : this.value * (1 - this.alpha) + x * this.alpha
|
|
55
|
+
this.value = v < this.min ? this.min : v > this.max ? this.max : v
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Current estimate (clamped to `[min, max]`). */
|
|
59
|
+
get estimate(): number {
|
|
60
|
+
return this.value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Reset the estimate (back to `initial`). */
|
|
64
|
+
reset(): void {
|
|
65
|
+
this.value = this.initial
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// src/shared/buffer-pool.ts — generic reusable byte-buffer pool for hot-path FFI calls.
|
|
2
|
+
//
|
|
3
|
+
// Hot native-FFI paths (e.g. the ingress pipeline) write into large output
|
|
4
|
+
// buffers. Allocating a fresh buffer per call is a measurable cost at high
|
|
5
|
+
// request rates. This pool hands out buffers that are returned to the pool
|
|
6
|
+
// once the consumer is done with them, eliminating steady-state allocation.
|
|
7
|
+
//
|
|
8
|
+
// It is deliberately generic: it works with any `Uint8Array` consumer, is safe
|
|
9
|
+
// for a single-threaded event loop, and supports both eager release (callers
|
|
10
|
+
// that copy out immediately) and in-flight borrowing (zero-copy callers that
|
|
11
|
+
// hold the buffer past the call). Memory is bounded by `maxBuffers` retained
|
|
12
|
+
// buffers; under heavy concurrency, extra temporary buffers are allocated and
|
|
13
|
+
// discarded on release.
|
|
14
|
+
/** Options for configuring a reusable byte-buffer pool. */ export interface BufferPoolOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Initial buffer size in bytes. Default: 131072.
|
|
17
|
+
*/
|
|
18
|
+
initialSize?: number
|
|
19
|
+
/**
|
|
20
|
+
* Upper bound on buffers retained by the pool. When this is reached and no
|
|
21
|
+
* free buffer is large enough, the largest free buffer is grown in place
|
|
22
|
+
* (replaced) so memory stays bounded. If the pool is fully in-flight, a
|
|
23
|
+
* temporary buffer is allocated and discarded on release. Default: 16.
|
|
24
|
+
*/
|
|
25
|
+
maxBuffers?: number
|
|
26
|
+
/**
|
|
27
|
+
* Hard cap on the number of buffers simultaneously in flight (acquired but
|
|
28
|
+
* not yet released). When exceeded, `acquire()` throws a `RangeError` — a
|
|
29
|
+
* backpressure signal for zero-copy responses consumed too slowly. 0
|
|
30
|
+
* (default) is unlimited: when the retained set is exhausted, temporary
|
|
31
|
+
* buffers are allocated and discarded on release (unbounded under a slow
|
|
32
|
+
* zero-copy consumer — set this to bound it).
|
|
33
|
+
*/
|
|
34
|
+
maxInFlight?: number
|
|
35
|
+
/**
|
|
36
|
+
* When true, learn the observed request size (EWMA over `minSize` at each
|
|
37
|
+
* take) and pre-size new buffers to at least the learned estimate (bounded
|
|
38
|
+
* by `maxBuffers` retention). Helps workloads with varying request sizes
|
|
39
|
+
* converge on one buffer size instead of re-growing each time. Default false.
|
|
40
|
+
*/
|
|
41
|
+
adaptive?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A borrowed buffer handle handed out by a pool; `release()` returns it. */
|
|
45
|
+
export interface PooledBuffer {
|
|
46
|
+
/**
|
|
47
|
+
* The underlying buffer. Its byteLength is guaranteed to be at least the
|
|
48
|
+
* `minSize` requested at acquire time.
|
|
49
|
+
*/
|
|
50
|
+
readonly buffer: Uint8Array
|
|
51
|
+
/**
|
|
52
|
+
* Whether this handle has already been returned to the pool.
|
|
53
|
+
*/
|
|
54
|
+
readonly released: boolean
|
|
55
|
+
/**
|
|
56
|
+
* Return the buffer to the pool for reuse. Safe to call more than once;
|
|
57
|
+
* subsequent calls are no-ops.
|
|
58
|
+
*/
|
|
59
|
+
release(): void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Concrete `PooledBuffer` handle.
|
|
64
|
+
*
|
|
65
|
+
* Implemented as a class (not a per-acquire object literal with a getter +
|
|
66
|
+
* closure) so that `acquire()` allocates a single small object instead of an
|
|
67
|
+
* object + getter + closure per request. The hot ingress `run()` path
|
|
68
|
+
* acquires/releases once per request, so this removes one object allocation
|
|
69
|
+
* and one closure allocation from the per-request cost. `release()` lives on
|
|
70
|
+
* the prototype; `pool`/`buffer` are the only per-instance fields.
|
|
71
|
+
*
|
|
72
|
+
* Handles are RECYCLED by the pool (see `BufferPool.freeHandles`): a released
|
|
73
|
+
* handle object is re-pointed at a fresh buffer on the next `acquire()` so the
|
|
74
|
+
* steady state allocates zero handle objects. `buffer` and `released` are the
|
|
75
|
+
* only fields that change across a recycle. Recycling is safe because it only
|
|
76
|
+
* happens after `release()` and every internal + documented usage releases
|
|
77
|
+
* each checkout exactly once; the `released` guard still makes a double
|
|
78
|
+
* release within a single checkout a no-op.
|
|
79
|
+
*/
|
|
80
|
+
class PooledBufferHandle implements PooledBuffer {
|
|
81
|
+
readonly pool: BufferPool
|
|
82
|
+
buffer: Uint8Array
|
|
83
|
+
released = false
|
|
84
|
+
|
|
85
|
+
constructor(pool: BufferPool, buffer: Uint8Array) {
|
|
86
|
+
this.pool = pool
|
|
87
|
+
this.buffer = buffer
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
release(): void {
|
|
91
|
+
if (this.released) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
this.released = true
|
|
95
|
+
this.pool.returnHandle(this)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
import { AdaptiveEstimate } from './adaptive'
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A pool of reusable `Uint8Array` buffers.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* const pool = new BufferPool({ initialSize: 131072 });
|
|
107
|
+
*
|
|
108
|
+
* const handle = pool.acquire(131072);
|
|
109
|
+
* try {
|
|
110
|
+
* // write into handle.buffer ...
|
|
111
|
+
* useBytes(handle.buffer.subarray(0, written));
|
|
112
|
+
* } finally {
|
|
113
|
+
* handle.release();
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export class BufferPool {
|
|
118
|
+
private readonly free: Uint8Array[] = []
|
|
119
|
+
/** Released handle objects available for recycling (bounded — see `maxHandles`). */
|
|
120
|
+
private readonly freeHandles: PooledBufferHandle[] = []
|
|
121
|
+
private readonly initialSize: number
|
|
122
|
+
private readonly maxBuffers: number
|
|
123
|
+
private readonly maxInFlight: number
|
|
124
|
+
/** Upper bound on recycled handle objects retained (peak concurrency). */
|
|
125
|
+
private readonly maxHandles: number
|
|
126
|
+
private readonly estimate: AdaptiveEstimate | null
|
|
127
|
+
private created = 0
|
|
128
|
+
private inFlight = 0
|
|
129
|
+
|
|
130
|
+
constructor(options: BufferPoolOptions = {}) {
|
|
131
|
+
this.initialSize = Math.max(1, Math.floor(options.initialSize ?? 131_072))
|
|
132
|
+
this.maxBuffers = Math.max(1, Math.floor(options.maxBuffers ?? 16))
|
|
133
|
+
this.maxInFlight = Math.max(0, Math.floor(options.maxInFlight ?? 0))
|
|
134
|
+
this.maxHandles = this.maxBuffers + Math.max(1, this.maxInFlight)
|
|
135
|
+
this.estimate =
|
|
136
|
+
options.adaptive === true
|
|
137
|
+
? new AdaptiveEstimate({ alpha: 0.25, min: this.initialSize })
|
|
138
|
+
: null
|
|
139
|
+
this.free.push(new Uint8Array(this.initialSize))
|
|
140
|
+
this.created = 1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Number of buffers currently available for immediate reuse. */
|
|
144
|
+
get freeCount(): number {
|
|
145
|
+
return this.free.length
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Total number of buffers this pool has allocated (monotonic). */
|
|
149
|
+
get createdCount(): number {
|
|
150
|
+
return this.created
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Acquire a buffer of at least `minSize` bytes. The returned handle must be
|
|
155
|
+
* released (directly, or via a zero-copy Response) once the caller is done.
|
|
156
|
+
*
|
|
157
|
+
* @param minSize - Minimum buffer size in bytes. Default: 0 (any free buffer).
|
|
158
|
+
* @throws RangeError when `maxInFlight` is set and that many buffers are
|
|
159
|
+
* already borrowed (never released).
|
|
160
|
+
*/
|
|
161
|
+
acquire(minSize = 0): PooledBuffer {
|
|
162
|
+
if (this.maxInFlight > 0 && this.inFlight >= this.maxInFlight) {
|
|
163
|
+
throw new RangeError(
|
|
164
|
+
`BufferPool: maxInFlight (${this.maxInFlight}) exceeded — too many ` +
|
|
165
|
+
'buffers borrowed at once (unreleased zero-copy responses?). ' +
|
|
166
|
+
'Raise maxInFlight or consume/release responses faster.',
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
const buffer = this.take(Math.max(0, Math.floor(minSize)))
|
|
170
|
+
this.inFlight++
|
|
171
|
+
// Recycle a released handle object in the steady state instead of
|
|
172
|
+
// allocating a fresh one per acquire (removes one per-request object from
|
|
173
|
+
// the hot ingress `run()` path). Safe: handles enter `freeHandles` only
|
|
174
|
+
// after `release()`, and each checkout is released exactly once by every
|
|
175
|
+
// internal + documented usage. `buffer`/`released` are the only fields
|
|
176
|
+
// that change across a recycle.
|
|
177
|
+
const handle = this.freeHandles.pop()
|
|
178
|
+
if (handle !== undefined) {
|
|
179
|
+
handle.buffer = buffer
|
|
180
|
+
handle.released = false
|
|
181
|
+
return handle
|
|
182
|
+
}
|
|
183
|
+
return new PooledBufferHandle(this, buffer)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Return a borrowed handle's buffer to the pool (called by
|
|
188
|
+
* `PooledBufferHandle.release`). Internal to the pool — do not call from
|
|
189
|
+
* outside.
|
|
190
|
+
*/
|
|
191
|
+
returnHandle(handle: PooledBufferHandle): void {
|
|
192
|
+
this.inFlight--
|
|
193
|
+
this.releaseBuffer(handle.buffer)
|
|
194
|
+
// Recycle the handle object for a later acquire, bounding retention so
|
|
195
|
+
// memory stays proportional to concurrency (peak in-flight), not the
|
|
196
|
+
// cumulative number of acquisitions.
|
|
197
|
+
if (this.freeHandles.length < this.maxHandles) {
|
|
198
|
+
this.freeHandles.push(handle)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Take a buffer of at least `minSize` bytes out of circulation. */
|
|
203
|
+
private take(minSize: number): Uint8Array {
|
|
204
|
+
// Learn the observed demand so future allocations pre-size to it.
|
|
205
|
+
this.estimate?.sample(minSize)
|
|
206
|
+
|
|
207
|
+
// 1) Reuse a free buffer that is already large enough.
|
|
208
|
+
for (let i = 0; i < this.free.length; i++) {
|
|
209
|
+
const candidate = this.free[i] as Uint8Array
|
|
210
|
+
if (candidate.byteLength >= minSize) {
|
|
211
|
+
this.free.splice(i, 1)
|
|
212
|
+
return candidate
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const target = this.nextSize(minSize)
|
|
217
|
+
|
|
218
|
+
// 2) No free buffer is large enough: grow the largest free buffer in place
|
|
219
|
+
// to keep the number of retained buffers bounded.
|
|
220
|
+
let largestBuf: Uint8Array | null = null
|
|
221
|
+
let largest = -1
|
|
222
|
+
for (let i = 0; i < this.free.length; i++) {
|
|
223
|
+
const candidate = this.free[i] as Uint8Array
|
|
224
|
+
if (largestBuf === null || candidate.byteLength > largestBuf.byteLength) {
|
|
225
|
+
largestBuf = candidate
|
|
226
|
+
largest = i
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (largestBuf !== null) {
|
|
230
|
+
this.free.splice(largest, 1)
|
|
231
|
+
this.created++
|
|
232
|
+
return new Uint8Array(target)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 3) Pool exhausted (all buffers in flight): allocate a fresh buffer.
|
|
236
|
+
this.created++
|
|
237
|
+
return new Uint8Array(target)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Return a buffer to the free list, bounding total retained memory. */
|
|
241
|
+
private releaseBuffer(buffer: Uint8Array): void {
|
|
242
|
+
if (this.free.length >= this.maxBuffers) {
|
|
243
|
+
// Free list is full: drop the smallest retained buffer to stay bounded.
|
|
244
|
+
let smallestBuf: Uint8Array | null = null
|
|
245
|
+
let smallest = 0
|
|
246
|
+
for (let i = 0; i < this.free.length; i++) {
|
|
247
|
+
const candidate = this.free[i] as Uint8Array
|
|
248
|
+
if (smallestBuf === null || candidate.byteLength < smallestBuf.byteLength) {
|
|
249
|
+
smallestBuf = candidate
|
|
250
|
+
smallest = i
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
this.free.splice(smallest, 1)
|
|
254
|
+
}
|
|
255
|
+
this.free.push(buffer)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private nextSize(minSize: number): number {
|
|
259
|
+
const floor = this.estimate
|
|
260
|
+
? Math.max(this.initialSize, Math.ceil(this.estimate.estimate))
|
|
261
|
+
: this.initialSize
|
|
262
|
+
return Math.max(floor, minSize)
|
|
263
|
+
}
|
|
264
|
+
}
|