Debounce canvas output updates and optimize image handling

Added debouncing to the updateOutput function to prevent excessive updates during rapid changes. Large images are now handled using blob URLs for better performance, while small images use data URIs. Also added logic to skip output updates when preview is disabled and improved cleanup of temporary file trackers when nodes are removed.
This commit is contained in:
Dariusz L
2025-07-23 16:27:12 +02:00
parent 472f8768a5
commit ab4a8f7ca7
3 changed files with 137 additions and 49 deletions

View File

@@ -534,45 +534,80 @@ async function createCanvasWidget(node, widget, app) {
};
updateButtonStates();
canvas.updateHistoryButtons();
// Debounce timer for updateOutput to prevent excessive updates
let updateOutputTimer = null;
const updateOutput = async (node, canvas) => {
// Check if preview is disabled - if so, skip updateOutput entirely
const showPreviewWidget = node.widgets.find((w) => w.name === "show_preview");
if (showPreviewWidget && !showPreviewWidget.value) {
log.debug("Preview disabled, skipping updateOutput");
return;
}
const triggerWidget = node.widgets.find((w) => w.name === "trigger");
if (triggerWidget) {
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
}
try {
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
if (blob) {
// Auto-register in clipspace for Impact Pack compatibility and get server URL
const serverImg = await registerImageInClipspace(node, blob);
if (serverImg) {
// Use server URL image as the main image for Impact Pack compatibility
node.imgs = [serverImg];
node.clipspaceImg = serverImg;
log.debug(`Using server URL for node.imgs: ${serverImg.src}`);
// Clear previous timer
if (updateOutputTimer) {
clearTimeout(updateOutputTimer);
}
// Debounce the update to prevent excessive processing during rapid changes
updateOutputTimer = setTimeout(async () => {
try {
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
if (blob) {
// For large images, use blob URL for better performance
if (blob.size > 2 * 1024 * 1024) { // 2MB threshold
const blobUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
node.imgs = [img];
log.debug(`Using blob URL for large image (${(blob.size / 1024 / 1024).toFixed(1)}MB): ${blobUrl.substring(0, 50)}...`);
// Clean up old blob URLs to prevent memory leaks
if (node.imgs.length > 1) {
const oldImg = node.imgs[0];
if (oldImg.src.startsWith('blob:')) {
URL.revokeObjectURL(oldImg.src);
}
}
};
img.src = blobUrl;
}
else {
// For smaller images, use data URI as before
const reader = new FileReader();
reader.onload = () => {
const dataUrl = reader.result;
const img = new Image();
img.onload = () => {
node.imgs = [img];
log.debug(`Using data URI for small image (${(blob.size / 1024).toFixed(1)}KB): ${dataUrl.substring(0, 50)}...`);
};
img.src = dataUrl;
};
reader.readAsDataURL(blob);
}
}
else {
// Fallback to blob URL if server upload failed
const new_preview = new Image();
new_preview.src = URL.createObjectURL(blob);
await new Promise(r => new_preview.onload = r);
node.imgs = [new_preview];
log.debug(`Fallback to blob URL for node.imgs: ${new_preview.src}`);
node.imgs = [];
}
}
else {
node.imgs = [];
catch (error) {
console.error("Error updating node preview:", error);
}
}
catch (error) {
console.error("Error updating node preview:", error);
}
}, 150); // 150ms debounce delay
};
// Store previous temp filenames for cleanup (make it globally accessible)
if (!window.layerForgeTempFileTracker) {
window.layerForgeTempFileTracker = new Map();
}
const tempFileTracker = window.layerForgeTempFileTracker;
// Function to register image in clipspace for Impact Pack compatibility
const registerImageInClipspace = async (node, blob) => {
try {
// Upload the image to ComfyUI's temp storage for clipspace access
const formData = new FormData();
const filename = `layerforge-auto-${node.id}-${Date.now()}.png`;
const filename = `layerforge-sam-${node.id}-${Date.now()}.png`; // Use timestamp for SAM Detector
formData.append("image", blob, filename);
formData.append("overwrite", "true");
formData.append("type", "temp");
@@ -1249,6 +1284,13 @@ app.registerExtension({
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
log.info(`Cleaning up canvas node ${this.id}`);
// Clean up temp file tracker for this node (just remove from tracker)
const nodeKey = `node-${this.id}`;
const tempFileTracker = window.layerForgeTempFileTracker;
if (tempFileTracker && tempFileTracker.has(nodeKey)) {
tempFileTracker.delete(nodeKey);
log.debug(`Removed temp file tracker for node ${this.id}`);
}
canvasNodeInstances.delete(this.id);
log.info(`Deregistered CanvasNode instance for ID: ${this.id}`);
if (window.canvasExecutionStates) {

View File

@@ -568,45 +568,83 @@ async function createCanvasWidget(node: ComfyNode, widget: any, app: ComfyApp):
updateButtonStates();
canvas.updateHistoryButtons();
// Debounce timer for updateOutput to prevent excessive updates
let updateOutputTimer: NodeJS.Timeout | null = null;
const updateOutput = async (node: ComfyNode, canvas: Canvas) => {
// Check if preview is disabled - if so, skip updateOutput entirely
const showPreviewWidget = node.widgets.find((w) => w.name === "show_preview");
if (showPreviewWidget && !showPreviewWidget.value) {
log.debug("Preview disabled, skipping updateOutput");
return;
}
const triggerWidget = node.widgets.find((w) => w.name === "trigger");
if (triggerWidget) {
triggerWidget.value = (triggerWidget.value + 1) % 99999999;
}
try {
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
if (blob) {
// Auto-register in clipspace for Impact Pack compatibility and get server URL
const serverImg = await registerImageInClipspace(node, blob);
if (serverImg) {
// Use server URL image as the main image for Impact Pack compatibility
node.imgs = [serverImg];
(node as any).clipspaceImg = serverImg;
log.debug(`Using server URL for node.imgs: ${serverImg.src}`);
} else {
// Fallback to blob URL if server upload failed
const new_preview = new Image();
new_preview.src = URL.createObjectURL(blob);
await new Promise(r => new_preview.onload = r);
node.imgs = [new_preview];
log.debug(`Fallback to blob URL for node.imgs: ${new_preview.src}`);
}
} else {
node.imgs = [];
}
} catch (error) {
console.error("Error updating node preview:", error);
// Clear previous timer
if (updateOutputTimer) {
clearTimeout(updateOutputTimer);
}
// Debounce the update to prevent excessive processing during rapid changes
updateOutputTimer = setTimeout(async () => {
try {
const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob();
if (blob) {
// For large images, use blob URL for better performance
if (blob.size > 2 * 1024 * 1024) { // 2MB threshold
const blobUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
node.imgs = [img];
log.debug(`Using blob URL for large image (${(blob.size / 1024 / 1024).toFixed(1)}MB): ${blobUrl.substring(0, 50)}...`);
// Clean up old blob URLs to prevent memory leaks
if (node.imgs.length > 1) {
const oldImg = node.imgs[0];
if (oldImg.src.startsWith('blob:')) {
URL.revokeObjectURL(oldImg.src);
}
}
};
img.src = blobUrl;
} else {
// For smaller images, use data URI as before
const reader = new FileReader();
reader.onload = () => {
const dataUrl = reader.result as string;
const img = new Image();
img.onload = () => {
node.imgs = [img];
log.debug(`Using data URI for small image (${(blob.size / 1024).toFixed(1)}KB): ${dataUrl.substring(0, 50)}...`);
};
img.src = dataUrl;
};
reader.readAsDataURL(blob);
}
} else {
node.imgs = [];
}
} catch (error) {
console.error("Error updating node preview:", error);
}
}, 250); // 150ms debounce delay
};
// Store previous temp filenames for cleanup (make it globally accessible)
if (!(window as any).layerForgeTempFileTracker) {
(window as any).layerForgeTempFileTracker = new Map<string, string>();
}
const tempFileTracker = (window as any).layerForgeTempFileTracker;
// Function to register image in clipspace for Impact Pack compatibility
const registerImageInClipspace = async (node: ComfyNode, blob: Blob): Promise<HTMLImageElement | null> => {
try {
// Upload the image to ComfyUI's temp storage for clipspace access
const formData = new FormData();
const filename = `layerforge-auto-${node.id}-${Date.now()}.png`;
const filename = `layerforge-sam-${node.id}-${Date.now()}.png`; // Use timestamp for SAM Detector
formData.append("image", blob, filename);
formData.append("overwrite", "true");
formData.append("type", "temp");
@@ -1387,6 +1425,14 @@ app.registerExtension({
nodeType.prototype.onRemoved = function (this: ComfyNode) {
log.info(`Cleaning up canvas node ${this.id}`);
// Clean up temp file tracker for this node (just remove from tracker)
const nodeKey = `node-${this.id}`;
const tempFileTracker = (window as any).layerForgeTempFileTracker;
if (tempFileTracker && tempFileTracker.has(nodeKey)) {
tempFileTracker.delete(nodeKey);
log.debug(`Removed temp file tracker for node ${this.id}`);
}
canvasNodeInstances.delete(this.id);
log.info(`Deregistered CanvasNode instance for ID: ${this.id}`);

View File

@@ -2,4 +2,4 @@ import { LogLevel } from "./logger";
// Log level for development.
// Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'
export const LOG_LEVEL: keyof typeof LogLevel = 'DEBUG';
export const LOG_LEVEL: keyof typeof LogLevel = 'NONE';