Merge branch 'feature/wasm-example-bench' into 'main'

Draft: Add example-bench app for veilid-wasm

See merge request veilid/veilid!369
This commit is contained in:
Brandon Vandegrift 2025-03-29 16:22:18 +00:00
commit 4ba0cc6f34
19 changed files with 3577 additions and 0 deletions

24
veilid-wasm/example-bench/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,5 @@
# Veilid WASM Example Bench
This is an example application used as a test bench for veilid-wasm.
Run `npm run build:wasm`, then run `npm run dev` to start the dev server.

View File

@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Veilid WASM Example Bench</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2919
veilid-wasm/example-bench/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
{
"name": "veilid-wasm-example-bench",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "npm run build:wasm && tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"build:wasm": "cd .. && wasm-pack build --release --target web --weak-refs --out-dir ./example-bench/veilid-wasm-pkg"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react-swc": "^3.8.0",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"events": "^3.3.0",
"globals": "^15.15.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"uuid": "^11.1.0",
"veilid-wasm": "file:./veilid-wasm-pkg",
"vite": "^6.2.0",
"vite-plugin-wasm": "^3.4.1"
}
}

View File

@ -0,0 +1,14 @@
import { DhtStressTest } from './bench/DhtStressTest';
function App() {
return (
<>
<div>
<h1>Veilid DHT Stress Test</h1>
<DhtStressTest />
</div>
</>
)
}
export default App

View File

@ -0,0 +1,83 @@
import { useState } from 'react';
import { getRoutingContext } from '../veilid-utils/veilid-core';
async function dhtStressTest() {
const routingContext = getRoutingContext();
const recordCount = 30;
const subkeyCount = 32;
const inspectCount = 1;
// Create a 32KB data buffer
const dataSize = 32 * 1024; // 32KB in bytes
const dataArray = new Uint8Array(dataSize);
// Fill the array with some pattern (using values 0-255 repeating)
for (let i = 0; i < dataSize; i++) {
dataArray[i] = i % 256;
}
let a = Array();
for (var r = 0; r < recordCount; r++) {
let dhtRecord = await routingContext.createDhtRecord(
{
kind: 'DFLT',
o_cnt: subkeyCount,
},
);
// Set all subkeys
for (var n = 0; n < subkeyCount; n++) {
a.push((async () => {
// const measureName = `${r}-setDhtValue-${n}`;
// performance.mark(measureName + "-start")
await routingContext.setDhtValue(
dhtRecord.key,
n,
dataArray,
);
// performance.measure(measureName, measureName + "-start")
})());
}
// Inspect all records N times while sets are happening
for (var n = 0; n < inspectCount; n++) {
a.push((async () => {
const measureName = `${r}-inspectDhtRecord-${n}`;
performance.mark(measureName + "-start")
await routingContext.inspectDhtRecord(
dhtRecord.key,
null,
"SyncSet",
);
performance.measure(measureName, measureName + "-start")
})());
}
}
// Wait for all results
await Promise.all(a)
}
export function DhtStressTest() {
const [isRunning, setIsRunning] = useState(false);
return (
<button onClick={() => {
if (isRunning) {
return;
}
setIsRunning(true);
dhtStressTest().finally(() => {
setIsRunning(false);
});
}} disabled={isRunning}>
{isRunning ? 'Running...' : 'Run DHT Stress Test'}
</button>
)
}

View File

@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@ -0,0 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import { initVeilid, startVeilid } from './veilid-utils/veilid-core.ts'
await initVeilid();
await startVeilid();
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1,2 @@
export const textDecoder = new TextDecoder();
export const textEncoder = new TextEncoder();

View File

@ -0,0 +1,156 @@
import { VeilidConfigInner, VeilidWASMConfig } from 'veilid-wasm';
export interface VeilidConfigOptions {
namespace: string;
password: string;
}
export const veildCoreInitConfig: VeilidWASMConfig = {
logging: {
api: {
enabled: true,
level: 'Info',
ignore_log_targets: [],
},
performance: {
enabled: false,
level: 'Info',
logs_in_timings: false,
logs_in_console: false,
ignore_log_targets: [],
},
},
};
export function getVeilidCoreStartupConfig(options: VeilidConfigOptions) {
const config: VeilidConfigInner = {
program_name: 'vdrop.link',
namespace: options.namespace,
capabilities: {
disable: [],
},
protected_store: {
allow_insecure_fallback: true,
always_use_insecure_storage: true,
directory: '',
delete: false,
device_encryption_key_password: options.password,
// "new_device_encryption_key_password": "<secret>"
},
table_store: {
directory: 'table_store',
delete: false,
},
block_store: {
directory: 'block_store',
delete: false,
},
network: {
connection_initial_timeout_ms: 2000,
connection_inactivity_timeout_ms: 60000,
max_connections_per_ip4: 32,
max_connections_per_ip6_prefix: 32,
max_connections_per_ip6_prefix_size: 56,
max_connection_frequency_per_min: 128,
client_allowlist_timeout_ms: 300000,
reverse_connection_receipt_time_ms: 5000,
hole_punch_receipt_time_ms: 5000,
network_key_password: '',
routing_table: {
node_id: [],
node_id_secret: [],
bootstrap: [
'ws://bootstrap.veilid.net:5150/ws',
'ws://veilid-bootstrap.bgrift.com:5150/ws'
],
limit_over_attached: 64,
limit_fully_attached: 32,
limit_attached_strong: 16,
limit_attached_good: 8,
limit_attached_weak: 4,
},
rpc: {
concurrency: 8,
queue_size: 1024,
max_timestamp_behind_ms: 10000,
max_timestamp_ahead_ms: 10000,
timeout_ms: 5000,
// timeout_ms: 10000,
max_route_hop_count: 4,
default_route_hop_count: 1,
},
dht: {
max_find_node_count: 20,
resolve_node_timeout_ms: 10000,
resolve_node_count: 1,
resolve_node_fanout: 4,
get_value_timeout_ms: 10000,
get_value_count: 3,
get_value_fanout: 4,
set_value_timeout_ms: 10000,
set_value_count: 5,
set_value_fanout: 4,
min_peer_count: 20,
min_peer_refresh_time_ms: 60000,
validate_dial_info_receipt_time_ms: 2000,
local_subkey_cache_size: 128,
local_max_subkey_cache_memory_mb: 256,
remote_subkey_cache_size: 0,
remote_max_records: 0,
remote_max_subkey_cache_memory_mb: 0,
remote_max_storage_space_mb: 0,
public_watch_limit: 32,
member_watch_limit: 32,
max_watch_expiration_ms: 600000,
},
upnp: true,
detect_address_changes: true,
restricted_nat_retries: 0,
tls: {
certificate_path: '',
private_key_path: '',
connection_initial_timeout_ms: 2000,
},
application: {
https: {
enabled: false,
listen_address: ':5150',
path: 'app',
},
http: {
enabled: false,
listen_address: ':5150',
path: 'app',
},
},
protocol: {
udp: {
enabled: false,
socket_pool_size: 0,
listen_address: '',
},
tcp: {
connect: false,
listen: false,
max_connections: 32,
listen_address: '',
},
ws: {
connect: true,
listen: true,
max_connections: 128,
listen_address: ':5150',
path: 'ws',
},
wss: {
connect: true,
listen: false,
max_connections: 32,
listen_address: '',
path: 'ws',
},
},
},
};
return config;
}

View File

@ -0,0 +1,101 @@
import * as veilid from 'veilid-wasm';
import loadVeilidWasm, {
veilidClient,
VeilidRoutingContext,
} from 'veilid-wasm'
import { getVeilidCoreStartupConfig, veildCoreInitConfig } from './veilid-config';
import { veilidEventEmitter } from './veilid-event-emitter';
import { v4 as uuidV4 } from 'uuid';
let loadVeilidWasmPromise: Promise<veilid.InitOutput>;
let isVeilidWasmLoaded = false;
export async function initVeilid() {
if (isVeilidWasmLoaded) {
return;
}
if (!loadVeilidWasmPromise) {
console.log('loading veilid-wasm...');
loadVeilidWasmPromise = loadVeilidWasm();
}
await loadVeilidWasmPromise;
isVeilidWasmLoaded = true;
console.log('veilid-wasm loaded!');
}
let IS_VEILID_RUNNING = false;
export function isVeilidRunning() {
return IS_VEILID_RUNNING;
}
export async function startVeilid() {
const config = getVeilidCoreStartupConfig({
namespace: `example-bench-${uuidV4()}`,
password: 'singleton',
});
console.log('starting veilid core...');
await veilidClient.initializeCore(veildCoreInitConfig);
veilidClient.startupCore(async (data) => {
veilidEventEmitter.emit(data.kind, data);
}, JSON.stringify(config));
return veilidClient;
}
// Listen to updates, and `attach()` once startup is complete.
veilidEventEmitter.on('Log', (data) => {
switch (data?.log_level) {
case 'Warn':
console.warn(data.message);
break;
case 'Info':
console.info(data.message);
break;
case 'Debug':
console.log(data.message);
break;
default:
console.log(data.message);
break;
}
// TODO: Wonder if there's a better way to detect startup complete.
if (data.message?.includes('Veilid API startup complete')) {
console.log('veilid core started!');
IS_VEILID_RUNNING = true;
console.log('Veilid Version', veilidClient.versionString());
console.log('attaching to veilid network');
veilidClient.attach();
}
});
veilidEventEmitter.on('RouteChange', (routeChange) => {
console.log('ROUTE CHANGE', routeChange);
});
veilidEventEmitter.on('ValueChange', (valueChange) => {
console.log('VALUE CHANGE', valueChange);
});
export async function stopVeilid() {
if (ROUTING_CONTEXT_SINGLETON) {
ROUTING_CONTEXT_SINGLETON.free();
ROUTING_CONTEXT_SINGLETON = undefined;
}
if (isVeilidRunning()) {
IS_VEILID_RUNNING = false;
await veilidClient.detach();
await veilidClient.shutdownCore();
}
}
let ROUTING_CONTEXT_SINGLETON: VeilidRoutingContext | undefined;
export function getRoutingContext() {
if (!ROUTING_CONTEXT_SINGLETON) {
ROUTING_CONTEXT_SINGLETON = VeilidRoutingContext.create();
}
return ROUTING_CONTEXT_SINGLETON;
}

View File

@ -0,0 +1,43 @@
import * as veilid from 'veilid-wasm';
import EventEmitter from 'events';
export type VeilidUpdateKind = veilid.VeilidUpdate['kind'];
type VeilidUpdateKindMap = {
[TKind in VeilidUpdateKind]: Extract<veilid.VeilidUpdate, { kind: TKind }>;
};
export type VeilidUpdateType<TKind extends VeilidUpdateKind> =
VeilidUpdateKindMap[TKind];
/**
* A typesafe event emitter for VeilidUpdate events.
*/
class VeilidEventEmitter extends EventEmitter {
on<TKind extends VeilidUpdateKind>(
event: TKind,
listener: (veilidUpdate: VeilidUpdateType<TKind>) => void
): this {
super.on(event, listener);
return this;
}
emit<TKind extends VeilidUpdateKind>(
event: TKind,
veilidUpdate: VeilidUpdateType<TKind>
): boolean {
return super.emit(event, veilidUpdate);
}
removeListener<TKind extends VeilidUpdateKind>(
event: TKind,
listener: (veilidUpdate: VeilidUpdateType<TKind>) => void
): this {
super.removeListener(event, listener);
return this;
}
}
/**
* a singleton instance of VeilidEventEmitter.
*/
export const veilidEventEmitter = new VeilidEventEmitter();

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import wasm from 'vite-plugin-wasm';
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), wasm()],
server: {
port: 5001,
},
})