feat: implement performance view
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m10s

This commit is contained in:
max_richter 2024-04-25 03:37:52 +02:00
parent c28ef550a9
commit e0e1743b77
12 changed files with 421 additions and 179 deletions

View File

@ -52,7 +52,6 @@
let mouseDown: null | [number, number] = null;
let mouseDownId = -1;
let boxSelection = false;
let loaded = false;
const cameraDown = [0, 0];
let cameraPosition: [number, number, number] = [0, 0, 4];
let addMenuPosition: [number, number] | null = null;
@ -783,7 +782,7 @@
event.preventDefault();
isDragging = false;
if (!event.dataTransfer) return;
const nodeId: NodeId = event.dataTransfer.getData("data/node-id");
const nodeId = event.dataTransfer.getData("data/node-id") as NodeId;
if (nodeId) {
let mx = event.clientX - rect.x;
@ -805,7 +804,7 @@
}
const pos = projectScreenToWorld(mx, my);
graph.load([nodeId]).then(() => {
graph.registry.load([nodeId]).then(() => {
graph.createNode({
type: nodeId,
props,

View File

@ -114,3 +114,15 @@ export function withSubComponents<A, B extends Record<string, any>>(
});
return component as A & B;
}
export function humanizeNumber(number: number): string {
const suffixes = ["", "K", "M", "B", "T"];
if (number < 1000) {
return number.toString();
}
const numLength = Math.floor(Math.log10(number)) + 1;
const baseIndex = Math.floor((numLength - 1) / 3);
const base = Math.pow(10, baseIndex * 3);
const rounded = Math.round(number / base * 10) / 10;
return rounded + suffixes[baseIndex];
}

View File

@ -0,0 +1,68 @@
<script lang="ts">
export let points: number[];
$: max = Math.max(...points);
$: min = Math.min(...points);
function constructPath() {
return points
.map((point, i) => {
const x = (i / (points.length - 1)) * 100;
const y = 100 - ((point - min) / (max - min)) * 100;
return `${x},${y}`;
})
.join(" ");
}
</script>
<div class="wrapper">
<p>Runtime Execution</p>
<span class="min">{min}ms</span>
<span class="max">{max}ms</span>
<svg preserveAspectRatio="none" viewBox="0 0 100 100">
<polyline vector-effect="non-scaling-stroke" points={constructPath()} />
</svg>
</div>
<style>
span {
position: absolute;
right: 10px;
font-size: 0.8em;
opacity: 0.5;
}
.max {
top: 4px;
}
.min {
bottom: 5px;
}
.wrapper {
position: relative;
border-bottom: solid thin var(--outline);
display: flex;
}
p {
margin: 0px;
top: 3px;
left: 5px;
font-size: 0.9em;
opacity: 0.5;
position: absolute;
}
svg {
height: 124px;
margin: 24px 0px;
border-top: solid thin var(--outline);
border-bottom: solid thin var(--outline);
width: 100%;
}
polyline {
fill: none;
stroke: var(--layer-3);
opacity: 0.5;
stroke-width: 1;
}
</style>

View File

@ -1,17 +1,122 @@
<script lang="ts">
import type { PerformanceData } from ".";
import { browser } from "$app/environment";
import Monitor from "./Monitor.svelte";
import { humanizeNumber } from "$lib/helpers";
type PerformanceData = {
total: Record<string, number>;
runs: Record<string, number[]>[];
};
export let data: PerformanceData;
export let viewer: PerformanceData;
function getPerformanceData() {
return Object.entries(data.total).sort((a, b) => b[1] - a[1]);
return Object.entries(data.total)
.sort((a, b) => b[1] - a[1])
.filter(([key]) => !key.startsWith("node/"));
}
function getNodePerformanceData() {
return Object.entries(data.total)
.filter(([key]) => key.startsWith("node/"))
.sort((a, b) => b[1] - a[1]);
}
function getViewerPerformanceData() {
return Object.entries(viewer.total)
.filter(([key]) => key !== "total-vertices" && key !== "total-faces")
.sort((a, b) => b[1] - a[1]);
}
function constructPoints(key: keyof (typeof data.runs)[0]) {
return data.runs
.map((run, i) => {
return run[key][0];
})
.slice(-100);
}
</script>
{#if data.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}
{#key data}
{#if browser}
<Monitor points={constructPoints("total")} />
{/if}
<div class="px-4">
{#if data.runs.length !== 0}
<h3>General</h3>
<table>
{#each getPerformanceData() as [key, value]}
<tr>
<td>
{Math.floor(value * 100) / 100}<span>ms</span>
</td>
<td>
{key}
</td>
</tr>
{/each}
<h3>Nodes</h3>
{#each getNodePerformanceData() as [key, value]}
<tr>
<td>
{Math.floor(value * 100) / 100}<span>ms</span>
</td>
<td>
{key.split("/").slice(-1).join("/")}
</td>
</tr>
{/each}
{#if viewer.runs.length}
<h3>Viewer</h3>
<tr>
<td>{humanizeNumber(viewer.runs.at(-1)?.["total-vertices"])}</td>
<td>Vertices</td>
</tr>
<tr>
<td>{humanizeNumber(viewer.runs.at(-1)?.["total-faces"])}</td>
<td>Faces</td>
</tr>
{#each getViewerPerformanceData() as [key, value]}
<tr>
<td>
{Math.floor(value * 100) / 100}<span>ms</span>
</td>
<td>
{key.split("/").slice(-1).join("/")}
</td>
</tr>
{/each}
{/if}
</table>
{:else}
<p>No runs available</p>
{/if}
</div>
{/key}
<style>
h3 {
margin: 0;
margin-top: 1em;
margin-bottom: 0.2em;
margin-left: 3px;
}
span {
opacity: 0.3;
margin-left: 4px;
}
td {
padding-right: 10px;
padding-block: 5px;
}
tr > td:nth-child(1) {
text-align: right;
}
tr > td:nth-child(2) {
opacity: 0.5;
}
</style>

View File

@ -1,68 +1,2 @@
import { readable, type Readable } from "svelte/store";
export 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;
get: () => PerformanceData;
}
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;
if (set) set(data);
}
}
function addPoint(name: string, value: number) {
if (!currentRun) return;
currentRun[name] = currentRun[name] || [];
currentRun[name].push(value);
}
function get() {
return data;
}
return {
subscribe,
startRun,
stopRun,
addPoint,
get
}
}
export * from "./store";
export { default as PerformanceViewer } from "./PerformanceViewer.svelte";

View File

@ -0,0 +1,67 @@
import { readable, type Readable } from "svelte/store";
export 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;
get: () => PerformanceData;
}
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;
if (set) set(data);
}
}
function addPoint(name: string, value: number) {
if (!currentRun) return;
currentRun[name] = currentRun[name] || [];
currentRun[name].push(value);
}
function get() {
return data;
}
return {
subscribe,
startRun,
stopRun,
addPoint,
get
}
}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { T } from "@threlte/core";
import { T, useTask } from "@threlte/core";
import {
MeshLineGeometry,
MeshLineMaterial,
@ -7,9 +7,12 @@
useTexture,
} from "@threlte/extras";
import {
type Group,
type BufferGeometry,
type PerspectiveCamera,
type Vector3,
Vector3,
type Vector3Tuple,
Box3,
} from "three";
import type { OrbitControls as OrbitControlsType } from "three/addons/controls/OrbitControls.js";
import { OrbitControls } from "@threlte/extras";
@ -18,19 +21,21 @@
export let geometries: BufferGeometry[];
export let lines: Vector3[][];
let geos: Group;
export let camera: PerspectiveCamera;
export let controls: OrbitControlsType;
let camera: PerspectiveCamera;
let controls: OrbitControlsType;
export let centerCamera: boolean = true;
const matcap = useTexture("/matcap_green.jpg");
const cameraTransform = localStore<{ camera: number[]; target: number[] }>(
"nodes.camera.transform",
{
camera: [0, 0, 10],
target: [0, 0, 0],
},
);
const cameraTransform = localStore<{
camera: Vector3Tuple;
target: Vector3Tuple;
}>("nodes.camera.transform", {
camera: [0, 0, 10],
target: [0, 0, 0],
});
function saveCameraState() {
if (!camera) return;
@ -41,12 +46,49 @@
}
function getPosition(geo: BufferGeometry, i: number) {
const pos = [
return [
geo.attributes.position.array[i],
geo.attributes.position.array[i + 1],
geo.attributes.position.array[i + 2],
];
return pos;
] as Vector3Tuple;
}
let cameraTarget: Vector3;
let duration = 0;
let totalDuration = 5;
const { start, stop, started } = useTask((delta) => {
duration += delta;
if (!cameraTarget) {
stop();
return;
}
// This function will be executed on every frame
if (duration >= totalDuration) {
controls.target.copy(cameraTarget);
stop();
controls.update();
} else {
const t = duration / totalDuration;
controls.target.lerp(cameraTarget, t);
controls.update();
}
});
stop();
$: if (geometries && geos && centerCamera) {
const aabb = new Box3();
aabb.setFromObject(geos);
const newCenter = aabb.getCenter(new Vector3());
if (
newCenter &&
newCenter.x !== 0 &&
newCenter.y !== 0 &&
newCenter.z !== 0
) {
cameraTarget = newCenter;
duration = 0;
start();
}
}
</script>
@ -70,30 +112,32 @@
<T.DirectionalLight position={[0, 10, 10]} />
<T.AmbientLight intensity={2} />
{#each geometries as geo}
{#if $AppSettings.showIndices}
{#each geo.attributes.position.array as _, i}
{#if i % 3 === 0}
<Text text={i / 3} fontSize={0.25} position={getPosition(geo, i)} />
{/if}
{/each}
{/if}
<T.Group bind:ref={geos}>
{#each geometries as geo}
{#if $AppSettings.showIndices}
{#each geo.attributes.position.array as _, i}
{#if i % 3 === 0}
<Text fontSize={0.25} position={getPosition(geo, i)} />
{/if}
{/each}
{/if}
{#if $AppSettings.showVertices}
<T.Points visible={true}>
<T is={geo} />
<T.PointsMaterial size={0.25} />
</T.Points>
{/if}
{#await matcap then value}
<T.Mesh geometry={geo}>
<T.MeshMatcapMaterial matcap={value} wireframe={$AppSettings.wireframe} />
{#if false}
<T.MeshStandardMaterial color="green" depthTest={true} />
{/if}
</T.Mesh>
{/await}
{/each}
{#if $AppSettings.showVertices}
<T.Points visible={true}>
<T is={geo} />
<T.PointsMaterial size={0.25} />
</T.Points>
{/if}
{#await matcap then value}
<T.Mesh geometry={geo}>
<T.MeshMatcapMaterial
matcap={value}
wireframe={$AppSettings.wireframe}
/>
</T.Mesh>
{/await}
{/each}
</T.Group>
{#if $AppSettings.showStemLines && lines}
{#each lines as line}

View File

@ -1,36 +1,34 @@
<script lang="ts">
import { Canvas } from "@threlte/core";
import Scene from "./Scene.svelte";
import {
BufferGeometry,
Float32BufferAttribute,
PerspectiveCamera,
Vector3,
} from "three";
import { BufferGeometry, Float32BufferAttribute, Vector3 } from "three";
import { decodeFloat } from "@nodes/utils";
import type { OrbitControls } from "three/examples/jsm/Addons.js";
import type { PerformanceStore } from "$lib/performance";
export let result: Int32Array;
let camera: PerspectiveCamera;
let controls: OrbitControls;
let center: Vector3;
export let centerCamera: boolean = true;
export let perf: PerformanceStore;
let geometries: BufferGeometry[] = [];
let lines: Vector3[][] = [];
let totalVertices = 0;
let totalFaces = 0;
function createGeometryFromEncodedData(
encodedData: Int32Array,
geometry = new BufferGeometry(),
): BufferGeometry {
const geometry = new BufferGeometry();
// Extract data from the encoded array
let index = 0;
const geometryType = encodedData[index++];
const vertexCount = encodedData[index++];
const faceCount = encodedData[index++];
totalVertices += vertexCount;
totalFaces += faceCount;
// Indices
const indicesEnd = index + faceCount * 3;
const indices = encodedData.subarray(index, indicesEnd);
@ -119,8 +117,17 @@
}
$: if (result) {
const inputs = parse_args(result);
perf?.startRun();
let a = performance.now();
const inputs = parse_args(result);
let b = performance.now();
perf?.addPoint("parse-args", b - a);
totalVertices = 0;
totalFaces = 0;
a = performance.now();
lines = inputs
.map((input) => {
if (input[0] === 0) {
@ -128,26 +135,27 @@
}
})
.filter(Boolean) as Vector3[][];
b = performance.now();
perf?.addPoint("create-lines", b - a);
center = new Vector3();
a = performance.now();
geometries = inputs
.map((input) => {
.map((input, i) => {
if (input[0] === 1) {
const geo = createGeometryFromEncodedData(input);
geo?.computeBoundingSphere();
if (geo.boundingSphere) {
center.add(geo.boundingSphere.center);
}
return geo;
return createGeometryFromEncodedData(input, geometries[i]);
}
})
.filter(Boolean) as BufferGeometry[];
b = performance.now();
perf?.addPoint("create-geometries", b - a);
center = center.divideScalar(geometries.length);
perf?.addPoint("total-vertices", totalVertices);
perf?.addPoint("total-faces", totalFaces);
perf?.stopRun();
}
</script>
<Canvas>
<Scene bind:camera bind:controls {geometries} {lines} />
<Scene {geometries} {lines} {centerCamera} />
</Canvas>

View File

@ -5,7 +5,7 @@ export const AppSettings = localStore("node-settings", {
showGrid: true,
showNodeGrid: true,
snapToGrid: true,
wireframes: false,
wireframe: false,
showIndices: false,
showVertices: false,
centerCamera: true,

View File

@ -1,7 +1,7 @@
import { MemoryRuntimeExecutor } from "./runtime-executor";
import { RemoteNodeRegistry } from "./node-registry-client";
import type { Graph } from "@nodes/types";
import { createPerformanceStore } from "./performance";
import { createPerformanceStore } from "./performance/store";
const nodeRegistry = new RemoteNodeRegistry("");
const executor = new MemoryRuntimeExecutor(nodeRegistry);

View File

@ -24,11 +24,14 @@
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 PerformanceData } from "$lib/performance/store";
const nodeRegistry = new RemoteNodeRegistry("");
const workerRuntime = new WorkerRuntimeExecutor();
let performanceData: PerformanceData;
let viewerPerformance = createPerformanceStore();
globalThis.decode = decodeNestedArray;
globalThis.encode = encodeNestedArray;
@ -51,15 +54,38 @@
let graphSettings = writable<Record<string, any>>({});
let graphSettingTypes = {};
async function handleResult(event: CustomEvent<Graph>) {
const settings = $graphSettings;
if (!settings) return;
let isWorking = false;
let unfinished:
| {
graph: Graph;
settings: Record<string, any>;
}
| undefined;
async function handleResult(_graph: Graph, _settings: Record<string, any>) {
if (!_settings) return;
if (isWorking) {
unfinished = {
graph: _graph,
settings: _settings,
};
return;
}
isWorking = true;
try {
res = await workerRuntime.execute(event.detail, settings);
res = await workerRuntime.execute(_graph, _settings);
performanceData = await workerRuntime.getPerformanceData();
isWorking = false;
} catch (error) {
console.log("errors", error);
}
if (unfinished) {
let d = unfinished;
unfinished = undefined;
handleResult(d.graph, d.settings);
}
}
$: if (AppSettings) {
@ -69,7 +95,7 @@
};
//@ts-ignore
AppSettingTypes.debug.stressTest.loadTree.callback = () => {
graph = templates.tree($AppSettings.amount, $AppSettings.amount);
graph = templates.tree($AppSettings.amount);
};
}
@ -82,7 +108,11 @@
<header></header>
<Grid.Row>
<Grid.Cell>
<Viewer centerCamera={$AppSettings.centerCamera} result={res} />
<Viewer
centerCamera={$AppSettings.centerCamera}
result={res}
perf={viewerPerformance}
/>
</Grid.Cell>
<Grid.Cell>
{#key graph}
@ -96,7 +126,7 @@
snapToGrid={$AppSettings?.snapToGrid}
bind:settings={graphSettings}
bind:settingTypes={graphSettingTypes}
on:result={handleResult}
on:result={(ev) => handleResult(ev.detail, $graphSettings)}
on:save={handleSave}
/>
<Settings>
@ -116,7 +146,10 @@
icon="i-tabler-brand-speedtest"
>
{#if performanceData}
<PerformanceViewer data={performanceData} />
<PerformanceViewer
data={performanceData}
viewer={$viewerPerformance}
/>
{/if}
</Panel>
<Panel

View File

@ -102,54 +102,26 @@ importers:
specifier: ^1.5.1
version: 1.5.1(@types/node@20.12.7)(jsdom@24.0.0)(sass@1.75.0)
nodes/max/plantarium/array: {}
nodes/max/plantarium/array/pkg: {}
nodes/max/plantarium/box: {}
nodes/max/plantarium/box/pkg: {}
nodes/max/plantarium/branches: {}
nodes/max/plantarium/branches/pkg: {}
nodes/max/plantarium/branch: {}
nodes/max/plantarium/float: {}
nodes/max/plantarium/float/pkg: {}
nodes/max/plantarium/math: {}
nodes/max/plantarium/math/pkg: {}
nodes/max/plantarium/noise: {}
nodes/max/plantarium/noise/pkg: {}
nodes/max/plantarium/output: {}
nodes/max/plantarium/output/pkg: {}
nodes/max/plantarium/random: {}
nodes/max/plantarium/random/pkg: {}
nodes/max/plantarium/stem: {}
nodes/max/plantarium/stem/pkg: {}
nodes/max/plantarium/sum: {}
nodes/max/plantarium/sum/pkg: {}
nodes/max/plantarium/triangle: {}
nodes/max/plantarium/triangle/pkg: {}
nodes/max/plantarium/vec3: {}
nodes/max/plantarium/vec3/pkg: {}
packages/types:
dependencies:
zod: