feat: add simple performance tracker
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m23s

This commit is contained in:
max_richter 2024-04-25 00:02:02 +02:00
parent 2de2560a57
commit f51f61df17
12 changed files with 408 additions and 224 deletions

View File

@ -7,7 +7,7 @@
import { getContext, onMount, setContext } from "svelte"; import { getContext, onMount, setContext } from "svelte";
import Camera from "../Camera.svelte"; import Camera from "../Camera.svelte";
import GraphView from "./GraphView.svelte"; import GraphView from "./GraphView.svelte";
import type { Node, Node as NodeType, Socket } from "@nodes/types"; import type { Node, NodeId, Node as NodeType, Socket } from "@nodes/types";
import { NodeDefinitionSchema } from "@nodes/types"; import { NodeDefinitionSchema } from "@nodes/types";
import FloatingEdge from "../edges/FloatingEdge.svelte"; import FloatingEdge from "../edges/FloatingEdge.svelte";
import { import {
@ -783,7 +783,7 @@
event.preventDefault(); event.preventDefault();
isDragging = false; isDragging = false;
if (!event.dataTransfer) return; if (!event.dataTransfer) return;
const nodeId = event.dataTransfer.getData("data/node-id"); const nodeId: NodeId = event.dataTransfer.getData("data/node-id");
if (nodeId) { if (nodeId) {
let mx = event.clientX - rect.x; let mx = event.clientX - rect.x;
@ -805,7 +805,7 @@
} }
const pos = projectScreenToWorld(mx, my); const pos = projectScreenToWorld(mx, my);
graph.loadNode(nodeId).then(() => { graph.load([nodeId]).then(() => {
graph.createNode({ graph.createNode({
type: nodeId, type: nodeId,
props, props,

View File

@ -11,17 +11,6 @@ export class RemoteNodeRegistry implements NodeRegistry {
constructor(private url: string) { } constructor(private url: string) { }
async loadNode(id: `${string}/${string}/${string}`) { async loadNode(id: `${string}/${string}/${string}`) {
const wasmResponse = await this.fetchNode(id);
const wrapper = createWasmWrapper(wasmResponse);
const definition = wrapper.get_definition();
return {
...definition,
id,
execute: wrapper.execute
};
} }
async fetchUsers() { async fetchUsers() {
@ -67,7 +56,20 @@ export class RemoteNodeRegistry implements NodeRegistry {
async load(nodeIds: `${string}/${string}/${string}`[]) { async load(nodeIds: `${string}/${string}/${string}`[]) {
const a = performance.now(); const a = performance.now();
const nodes = await Promise.all(nodeIds.map(id => this.loadNode(id))); const nodes = await Promise.all(nodeIds.map(async id => {
const wasmResponse = await this.fetchNode(id);
const wrapper = createWasmWrapper(wasmResponse);
const definition = wrapper.get_definition();
return {
...definition,
id,
execute: wrapper.execute
};
}));
for (const node of nodes) { for (const node of nodes) {
this.nodes.set(node.id, node); this.nodes.set(node.id, node);

View File

@ -0,0 +1,17 @@
<script lang="ts">
import type { PerformanceStore } from ".";
export let store: PerformanceStore;
function getPerformanceData() {
return Object.entries($store.total).sort((a, b) => b[1] - a[1]);
}
</script>
{#if $store.runs.length !== 0}
{#each getPerformanceData() as [key, value]}
<p>{key}: {Math.floor(value * 100) / 100}ms</p>
{/each}
{:else}
<p>No runs available</p>
{/if}

View File

@ -0,0 +1,62 @@
import { readable, type Readable } from "svelte/store";
type PerformanceData = {
total: Record<string, number>;
runs: Record<string, number[]>[];
}
export interface PerformanceStore extends Readable<PerformanceData> {
startRun(): void;
stopRun(): void;
addPoint(name: string, value?: number): void;
}
export function createPerformanceStore(): PerformanceStore {
let data: PerformanceData = { total: {}, runs: [] };
let currentRun: Record<string, number[]> | undefined;
let set: (v: PerformanceData) => void;
const { subscribe } = readable<PerformanceData>({ total: {}, runs: [] }, (_set) => {
set = _set;
});
function startRun() {
currentRun = {};
}
function stopRun() {
if (currentRun) {
// Calculate total
Object.keys(currentRun).forEach((name) => {
if (!currentRun?.[name]?.length) return;
let runTotal = currentRun[name].reduce((a, b) => a + b, 0) / currentRun[name].length;
if (!data.total[name]) {
data.total[name] = runTotal;
} else {
data.total[name] = (data.total[name] + runTotal) / 2;
}
});
data.runs.push(currentRun);
currentRun = undefined;
set(data);
}
}
function addPoint(name: string, value: number) {
if (!currentRun) return;
currentRun[name] = currentRun[name] || [];
currentRun[name].push(value);
}
return {
subscribe,
startRun,
stopRun,
addPoint,
}
}
export { default as PerformanceViewer } from "./PerformanceViewer.svelte";

View File

@ -1,16 +1,51 @@
import type { Graph, NodeRegistry, NodeDefinition, RuntimeExecutor } from "@nodes/types"; import type { Graph, NodeRegistry, NodeDefinition, RuntimeExecutor, NodeInput } from "@nodes/types";
import { fastHash, concatEncodedArrays, encodeFloat, decodeNestedArray } from "@nodes/utils" import { concatEncodedArrays, encodeFloat, fastHashArray } from "@nodes/utils"
import { createLogger } from "./helpers"; import { createLogger } from "./helpers";
import type { RuntimeCache } from "@nodes/types";
import type { PerformanceStore } from "./performance";
const log = createLogger("runtime-executor"); 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 { export class MemoryRuntimeExecutor implements RuntimeExecutor {
private definitionMap: Map<string, NodeDefinition> = new Map(); private definitionMap: Map<string, NodeDefinition> = new Map();
private cache: Record<string, { eol: number, value: any }> = {}; private randomSeed = Math.floor(Math.random() * 100000000);
constructor(private registry: NodeRegistry) { } perf?: PerformanceStore;
constructor(private registry: NodeRegistry, private cache?: RuntimeCache) { }
private getNodeDefinitions(graph: Graph) { private getNodeDefinitions(graph: Graph) {
@ -67,26 +102,23 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
const stack = [outputNode]; const stack = [outputNode];
while (stack.length) { while (stack.length) {
const node = stack.pop(); const node = stack.pop();
if (node) { if (!node) continue;
node.tmp = node.tmp || {}; node.tmp = node.tmp || {};
if (node?.tmp?.depth === undefined) {
if (node?.tmp?.depth === undefined) { node.tmp.depth = 0;
node.tmp.depth = 0; }
} if (node?.tmp?.parents !== undefined) {
if (node?.tmp?.parents !== undefined) { for (const parent of node.tmp.parents) {
for (const parent of node.tmp.parents) { parent.tmp = parent.tmp || {};
parent.tmp = parent.tmp || {}; if (parent.tmp?.depth === undefined) {
if (parent.tmp?.depth === undefined) { parent.tmp.depth = node.tmp.depth + 1;
parent.tmp.depth = node.tmp.depth + 1; stack.push(parent);
stack.push(parent); } else {
} else { parent.tmp.depth = Math.max(parent.tmp.depth, node.tmp.depth + 1);
parent.tmp.depth = Math.max(parent.tmp.depth, node.tmp.depth + 1);
}
} }
} }
nodes.push(node);
} }
nodes.push(node);
} }
return [outputNode, nodes] as const; return [outputNode, nodes] as const;
@ -94,8 +126,17 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
execute(graph: Graph, settings: Record<string, unknown>) { execute(graph: Graph, settings: Record<string, unknown>) {
this.perf?.startRun();
let a0 = performance.now();
let a = performance.now();
// Then we add some metadata to the graph // Then we add some metadata to the graph
const [outputNode, nodes] = this.addMetaData(graph); const [outputNode, nodes] = this.addMetaData(graph);
let b = performance.now();
this.perf?.addPoint("metadata", b - a);
/* /*
* Here we sort the nodes into buckets, which we then execute one by one * Here we sort the nodes into buckets, which we then execute one by one
@ -114,100 +155,109 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
// here we store the intermediate results of the nodes // here we store the intermediate results of the nodes
const results: Record<string, Int32Array> = {}; const results: Record<string, Int32Array> = {};
const runSeed = settings["randomSeed"] === true ? Math.floor(Math.random() * 100000000) : 5120983;
for (const node of sortedNodes) { for (const node of sortedNodes) {
const node_type = this.definitionMap.get(node.type)!; const node_type = this.definitionMap.get(node.type)!;
if (node?.tmp && node_type?.execute) { if (!node_type || !node.tmp || !node_type.execute) {
log.warn(`Node ${node.id} has no definition`);
continue;
};
const inputs = Object.entries(node_type.inputs || {}).map(([key, input]) => { a = performance.now();
if (input.type === "seed") { // Collect the inputs for the node
return runSeed; 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 (input.setting) {
if (settings[input.setting] === undefined) {
if ("value" in input && input.value !== undefined) {
if (input.type === "float") {
return encodeFloat(input.value);
}
return input.value;
} else {
log.warn(`Setting ${input.setting} is not defined`);
}
} else {
if (input.type === "float") {
return encodeFloat(settings[input.setting] as number);
}
return 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) {
let value = node.props[key];
if (input.type === "vec3") {
return [0, 4, ...value.map(v => encodeFloat(v)), 1, 1]
} else if (Array.isArray(value)) {
return [0, value.length + 1, ...value, 1, 1];
} else if (input.type === "float") {
return encodeFloat(value);
} else {
return value;
}
}
let defaultValue = input.value;
if (defaultValue !== undefined) {
if (Array.isArray(defaultValue)) {
return [0, defaultValue.length + 1, ...defaultValue.map(v => encodeFloat(v)), 1, 1];
} else if (input.type === "float") {
return encodeFloat(defaultValue);
} else {
return defaultValue;
}
}
throw new Error(`Input ${key} is not connected and has no default value`);
});
try {
const encoded_inputs = concatEncodedArrays(inputs);
log.group(`executing ${node_type.id || node.id}`);
log.log(`Inputs:`, inputs);
log.log(`Encoded Inputs:`, encoded_inputs);
results[node.id] = node_type.execute(encoded_inputs);
log.log("Result:", results[node.id]);
log.log("Result (decoded):", decodeNestedArray(results[node.id]));
log.groupEnd();
} catch (e) {
log.groupEnd();
log.error(`Error executing node ${node_type.id || node.id}`, e);
} }
// 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);
let inputHash = `node-${node.id}-${fastHashArray(encoded_inputs)}`;
let cachedValue = this.cache?.get(inputHash);
if (cachedValue !== undefined) {
log.log(`Using cached value for ${node_type.id || node.id}`);
results[node.id] = cachedValue as Int32Array;
continue;
}
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();
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 // return the result of the parent of the output node
const res = results[outputNode.id]; const res = results[outputNode.id];
this.perf?.addPoint("total", performance.now() - a0);
this.perf?.stopRun();
return res as unknown as Int32Array; return res as unknown as Int32Array;
} }
} }
export class MemoryRuntimeCache implements RuntimeCache {
private cache: Record<string, unknown> = {};
get<T>(key: string): T | undefined {
return this.cache[key] as T;
}
set<T>(key: string, value: T): void {
this.cache[key] = value;
}
clear(): void {
this.cache = {};
}
}

View File

@ -1,7 +1,10 @@
<script lang="ts"> <script lang="ts">
import Grid from "$lib/grid"; import Grid from "$lib/grid";
import GraphInterface from "$lib/graph-interface"; import GraphInterface from "$lib/graph-interface";
import { MemoryRuntimeExecutor } from "$lib/runtime-executor"; import {
MemoryRuntimeExecutor,
MemoryRuntimeCache,
} from "$lib/runtime-executor";
import { RemoteNodeRegistry } from "$lib/node-registry-client"; import { RemoteNodeRegistry } from "$lib/node-registry-client";
import * as templates from "$lib/graph-templates"; import * as templates from "$lib/graph-templates";
import type { Graph, Node } from "@nodes/types"; import type { Graph, Node } from "@nodes/types";
@ -22,9 +25,15 @@
import type { PerspectiveCamera, Vector3 } from "three"; import type { PerspectiveCamera, Vector3 } from "three";
import type { OrbitControls } from "three/examples/jsm/Addons.js"; import type { OrbitControls } from "three/examples/jsm/Addons.js";
import ActiveNode from "$lib/settings/panels/ActiveNode.svelte"; import ActiveNode from "$lib/settings/panels/ActiveNode.svelte";
import { createPerformanceStore } from "$lib/performance";
import PerformanceViewer from "$lib/performance/PerformanceViewer.svelte";
const nodePerformance = createPerformanceStore();
const runtimeCache = new MemoryRuntimeCache();
const nodeRegistry = new RemoteNodeRegistry(""); const nodeRegistry = new RemoteNodeRegistry("");
const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry); const runtimeExecutor = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
runtimeExecutor.perf = nodePerformance;
globalThis.decode = decodeNestedArray; globalThis.decode = decodeNestedArray;
globalThis.encode = encodeNestedArray; globalThis.encode = encodeNestedArray;
@ -96,6 +105,12 @@
}, },
}, },
}, },
performance: {
id: "performance",
icon: "i-tabler-brand-speedtest",
props: { store: nodePerformance, title: "Runtime Performance" },
component: PerformanceViewer,
},
activeNode: { activeNode: {
id: "Active Node", id: "Active Node",
icon: "i-tabler-adjustments", icon: "i-tabler-adjustments",

View File

@ -15,7 +15,6 @@ pub fn execute(input: &[i32]) -> Vec<i32> {
let args = split_args(input); let args = split_args(input);
assert_eq!(args.len(), 2, "Expected 2 arguments, got {}", args.len()); assert_eq!(args.len(), 2, "Expected 2 arguments, got {}", args.len());
let inputs = split_args(args[0]); let inputs = split_args(args[0]);
let resolution = evaluate_int(args[1]) as usize; let resolution = evaluate_int(args[1]) as usize;

View File

@ -0,0 +1,57 @@
import { Graph, NodeDefinition, NodeId } from "./types";
export interface NodeRegistry {
/**
* The status of the node registry
* @remarks The status should be "loading" when the registry is loading, "ready" when the registry is ready, and "error" if an error occurred while loading the registry
*/
status: "loading" | "ready" | "error";
/**
* Load the nodes with the given ids
* @param nodeIds - The ids of the nodes to load
* @returns A promise that resolves when the nodes are loaded
* @throws An error if the nodes could not be loaded
* @remarks This method should be called before calling getNode or getAllNodes
*/
load: (nodeIds: NodeId[]) => Promise<NodeDefinition[]>;
/**
* Get a node by id
* @param id - The id of the node to get
* @returns The node with the given id, or undefined if no such node exists
*/
getNode: (id: NodeId) => NodeDefinition | undefined;
/**
* Get all nodes
* @returns An array of all nodes
*/
getAllNodes: () => NodeDefinition[];
}
export interface RuntimeExecutor {
/**
* Execute the given graph
* @param graph - The graph to execute
* @returns The result of the execution
*/
execute: (graph: Graph, settings: Record<string, unknown>) => unknown;
}
export interface RuntimeCache {
/**
* Get the value for the given key
* @param key - The key to get the value for
* @returns The value for the given key, or undefined if no such value exists
*/
get: (key: string) => unknown | undefined;
/**
* Set the value for the given key
* @param key - The key to set the value for
* @param value - The value to set
*/
set: (key: string, value: unknown) => void;
/**
* Clear the cache
*/
clear: () => void;
}

View File

@ -1,100 +1,5 @@
import { z } from "zod";
import { NodeInputSchema } from "./inputs";
export type { NodeInput } from "./inputs"; export type { NodeInput } from "./inputs";
export type { NodeRegistry, RuntimeExecutor, RuntimeCache } from "./components";
export type { Node, NodeDefinition, Socket, NodeId, Edge, Graph } from "./types";
export { NodeDefinitionSchema } from "./types";
export type Node = {
id: number;
type: string;
props?: Record<string, any>,
tmp?: {
depth?: number;
mesh?: any;
random?: number;
parents?: Node[],
children?: Node[],
inputNodes?: Record<string, Node>
type?: NodeDefinition;
downX?: number;
downY?: number;
x?: number;
y?: number;
ref?: HTMLElement;
visible?: boolean;
isMoving?: boolean;
},
meta?: {
title?: string;
lastModified?: string;
},
position: [x: number, y: number]
}
export const NodeDefinitionSchema = z.object({
id: z.string(),
inputs: z.record(NodeInputSchema).optional(),
outputs: z.array(z.string()).optional(),
meta: z.object({
description: z.string().optional(),
title: z.string().optional(),
}).optional(),
});
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
execute(input: Int32Array): Int32Array;
};
export type Socket = {
node: Node;
index: number | string;
position: [number, number];
};
export interface NodeRegistry {
/**
* The status of the node registry
* @remarks The status should be "loading" when the registry is loading, "ready" when the registry is ready, and "error" if an error occurred while loading the registry
*/
status: "loading" | "ready" | "error";
/**
* Load the nodes with the given ids
* @param nodeIds - The ids of the nodes to load
* @returns A promise that resolves when the nodes are loaded
* @throws An error if the nodes could not be loaded
* @remarks This method should be called before calling getNode or getAllNodes
*/
load: (nodeIds: string[]) => Promise<void>;
/**
* Get a node by id
* @param id - The id of the node to get
* @returns The node with the given id, or undefined if no such node exists
*/
getNode: (id: string) => NodeDefinition | undefined;
/**
* Get all nodes
* @returns An array of all nodes
*/
getAllNodes: () => NodeDefinition[];
}
export interface RuntimeExecutor {
/**
* Execute the given graph
* @param graph - The graph to execute
* @returns The result of the execution
*/
execute: (graph: Graph, settings: Record<string, unknown>) => unknown;
}
export type Edge = [Node, number, Node, string];
export type Graph = {
id: number;
meta?: {
title?: string;
lastModified?: string;
},
settings?: Record<string, any>,
nodes: Node[];
edges: [number, number, number, string][];
}

View File

@ -0,0 +1,64 @@
import { z } from "zod";
import { NodeInputSchema } from "./inputs";
export type NodeId = `${string}/${string}/${string}`;
export type Node = {
id: number;
type: NodeId;
props?: Record<string, number | number[]>,
tmp?: {
depth?: number;
mesh?: any;
random?: number;
parents?: Node[],
children?: Node[],
inputNodes?: Record<string, Node>
type?: NodeDefinition;
downX?: number;
downY?: number;
x?: number;
y?: number;
ref?: HTMLElement;
visible?: boolean;
isMoving?: boolean;
},
meta?: {
title?: string;
lastModified?: string;
},
position: [x: number, y: number]
}
export const NodeDefinitionSchema = z.object({
id: z.string(),
inputs: z.record(NodeInputSchema).optional(),
outputs: z.array(z.string()).optional(),
meta: z.object({
description: z.string().optional(),
title: z.string().optional(),
}).optional(),
});
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema> & {
execute(input: Int32Array): Int32Array;
};
export type Socket = {
node: Node;
index: number | string;
position: [number, number];
};
export type Edge = [Node, number, Node, string];
export type Graph = {
id: number;
meta?: {
title?: string;
lastModified?: string;
},
settings?: Record<string, any>,
nodes: Node[];
edges: [number, number, number, string][];
}

View File

@ -1,27 +1,40 @@
type SparseArray<T = number> = (T | T[] | SparseArray<T>)[]; type SparseArray<T = number> = (T | T[] | SparseArray<T>)[];
export function concatEncodedArrays(input: (number | number[])[]): number[] { export function concatEncodedArrays(input: (number | number[] | Int32Array)[]): Int32Array {
if (input.length === 1 && Array.isArray(input[0])) { let totalLength = 4;
return input[0] for (let i = 0; i < input.length; i++) {
const item = input[i];
if (Array.isArray(item) || item instanceof Int32Array) {
totalLength += item.length;
} else {
totalLength++;
}
} }
const result = [0, 1]; // opening bracket const result = new Int32Array(totalLength);
result[0] = 0;
result[1] = 1;
let index = 2; // Start after the opening bracket
let last_closing_bracket = 1; let last_closing_bracket = 1;
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
const item = input[i]; const item = input[i];
if (Array.isArray(item) || item instanceof Int32Array) { if (Array.isArray(item) || item instanceof Int32Array) {
result.push(...item); result.set(item, index);
last_closing_bracket = result.length - 1; index += item.length;
last_closing_bracket = index - 1;
} else { } else {
result[last_closing_bracket]++; result[last_closing_bracket]++;
result.push(item); result[index] = item;
index++;
} }
} }
result.push(1, 1); // closing bracket result[totalLength - 2] = 1;
result[totalLength - 1] = 1;
return result return result
} }

View File

@ -1,6 +1,6 @@
use utils::{ use utils::{
geometry::{create_multiple_paths, create_path, wrap_multiple_paths}, geometry::{create_multiple_paths, create_path, wrap_multiple_paths},
get_args, split_args,
}; };
#[allow(dead_code)] #[allow(dead_code)]
@ -16,7 +16,7 @@ fn test_split_args(){
]; ];
for input in inputs { for input in inputs {
println!("RESULT: {:?}", get_args(&input)); println!("RESULT: {:?}", split_args(&input));
} }
} }