Add authenticated protocol 2 and TypeScript WASM client
This commit is contained in:
1 parent
88ceeb2ae9
commit
237d404bcd
52 files changed
+3249
-150
No files matched your search
@@ -0,0 +1,39 @@
|
||||
// Starts only the disposable localhost fixture; never connects to deployment hosts.
|
||||
import { spawn } from 'node:child_process';
|
||||
import net from 'node:net';
|
||||
import { setTimeout as pause } from 'node:timers/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
const executable = new URL('../evaluation/server/build/install/server/bin/server', import.meta.url).pathname;
|
||||
if (!existsSync(executable)) throw Error('Build the fixture first: npm run fixture:build');
|
||||
const listener = net.createServer();
|
||||
await new Promise((resolve, reject) => listener.once('error', reject).listen(0, '127.0.0.1', resolve));
|
||||
const port = listener.address().port;
|
||||
await new Promise(resolve => listener.close(resolve));
|
||||
const server = spawn(executable, [String(port)], { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let diagnostics = ''; let exited = false;
|
||||
server.stdout.on('data', b => { diagnostics = (diagnostics + b).slice(-8000); });
|
||||
server.stderr.on('data', b => { diagnostics = (diagnostics + b).slice(-8000); });
|
||||
server.on('exit', () => { exited = true; });
|
||||
server.on('error', e => { diagnostics += e.message; exited = true; });
|
||||
try {
|
||||
let ready = false;
|
||||
for (let n = 0; n < 100 && !exited; n++) {
|
||||
ready = await new Promise(resolve => {
|
||||
const socket = net.connect(port, '127.0.0.1');
|
||||
socket.once('connect', () => { socket.destroy(); resolve(true); });
|
||||
socket.once('error', () => resolve(false));
|
||||
});
|
||||
if (ready) break;
|
||||
await pause(100);
|
||||
}
|
||||
if (!ready) throw Error(`Local JVM fixture did not start:\n${diagnostics}`);
|
||||
const run = spawn(process.execPath, ['--test', 'test/interop.test.mjs'], { stdio: 'inherit', env: { ...process.env, KILOPARSEC_TEST_URL: `ws://127.0.0.1:${port}/kp` } });
|
||||
process.exitCode = await new Promise(resolve => run.once('exit', code => resolve(code ?? 1)));
|
||||
} finally {
|
||||
server.kill('SIGTERM');
|
||||
if (!exited) {
|
||||
const hardStop = setTimeout(() => server.kill('SIGKILL'), 3000);
|
||||
await new Promise(resolve => { if (exited) resolve(); else server.once('exit', resolve); });
|
||||
clearTimeout(hardStop);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/** Optimized Kotlin/Wasm baseline; includes all required JS and sodium WASM. */
|
||||
import { build } from 'esbuild';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdir, copyFile, readFile, writeFile } from 'node:fs/promises';
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import path from 'node:path';
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const output = path.join(root, 'evaluation/build/measurement/wasm');
|
||||
await mkdir(output, { recursive: true });
|
||||
await build({
|
||||
absWorkingDir: root, entryPoints: ['evaluation/wasm-entry.mjs'],
|
||||
outfile: path.join(output, 'client.mjs'), bundle: true, minify: true,
|
||||
platform: 'node', format: 'esm', sourcemap: false,
|
||||
banner: { js: "import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const __filename = import.meta.filename; const __dirname = import.meta.dirname;" },
|
||||
});
|
||||
const wasmName = 'kiloparsec-evaluation-wasm-baseline.wasm';
|
||||
await copyFile(path.join(root, 'evaluation/wasm-baseline/build/compileSync/wasmJs/main/productionExecutable/optimized', wasmName), path.join(output, wasmName));
|
||||
const assets = [];
|
||||
for (const name of ['client.mjs', wasmName]) {
|
||||
const bytes = await readFile(path.join(output, name));
|
||||
assets.push({ path: `evaluation/build/measurement/wasm/${name}`, sha256: createHash('sha256').update(bytes).digest('hex'), rawBytes: bytes.length, gzipBytes: gzipSync(bytes, { level: 9 }).length });
|
||||
}
|
||||
const samples = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const program = `const before=process.memoryUsage().rss; const start=performance.now();
|
||||
try { const {api}=await import(${JSON.stringify(pathToFileURL(path.join(output, 'client.mjs')).href)});
|
||||
await new Promise((resolve,reject)=>api.initializeIdentity(resolve,reject)); api.close();
|
||||
console.log(JSON.stringify({milliseconds:performance.now()-start,rss:process.memoryUsage().rss,rssDelta:process.memoryUsage().rss-before}));
|
||||
} catch(e) { console.error(String(e)); process.exitCode=1; }`;
|
||||
const child = spawnSync(process.execPath, ['--input-type=module', '-e', program], { encoding: 'utf8', timeout: 15000 });
|
||||
if (child.status !== 0) throw Error(child.stderr || String(child.error));
|
||||
samples.push(JSON.parse(child.stdout.trim().split('\n').at(-1)));
|
||||
}
|
||||
const median = key => samples.map(s => s[key]).sort((a,b)=>a-b)[2];
|
||||
const report = {
|
||||
measuredAt: new Date().toISOString(), node: process.version, platform: process.platform,
|
||||
architecture: process.arch, kotlin: '2.2.21', sodiumWrapper: '0.7.15', rawSodium: '0.7.16', sourceVersion: '0.8.0-SNAPSHOT', protocol: 2,
|
||||
notes: [
|
||||
'Actual optimized production Kotlin/Wasm executable, not klib or test runtime.',
|
||||
'Minified Node ESM loader includes all sodium and joda dependencies; Kotlin wasm is a separate asset.',
|
||||
'Sodium WASM is embedded in client.mjs. Gzip level9 is applied independently to each transferred asset and summed.',
|
||||
'Full DH, expected server identity pin, client identity and echo RPC retained. Export bridge uses strings/callbacks.',
|
||||
'Cold fresh Node process per sample, module loading plus sodium init plus one new Ed25519 identity; process startup excluded.',
|
||||
], assets,
|
||||
rawBytes: assets.reduce((n,a)=>n+a.rawBytes,0), gzipBytes: assets.reduce((n,a)=>n+a.gzipBytes,0),
|
||||
medianMilliseconds: median('milliseconds'), medianRssBytes: median('rss'), medianRssDeltaBytes: median('rssDelta'), samples,
|
||||
};
|
||||
await writeFile(path.join(output, 'results.json'), JSON.stringify(report,null,2)+'\n');
|
||||
console.log(JSON.stringify(report,null,2));
|
||||
@@ -0,0 +1,36 @@
|
||||
/** Same-platform bundled measurements; run `npm run build` and Kotlin production build first. */
|
||||
import { build } from 'esbuild';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
const root = fileURLToPath(new URL('../', import.meta.url));
|
||||
const output = path.join(root, 'evaluation/build/measurement');
|
||||
await mkdir(output, { recursive: true });
|
||||
const entries = { ts: 'dist/index.js', kotlin: 'evaluation/baseline-entry.cjs' };
|
||||
const report = { protocol: 2, sourceVersion: '0.8.0-SNAPSHOT', measuredAt: new Date().toISOString(), node: process.version, platform: process.platform, architecture: process.arch, sodiumWrapper: '0.7.15', notes: [
|
||||
'Node CJS bundles, same esbuild settings: bundle/minify/platform=node; gzip level9, no source maps.',
|
||||
'Both include sodium WASM. TS includes all exported API; Kotlin retains only baseline raw bridge, identity init and echo API.',
|
||||
'Cold fresh Node subprocess per sample; elapsed begins immediately before module require, includes WASM init and one new Ed25519 identity.',
|
||||
'RSS includes Node VM; delta measures process resident growth after workload. Five samples, use medians; no process startup time included.',
|
||||
'These figures compare retained subsets, not full feature equivalence or proof of browser runtime compatibility.'
|
||||
], artifacts: {} };
|
||||
for (const [name, entry] of Object.entries(entries)) {
|
||||
const outfile = path.join(output, `${name}.cjs`);
|
||||
await build({ absWorkingDir: root, entryPoints: [entry], outfile, bundle: true, minify: true, platform: 'node', format: 'cjs', sourcemap: false, logLevel: 'warning' });
|
||||
const data = await readFile(outfile);
|
||||
const samples = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const program = `const before=process.memoryUsage().rss; const start=performance.now();
|
||||
(async()=>{const m=require(${JSON.stringify(outfile)}); ${name === 'ts' ? 'await m.generateSigningKey();' : 'const api=await m.load(); await api.initializeIdentity();'}
|
||||
console.log(JSON.stringify({milliseconds:performance.now()-start,rss:process.memoryUsage().rss,rssDelta:process.memoryUsage().rss-before}));})().catch(e=>{console.error(e);process.exitCode=1});`;
|
||||
const child = spawnSync(process.execPath, ['-e', program], { encoding: 'utf8', timeout: 15000 });
|
||||
if (child.status !== 0) throw Error(`${name}: ${child.stderr || child.error}`);
|
||||
samples.push(JSON.parse(child.stdout.trim().split('\n').at(-1)));
|
||||
}
|
||||
const median = key => samples.map(s => s[key]).sort((a,b)=>a-b)[2];
|
||||
report.artifacts[name] = { rawBytes: data.length, gzipBytes: gzipSync(data, { level: 9 }).length, medianMilliseconds: median('milliseconds'), medianRssBytes: median('rss'), medianRssDeltaBytes: median('rssDelta'), samples };
|
||||
}
|
||||
await writeFile(path.join(output, 'results.json'), JSON.stringify(report, null, 2) + '\n');
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
Reference in new issue
Block a user