first implementation of displaying graphed GPU data

This commit is contained in:
Meliox 2026-04-06 20:31:33 +02:00
parent e5ab4eb42a
commit ec0ec5fc4a
3 changed files with 764 additions and 9 deletions

View File

@ -6,7 +6,9 @@ use JSON;
use POSIX qw(WNOHANG);
use Time::HiRes qw(time);
use Fcntl qw(:flock O_CREAT O_EXCL O_WRONLY);
use File::Path qw(remove_tree);
use File::Path qw(remove_tree make_path);
use PVE::INotify;
use RRDs;
# debug configuration - set to 0 to disable all _debug output
my $DEBUG_ENABLED = 1;
@ -508,6 +510,7 @@ sub _collector_for_intel_device {
# Write to device-specific file
_safe_write_json($device_state_file, $device_data);
_update_intel_gpu_rrd($device->{card}, $stats);
}
}
}
@ -517,14 +520,145 @@ sub _collector_for_intel_device {
exit 0;
}
# ============================================================================
# RRD Support for GPU metrics
# ============================================================================
my $RRD_SOCKET = '/var/run/rrdcached.sock';
my $RRD_BASE = '/var/lib/rrdcached/db/pve-mod-gpu';
sub _get_nodename {
return PVE::INotify::nodename();
}
sub _gpu_rrd_path {
my ($card) = @_;
return "$RRD_BASE/" . _get_nodename() . "/$card";
}
sub _ensure_intel_gpu_rrd {
my ($card) = @_;
my $path = _gpu_rrd_path($card);
return if -f $path;
my $dir = "$RRD_BASE/" . _get_nodename();
make_path($dir, { mode => 0755 }) unless -d $dir;
RRDs::create(
$path,
'--step', '1',
'DS:freq_req:GAUGE:120:0:U',
'DS:freq_act:GAUGE:120:0:U',
'DS:rc6:GAUGE:120:0:100',
'DS:power_gpu:GAUGE:120:0:U',
'DS:power_pkg:GAUGE:120:0:U',
'DS:render_busy:GAUGE:120:0:100',
'DS:blitter_busy:GAUGE:120:0:100',
'DS:video_busy:GAUGE:120:0:100',
'DS:videnh_busy:GAUGE:120:0:100',
'RRA:AVERAGE:0.5:1:1440',
'RRA:AVERAGE:0.5:60:1440',
'RRA:AVERAGE:0.5:1800:1344',
'RRA:AVERAGE:0.5:21600:1464',
'RRA:AVERAGE:0.5:604800:520',
'RRA:MAX:0.5:1:1440',
'RRA:MAX:0.5:60:1440',
'RRA:MAX:0.5:1800:1344',
'RRA:MAX:0.5:21600:1464',
'RRA:MAX:0.5:604800:520',
);
my $err = RRDs::error();
_debug(__LINE__, "Created Intel GPU RRD $path: " . ($err // 'OK'));
}
sub _update_intel_gpu_rrd {
my ($card, $stats) = @_;
_ensure_intel_gpu_rrd($card);
my $path = _gpu_rrd_path($card);
my $freq_req = $stats->{frequency}{requested} // 'U';
my $freq_act = $stats->{frequency}{actual} // 'U';
my $rc6 = $stats->{rc6}{value} // 'U';
my $power_gpu = $stats->{power}{GPU} // 'U';
my $power_pkg = $stats->{power}{Package} // 'U';
my $render_busy = $stats->{engines}{'Render/3D'}{busy} // 'U';
my $blitter = $stats->{engines}{Blitter}{busy} // 'U';
my $video = $stats->{engines}{Video}{busy} // 'U';
my $videnh = $stats->{engines}{VideoEnhance}{busy} // 'U';
my @daemon_args = (-S $RRD_SOCKET) ? ('--daemon', "unix:$RRD_SOCKET") : ();
RRDs::update(
$path,
@daemon_args,
"N:$freq_req:$freq_act:$rc6:$power_gpu:$power_pkg:$render_busy:$blitter:$video:$videnh",
);
my $err = RRDs::error();
_debug(__LINE__, "RRD update intel $card: $err") if $err;
}
sub _ensure_nvidia_gpu_rrd {
my ($index) = @_;
my $card = "nvidia$index";
my $path = _gpu_rrd_path($card);
return if -f $path;
my $dir = "$RRD_BASE/" . _get_nodename();
make_path($dir, { mode => 0755 }) unless -d $dir;
RRDs::create(
$path,
'--step', '1',
'DS:gpu_util:GAUGE:120:0:100',
'DS:mem_util:GAUGE:120:0:100',
'DS:mem_used:GAUGE:120:0:U',
'DS:mem_total:GAUGE:120:0:U',
'DS:power_draw:GAUGE:120:0:U',
'DS:power_limit:GAUGE:120:0:U',
'DS:temp_gpu:GAUGE:120:0:U',
'DS:fan_speed:GAUGE:120:0:100',
'RRA:AVERAGE:0.5:1:1440',
'RRA:AVERAGE:0.5:60:1440',
'RRA:AVERAGE:0.5:1800:1344',
'RRA:AVERAGE:0.5:21600:1464',
'RRA:AVERAGE:0.5:604800:520',
'RRA:MAX:0.5:1:1440',
'RRA:MAX:0.5:60:1440',
'RRA:MAX:0.5:1800:1344',
'RRA:MAX:0.5:21600:1464',
'RRA:MAX:0.5:604800:520',
);
my $err = RRDs::error();
_debug(__LINE__, "Created NVIDIA GPU RRD $path: " . ($err // 'OK'));
}
sub _update_nvidia_gpu_rrd {
my ($index, $stats) = @_;
_ensure_nvidia_gpu_rrd($index);
my $card = "nvidia$index";
my $path = _gpu_rrd_path($card);
my $gpu_util = $stats->{utilization}{gpu} // 'U';
my $mem_util = $stats->{utilization}{memory} // 'U';
my $mem_used = $stats->{memory}{used} // 'U';
my $mem_total = $stats->{memory}{total} // 'U';
my $power_draw = $stats->{power}{draw} // 'U';
my $power_limit = $stats->{power}{limit} // 'U';
my $temp_gpu = $stats->{temperature}{gpu} // 'U';
my $fan_speed = $stats->{fan}{speed} // 'U';
my @daemon_args = (-S $RRD_SOCKET) ? ('--daemon', "unix:$RRD_SOCKET") : ();
RRDs::update(
$path,
@daemon_args,
"N:$gpu_util:$mem_util:$mem_used:$mem_total:$power_draw:$power_limit:$temp_gpu:$fan_speed",
);
my $err = RRDs::error();
_debug(__LINE__, "RRD update nvidia$index: $err") if $err;
}
# Parse information for graphical presentation.
sub _parse_graphic_info {
my ($line) = @_;
# Create a RRD Database (One-Time Setup)
# Collect intel GPU data and save it into the database
return undef;
}
@ -752,6 +886,7 @@ sub _get_and_write_nvidia_stats {
# Write to device-specific file
_safe_write_json($device_state_file, $device_data);
_update_nvidia_gpu_rrd($device_index, $stats);
}
unless (@all_stats) {

View File

@ -624,7 +624,7 @@ Ext.define('PVE.node.StatusView', {
nvmeData.forEach((data) => {
let deviceName = data.model;
if (data.serial) {
deviceName += ` (${data.serial})`;
deviceName += ` (${data.serial})`;
}
html += '<tr>';
html += `<td style="padding: 2px 10px 2px 0; text-align: left; width: 30%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word;">${deviceName}</td>`;
@ -1032,3 +1032,503 @@ Ext.define('PVE.node.StatusView', {
me.callParent();
},
});
Ext.define('pve-rrd-gpu', {
extend: 'Ext.data.Model',
fields: [
'freq_req', 'freq_act', 'rc6',
'power_gpu', 'power_pkg',
'render_busy', 'blitter_busy', 'video_busy', 'videnh_busy',
'gpu_util', 'mem_util', 'mem_used', 'mem_total',
'power_draw', 'power_limit', 'temp_gpu', 'fan_speed',
{ type: 'date', dateFormat: 'timestamp', name: 'time' },
],
});
Ext.define('PVE.data.GpuRRDStore', {
extend: 'Proxmox.data.RRDStore',
alias: 'store.pveGpuRRDStore',
model: 'pve-rrd-gpu',
card: undefined,
setRRDUrl: function(timeframe, cf) {
var me = this;
if (!me.rrdurl) { return; }
if (!timeframe) { timeframe = me.timeframe; }
if (!cf) { cf = me.cf; }
me.proxy.url = me.rrdurl +
'?card=' + encodeURIComponent(me.card) +
'&timeframe=' + timeframe +
'&cf=' + cf;
},
});
Ext.define('PVE.node.GpuRRD', {
extend: 'Ext.panel.Panel',
alias: 'widget.pveNodeGpuRRD',
layout: 'fit',
title: 'GPU',
initComponent: function() {
var me = this;
var nodename = me.nodename;
var card = me.card || 'card0';
var baseurl = '/api2/json/nodes/' + nodename + '/gpurrddata';
var isNvidia = card.indexOf('nvidia') === 0;
var store = Ext.create('PVE.data.GpuRRDStore', {
rrdurl: baseurl,
card: card,
});
var items;
if (isNvidia) {
items = [
{
xtype: 'proxmoxRRDChart',
title: 'GPU & Memory Utilization',
fields: ['gpu_util', 'mem_util'],
fieldTitles: ['GPU %', 'Memory %'],
unit: 'percent',
store: store,
},
{
xtype: 'proxmoxRRDChart',
title: 'Memory Usage (MiB)',
fields: ['mem_used', 'mem_total'],
fieldTitles: ['Used', 'Total'],
store: store,
},
{
xtype: 'proxmoxRRDChart',
title: 'Power Draw (W)',
fields: ['power_draw', 'power_limit'],
fieldTitles: ['Draw', 'Limit'],
store: store,
},
{
xtype: 'proxmoxRRDChart',
title: 'Temperature & Fan',
fields: ['temp_gpu', 'fan_speed'],
fieldTitles: ['Temp (°C)', 'Fan %'],
store: store,
},
];
} else {
items = [
{
xtype: 'proxmoxRRDChart',
title: 'GPU Frequency (MHz)',
fields: ['freq_req', 'freq_act'],
fieldTitles: ['Requested', 'Actual'],
store: store,
},
{
xtype: 'proxmoxRRDChart',
title: 'Engine Busy',
fields: ['render_busy', 'blitter_busy', 'video_busy', 'videnh_busy'],
fieldTitles: ['Render/3D %', 'Blitter %', 'Video %', 'VideoEnh %'],
unit: 'percent',
store: store,
},
{
xtype: 'proxmoxRRDChart',
title: 'Power (W)',
fields: ['power_gpu', 'power_pkg'],
fieldTitles: ['GPU', 'Package'],
store: store,
},
{
xtype: 'proxmoxRRDChart',
title: 'RC6 Residency',
fields: ['rc6'],
fieldTitles: ['RC6 %'],
unit: 'percent',
store: store,
},
];
}
Ext.apply(me, {
items: [{
xtype: 'container',
layout: {
type: 'vbox',
align: 'stretch',
},
items: items,
}],
});
me.callParent();
me.on('activate', function() { store.startUpdate(); });
me.on('deactivate', function() { store.stopUpdate(); });
me.on('destroy', function() { store.stopUpdate(); });
},
});
Ext.define('PVE.node.Summary', {
extend: 'Ext.panel.Panel',
alias: 'widget.pveNodeSummary',
scrollable: true,
bodyPadding: 5,
showVersions: function () {
var me = this;
var nodename = me.pveSelNode.data.node;
var view = Ext.createWidget('component', {
autoScroll: true,
id: 'pkgversions',
padding: 5,
style: {
'white-space': 'pre',
'font-family': 'monospace',
},
});
var win = Ext.create('Ext.window.Window', {
title: gettext('Package versions'),
width: 600,
height: 600,
layout: 'fit',
modal: true,
items: [view],
buttons: [
{
xtype: 'button',
iconCls: 'fa fa-clipboard',
handler: function (button) {
window
.getSelection()
.selectAllChildren(document.getElementById('pkgversions'));
document.execCommand('copy');
},
text: gettext('Copy'),
},
{
text: gettext('Ok'),
handler: function () {
this.up('window').close();
},
},
],
});
Proxmox.Utils.API2Request({
waitMsgTarget: me,
url: `/nodes/${nodename}/apt/versions`,
method: 'GET',
failure: function (response, opts) {
win.close();
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function (response, opts) {
win.show();
let text = '';
Ext.Array.each(response.result.data, function (rec) {
let version = 'not correctly installed';
let pkg = rec.Package;
if (rec.OldVersion && rec.CurrentState === 'Installed') {
version = rec.OldVersion;
}
if (rec.RunningKernel) {
text += `${pkg}: ${version} (running kernel: ${rec.RunningKernel})\n`;
} else if (rec.ManagerVersion) {
text += `${pkg}: ${version} (running version: ${rec.ManagerVersion})\n`;
} else {
text += `${pkg}: ${version}\n`;
}
});
view.update(Ext.htmlEncode(text));
},
});
},
updateRepositoryStatus: function () {
let me = this;
let repoStatus = me.nodeStatus.down('#repositoryStatus');
let nodename = me.pveSelNode.data.node;
Proxmox.Utils.API2Request({
url: `/nodes/${nodename}/apt/repositories`,
method: 'GET',
failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
success: (response) =>
repoStatus.setRepositoryInfo(response.result.data['standard-repos']),
});
Proxmox.Utils.API2Request({
url: `/nodes/${nodename}/subscription`,
method: 'GET',
failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
success: function (response, opts) {
const res = response.result;
const subscription = res?.data?.status.toLowerCase() === 'active';
repoStatus.setSubscriptionStatus(subscription);
},
});
},
initComponent: function () {
var me = this;
var nodename = me.pveSelNode.data.node;
if (!nodename) {
throw 'no node name specified';
}
if (!me.statusStore) {
throw 'no status storage specified';
}
var rstore = me.statusStore;
var version_btn = new Ext.Button({
text: gettext('Package versions'),
handler: function () {
Proxmox.Utils.checked_command(function () {
me.showVersions();
});
},
});
var rrdstore = Ext.create('Proxmox.data.RRDStore', {
rrdurl: '/api2/json/nodes/' + nodename + '/rrddata',
model: 'pve-rrd-node',
});
var gpurrdstore = Ext.create('PVE.data.GpuRRDStore', {
rrdurl: '/api2/json/nodes/' + nodename + '/gpurrddata',
card: 'card0',
});
let nodeStatus = Ext.create('PVE.node.StatusView', {
xtype: 'pveNodeStatus',
rstore: rstore,
width: 770,
pveSelNode: me.pveSelNode,
});
Ext.apply(me, {
tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' }],
nodeStatus: nodeStatus,
items: [
{
xtype: 'container',
itemId: 'itemcontainer',
layout: 'column',
minWidth: 700,
defaults: {
minHeight: 360,
padding: 5,
columnWidth: 1,
},
items: [
nodeStatus,
{
xtype: 'proxmoxRRDChart',
title: gettext('CPU Usage'),
fields: ['cpu', 'iowait'],
fieldTitles: [gettext('CPU usage'), gettext('IO delay')],
unit: 'percent',
store: rrdstore,
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Server Load'),
fields: ['loadavg'],
fieldTitles: [gettext('Load average')],
store: rrdstore,
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Memory usage'),
fields: [
{
yField: 'memtotal',
title: gettext('Total'),
tooltip: {
trackMouse: true,
renderer: function (toolTip, record, item) {
let value = record.get('memtotal');
if (value === null) {
toolTip.setHtml(gettext('No Data'));
} else {
let total = Proxmox.Utils.format_size(value);
let time = new Date(record.get('time'));
let avail = record.get('memavailable');
let availText = '';
if (Ext.isNumeric(avail)) {
let v = Proxmox.Utils.format_size(avail);
availText = ` (${gettext('Available')}: ${v})`;
}
toolTip.setHtml(
`${gettext('Total')}: ${total}${availText}<br>${time}`,
);
}
},
},
},
{
yField: 'memused',
title: gettext('Used'),
tooltip: {
trackMouse: true,
renderer: function (toolTip, record, item) {
let value = record.get('memused');
if (value === null) {
toolTip.setHtml(gettext('No Data'));
} else {
let total = Proxmox.Utils.format_size(value);
let time = new Date(record.get('time'));
let arc = record.get('arcsize');
let arcText = '';
if (Ext.isNumeric(arc) && arc > 1024 * 1024) {
let v = Proxmox.Utils.format_size(value - arc);
arcText = ` (${gettext('Without ZFS ARC')}: ${v})`;
}
toolTip.setHtml(
`${gettext('Used')}: ${total}${arcText}<br>${time}`,
);
}
},
},
},
'arcsize',
{
type: 'line',
fill: false,
yField: 'memavailable',
title: gettext('Available'),
style: {
lineWidth: 2.5,
opacity: 1,
},
},
],
fieldTitles: [
gettext('Total'),
gettext('Used'),
gettext('ZFS ARC'),
gettext('Available'),
],
colors: ['#94ae0a', '#115fa6', '#24AD9A', '#bbde0d'],
unit: 'bytes',
powerOfTwo: true,
store: rrdstore,
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Network Traffic'),
fields: ['netin', 'netout'],
fieldTitles: [gettext('Incoming'), gettext('Outgoing')],
store: rrdstore,
},
{
xtype: 'proxmoxRRDChart',
title: gettext('CPU Pressure Stall'),
fieldTitles: ['Some'],
fields: ['pressurecpusome'],
colors: ['#FFD13E', '#A61120'],
store: rrdstore,
unit: 'percent',
},
{
xtype: 'proxmoxRRDChart',
title: gettext('IO Pressure Stall'),
fieldTitles: ['Some', 'Full'],
fields: ['pressureiosome', 'pressureiofull'],
colors: ['#FFD13E', '#A61120'],
store: rrdstore,
unit: 'percent',
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Memory Pressure Stall'),
fieldTitles: ['Some', 'Full'],
fields: ['pressurememorysome', 'pressurememoryfull'],
colors: ['#FFD13E', '#A61120'],
store: rrdstore,
unit: 'percent',
},
{
xtype: 'proxmoxRRDChart',
title: gettext('GPU Frequency (MHz)'),
fields: ['freq_req', 'freq_act'],
fieldTitles: [gettext('Requested'), gettext('Actual')],
store: gpurrdstore,
},
{
xtype: 'proxmoxRRDChart',
title: gettext('GPU Engine Busy'),
fields: ['render_busy', 'blitter_busy', 'video_busy', 'videnh_busy'],
fieldTitles: [gettext('Render/3D'), gettext('Blitter'), gettext('Video'), gettext('VideoEnh')],
unit: 'percent',
store: gpurrdstore,
},
{
xtype: 'proxmoxRRDChart',
title: gettext('GPU Power (W)'),
fields: ['power_gpu', 'power_pkg'],
fieldTitles: [gettext('GPU'), gettext('Package')],
store: gpurrdstore,
},
{
xtype: 'proxmoxRRDChart',
title: gettext('GPU RC6 Residency'),
fields: ['rc6'],
fieldTitles: [gettext('RC6 %')],
unit: 'percent',
store: gpurrdstore,
},
],
listeners: {
resize: function (panel) {
Proxmox.Utils.updateColumns(panel);
},
},
},
],
listeners: {
activate: function () {
rstore.setInterval(1000);
rstore.startUpdate();
rrdstore.startUpdate();
gpurrdstore.startUpdate();
},
destroy: function () {
rstore.setInterval(5000);
rrdstore.stopUpdate();
gpurrdstore.stopUpdate();
},
},
});
me.updateRepositoryStatus();
me.callParent();
let sp = Ext.state.Manager.getProvider();
me.mon(sp, 'statechange', function (provider, key, value) {
if (key !== 'summarycolumns') {
return;
}
Proxmox.Utils.updateColumns(me.getComponent('itemcontainer'));
});
},
});

View File

@ -35,6 +35,7 @@ PVE_MOD_JS_TARGET_FILE="/usr/share/pve-manager/js/PveMod_PveNodeStatusView.js"
NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm"
PVE_SENSOR_INFO_MOD_FILE="/usr/share/perl5/PVE/API2/PveMod_SensorInfo.pm"
PVE_SENSOR_INFO_SOURCE_FILE="$SCRIPT_CWD/PveMod_SensorInfo.pm"
GPU_RRD_DIR="/var/lib/rrdcached/db/pve-mod-gpu"
#region message tools
# Section header (bold)
@ -281,6 +282,28 @@ function configure {
#endregion Graphics setup
#### GPU Historical Data ####
#region gpu history setup
ENABLE_GPU_HISTORY=false
if [[ "$ENABLE_GPU_INFO" == true ]]; then
msgb "\n=== GPU Historical Data ==="
local choiceGpuHistory
choiceGpuHistory=$(ask "Store historical GPU data for graphs? (y/N)")
case "$choiceGpuHistory" in
[yY])
ENABLE_GPU_HISTORY=true
info "Historical GPU data will be stored."
;;
[nN]|"")
info "Historical GPU data will not be stored."
;;
*)
warn "Invalid selection. Historical GPU data will not be stored."
;;
esac
fi
#endregion gpu history setup
#### RAM ####
#region ram setup
local ramList ramCount
@ -477,6 +500,11 @@ function install_mod {
insert_sensor_monitor_into_pve
insert_system_info_into_pve
## Historical GPU data ##
if [[ "$ENABLE_GPU_HISTORY" == true ]]; then
install_gpu_history
fi
#### Install UI modification module ####
msgb "\n=== Installing UI modification module ==="
install_node_status_view_module
@ -624,6 +652,14 @@ install_node_status_view_module() {
warn "Original StatusView definition not found in expected format in pvemanagerlib.js"
fi
# Comment out the original PVE.node.Summary definition in pvemanagerlib.js
if grep -q "^Ext.define('PVE.node.Summary'," "$PVE_MANAGER_LIB_JS_FILE" 2>/dev/null; then
sed -i "/^Ext\.define('PVE\.node\.Summary',/,/^});/s|^|// |" "$PVE_MANAGER_LIB_JS_FILE"
info "Commented out original Summary definition in pvemanagerlib.js"
else
warn "Original Summary definition not found in expected format in pvemanagerlib.js"
fi
# Add a dynamic script loader to load our custom module
# Insert before the commented-out Ext.define to load it
if ! grep -q "PveMod_PveNodeStatusView.js" "$PVE_MANAGER_LIB_JS_FILE" 2>/dev/null; then
@ -643,6 +679,83 @@ Ext.Loader.loadScript({\\
}
#endregion UI Module Installation
#region historical GPU data
# Install GPU historical data storage: RRD directory + API endpoint in Nodes.pm
install_gpu_history() {
msgb "\n=== Installing GPU historical data support ==="
# Create and configure the RRD directory
mkdir -p "$GPU_RRD_DIR" || err "Failed to create GPU RRD directory: $GPU_RRD_DIR"
chown www-data:www-data "$GPU_RRD_DIR" || err "Failed to set ownership on: $GPU_RRD_DIR"
info "GPU RRD directory ready: $GPU_RRD_DIR"
# Register gpurrddata in the node method list
sed -i "s/{ name => 'rrddata' },/{ name => 'rrddata' },\n { name => 'gpurrddata' },/" "$NODES_PM_FILE" \
|| err "Failed to register gpurrddata method in $NODES_PM_FILE"
# Insert the gpurrddata API method definition after the rrddata method
sed -i "/\"pve-node-9\.0\/\$param->{node}\", \$param->{timeframe}, \$param->{cf},/{
n
n
a\\
\\
__PACKAGE__->register_method({\\
name => 'gpurrddata',\\
path => 'gpurrddata',\\
method => 'GET',\\
protected => 1,\\
proxyto => 'node',\\
permissions => {\\
check => ['perm', '/nodes/{node}', ['Sys.Audit']],\\
},\\
description => \"Read GPU RRD statistics\",\\
parameters => {\\
additionalProperties => 0,\\
properties => {\\
node => get_standard_option('pve-node'),\\
card => {\\
description => \"The GPU card identifier (e.g. card0, nvidia0).\",\\
type => 'string',\\
pattern => '[a-zA-Z0-9]+',\\
},\\
timeframe => {\\
description => \"Specify the time frame you are interested in.\",\\
type => 'string',\\
enum => ['hour', 'day', 'week', 'month', 'year', 'decade'],\\
},\\
cf => {\\
description => \"The RRD consolidation function\",\\
type => 'string',\\
enum => ['AVERAGE', 'MAX'],\\
optional => 1,\\
},\\
},\\
},\\
returns => {\\
type => \"array\",\\
items => {\\
type => \"object\",\\
properties => {},\\
},\\
},\\
code => sub {\\
my (\$param) = \@_;\\
my \$nodename = PVE::INotify::nodename();\\
my \$card = \$param->{card};\\
die \"invalid card name\\n\" unless \$card =~ /^[a-zA-Z0-9]+\$/;\\
return PVE::RRD::create_rrd_data(\\
\"pve-mod-gpu/\$nodename/\$card\", \$param->{timeframe}, \$param->{cf},\\
);\\
},\\
});
}" "$NODES_PM_FILE" \
|| err "Failed to insert gpurrddata method in $NODES_PM_FILE"
info "GPU historical data API endpoint added to $NODES_PM_FILE"
}
#endregion historical GPU data
# Function to uninstall the modification
function uninstall_mod {
msgb "=== Uninstalling Mod ==="
@ -706,6 +819,13 @@ function uninstall_mod {
warn "No PveMod_SensorInfo.pm backup files found and module not installed."
fi
# Remove GPU RRD directory (contains historical GPU data — destroyed permanently on uninstall)
if [[ -d "$GPU_RRD_DIR" ]]; then
msgb "Removing GPU RRD directory: $GPU_RRD_DIR"
rm -rf "$GPU_RRD_DIR"
info "Removed GPU RRD directory."
fi
if [ -n "$latest_nodes_pm" ] || [ -n "$latest_pvemanagerlibjs" ] || [ -f "$PVE_MOD_JS_TARGET_FILE" ] || [ -n "$latest_sensor_info_pm" ] || [ -f "$PVE_SENSOR_INFO_MOD_FILE" ]; then
# At least one file was modified, restart the proxy
restart_proxy