Files
fnx_web/svelte/src/admin_panel/Home.svelte

344 lines
10 KiB
Svelte

<script lang="ts">
import { onMount } from "svelte";
import { formatDataVolume, formatThousands, formatDate, formatNumber, formatDuration } from "util/Formatting";
import Chart from "util/Chart.svelte";
import { color_by_name } from "util/Util";
import ServerDiagnostics from "./ServerDiagnostics.svelte";
import PeerTable from "./PeerTable.svelte";
import { get_endpoint } from "lib/NovaAPI";
let graphEgress: Chart = $state()
let graphDownloads: Chart = $state()
let graphTimeout: NodeJS.Timeout = null
let start_time = $state("")
let end_time = $state("")
let total_egress = $state(0)
let total_downloads = $state(0)
const loadGraph = (minutes: number, interval: number, live: boolean) => {
if (graphTimeout !== null) { clearTimeout(graphTimeout) }
if (live) {
graphTimeout = setTimeout(() => { loadGraph(minutes, interval, true) }, 10000)
}
let today = new Date()
let start = new Date()
start.setMinutes(start.getMinutes() - minutes)
fetch(
get_endpoint() + "/admin/files/timeseries" +
"?start=" + start.toISOString() +
"&end=" + today.toISOString() +
"&interval=" + interval
).then(resp => {
if (!resp.ok) { return Promise.reject("Error: " + resp.status); }
return resp.json();
}).then(resp => {
resp.egress.timestamps.forEach((val, idx) => {
let date = new Date(val);
let dateStr: string = date.getFullYear().toString();
dateStr += "-" + ("00" + (date.getMonth() + 1)).slice(-2);
dateStr += "-" + ("00" + date.getDate()).slice(-2);
dateStr += " " + ("00" + date.getHours()).slice(-2);
dateStr += ":" + ("00" + date.getMinutes()).slice(-2);
resp.egress.timestamps[idx] = " " + dateStr + " "; // Poor man's padding
});
graphEgress.data().labels = resp.egress.timestamps;
graphEgress.data().datasets[0].data = resp.egress.amounts;
graphDownloads.data().labels = resp.downloads.timestamps;
graphDownloads.data().datasets[0].data = resp.downloads.amounts
graphEgress.update()
graphDownloads.update()
start_time = resp.egress.timestamps[0]
end_time = resp.egress.timestamps.slice(-1)[0];
total_egress = resp.egress.amounts.reduce((acc, val) => acc + val)
total_downloads = resp.downloads.amounts.reduce((acc, val) => acc + val)
})
}
// Load performance statistics
let lastOrder: string = $state();
let status = $state({
pid: 0,
cpu_profile_running_since: "",
db_latency: 0,
db_time: "",
cache_threshold: 0,
local_read_size: 0,
local_read_size_per_sec: 0,
local_reads: 0,
local_reads_per_sec: 0,
local_readers: 0,
neighbour_read_size: 0,
neighbour_read_size_per_sec: 0,
neighbour_reads: 0,
neighbour_reads_per_sec: 0,
neighbour_readers: 0,
remote_read_size: 0,
remote_read_size_per_sec: 0,
remote_reads: 0,
remote_reads_per_sec: 0,
remote_readers: 0,
peers: [],
query_statistics: [],
running_since: "",
stats_watcher_listeners: 0,
stats_watcher_threads: 0,
filesystem_watcher_listeners: 0,
filesystem_watcher_threads: 0,
})
let total_reads = $derived(status.local_reads + status.neighbour_reads + status.remote_reads)
let total_read_size = $derived(status.local_read_size + status.neighbour_read_size + status.remote_read_size)
function getStats(order: string) {
lastOrder = order
fetch(get_endpoint() + "/status").then(
resp => resp.json()
).then(resp => {
// Sort all queries by the selected sort column
resp.query_statistics.sort((a, b) => {
if (typeof (a[order]) === "number") {
// Sort ints from high to low
return b[order] - a[order]
} else {
// Sort strings alphabetically
return a[order].localeCompare(b[order])
}
})
// Sort individual query callers by frequency
resp.query_statistics.forEach((v) => {
v.callers.sort((a, b) => b.count - a.count)
})
status = resp
})
}
let statsInterval = null
onMount(() => {
// Prepare chart datasets
graphEgress.data().datasets = [
{
label: "Egress",
data: [],
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("highlight_color"),
},
];
graphDownloads.data().datasets = [
{
label: "Downloads",
data: [],
borderWidth: 2,
pointRadius: 0,
borderColor: color_by_name("highlight_color"),
backgroundColor: color_by_name("highlight_color"),
},
];
loadGraph(10080, 10, true);
getStats("calls")
statsInterval = setInterval(() => {
getStats(lastOrder)
}, 10000)
return () => {
if (graphTimeout !== null) {
clearTimeout(graphTimeout)
}
if (statsInterval !== null) {
clearInterval(statsInterval)
}
}
})
</script>
<section>
<h3>Bandwidth usage and file views</h3>
</section>
<div>
<button onclick={() => loadGraph(1440, 1, true)}>Day 1m</button>
<button onclick={() => loadGraph(10080, 10, true)}>Week 10m</button>
<button onclick={() => loadGraph(43200, 60, true)}>Month 1h</button>
<button onclick={() => loadGraph(131400, 1440, false)}>Quarter 1d</button>
<button onclick={() => loadGraph(262800, 1440, false)}>Half-year 1d</button>
<button onclick={() => loadGraph(525600, 1440, false)}>Year 1d</button>
<button onclick={() => loadGraph(1051200, 1440, false)}>Two Years 1d</button>
<button onclick={() => loadGraph(2628000, 1440, false)}>Five Years 1d</button>
</div>
<Chart bind:this={graphEgress} data_type="bytes" ticks={false}/>
<Chart bind:this={graphDownloads} data_type="number" ticks={false}/>
<div>
Total usage from {start_time} to {end_time}<br/>
{formatDataVolume(total_egress, 3)} egress,
{formatThousands(total_downloads)} downloads
</div>
<br/>
<ServerDiagnostics running_since={status.cpu_profile_running_since} refresh={() => getStats(lastOrder)}/>
<section>
<h3>Process stats</h3>
<div class="table_scroll">
<table>
<tbody>
<tr>
<td>DB Time</td>
<td>{formatDate(new Date(status.db_time), true, true, true)}</td>
<td>DB Latency</td>
<td>{formatNumber(status.db_latency / 1000, 3)} ms</td>
<td>PID</td>
<td>{status.pid}</td>
</tr>
</tbody>
</table>
</div>
<h3>Pixelstore stats</h3>
<div class="table_scroll">
<table>
<thead>
<tr>
<td>Source</td>
<td>Current</td>
<td>Reads</td>
<td>Reads %</td>
<td>Reads / s</td>
<td>Total size</td>
<td>Size %</td>
<td>Size / s</td>
</tr>
</thead>
<tbody>
<tr>
<td>Local cache</td>
<td>{status.local_readers}</td>
<td>{status.local_reads}</td>
<td>{((status.local_reads / total_reads) * 100).toPrecision(3)}%</td>
<td>{status.local_reads_per_sec.toPrecision(4)}/s</td>
<td>{formatDataVolume(status.local_read_size, 4)}</td>
<td>{((status.local_read_size / total_read_size) * 100).toPrecision(3)}%</td>
<td>{formatDataVolume(status.local_read_size_per_sec, 4)}/s</td>
</tr>
<tr>
<td>Neighbour</td>
<td>{status.neighbour_readers}</td>
<td>{status.neighbour_reads}</td>
<td>{((status.neighbour_reads / total_reads) * 100).toPrecision(3)}%</td>
<td>{status.neighbour_reads_per_sec.toPrecision(4)}/s</td>
<td>{formatDataVolume(status.neighbour_read_size, 4)}</td>
<td>{((status.neighbour_read_size / total_read_size) * 100).toPrecision(3)}%</td>
<td>{formatDataVolume(status.neighbour_read_size_per_sec, 4)}/s</td>
</tr>
<tr>
<td>Reed-solomon</td>
<td>{status.remote_readers}</td>
<td>{status.remote_reads}</td>
<td>{((status.remote_reads / total_reads) * 100).toPrecision(3)}%</td>
<td>{status.remote_reads_per_sec.toPrecision(4)}/s</td>
<td>{formatDataVolume(status.remote_read_size, 4)}</td>
<td>{((status.remote_read_size / total_read_size) * 100).toPrecision(3)}%</td>
<td>{formatDataVolume(status.remote_read_size_per_sec, 4)}/s</td>
</tr>
<tr>
<td>Total</td>
<td>{status.local_readers+status.neighbour_readers+status.remote_readers}</td>
<td>{status.local_reads+status.neighbour_reads+status.remote_reads}</td>
<td></td>
<td>{(status.local_reads_per_sec+status.neighbour_reads_per_sec+status.remote_reads_per_sec).toPrecision(4)}/s</td>
<td>{formatDataVolume(status.local_read_size+status.neighbour_read_size+status.remote_read_size, 4)}</td>
<td></td>
<td>{formatDataVolume(status.local_read_size_per_sec+status.neighbour_read_size_per_sec+status.remote_read_size_per_sec, 4)}/s</td>
</tr>
</tbody>
</table>
</div>
<p>
Cache threshold: {formatDataVolume(status.cache_threshold, 4)}
</p>
<h3>Socket stats</h3>
<table>
<thead>
<tr>
<td>Watcher</td>
<td>Threads</td>
<td>Listeners</td>
<td>Ratio</td>
</tr>
</thead>
<tbody>
<tr>
<td>Filesystem statistics (per file)</td>
<td>{status.filesystem_watcher_threads}</td>
<td>{status.filesystem_watcher_listeners}</td>
<td>{(status.filesystem_watcher_listeners / status.filesystem_watcher_threads).toPrecision(3)}</td>
</tr>
</tbody>
</table>
<h3>Cache nodes</h3>
</section>
<PeerTable peers={status.peers.reduce((acc, val) => {if (val.role === "cache") {acc.push(val)}; return acc}, [])}/>
<section>
<h3>Storage nodes</h3>
</section>
<PeerTable peers={status.peers.reduce((acc, val) => {if (val.role === "storage") {acc.push(val)}; return acc}, [])}/>
<section>
<h3>Query statistics</h3>
<div class="table_scroll" style="text-align: left;">
<table>
<thead>
<tr>
<td>
<button onclick={() => { getStats('query_name') }}>
Query
</button>
</td>
<td>
<button style="cursor: pointer;" onclick={() => { getStats('calls') }}>
Calls
</button>
</td>
<td>
<button style="cursor: pointer;" onclick={() => { getStats('average_duration') }}>
Avg
</button>
</td>
<td>
<button style="cursor: pointer;" onclick={() => { getStats('total_duration') }}>
Total
</button>
</td>
<td>Callers</td>
</tr>
</thead>
<tbody id="tstat_body">
{#each status.query_statistics as q}
<tr>
<td>{q.query_name}</td>
<td>{q.calls}</td>
<td>{q.average_duration}ms</td>
<td>{formatDuration(q.total_duration, 0)}</td>
<td>
{#each q.callers as caller}
{caller.count}x {caller.name}<br/>
{/each}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>