castrum 0.7.0 → 0.9.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/LICENSE +21 -0
- package/README.md +322 -59
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +3161 -0
- package/dist/index.js +7306 -0
- package/docs/ARCHITECTURE.md +415 -0
- package/docs/BENCHMARKS.md +307 -0
- package/docs/ENVIRONMENT.md +79 -0
- package/docs/GETTING_STARTED.md +209 -0
- package/docs/INGRESS.md +745 -0
- package/docs/REPO_MAP.md +259 -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 +42 -0
- package/docs/bun-builtins-decision-matrix.md +108 -0
- package/index.ts +51 -8
- package/package.json +73 -20
- package/src/data/json-rows.ts +13 -13
- package/src/ingress/body.ts +149 -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 +563 -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 +67 -0
- package/src/ingress/index.ts +130 -405
- package/src/ingress/metrics.ts +132 -0
- package/src/ingress/options.ts +197 -0
- package/src/ingress/packing/gather-raw-headers.ts +75 -0
- package/src/ingress/packing/header-packing.ts +75 -0
- package/src/ingress/packing/input-packer.ts +150 -0
- package/src/ingress/packing/scratch.ts +50 -0
- package/src/ingress/packing/select-headers.ts +96 -0
- package/src/ingress/response/error-bodies.ts +75 -0
- package/src/ingress/response/terminal.ts +59 -0
- package/src/ingress/routes/common.ts +38 -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 +37 -0
- package/src/ingress/routes/index.ts +10 -0
- package/src/ingress/routes/json-write.ts +153 -0
- package/src/ingress/routes/options.ts +36 -0
- package/src/ingress/routes/read.ts +45 -0
- package/src/ingress/server-node.ts +376 -0
- package/src/ingress/server.ts +443 -0
- package/src/ingress/shared.ts +132 -0
- package/src/ingress/status.ts +97 -0
- package/src/ingress/types.ts +157 -0
- package/src/integration/batch.ts +58 -0
- package/src/integration/index.ts +29 -0
- package/src/integration/pipeline.ts +217 -0
- package/src/integration/streaming.ts +79 -0
- package/src/integration/websocket.ts +92 -0
- package/src/loader/batch.ts +133 -0
- package/src/loader/cost.ts +104 -0
- package/src/loader/index.ts +930 -0
- package/src/loader/ops.ts +657 -0
- package/src/native/ffi.ts +1541 -0
- package/src/native/index.ts +8 -207
- package/src/native/loader.ts +194 -0
- package/src/native/types.ts +598 -0
- package/src/rust-ffi/addon.ts +10 -0
- package/src/rust-ffi/batch.ts +431 -0
- package/src/rust-ffi/client.ts +88 -0
- package/src/rust-ffi/context.ts +164 -0
- package/src/rust-ffi/index.ts +40 -541
- package/src/rust-ffi/options.ts +113 -0
- package/src/rust-ffi/packed.ts +253 -0
- package/src/rust-ffi/proven.ts +42 -0
- package/src/rust-ffi/scalar/crypto.ts +265 -0
- package/src/rust-ffi/scalar/factories.ts +233 -0
- package/src/rust-ffi/scalar/hashing.ts +72 -0
- package/src/rust-ffi/scalar/http.ts +206 -0
- package/src/rust-ffi/scalar/index.ts +31 -0
- package/src/rust-ffi/scalar/interface.ts +450 -0
- package/src/rust-ffi/scalar/json.ts +45 -0
- package/src/rust-ffi/scalar/payload.ts +124 -0
- package/src/rust-ffi/text.ts +95 -0
- package/src/selection.json +194 -0
- package/src/selection.ts +77 -0
- package/src/shared/adaptive.ts +68 -0
- package/src/shared/bench-classify.ts +209 -0
- package/src/shared/buffer-pool.ts +209 -0
- package/src/shared/bytes.ts +35 -5
- package/src/shared/env.ts +20 -0
- package/src/shared/log.ts +74 -0
- package/src/shared/metrics.ts +247 -0
- package/src/shared/packed.ts +350 -154
- package/src/shared/proven.ts +596 -0
- package/src/shared/request-id.ts +67 -0
- package/src/shared/response.ts +104 -0
- package/src/shared/runtime.ts +34 -0
- package/src/shared/trace.ts +81 -0
- package/src/shared/uuid.ts +31 -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/ingress/packed-input.ts +0 -59
- package/src/shared/json.ts +0 -18
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/rust-ffi/text.ts — String-oriented FFI namespace (text in → text out).
|
|
2
|
+
//
|
|
3
|
+
// `rust.text.*` is the ergonomic string API over the byte-level native
|
|
4
|
+
// functions. Pure wrappers: encode input, call native, decode output.
|
|
5
|
+
|
|
6
|
+
import { decoder, encoder } from '../shared/bytes'
|
|
7
|
+
import { isBun } from '../shared/runtime'
|
|
8
|
+
import { resolveNative, type RustClientContext } from './context'
|
|
9
|
+
|
|
10
|
+
/** String-oriented FFI namespace. */
|
|
11
|
+
export interface RustText {
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a lowercase file extension to a MIME type string (e.g. `"html"` →
|
|
14
|
+
* `"text/html"`). Unknown extensions return `"application/octet-stream"`.
|
|
15
|
+
* @param ext the file extension WITHOUT a leading dot, case-insensitive.
|
|
16
|
+
* @returns the MIME type for `ext`.
|
|
17
|
+
*/
|
|
18
|
+
mimeFromExtension(ext: string): string
|
|
19
|
+
/**
|
|
20
|
+
* RFC 3986 percent-encode a string (the `encodeURIComponent` unreserved
|
|
21
|
+
* set). Under Bun this delegates to the native `encodeURIComponent` for
|
|
22
|
+
* ~4x speed with byte-for-byte parity.
|
|
23
|
+
* @param input the string to encode.
|
|
24
|
+
* @returns the percent-encoded string.
|
|
25
|
+
*/
|
|
26
|
+
urlEncode(input: string): string
|
|
27
|
+
/**
|
|
28
|
+
* Percent-decode a string. Throws on malformed `%XX` or invalid UTF-8
|
|
29
|
+
* output (parity with `decodeURIComponent`, which is used under Bun).
|
|
30
|
+
* @param input the percent-encoded string to decode.
|
|
31
|
+
* @returns the decoded string.
|
|
32
|
+
*/
|
|
33
|
+
urlDecode(input: string): string
|
|
34
|
+
/**
|
|
35
|
+
* Compute the RFC 6455 `Sec-WebSocket-Accept` value for a client key.
|
|
36
|
+
* @param key the base64 `Sec-WebSocket-Key` header value.
|
|
37
|
+
* @returns the 28-byte base64 accept value.
|
|
38
|
+
*/
|
|
39
|
+
wsAcceptKey(key: string): string
|
|
40
|
+
/** @param input an email address. @returns whether it is RFC-valid. */
|
|
41
|
+
validateEmail(input: string): boolean
|
|
42
|
+
/** @param input a UUID string. @returns whether it is a valid (v1-v8) UUID. */
|
|
43
|
+
validateUuid(input: string): boolean
|
|
44
|
+
/** @param input an IPv4 address. @returns whether it is valid. */
|
|
45
|
+
validateIpv4(input: string): boolean
|
|
46
|
+
/** @param input an IPv6 address. @returns whether it is valid. */
|
|
47
|
+
validateIpv6(input: string): boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Build the `text` namespace for a client context. */
|
|
51
|
+
export function buildText(ctx: RustClientContext): RustText {
|
|
52
|
+
// String memo: the byte path already caches the native bytes; this avoids the
|
|
53
|
+
// encode → native lookup → slice → decode round-trip for repeated extension
|
|
54
|
+
// strings (the common case) — 0 allocations on a cache hit.
|
|
55
|
+
const mimeStrCache = new Map<string, string>()
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
mimeFromExtension(ext) {
|
|
59
|
+
let mime = mimeStrCache.get(ext)
|
|
60
|
+
if (mime === undefined) {
|
|
61
|
+
mime = decoder.decode(ctx.cachedMime(encoder.encode(ext)))
|
|
62
|
+
mimeStrCache.set(ext, mime)
|
|
63
|
+
}
|
|
64
|
+
return mime
|
|
65
|
+
},
|
|
66
|
+
urlEncode(input) {
|
|
67
|
+
// Bun's native encodeURIComponent beats the rust+FFI/NAPI string crossing
|
|
68
|
+
// (~4x, measured) with byte-for-byte parity (RFC 3986 unreserved set) —
|
|
69
|
+
// see docs/bun-builtins-decision-matrix.md.
|
|
70
|
+
if (isBun()) return encodeURIComponent(input)
|
|
71
|
+
return resolveNative(ctx, 'urlEncodeStr')(input) as string
|
|
72
|
+
},
|
|
73
|
+
urlDecode(input) {
|
|
74
|
+
// decodeURIComponent matches the rust decoder on valid input and also
|
|
75
|
+
// throws on malformed/invalid UTF-8 (URIError, an Error subclass).
|
|
76
|
+
if (isBun()) return decodeURIComponent(input)
|
|
77
|
+
return resolveNative(ctx, 'urlDecodeStr')(input) as string
|
|
78
|
+
},
|
|
79
|
+
wsAcceptKey(key) {
|
|
80
|
+
return decoder.decode(resolveNative(ctx, 'wsAcceptKey')(encoder.encode(key)) as Uint8Array)
|
|
81
|
+
},
|
|
82
|
+
validateEmail(input) {
|
|
83
|
+
return resolveNative(ctx, 'validateEmail')(encoder.encode(input)) as boolean
|
|
84
|
+
},
|
|
85
|
+
validateUuid(input) {
|
|
86
|
+
return resolveNative(ctx, 'validateUuid')(encoder.encode(input)) as boolean
|
|
87
|
+
},
|
|
88
|
+
validateIpv4(input) {
|
|
89
|
+
return resolveNative(ctx, 'validateIpv4')(encoder.encode(input)) as boolean
|
|
90
|
+
},
|
|
91
|
+
validateIpv6(input) {
|
|
92
|
+
return resolveNative(ctx, 'validateIpv6')(encoder.encode(input)) as boolean
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -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,77 @@
|
|
|
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
|
+
import { lazyAddon, getAddon } from './native'
|
|
13
|
+
import { isBun } from './shared/runtime'
|
|
14
|
+
|
|
15
|
+
/** Recommended implementation for an operation. */
|
|
16
|
+
export type OpImpl = 'native' | 'js'
|
|
17
|
+
|
|
18
|
+
export interface OpDecision {
|
|
19
|
+
/** The recommended implementation when the addon is available. */
|
|
20
|
+
readonly impl: OpImpl
|
|
21
|
+
/** Why this decision (measured ratio / rationale). */
|
|
22
|
+
readonly note?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ops where Bun's NATIVE built-in beats the Rust addon (measured in
|
|
27
|
+
* `docs/bun-builtins-decision-matrix.md`: `Bun.gzipSync` ~2.0x, `Bun.hash.crc32`
|
|
28
|
+
* 2.8–8.4x, `Bun.CryptoHasher` ~1.2x, `Bun.hash.xxHash3` ~4.2x, and
|
|
29
|
+
* `crypto.getRandomValues` for token-sized random). Under Bun the fastest
|
|
30
|
+
* choice for these is the pure-JS path that DELEGATES to the Bun built-in
|
|
31
|
+
* (`"js"`), not the Rust addon — so we never ship something slower than what
|
|
32
|
+
* Bun natively provides. Under Node the base benchmark decision stands (Rust
|
|
33
|
+
* wins for gzip/random/hmac there).
|
|
34
|
+
*
|
|
35
|
+
* NOTE: `gzipDecompress` is deliberately NOT in this set — the rust surface
|
|
36
|
+
* keeps the native path (with its 64 MiB decompression-bomb cap) even under
|
|
37
|
+
* Bun, because `Bun.gunzipSync` has no output-size bound.
|
|
38
|
+
*/
|
|
39
|
+
const BUN_WINS = new Set([
|
|
40
|
+
'gzipCompress',
|
|
41
|
+
'crc32',
|
|
42
|
+
'randomToken',
|
|
43
|
+
'hmacSha256',
|
|
44
|
+
'xxh3',
|
|
45
|
+
'urlEncode',
|
|
46
|
+
'urlDecode',
|
|
47
|
+
'base64Encode',
|
|
48
|
+
'httpDate',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
// Lazy: importing this module does NOT dlopen the addon; `opImpl` reads the
|
|
52
|
+
// baked decision from Rust on first call.
|
|
53
|
+
const addon = lazyAddon(getAddon)
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The benchmark-driven native-vs-JS decision for `op` — runtime-aware.
|
|
57
|
+
*
|
|
58
|
+
* Returns `"native"` when the Rust addon is the recommended implementation,
|
|
59
|
+
* `"js"` when a pure-JS (or Bun built-in) implementation wins, or `null` for
|
|
60
|
+
* unknown ops (or when the addon is unavailable — with no addon there is no
|
|
61
|
+
* "native" choice). Under Bun, ops in {@link BUN_WINS} resolve to `"js"` so the
|
|
62
|
+
* consumer's JS path delegates to the faster Bun built-in.
|
|
63
|
+
*/
|
|
64
|
+
export const opImpl = (op: string): OpImpl | null => {
|
|
65
|
+
if (isBun() && BUN_WINS.has(op)) return 'js'
|
|
66
|
+
return addon.opImpl(op)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `true` when the Rust addon is the recommended impl for `op`. */
|
|
70
|
+
export const isNativeOp = (op: string): boolean => opImpl(op) === 'native'
|
|
71
|
+
|
|
72
|
+
/** The decision record for `op`, or `null` when unknown. */
|
|
73
|
+
export const opDecision = (op: string): OpDecision | null => {
|
|
74
|
+
const impl = opImpl(op)
|
|
75
|
+
if (impl === null) return null
|
|
76
|
+
return { impl }
|
|
77
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
|
|
11
|
+
export interface AdaptiveEstimateOptions {
|
|
12
|
+
/**
|
|
13
|
+
* EWMA smoothing factor in (0, 1]. Higher reacts faster to new samples;
|
|
14
|
+
* lower smooths more. Default: 0.25.
|
|
15
|
+
*/
|
|
16
|
+
alpha?: number
|
|
17
|
+
/** Starting estimate. Default 0 (the first sample wins). */
|
|
18
|
+
initial?: number
|
|
19
|
+
/** Clamp the estimate to at least this. Default 0. */
|
|
20
|
+
min?: number
|
|
21
|
+
/** Clamp the estimate to at most this (bounds memory / resources). Default Infinity. */
|
|
22
|
+
max?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A bounded exponential-moving-average estimate of an observed quantity.
|
|
27
|
+
*
|
|
28
|
+
* `sample(x)` folds a new observation into the running estimate; `estimate`
|
|
29
|
+
* reads the current value. The estimate is clamped to `[min, max]`, so it is
|
|
30
|
+
* safe to drive resource sizing (buffer pools, batch thresholds) with it —
|
|
31
|
+
* a single outlier cannot blow up the estimate, and it decays toward recent
|
|
32
|
+
* observations.
|
|
33
|
+
*/
|
|
34
|
+
export class AdaptiveEstimate {
|
|
35
|
+
private readonly alpha: number
|
|
36
|
+
private readonly min: number
|
|
37
|
+
private readonly max: number
|
|
38
|
+
private readonly initial: number
|
|
39
|
+
private value: number
|
|
40
|
+
|
|
41
|
+
constructor(options: AdaptiveEstimateOptions = {}) {
|
|
42
|
+
const alpha = options.alpha ?? 0.25
|
|
43
|
+
if (!Number.isFinite(alpha) || alpha <= 0 || alpha > 1) {
|
|
44
|
+
throw new RangeError(`AdaptiveEstimate: alpha must be in (0,1], got ${alpha}`)
|
|
45
|
+
}
|
|
46
|
+
this.alpha = alpha
|
|
47
|
+
this.min = Math.max(0, options.min ?? 0)
|
|
48
|
+
this.max = options.max ?? Number.POSITIVE_INFINITY
|
|
49
|
+
this.initial = options.initial ?? 0
|
|
50
|
+
this.value = this.initial
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Fold a new observation into the running estimate. */
|
|
54
|
+
sample(x: number): void {
|
|
55
|
+
const v = this.value === 0 ? x : this.value * (1 - this.alpha) + x * this.alpha
|
|
56
|
+
this.value = v < this.min ? this.min : v > this.max ? this.max : v
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Current estimate (clamped to `[min, max]`). */
|
|
60
|
+
get estimate(): number {
|
|
61
|
+
return this.value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Reset the estimate (back to `initial`). */
|
|
65
|
+
reset(): void {
|
|
66
|
+
this.value = this.initial
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/shared/bench-classify.ts — Pure benchmark classification for PROVEN_SURFACE.
|
|
3
|
+
*
|
|
4
|
+
* Given the CPU report's `comparisons` array and the static PROVEN_SURFACE
|
|
5
|
+
* registry, compute per-function live classification: aggregate all of a
|
|
6
|
+
* function's benchmark tasks (the canonical comparison plus its `_`-suffixed
|
|
7
|
+
* variants that no other registry entry claims), score each task pass/fail,
|
|
8
|
+
* and derive:
|
|
9
|
+
*
|
|
10
|
+
* - `live` status purely from the latest report (majority-fail rule)
|
|
11
|
+
* - `effective` hybrid status the JSDoc annotator and the audit use:
|
|
12
|
+
* a clear MAJORITY of task losses overrides the static
|
|
13
|
+
* status and auto-deprecates the function, while a single
|
|
14
|
+
* good run never auto-promotes a "not-competitive" function
|
|
15
|
+
* (static classifications reflect the shipped release build).
|
|
16
|
+
* - `promotable` static "not-competitive" but live majority WIN (ratio >= 1)
|
|
17
|
+
* - `won`/`failed`/`total` — the score embedded in the generated JSDoc.
|
|
18
|
+
*
|
|
19
|
+
* Task bands: won = ratio >= 1 (rust at least as fast), failed = ratio <
|
|
20
|
+
* 1/LOSS_TOLERANCE (clear loss), middle = tie (counts toward neither).
|
|
21
|
+
*
|
|
22
|
+
* PURE DATA — imports only the registry (no addon, no `rust` client), so it is
|
|
23
|
+
* unit-testable and shared by scripts/annotate-performance.ts and
|
|
24
|
+
* scripts/check-proven.ts (one source of truth for classification).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { PROVEN_SURFACE, type PerformanceStatus, type ProvenEntry } from './proven'
|
|
28
|
+
|
|
29
|
+
/** A "proven"/"parity" function whose baseline is more than this much faster is a clear loss. */
|
|
30
|
+
export const LOSS_TOLERANCE = 1.35
|
|
31
|
+
/** A "not-competitive" function that wins by more than this is a promotion candidate. */
|
|
32
|
+
export const PROMOTE_TOLERANCE = 1.5
|
|
33
|
+
|
|
34
|
+
/** One measured rust-vs-baseline comparison from the CPU report. */
|
|
35
|
+
export interface Comparison {
|
|
36
|
+
label: string
|
|
37
|
+
nativeName: string
|
|
38
|
+
rustName: string
|
|
39
|
+
nativeAvgMs: number
|
|
40
|
+
rustAvgMs: number
|
|
41
|
+
ratio: number
|
|
42
|
+
faster: 'rust' | 'native'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pass/fail outcome for one of a function's benchmark tasks. */
|
|
46
|
+
export interface TaskOutcome {
|
|
47
|
+
rustName: string
|
|
48
|
+
label: string
|
|
49
|
+
ratio: number
|
|
50
|
+
/** True when rust is at least as fast (ratio >= 1) — a genuine win. */
|
|
51
|
+
won: boolean
|
|
52
|
+
/** True when rust is a CLEAR loss (ratio < 1 / LOSS_TOLERANCE) — the deprecation signal. */
|
|
53
|
+
failed: boolean
|
|
54
|
+
/** False for the registry's canonical rustTask; true for aggregated variants. */
|
|
55
|
+
isVariant: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ClassifiedEntry {
|
|
59
|
+
entry: ProvenEntry
|
|
60
|
+
tasks: TaskOutcome[]
|
|
61
|
+
total: number
|
|
62
|
+
/** Tasks where rust is at least as fast (ratio >= 1). */
|
|
63
|
+
won: number
|
|
64
|
+
/** Tasks where rust is a clear loss (ratio < 1 / LOSS_TOLERANCE). */
|
|
65
|
+
failed: number
|
|
66
|
+
/** True when a clear majority of the function's benchmarks lose (failed > total/2). */
|
|
67
|
+
majorityFail: boolean
|
|
68
|
+
/** True when a clear majority of the function's benchmarks win (won > total/2). */
|
|
69
|
+
majorityWin: boolean
|
|
70
|
+
/**
|
|
71
|
+
* Ratio to display in JSDoc: the worst task when majority-failing (honest
|
|
72
|
+
* "this loses" number), otherwise the canonical comparison, else the median.
|
|
73
|
+
*/
|
|
74
|
+
ratio: number | undefined
|
|
75
|
+
/** Status derived purely from the latest report. */
|
|
76
|
+
live: PerformanceStatus
|
|
77
|
+
/** Hybrid status the annotator/audit should act on (majority-fail overrides static). */
|
|
78
|
+
effective: PerformanceStatus
|
|
79
|
+
/** Live results contradict the static classification (majority fail on a proven/parity fn). */
|
|
80
|
+
drifted: boolean
|
|
81
|
+
/** Static "not-competitive" that now majority-wins (keep deprecated, add a note). */
|
|
82
|
+
promotable: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function median(values: number[]): number | undefined {
|
|
86
|
+
if (values.length === 0) return undefined
|
|
87
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
88
|
+
const mid = Math.floor(sorted.length / 2)
|
|
89
|
+
if (sorted.length % 2 === 1) return sorted[mid] ?? 0
|
|
90
|
+
return ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Every rustTask claimed by any registry entry (prevents cross-entry variant theft). */
|
|
94
|
+
function defaultClaimed(): Set<string> {
|
|
95
|
+
return new Set(PROVEN_SURFACE.map((e) => e.rustTask).filter(Boolean))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Classify a single registry entry against the report's comparisons.
|
|
100
|
+
*
|
|
101
|
+
* `claimedRustTasks` is the set of rustTasks owned by OTHER registry entries;
|
|
102
|
+
* a variant comparison is aggregated only when no other entry claims it (e.g.
|
|
103
|
+
* `rust:jwt_sign_bytes` belongs to `jwtSignBytes`, not `jwtSign`).
|
|
104
|
+
*/
|
|
105
|
+
export function classifyEntry(
|
|
106
|
+
entry: ProvenEntry,
|
|
107
|
+
comparisons: Comparison[],
|
|
108
|
+
claimedRustTasks: ReadonlySet<string> = defaultClaimed(),
|
|
109
|
+
): ClassifiedEntry {
|
|
110
|
+
const unmeasured: ClassifiedEntry = {
|
|
111
|
+
entry,
|
|
112
|
+
tasks: [],
|
|
113
|
+
total: 0,
|
|
114
|
+
won: 0,
|
|
115
|
+
failed: 0,
|
|
116
|
+
majorityFail: false,
|
|
117
|
+
majorityWin: false,
|
|
118
|
+
ratio: undefined,
|
|
119
|
+
live: 'unmeasured',
|
|
120
|
+
effective: 'unmeasured',
|
|
121
|
+
drifted: false,
|
|
122
|
+
promotable: false,
|
|
123
|
+
}
|
|
124
|
+
if (!entry.rustTask) return unmeasured
|
|
125
|
+
|
|
126
|
+
const tasks: TaskOutcome[] = []
|
|
127
|
+
for (const c of comparisons) {
|
|
128
|
+
const isCanonical = c.rustName === entry.rustTask
|
|
129
|
+
const isVariant =
|
|
130
|
+
!isCanonical &&
|
|
131
|
+
!claimedRustTasks.has(c.rustName) &&
|
|
132
|
+
c.rustName.startsWith(`${entry.rustTask}_`)
|
|
133
|
+
if (isCanonical || isVariant) {
|
|
134
|
+
// Three-band classification (honest, noise-resistant): a task is WON when
|
|
135
|
+
// rust is at least as fast (ratio >= 1), FAILED when rust clearly loses
|
|
136
|
+
// (ratio < 1 / LOSS_TOLERANCE), and a TIE in between. The middle band
|
|
137
|
+
// counts toward neither majority, so marginal losses can never produce a
|
|
138
|
+
// false win (e.g. a stale PROMOTABLE flag for a function that never
|
|
139
|
+
// actually beat its baseline).
|
|
140
|
+
tasks.push({
|
|
141
|
+
rustName: c.rustName,
|
|
142
|
+
label: c.label,
|
|
143
|
+
ratio: c.ratio,
|
|
144
|
+
won: c.ratio >= 1,
|
|
145
|
+
failed: c.ratio < 1 / LOSS_TOLERANCE,
|
|
146
|
+
isVariant: !isCanonical,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const total = tasks.length
|
|
152
|
+
if (total === 0) return unmeasured
|
|
153
|
+
|
|
154
|
+
const won = tasks.filter((t) => t.won).length
|
|
155
|
+
const failed = tasks.filter((t) => t.failed).length
|
|
156
|
+
const majorityFail = failed * 2 > total
|
|
157
|
+
const majorityWin = won * 2 > total
|
|
158
|
+
|
|
159
|
+
const canonical = tasks.find((t) => !t.isVariant)
|
|
160
|
+
const ratio = majorityFail
|
|
161
|
+
? Math.min(...tasks.map((t) => t.ratio))
|
|
162
|
+
: (canonical?.ratio ?? median(tasks.map((t) => t.ratio)))
|
|
163
|
+
|
|
164
|
+
const live: PerformanceStatus = majorityFail
|
|
165
|
+
? 'not-competitive'
|
|
166
|
+
: majorityWin
|
|
167
|
+
? 'proven'
|
|
168
|
+
: 'parity'
|
|
169
|
+
|
|
170
|
+
// Hybrid effective status: a clear majority of losses overrides the static
|
|
171
|
+
// registry (auto-deprecate); otherwise the static classification stands —
|
|
172
|
+
// classifications reflect the shipped release build, not one local run.
|
|
173
|
+
const effective: PerformanceStatus =
|
|
174
|
+
entry.status !== 'unmeasured' && majorityFail ? 'not-competitive' : entry.status
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
entry,
|
|
178
|
+
tasks,
|
|
179
|
+
total,
|
|
180
|
+
won,
|
|
181
|
+
failed,
|
|
182
|
+
majorityFail,
|
|
183
|
+
majorityWin,
|
|
184
|
+
ratio,
|
|
185
|
+
live,
|
|
186
|
+
effective,
|
|
187
|
+
drifted: entry.status !== 'unmeasured' && effective !== entry.status,
|
|
188
|
+
promotable: entry.status === 'not-competitive' && majorityWin,
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Classify every registry entry (single source of truth for the audit + annotator). */
|
|
193
|
+
export function classifySurface(
|
|
194
|
+
entries: readonly ProvenEntry[],
|
|
195
|
+
comparisons: Comparison[],
|
|
196
|
+
): Map<string, ClassifiedEntry> {
|
|
197
|
+
const claimed = new Set(entries.map((e) => e.rustTask).filter(Boolean))
|
|
198
|
+
const out = new Map<string, ClassifiedEntry>()
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
out.set(entry.name, classifyEntry(entry, comparisons, claimed))
|
|
201
|
+
}
|
|
202
|
+
return out
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Human ratio summary: "3.18x rust" / "2.34x baseline". */
|
|
206
|
+
export function formatRatio(ratio: number): string {
|
|
207
|
+
if (ratio >= 1) return `${ratio.toFixed(2)}x rust`
|
|
208
|
+
return `${(1 / ratio).toFixed(2)}x baseline`
|
|
209
|
+
}
|