From 548e445eb791652a9b237a5c0bf911c8f500372d Mon Sep 17 00:00:00 2001 From: Max Richter Date: Wed, 3 Dec 2025 22:59:06 +0100 Subject: [PATCH] fix: correctly show hide geometries in geometrypool --- app/src/lib/graph-interface/node/Node.svelte | 4 +- .../lib/graph-interface/node/NodeHTML.svelte | 17 +- .../graph-interface/node/NodeHeader.svelte | 7 +- .../graph-interface/node/NodeParameter.svelte | 14 +- app/src/lib/helpers/index.ts | 2 +- app/src/lib/node-store/DraggableNode.svelte | 12 +- .../performance/SmallPerformanceViewer.svelte | 29 +-- app/src/lib/result-viewer/geometryPool.ts | 7 +- app/src/lib/runtime/runtime-executor-cache.ts | 34 ++- .../worker-runtime-executor-backend.ts | 4 +- app/src/routes/+page.svelte | 3 - packages/utils/src/fastHash.test.ts | 14 +- packages/utils/src/fastHash.ts | 202 ++++-------------- 13 files changed, 113 insertions(+), 236 deletions(-) diff --git a/app/src/lib/graph-interface/node/Node.svelte b/app/src/lib/graph-interface/node/Node.svelte index 2fba499..f9b52b1 100644 --- a/app/src/lib/graph-interface/node/Node.svelte +++ b/app/src/lib/graph-interface/node/Node.svelte @@ -16,7 +16,7 @@ inView: boolean; z: number; }; - let { node, inView, z }: Props = $props(); + let { node = $bindable(), inView, z }: Props = $props(); const isActive = $derived(graphState.activeNodeId === node.id); const isSelected = $derived(graphState.selectedNodes.has(node.id)); @@ -67,4 +67,4 @@ /> - + diff --git a/app/src/lib/graph-interface/node/NodeHTML.svelte b/app/src/lib/graph-interface/node/NodeHTML.svelte index e0261b6..cd02ed2 100644 --- a/app/src/lib/graph-interface/node/NodeHTML.svelte +++ b/app/src/lib/graph-interface/node/NodeHTML.svelte @@ -1,5 +1,5 @@
@@ -27,12 +25,12 @@ ($open.runtime = !$open.runtime)} + onclick={() => (open.value.runtime = !open.value.runtime)} > - {$open.runtime ? "-" : "+"} runtime + {open.value.runtime ? "-" : "+"} runtime {humanizeDuration(runtime || 1000)} - {#if $open.runtime} + {#if open.value.runtime} @@ -40,13 +38,16 @@ {/if} - ($open.fps = !$open.fps)}> - {$open.fps ? "-" : "+"} fps + (open.value.fps = !open.value.fps)} + > + {open.value.fps ? "-" : "+"} fps {Math.floor(fps[fps.length - 1])}fps - {#if $open.fps} + {#if open.value.fps} diff --git a/app/src/lib/result-viewer/geometryPool.ts b/app/src/lib/result-viewer/geometryPool.ts index 1ecf794..960dc99 100644 --- a/app/src/lib/result-viewer/geometryPool.ts +++ b/app/src/lib/result-viewer/geometryPool.ts @@ -11,7 +11,7 @@ import { } from "three"; function fastArrayHash(arr: Int32Array) { - const sampleDistance = Math.max(Math.floor(arr.length / 100), 1); + const sampleDistance = Math.max(Math.floor(arr.length / 1000), 1); const sampleCount = Math.floor(arr.length / sampleDistance); let hash = new Int32Array(sampleCount); @@ -40,6 +40,9 @@ export function createGeometryPool(parentScene: Group, material: Material) { let hash = fastArrayHash(data); let geometry = existingMesh ? existingMesh.geometry : new BufferGeometry(); + if (existingMesh) { + existingMesh.visible = true; + } // Extract data from the encoded array // const geometryType = encodedData[index++]; @@ -121,7 +124,6 @@ export function createGeometryPool(parentScene: Group, material: Material) { updateSingleGeometry(input, existingMesh || null); } else if (existingMesh) { existingMesh.visible = false; - scene.remove(existingMesh); } } return { totalVertices, totalFaces }; @@ -258,7 +260,6 @@ export function createInstancedGeometryPool( updateSingleInstance(input, existingMesh || null); } else if (existingMesh) { existingMesh.visible = false; - scene.remove(existingMesh); } } return { totalVertices, totalFaces }; diff --git a/app/src/lib/runtime/runtime-executor-cache.ts b/app/src/lib/runtime/runtime-executor-cache.ts index 59514b6..a77f223 100644 --- a/app/src/lib/runtime/runtime-executor-cache.ts +++ b/app/src/lib/runtime/runtime-executor-cache.ts @@ -1,19 +1,33 @@ import { type SyncCache } from "@nodarium/types"; export class MemoryRuntimeCache implements SyncCache { + private map = new Map(); + size: number; - private cache: [string, unknown][] = []; - size = 50; + constructor(size = 50) { + this.size = size; + } get(key: string): T | undefined { - return this.cache.find(([k]) => k === key)?.[1] as T; - } - set(key: string, value: T): void { - this.cache.push([key, value]); - this.cache = this.cache.slice(-this.size); - } - clear(): void { - this.cache = []; + if (!this.map.has(key)) return undefined; + const value = this.map.get(key) as T; + this.map.delete(key); + this.map.set(key, value); + return value; } + set(key: string, value: T): void { + if (this.map.has(key)) { + this.map.delete(key); + } + this.map.set(key, value); + while (this.map.size > this.size) { + const oldestKey = this.map.keys().next().value as string; + this.map.delete(oldestKey); + } + } + + clear(): void { + this.map.clear(); + } } diff --git a/app/src/lib/runtime/worker-runtime-executor-backend.ts b/app/src/lib/runtime/worker-runtime-executor-backend.ts index 97478b4..b26cc07 100644 --- a/app/src/lib/runtime/worker-runtime-executor-backend.ts +++ b/app/src/lib/runtime/worker-runtime-executor-backend.ts @@ -2,11 +2,13 @@ import { MemoryRuntimeExecutor } from "./runtime-executor"; import { RemoteNodeRegistry, IndexDBCache } from "@nodarium/registry"; import type { Graph } from "@nodarium/types"; import { createPerformanceStore } from "@nodarium/utils"; +import { MemoryRuntimeCache } from "./runtime-executor-cache"; const indexDbCache = new IndexDBCache("node-registry"); const nodeRegistry = new RemoteNodeRegistry("", indexDbCache); -const executor = new MemoryRuntimeExecutor(nodeRegistry); +const cache = new MemoryRuntimeCache() +const executor = new MemoryRuntimeExecutor(nodeRegistry, cache); const performanceStore = createPerformanceStore(); executor.perf = performanceStore; diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index d992265..60d619f 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -88,13 +88,10 @@ randomSeed: { type: "boolean", value: false }, }); - let runIndex = 0; - async function update( g: Graph, s: Record = $state.snapshot(graphSettings), ) { - runIndex++; performanceStore.startRun(); try { let a = performance.now(); diff --git a/packages/utils/src/fastHash.test.ts b/packages/utils/src/fastHash.test.ts index ae8d729..291960f 100644 --- a/packages/utils/src/fastHash.test.ts +++ b/packages/utils/src/fastHash.test.ts @@ -1,5 +1,5 @@ import { test, expect } from 'vitest'; -import { fastHashArray, fastHashString } from './fastHash'; +import { fastHashArrayBuffer, fastHashString } from './fastHash'; test('fastHashString doesnt produce clashes', () => { const hashA = fastHashString('abcdef'); @@ -14,10 +14,10 @@ test("fastHashArray doesnt product collisions", () => { const a = new Int32Array(1000); - const hash_a = fastHashArray(a.buffer); + const hash_a = fastHashArrayBuffer(a); a[0] = 1; - const hash_b = fastHashArray(a.buffer); + const hash_b = fastHashArrayBuffer(a); expect(hash_a).not.toEqual(hash_b); @@ -28,13 +28,13 @@ test('fastHashArray is fast(ish) < 20ms', () => { const a = new Int32Array(10_000); const t0 = performance.now(); - fastHashArray(a.buffer); + fastHashArrayBuffer(a); const t1 = performance.now(); a[0] = 1; - fastHashArray(a.buffer); + fastHashArrayBuffer(a); const t2 = performance.now(); @@ -48,7 +48,7 @@ test('fastHashArray is deterministic', () => { a[42] = 69; const b = new Int32Array(1000); b[42] = 69; - const hashA = fastHashArray(a.buffer); - const hashB = fastHashArray(b.buffer); + const hashA = fastHashArrayBuffer(a); + const hashB = fastHashArrayBuffer(b); expect(hashA).toEqual(hashB); }); diff --git a/packages/utils/src/fastHash.ts b/packages/utils/src/fastHash.ts index 8feba27..8259bcd 100644 --- a/packages/utils/src/fastHash.ts +++ b/packages/utils/src/fastHash.ts @@ -1,156 +1,45 @@ -// https://github.com/6502/sha256/blob/main/sha256.js -function sha256(data?: string | Int32Array) { - let h0 = 0x6a09e667, - h1 = 0xbb67ae85, - h2 = 0x3c6ef372, - h3 = 0xa54ff53a, - h4 = 0x510e527f, - h5 = 0x9b05688c, - h6 = 0x1f83d9ab, - h7 = 0x5be0cd19, - tsz = 0, - bp = 0; - const k = [ - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, - 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, - 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, - 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, - 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, - 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, - 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, - 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, - 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, - ], - rrot = (x: number, n: number) => (x >>> n) | (x << (32 - n)), - w = new Uint32Array(64), - buf = new Uint8Array(64), - process = () => { - for (let j = 0, r = 0; j < 16; j++, r += 4) { - w[j] = - (buf[r] << 24) | (buf[r + 1] << 16) | (buf[r + 2] << 8) | buf[r + 3]; - } - for (let j = 16; j < 64; j++) { - let s0 = rrot(w[j - 15], 7) ^ rrot(w[j - 15], 18) ^ (w[j - 15] >>> 3); - let s1 = rrot(w[j - 2], 17) ^ rrot(w[j - 2], 19) ^ (w[j - 2] >>> 10); - w[j] = (w[j - 16] + s0 + w[j - 7] + s1) | 0; - } - let a = h0, - b = h1, - c = h2, - d = h3, - e = h4, - f = h5, - g = h6, - h = h7; - for (let j = 0; j < 64; j++) { - let S1 = rrot(e, 6) ^ rrot(e, 11) ^ rrot(e, 25), - ch = (e & f) ^ (~e & g), - t1 = (h + S1 + ch + k[j] + w[j]) | 0, - S0 = rrot(a, 2) ^ rrot(a, 13) ^ rrot(a, 22), - maj = (a & b) ^ (a & c) ^ (b & c), - t2 = (S0 + maj) | 0; - h = g; - g = f; - f = e; - e = (d + t1) | 0; - d = c; - c = b; - b = a; - a = (t1 + t2) | 0; - } - h0 = (h0 + a) | 0; - h1 = (h1 + b) | 0; - h2 = (h2 + c) | 0; - h3 = (h3 + d) | 0; - h4 = (h4 + e) | 0; - h5 = (h5 + f) | 0; - h6 = (h6 + g) | 0; - h7 = (h7 + h) | 0; - bp = 0; - }, - add = (input: string | Int32Array) => { - const data = - typeof input === "string" - ? typeof TextEncoder === "undefined" - ? //@ts-ignore - Buffer.from(input) - : new TextEncoder().encode(input) - : input; +export function fastHashArrayBuffer(input: string | Int32Array): string { + const mask = (1n << 64n) - 1n - for (let i = 0; i < data.length; i++) { - buf[bp++] = data[i]; - if (bp === 64) process(); - } - tsz += data.length; - }, - digest = () => { - buf[bp++] = 0x80; - if (bp == 64) process(); - if (bp + 8 > 64) { - while (bp < 64) buf[bp++] = 0x00; - process(); - } - while (bp < 58) buf[bp++] = 0x00; - // Max number of bytes is 35,184,372,088,831 - let L = tsz * 8; - buf[bp++] = (L / 1099511627776) & 255; - buf[bp++] = (L / 4294967296) & 255; - buf[bp++] = L >>> 24; - buf[bp++] = (L >>> 16) & 255; - buf[bp++] = (L >>> 8) & 255; - buf[bp++] = L & 255; - process(); - let reply = new Uint8Array(32); - reply[0] = h0 >>> 24; - reply[1] = (h0 >>> 16) & 255; - reply[2] = (h0 >>> 8) & 255; - reply[3] = h0 & 255; - reply[4] = h1 >>> 24; - reply[5] = (h1 >>> 16) & 255; - reply[6] = (h1 >>> 8) & 255; - reply[7] = h1 & 255; - reply[8] = h2 >>> 24; - reply[9] = (h2 >>> 16) & 255; - reply[10] = (h2 >>> 8) & 255; - reply[11] = h2 & 255; - reply[12] = h3 >>> 24; - reply[13] = (h3 >>> 16) & 255; - reply[14] = (h3 >>> 8) & 255; - reply[15] = h3 & 255; - reply[16] = h4 >>> 24; - reply[17] = (h4 >>> 16) & 255; - reply[18] = (h4 >>> 8) & 255; - reply[19] = h4 & 255; - reply[20] = h5 >>> 24; - reply[21] = (h5 >>> 16) & 255; - reply[22] = (h5 >>> 8) & 255; - reply[23] = h5 & 255; - reply[24] = h6 >>> 24; - reply[25] = (h6 >>> 16) & 255; - reply[26] = (h6 >>> 8) & 255; - reply[27] = h6 & 255; - reply[28] = h7 >>> 24; - reply[29] = (h7 >>> 16) & 255; - reply[30] = (h7 >>> 8) & 255; - reply[31] = h7 & 255; - let res = ""; - reply.forEach((x) => (res += ("0" + x.toString(16)).slice(-2))); - return res; - }; + // FNV-1a 64-bit constants + let h = 0xcbf29ce484222325n // offset basis + const FNV_PRIME = 0x100000001b3n - if (data) add(data); + // get bytes for string or Int32Array + let bytes: Uint8Array + if (typeof input === "string") { + // utf-8 encoding + bytes = new TextEncoder().encode(input) + } else { + // Int32Array -> bytes (little-endian) + bytes = new Uint8Array(input.length * 4) + for (let i = 0; i < input.length; i++) { + const v = input[i] >>> 0 // ensure unsigned 32-bit + const base = i * 4 + bytes[base] = v & 0xff + bytes[base + 1] = (v >>> 8) & 0xff + bytes[base + 2] = (v >>> 16) & 0xff + bytes[base + 3] = (v >>> 24) & 0xff + } + } - return { add, digest }; + // FNV-1a byte-wise + for (let i = 0; i < bytes.length; i++) { + h = (h ^ BigInt(bytes[i])) & mask + h = (h * FNV_PRIME) & mask + } + + // MurmurHash3's fmix64 finalizer (good avalanche) + h ^= h >> 33n + h = (h * 0xff51afd7ed558ccdn) & mask + h ^= h >> 33n + h = (h * 0xc4ceb9fe1a85ec53n) & mask + h ^= h >> 33n + + // to 16-char hex + return h.toString(16).padStart(16, "0").slice(-16) } -export function fastHashArrayBuffer(buffer: string | Int32Array): string { - return sha256(buffer).digest(); -} - -// Shamelessly copied from -// https://stackoverflow.com/a/8831937 export function fastHashString(input: string) { if (input.length === 0) return 0; @@ -162,20 +51,3 @@ export function fastHashString(input: string) { return hash; } - -export function fastHash(input: (string | Int32Array | number)[]) { - const s = sha256(); - - for (let i = 0; i < input.length; i++) { - const v = input[i]; - if (typeof v === "string") { - s.add(v); - } else if (v instanceof Int32Array) { - s.add(v); - } else { - s.add(v.toString()); - } - } - - return s.digest(); -}