Files
ComfyUI-Lora-Manager/web/comfyui/usage_stats.js
Will Miao 26b139884c perf(usage-stats): prevent unnecessary writes when idle
- Add is_dirty flag to track if statistics have changed
- Only write stats file when data actually changes
- Add enable_usage_statistics setting in ComfyUI settings
- Skip backend requests when usage statistics is disabled
- Fix standalone mode compatibility for MetadataRegistry

Fixes #826
2026-02-23 14:00:00 +08:00

168 lines
6.4 KiB
JavaScript

// ComfyUI extension to track model usage statistics
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { showToast } from "./utils.js";
import { getAutoPathCorrectionPreference, getUsageStatisticsPreference } from "./settings.js";
// Define target nodes and their widget configurations
const PATH_CORRECTION_TARGETS = [
{ comfyClass: "CheckpointLoaderSimple", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "Checkpoint Loader with Name (Image Saver)", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "UNETLoader", widgetName: "unet_name", modelType: "checkpoints" },
{ comfyClass: "easy comfyLoader", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "CheckpointLoader|pysssss", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "Efficient Loader", widgetName: "ckpt_name", modelType: "checkpoints" },
{ comfyClass: "UnetLoaderGGUF", widgetName: "unet_name", modelType: "checkpoints" },
{ comfyClass: "UnetLoaderGGUFAdvanced", widgetName: "unet_name", modelType: "checkpoints" },
{ comfyClass: "LoraLoader", widgetName: "lora_name", modelType: "loras" },
{ comfyClass: "easy loraStack", widgetNamePattern: "lora_\\d+_name", modelType: "loras" }
];
// Register the extension
app.registerExtension({
name: "LoraManager.UsageStats",
setup() {
// Listen for successful executions
api.addEventListener("execution_success", ({ detail }) => {
// Skip if usage statistics is disabled
if (!getUsageStatisticsPreference()) {
return;
}
if (detail && detail.prompt_id) {
this.updateUsageStats(detail.prompt_id);
}
});
},
async updateUsageStats(promptId) {
try {
// Call backend endpoint with the prompt_id
const response = await fetch(`/api/lm/update-usage-stats`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt_id: promptId }),
});
if (!response.ok) {
console.warn("Failed to update usage statistics:", response.statusText);
}
} catch (error) {
console.error("Error updating usage statistics:", error);
}
},
async loadedGraphNode(node) {
if (!getAutoPathCorrectionPreference()) {
return;
}
// Check if this node type needs path correction
const target = PATH_CORRECTION_TARGETS.find(t => t.comfyClass === node.comfyClass);
if (!target) {
return;
}
await this.correctNodePaths(node, target);
},
async correctNodePaths(node, target) {
try {
if (target.widgetNamePattern) {
// Handle pattern-based widget names (like lora_1_name, lora_2_name, etc.)
const pattern = new RegExp(target.widgetNamePattern);
const widgetIndexes = [];
if (node.widgets) {
node.widgets.forEach((widget, index) => {
if (pattern.test(widget.name)) {
widgetIndexes.push(index);
}
});
}
// Process each matching widget
for (const widgetIndex of widgetIndexes) {
await this.correctWidgetPath(node, widgetIndex, target.modelType);
}
} else {
// Handle single widget name
if (node.widgets) {
const widgetIndex = node.widgets.findIndex(w => w.name === target.widgetName);
if (widgetIndex !== -1) {
await this.correctWidgetPath(node, widgetIndex, target.modelType);
}
}
}
} catch (error) {
console.error("Error correcting node paths:", error);
}
},
async correctWidgetPath(node, widgetIndex, modelType) {
if (!node.widgets_values || !node.widgets_values[widgetIndex]) {
return;
}
const currentPath = node.widgets_values[widgetIndex];
if (!currentPath || typeof currentPath !== 'string') {
return;
}
// Extract filename from path (after last separator)
const fileName = currentPath.split(/[/\\]/).pop();
if (!fileName) {
return;
}
try {
// Search for current relative path
const response = await api.fetchApi(`/lm/${modelType}/relative-paths?search=${encodeURIComponent(fileName)}&limit=2`);
const data = await response.json();
if (!data.success || !data.relative_paths || data.relative_paths.length === 0) {
return;
}
const foundPaths = data.relative_paths;
const firstPath = foundPaths[0];
// Check if we need to update the path
if (firstPath !== currentPath) {
// Update the widget value
// node.widgets_values[widgetIndex] = firstPath;
node.widgets[widgetIndex].value = firstPath;
if (foundPaths.length === 1) {
// Single match found - success
showToast({
severity: 'info',
summary: 'LoRA Manager Path Correction',
detail: `Updated path for ${fileName}: ${firstPath}`,
life: 5000
});
} else {
// Multiple matches found - warning
showToast({
severity: 'warn',
summary: 'LoRA Manager Path Correction',
detail: `Multiple paths found for ${fileName}, using: ${firstPath}`,
life: 5000
});
}
// Mark node as modified
if (node.setDirtyCanvas) {
node.setDirtyCanvas(true);
}
}
} catch (error) {
console.error(`Error correcting path for ${fileName}:`, error);
}
}
});