knitting 0.1.51 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -54
- package/knitting.d.ts +1 -0
- package/map.md +15 -3
- package/package.json +14 -2
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/scripts/build-native-addons.ts +5 -0
- package/src/api.d.ts +29 -16
- package/src/api.js +98 -28
- package/src/common/envelope.d.ts +9 -3
- package/src/common/envelope.js +14 -0
- package/src/common/worker-runtime.d.ts +2 -0
- package/src/common/worker-runtime.js +9 -0
- package/src/connections/buffer-reference-native.d.ts +56 -0
- package/src/connections/buffer-reference-native.js +217 -0
- package/src/connections/buffer-reference.d.ts +78 -0
- package/src/connections/buffer-reference.js +461 -0
- package/src/connections/index.d.ts +1 -0
- package/src/connections/index.js +1 -0
- package/src/connections/node-addons.d.ts +1 -1
- package/src/connections/node-buffer-pointer.d.ts +20 -0
- package/src/connections/node-buffer-pointer.js +16 -0
- package/src/connections/process-shared-buffer.d.ts +6 -0
- package/src/connections/process-shared-buffer.js +6 -0
- package/src/connections/shared-array-buffer-payload.d.ts +36 -0
- package/src/connections/shared-array-buffer-payload.js +235 -0
- package/src/debug/env-diff.d.ts +26 -0
- package/src/debug/env-diff.js +49 -0
- package/src/debug/gate.d.ts +18 -0
- package/src/debug/gate.js +69 -0
- package/src/debug/handle.d.ts +23 -0
- package/src/debug/handle.js +48 -0
- package/src/ipc/transport/shared-memory.d.ts +1 -3
- package/src/knitting_buffer_pointer.cc +425 -0
- package/src/memory/lock.d.ts +12 -1
- package/src/memory/lock.js +47 -4
- package/src/memory/payload-config.d.ts +9 -0
- package/src/memory/payloadCodec.js +220 -37
- package/src/permission/protocol.d.ts +1 -1
- package/src/permission/protocol.js +30 -20
- package/src/runtime/pool.d.ts +3 -5
- package/src/runtime/pool.js +18 -18
- package/src/runtime/process-worker.js +5 -2
- package/src/runtime/tx-queue.d.ts +3 -2
- package/src/runtime/tx-queue.js +18 -13
- package/src/types.d.ts +54 -50
- package/src/utils/http.d.ts +29 -0
- package/src/utils/http.js +100 -0
- package/src/worker/loop.js +76 -14
- package/src/worker/rx-queue.d.ts +4 -1
- package/src/worker/rx-queue.js +53 -4
- package/src/worker/safety/startup.d.ts +2 -3
- package/src/worker/safety/startup.js +1 -4
- package/src/worker/timers.js +7 -2
- package/unsafe.d.ts +1 -0
- package/unsafe.js +1 -0
- package/utils.d.ts +1 -0
- package/utils.js +1 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { RUNTIME } from "../common/runtime.js";
|
|
2
|
+
import { getNodeProcess } from "../common/node-compat.js";
|
|
3
|
+
import { getBufferReferenceCapabilities } from "./buffer-reference-native.js";
|
|
4
|
+
// Thread-worker SharedArrayBuffer transport by process-local pointer.
|
|
5
|
+
// SABs are shared, not moved or detached; process workers reject them.
|
|
6
|
+
export const SHARED_ARRAY_BUFFER_CODEC_ID = "knitting.sharedArrayBuffer";
|
|
7
|
+
export const SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER = Symbol.for("knitting.sharedArrayBuffer.numericTransfer");
|
|
8
|
+
export const SHARED_ARRAY_BUFFER_NUMERIC_WORDS = 8;
|
|
9
|
+
const SHARED_ARRAY_BUFFER_TOKEN_NUMERIC_WORDS = 2;
|
|
10
|
+
const EXTERNAL_PAYLOAD_BRAND = Symbol.for("knitting.payloadCodec");
|
|
11
|
+
const getProcessId = () => {
|
|
12
|
+
const proc = getNodeProcess();
|
|
13
|
+
if (proc !== undefined && typeof proc.pid === "number")
|
|
14
|
+
return proc.pid;
|
|
15
|
+
const deno = globalThis.Deno;
|
|
16
|
+
if (typeof deno?.pid === "number")
|
|
17
|
+
return deno.pid;
|
|
18
|
+
return 0;
|
|
19
|
+
};
|
|
20
|
+
const PROCESS_ORIGIN = `${RUNTIME}:${getProcessId()}`;
|
|
21
|
+
const hasSharedArrayBuffer = typeof SharedArrayBuffer === "function";
|
|
22
|
+
export const isSharedArrayBufferValue = (value) => hasSharedArrayBuffer && value instanceof SharedArrayBuffer;
|
|
23
|
+
const pinnedBySab = new WeakMap();
|
|
24
|
+
const payloadBySharedBuffer = new WeakMap();
|
|
25
|
+
const warmedTokensByTransport = new WeakMap();
|
|
26
|
+
const cachedSharedBuffersByToken = new Map();
|
|
27
|
+
const pinFinalizer = typeof FinalizationRegistry === "function"
|
|
28
|
+
? new FinalizationRegistry((token) => {
|
|
29
|
+
try {
|
|
30
|
+
getBufferReferenceCapabilities().releaseShared(token);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// best effort
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
: undefined;
|
|
37
|
+
const splitU64 = (value) => [
|
|
38
|
+
Number(value & 0xffffffffn) >>> 0,
|
|
39
|
+
Number((value >> 32n) & 0xffffffffn) >>> 0,
|
|
40
|
+
];
|
|
41
|
+
const joinU64 = (low, high) => (BigInt(high >>> 0) << 32n) | BigInt(low >>> 0);
|
|
42
|
+
const encodeRuntime = (runtime) => {
|
|
43
|
+
switch (runtime) {
|
|
44
|
+
case "node":
|
|
45
|
+
return 1;
|
|
46
|
+
case "deno":
|
|
47
|
+
return 2;
|
|
48
|
+
case "bun":
|
|
49
|
+
return 3;
|
|
50
|
+
default:
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const decodeRuntime = (value) => {
|
|
55
|
+
switch (value) {
|
|
56
|
+
case 1:
|
|
57
|
+
return "node";
|
|
58
|
+
case 2:
|
|
59
|
+
return "deno";
|
|
60
|
+
case 3:
|
|
61
|
+
return "bun";
|
|
62
|
+
default:
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const getWarmTokens = (transportKey) => {
|
|
67
|
+
if (transportKey === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
let warmTokens = warmedTokensByTransport.get(transportKey);
|
|
70
|
+
if (warmTokens === undefined) {
|
|
71
|
+
warmTokens = new Set();
|
|
72
|
+
warmedTokensByTransport.set(transportKey, warmTokens);
|
|
73
|
+
}
|
|
74
|
+
return warmTokens;
|
|
75
|
+
};
|
|
76
|
+
const pinSab = (sab) => {
|
|
77
|
+
let pin = pinnedBySab.get(sab);
|
|
78
|
+
if (pin === undefined) {
|
|
79
|
+
const produced = getBufferReferenceCapabilities().produceShared(sab);
|
|
80
|
+
pin = {
|
|
81
|
+
token: produced.token,
|
|
82
|
+
pointer: produced.pointer,
|
|
83
|
+
byteLength: produced.byteLength,
|
|
84
|
+
};
|
|
85
|
+
pinnedBySab.set(sab, pin);
|
|
86
|
+
pinFinalizer?.register(sab, pin.token);
|
|
87
|
+
}
|
|
88
|
+
return pin;
|
|
89
|
+
};
|
|
90
|
+
const makeMetadata = (pin) => ({
|
|
91
|
+
kind: SHARED_ARRAY_BUFFER_CODEC_ID,
|
|
92
|
+
origin: PROCESS_ORIGIN,
|
|
93
|
+
runtime: RUNTIME,
|
|
94
|
+
pointer: pin.pointer.toString(),
|
|
95
|
+
token: pin.token.toString(),
|
|
96
|
+
byteLength: pin.byteLength,
|
|
97
|
+
});
|
|
98
|
+
const makeFullNumericMetadata = (pin) => {
|
|
99
|
+
if (pin.byteLength > 0xffffffff)
|
|
100
|
+
return undefined;
|
|
101
|
+
const [tokenLow, tokenHigh] = splitU64(pin.token);
|
|
102
|
+
const [pointerLow, pointerHigh] = splitU64(pin.pointer);
|
|
103
|
+
return [
|
|
104
|
+
tokenLow,
|
|
105
|
+
tokenHigh,
|
|
106
|
+
pointerLow,
|
|
107
|
+
pointerHigh,
|
|
108
|
+
pin.byteLength >>> 0,
|
|
109
|
+
encodeRuntime(RUNTIME),
|
|
110
|
+
getProcessId() >>> 0,
|
|
111
|
+
0,
|
|
112
|
+
];
|
|
113
|
+
};
|
|
114
|
+
const makeTokenNumericMetadata = (pin) => {
|
|
115
|
+
const [tokenLow, tokenHigh] = splitU64(pin.token);
|
|
116
|
+
return [tokenLow, tokenHigh];
|
|
117
|
+
};
|
|
118
|
+
/** Wrap a SAB as external payload; GC-managed pins mean no settle finalizer. */
|
|
119
|
+
export const wrapSharedArrayBufferPayload = (sab) => {
|
|
120
|
+
let payload = payloadBySharedBuffer.get(sab);
|
|
121
|
+
if (payload !== undefined)
|
|
122
|
+
return payload;
|
|
123
|
+
const pin = pinSab(sab);
|
|
124
|
+
payload = createSharedArrayBufferPayload(sab, pin, makeMetadata(pin));
|
|
125
|
+
return payload;
|
|
126
|
+
};
|
|
127
|
+
const createSharedArrayBufferPayload = (buffer, pin, metadata) => {
|
|
128
|
+
let payload = payloadBySharedBuffer.get(buffer);
|
|
129
|
+
if (payload !== undefined)
|
|
130
|
+
return payload;
|
|
131
|
+
const fullNumeric = makeFullNumericMetadata(pin);
|
|
132
|
+
const tokenOnlyNumeric = makeTokenNumericMetadata(pin);
|
|
133
|
+
payload = {
|
|
134
|
+
[EXTERNAL_PAYLOAD_BRAND]: SHARED_ARRAY_BUFFER_CODEC_ID,
|
|
135
|
+
toMetadata: () => metadata,
|
|
136
|
+
[SHARED_ARRAY_BUFFER_NUMERIC_TRANSFER]: (transportKey) => {
|
|
137
|
+
if (fullNumeric === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const warmTokens = getWarmTokens(transportKey);
|
|
141
|
+
if (warmTokens === undefined)
|
|
142
|
+
return fullNumeric;
|
|
143
|
+
if (warmTokens.has(pin.token))
|
|
144
|
+
return tokenOnlyNumeric;
|
|
145
|
+
warmTokens.add(pin.token);
|
|
146
|
+
return fullNumeric;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
payloadBySharedBuffer.set(buffer, payload);
|
|
150
|
+
return payload;
|
|
151
|
+
};
|
|
152
|
+
export const getSharedArrayBufferPayload = (value) => {
|
|
153
|
+
if (isSharedArrayBufferValue(value))
|
|
154
|
+
return wrapSharedArrayBufferPayload(value);
|
|
155
|
+
return payloadBySharedBuffer.get(value);
|
|
156
|
+
};
|
|
157
|
+
const isSharedArrayBufferMetadata = (value) => {
|
|
158
|
+
if (value === null || typeof value !== "object")
|
|
159
|
+
return false;
|
|
160
|
+
const meta = value;
|
|
161
|
+
return (meta.kind === SHARED_ARRAY_BUFFER_CODEC_ID &&
|
|
162
|
+
typeof meta.origin === "string" &&
|
|
163
|
+
typeof meta.runtime === "string" &&
|
|
164
|
+
typeof meta.pointer === "string" &&
|
|
165
|
+
typeof meta.token === "string" &&
|
|
166
|
+
typeof meta.byteLength === "number" &&
|
|
167
|
+
Number.isInteger(meta.byteLength) &&
|
|
168
|
+
meta.byteLength >= 0);
|
|
169
|
+
};
|
|
170
|
+
const materializeSharedBuffer = (metadata, warmOnly) => {
|
|
171
|
+
if (metadata.origin !== PROCESS_ORIGIN) {
|
|
172
|
+
throw new Error(`SharedArrayBuffer cannot cross a process boundary (origin ${metadata.origin} ` +
|
|
173
|
+
`!= ${PROCESS_ORIGIN}); it is shared by reference to thread workers only.`);
|
|
174
|
+
}
|
|
175
|
+
const token = BigInt(metadata.token);
|
|
176
|
+
const cached = cachedSharedBuffersByToken.get(token);
|
|
177
|
+
if (cached !== undefined)
|
|
178
|
+
return cached;
|
|
179
|
+
if (warmOnly) {
|
|
180
|
+
throw new TypeError("SharedArrayBuffer cache miss for warm token payload");
|
|
181
|
+
}
|
|
182
|
+
const region = getBufferReferenceCapabilities().adoptShared({
|
|
183
|
+
token,
|
|
184
|
+
pointer: BigInt(metadata.pointer),
|
|
185
|
+
byteOffset: 0,
|
|
186
|
+
byteLength: metadata.byteLength,
|
|
187
|
+
});
|
|
188
|
+
cachedSharedBuffersByToken.set(token, region.buffer);
|
|
189
|
+
createSharedArrayBufferPayload(region.buffer, {
|
|
190
|
+
token,
|
|
191
|
+
pointer: BigInt(metadata.pointer),
|
|
192
|
+
byteLength: metadata.byteLength,
|
|
193
|
+
}, metadata);
|
|
194
|
+
return region.buffer;
|
|
195
|
+
};
|
|
196
|
+
const decode = (metadata) => {
|
|
197
|
+
if (!isSharedArrayBufferMetadata(metadata)) {
|
|
198
|
+
throw new TypeError("Invalid SharedArrayBuffer payload metadata");
|
|
199
|
+
}
|
|
200
|
+
return materializeSharedBuffer(metadata, false);
|
|
201
|
+
};
|
|
202
|
+
const decodeNumeric = (words) => {
|
|
203
|
+
if (words.length === SHARED_ARRAY_BUFFER_TOKEN_NUMERIC_WORDS) {
|
|
204
|
+
const token = joinU64(words[0] ?? 0, words[1] ?? 0);
|
|
205
|
+
const cached = cachedSharedBuffersByToken.get(token);
|
|
206
|
+
if (cached !== undefined)
|
|
207
|
+
return cached;
|
|
208
|
+
throw new TypeError("SharedArrayBuffer cache miss for warm token payload");
|
|
209
|
+
}
|
|
210
|
+
const runtime = decodeRuntime(words[5] ?? 0);
|
|
211
|
+
if (runtime === undefined) {
|
|
212
|
+
throw new TypeError("Invalid SharedArrayBuffer numeric runtime");
|
|
213
|
+
}
|
|
214
|
+
const originPid = words[6];
|
|
215
|
+
if (originPid === undefined ||
|
|
216
|
+
!Number.isInteger(originPid) ||
|
|
217
|
+
originPid < 0) {
|
|
218
|
+
throw new TypeError("Invalid SharedArrayBuffer numeric origin");
|
|
219
|
+
}
|
|
220
|
+
if (words.length !== SHARED_ARRAY_BUFFER_NUMERIC_WORDS) {
|
|
221
|
+
throw new TypeError("Invalid SharedArrayBuffer numeric word count");
|
|
222
|
+
}
|
|
223
|
+
const metadata = {
|
|
224
|
+
kind: SHARED_ARRAY_BUFFER_CODEC_ID,
|
|
225
|
+
origin: `${runtime}:${originPid >>> 0}`,
|
|
226
|
+
runtime,
|
|
227
|
+
pointer: joinU64(words[2] ?? 0, words[3] ?? 0).toString(),
|
|
228
|
+
token: joinU64(words[0] ?? 0, words[1] ?? 0).toString(),
|
|
229
|
+
byteLength: words[4] ?? 0,
|
|
230
|
+
};
|
|
231
|
+
return materializeSharedBuffer(metadata, false);
|
|
232
|
+
};
|
|
233
|
+
const codecGlobal = globalThis;
|
|
234
|
+
const codecs = codecGlobal.__KNITTING_PAYLOAD_CODECS__ ??= Object.create(null);
|
|
235
|
+
codecs[SHARED_ARRAY_BUFFER_CODEC_ID] = { decode, decodeNumeric };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-diff primitive: snapshot the keys present on `globalThis`, then
|
|
3
|
+
* later compare to see what the loaded code added, removed, or redefined.
|
|
4
|
+
*
|
|
5
|
+
* This is the core of "check what happens" debugging — a concrete before/after
|
|
6
|
+
* of the real runtime a worker's modules created, not an aggregate metric. The
|
|
7
|
+
* same snapshot/diff shape generalises to other ambient state (process
|
|
8
|
+
* listeners, open handles, prototype patches); `globalThis` keys are the first
|
|
9
|
+
* instance.
|
|
10
|
+
*/
|
|
11
|
+
export type EnvSnapshot = {
|
|
12
|
+
readonly keys: ReadonlySet<string | symbol>;
|
|
13
|
+
};
|
|
14
|
+
/** Capture every own key on `globalThis`, including symbols. */
|
|
15
|
+
export declare const snapshotGlobals: () => EnvSnapshot;
|
|
16
|
+
export type GlobalsDiff = {
|
|
17
|
+
readonly added: (string | symbol)[];
|
|
18
|
+
readonly removed: (string | symbol)[];
|
|
19
|
+
};
|
|
20
|
+
export declare const diffGlobals: (before: EnvSnapshot, after: EnvSnapshot) => GlobalsDiff;
|
|
21
|
+
/**
|
|
22
|
+
* Render a key with enough provenance to tell a fresh global from a
|
|
23
|
+
* monkeypatch: its `typeof`/accessor kind plus writable/configurable/enumerable
|
|
24
|
+
* flags (`w`/`c`/`e`).
|
|
25
|
+
*/
|
|
26
|
+
export declare const describeGlobalKey: (key: string | symbol) => string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-diff primitive: snapshot the keys present on `globalThis`, then
|
|
3
|
+
* later compare to see what the loaded code added, removed, or redefined.
|
|
4
|
+
*
|
|
5
|
+
* This is the core of "check what happens" debugging — a concrete before/after
|
|
6
|
+
* of the real runtime a worker's modules created, not an aggregate metric. The
|
|
7
|
+
* same snapshot/diff shape generalises to other ambient state (process
|
|
8
|
+
* listeners, open handles, prototype patches); `globalThis` keys are the first
|
|
9
|
+
* instance.
|
|
10
|
+
*/
|
|
11
|
+
/** Capture every own key on `globalThis`, including symbols. */
|
|
12
|
+
export const snapshotGlobals = () => ({
|
|
13
|
+
keys: new Set(Reflect.ownKeys(globalThis)),
|
|
14
|
+
});
|
|
15
|
+
export const diffGlobals = (before, after) => {
|
|
16
|
+
const added = [];
|
|
17
|
+
const removed = [];
|
|
18
|
+
for (const key of after.keys) {
|
|
19
|
+
if (!before.keys.has(key))
|
|
20
|
+
added.push(key);
|
|
21
|
+
}
|
|
22
|
+
for (const key of before.keys) {
|
|
23
|
+
if (!after.keys.has(key))
|
|
24
|
+
removed.push(key);
|
|
25
|
+
}
|
|
26
|
+
return { added, removed };
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Render a key with enough provenance to tell a fresh global from a
|
|
30
|
+
* monkeypatch: its `typeof`/accessor kind plus writable/configurable/enumerable
|
|
31
|
+
* flags (`w`/`c`/`e`).
|
|
32
|
+
*/
|
|
33
|
+
export const describeGlobalKey = (key) => {
|
|
34
|
+
const name = typeof key === "symbol" ? key.toString() : key;
|
|
35
|
+
let descriptor;
|
|
36
|
+
try {
|
|
37
|
+
descriptor = Object.getOwnPropertyDescriptor(globalThis, key);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return name;
|
|
41
|
+
}
|
|
42
|
+
if (descriptor === undefined)
|
|
43
|
+
return name;
|
|
44
|
+
const kind = descriptor.get !== undefined || descriptor.set !== undefined
|
|
45
|
+
? "accessor"
|
|
46
|
+
: typeof descriptor.value;
|
|
47
|
+
const flags = `${descriptor.writable === false ? "" : "w"}${descriptor.configurable ? "c" : ""}${descriptor.enumerable ? "e" : ""}`;
|
|
48
|
+
return flags.length > 0 ? `${name} (${kind} ${flags})` : `${name} (${kind})`;
|
|
49
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DebugOptions } from "../types.js";
|
|
2
|
+
/** Namespaces explicitly requested via `KNITTING_DEBUG`. */
|
|
3
|
+
export declare const DEBUG_NAMESPACES: ReadonlySet<string>;
|
|
4
|
+
/**
|
|
5
|
+
* True when at least one namespace is requested via the env var alone. Guards
|
|
6
|
+
* the env-only paths; callers that also accept a `debug` option should use
|
|
7
|
+
* {@link resolveDebugNamespaces} instead.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEBUG_ENABLED: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Merge the namespaces requested through the `debug` pool option with those from
|
|
12
|
+
* `KNITTING_DEBUG`; either source can enable a namespace. Returns an empty set
|
|
13
|
+
* when nothing is requested, so callers use `.size === 0` to keep the rest of
|
|
14
|
+
* `src/debug` unloaded — zero cost when off.
|
|
15
|
+
*/
|
|
16
|
+
export declare const resolveDebugNamespaces: (debug?: DebugOptions) => Set<string>;
|
|
17
|
+
/** True when `namespace` (or `"*"`) is active for the given `debug` config + env. */
|
|
18
|
+
export declare const debugHas: (debug: DebugOptions | undefined, namespace: string) => boolean;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-cost debug gate.
|
|
3
|
+
*
|
|
4
|
+
* Read once at module load from `KNITTING_DEBUG`. This module is deliberately
|
|
5
|
+
* tiny and dependency-light: importing it must never pull in the logger or the
|
|
6
|
+
* environment-diff machinery. Callers branch on {@link DEBUG_ENABLED} and only
|
|
7
|
+
* then `await import("./handle.ts")`, so when debug is off nothing else under
|
|
8
|
+
* `src/debug` is ever loaded — literally zero cost, not merely cheap.
|
|
9
|
+
*
|
|
10
|
+
* `KNITTING_DEBUG` is a comma-separated list of namespaces:
|
|
11
|
+
* KNITTING_DEBUG=host,imports # only those
|
|
12
|
+
* KNITTING_DEBUG=* # everything
|
|
13
|
+
*/
|
|
14
|
+
import { getNodeProcess } from "../common/node-compat.js";
|
|
15
|
+
const readEnv = (key) => {
|
|
16
|
+
// Deno: `process.env` exists under node-compat, but reading it may require
|
|
17
|
+
// --allow-env. Prefer the typed `Deno.env` and swallow permission errors.
|
|
18
|
+
const denoEnv = globalThis.Deno?.env;
|
|
19
|
+
if (typeof denoEnv?.get === "function") {
|
|
20
|
+
try {
|
|
21
|
+
return denoEnv.get(key);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
/* env permission denied — fall through to node/bun */
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return getNodeProcess()?.env?.[key];
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const raw = readEnv("KNITTING_DEBUG");
|
|
35
|
+
/** Namespaces explicitly requested via `KNITTING_DEBUG`. */
|
|
36
|
+
export const DEBUG_NAMESPACES = new Set((raw ?? "")
|
|
37
|
+
.split(",")
|
|
38
|
+
.map((part) => part.trim())
|
|
39
|
+
.filter((part) => part.length > 0));
|
|
40
|
+
/**
|
|
41
|
+
* True when at least one namespace is requested via the env var alone. Guards
|
|
42
|
+
* the env-only paths; callers that also accept a `debug` option should use
|
|
43
|
+
* {@link resolveDebugNamespaces} instead.
|
|
44
|
+
*/
|
|
45
|
+
export const DEBUG_ENABLED = DEBUG_NAMESPACES.size > 0;
|
|
46
|
+
/**
|
|
47
|
+
* Merge the namespaces requested through the `debug` pool option with those from
|
|
48
|
+
* `KNITTING_DEBUG`; either source can enable a namespace. Returns an empty set
|
|
49
|
+
* when nothing is requested, so callers use `.size === 0` to keep the rest of
|
|
50
|
+
* `src/debug` unloaded — zero cost when off.
|
|
51
|
+
*/
|
|
52
|
+
export const resolveDebugNamespaces = (debug) => {
|
|
53
|
+
const namespaces = new Set(DEBUG_NAMESPACES);
|
|
54
|
+
if (debug === true) {
|
|
55
|
+
namespaces.add("*");
|
|
56
|
+
}
|
|
57
|
+
else if (debug !== undefined && debug !== false) {
|
|
58
|
+
for (const [key, value] of Object.entries(debug)) {
|
|
59
|
+
if (value === true)
|
|
60
|
+
namespaces.add(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return namespaces;
|
|
64
|
+
};
|
|
65
|
+
/** True when `namespace` (or `"*"`) is active for the given `debug` config + env. */
|
|
66
|
+
export const debugHas = (debug, namespace) => {
|
|
67
|
+
const namespaces = resolveDebugNamespaces(debug);
|
|
68
|
+
return namespaces.has("*") || namespaces.has(namespace);
|
|
69
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type DebugInit = {
|
|
2
|
+
/** Identity prefix for log tags, e.g. `"w0"` for a worker or `"main"`. */
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly runtime: string;
|
|
5
|
+
readonly namespaces: ReadonlySet<string>;
|
|
6
|
+
};
|
|
7
|
+
export type Debug = {
|
|
8
|
+
/**
|
|
9
|
+
* Is a namespace active? Capture this once before a hot loop and branch on
|
|
10
|
+
* the boolean — never call per-iteration.
|
|
11
|
+
*/
|
|
12
|
+
enabled: (namespace: string) => boolean;
|
|
13
|
+
/** Emit a tagged line to stderr when `namespace` is active. */
|
|
14
|
+
log: (namespace: string, message: string) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Re-snapshot `globalThis` and report what changed since the previous phase.
|
|
17
|
+
* Drives two-phase pollution attribution (e.g. `"bootstrap"` then
|
|
18
|
+
* `"tasks"`), so you can see which loader injected which global. No-op unless
|
|
19
|
+
* the `globals` namespace is active.
|
|
20
|
+
*/
|
|
21
|
+
envPhase: (label: string) => void;
|
|
22
|
+
};
|
|
23
|
+
export declare const initDebug: ({ name, runtime, namespaces }: DebugInit) => Debug;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side debug handle. Lazily imported (see {@link ./gate.ts}) only when
|
|
3
|
+
* `KNITTING_DEBUG` names at least one namespace, so neither this code nor the
|
|
4
|
+
* baseline snapshot it takes exists when debug is off.
|
|
5
|
+
*
|
|
6
|
+
* Diagnostics go to stderr so they never corrupt a worker's stdout, and every
|
|
7
|
+
* line is tagged with the worker id, runtime, and a clock relative to when this
|
|
8
|
+
* worker's debug initialised. The clock is worker-local on purpose: a main-thread
|
|
9
|
+
* timestamp can't be compared against `performance.now()` here because the time
|
|
10
|
+
* origins differ across the thread/process boundary (it would read negative).
|
|
11
|
+
*/
|
|
12
|
+
import { describeGlobalKey, diffGlobals, snapshotGlobals, } from "./env-diff.js";
|
|
13
|
+
export const initDebug = ({ name, runtime, namespaces }) => {
|
|
14
|
+
const all = namespaces.has("*");
|
|
15
|
+
const enabled = (namespace) => all || namespaces.has(namespace);
|
|
16
|
+
const base = performance.now();
|
|
17
|
+
const tag = `${name}·${runtime}`;
|
|
18
|
+
const log = (namespace, message) => {
|
|
19
|
+
if (!enabled(namespace))
|
|
20
|
+
return;
|
|
21
|
+
const elapsed = (performance.now() - base).toFixed(1);
|
|
22
|
+
console.error(`[${tag}·+${elapsed}ms] ${namespace}: ${message}`);
|
|
23
|
+
};
|
|
24
|
+
// Baseline for the environment diff, taken the moment debug initialises:
|
|
25
|
+
// before worker bootstrap and before any task module is imported. Only paid
|
|
26
|
+
// when `globals` tracing is actually on.
|
|
27
|
+
let previous = enabled("globals")
|
|
28
|
+
? snapshotGlobals()
|
|
29
|
+
: undefined;
|
|
30
|
+
const envPhase = (label) => {
|
|
31
|
+
if (previous === undefined)
|
|
32
|
+
return;
|
|
33
|
+
const current = snapshotGlobals();
|
|
34
|
+
const { added, removed } = diffGlobals(previous, current);
|
|
35
|
+
previous = current;
|
|
36
|
+
if (added.length === 0 && removed.length === 0) {
|
|
37
|
+
log("globals", `${label}: no new globals`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (added.length > 0) {
|
|
41
|
+
log("globals", `${label} +${added.length}: ${added.map(describeGlobalKey).join(" ")}`);
|
|
42
|
+
}
|
|
43
|
+
if (removed.length > 0) {
|
|
44
|
+
log("globals", `${label} -${removed.length}: ${removed.map(String).join(" ")}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
return { enabled, log, envPhase };
|
|
48
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export type SignalArguments = ReturnType<typeof createSharedMemoryTransport>;
|
|
2
2
|
import { type SharedBufferSource } from "../../common/shared-buffer-region.js";
|
|
3
|
-
import { type DebugOptions } from "../../types.js";
|
|
4
3
|
export declare const TRANSPORT_SIGNAL_BYTES: number;
|
|
5
4
|
export type Sab = {
|
|
6
5
|
size?: number;
|
|
@@ -10,11 +9,10 @@ type SignalForWorker = {
|
|
|
10
9
|
sabObject?: Sab;
|
|
11
10
|
isMain: boolean;
|
|
12
11
|
thread: number;
|
|
13
|
-
debug?: DebugOptions;
|
|
14
12
|
startTime?: number;
|
|
15
13
|
};
|
|
16
14
|
export declare const createSharedMemoryTransport: ({ sabObject, isMain, startTime }: SignalForWorker) => {
|
|
17
|
-
sab: import("../../
|
|
15
|
+
sab: import("../../common/shared-buffer-region.js").SharedBufferRegion;
|
|
18
16
|
op: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|
|
19
17
|
startAt: number;
|
|
20
18
|
opView: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|