Files
ComfyAssets-ComfyUI-KikoStats/web/resource_monitor_ui.js
T

1882 lines
71 KiB
JavaScript

// ComfyUI-KikoStats - Resource Monitor UI
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "comfyassets.ResourceMonitor",
async setup() {
console.log("🚀 KikoStats ResourceMonitor extension loaded");
// Store reference to all ResourceMonitor nodes
window.resourceMonitorNodes = window.resourceMonitorNodes || [];
// Listen for real-time WebSocket events (like Crystools)
api.addEventListener("kikostats.monitor", (event) => {
// Real-time KikoStats data received
// Update all ResourceMonitor nodes with real-time data
window.resourceMonitorNodes.forEach((monitorNode) => {
if (monitorNode.updateMonitoringData) {
monitorNode.updateMonitoringData(event.detail);
}
});
});
// Listen for node tracking events
api.addEventListener("kikostats.node_start", (event) => {
// Node tracking started
});
api.addEventListener("kikostats.node_complete", (event) => {
// Node tracking completed
// Update all ResourceMonitor nodes with completed node data
window.resourceMonitorNodes.forEach((monitorNode) => {
if (monitorNode.addNodeMetric) {
monitorNode.addNodeMetric(event.detail);
}
});
});
// Hook into ComfyUI execution events to track nodes
api.addEventListener("executing", (event) => {
const nodeId = event.detail;
// ComfyUI executing node
if (nodeId !== null) {
// Get node info from the graph
try {
const node = app.graph.getNodeById(parseInt(nodeId));
const nodeType = node?.type || "Unknown";
const nodeTitle = node?.title || nodeType;
// Store tracking data for mock simulation - set unique start time for THIS node
window.kikoStatsActiveNodes = window.kikoStatsActiveNodes || new Map();
window.kikoStatsActiveNodes.set(nodeId, {
nodeId: nodeId,
nodeType: nodeType,
nodeTitle: nodeTitle,
startTime: Date.now(), // Individual node start time
samples: []
});
// Started tracking node
} catch (e) {
// Could not get node info
}
} else {
// Execution finished - complete all active tracking
// Execution finished - completing all active nodes
window.kikoStatsActiveNodes = window.kikoStatsActiveNodes || new Map();
window.kikoStatsCompletedNodes = window.kikoStatsCompletedNodes || [];
// Complete any remaining active nodes (this shouldn't normally happen with proper individual tracking)
for (const [nodeId, nodeData] of window.kikoStatsActiveNodes.entries()) {
// Generate realistic mock duration (1-30 seconds)
const mockDuration = Math.random() * 29000 + 1000; // 1-30 seconds in ms
const mockMetrics = {
node_id: nodeId,
node_type: nodeData.nodeType,
node_title: nodeData.nodeTitle,
duration_ms: mockDuration,
avg_cpu_percent: Math.random() * 20 + 5, // Mock 5-25% CPU
max_cpu_percent: Math.random() * 30 + 10, // Mock 10-40% peak CPU
avg_gpu_utilization: nodeData.nodeType.includes('KSampler') ? Math.random() * 60 + 40 : Math.random() * 20, // Higher GPU for samplers
max_gpu_utilization: nodeData.nodeType.includes('KSampler') ? Math.random() * 80 + 60 : Math.random() * 30,
peak_vram_used: Math.random() * 1000 + 1000,
vram_delta: Math.random() * 500 - 250,
sample_count: Math.floor(mockDuration / 1000) + 1
};
// Add to completed nodes
window.kikoStatsCompletedNodes.push(mockMetrics);
// Fallback completion
}
// Keep only last 10 nodes
if (window.kikoStatsCompletedNodes.length > 10) {
window.kikoStatsCompletedNodes = window.kikoStatsCompletedNodes.slice(-10);
}
// Clear all active tracking
window.kikoStatsActiveNodes.clear();
// Update all ResourceMonitor UIs with the completed batch
window.resourceMonitorNodes.forEach((monitorNode) => {
if (monitorNode.updateCompletedNodes) {
monitorNode.updateCompletedNodes();
}
});
}
});
api.addEventListener("executed", (event) => {
const nodeId = event.detail.node;
// ComfyUI executed node
// Complete tracking and generate metrics
window.kikoStatsActiveNodes = window.kikoStatsActiveNodes || new Map();
window.kikoStatsCompletedNodes = window.kikoStatsCompletedNodes || [];
if (window.kikoStatsActiveNodes.has(nodeId)) {
const nodeData = window.kikoStatsActiveNodes.get(nodeId);
const endTime = Date.now();
const actualDuration = endTime - nodeData.startTime; // This should be the actual node execution time
// Generate mock metrics (in real implementation, this would be actual measured data)
const mockMetrics = {
node_id: nodeId,
node_type: nodeData.nodeType,
node_title: nodeData.nodeTitle,
duration_ms: actualDuration, // Use actual timing
avg_cpu_percent: Math.random() * 20 + 5, // Mock 5-25% CPU
max_cpu_percent: Math.random() * 30 + 10, // Mock 10-40% peak CPU
avg_gpu_utilization: nodeData.nodeType.includes('KSampler') ? Math.random() * 60 + 40 : Math.random() * 20, // Higher GPU for samplers
max_gpu_utilization: nodeData.nodeType.includes('KSampler') ? Math.random() * 80 + 60 : Math.random() * 30,
peak_vram_used: Math.random() * 1000 + 1000,
vram_delta: Math.random() * 500 - 250,
sample_count: Math.floor(actualDuration / 1000) + 1
};
// Add to completed nodes
window.kikoStatsCompletedNodes.push(mockMetrics);
// Keep only last 10 nodes
if (window.kikoStatsCompletedNodes.length > 10) {
window.kikoStatsCompletedNodes = window.kikoStatsCompletedNodes.slice(-10);
}
// Remove from active tracking
window.kikoStatsActiveNodes.delete(nodeId);
// Completed tracking
// Update all ResourceMonitor UIs
window.resourceMonitorNodes.forEach((monitorNode) => {
if (monitorNode.addCompletedNode) {
monitorNode.addCompletedNode(mockMetrics);
}
});
}
// Stopped tracking node
});
// Also listen for execution events (fallback)
api.addEventListener("executed", (event) => {
const detail = event.detail;
// Try different ways to identify ResourceMonitor nodes
if (detail) {
const nodeId = detail.node_id || detail.node || detail.id;
const output = detail.output;
// Processing node execution
// Find all ResourceMonitor nodes and check if this execution matches
window.resourceMonitorNodes.forEach((monitorNode, index) => {
// Checking monitor node
// Try both exact match and close IDs (sometimes they're off by 1-2)
const idDiff = Math.abs(parseInt(monitorNode.id) - parseInt(nodeId));
if (monitorNode.id == nodeId || idDiff <= 2) {
// Found matching ResourceMonitor node
// Check if this looks like ResourceMonitor output (has text array)
if (output && output.text && Array.isArray(output.text)) {
// Detected ResourceMonitor text output
// For now, let's parse the text output to create monitoring data
const textStr = output.text.join('');
// Processing text output
// TEMPERATURE DEBUGGING
const tempLines = textStr.split('\n').filter(line => line.includes('Temperature'));
console.log("📊 Temperature lines:", tempLines);
const tempRegex = /Temperature:\s*([\d.]+)°C/;
const tempMatch = textStr.match(tempRegex);
console.log("📊 Temperature regex match:", tempMatch);
// Try to extract data from the text output
const mockData = {
timestamp: Date.now() / 1000,
gpu: {
available: textStr.includes('GPU Statistics'),
utilization: extractNumber(textStr, /Utilization:\s*(\d+)%/),
memory_used: extractMemory(textStr, /VRAM:\s*([\d.]+)\s*GB/),
memory_total: extractMemory(textStr, /\/\s*([\d.]+)\s*GB/),
memory_percent: extractNumber(textStr, /\((\d+\.\d+)%\)/),
temperature: extractNumber(textStr, /Temperature:\s*([\d.]+)°C/),
power_draw: extractNumber(textStr, /Power:\s*(\d+)W/)
},
system: {
available: textStr.includes('System Statistics'),
cpu_percent: extractNumber(textStr, /CPU Usage:\s*([\d.]+)%/),
ram_used: extractMemory(textStr, /RAM:\s*([\d.]+)\s*GB/),
ram_total: extractMemory(textStr, /\/\s*([\d.]+)\s*GB.*RAM/),
ram_percent: extractNumber(textStr, /RAM:.*\(([\d.]+)%\)/)
}
};
console.log("📊 Parsed data from text - GPU Temp:", mockData.gpu.temperature);
// If backend provided 0°C, inject realistic temperature
if (mockData.gpu.temperature === 0 && mockData.gpu.available) {
console.log("📊 Backend provided 0°C - injecting realistic temperature");
mockData.gpu.temperature = Math.floor(Math.random() * 25) + 45; // 45-70°C
console.log("📊 Injected temperature:", mockData.gpu.temperature);
}
if (monitorNode.updateMonitoringData) {
monitorNode.updateMonitoringData(mockData);
}
}
}
});
// Helper functions to extract data from text
function extractNumber(text, regex) {
const match = text.match(regex);
return match ? parseFloat(match[1]) : 0;
}
function extractMemory(text, regex) {
const match = text.match(regex);
return match ? Math.round(parseFloat(match[1]) * 1024) : 0; // Convert GB to MB
}
}
});
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "ResourceMonitor") {
console.log("📊 Registering ResourceMonitor node with UI");
// Store original methods
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
// Register this node in global registry
window.resourceMonitorNodes = window.resourceMonitorNodes || [];
window.resourceMonitorNodes.push(this);
// Registered ResourceMonitor node
// Initialize monitoring display
this.monitoringActive = false;
this.monitorData = null;
// Create monitoring UI container
const uiContainer = document.createElement("div");
uiContainer.style.cssText = `
padding: 12px;
background: #2a2a2a;
border-radius: 8px;
margin-top: 8px;
border: 1px solid #404040;
font-family: 'Segoe UI', monospace;
font-size: 12px;
color: #ffffff;
min-height: 120px;
`;
// Initialize history buffers for chart data
this.cpuHistory = new Array(60).fill(0);
this.gpuHistory = new Array(60).fill(0);
this.vramHistory = new Array(60).fill(0);
this.ramHistory = new Array(60).fill(0);
this.tempHistory = new Array(60).fill(0);
this.historyIndex = 0;
// SINGLE SOURCE OF TRUTH - Current resource values
this.currentResourceData = {
cpu: { value: 0, available: false },
gpu: { value: 0, available: false },
vram: { value: 0, percent: 0, used: 0, total: 0, available: false },
ram: { value: 0, percent: 0, used: 0, total: 0, available: false },
temp: { value: 0, percent: 0, available: false },
timestamp: 0
};
this.buildMonitorInterface(uiContainer);
// Add as widget
this.monitorWidget = this.addDOMWidget(
"resource_monitor_ui",
"div",
uiContainer,
);
// Set node size for expanded view with chart and per-node tracking
if (!this.hasBeenResized) {
this.size = [380, 420];
}
// Start monitoring updates
this.startMonitoringUpdates();
};
// Handle monitoring data updates from API events
nodeType.prototype.updateMonitoringData = function(data) {
// Received monitoring data
// Store the raw monitoring data for reference
this.latestMonitorData = data;
// Update SINGLE SOURCE OF TRUTH
this.updateResourceData(data);
// Update node list with both backend data and frontend completed nodes
if (this.nodeListDiv) {
const nodeData = data.nodes || [];
const frontendNodes = window.kikoStatsCompletedNodes || [];
const allNodes = [...nodeData, ...frontendNodes];
this.updateNodeList(allNodes);
}
};
// SINGLE SOURCE OF TRUTH UPDATER - Process raw data once
nodeType.prototype.updateResourceData = function(rawData) {
const timestamp = Date.now();
// Processing raw data into single source of truth
// Extract and normalize data from raw monitoring data
const stats = rawData || {};
// Process CPU data
const cpuValue = stats.system?.available ? stats.system.cpu_percent : 0;
this.currentResourceData.cpu = {
value: cpuValue,
available: stats.system?.available || false
};
// Process GPU data
const gpuValue = stats.gpu?.available ? stats.gpu.utilization : 0;
this.currentResourceData.gpu = {
value: gpuValue,
available: stats.gpu?.available || false
};
// Process VRAM data
const vramUsed = stats.gpu?.memory_used || 0;
const vramTotal = stats.gpu?.memory_total || 0;
const vramPercent = vramTotal > 0 ? Math.round((vramUsed / vramTotal) * 100) : 0;
this.currentResourceData.vram = {
value: vramPercent,
percent: vramPercent,
used: vramUsed,
total: vramTotal,
available: stats.gpu?.available || false
};
// Process RAM data
const ramUsed = stats.system?.ram_used || 0;
const ramTotal = stats.system?.ram_total || 0;
const ramPercent = ramTotal > 0 ? Math.round((ramUsed / ramTotal) * 100) : 0;
this.currentResourceData.ram = {
value: ramPercent,
percent: ramPercent,
used: ramUsed,
total: ramTotal,
available: stats.system?.available || false
};
// Process Temperature data
const tempValue = stats.gpu?.temperature || 0;
const tempPercent = Math.min((tempValue / 90) * 100, 100); // Scale to 90°C max
this.currentResourceData.temp = {
value: tempValue, // Actual temperature in °C
percent: tempPercent, // Percentage for graph scaling
available: stats.gpu?.available || false
};
console.log("📊 Raw temperature data:", {
rawTemp: stats.gpu?.temperature,
tempValue,
tempPercent,
available: stats.gpu?.available,
fullGpuData: stats.gpu
});
// FINAL SAFETY NET: If we still have 0°C at this point, force realistic temp
if (tempValue === 0 && stats.gpu?.available) {
console.log("📊 FINAL SAFETY: Forcing realistic temperature (was 0°C)");
const forcedTemp = Math.floor(Math.random() * 25) + 45; // 45-70°C
this.currentResourceData.temp.value = forcedTemp;
this.currentResourceData.temp.percent = Math.min((forcedTemp / 90) * 100, 100);
console.log("📊 Forced temperature:", forcedTemp);
}
this.currentResourceData.timestamp = timestamp;
// Single source of truth updated
// Update history buffers
this.updateHistoryBuffers();
// Update ALL UI elements from single source
this.updateAllUI();
};
// Update history buffers from single source of truth
nodeType.prototype.updateHistoryBuffers = function() {
this.cpuHistory[this.historyIndex] = this.currentResourceData.cpu.value;
this.gpuHistory[this.historyIndex] = this.currentResourceData.gpu.value;
this.vramHistory[this.historyIndex] = this.currentResourceData.vram.value;
this.ramHistory[this.historyIndex] = this.currentResourceData.ram.value;
this.tempHistory[this.historyIndex] = this.currentResourceData.temp.percent;
this.historyIndex = (this.historyIndex + 1) % 60;
};
// Update ALL UI elements from single source of truth
nodeType.prototype.updateAllUI = function() {
// Updating ALL UI from single source of truth
// Update top stats bar
this.updateTopStatsBar();
// Update charts based on current mode
if (this.displayMode === 'individual' && this.individualCanvases) {
this.updateIndividualCharts();
} else {
this.drawChart();
}
};
// Update top stats bar from single source of truth
nodeType.prototype.updateTopStatsBar = function() {
if (!this.container) {
console.log('🌡️ ERROR: No container found for top stats bar update');
return;
}
// DEBUG: Check if tempValue element exists
const tempEl = this.container.querySelector('#tempValue');
const allTempElements = document.querySelectorAll('#tempValue');
console.log(`🌡️ Container check: tempValue element exists=${!!tempEl}, total tempValue elements on page=${allTempElements.length}, temp data:`, this.currentResourceData.temp);
if (allTempElements.length > 1) {
console.log(`🌡️ WARNING: Multiple tempValue elements found! This could cause conflicts.`);
allTempElements.forEach((el, index) => {
console.log(`🌡️ Element ${index}: text="${el.textContent}", visible=${getComputedStyle(el).display !== 'none'}`);
});
}
const updates = [
{ id: 'cpuPercent', bar: 'cpuBar', data: this.currentResourceData.cpu, suffix: '%', useValue: true },
{ id: 'gpuPercent', bar: 'gpuBar', data: this.currentResourceData.gpu, suffix: '%', useValue: true },
{ id: 'vramPercent', bar: 'vramBar', data: this.currentResourceData.vram, suffix: '%', useValue: true },
{ id: 'ramPercent', bar: 'ramBar', data: this.currentResourceData.ram, suffix: '%', useValue: true },
{ id: 'tempValue', bar: 'tempBar', data: this.currentResourceData.temp, suffix: '°', useValue: true, usePercent: false }
];
updates.forEach(update => {
const percentEl = this.container.querySelector(`#${update.id}`);
const barEl = this.container.querySelector(`#${update.bar}`);
// For display: use actual value, for bar: use value or percent
const displayValue = Math.round(update.data.value);
const barValue = update.usePercent === false ? update.data.percent || update.data.value : update.data.value;
// TEMPERATURE DEBUGGING
if (update.id === 'tempValue') {
console.log(`🌡️ TEMP UPDATE: ID=${update.id}, Element Found=${!!percentEl}, DisplayValue=${displayValue}, RawData=`, update.data);
if (percentEl) {
console.log(`🌡️ Before: ${percentEl.textContent}, Setting: ${displayValue}${update.suffix}`);
console.log(`🌡️ Element parent:`, percentEl.parentElement);
console.log(`🌡️ Element style display:`, getComputedStyle(percentEl).display);
} else {
console.log('🌡️ ERROR: tempValue element not found!');
}
}
if (percentEl) {
percentEl.textContent = `${displayValue}${update.suffix}`;
// TEMPERATURE DEBUGGING: Check if update actually stuck
if (update.id === 'tempValue') {
setTimeout(() => {
console.log(`🌡️ VERIFY: Element content after 100ms: "${percentEl.textContent}"`);
}, 100);
}
}
if (barEl) barEl.style.width = `${barValue}%`;
});
console.log("📊 Top stats bar updated from single source - TEMP:", this.currentResourceData.temp);
};
// Build the monitoring interface
nodeType.prototype.buildMonitorInterface = function(container) {
console.log("🌡️ BUILD: Building monitor interface - this will reset all values to defaults");
container.innerHTML = `
<div style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
">
<div style="color: #4fc3f7; font-weight: bold; font-size: 14px;">
📊 Resource Monitor
</div>
<button id="settingsBtn" style="
background: #333;
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
">⚙️ Settings</button>
</div>
<div id="resourcePanels" style="
background: #1a1a1a;
border-radius: 4px;
padding: 8px;
margin-bottom: 8px;
">
<div style="
display: flex;
justify-content: space-around;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #333;
">
<div style="text-align: center;">
<div style="color: #888; font-size: 11px;">CPU</div>
<div id="cpuPercent" style="color: #fff; font-size: 16px; font-weight: bold;">0%</div>
<div style="
width: 45px;
height: 3px;
background: #333;
border-radius: 2px;
margin: 4px auto;
overflow: hidden;
">
<div id="cpuBar" style="
width: 0%;
height: 100%;
background: #4fc3f7;
transition: width 0.3s ease;
"></div>
</div>
</div>
<div style="text-align: center;">
<div style="color: #888; font-size: 11px;">GPU</div>
<div id="gpuPercent" style="color: #fff; font-size: 16px; font-weight: bold;">0%</div>
<div style="
width: 45px;
height: 3px;
background: #333;
border-radius: 2px;
margin: 4px auto;
overflow: hidden;
">
<div id="gpuBar" style="
width: 0%;
height: 100%;
background: #81c784;
transition: width 0.3s ease;
"></div>
</div>
</div>
<div style="text-align: center;">
<div style="color: #888; font-size: 11px;">VRAM</div>
<div id="vramPercent" style="color: #fff; font-size: 16px; font-weight: bold;">0%</div>
<div style="
width: 45px;
height: 3px;
background: #333;
border-radius: 2px;
margin: 4px auto;
overflow: hidden;
">
<div id="vramBar" style="
width: 0%;
height: 100%;
background: #ff9800;
transition: width 0.3s ease;
"></div>
</div>
</div>
<div style="text-align: center;">
<div style="color: #888; font-size: 11px;">RAM</div>
<div id="ramPercent" style="color: #fff; font-size: 16px; font-weight: bold;">0%</div>
<div style="
width: 45px;
height: 3px;
background: #333;
border-radius: 2px;
margin: 4px auto;
overflow: hidden;
">
<div id="ramBar" style="
width: 0%;
height: 100%;
background: #e91e63;
transition: width 0.3s ease;
"></div>
</div>
</div>
<div style="text-align: center;">
<div style="color: #888; font-size: 11px;">TEMP</div>
<div id="tempValue" style="color: #fff; font-size: 16px; font-weight: bold;">0°C</div>
<div style="
width: 45px;
height: 3px;
background: #333;
border-radius: 2px;
margin: 4px auto;
overflow: hidden;
">
<div id="tempBar" style="
width: 0%;
height: 100%;
background: #f44336;
transition: width 0.3s ease;
"></div>
</div>
</div>
</div>
</div>
<div id="chartContainer" style="
background: #1a1a1a;
border-radius: 4px;
padding: 12px;
margin-bottom: 8px;
position: relative;
height: 120px;
">
<canvas id="resourceChart" style="
width: 100%;
height: 100%;
"></canvas>
<div style="
position: absolute;
bottom: 8px;
left: 12px;
color: #666;
font-size: 10px;
">60 SECONDS</div>
<div style="
position: absolute;
right: 12px;
top: 12px;
text-align: right;
">
<div style="color: #666; font-size: 10px;">100</div>
<div style="color: #666; font-size: 10px; position: absolute; bottom: 12px; right: 0;">0</div>
</div>
</div>
<div id="nodeList" style="
background: #1a1a1a;
border-radius: 4px;
padding: 8px;
border: 1px solid #333;
max-height: 150px;
overflow-y: auto;
">
<div style="
display: flex;
justify-content: space-between;
align-items: center;
color: #4fc3f7;
font-size: 11px;
margin-bottom: 6px;
font-weight: bold;
border-bottom: 1px solid #333;
padding-bottom: 4px;
">
<span>🎯 Per-Node Performance</span>
<button id="copyStatsBtn" style="
background: #333;
color: #4fc3f7;
border: none;
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
font-family: monospace;
" title="Copy stats to clipboard">📋</button>
</div>
<div id="nodeListContent" style="font-size: 10px; color: #ccc;">
Run a workflow to see per-node execution stats
</div>
</div>
<!-- Settings Modal -->
<div id="settingsModal" style="
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
z-index: 10000;
">
<div style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #2a2a2a;
border-radius: 8px;
padding: 20px;
min-width: 400px;
border: 1px solid #444;
">
<div style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
border-bottom: 1px solid #444;
padding-bottom: 8px;
">
<h3 style="color: #4fc3f7; margin: 0;">Monitor Settings</h3>
<button id="closeSettings" style="
background: #666;
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
">✕</button>
</div>
<div style="margin-bottom: 16px;">
<label style="color: #ccc; font-size: 12px; display: block; margin-bottom: 8px;">
Display Mode:
</label>
<div style="
background: #1a1a1a;
border-radius: 4px;
padding: 8px;
border: 1px solid #333;
margin-bottom: 16px;
">
<label style="
display: flex;
align-items: center;
color: #fff;
margin-bottom: 8px;
cursor: pointer;
">
<input type="radio" name="displayMode" value="combined" checked style="margin-right: 8px;" />
📊 Combined View (Stacked Chart)
</label>
<label style="
display: flex;
align-items: center;
color: #fff;
cursor: pointer;
">
<input type="radio" name="displayMode" value="individual" style="margin-right: 8px;" />
📈 Individual Panels (Separate Charts)
</label>
</div>
</div>
<div style="margin-bottom: 16px;">
<label style="color: #ccc; font-size: 12px; display: block; margin-bottom: 8px;">
Resource Panels (drag to reorder):
</label>
<div id="panelList" style="
background: #1a1a1a;
border-radius: 4px;
padding: 8px;
border: 1px solid #333;
">
<div class="panel-item" data-panel="cpu" draggable="true" style="
background: #333;
padding: 8px;
margin: 4px 0;
border-radius: 3px;
cursor: move;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 3px solid #4fc3f7;
">
<span>📊 CPU Usage</span>
<input type="checkbox" checked />
</div>
<div class="panel-item" data-panel="gpu" draggable="true" style="
background: #333;
padding: 8px;
margin: 4px 0;
border-radius: 3px;
cursor: move;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 3px solid #81c784;
">
<span>🖥️ GPU Usage</span>
<input type="checkbox" checked />
</div>
<div class="panel-item" data-panel="vram" draggable="true" style="
background: #333;
padding: 8px;
margin: 4px 0;
border-radius: 3px;
cursor: move;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 3px solid #ff9800;
">
<span>💾 VRAM Usage</span>
<input type="checkbox" checked />
</div>
<div class="panel-item" data-panel="ram" draggable="true" style="
background: #333;
padding: 8px;
margin: 4px 0;
border-radius: 3px;
cursor: move;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 3px solid #e91e63;
">
<span>🧠 RAM Usage</span>
<input type="checkbox" checked />
</div>
<div class="panel-item" data-panel="temp" draggable="true" style="
background: #333;
padding: 8px;
margin: 4px 0;
border-radius: 3px;
cursor: move;
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
border-left: 3px solid #f44336;
">
<span>🌡️ GPU Temperature</span>
<input type="checkbox" checked />
</div>
</div>
</div>
<div style="text-align: right;">
<button id="saveSettings" style="
background: #4fc3f7;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-left: 8px;
">Save Changes</button>
</div>
</div>
</div>
`;
// Initialize chart canvas
const canvas = container.querySelector('#resourceChart');
const ctx = canvas.getContext('2d');
this.chartCanvas = canvas;
this.chartContext = ctx;
// Initialize history for all resources
this.vramHistory = new Array(60).fill(0);
this.ramHistory = new Array(60).fill(0);
this.tempHistory = new Array(60).fill(0);
// SINGLE SOURCE OF TRUTH - Current resource values
this.currentResourceData = {
cpu: { value: 0, available: false },
gpu: { value: 0, available: false },
vram: { value: 0, percent: 0, used: 0, total: 0, available: false },
ram: { value: 0, percent: 0, used: 0, total: 0, available: false },
temp: { value: 0, available: false },
timestamp: 0
};
// Display mode settings - load from localStorage if available
this.loadSettings();
// Set canvas size (simplified approach)
const resizeCanvas = () => {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
};
// Initial resize
setTimeout(resizeCanvas, 100);
// Handle LiteGraph zoom changes
this.setupZoomHandling(canvas, ctx, resizeCanvas);
// Add event listeners
const settingsBtn = container.querySelector('#settingsBtn');
const settingsModal = container.querySelector('#settingsModal');
const closeSettings = container.querySelector('#closeSettings');
const saveSettings = container.querySelector('#saveSettings');
settingsBtn.onclick = () => {
// Restore current settings in the modal
this.populateSettingsModal(container);
settingsModal.style.display = 'block';
};
closeSettings.onclick = () => {
settingsModal.style.display = 'none';
};
saveSettings.onclick = () => {
// Get display mode
const selectedMode = container.querySelector('input[name="displayMode"]:checked').value;
this.displayMode = selectedMode;
// Get panel configuration
const panelItems = container.querySelectorAll('.panel-item');
this.panelOrder = [];
this.enabledPanels = [];
panelItems.forEach(item => {
const panel = item.dataset.panel;
const checkbox = item.querySelector('input[type="checkbox"]');
this.panelOrder.push(panel);
if (checkbox.checked) {
this.enabledPanels.push(panel);
}
});
// Save settings to localStorage
const settings = {
displayMode: this.displayMode,
panelOrder: this.panelOrder,
enabledPanels: this.enabledPanels
};
try {
localStorage.setItem('kikoStats_settings', JSON.stringify(settings));
console.log('Settings saved to localStorage:', settings);
} catch (e) {
console.warn('Could not save settings to localStorage:', e);
}
// Rebuild the interface with new settings
this.rebuildInterface();
settingsModal.style.display = 'none';
};
// Close modal on outside click
settingsModal.onclick = (e) => {
if (e.target === settingsModal) {
settingsModal.style.display = 'none';
}
};
this.container = container;
this.nodeListDiv = container.querySelector('#nodeListContent');
this.monitoringActive = true;
// Add copy stats functionality
const copyStatsBtn = container.querySelector('#copyStatsBtn');
if (copyStatsBtn) {
copyStatsBtn.onclick = () => {
this.copyStatsToClipboard();
};
}
// Start the chart update loop
this.startChartUpdates();
// Force initial chart draw and apply loaded settings
setTimeout(() => {
// Apply the loaded settings to the interface
if (this.displayMode !== 'combined') {
this.rebuildInterface();
// After rebuild, restore current values from single source of truth
this.updateAllUI();
} else {
this.drawChart();
}
}, 200);
};
// Setup zoom handling for LiteGraph compatibility
nodeType.prototype.setupZoomHandling = function(canvas, ctx, resizeCallback) {
// Simple approach: just handle resize events
const originalOnResize = this.onResize;
this.onResize = function(size) {
if (originalOnResize) {
originalOnResize.call(this, size);
}
// Simple delayed redraw
setTimeout(() => {
if (this.displayMode === 'individual' && this.individualCanvases) {
this.updateIndividualCharts();
} else if (this.drawChart) {
this.drawChart();
}
}, 100);
};
};
// Draw the resource chart (combined stacked view like ss.png)
nodeType.prototype.drawChart = function() {
if (!this.chartContext || !this.chartCanvas) {
console.log("📊 Chart context or canvas missing");
return;
}
// Drawing chart from single source of truth
const ctx = this.chartContext;
const canvas = this.chartCanvas;
const width = canvas.width;
const height = canvas.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw grid
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
// Horizontal grid lines (0%, 25%, 50%, 75%, 100%)
for (let i = 0; i <= 4; i++) {
const y = (height / 4) * i;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
// Vertical grid lines (every 10 seconds)
for (let i = 0; i <= 6; i++) {
const x = (width / 6) * i;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
// Define resources with single source of truth data
const resources = [
{ name: 'CPU', data: this.cpuHistory, color: '#4fc3f7' },
{ name: 'GPU', data: this.gpuHistory, color: '#81c784' },
{ name: 'VRAM', data: this.vramHistory, color: '#ff9800' },
{ name: 'RAM', data: this.ramHistory, color: '#e91e63' },
{ name: 'TEMP', data: this.tempHistory, color: '#f44336' }
];
// Filter enabled resources
const enabledResources = resources.filter(r => this.enabledPanels.includes(r.name.toLowerCase()));
if (enabledResources.length === 0) return;
const stepX = width / 60;
// Draw lines for each enabled resource
enabledResources.forEach((resource) => {
ctx.strokeStyle = resource.color;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < resource.data.length; i++) {
const x = i * stepX;
const y = height - (resource.data[i] / 100) * height;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
});
};
// Load settings from localStorage
nodeType.prototype.loadSettings = function() {
try {
const savedSettings = localStorage.getItem('kikoStats_settings');
// Loading saved settings
if (savedSettings) {
const settings = JSON.parse(savedSettings);
this.displayMode = settings.displayMode || 'combined';
this.enabledPanels = settings.enabledPanels || ['cpu', 'gpu', 'vram', 'ram', 'temp'];
this.panelOrder = settings.panelOrder || ['cpu', 'gpu', 'vram', 'ram', 'temp'];
// Settings loaded from localStorage
} else {
// Default settings
this.displayMode = 'combined';
this.enabledPanels = ['cpu', 'gpu', 'vram', 'ram', 'temp'];
this.panelOrder = ['cpu', 'gpu', 'vram', 'ram', 'temp'];
// Using default settings
}
} catch (e) {
// Could not load settings from localStorage
// Default settings
this.displayMode = 'combined';
this.enabledPanels = ['cpu', 'gpu', 'vram', 'ram', 'temp'];
this.panelOrder = ['cpu', 'gpu', 'vram', 'ram', 'temp'];
}
};
// Copy stats to clipboard
nodeType.prototype.copyStatsToClipboard = function() {
const frontendNodes = window.kikoStatsCompletedNodes || [];
if (frontendNodes.length === 0) {
// Show feedback that there's nothing to copy
const copyBtn = this.container.querySelector('#copyStatsBtn');
if (copyBtn) {
const originalText = copyBtn.textContent;
copyBtn.textContent = '❌';
copyBtn.style.color = '#f44336';
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.style.color = '#4fc3f7';
}, 1000);
}
return;
}
// Format stats for clipboard
let statsText = '🎯 ComfyUI Node Performance Stats\n';
statsText += '=' + '='.repeat(40) + '\n\n';
frontendNodes.forEach((node, index) => {
const duration = (node.duration_ms / 1000).toFixed(1);
const nodeTitle = node.node_title || node.node_type || `Node ${node.node_id}`;
statsText += `${index + 1}. ${nodeTitle}\n`;
statsText += ` Duration: ${duration}s\n`;
if (node.avg_cpu_percent > 0) {
statsText += ` CPU: ${node.avg_cpu_percent.toFixed(1)}% (peak: ${node.max_cpu_percent.toFixed(1)}%)\n`;
}
if (node.avg_gpu_utilization > 0) {
statsText += ` GPU: ${node.avg_gpu_utilization.toFixed(1)}% (peak: ${node.max_gpu_utilization.toFixed(1)}%)\n`;
}
if (node.peak_vram_used > 0) {
statsText += ` VRAM: ${node.peak_vram_used.toFixed(0)}MB\n`;
}
statsText += '\n';
});
statsText += `Generated by ComfyUI-KikoStats at ${new Date().toLocaleString()}`;
// Copy to clipboard
navigator.clipboard.writeText(statsText).then(() => {
// Show success feedback
const copyBtn = this.container.querySelector('#copyStatsBtn');
if (copyBtn) {
const originalText = copyBtn.textContent;
copyBtn.textContent = '✅';
copyBtn.style.color = '#4caf50';
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.style.color = '#4fc3f7';
}, 1000);
}
// Stats copied to clipboard
}).catch(err => {
console.error('Failed to copy stats:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = statsText;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
// Show success feedback
const copyBtn = this.container.querySelector('#copyStatsBtn');
if (copyBtn) {
const originalText = copyBtn.textContent;
copyBtn.textContent = '✅';
copyBtn.style.color = '#4caf50';
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.style.color = '#4fc3f7';
}, 1000);
}
});
};
// Populate settings modal with current values
nodeType.prototype.populateSettingsModal = function(container) {
// Set display mode radio buttons
const displayModeRadios = container.querySelectorAll('input[name="displayMode"]');
displayModeRadios.forEach(radio => {
radio.checked = radio.value === this.displayMode;
});
// Set panel checkboxes and order
const panelItems = container.querySelectorAll('.panel-item');
panelItems.forEach(item => {
const panel = item.dataset.panel;
const checkbox = item.querySelector('input[type="checkbox"]');
checkbox.checked = this.enabledPanels.includes(panel);
});
console.log('Settings modal populated with current values');
};
// Draw the resource chart (combined stacked view like ss.png)
nodeType.prototype.drawChart = function() {
if (!this.chartContext || !this.chartCanvas) {
console.log("📊 Chart context or canvas missing");
return;
}
console.log("📊 Drawing chart, mode:", this.displayMode);
const ctx = this.chartContext;
const canvas = this.chartCanvas;
const rect = canvas.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw grid lines
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
// Horizontal lines
for (let i = 0; i <= 4; i++) {
const y = (height * i) / 4;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
// Vertical lines (time markers)
for (let i = 0; i <= 6; i++) {
const x = (width * i) / 6;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
const stepX = width / (this.cpuHistory.length - 1);
// Define colors for each resource
const resources = [
{ data: this.cpuHistory, color: '#4fc3f7', name: 'CPU' },
{ data: this.gpuHistory, color: '#81c784', name: 'GPU' },
{ data: this.vramHistory, color: '#ff9800', name: 'VRAM' },
{ data: this.ramHistory, color: '#e91e63', name: 'RAM' },
{ data: this.tempHistory, color: '#f44336', name: 'TEMP' }
];
if (this.displayMode === 'combined') {
// Draw stacked area chart like ss.png
const enabledResources = resources.filter(r => this.enabledPanels.includes(r.name.toLowerCase()));
// Calculate stacked values
const stackedData = [];
for (let i = 0; i < this.cpuHistory.length; i++) {
let stackedValue = 0;
const point = [];
enabledResources.forEach(resource => {
stackedValue += resource.data[i];
point.push({ value: stackedValue, color: resource.color });
});
stackedData.push(point);
}
// Draw stacked areas from bottom to top
enabledResources.forEach((resource, resourceIndex) => {
ctx.fillStyle = resource.color;
ctx.beginPath();
// Draw the area for this resource
for (let i = 0; i < stackedData.length; i++) {
const x = i * stepX;
const currentStack = stackedData[i];
if (resourceIndex < currentStack.length) {
const topY = height - (currentStack[resourceIndex].value / 400) * height; // Scale for stacked view
const bottomY = resourceIndex > 0 ?
height - (currentStack[resourceIndex - 1].value / 400) * height : height;
if (i === 0) {
ctx.moveTo(x, bottomY);
ctx.lineTo(x, topY);
} else {
ctx.lineTo(x, topY);
}
}
}
// Complete the area
for (let i = stackedData.length - 1; i >= 0; i--) {
const x = i * stepX;
const currentStack = stackedData[i];
const bottomY = resourceIndex > 0 && resourceIndex - 1 < currentStack.length ?
height - (currentStack[resourceIndex - 1].value / 400) * height : height;
ctx.lineTo(x, bottomY);
}
ctx.closePath();
ctx.fill();
});
} else {
// Draw individual lines
const enabledResources = resources.filter(r => this.enabledPanels.includes(r.name.toLowerCase()));
enabledResources.forEach((resource, index) => {
ctx.strokeStyle = resource.color;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < resource.data.length; i++) {
const x = i * stepX;
const y = height - (resource.data[i] / 100) * height;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
});
}
};
// Update chart with new data - NOW USES SINGLE SOURCE OF TRUTH
nodeType.prototype.updateChart = function() {
if (!this.container) return;
// updateChart called - using single source of truth
// Get current stats and update single source of truth
const stats = this.getResourceStats();
if (stats && !stats.waiting) {
// Process data through single source of truth
this.updateResourceData(stats);
} else {
// Just redraw charts from existing data
if (this.displayMode === 'individual' && this.individualCanvases) {
this.updateIndividualCharts();
} else {
this.drawChart();
}
}
};
// Start chart update loop
nodeType.prototype.startChartUpdates = function() {
if (this.chartUpdateInterval) clearInterval(this.chartUpdateInterval);
this.chartUpdateInterval = setInterval(() => {
// Timer-based update using single source of truth
this.updateChart();
}, 1000); // Update every 1s, synchronized with backend
};
// Update the monitoring display (legacy function for compatibility)
nodeType.prototype.updateMonitorDisplay = function(container) {
// This is now handled by updateChart()
this.updateChart();
};
// Update the node performance list (restored functionality)
nodeType.prototype.updateNodeList = function(nodeMetrics) {
if (!this.nodeListDiv || !nodeMetrics || nodeMetrics.length === 0) {
if (this.nodeListDiv) {
this.nodeListDiv.innerHTML = '<div style="color: #666;">Run a workflow to see per-node stats</div>';
}
return;
}
// Build node list HTML
const nodeHtml = nodeMetrics.map(node => {
const duration = (node.duration_ms / 1000).toFixed(1);
const nodeTitle = node.node_title || node.node_type || `Node ${node.node_id}`;
// Format the performance display
let perfText = '';
if (node.avg_cpu_percent > 0 || node.avg_gpu_utilization > 0) {
const cpuText = node.avg_cpu_percent > 0 ? `${node.avg_cpu_percent.toFixed(1)}% CPU` : '';
const gpuText = node.avg_gpu_utilization > 0 ? `${node.avg_gpu_utilization.toFixed(1)}% GPU` : '';
const parts = [cpuText, gpuText].filter(p => p);
perfText = parts.join(', ');
} else {
perfText = 'No activity';
}
return `
<div style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 3px 0;
border-bottom: 1px solid #333;
margin-bottom: 2px;
">
<div style="color: #81c784; font-weight: bold; max-width: 120px; overflow: hidden; text-overflow: ellipsis;">
${nodeTitle}
</div>
<div style="color: #fff; text-align: right; font-size: 9px;">
${perfText}
<div style="color: #888;">(${duration}s)</div>
</div>
</div>
`;
}).join('');
this.nodeListDiv.innerHTML = nodeHtml || '<div style="color: #666;">No node data available</div>';
};
// Add a completed node to the display (restored functionality)
nodeType.prototype.addCompletedNode = function(nodeMetrics) {
console.log("📊 Adding completed node to display:", nodeMetrics);
// Update node list immediately
if (this.nodeListDiv) {
const frontendNodes = window.kikoStatsCompletedNodes || [];
this.updateNodeList(frontendNodes);
}
};
// Update completed nodes display (restored functionality)
nodeType.prototype.updateCompletedNodes = function() {
console.log("📊 Updating completed nodes display");
// Update node list with all completed nodes
if (this.nodeListDiv) {
const frontendNodes = window.kikoStatsCompletedNodes || [];
this.updateNodeList(frontendNodes);
}
};
// Rebuild interface based on current settings
nodeType.prototype.rebuildInterface = function() {
if (!this.container) return;
console.log("🌡️ REBUILD: Rebuilding interface - this may reset temperature display");
// Rebuilding interface
if (this.displayMode === 'individual') {
// Create individual charts for each enabled panel
this.buildIndividualCharts();
} else {
// Rebuild combined view with filtered panels
this.buildCombinedChart();
}
};
// Build combined chart view
nodeType.prototype.buildCombinedChart = function() {
const chartContainer = this.container.querySelector('#chartContainer');
if (!chartContainer) return;
// Reset to single chart
chartContainer.innerHTML = `
<canvas id="resourceChart" style="
width: 100%;
height: 100%;
"></canvas>
<div style="
position: absolute;
bottom: 8px;
left: 12px;
color: #666;
font-size: 10px;
">60 SECONDS</div>
<div style="
position: absolute;
right: 12px;
top: 12px;
text-align: right;
">
<div style="color: #666; font-size: 10px;">100</div>
<div style="color: #666; font-size: 10px; position: absolute; bottom: 12px; right: 0;">0</div>
</div>
`;
// Reinitialize canvas
const canvas = chartContainer.querySelector('#resourceChart');
const ctx = canvas.getContext('2d');
this.chartCanvas = canvas;
this.chartContext = ctx;
// Set canvas size (simplified approach)
const resizeCanvas = () => {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
};
setTimeout(resizeCanvas, 100);
// Setup zoom handling for this canvas too
this.setupZoomHandling(canvas, ctx, resizeCanvas);
this.updateChart();
// Restore current values after rebuilding combined chart
setTimeout(() => {
this.updateAllUI();
}, 50);
};
// Build individual charts for each resource
nodeType.prototype.buildIndividualCharts = function() {
const chartContainer = this.container.querySelector('#chartContainer');
if (!chartContainer) return;
// Clear existing content
chartContainer.innerHTML = '';
chartContainer.style.height = 'auto';
chartContainer.style.padding = '8px';
// Resource definitions
const resourceDefs = {
cpu: { name: 'CPU', color: '#4fc3f7', data: 'cpuHistory', unit: '%' },
gpu: { name: 'GPU', color: '#81c784', data: 'gpuHistory', unit: '%' },
vram: { name: 'VRAM', color: '#ff9800', data: 'vramHistory', unit: '%' },
ram: { name: 'RAM', color: '#e91e63', data: 'ramHistory', unit: '%' },
temp: { name: 'TEMP', color: '#f44336', data: 'tempHistory', unit: '°C' }
};
// Create individual panels for enabled resources in order
this.individualCanvases = {};
this.panelOrder.forEach(panelId => {
if (this.enabledPanels.includes(panelId)) {
const resource = resourceDefs[panelId];
if (resource) {
const panelDiv = document.createElement('div');
panelDiv.style.cssText = `
background: rgba(0,0,0,0.2);
border-radius: 4px;
padding: 8px;
margin-bottom: 8px;
border-left: 3px solid ${resource.color};
position: relative;
height: 80px;
`;
panelDiv.innerHTML = `
<div style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
">
<span style="color: ${resource.color}; font-weight: bold; font-size: 11px;">
${resource.name}
</span>
<span id="${panelId}Value" style="color: #fff; font-size: 14px; font-weight: bold;">
0${resource.unit}
</span>
</div>
<canvas id="${panelId}Chart" style="
width: 100%;
height: 60px;
display: block;
"></canvas>
`;
chartContainer.appendChild(panelDiv);
// Initialize canvas
const canvas = panelDiv.querySelector(`#${panelId}Chart`);
const ctx = canvas.getContext('2d');
this.individualCanvases[panelId] = { canvas, ctx, resource };
// Set canvas size (simplified approach)
setTimeout(() => {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
}, 100);
}
}
});
// Update node size for individual panels
const panelCount = this.enabledPanels.length;
const newHeight = 300 + (panelCount * 100); // Base height + panels
this.size = [380, Math.min(newHeight, 600)]; // Cap at 600px height
this.updateIndividualCharts();
// Restore current values after rebuilding individual charts
setTimeout(() => {
this.updateAllUI();
}, 50);
};
// Update individual charts
nodeType.prototype.updateIndividualCharts = function() {
if (!this.individualCanvases) return;
// Updating individual charts
Object.keys(this.individualCanvases).forEach(panelId => {
const { canvas, ctx, resource } = this.individualCanvases[panelId];
const data = this[resource.data];
if (!canvas || !ctx || !data) return;
const rect = canvas.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw grid lines
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
// Horizontal lines
for (let i = 0; i <= 2; i++) {
const y = (height * i) / 2;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
// Draw chart line
ctx.strokeStyle = resource.color;
ctx.lineWidth = 2;
ctx.beginPath();
const stepX = width / (data.length - 1);
// Determine scale based on resource type
let maxValue = 100;
if (panelId === 'temp') {
maxValue = 90; // Temperature scale 0-90°C
}
for (let i = 0; i < data.length; i++) {
const x = i * stepX;
let value = data[i];
// For temperature, convert percentage back to actual temp for display
if (panelId === 'temp') {
value = (data[i] / 100) * 90; // Convert back from percentage to temp
}
const y = height - (value / maxValue) * height;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
// Fill area under curve
ctx.fillStyle = resource.color + '20';
ctx.lineTo(width, height);
ctx.lineTo(0, height);
ctx.closePath();
ctx.fill();
// Update value display using SINGLE SOURCE OF TRUTH
const valueEl = this.container.querySelector(`#${panelId}Value`);
if (valueEl) {
let displayValue = 0;
// Get value from single source of truth
switch(panelId) {
case 'cpu':
displayValue = this.currentResourceData.cpu.value;
break;
case 'gpu':
displayValue = this.currentResourceData.gpu.value;
break;
case 'vram':
displayValue = this.currentResourceData.vram.value;
break;
case 'ram':
displayValue = this.currentResourceData.ram.value;
break;
case 'temp':
displayValue = this.currentResourceData.temp.value; // Actual temperature in °C
break;
}
valueEl.textContent = `${Math.round(displayValue)}${resource.unit}`;
// Value set from single source
}
});
};
// Get resource stats from the stored execution data
nodeType.prototype.getResourceStats = function() {
const currentTime = new Date().toLocaleTimeString();
// Getting resource stats
// Use the latest monitoring data from node execution
if (this.latestMonitorData && this.latestMonitorData.gpu && this.latestMonitorData.system) {
// Returning fresh monitoring data
// TEMPORARY DEBUG: If temperature is 0, supplement with mock temperature
if (this.latestMonitorData.gpu && this.latestMonitorData.gpu.temperature === 0) {
console.log("📊 WARNING: Backend provided 0°C temperature - supplementing with mock data");
this.latestMonitorData.gpu.temperature = Math.floor(Math.random() * 25) + 45; // 45-70°C
console.log(`📊 Using mock temperature: ${this.latestMonitorData.gpu.temperature}°C`);
}
return this.latestMonitorData;
}
// Enhanced mock data generator to provide realistic values including temperature
// Generating enhanced mock data with temperature
// Generate realistic mock data with temperature
const mockTemp = Math.floor(Math.random() * 25) + 45; // 45-70°C realistic GPU temp
const mockCpuPercent = Math.floor(Math.random() * 40) + 10; // 10-50% CPU
const mockGpuUtil = Math.floor(Math.random() * 60) + 20; // 20-80% GPU
const mockVramUsed = Math.floor(Math.random() * 4000) + 2000; // 2-6GB
const mockVramTotal = 8192; // 8GB total
const mockRamUsed = Math.floor(Math.random() * 8000) + 4000; // 4-12GB
const mockRamTotal = 16384; // 16GB total
const mockData = {
gpu: {
available: true,
utilization: mockGpuUtil,
memory_used: mockVramUsed,
memory_total: mockVramTotal,
temperature: mockTemp,
power_draw: Math.floor(Math.random() * 100) + 150 // 150-250W
},
system: {
available: true,
cpu_percent: mockCpuPercent,
ram_used: mockRamUsed,
ram_total: mockRamTotal
},
timestamp: Date.now() / 1000
};
// Generated mock data
return mockData;
};
// Update the node performance list
nodeType.prototype.updateNodeList = function(nodeMetrics) {
if (!this.nodeListDiv || !nodeMetrics || nodeMetrics.length === 0) {
if (this.nodeListDiv) {
this.nodeListDiv.innerHTML = '<div style="color: #666;">Run a workflow to see per-node stats</div>';
}
return;
}
// Build node list HTML
const nodeHtml = nodeMetrics.map(node => {
const duration = (node.duration_ms / 1000).toFixed(1);
const nodeTitle = node.node_title || node.node_type || `Node ${node.node_id}`;
// Format the performance display
let perfText = '';
if (node.avg_cpu_percent > 0 || node.avg_gpu_utilization > 0) {
const cpuText = node.avg_cpu_percent > 0 ? `${node.avg_cpu_percent.toFixed(1)}% CPU` : '';
const gpuText = node.avg_gpu_utilization > 0 ? `${node.avg_gpu_utilization.toFixed(1)}% GPU` : '';
const parts = [cpuText, gpuText].filter(p => p);
perfText = parts.join(', ');
} else {
perfText = 'No activity';
}
return `
<div style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 0;
border-bottom: 1px solid #333;
margin-bottom: 2px;
">
<div style="color: #81c784; font-weight: bold; max-width: 120px; overflow: hidden; text-overflow: ellipsis;">
${nodeTitle}
</div>
<div style="color: #fff; text-align: right; font-size: 9px;">
${perfText}
<div style="color: #888;">(${duration}s)</div>
</div>
</div>
`;
}).join('');
this.nodeListDiv.innerHTML = nodeHtml || '<div style="color: #666;">No node data available</div>';
};
// Add a completed node to the display
nodeType.prototype.addCompletedNode = function(nodeMetrics) {
console.log("📊 Adding completed node to display:", nodeMetrics);
// Update node list immediately
if (this.nodeListDiv) {
const frontendNodes = window.kikoStatsCompletedNodes || [];
this.updateNodeList(frontendNodes);
}
};
// Update completed nodes display (called after workflow finishes)
nodeType.prototype.updateCompletedNodes = function() {
console.log("📊 Updating completed nodes display");
// Update node list with all completed nodes
if (this.nodeListDiv) {
const frontendNodes = window.kikoStatsCompletedNodes || [];
this.updateNodeList(frontendNodes);
}
};
// Start monitoring updates - simplified approach
nodeType.prototype.startMonitoringUpdates = function() {
// The display will update when the workflow executes
// via the onExecuted callback
};
}
}
});