first implementation of displaying graphed GPU data
This commit is contained in:
parent
e5ab4eb42a
commit
ec0ec5fc4a
@ -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) {
|
||||
|
||||
@ -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'));
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user