mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 12:52:10 -03:00
253 lines
8.9 KiB
JavaScript
253 lines
8.9 KiB
JavaScript
import { removeImage, getAllImageIds } from "./db.js";
|
|
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
|
const log = createModuleLogger('ImageReferenceManager');
|
|
export class ImageReferenceManager {
|
|
constructor(canvas) {
|
|
this.canvas = canvas;
|
|
this.imageReferences = new Map(); // imageId -> count
|
|
this.imageLastUsed = new Map(); // imageId -> timestamp
|
|
this.gcInterval = 5 * 60 * 1000; // 5 minut (nieużywane)
|
|
this.maxAge = 30 * 60 * 1000; // 30 minut bez użycia
|
|
this.gcTimer = null;
|
|
this.isGcRunning = false;
|
|
this.operationCount = 0;
|
|
this.operationThreshold = 500; // Uruchom GC po 500 operacjach
|
|
}
|
|
/**
|
|
* Uruchamia automatyczne garbage collection
|
|
*/
|
|
startGarbageCollection() {
|
|
if (this.gcTimer) {
|
|
clearInterval(this.gcTimer);
|
|
}
|
|
this.gcTimer = window.setInterval(() => {
|
|
this.performGarbageCollection();
|
|
}, this.gcInterval);
|
|
log.info("Garbage collection started with interval:", this.gcInterval / 1000, "seconds");
|
|
}
|
|
/**
|
|
* Zatrzymuje automatyczne garbage collection
|
|
*/
|
|
stopGarbageCollection() {
|
|
if (this.gcTimer) {
|
|
clearInterval(this.gcTimer);
|
|
this.gcTimer = null;
|
|
}
|
|
log.info("Garbage collection stopped");
|
|
}
|
|
/**
|
|
* Dodaje referencję do obrazu
|
|
* @param {string} imageId - ID obrazu
|
|
*/
|
|
addReference(imageId) {
|
|
if (!imageId)
|
|
return;
|
|
const currentCount = this.imageReferences.get(imageId) || 0;
|
|
this.imageReferences.set(imageId, currentCount + 1);
|
|
this.imageLastUsed.set(imageId, Date.now());
|
|
log.debug(`Added reference to image ${imageId}, count: ${currentCount + 1}`);
|
|
}
|
|
/**
|
|
* Usuwa referencję do obrazu
|
|
* @param {string} imageId - ID obrazu
|
|
*/
|
|
removeReference(imageId) {
|
|
if (!imageId)
|
|
return;
|
|
const currentCount = this.imageReferences.get(imageId) || 0;
|
|
if (currentCount <= 1) {
|
|
this.imageReferences.delete(imageId);
|
|
log.debug(`Removed last reference to image ${imageId}`);
|
|
}
|
|
else {
|
|
this.imageReferences.set(imageId, currentCount - 1);
|
|
log.debug(`Removed reference to image ${imageId}, count: ${currentCount - 1}`);
|
|
}
|
|
}
|
|
/**
|
|
* Aktualizuje referencje na podstawie aktualnego stanu canvas
|
|
*/
|
|
updateReferences() {
|
|
log.debug("Updating image references...");
|
|
this.imageReferences.clear();
|
|
const usedImageIds = this.collectAllUsedImageIds();
|
|
usedImageIds.forEach(imageId => {
|
|
this.addReference(imageId);
|
|
});
|
|
log.info(`Updated references for ${usedImageIds.size} unique images`);
|
|
}
|
|
/**
|
|
* Zbiera wszystkie używane imageId z różnych źródeł
|
|
* @returns {Set<string>} Zbiór używanych imageId
|
|
*/
|
|
collectAllUsedImageIds() {
|
|
const usedImageIds = new Set();
|
|
this.canvas.layers.forEach((layer) => {
|
|
if (layer.imageId) {
|
|
usedImageIds.add(layer.imageId);
|
|
}
|
|
});
|
|
if (this.canvas.canvasState && this.canvas.canvasState.layersUndoStack) {
|
|
this.canvas.canvasState.layersUndoStack.forEach((layersState) => {
|
|
layersState.forEach((layer) => {
|
|
if (layer.imageId) {
|
|
usedImageIds.add(layer.imageId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
if (this.canvas.canvasState && this.canvas.canvasState.layersRedoStack) {
|
|
this.canvas.canvasState.layersRedoStack.forEach((layersState) => {
|
|
layersState.forEach((layer) => {
|
|
if (layer.imageId) {
|
|
usedImageIds.add(layer.imageId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
log.debug(`Collected ${usedImageIds.size} used image IDs`);
|
|
return usedImageIds;
|
|
}
|
|
/**
|
|
* Znajduje nieużywane obrazy
|
|
* @param {Set<string>} usedImageIds - Zbiór używanych imageId
|
|
* @returns {Promise<string[]>} Lista nieużywanych imageId
|
|
*/
|
|
async findUnusedImages(usedImageIds) {
|
|
try {
|
|
const allImageIds = await getAllImageIds();
|
|
const unusedImages = [];
|
|
const now = Date.now();
|
|
for (const imageId of allImageIds) {
|
|
if (!usedImageIds.has(imageId)) {
|
|
const lastUsed = this.imageLastUsed.get(imageId) || 0;
|
|
const age = now - lastUsed;
|
|
if (age > this.maxAge) {
|
|
unusedImages.push(imageId);
|
|
}
|
|
else {
|
|
log.debug(`Image ${imageId} is unused but too young (age: ${Math.round(age / 1000)}s)`);
|
|
}
|
|
}
|
|
}
|
|
log.debug(`Found ${unusedImages.length} unused images ready for cleanup`);
|
|
return unusedImages;
|
|
}
|
|
catch (error) {
|
|
log.error("Error finding unused images:", error);
|
|
return [];
|
|
}
|
|
}
|
|
/**
|
|
* Czyści nieużywane obrazy
|
|
* @param {string[]} unusedImages - Lista nieużywanych imageId
|
|
*/
|
|
async cleanupUnusedImages(unusedImages) {
|
|
if (unusedImages.length === 0) {
|
|
log.debug("No unused images to cleanup");
|
|
return;
|
|
}
|
|
log.info(`Starting cleanup of ${unusedImages.length} unused images`);
|
|
let cleanedCount = 0;
|
|
let errorCount = 0;
|
|
for (const imageId of unusedImages) {
|
|
try {
|
|
await removeImage(imageId);
|
|
if (this.canvas.imageCache && this.canvas.imageCache.has(imageId)) {
|
|
this.canvas.imageCache.delete(imageId);
|
|
}
|
|
this.imageReferences.delete(imageId);
|
|
this.imageLastUsed.delete(imageId);
|
|
cleanedCount++;
|
|
log.debug(`Cleaned up image: ${imageId}`);
|
|
}
|
|
catch (error) {
|
|
errorCount++;
|
|
log.error(`Error cleaning up image ${imageId}:`, error);
|
|
}
|
|
}
|
|
log.info(`Garbage collection completed: ${cleanedCount} images cleaned, ${errorCount} errors`);
|
|
}
|
|
/**
|
|
* Wykonuje pełne garbage collection
|
|
*/
|
|
async performGarbageCollection() {
|
|
if (this.isGcRunning) {
|
|
log.debug("Garbage collection already running, skipping");
|
|
return;
|
|
}
|
|
this.isGcRunning = true;
|
|
log.info("Starting garbage collection...");
|
|
try {
|
|
this.updateReferences();
|
|
const usedImageIds = this.collectAllUsedImageIds();
|
|
const unusedImages = await this.findUnusedImages(usedImageIds);
|
|
await this.cleanupUnusedImages(unusedImages);
|
|
}
|
|
catch (error) {
|
|
log.error("Error during garbage collection:", error);
|
|
}
|
|
finally {
|
|
this.isGcRunning = false;
|
|
}
|
|
}
|
|
/**
|
|
* Zwiększa licznik operacji i sprawdza czy uruchomić GC
|
|
*/
|
|
incrementOperationCount() {
|
|
this.operationCount++;
|
|
log.debug(`Operation count: ${this.operationCount}/${this.operationThreshold}`);
|
|
if (this.operationCount >= this.operationThreshold) {
|
|
log.info(`Operation threshold reached (${this.operationThreshold}), triggering garbage collection`);
|
|
this.operationCount = 0; // Reset counter
|
|
setTimeout(() => {
|
|
this.performGarbageCollection();
|
|
}, 100);
|
|
}
|
|
}
|
|
/**
|
|
* Resetuje licznik operacji
|
|
*/
|
|
resetOperationCount() {
|
|
this.operationCount = 0;
|
|
log.debug("Operation count reset");
|
|
}
|
|
/**
|
|
* Ustawia próg operacji dla automatycznego GC
|
|
* @param {number} threshold - Nowy próg operacji
|
|
*/
|
|
setOperationThreshold(threshold) {
|
|
this.operationThreshold = Math.max(1, threshold);
|
|
log.info(`Operation threshold set to: ${this.operationThreshold}`);
|
|
}
|
|
/**
|
|
* Ręczne uruchomienie garbage collection
|
|
*/
|
|
async manualGarbageCollection() {
|
|
log.info("Manual garbage collection triggered");
|
|
await this.performGarbageCollection();
|
|
}
|
|
/**
|
|
* Zwraca statystyki garbage collection
|
|
* @returns {GarbageCollectionStats} Statystyki
|
|
*/
|
|
getStats() {
|
|
return {
|
|
trackedImages: this.imageReferences.size,
|
|
totalReferences: Array.from(this.imageReferences.values()).reduce((sum, count) => sum + count, 0),
|
|
isRunning: this.isGcRunning,
|
|
gcInterval: this.gcInterval,
|
|
maxAge: this.maxAge
|
|
};
|
|
}
|
|
/**
|
|
* Czyści wszystkie dane (przy usuwaniu canvas)
|
|
*/
|
|
destroy() {
|
|
this.stopGarbageCollection();
|
|
this.imageReferences.clear();
|
|
this.imageLastUsed.clear();
|
|
log.info("ImageReferenceManager destroyed");
|
|
}
|
|
}
|