feat: move registry and runtime into separate packages
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m32s

This commit is contained in:
2024-05-05 15:11:53 +02:00
parent f4853821d4
commit a01a409b97
29 changed files with 205 additions and 156 deletions

View File

@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"@nodes/registry": "link:../packages/registry",
"@nodes/runtime": "link:../packages/runtime",
"@nodes/ui": "link:../packages/ui",
"@nodes/utils": "link:../packages/utils",
"@sveltejs/kit": "^2.5.7",

View File

@ -73,35 +73,6 @@ export const debounce = (fn: Function, ms = 300) => {
export const clone: <T>(v: T) => T = "structedClone" in globalThis ? globalThis.structuredClone : (obj) => JSON.parse(JSON.stringify(obj));
export const createLogger = (() => {
let maxLength = 5;
return (scope: string) => {
maxLength = Math.max(maxLength, scope.length);
let muted = false;
let isGrouped = false;
function s(color: string, ...args: any) {
return isGrouped ? [...args] : [`[%c${scope.padEnd(maxLength, " ")}]:`, `color: ${color}`, ...args];
}
return {
log: (...args: any[]) => !muted && console.log(...s("#888", ...args)),
info: (...args: any[]) => !muted && console.info(...s("#888", ...args)),
warn: (...args: any[]) => !muted && console.warn(...s("#888", ...args)),
error: (...args: any[]) => console.error(...s("#f88", ...args)),
group: (...args: any[]) => { if (!muted) { console.groupCollapsed(...s("#888", ...args)); isGrouped = true; } },
groupEnd: () => { if (!muted) { console.groupEnd(); isGrouped = false } },
mute() {
muted = true;
},
unmute() {
muted = false;
}
}
}
})();
export function withSubComponents<A, B extends Record<string, any>>(

View File

@ -1,38 +0,0 @@
import type { RuntimeCache } from '@nodes/types';
import { openDB, type IDBPDatabase } from 'idb';
export class IndexDBCache implements RuntimeCache<ArrayBuffer> {
size: number = 100;
db: Promise<IDBPDatabase<ArrayBuffer>>;
private _cache = new Map<string, ArrayBuffer>();
constructor(id: string) {
this.db = openDB<ArrayBuffer>('cache/' + id, 1, {
upgrade(db) {
db.createObjectStore('keyval');
},
});
}
async get(key: string) {
let res = this._cache.get(key);
if (!res) {
res = await (await this.db).get('keyval', key);
}
if (res) {
this._cache.set(key, res);
}
return res;
}
async set(key: string, value: ArrayBuffer) {
this._cache.set(key, value);
const db = await this.db;
await db.put('keyval', value, key);
}
clear() {
this.db.then(db => db.clear('keyval'));
}
}

View File

@ -1,126 +0,0 @@
import { type NodeRegistry, type NodeDefinition, NodeDefinitionSchema, type RuntimeCache } from "@nodes/types";
import { createWasmWrapper } from "@nodes/utils";
import { createLogger } from "./helpers";
const log = createLogger("node-registry");
log.mute();
export class RemoteNodeRegistry implements NodeRegistry {
status: "loading" | "ready" | "error" = "loading";
private nodes: Map<string, NodeDefinition> = new Map();
cache?: RuntimeCache<ArrayBuffer>;
fetch: typeof fetch = globalThis.fetch.bind(globalThis);
constructor(private url: string) { }
async fetchUsers() {
const response = await this.fetch(`${this.url}/nodes/users.json`);
if (!response.ok) {
throw new Error(`Failed to load users`);
}
return response.json();
}
async fetchUser(userId: `${string}`) {
const response = await this.fetch(`${this.url}/nodes/${userId}.json`);
if (!response.ok) {
throw new Error(`Failed to load user ${userId}`);
}
return response.json();
}
async fetchCollection(userCollectionId: `${string}/${string}`) {
const response = await this.fetch(`${this.url}/nodes/${userCollectionId}.json`);
if (!response.ok) {
throw new Error(`Failed to load collection ${userCollectionId}`);
}
return response.json();
}
async fetchNodeDefinition(nodeId: `${string}/${string}/${string}`) {
const response = await this.fetch(`${this.url}/nodes/${nodeId}.json`);
if (!response.ok) {
throw new Error(`Failed to load node definition ${nodeId}`);
}
return response.json()
}
private async fetchNodeWasm(nodeId: `${string}/${string}/${string}`) {
const response = await this.fetch(`${this.url}/nodes/${nodeId}.wasm`);
if (!response.ok) {
if (this.cache) {
let value = await this.cache.get(nodeId);
if (value) {
return value;
}
}
throw new Error(`Failed to load node wasm ${nodeId}`);
}
return response.arrayBuffer();
}
async load(nodeIds: `${string}/${string}/${string}`[]) {
const a = performance.now();
const nodes = await Promise.all([...new Set(nodeIds).values()].map(async id => {
if (this.nodes.has(id)) {
return this.nodes.get(id)!;
}
const wasmBuffer = await this.fetchNodeWasm(id);
return this.register(wasmBuffer);
}));
const duration = performance.now() - a;
log.group("loaded nodes in", duration, "ms");
log.info(nodeIds);
log.info(nodes);
log.groupEnd();
this.status = "ready";
return nodes
}
async register(wasmBuffer: ArrayBuffer) {
const wrapper = createWasmWrapper(wasmBuffer);
const definition = NodeDefinitionSchema.safeParse(wrapper.get_definition());
if (definition.error) {
console.error(definition.error);
throw definition.error;
}
if (this.cache) {
await this.cache.set(definition.data.id, wasmBuffer);
}
let node = {
...definition.data,
execute: wrapper.execute
}
this.nodes.set(definition.data.id, node);
return node;
}
getNode(id: string) {
return this.nodes.get(id);
}
getAllNodes() {
return [...this.nodes.values()];
}
}

View File

@ -3,7 +3,7 @@
import { humanizeNumber } from "$lib/helpers";
import { Checkbox } from "@nodes/ui";
import localStore from "$lib/helpers/localStore";
import { type PerformanceData } from "./store";
import { type PerformanceData } from "@nodes/utils";
import BarSplit from "./BarSplit.svelte";
export let data: PerformanceData;

View File

@ -2,15 +2,13 @@
import { humanizeDuration, humanizeNumber } from "$lib/helpers";
import localStore from "$lib/helpers/localStore";
import SmallGraph from "./SmallGraph.svelte";
import type { PerformanceData, PerformanceStore } from "./store";
import type { PerformanceData, PerformanceStore } from "@nodes/utils";
export let store: PerformanceStore;
const open = localStore("node.performance.small.open", {
runtime: false,
fps: false,
vertices: false,
faces: false,
});
$: vertices = $store?.at(-1)?.["total-vertices"][0] || 0;
@ -54,29 +52,15 @@
</tr>
{/if}
<tr on:click={() => ($open.vertices = !$open.vertices)}>
<td>{$open.vertices ? "-" : "+"} vertices </td>
<tr>
<td>vertices </td>
<td>{humanizeNumber(vertices || 0)}</td>
</tr>
{#if $open.vertices}
<tr>
<td colspan="2">
<SmallGraph points={getPoints($store, "total-vertices")} />
</td>
</tr>
{/if}
<tr on:click={() => ($open.faces = !$open.faces)}>
<td>{$open.faces ? "-" : "+"} faces </td>
<tr>
<td>faces </td>
<td>{humanizeNumber(faces || 0)}</td>
</tr>
{#if $open.faces}
<tr>
<td colspan="2">
<SmallGraph points={getPoints($store, "total-faces")} />
</td>
</tr>
{/if}
</table>
</div>

View File

@ -1,2 +1 @@
export * from "./store";
export { default as PerformanceViewer } from "./PerformanceViewer.svelte";

View File

@ -1,97 +0,0 @@
import { readable, type Readable } from "svelte/store";
export type PerformanceData = Record<string, number[]>[];
export interface PerformanceStore extends Readable<PerformanceData> {
startRun(): void;
stopRun(): void;
addPoint(name: string, value?: number): void;
endPoint(name?: string): void;
mergeData(data: PerformanceData[number]): void;
get: () => PerformanceData;
}
export function createPerformanceStore(id?: string): PerformanceStore {
let data: PerformanceData = [];
let currentRun: Record<string, number[]> | undefined;
let temp: Record<string, number> | undefined;
let lastPoint: string | undefined;
let set: (v: PerformanceData) => void;
const { subscribe } = readable<PerformanceData>([], (_set) => {
set = _set;
});
function startRun() {
if (currentRun) return;
currentRun = {};
lastPoint = undefined;
temp = {
start: performance.now()
}
}
function stopRun() {
if (currentRun && temp) {
currentRun["total"] = [performance.now() - temp.start];
data.push(currentRun);
data = data.slice(-100);
currentRun = undefined;
temp = undefined;
if (set) set(data);
}
}
function addPoint(name: string, value?: number) {
if (!currentRun) return;
if (value === undefined) {
if (temp) {
lastPoint = name;
temp[name] = performance.now();
}
} else {
currentRun[name] = currentRun[name] || [];
currentRun[name].push(value);
}
}
function get() {
return data;
}
function mergeData(newData: PerformanceData[number]) {
let r = currentRun;
if (!r) return;
Object.keys(newData).forEach((name) => {
if (name in r) {
r[name].push(...newData[name]);
} else {
r[name] = newData[name];
}
});
}
function endPoint(name = lastPoint) {
if (name === lastPoint) lastPoint = undefined;
if (name && currentRun && temp && name in temp) {
currentRun[name] = currentRun[name] || [];
currentRun[name].push(performance.now() - temp[name]);
delete temp[name];
}
}
return {
subscribe,
startRun,
stopRun,
addPoint,
endPoint,
mergeData,
get
}
}

View File

@ -1,18 +0,0 @@
import type { Graph, RuntimeExecutor } from "@nodes/types";
export class RemoteRuntimeExecutor implements RuntimeExecutor {
constructor(private url: string) { }
async execute(graph: Graph, settings: Record<string, any>): Promise<Int32Array> {
const res = await fetch(this.url, { method: "POST", body: JSON.stringify({ graph, settings }) });
if (!res.ok) {
throw new Error(`Failed to execute graph`);
}
return new Int32Array(await res.arrayBuffer());
}
}

View File

@ -5,7 +5,7 @@
import { updateGeometries } from "./updateGeometries";
import { decodeFloat, splitNestedArray } from "@nodes/utils";
import type { PerformanceStore } from "$lib/performance";
import type { PerformanceStore } from "@nodes/utils";
import { AppSettings } from "$lib/settings/app-settings";
import SmallPerformanceViewer from "$lib/performance/SmallPerformanceViewer.svelte";

View File

@ -1,283 +0,0 @@
import type { Graph, NodeRegistry, NodeDefinition, RuntimeExecutor, NodeInput } from "@nodes/types";
import { concatEncodedArrays, encodeFloat, fastHashArrayBuffer } from "@nodes/utils"
import { createLogger } from "./helpers";
import type { RuntimeCache } from "@nodes/types";
import type { PerformanceStore } from "./performance";
const log = createLogger("runtime-executor");
log.mute()
function getValue(input: NodeInput, value?: unknown) {
if (value === undefined && "value" in input) {
value = input.value
}
if (input.type === "float") {
return encodeFloat(value as number);
}
if (Array.isArray(value)) {
if (input.type === "vec3") {
return [0, value.length + 1, ...value.map(v => encodeFloat(v)), 1, 1] as number[];
}
return [0, value.length + 1, ...value, 1, 1] as number[];
}
if (typeof value === "boolean") {
return value ? 1 : 0;
}
if (typeof value === "number") {
return value;
}
if (value instanceof Int32Array) {
return value;
}
throw new Error(`Unknown input type ${input.type}`);
}
export class MemoryRuntimeExecutor implements RuntimeExecutor {
private definitionMap: Map<string, NodeDefinition> = new Map();
private randomSeed = Math.floor(Math.random() * 100000000);
perf?: PerformanceStore;
constructor(private registry: NodeRegistry, private cache?: RuntimeCache<Int32Array>) { }
private async getNodeDefinitions(graph: Graph) {
if (this.registry.status !== "ready") {
throw new Error("Node registry is not ready");
}
await this.registry.load(graph.nodes.map(node => node.type));
const typeMap = new Map<string, NodeDefinition>();
for (const node of graph.nodes) {
if (!typeMap.has(node.type)) {
const type = this.registry.getNode(node.type);
if (type) {
typeMap.set(node.type, type);
}
}
}
return typeMap;
}
private async addMetaData(graph: Graph) {
// First, lets check if all nodes have a definition
this.definitionMap = await this.getNodeDefinitions(graph);
const outputNode = graph.nodes.find(node => node.type.endsWith("/output"));
if (!outputNode) {
throw new Error("No output node found");
}
outputNode.tmp = outputNode.tmp || {};
outputNode.tmp.depth = 0;
const nodeMap = new Map(graph.nodes.map(node => [node.id, node]));
// loop through all edges and assign the parent and child nodes to each node
for (const edge of graph.edges) {
const [parentId, _parentOutput, childId, childInput] = edge;
const parent = nodeMap.get(parentId);
const child = nodeMap.get(childId);
if (parent && child) {
parent.tmp = parent.tmp || {};
parent.tmp.children = parent.tmp.children || [];
parent.tmp.children.push(child);
child.tmp = child.tmp || {};
child.tmp.parents = child.tmp.parents || [];
child.tmp.parents.push(parent);
child.tmp.inputNodes = child.tmp.inputNodes || {};
child.tmp.inputNodes[childInput] = parent;
}
}
const nodes = []
// loop through all the nodes and assign each nodes its depth
const stack = [outputNode];
while (stack.length) {
const node = stack.pop();
if (!node) continue;
node.tmp = node.tmp || {};
if (node?.tmp?.depth === undefined) {
node.tmp.depth = 0;
}
if (node?.tmp?.parents !== undefined) {
for (const parent of node.tmp.parents) {
parent.tmp = parent.tmp || {};
if (parent.tmp?.depth === undefined) {
parent.tmp.depth = node.tmp.depth + 1;
stack.push(parent);
} else {
parent.tmp.depth = Math.max(parent.tmp.depth, node.tmp.depth + 1);
}
}
}
nodes.push(node);
}
return [outputNode, nodes] as const;
}
async execute(graph: Graph, settings: Record<string, unknown>) {
this.perf?.addPoint("runtime");
let a = performance.now();
// Then we add some metadata to the graph
const [outputNode, nodes] = await this.addMetaData(graph);
let b = performance.now();
this.perf?.addPoint("collect-metadata", b - a);
/*
* Here we sort the nodes into buckets, which we then execute one by one
* +-b2-+-b1-+---b0---+
* | | | |
* | n3 | n2 | Output |
* | n6 | n4 | Level |
* | | n5 | |
* | | | |
* +----+----+--------+
*/
// we execute the nodes from the bottom up
const sortedNodes = nodes.sort((a, b) => (b.tmp?.depth || 0) - (a.tmp?.depth || 0));
// here we store the intermediate results of the nodes
const results: Record<string, Int32Array> = {};
for (const node of sortedNodes) {
const node_type = this.definitionMap.get(node.type)!;
if (!node_type || !node.tmp || !node_type.execute) {
log.warn(`Node ${node.id} has no definition`);
continue;
};
a = performance.now();
// Collect the inputs for the node
const inputs = Object.entries(node_type.inputs || {}).map(([key, input]) => {
if (input.type === "seed") {
if (settings["randomSeed"] === true) {
return Math.floor(Math.random() * 100000000)
} else {
return this.randomSeed
}
}
// If the input is linked to a setting, we use that value
if (input.setting) {
return getValue(input, settings[input.setting]);
}
// check if the input is connected to another node
const inputNode = node.tmp?.inputNodes?.[key];
if (inputNode) {
if (results[inputNode.id] === undefined) {
throw new Error("Input node has no result");
}
return results[inputNode.id];
}
// If the value is stored in the node itself, we use that value
if (node.props?.[key] !== undefined) {
return getValue(input, node.props[key]);
}
return getValue(input);
});
b = performance.now();
this.perf?.addPoint("collected-inputs", b - a);
try {
a = performance.now();
const encoded_inputs = concatEncodedArrays(inputs);
b = performance.now();
this.perf?.addPoint("encoded-inputs", b - a);
a = performance.now();
let inputHash = `node-${node.id}-${fastHashArrayBuffer(encoded_inputs)}`;
b = performance.now();
this.perf?.addPoint("hash-inputs", b - a);
let cachedValue = this.cache?.get(inputHash);
if (cachedValue !== undefined) {
log.log(`Using cached value for ${node_type.id || node.id}`);
this.perf?.addPoint("cache-hit", 1);
results[node.id] = cachedValue as Int32Array;
continue;
}
this.perf?.addPoint("cache-hit", 0);
log.group(`executing ${node_type.id || node.id}`);
log.log(`Inputs:`, inputs);
a = performance.now();
results[node.id] = node_type.execute(encoded_inputs);
b = performance.now();
if (this.cache) {
this.cache.set(inputHash, results[node.id]);
}
this.perf?.addPoint("node/" + node_type.id, b - a);
log.log("Result:", results[node.id]);
log.groupEnd();
} catch (e) {
log.groupEnd();
log.error(`Error executing node ${node_type.id || node.id}`, e);
}
}
// return the result of the parent of the output node
const res = results[outputNode.id];
if (this.cache) {
this.cache.size = sortedNodes.length * 2;
}
this.perf?.endPoint("runtime");
return res as unknown as Int32Array;
}
getPerformanceData() {
return this.perf?.get();
}
}
export class MemoryRuntimeCache implements RuntimeCache {
private cache: [string, unknown][] = [];
size = 50;
get<T>(key: string): T | undefined {
return this.cache.find(([k]) => k === key)?.[1] as T;
}
set<T>(key: string, value: T): void {
this.cache.push([key, value]);
this.cache = this.cache.slice(-this.size);
}
clear(): void {
this.cache = [];
}
}

View File

@ -136,16 +136,6 @@
border: solid thin var(--outline);
border-bottom: none;
}
textarea {
width: 100%;
height: 1em;
font-size: 1em;
padding: 0.5em;
border: solid thin var(--outline);
background: var(--layer-2);
box-sizing: border-box;
height: 2.5em;
}
i {
opacity: 0.5;
font-size: 0.8em;

View File

@ -1,26 +0,0 @@
import { MemoryRuntimeExecutor, MemoryRuntimeCache } from "./runtime-executor";
import { RemoteNodeRegistry } from "./node-registry-client";
import type { Graph } from "@nodes/types";
import { createPerformanceStore } from "./performance/store";
import { IndexDBCache } from "./node-registry-cache";
const cache = new MemoryRuntimeCache();
const indexDbCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("");
nodeRegistry.cache = indexDbCache;
const executor = new MemoryRuntimeExecutor(nodeRegistry, cache);
const performanceStore = createPerformanceStore("worker");
executor.perf = performanceStore;
export async function executeGraph(graph: Graph, settings: Record<string, unknown>): Promise<Int32Array> {
await nodeRegistry.load(graph.nodes.map((n) => n.type));
performanceStore.startRun();
let res = await executor.execute(graph, settings);
performanceStore.stopRun();
return res;
}
export function getPerformanceData() {
return performanceStore.get();
}

View File

@ -1,16 +0,0 @@
/// <reference types="vite-plugin-comlink/client" />
import type { Graph, RuntimeExecutor } from "@nodes/types";
export class WorkerRuntimeExecutor implements RuntimeExecutor {
private worker = new ComlinkWorker<typeof import('./worker-runtime-executor-backend.ts')>(new URL("./worker-runtime-executor-backend.ts", import.meta.url));
constructor() {
}
async execute(graph: Graph, settings: Record<string, unknown>) {
return this.worker.executeGraph(graph, settings);
}
async getPerformanceData() {
return this.worker.getPerformanceData();
}
}

View File

@ -1,8 +1,6 @@
<script lang="ts">
import Grid from "$lib/grid";
import GraphInterface from "$lib/graph-interface";
import { WorkerRuntimeExecutor } from "$lib/worker-runtime-executor";
import { RemoteNodeRegistry } from "$lib/node-registry-client";
import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types";
import Viewer from "$lib/result-viewer/Viewer.svelte";
@ -18,17 +16,17 @@
import Panel from "$lib/settings/Panel.svelte";
import GraphSettings from "$lib/settings/panels/GraphSettings.svelte";
import NestedSettings from "$lib/settings/panels/NestedSettings.svelte";
import { createPerformanceStore } from "$lib/performance";
import type { Group, Scene } from "three";
import type { Group } from "three";
import ExportSettings from "$lib/settings/panels/ExportSettings.svelte";
import {
MemoryRuntimeCache,
WorkerRuntimeExecutor,
MemoryRuntimeExecutor,
} from "$lib/runtime-executor";
import { IndexDBCache } from "$lib/node-registry-cache";
import { decodeNestedArray, fastHashString } from "@nodes/utils";
} from "@nodes/runtime";
import { IndexDBCache, RemoteNodeRegistry } from "@nodes/registry";
import { decodeNestedArray, createPerformanceStore } from "@nodes/utils";
import BenchmarkPanel from "$lib/settings/panels/BenchmarkPanel.svelte";
import { debounceAsyncFunction, withArgsChangeOnly } from "$lib/helpers";
import { debounceAsyncFunction } from "$lib/helpers";
let performanceStore = createPerformanceStore("page");

View File

@ -1,34 +0,0 @@
import type { RequestHandler } from "./$types";
import { MemoryRuntimeExecutor } from "$lib/runtime-executor";
import { RemoteNodeRegistry } from "$lib/node-registry-client";
import { createPerformanceStore } from "$lib/performance";
const registry = new RemoteNodeRegistry("");
const runtime = new MemoryRuntimeExecutor(registry);
const performanceStore = createPerformanceStore();
runtime.perf = performanceStore;
export const prerender = false;
export const POST: RequestHandler = async ({ request, fetch }) => {
const { graph, settings } = await request.json();
if (!graph || !settings) {
return new Response("Invalid request", { status: 400 });
}
registry.fetch = fetch;
await registry.load(graph.nodes.map(node => node.type))
const res = await runtime.execute(graph, settings);
let headers: Record<string, string> = { "Content-Type": "application/octet-stream" };
if (runtime.perf) {
headers["performance"] = JSON.stringify(runtime.perf.get());
}
return new Response(res, { headers });
}