Use tailwind v4 @theme block so we can use bg-layer-0 instead of bg-[--layer-0] for theme colors.
329 lines
7.7 KiB
Svelte
329 lines
7.7 KiB
Svelte
<script lang="ts">
|
|
import { humanizeNumber } from '$lib/helpers';
|
|
import { InputCheckbox } from '@nodarium/ui';
|
|
import type { PerformanceData } from '@nodarium/utils';
|
|
import BarSplit from './BarSplit.svelte';
|
|
import Monitor from './Monitor.svelte';
|
|
|
|
const { data }: { data: PerformanceData } = $props();
|
|
|
|
let activeType = $state('total');
|
|
let showAverage = $state(true);
|
|
|
|
function round(v: number) {
|
|
if (v < 1) {
|
|
return Math.floor(v * 100) / 100;
|
|
}
|
|
if (v < 10) {
|
|
return Math.floor(v * 10) / 10;
|
|
}
|
|
return Math.floor(v);
|
|
}
|
|
|
|
function getTitle(t: string) {
|
|
if (t.includes('/')) {
|
|
return `Node ${t.split('/').slice(-1).join('/')}`;
|
|
}
|
|
|
|
return t
|
|
.split('-')
|
|
.map((v) => v[0].toUpperCase() + v.slice(1))
|
|
.join(' ');
|
|
}
|
|
|
|
const viewerKeys = [
|
|
'total-vertices',
|
|
'total-faces',
|
|
'update-geometries',
|
|
'split-result'
|
|
];
|
|
|
|
// --- Small helpers that query `data` directly ---
|
|
function getAverage(key: string) {
|
|
const vals = data
|
|
.map((run) => run[key]?.[0])
|
|
.filter((v) => v !== undefined) as number[];
|
|
|
|
if (vals.length === 0) return 0;
|
|
return vals.reduce((acc, v) => acc + v, 0) / vals.length;
|
|
}
|
|
|
|
function getLast(key: string) {
|
|
return data.at(-1)?.[key]?.[0] || 0;
|
|
}
|
|
|
|
const averages = $derived.by(() => {
|
|
const lr = data.at(-1);
|
|
if (!lr) return {} as Record<string, number>;
|
|
return Object.keys(lr).reduce((acc: Record<string, number>, key) => {
|
|
acc[key] = getAverage(key);
|
|
return acc;
|
|
}, {});
|
|
});
|
|
|
|
const lasts = $derived.by(() => data.at(-1) || {});
|
|
|
|
const totalPerformance = $derived.by(() => {
|
|
const onlyLast = getLast('runtime')
|
|
+ getLast('update-geometries')
|
|
+ getLast('worker-transfer');
|
|
const average = getAverage('runtime')
|
|
+ getAverage('update-geometries')
|
|
+ getAverage('worker-transfer');
|
|
return { onlyLast, average };
|
|
});
|
|
|
|
const cacheRatio = $derived.by(() => {
|
|
return {
|
|
onlyLast: Math.floor(getLast('cache-hit') * 100),
|
|
average: Math.floor(getAverage('cache-hit') * 100)
|
|
};
|
|
});
|
|
|
|
const performanceData = $derived.by(() => {
|
|
const source = showAverage ? averages : lasts;
|
|
return Object.entries(source)
|
|
.filter(
|
|
([key]) =>
|
|
!key.startsWith('node/')
|
|
&& key !== 'total'
|
|
&& !key.includes('cache')
|
|
&& !viewerKeys.includes(key)
|
|
)
|
|
.sort((a, b) => b[1] - a[1]);
|
|
});
|
|
|
|
const nodePerformanceData = $derived.by(() => {
|
|
const source = showAverage ? averages : lasts;
|
|
return Object.entries(source)
|
|
.filter(([key]) => key.startsWith('node/'))
|
|
.sort((a, b) => b[1] - a[1]);
|
|
});
|
|
|
|
const viewerPerformanceData = $derived.by(() => {
|
|
const source = showAverage ? averages : lasts;
|
|
return Object.entries(source)
|
|
.filter(
|
|
([key]) =>
|
|
key !== 'total-vertices'
|
|
&& key !== 'total-faces'
|
|
&& viewerKeys.includes(key)
|
|
)
|
|
.sort((a, b) => b[1] - a[1]);
|
|
});
|
|
|
|
const splitValues = $derived.by(() => {
|
|
if (showAverage) {
|
|
return [
|
|
getAverage('worker-transfer'),
|
|
getAverage('runtime'),
|
|
getAverage('update-geometries')
|
|
];
|
|
}
|
|
return [
|
|
getLast('worker-transfer'),
|
|
getLast('runtime'),
|
|
getLast('update-geometries')
|
|
];
|
|
});
|
|
|
|
const totalPoints = $derived.by(() => {
|
|
if (showAverage) {
|
|
return data.map((run) => {
|
|
return (
|
|
(run['runtime']?.reduce((acc, v) => acc + v, 0) || 0)
|
|
+ (run['update-geometries']?.reduce((acc, v) => acc + v, 0) || 0)
|
|
+ (run['worker-transfer']?.reduce((acc, v) => acc + v, 0) || 0)
|
|
);
|
|
});
|
|
}
|
|
|
|
return data.map((run) => {
|
|
return (
|
|
(run['runtime']?.[0] || 0)
|
|
+ (run['update-geometries']?.[0] || 0)
|
|
+ (run['worker-transfer']?.[0] || 0)
|
|
);
|
|
});
|
|
});
|
|
|
|
function constructPoints(key: string) {
|
|
if (key === 'total') {
|
|
return totalPoints;
|
|
}
|
|
return data.map((run) => {
|
|
if (key in run) {
|
|
if (showAverage) {
|
|
return run[key].reduce((acc, v) => acc + v, 0) / run[key].length;
|
|
} else {
|
|
return run[key][0];
|
|
}
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
const computedTotalDisplay = $derived.by(() =>
|
|
round(showAverage ? totalPerformance.average : totalPerformance.onlyLast)
|
|
);
|
|
|
|
const computedFps = $derived.by(() =>
|
|
Math.floor(
|
|
1000
|
|
/ (showAverage
|
|
? totalPerformance.average || 1
|
|
: totalPerformance.onlyLast || 1)
|
|
)
|
|
);
|
|
</script>
|
|
|
|
{#if data.length !== 0}
|
|
{#if activeType === 'cache-hit'}
|
|
<Monitor
|
|
title="Cache Hits"
|
|
points={constructPoints(activeType)}
|
|
min={0}
|
|
max={1}
|
|
type="%"
|
|
/>
|
|
{:else}
|
|
<Monitor
|
|
title={getTitle(activeType)}
|
|
points={constructPoints(activeType)}
|
|
/>
|
|
{/if}
|
|
|
|
<div class="p-4 performance-tabler">
|
|
<div class="flex items-center gap-2">
|
|
<InputCheckbox id="show-total" bind:value={showAverage} />
|
|
<label for="show-total">Show Average</label>
|
|
</div>
|
|
|
|
<BarSplit
|
|
labels={['worker-transfer', 'runtime', 'update-geometries']}
|
|
values={splitValues}
|
|
/>
|
|
|
|
<h3>General</h3>
|
|
|
|
<table>
|
|
<tbody>
|
|
<tr>
|
|
<td>
|
|
{computedTotalDisplay}<span>ms</span>
|
|
</td>
|
|
<td
|
|
class:active={activeType === 'total'}
|
|
onclick={() => (activeType = 'total')}
|
|
>
|
|
total<span>({computedFps}fps)</span>
|
|
</td>
|
|
</tr>
|
|
|
|
{#each performanceData as [key, value] (key)}
|
|
<tr>
|
|
<td>{round(value)}<span>ms</span></td>
|
|
<td
|
|
class:active={activeType === key}
|
|
onclick={() => (activeType = key)}
|
|
>
|
|
{key}
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
|
|
<tr>
|
|
<td>{data.length}</td>
|
|
<td>Samples</td>
|
|
</tr>
|
|
</tbody>
|
|
|
|
<tbody>
|
|
<tr><td><h3>Nodes</h3></td></tr>
|
|
</tbody>
|
|
|
|
<tbody>
|
|
<tr>
|
|
<td>{showAverage ? cacheRatio.average : cacheRatio.onlyLast}<span>%</span></td>
|
|
<td
|
|
class:active={activeType === 'cache-hit'}
|
|
onclick={() => (activeType = 'cache-hit')}
|
|
>
|
|
cache hits
|
|
</td>
|
|
</tr>
|
|
|
|
{#each nodePerformanceData as [key, value] (key)}
|
|
<tr>
|
|
<td>{round(value)}<span>ms</span></td>
|
|
<td
|
|
class:active={activeType === key}
|
|
onclick={() => (activeType = key)}
|
|
>
|
|
{key.split('/').slice(-1).join('/')}
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
|
|
<tbody>
|
|
<tr><td><h3>Viewer</h3></td></tr>
|
|
</tbody>
|
|
|
|
<tbody>
|
|
<tr>
|
|
<td>{humanizeNumber(getLast('total-vertices'))}</td>
|
|
<td>Vertices</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{humanizeNumber(getLast('total-faces'))}</td>
|
|
<td>Faces</td>
|
|
</tr>
|
|
|
|
{#each viewerPerformanceData as [key, value] (key)}
|
|
<tr>
|
|
<td>{round(value)}<span>ms</span></td>
|
|
<td
|
|
class:active={activeType === key}
|
|
onclick={() => (activeType = key)}
|
|
>
|
|
{key.split('/').slice(-1).join('/')}
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{:else}
|
|
<p>No runs available</p>
|
|
{/if}
|
|
|
|
<style>
|
|
h3 {
|
|
margin: 0;
|
|
margin-top: 1em;
|
|
margin-bottom: 0.2em;
|
|
margin-left: 3px;
|
|
}
|
|
span {
|
|
opacity: 0.3;
|
|
margin-left: 4px;
|
|
}
|
|
table {
|
|
margin-bottom: 70px;
|
|
}
|
|
td {
|
|
padding-right: 10px;
|
|
padding-block: 5px;
|
|
}
|
|
td.active {
|
|
font-weight: bold;
|
|
}
|
|
tr > td:nth-child(1) {
|
|
text-align: right;
|
|
}
|
|
tr > td:nth-child(2) {
|
|
opacity: 0.5;
|
|
}
|
|
</style>
|