relaxnative 0.1.2 → 0.1.4
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/dist/chunk-2KRTGQYD.js +898 -0
- package/dist/chunk-4TYB2LNW.js +1052 -0
- package/dist/chunk-4ZOOQ7GU.js +431 -0
- package/dist/chunk-6GKAG5SO.js +1042 -0
- package/dist/chunk-AXIKKGDT.js +788 -0
- package/dist/chunk-EMSPE6HY.js +788 -0
- package/dist/chunk-GCYH2XU5.js +788 -0
- package/dist/chunk-H6WRQCNQ.js +817 -0
- package/dist/chunk-HBO7F7P4.js +431 -0
- package/dist/chunk-HXUEYL4P.js +1042 -0
- package/dist/chunk-IAQ27C7W.js +788 -0
- package/dist/chunk-JYYW2Q42.js +415 -0
- package/dist/chunk-KHDXEK4J.js +1042 -0
- package/dist/chunk-KJ7GQCDE.js +1042 -0
- package/dist/chunk-N32TLKJ3.js +797 -0
- package/dist/chunk-STPJVBXH.js +817 -0
- package/dist/chunk-UGGUZN4H.js +818 -0
- package/dist/chunk-UGS3DYRK.js +1042 -0
- package/dist/chunk-WERFVUWK.js +1042 -0
- package/dist/chunk-Y6HEDJ2E.js +1042 -0
- package/dist/chunk-YBMUKRIZ.js +1042 -0
- package/dist/chunk-ZIVSD5K6.js +1042 -0
- package/dist/cli.js +33 -4
- package/dist/index.d.ts +33 -1
- package/dist/index.js +3 -3
- package/dist/memory/memory.selftest.js +2 -2
- package/dist/worker/processEntry.js +50 -3
- package/package.json +6 -4
|
@@ -0,0 +1,898 @@
|
|
|
1
|
+
import {
|
|
2
|
+
alloc,
|
|
3
|
+
compileWithCache,
|
|
4
|
+
detectCompilers,
|
|
5
|
+
detectLanguage,
|
|
6
|
+
loadNative,
|
|
7
|
+
loadNativeWithBindings,
|
|
8
|
+
parseNativeSource
|
|
9
|
+
} from "./chunk-UGGUZN4H.js";
|
|
10
|
+
import {
|
|
11
|
+
loadFfi
|
|
12
|
+
} from "./chunk-LLZ4I4OR.js";
|
|
13
|
+
|
|
14
|
+
// src/registry/registryPaths.ts
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
function getProjectRoot(cwd = process.cwd()) {
|
|
17
|
+
return cwd;
|
|
18
|
+
}
|
|
19
|
+
function getRegistryRoot(projectRoot = getProjectRoot()) {
|
|
20
|
+
return join(projectRoot, "native", "registry");
|
|
21
|
+
}
|
|
22
|
+
function getInstalledPackageDir(pkg, projectRoot = getProjectRoot()) {
|
|
23
|
+
return join(getRegistryRoot(projectRoot), pkg);
|
|
24
|
+
}
|
|
25
|
+
function resolveRegistryImport(specifier, projectRoot = getProjectRoot()) {
|
|
26
|
+
const prefix = "relaxnative/";
|
|
27
|
+
if (!specifier.startsWith(prefix)) return null;
|
|
28
|
+
const pkg = specifier.slice(prefix.length);
|
|
29
|
+
return resolve(getInstalledPackageDir(pkg, projectRoot));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/registry/relaxJson.ts
|
|
33
|
+
import { readFileSync } from "fs";
|
|
34
|
+
import { join as join2 } from "path";
|
|
35
|
+
|
|
36
|
+
// src/registry/trust.ts
|
|
37
|
+
function normalizeTrustLevel(value) {
|
|
38
|
+
if (value === "local" || value === "community" || value === "verified") return value;
|
|
39
|
+
return "community";
|
|
40
|
+
}
|
|
41
|
+
function trustPolicy(level) {
|
|
42
|
+
if (level === "verified") {
|
|
43
|
+
return {
|
|
44
|
+
requireConfirm: false,
|
|
45
|
+
warnOnInstall: false,
|
|
46
|
+
defaultIsolation: "process",
|
|
47
|
+
defaultExecutionMode: "sync",
|
|
48
|
+
allowPermissions: true
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (level === "local") {
|
|
52
|
+
return {
|
|
53
|
+
requireConfirm: false,
|
|
54
|
+
warnOnInstall: false,
|
|
55
|
+
defaultIsolation: "process",
|
|
56
|
+
defaultExecutionMode: "sync",
|
|
57
|
+
allowPermissions: true
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
requireConfirm: true,
|
|
62
|
+
warnOnInstall: true,
|
|
63
|
+
defaultIsolation: "process",
|
|
64
|
+
defaultExecutionMode: "async",
|
|
65
|
+
allowPermissions: false
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/registry/relaxJson.ts
|
|
70
|
+
function readRelaxJson(pkgDir) {
|
|
71
|
+
const jsonPath = join2(pkgDir, "relax.json");
|
|
72
|
+
const raw = readFileSync(jsonPath, "utf8");
|
|
73
|
+
const parsed = JSON.parse(raw);
|
|
74
|
+
if (!parsed?.name || !parsed?.version || !Array.isArray(parsed?.exports)) {
|
|
75
|
+
throw new Error(`Invalid relax.json at ${jsonPath}`);
|
|
76
|
+
}
|
|
77
|
+
parsed.trust = normalizeTrustLevel(parsed.trust);
|
|
78
|
+
if (parsed.trust === "verified") {
|
|
79
|
+
const sig = parsed.registrySignature;
|
|
80
|
+
if (!sig || sig.alg !== "sha256" || typeof sig.digest !== "string" || sig.digest.length < 32) {
|
|
81
|
+
parsed.registrySignature = sig;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/registry/installer.ts
|
|
88
|
+
import {
|
|
89
|
+
mkdirSync,
|
|
90
|
+
readFileSync as readFileSync4,
|
|
91
|
+
rmSync,
|
|
92
|
+
writeFileSync,
|
|
93
|
+
existsSync as existsSync2,
|
|
94
|
+
readdirSync
|
|
95
|
+
} from "fs";
|
|
96
|
+
import { join as join4 } from "path";
|
|
97
|
+
import { createInterface } from "readline";
|
|
98
|
+
|
|
99
|
+
// src/registry/staticScan.ts
|
|
100
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
101
|
+
function staticScanNativeSource(sourcePath) {
|
|
102
|
+
const src = readFileSync2(sourcePath, "utf8");
|
|
103
|
+
const lines = src.split(/\r?\n/);
|
|
104
|
+
const rules = [
|
|
105
|
+
{
|
|
106
|
+
rule: "process-spawn",
|
|
107
|
+
re: /\b(system|popen|fork|execv|execve|execvp|execvpe|execl|execlp|CreateProcessW|WinExec)\b/,
|
|
108
|
+
message: "Potential process execution API found"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
rule: "raw-syscall",
|
|
112
|
+
re: /\b(syscall|__syscall)\b/,
|
|
113
|
+
message: "Raw syscall usage found"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
rule: "file-io",
|
|
117
|
+
re: /\b(fopen|open|CreateFileW|unlink|remove|rename)\b/,
|
|
118
|
+
message: "Potential filesystem API found"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
rule: "network",
|
|
122
|
+
re: /\b(socket|connect|bind|listen|accept|recv|send|getaddrinfo)\b/,
|
|
123
|
+
message: "Potential network API found"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
rule: "w-x-memory",
|
|
127
|
+
re: /\b(mprotect|VirtualProtect|PROT_EXEC|PAGE_EXECUTE_READWRITE)\b/,
|
|
128
|
+
message: "Writable/executable memory pattern found"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
rule: "dynamic-loader",
|
|
132
|
+
re: /\b(dlopen|LoadLibraryA|LoadLibraryW)\b/,
|
|
133
|
+
message: "Dynamic loader API found"
|
|
134
|
+
}
|
|
135
|
+
];
|
|
136
|
+
const findings = [];
|
|
137
|
+
for (let i = 0; i < lines.length; i++) {
|
|
138
|
+
const line = lines[i];
|
|
139
|
+
for (const r of rules) {
|
|
140
|
+
if (r.re.test(line)) {
|
|
141
|
+
findings.push({ rule: r.rule, message: r.message, line: i + 1 });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return findings;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/registry/signature.ts
|
|
149
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
150
|
+
import { readFileSync as readFileSync3, existsSync } from "fs";
|
|
151
|
+
import { join as join3 } from "path";
|
|
152
|
+
function sha256RelaxJsonWithoutSignatureHex(jsonPath) {
|
|
153
|
+
try {
|
|
154
|
+
const raw = readFileSync3(jsonPath, "utf8");
|
|
155
|
+
const parsed = JSON.parse(raw);
|
|
156
|
+
if (!parsed || typeof parsed !== "object") return { ok: false, reason: "Invalid JSON" };
|
|
157
|
+
if ("registrySignature" in parsed) delete parsed.registrySignature;
|
|
158
|
+
const canonical = JSON.stringify(parsed, null, 2);
|
|
159
|
+
const digest = createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
160
|
+
return { ok: true, digest };
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { ok: false, reason: e?.message ?? "Failed to hash relax.json" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function verifyRegistrySignature(pkgDir, sig) {
|
|
166
|
+
if (!sig) return { ok: false, reason: "Missing registrySignature" };
|
|
167
|
+
if (sig.alg !== "sha256") return { ok: false, reason: `Unsupported signature alg: ${sig.alg}` };
|
|
168
|
+
const jsonPath = join3(pkgDir, "relax.json");
|
|
169
|
+
if (!existsSync(jsonPath)) return { ok: false, reason: "Missing relax.json" };
|
|
170
|
+
const digestRes = sha256RelaxJsonWithoutSignatureHex(jsonPath);
|
|
171
|
+
if (!digestRes.ok) return { ok: false, reason: digestRes.reason };
|
|
172
|
+
const digest = digestRes.digest;
|
|
173
|
+
try {
|
|
174
|
+
const a = Buffer.from(digest, "hex");
|
|
175
|
+
const b = Buffer.from(sig.digest, "hex");
|
|
176
|
+
if (a.length !== b.length) return { ok: false, reason: "Digest length mismatch" };
|
|
177
|
+
if (!timingSafeEqual(a, b)) return { ok: false, reason: "Digest mismatch" };
|
|
178
|
+
return { ok: true };
|
|
179
|
+
} catch {
|
|
180
|
+
return { ok: false, reason: "Invalid digest encoding" };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/registry/installer.ts
|
|
185
|
+
function trustStatePath(registryRoot) {
|
|
186
|
+
return join4(registryRoot, ".trust.json");
|
|
187
|
+
}
|
|
188
|
+
function readTrustState(registryRoot) {
|
|
189
|
+
const p = trustStatePath(registryRoot);
|
|
190
|
+
try {
|
|
191
|
+
if (!existsSync2(p)) return { trusted: {} };
|
|
192
|
+
const raw = readFileSync4(p, "utf8");
|
|
193
|
+
const parsed = JSON.parse(raw);
|
|
194
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.trusted !== "object") return { trusted: {} };
|
|
195
|
+
return parsed;
|
|
196
|
+
} catch {
|
|
197
|
+
return { trusted: {} };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function writeTrustState(registryRoot, state) {
|
|
201
|
+
const p = trustStatePath(registryRoot);
|
|
202
|
+
writeFileSync(p, JSON.stringify(state, null, 2) + "\n");
|
|
203
|
+
}
|
|
204
|
+
function isInteractive() {
|
|
205
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
206
|
+
}
|
|
207
|
+
async function promptYesNo(question) {
|
|
208
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
209
|
+
return await new Promise((resolve2) => {
|
|
210
|
+
rl.question(question, (answer) => {
|
|
211
|
+
rl.close();
|
|
212
|
+
const a = String(answer ?? "").trim().toLowerCase();
|
|
213
|
+
resolve2(a === "y" || a === "yes");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function formatPermissions(perms) {
|
|
218
|
+
if (!perms) return [];
|
|
219
|
+
const lines = [];
|
|
220
|
+
const fsRead = perms.fs?.read?.length ? perms.fs.read.join(", ") : null;
|
|
221
|
+
const fsWrite = perms.fs?.write?.length ? perms.fs.write.join(", ") : null;
|
|
222
|
+
if (fsRead) lines.push(`- fs.read: ${fsRead}`);
|
|
223
|
+
if (fsWrite) lines.push(`- fs.write: ${fsWrite}`);
|
|
224
|
+
if (perms.network?.outbound === true) lines.push("- network.outbound: true");
|
|
225
|
+
if (perms.process?.spawn === true) lines.push("- process.spawn: true");
|
|
226
|
+
return lines;
|
|
227
|
+
}
|
|
228
|
+
function installPackage(pkgSpecifier, projectRoot = process.cwd()) {
|
|
229
|
+
const warnings = [];
|
|
230
|
+
const registryRoot = getRegistryRoot(projectRoot);
|
|
231
|
+
mkdirSync(registryRoot, { recursive: true });
|
|
232
|
+
const isFile = pkgSpecifier.startsWith("file:");
|
|
233
|
+
if (!isFile) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Only file: installs are supported right now (deterministic, offline). Got: ${pkgSpecifier}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const srcDir = pkgSpecifier.slice("file:".length);
|
|
239
|
+
const manifest = readRelaxJson(srcDir);
|
|
240
|
+
const trust = normalizeTrustLevel(manifest.trust);
|
|
241
|
+
const policy = trustPolicy(trust);
|
|
242
|
+
if (trust === "verified") {
|
|
243
|
+
const sig = verifyRegistrySignature(srcDir, manifest.registrySignature);
|
|
244
|
+
if (!sig.ok) {
|
|
245
|
+
throw new Error(`Refusing verified package: signature check failed (${sig.reason})`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const destDir = getInstalledPackageDir(manifest.name, projectRoot);
|
|
249
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
250
|
+
mkdirSync(destDir, { recursive: true });
|
|
251
|
+
writeFileSync(join4(destDir, "relax.json"), readFileSync4(join4(srcDir, "relax.json")));
|
|
252
|
+
for (const exp of manifest.exports) {
|
|
253
|
+
const srcPath = join4(srcDir, exp.source);
|
|
254
|
+
const dstPath = join4(destDir, exp.source);
|
|
255
|
+
mkdirSync(join4(dstPath, ".."), { recursive: true });
|
|
256
|
+
if (!existsSync2(srcPath)) {
|
|
257
|
+
throw new Error(`Missing export source: ${srcPath}`);
|
|
258
|
+
}
|
|
259
|
+
const findings = staticScanNativeSource(srcPath);
|
|
260
|
+
for (const f of findings) {
|
|
261
|
+
warnings.push(`${exp.source}:${f.line ?? "?"} ${f.rule}: ${f.message}`);
|
|
262
|
+
}
|
|
263
|
+
writeFileSync(dstPath, readFileSync4(srcPath));
|
|
264
|
+
}
|
|
265
|
+
return { pkg: manifest.name, dir: destDir, warnings, trust };
|
|
266
|
+
}
|
|
267
|
+
async function installPackageEnforcingTrust(pkgSpecifier, projectRoot = process.cwd(), options) {
|
|
268
|
+
const registryRoot = getRegistryRoot(projectRoot);
|
|
269
|
+
mkdirSync(registryRoot, { recursive: true });
|
|
270
|
+
if (!pkgSpecifier.startsWith("file:")) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Only file: installs are supported right now (deterministic, offline). Got: ${pkgSpecifier}`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const srcDir = pkgSpecifier.slice("file:".length);
|
|
276
|
+
const manifest = readRelaxJson(srcDir);
|
|
277
|
+
const trust = normalizeTrustLevel(manifest.trust);
|
|
278
|
+
const policy = trustPolicy(trust);
|
|
279
|
+
const state = readTrustState(registryRoot);
|
|
280
|
+
const key = `${manifest.name}@${manifest.version}`;
|
|
281
|
+
const known = state.trusted[key];
|
|
282
|
+
const permissionLines = formatPermissions(manifest.permissions);
|
|
283
|
+
const hasElevatedPerms = permissionLines.length > 0;
|
|
284
|
+
if (trust === "verified") {
|
|
285
|
+
const sig = verifyRegistrySignature(srcDir, manifest.registrySignature);
|
|
286
|
+
if (!sig.ok) {
|
|
287
|
+
throw new Error(`Refusing verified package: signature check failed (${sig.reason})`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (policy.requireConfirm && !known) {
|
|
291
|
+
const lines = [];
|
|
292
|
+
lines.push(`Installing community package ${manifest.name}@${manifest.version}`);
|
|
293
|
+
lines.push(`Trust: ${trust}`);
|
|
294
|
+
if (hasElevatedPerms) {
|
|
295
|
+
lines.push("Requested permissions:");
|
|
296
|
+
lines.push(...permissionLines);
|
|
297
|
+
} else {
|
|
298
|
+
lines.push("Requested permissions: (none)");
|
|
299
|
+
}
|
|
300
|
+
lines.push("This will compile and run native code on your machine.");
|
|
301
|
+
if (options?.yes) {
|
|
302
|
+
throw new Error("Community package install requires confirmation (re-run without --yes or mark as trusted).");
|
|
303
|
+
}
|
|
304
|
+
if (!isInteractive()) {
|
|
305
|
+
throw new Error("Community package install requires an interactive terminal.");
|
|
306
|
+
}
|
|
307
|
+
console.warn(lines.join("\n"));
|
|
308
|
+
const ok = await promptYesNo("Proceed? (y/N) ");
|
|
309
|
+
if (!ok) {
|
|
310
|
+
throw new Error("Aborted");
|
|
311
|
+
}
|
|
312
|
+
state.trusted[key] = { trust, at: Date.now() };
|
|
313
|
+
writeTrustState(registryRoot, state);
|
|
314
|
+
}
|
|
315
|
+
if (!policy.allowPermissions && hasElevatedPerms) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Refusing community package with declared permissions. Move to 'local' trust or get it verified.`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const res = installPackage(pkgSpecifier, projectRoot);
|
|
321
|
+
if (!state.trusted[key]) {
|
|
322
|
+
state.trusted[key] = { trust, at: Date.now() };
|
|
323
|
+
writeTrustState(registryRoot, state);
|
|
324
|
+
}
|
|
325
|
+
if (policy.warnOnInstall && res.warnings.length) {
|
|
326
|
+
console.warn(`
|
|
327
|
+
Static scan warnings (${res.warnings.length}):`);
|
|
328
|
+
}
|
|
329
|
+
return res;
|
|
330
|
+
}
|
|
331
|
+
function removePackage(pkg, projectRoot = process.cwd()) {
|
|
332
|
+
const destDir = getInstalledPackageDir(pkg, projectRoot);
|
|
333
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
334
|
+
}
|
|
335
|
+
function listPackages(projectRoot = process.cwd()) {
|
|
336
|
+
const root = getRegistryRoot(projectRoot);
|
|
337
|
+
if (!existsSync2(root)) return [];
|
|
338
|
+
return readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
339
|
+
}
|
|
340
|
+
function updateIndex(projectRoot = process.cwd()) {
|
|
341
|
+
const root = getRegistryRoot(projectRoot);
|
|
342
|
+
if (!existsSync2(root)) return;
|
|
343
|
+
const pkgs = listPackages(projectRoot);
|
|
344
|
+
writeFileSync(join4(root, ".index"), pkgs.join("\n") + (pkgs.length ? "\n" : ""));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/nativeTestHarness.ts
|
|
348
|
+
import { readdirSync as readdirSync2, existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
|
|
349
|
+
import { extname, join as join5 } from "path";
|
|
350
|
+
var NATIVE_EXTS = /* @__PURE__ */ new Set([".c", ".cpp", ".cc", ".cxx", ".rs"]);
|
|
351
|
+
function isNativeFile(p) {
|
|
352
|
+
return NATIVE_EXTS.has(extname(p));
|
|
353
|
+
}
|
|
354
|
+
function discoverNativeTestFiles(rootDir) {
|
|
355
|
+
const out = [];
|
|
356
|
+
function walk(dir) {
|
|
357
|
+
for (const ent of readdirSync2(dir, { withFileTypes: true })) {
|
|
358
|
+
const p = join5(dir, ent.name);
|
|
359
|
+
if (ent.isDirectory()) walk(p);
|
|
360
|
+
else if (ent.isFile() && isNativeFile(p)) out.push(p);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!existsSync3(rootDir)) return [];
|
|
364
|
+
walk(rootDir);
|
|
365
|
+
out.sort((a, b) => {
|
|
366
|
+
const at = /_test\./.test(a) ? 0 : 1;
|
|
367
|
+
const bt = /_test\./.test(b) ? 0 : 1;
|
|
368
|
+
return at - bt || a.localeCompare(b);
|
|
369
|
+
});
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
function discoverNativeTestsFromFile(sourcePath) {
|
|
373
|
+
const src = readFileSync5(sourcePath, "utf8");
|
|
374
|
+
const lines = src.split(/\r?\n/);
|
|
375
|
+
const out = [];
|
|
376
|
+
const re = /^(?:\s*)(const\s+char\s*\*|int)\s+(test_[A-Za-z0-9_]+)\s*\(/;
|
|
377
|
+
for (let i = 0; i < lines.length; i++) {
|
|
378
|
+
const m = lines[i].match(re);
|
|
379
|
+
if (!m) continue;
|
|
380
|
+
const ret = m[1];
|
|
381
|
+
const name = m[2];
|
|
382
|
+
const kind = /^int\b/.test(ret) ? "int" : "cstring";
|
|
383
|
+
out.push({ name, sourcePath, line: i + 1, kind });
|
|
384
|
+
}
|
|
385
|
+
if (out.length) return out;
|
|
386
|
+
const language = detectLanguage(sourcePath);
|
|
387
|
+
const bindings = parseNativeSource(sourcePath, language);
|
|
388
|
+
const tests = [];
|
|
389
|
+
for (const [name, fn] of Object.entries(bindings.functions ?? {})) {
|
|
390
|
+
if (!name.startsWith("test_")) continue;
|
|
391
|
+
const ret = fn.returns;
|
|
392
|
+
const kind = ret === "int" || ret === "uint" || ret === "long" || ret === "size_t" ? "int" : "cstring";
|
|
393
|
+
tests.push({
|
|
394
|
+
name,
|
|
395
|
+
sourcePath,
|
|
396
|
+
line: fn.sourceLine,
|
|
397
|
+
kind
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return tests;
|
|
401
|
+
}
|
|
402
|
+
function discoverNativeTests(rootDir) {
|
|
403
|
+
const files = discoverNativeTestFiles(rootDir);
|
|
404
|
+
const all = [];
|
|
405
|
+
for (const f of files) {
|
|
406
|
+
all.push(...discoverNativeTestsFromFile(f));
|
|
407
|
+
}
|
|
408
|
+
return all;
|
|
409
|
+
}
|
|
410
|
+
function formatLocation(sourcePath, line) {
|
|
411
|
+
if (!line) return sourcePath;
|
|
412
|
+
return `${sourcePath}:${line}`;
|
|
413
|
+
}
|
|
414
|
+
function formatNativeTestResults(results) {
|
|
415
|
+
const lines = [];
|
|
416
|
+
for (const r of results) {
|
|
417
|
+
if (r.ok) {
|
|
418
|
+
lines.push(`\u2713 ${r.name}`);
|
|
419
|
+
} else {
|
|
420
|
+
const loc = formatLocation(r.sourcePath, r.line);
|
|
421
|
+
const msg = r.message ? ` (${r.message})` : "";
|
|
422
|
+
lines.push(`\u2717 ${r.name}${msg}`);
|
|
423
|
+
lines.push(` at ${loc}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const passed = results.filter((r) => r.ok).length;
|
|
427
|
+
const failed = results.length - passed;
|
|
428
|
+
lines.push("");
|
|
429
|
+
lines.push(`${passed} passed, ${failed} failed`);
|
|
430
|
+
return lines.join("\n");
|
|
431
|
+
}
|
|
432
|
+
async function runNativeTests(rootDir, opts) {
|
|
433
|
+
const isolation = opts?.isolation ?? "in-process";
|
|
434
|
+
const tests = discoverNativeTests(rootDir);
|
|
435
|
+
if (!tests.length) {
|
|
436
|
+
return { results: [], exitCode: 0 };
|
|
437
|
+
}
|
|
438
|
+
const { c, rust, platform } = detectCompilers();
|
|
439
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
440
|
+
for (const t of tests) {
|
|
441
|
+
const arr = byFile.get(t.sourcePath) ?? [];
|
|
442
|
+
arr.push(t);
|
|
443
|
+
byFile.set(t.sourcePath, arr);
|
|
444
|
+
}
|
|
445
|
+
const results = [];
|
|
446
|
+
for (const [sourcePath, cases] of byFile) {
|
|
447
|
+
const language = detectLanguage(sourcePath);
|
|
448
|
+
const compiler = language === "rust" ? rust : c;
|
|
449
|
+
if (!compiler) throw new Error(`No compiler for ${language}`);
|
|
450
|
+
const compileRes = compileWithCache(compiler, platform, {
|
|
451
|
+
sourcePath,
|
|
452
|
+
outDir: ".cache/native"
|
|
453
|
+
});
|
|
454
|
+
let api;
|
|
455
|
+
if (isolation === "in-process") {
|
|
456
|
+
const bindings = parseNativeSource(sourcePath, language);
|
|
457
|
+
api = loadFfi(compileRes.outputPath, bindings);
|
|
458
|
+
} else {
|
|
459
|
+
const synth = {
|
|
460
|
+
functions: Object.fromEntries(
|
|
461
|
+
cases.map((tc) => [
|
|
462
|
+
tc.name,
|
|
463
|
+
{
|
|
464
|
+
name: tc.name,
|
|
465
|
+
// Note: mapType supports "char*" (and "cstring" used to), but
|
|
466
|
+
// some environments/version combos reject "cstring" when sent
|
|
467
|
+
// across worker/process boundaries. Use a stable alias.
|
|
468
|
+
returns: tc.kind === "int" ? "int" : "char*",
|
|
469
|
+
args: [],
|
|
470
|
+
mode: "sync",
|
|
471
|
+
cost: "low"
|
|
472
|
+
}
|
|
473
|
+
])
|
|
474
|
+
)
|
|
475
|
+
};
|
|
476
|
+
const { mod } = await loadNativeWithBindings(sourcePath, {
|
|
477
|
+
isolation,
|
|
478
|
+
mutateBindings(bindings) {
|
|
479
|
+
bindings.functions = synth.functions;
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
api = mod;
|
|
483
|
+
}
|
|
484
|
+
for (const tc of cases) {
|
|
485
|
+
const fn = api[tc.name];
|
|
486
|
+
if (typeof fn !== "function") {
|
|
487
|
+
results.push({
|
|
488
|
+
name: tc.name,
|
|
489
|
+
ok: false,
|
|
490
|
+
message: `Function not found: ${tc.name}`,
|
|
491
|
+
sourcePath: tc.sourcePath,
|
|
492
|
+
line: tc.line,
|
|
493
|
+
durationMs: 0
|
|
494
|
+
});
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
const start = process.hrtime.bigint();
|
|
498
|
+
let ok = false;
|
|
499
|
+
let message;
|
|
500
|
+
try {
|
|
501
|
+
const out = fn();
|
|
502
|
+
const awaited = out instanceof Promise ? await out : out;
|
|
503
|
+
if (tc.kind === "int") {
|
|
504
|
+
ok = Number(awaited) === 0;
|
|
505
|
+
if (!ok) message = `returned ${String(awaited)}`;
|
|
506
|
+
} else {
|
|
507
|
+
ok = awaited == null || String(awaited).length === 0;
|
|
508
|
+
if (!ok) message = String(awaited);
|
|
509
|
+
}
|
|
510
|
+
} catch (err) {
|
|
511
|
+
ok = false;
|
|
512
|
+
message = err?.message ?? String(err);
|
|
513
|
+
}
|
|
514
|
+
const end = process.hrtime.bigint();
|
|
515
|
+
const durationMs = Number(end - start) / 1e6;
|
|
516
|
+
results.push({
|
|
517
|
+
name: tc.name,
|
|
518
|
+
ok,
|
|
519
|
+
message,
|
|
520
|
+
sourcePath: tc.sourcePath,
|
|
521
|
+
line: tc.line,
|
|
522
|
+
durationMs
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const failed = results.some((r) => !r.ok);
|
|
527
|
+
return { results, exitCode: failed ? 1 : 0 };
|
|
528
|
+
}
|
|
529
|
+
function getSourceLine(sourcePath, line) {
|
|
530
|
+
if (!line) return null;
|
|
531
|
+
try {
|
|
532
|
+
const src = readFileSync5(sourcePath, "utf8");
|
|
533
|
+
const lines = src.split(/\r?\n/);
|
|
534
|
+
return lines[line - 1] ?? null;
|
|
535
|
+
} catch {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/benchmark.ts
|
|
541
|
+
function isTraceEnabled() {
|
|
542
|
+
return process.env.RELAXNATIVE_TRACE === "1";
|
|
543
|
+
}
|
|
544
|
+
function trace(...args) {
|
|
545
|
+
if (!isTraceEnabled()) return;
|
|
546
|
+
console.log("[relaxnative:trace]", ...args);
|
|
547
|
+
}
|
|
548
|
+
function nowNs() {
|
|
549
|
+
return process.hrtime.bigint();
|
|
550
|
+
}
|
|
551
|
+
function nsToMs(ns) {
|
|
552
|
+
return Number(ns) / 1e6;
|
|
553
|
+
}
|
|
554
|
+
function assertSafeRun(iterations) {
|
|
555
|
+
if (iterations > 2e6) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`Refusing to run benchmark with iterations=${iterations} (cap=2,000,000). Pass a smaller value.`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function benchmarkJsFunction(name, fn, opts) {
|
|
562
|
+
const iterations = opts?.iterations ?? 1e5;
|
|
563
|
+
const warmup = opts?.warmup ?? Math.min(1e4, Math.max(100, Math.floor(iterations * 0.05)));
|
|
564
|
+
assertSafeRun(iterations);
|
|
565
|
+
const args = Array.isArray(opts?.args) ? opts.args : fn.length === 0 ? [] : [1, 2];
|
|
566
|
+
for (let i = 0; i < warmup; i++) {
|
|
567
|
+
const out = fn(...args);
|
|
568
|
+
if (out instanceof Promise) await out;
|
|
569
|
+
}
|
|
570
|
+
const latencies = [];
|
|
571
|
+
const startAll = nowNs();
|
|
572
|
+
for (let i = 0; i < iterations; i++) {
|
|
573
|
+
const t0 = nowNs();
|
|
574
|
+
const out = fn(...args);
|
|
575
|
+
if (out instanceof Promise) await out;
|
|
576
|
+
const t1 = nowNs();
|
|
577
|
+
latencies.push(nsToMs(t1 - t0));
|
|
578
|
+
}
|
|
579
|
+
const endAll = nowNs();
|
|
580
|
+
const totalMs = nsToMs(endAll - startAll);
|
|
581
|
+
let min = Infinity;
|
|
582
|
+
let max = -Infinity;
|
|
583
|
+
let sum = 0;
|
|
584
|
+
for (const l of latencies) {
|
|
585
|
+
if (l < min) min = l;
|
|
586
|
+
if (l > max) max = l;
|
|
587
|
+
sum += l;
|
|
588
|
+
}
|
|
589
|
+
const avg = sum / latencies.length;
|
|
590
|
+
const callsPerSec = iterations / (totalMs / 1e3);
|
|
591
|
+
return {
|
|
592
|
+
name,
|
|
593
|
+
iterations,
|
|
594
|
+
warmup,
|
|
595
|
+
callsPerSec,
|
|
596
|
+
avgLatencyMs: avg,
|
|
597
|
+
minLatencyMs: min,
|
|
598
|
+
maxLatencyMs: max
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function pickArgs(fnName, jsArity, callerArgs) {
|
|
602
|
+
if (Array.isArray(callerArgs)) return callerArgs;
|
|
603
|
+
return jsArity === 0 ? [] : [1, 2];
|
|
604
|
+
}
|
|
605
|
+
function mapArgsForNative(fnName, args, opts) {
|
|
606
|
+
const keepAlive = [];
|
|
607
|
+
if (fnName === "sum_u8") {
|
|
608
|
+
const buf = args[0];
|
|
609
|
+
const n = args[1];
|
|
610
|
+
if (buf && typeof n === "number" && ArrayBuffer.isView(buf)) {
|
|
611
|
+
const nb = alloc(buf.byteLength);
|
|
612
|
+
nb.write(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
613
|
+
keepAlive.push(nb);
|
|
614
|
+
return { args: [nb, n], keepAlive };
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (fnName === "dot_f64") {
|
|
618
|
+
const a = args[0];
|
|
619
|
+
const b = args[1];
|
|
620
|
+
const n = args[2];
|
|
621
|
+
if (a && b && typeof n === "number" && ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
|
622
|
+
const na = alloc(a.byteLength);
|
|
623
|
+
na.write(new Uint8Array(a.buffer, a.byteOffset, a.byteLength));
|
|
624
|
+
const nb = alloc(b.byteLength);
|
|
625
|
+
nb.write(new Uint8Array(b.buffer, b.byteOffset, b.byteLength));
|
|
626
|
+
keepAlive.push(na, nb);
|
|
627
|
+
return { args: [na.f64(0, n), nb.f64(0, n), n], keepAlive };
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (fnName === "saxpy_f64") {
|
|
631
|
+
const a = args[0];
|
|
632
|
+
const x = args[1];
|
|
633
|
+
const y = args[2];
|
|
634
|
+
const n = args[3];
|
|
635
|
+
if (typeof a === "number" && x && y && typeof n === "number" && ArrayBuffer.isView(x) && ArrayBuffer.isView(y)) {
|
|
636
|
+
const nx = alloc(x.byteLength);
|
|
637
|
+
nx.write(new Uint8Array(x.buffer, x.byteOffset, x.byteLength));
|
|
638
|
+
const ny = alloc(y.byteLength);
|
|
639
|
+
ny.write(new Uint8Array(y.buffer, y.byteOffset, y.byteLength));
|
|
640
|
+
keepAlive.push(nx, ny);
|
|
641
|
+
return { args: [a, nx.address, ny.address, n], keepAlive };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (fnName === "matmul_f32") {
|
|
645
|
+
const A = args[0];
|
|
646
|
+
const B = args[1];
|
|
647
|
+
const C = args[2];
|
|
648
|
+
const M = args[3];
|
|
649
|
+
const K = args[4];
|
|
650
|
+
const N = args[5];
|
|
651
|
+
if (A && B && C && typeof M === "number" && typeof K === "number" && typeof N === "number" && ArrayBuffer.isView(A) && ArrayBuffer.isView(B) && ArrayBuffer.isView(C)) {
|
|
652
|
+
const nA = alloc(A.byteLength);
|
|
653
|
+
nA.write(new Uint8Array(A.buffer, A.byteOffset, A.byteLength));
|
|
654
|
+
const nB = alloc(B.byteLength);
|
|
655
|
+
nB.write(new Uint8Array(B.buffer, B.byteOffset, B.byteLength));
|
|
656
|
+
const nC = alloc(C.byteLength);
|
|
657
|
+
nC.write(new Uint8Array(C.buffer, C.byteOffset, C.byteLength));
|
|
658
|
+
keepAlive.push(nA, nB, nC);
|
|
659
|
+
return {
|
|
660
|
+
args: [
|
|
661
|
+
nA.f32(0, M * K),
|
|
662
|
+
nB.f32(0, K * N),
|
|
663
|
+
nC.f32(0, M * N),
|
|
664
|
+
M,
|
|
665
|
+
K,
|
|
666
|
+
N
|
|
667
|
+
],
|
|
668
|
+
keepAlive
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (fnName === "xor_u8") {
|
|
673
|
+
const a = args[0];
|
|
674
|
+
const b = args[1];
|
|
675
|
+
const out = args[2];
|
|
676
|
+
const n = args[3];
|
|
677
|
+
if (a && b && out && typeof n === "number" && ArrayBuffer.isView(a) && ArrayBuffer.isView(b) && ArrayBuffer.isView(out)) {
|
|
678
|
+
if (opts?.isolation === "worker") {
|
|
679
|
+
return { args: [a, b, out, n], keepAlive };
|
|
680
|
+
}
|
|
681
|
+
const na = alloc(a.byteLength);
|
|
682
|
+
na.write(new Uint8Array(a.buffer, a.byteOffset, a.byteLength));
|
|
683
|
+
const nb = alloc(b.byteLength);
|
|
684
|
+
nb.write(new Uint8Array(b.buffer, b.byteOffset, b.byteLength));
|
|
685
|
+
const no = alloc(out.byteLength);
|
|
686
|
+
keepAlive.push(na, nb, no);
|
|
687
|
+
return { args: [na.address, nb.address, no.address, n], keepAlive };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (fnName === "crc32_u8") {
|
|
691
|
+
const buf = args[0];
|
|
692
|
+
const n = args[1];
|
|
693
|
+
if (buf && typeof n === "number" && ArrayBuffer.isView(buf)) {
|
|
694
|
+
if (opts?.isolation === "worker") {
|
|
695
|
+
return { args: [buf, n], keepAlive };
|
|
696
|
+
}
|
|
697
|
+
const nb = alloc(buf.byteLength);
|
|
698
|
+
nb.write(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
699
|
+
keepAlive.push(nb);
|
|
700
|
+
return { args: [nb.address, n], keepAlive };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (fnName === "histogram_u8") {
|
|
704
|
+
const buf = args[0];
|
|
705
|
+
const n = args[1];
|
|
706
|
+
const out = args[2];
|
|
707
|
+
if (buf && out && typeof n === "number" && ArrayBuffer.isView(buf) && ArrayBuffer.isView(out)) {
|
|
708
|
+
if (opts?.isolation === "worker") {
|
|
709
|
+
return { args: [buf, n, out], keepAlive };
|
|
710
|
+
}
|
|
711
|
+
const nb = alloc(buf.byteLength);
|
|
712
|
+
nb.write(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
|
|
713
|
+
const no = alloc(out.byteLength);
|
|
714
|
+
keepAlive.push(nb, no);
|
|
715
|
+
return { args: [nb.address, n, no.address], keepAlive };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return { args, keepAlive };
|
|
719
|
+
}
|
|
720
|
+
async function benchmarkNativeFunction(nativePath, fnName, opts) {
|
|
721
|
+
const iterations = opts?.iterations ?? 1e5;
|
|
722
|
+
const warmup = opts?.warmup ?? Math.min(1e4, Math.max(100, Math.floor(iterations * 0.05)));
|
|
723
|
+
const mode = opts?.mode ?? "sync";
|
|
724
|
+
assertSafeRun(iterations);
|
|
725
|
+
const pointerHeavy = /* @__PURE__ */ new Set([
|
|
726
|
+
"sum_u8",
|
|
727
|
+
"dot_f64",
|
|
728
|
+
"saxpy_f64",
|
|
729
|
+
"matmul_f32",
|
|
730
|
+
"xor_u8",
|
|
731
|
+
"crc32_u8",
|
|
732
|
+
"histogram_u8"
|
|
733
|
+
]);
|
|
734
|
+
const isolation = mode === "worker" ? "worker" : pointerHeavy.has(fnName) ? "worker" : "in-process";
|
|
735
|
+
trace("benchmark loadNative: begin", { nativePath, fnName, isolation });
|
|
736
|
+
const mod = await loadNative(nativePath, { isolation });
|
|
737
|
+
trace("benchmark loadNative: done", { nativePath, fnName });
|
|
738
|
+
const fn = mod?.[fnName];
|
|
739
|
+
if (typeof fn !== "function") {
|
|
740
|
+
throw new Error(`Function not found: ${fnName}`);
|
|
741
|
+
}
|
|
742
|
+
const isMaybeAsync = isolation === "worker";
|
|
743
|
+
const { args, keepAlive } = mapArgsForNative(
|
|
744
|
+
fnName,
|
|
745
|
+
pickArgs(fnName, fn.length, opts?.args),
|
|
746
|
+
{ isolation }
|
|
747
|
+
);
|
|
748
|
+
trace("benchmark args ready", {
|
|
749
|
+
fnName,
|
|
750
|
+
argc: args.length,
|
|
751
|
+
argTypes: args.map(
|
|
752
|
+
(a) => a == null ? "null" : ArrayBuffer.isView(a) ? a.constructor?.name : typeof a
|
|
753
|
+
),
|
|
754
|
+
keepAlive: keepAlive.length
|
|
755
|
+
});
|
|
756
|
+
trace("benchmark warmup: begin", { fnName, warmup });
|
|
757
|
+
for (let i = 0; i < warmup; i++) {
|
|
758
|
+
const out = fn(...args);
|
|
759
|
+
if (isMaybeAsync || out instanceof Promise) await out;
|
|
760
|
+
}
|
|
761
|
+
trace("benchmark warmup: done", { fnName });
|
|
762
|
+
const latencies = [];
|
|
763
|
+
const startAll = nowNs();
|
|
764
|
+
trace("benchmark loop: begin", { fnName, iterations });
|
|
765
|
+
for (let i = 0; i < iterations; i++) {
|
|
766
|
+
const t0 = nowNs();
|
|
767
|
+
const out = fn(...args);
|
|
768
|
+
if (isMaybeAsync || out instanceof Promise) await out;
|
|
769
|
+
const t1 = nowNs();
|
|
770
|
+
latencies.push(nsToMs(t1 - t0));
|
|
771
|
+
if (isTraceEnabled() && (i === 0 || i === 1e4 || i === 1e5 || i === iterations - 1)) {
|
|
772
|
+
trace("benchmark progress", { fnName, i: i + 1 });
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
trace("benchmark loop: done", { fnName });
|
|
776
|
+
void keepAlive;
|
|
777
|
+
const endAll = nowNs();
|
|
778
|
+
const totalMs = nsToMs(endAll - startAll);
|
|
779
|
+
let min = Infinity;
|
|
780
|
+
let max = -Infinity;
|
|
781
|
+
let sum = 0;
|
|
782
|
+
for (const l of latencies) {
|
|
783
|
+
if (l < min) min = l;
|
|
784
|
+
if (l > max) max = l;
|
|
785
|
+
sum += l;
|
|
786
|
+
}
|
|
787
|
+
const avg = sum / latencies.length;
|
|
788
|
+
const callsPerSec = iterations / (totalMs / 1e3);
|
|
789
|
+
return {
|
|
790
|
+
fnName,
|
|
791
|
+
mode,
|
|
792
|
+
iterations,
|
|
793
|
+
warmup,
|
|
794
|
+
callsPerSec,
|
|
795
|
+
avgLatencyMs: avg,
|
|
796
|
+
minLatencyMs: min,
|
|
797
|
+
maxLatencyMs: max
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
async function benchmarkCompareSyncVsWorker(nativePath, fnName, opts) {
|
|
801
|
+
const sync = await benchmarkNativeFunction(nativePath, fnName, {
|
|
802
|
+
...opts,
|
|
803
|
+
mode: "sync"
|
|
804
|
+
});
|
|
805
|
+
const worker = await benchmarkNativeFunction(nativePath, fnName, {
|
|
806
|
+
...opts,
|
|
807
|
+
mode: "worker"
|
|
808
|
+
});
|
|
809
|
+
return { sync, worker };
|
|
810
|
+
}
|
|
811
|
+
async function benchmarkCompareTraditionalVsRelaxnative(nativePath, fnName, opts) {
|
|
812
|
+
const baseline = opts?.baseline ?? (() => {
|
|
813
|
+
throw new Error(
|
|
814
|
+
`Missing JS baseline for fn=${fnName}. Provide opts.baseline (a JS function) and opts.args (a JSON-serializable args array) to compare.`
|
|
815
|
+
);
|
|
816
|
+
})();
|
|
817
|
+
const baselineName = opts?.baselineName ?? "traditional-js";
|
|
818
|
+
const traditional = await benchmarkJsFunction(baselineName, baseline, {
|
|
819
|
+
iterations: opts?.iterations,
|
|
820
|
+
warmup: opts?.warmup,
|
|
821
|
+
args: pickArgs(fnName, baseline.length, opts?.args)
|
|
822
|
+
});
|
|
823
|
+
const relaxnative = await benchmarkCompareSyncVsWorker(nativePath, fnName, opts);
|
|
824
|
+
return { traditional, relaxnative };
|
|
825
|
+
}
|
|
826
|
+
function formatBenchmarkResult(r) {
|
|
827
|
+
const f = (n) => Number.isFinite(n) ? n.toFixed(3) : String(n);
|
|
828
|
+
return [
|
|
829
|
+
`${r.fnName} (${r.mode})`,
|
|
830
|
+
` iterations: ${r.iterations} (warmup ${r.warmup})`,
|
|
831
|
+
` calls/sec: ${f(r.callsPerSec)}`,
|
|
832
|
+
` avg ms: ${f(r.avgLatencyMs)}`,
|
|
833
|
+
` min ms: ${f(r.minLatencyMs)}`,
|
|
834
|
+
` max ms: ${f(r.maxLatencyMs)}`
|
|
835
|
+
].join("\n");
|
|
836
|
+
}
|
|
837
|
+
function formatBenchmarkCompare(res) {
|
|
838
|
+
return [formatBenchmarkResult(res.sync), "", formatBenchmarkResult(res.worker)].join("\n");
|
|
839
|
+
}
|
|
840
|
+
function formatBenchmarkTraditionalCompare(res) {
|
|
841
|
+
const f = (n) => Number.isFinite(n) ? n.toFixed(3) : String(n);
|
|
842
|
+
const t = res.traditional;
|
|
843
|
+
const speedup = (relax) => {
|
|
844
|
+
if (!Number.isFinite(relax) || !Number.isFinite(t.callsPerSec) || t.callsPerSec <= 0) return "n/a";
|
|
845
|
+
const s = relax / t.callsPerSec;
|
|
846
|
+
return s.toFixed(2) + "x";
|
|
847
|
+
};
|
|
848
|
+
const traditionalBlock = [
|
|
849
|
+
`${t.name} (baseline)`,
|
|
850
|
+
` iterations: ${t.iterations} (warmup ${t.warmup})`,
|
|
851
|
+
` calls/sec: ${f(t.callsPerSec)}`,
|
|
852
|
+
` avg ms: ${f(t.avgLatencyMs)}`,
|
|
853
|
+
` min ms: ${f(t.minLatencyMs)}`,
|
|
854
|
+
` max ms: ${f(t.maxLatencyMs)}`
|
|
855
|
+
].join("\n");
|
|
856
|
+
const summary = [
|
|
857
|
+
"Speedup vs baseline (higher is better)",
|
|
858
|
+
` sync: ${speedup(res.relaxnative.sync.callsPerSec)}`,
|
|
859
|
+
` worker: ${speedup(res.relaxnative.worker.callsPerSec)}`
|
|
860
|
+
].join("\n");
|
|
861
|
+
return [
|
|
862
|
+
traditionalBlock,
|
|
863
|
+
"",
|
|
864
|
+
summary,
|
|
865
|
+
"",
|
|
866
|
+
formatBenchmarkResult(res.relaxnative.sync),
|
|
867
|
+
"",
|
|
868
|
+
formatBenchmarkResult(res.relaxnative.worker)
|
|
869
|
+
].join("\n");
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export {
|
|
873
|
+
getProjectRoot,
|
|
874
|
+
getRegistryRoot,
|
|
875
|
+
getInstalledPackageDir,
|
|
876
|
+
resolveRegistryImport,
|
|
877
|
+
normalizeTrustLevel,
|
|
878
|
+
trustPolicy,
|
|
879
|
+
readRelaxJson,
|
|
880
|
+
installPackage,
|
|
881
|
+
installPackageEnforcingTrust,
|
|
882
|
+
removePackage,
|
|
883
|
+
listPackages,
|
|
884
|
+
updateIndex,
|
|
885
|
+
discoverNativeTestFiles,
|
|
886
|
+
discoverNativeTestsFromFile,
|
|
887
|
+
discoverNativeTests,
|
|
888
|
+
formatNativeTestResults,
|
|
889
|
+
runNativeTests,
|
|
890
|
+
getSourceLine,
|
|
891
|
+
benchmarkJsFunction,
|
|
892
|
+
benchmarkNativeFunction,
|
|
893
|
+
benchmarkCompareSyncVsWorker,
|
|
894
|
+
benchmarkCompareTraditionalVsRelaxnative,
|
|
895
|
+
formatBenchmarkResult,
|
|
896
|
+
formatBenchmarkCompare,
|
|
897
|
+
formatBenchmarkTraditionalCompare
|
|
898
|
+
};
|