mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-20 00:51:41 +00:00
Publishes metrics to NT using a protobuf under `photonvision/coprocessors/metrics` using the device host name as the key. Refactors metrics to use numbers where possible, instead of strings. Removes GPU mem display from metrics card when it can't be determined. Updates UI metrics periodically. Resolves #1988 Closes #830 --------- Co-authored-by: Matt <matthew.morley.ca@gmail.com> Co-authored-by: Gold856 <117957790+Gold856@users.noreply.github.com>
316 lines
8.0 KiB
Vue
316 lines
8.0 KiB
Vue
<script setup lang="ts">
|
|
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
|
import { computed, onBeforeMount, ref } from "vue";
|
|
import { useStateStore } from "@/stores/StateStore";
|
|
|
|
interface MetricItem {
|
|
header: string;
|
|
value?: string;
|
|
}
|
|
|
|
const generalMetrics = computed<MetricItem[]>(() => {
|
|
const stats = [
|
|
{
|
|
header: "Version",
|
|
value: useSettingsStore().general.version || "Unknown"
|
|
},
|
|
{
|
|
header: "Hardware Model",
|
|
value: useSettingsStore().general.hardwareModel || "Unknown"
|
|
},
|
|
{
|
|
header: "Platform",
|
|
value: useSettingsStore().general.hardwarePlatform || "Unknown"
|
|
},
|
|
|
|
{
|
|
header: "GPU Acceleration",
|
|
value: useSettingsStore().general.gpuAcceleration || "Unknown"
|
|
}
|
|
];
|
|
|
|
if (!useSettingsStore().network.networkingDisabled) {
|
|
stats.push({
|
|
header: "IP Address",
|
|
value: useSettingsStore().metrics.ipAddress || "Unknown"
|
|
});
|
|
}
|
|
|
|
return stats;
|
|
});
|
|
|
|
// @ts-expect-error This uses Intl.DurationFormat which is newly implemented and not available in TS.
|
|
const durationFormatter = new Intl.DurationFormat("en", { style: "narrow" });
|
|
const platformMetrics = computed<MetricItem[]>(() => {
|
|
const metrics = useSettingsStore().metrics;
|
|
const stats = [
|
|
{
|
|
header: "CPU Temp",
|
|
value: metrics.cpuTemp === undefined || metrics.cpuTemp == -1 ? "Unknown" : `${metrics.cpuTemp}°C`
|
|
},
|
|
{
|
|
header: "CPU Usage",
|
|
value: metrics.cpuUtil === undefined ? "Unknown" : `${metrics.cpuUtil}%`
|
|
},
|
|
{
|
|
header: "CPU Memory Usage",
|
|
value:
|
|
metrics.ramUtil && metrics.ramMem && metrics.ramUtil >= 0 && metrics.ramMem >= 0
|
|
? `${metrics.ramUtil}MB of ${metrics.ramMem}MB`
|
|
: "Unknown"
|
|
},
|
|
{
|
|
header: "Uptime",
|
|
value: (() => {
|
|
const seconds = metrics.uptime;
|
|
if (seconds === undefined) return "Unknown";
|
|
|
|
const days = Math.floor(seconds / 86400);
|
|
const hours = Math.floor((seconds % 86400) / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = Math.floor(seconds % 60);
|
|
|
|
return durationFormatter.format({
|
|
days: days,
|
|
hours: hours,
|
|
minutes: minutes,
|
|
seconds: secs
|
|
});
|
|
})()
|
|
},
|
|
{
|
|
header: "Disk Usage",
|
|
value: metrics.diskUtilPct === undefined ? "Unknown" : `${metrics.diskUtilPct}%`
|
|
}
|
|
];
|
|
|
|
if (metrics.npuUsage && metrics.npuUsage.length > 0) {
|
|
stats.push({
|
|
header: "NPU Usage",
|
|
value: metrics.npuUsage?.map((usage, index) => `Core${index} ${usage}%`).join(", ") || "Unknown"
|
|
});
|
|
}
|
|
|
|
if (metrics.gpuMem && metrics.gpuMemUtil && metrics.gpuMem > 0 && metrics.gpuMemUtil > 0) {
|
|
stats.push({
|
|
header: "GPU Memory Usage",
|
|
value: `${metrics.gpuMemUtil}MB of ${metrics.gpuMem}MB`
|
|
});
|
|
}
|
|
|
|
if (metrics.cpuThr) {
|
|
stats.push({
|
|
header: "CPU Throttling",
|
|
value: metrics.cpuThr.toString()
|
|
});
|
|
}
|
|
|
|
return stats;
|
|
});
|
|
|
|
const metricsLastFetched = ref("Never");
|
|
const fetchMetrics = () => {
|
|
useSettingsStore()
|
|
.requestMetricsUpdate()
|
|
.catch((error) => {
|
|
if (error.request) {
|
|
useStateStore().showSnackbarMessage({
|
|
color: "error",
|
|
message: "Unable to fetch metrics! The backend didn't respond."
|
|
});
|
|
} else {
|
|
useStateStore().showSnackbarMessage({
|
|
color: "error",
|
|
message: "An error occurred while trying to fetch metrics."
|
|
});
|
|
}
|
|
})
|
|
.finally(() => {
|
|
const pad = (num: number): string => {
|
|
return String(num).padStart(2, "0");
|
|
};
|
|
|
|
const date = new Date();
|
|
metricsLastFetched.value = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
});
|
|
};
|
|
|
|
onBeforeMount(() => {
|
|
fetchMetrics();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<v-card class="mb-3" style="background-color: #006492">
|
|
<v-card-title style="display: flex; justify-content: space-between">
|
|
<span>Stats</span>
|
|
<v-btn variant="text" @click="fetchMetrics">
|
|
<v-icon start class="open-icon">mdi-reload</v-icon>
|
|
Last Fetched: {{ metricsLastFetched }}
|
|
</v-btn>
|
|
</v-card-title>
|
|
<v-card-text class="pt-0 pb-3">
|
|
<v-card-subtitle class="pa-0" style="font-size: 16px">General Metrics</v-card-subtitle>
|
|
<v-table class="metrics-table mt-3">
|
|
<thead>
|
|
<tr>
|
|
<th
|
|
v-for="(item, itemIndex) in generalMetrics"
|
|
:key="itemIndex"
|
|
class="metric-item metric-item-title"
|
|
:class="{
|
|
tl: itemIndex === 0,
|
|
tr: itemIndex === generalMetrics.length - 1,
|
|
t: 0 < itemIndex && itemIndex < generalMetrics.length - 1
|
|
}"
|
|
>
|
|
{{ item.header }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td
|
|
v-for="(item, itemIndex) in generalMetrics"
|
|
:key="itemIndex"
|
|
class="metric-item"
|
|
:class="{
|
|
bl: itemIndex === 0,
|
|
br: itemIndex === generalMetrics.length - 1,
|
|
b: 0 < itemIndex && itemIndex < generalMetrics.length - 1
|
|
}"
|
|
>
|
|
{{ item.value }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
</v-card-text>
|
|
<v-card-text class="pt-4">
|
|
<v-card-subtitle class="pa-0 pb-1" style="font-size: 16px">Hardware Metrics</v-card-subtitle>
|
|
<v-table class="metrics-table mt-3">
|
|
<thead>
|
|
<tr>
|
|
<th
|
|
v-for="(item, itemIndex) in platformMetrics"
|
|
:key="itemIndex"
|
|
class="metric-item metric-item-title"
|
|
:class="{
|
|
tl: itemIndex === 0,
|
|
tr: itemIndex === platformMetrics.length - 1,
|
|
t: 0 < itemIndex && itemIndex < platformMetrics.length - 1
|
|
}"
|
|
>
|
|
{{ item.header }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td
|
|
v-for="(item, itemIndex) in platformMetrics"
|
|
:key="itemIndex"
|
|
class="metric-item"
|
|
:class="{
|
|
bl: itemIndex === 0,
|
|
br: itemIndex === platformMetrics.length - 1,
|
|
b: 0 < itemIndex && itemIndex < platformMetrics.length - 1
|
|
}"
|
|
>
|
|
<span v-if="useSettingsStore().metrics.cpuUtil !== undefined">{{ item.value }}</span>
|
|
<span v-else>---</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
</v-card-text>
|
|
</v-card>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.metrics-table {
|
|
width: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
.t {
|
|
border-top: 1px solid white;
|
|
border-right: 1px solid white;
|
|
}
|
|
|
|
.b {
|
|
border-bottom: 1px solid white;
|
|
border-right: 1px solid white;
|
|
}
|
|
|
|
.tl {
|
|
border-top: 1px solid white;
|
|
border-left: 1px solid white;
|
|
border-right: 1px solid white;
|
|
border-top-left-radius: 5px;
|
|
}
|
|
|
|
.tr {
|
|
border-top: 1px solid white;
|
|
border-right: 1px solid white;
|
|
border-top-right-radius: 5px;
|
|
}
|
|
|
|
.bl {
|
|
border-bottom: 1px solid white;
|
|
border-left: 1px solid white;
|
|
border-right: 1px solid white;
|
|
border-bottom-left-radius: 5px;
|
|
}
|
|
|
|
.br {
|
|
border-bottom: 1px solid white;
|
|
border-right: 1px solid white;
|
|
border-bottom-right-radius: 5px;
|
|
}
|
|
|
|
.metric-item {
|
|
font-size: 16px !important;
|
|
padding: 1px 15px 1px 10px;
|
|
border-right: 1px solid;
|
|
font-weight: normal;
|
|
color: white !important;
|
|
text-align: center !important;
|
|
}
|
|
|
|
.metric-item-title {
|
|
font-size: 18px !important;
|
|
text-decoration: underline;
|
|
text-decoration-color: #ffd843;
|
|
}
|
|
|
|
.v-table {
|
|
thead,
|
|
tbody {
|
|
background-color: #006492;
|
|
}
|
|
|
|
:hover {
|
|
tbody > tr {
|
|
background-color: #005281 !important;
|
|
}
|
|
}
|
|
|
|
::-webkit-scrollbar {
|
|
width: 0;
|
|
height: 0.55em;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background-color: #ffd843;
|
|
border-radius: 10px;
|
|
}
|
|
}
|
|
</style>
|