mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 23:25:43 -03:00
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
This commit is contained in:
@@ -57,6 +57,9 @@ class UsageStats:
|
|||||||
"last_save_time": 0
|
"last_save_time": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Track if stats have been modified since last save
|
||||||
|
self._is_dirty = False
|
||||||
|
|
||||||
# Queue for prompt_ids to process
|
# Queue for prompt_ids to process
|
||||||
self.pending_prompt_ids = set()
|
self.pending_prompt_ids = set()
|
||||||
|
|
||||||
@@ -180,27 +183,39 @@ class UsageStats:
|
|||||||
async def save_stats(self, force=False):
|
async def save_stats(self, force=False):
|
||||||
"""Save statistics to file"""
|
"""Save statistics to file"""
|
||||||
try:
|
try:
|
||||||
# Only save if it's been at least save_interval since last save or force is True
|
# Only save if:
|
||||||
|
# 1. force is True, OR
|
||||||
|
# 2. stats have been modified (is_dirty) AND save_interval has passed
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
if not force and (current_time - self.stats.get("last_save_time", 0)) < self.save_interval:
|
time_since_last_save = current_time - self.stats.get("last_save_time", 0)
|
||||||
return False
|
|
||||||
|
if not force:
|
||||||
|
if not self._is_dirty:
|
||||||
|
# No changes to save
|
||||||
|
return False
|
||||||
|
if time_since_last_save < self.save_interval:
|
||||||
|
# Too soon since last save
|
||||||
|
return False
|
||||||
|
|
||||||
# Use a lock to prevent concurrent writes
|
# Use a lock to prevent concurrent writes
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
# Update last save time
|
# Update last save time
|
||||||
self.stats["last_save_time"] = current_time
|
self.stats["last_save_time"] = current_time
|
||||||
|
|
||||||
# Create directory if it doesn't exist
|
# Create directory if it doesn't exist
|
||||||
os.makedirs(os.path.dirname(self._stats_file_path), exist_ok=True)
|
os.makedirs(os.path.dirname(self._stats_file_path), exist_ok=True)
|
||||||
|
|
||||||
# Write to a temporary file first, then move it to avoid corruption
|
# Write to a temporary file first, then move it to avoid corruption
|
||||||
temp_path = f"{self._stats_file_path}.tmp"
|
temp_path = f"{self._stats_file_path}.tmp"
|
||||||
with open(temp_path, 'w', encoding='utf-8') as f:
|
with open(temp_path, 'w', encoding='utf-8') as f:
|
||||||
json.dump(self.stats, f, indent=2, ensure_ascii=False)
|
json.dump(self.stats, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
# Replace the old file with the new one
|
# Replace the old file with the new one
|
||||||
os.replace(temp_path, self._stats_file_path)
|
os.replace(temp_path, self._stats_file_path)
|
||||||
|
|
||||||
|
# Clear dirty flag since we've saved
|
||||||
|
self._is_dirty = False
|
||||||
|
|
||||||
logger.debug(f"Saved usage statistics to {self._stats_file_path}")
|
logger.debug(f"Saved usage statistics to {self._stats_file_path}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -218,25 +233,32 @@ class UsageStats:
|
|||||||
while True:
|
while True:
|
||||||
# Wait a short interval before checking for new prompt_ids
|
# Wait a short interval before checking for new prompt_ids
|
||||||
await asyncio.sleep(5) # Check every 5 seconds
|
await asyncio.sleep(5) # Check every 5 seconds
|
||||||
|
|
||||||
# Process any pending prompt_ids
|
# Process any pending prompt_ids
|
||||||
if self.pending_prompt_ids:
|
if self.pending_prompt_ids:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
# Get a copy of the set and clear original
|
# Get a copy of the set and clear original
|
||||||
prompt_ids = self.pending_prompt_ids.copy()
|
prompt_ids = self.pending_prompt_ids.copy()
|
||||||
self.pending_prompt_ids.clear()
|
self.pending_prompt_ids.clear()
|
||||||
|
|
||||||
# Process each prompt_id
|
# Process each prompt_id
|
||||||
registry = MetadataRegistry()
|
try:
|
||||||
for prompt_id in prompt_ids:
|
registry = MetadataRegistry()
|
||||||
try:
|
except NameError:
|
||||||
metadata = registry.get_metadata(prompt_id)
|
# MetadataRegistry not available (standalone mode)
|
||||||
await self._process_metadata(metadata)
|
registry = None
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error processing prompt_id {prompt_id}: {e}")
|
if registry:
|
||||||
|
for prompt_id in prompt_ids:
|
||||||
# Periodically save stats
|
try:
|
||||||
await self.save_stats()
|
metadata = registry.get_metadata(prompt_id)
|
||||||
|
await self._process_metadata(metadata)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing prompt_id {prompt_id}: {e}")
|
||||||
|
|
||||||
|
# Periodically save stats (only if there are changes)
|
||||||
|
if self._is_dirty:
|
||||||
|
await self.save_stats()
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
# Task was cancelled, clean up
|
# Task was cancelled, clean up
|
||||||
await self.save_stats(force=True)
|
await self.save_stats(force=True)
|
||||||
@@ -254,9 +276,10 @@ class UsageStats:
|
|||||||
"""Process metadata from an execution"""
|
"""Process metadata from an execution"""
|
||||||
if not metadata or not isinstance(metadata, dict):
|
if not metadata or not isinstance(metadata, dict):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Increment total executions count
|
# Increment total executions count
|
||||||
self.stats["total_executions"] += 1
|
self.stats["total_executions"] += 1
|
||||||
|
self._is_dirty = True
|
||||||
|
|
||||||
# Get today's date in YYYY-MM-DD format
|
# Get today's date in YYYY-MM-DD format
|
||||||
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
@@ -373,7 +396,11 @@ class UsageStats:
|
|||||||
"""Process a prompt execution immediately (synchronous approach)"""
|
"""Process a prompt execution immediately (synchronous approach)"""
|
||||||
if not prompt_id:
|
if not prompt_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if standalone_mode:
|
||||||
|
# Usage statistics are not available in standalone mode
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Process metadata for this prompt_id
|
# Process metadata for this prompt_id
|
||||||
registry = MetadataRegistry()
|
registry = MetadataRegistry()
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ const PROMPT_TAG_AUTOCOMPLETE_DEFAULT = true;
|
|||||||
const TAG_SPACE_REPLACEMENT_SETTING_ID = "loramanager.tag_space_replacement";
|
const TAG_SPACE_REPLACEMENT_SETTING_ID = "loramanager.tag_space_replacement";
|
||||||
const TAG_SPACE_REPLACEMENT_DEFAULT = false;
|
const TAG_SPACE_REPLACEMENT_DEFAULT = false;
|
||||||
|
|
||||||
|
const USAGE_STATISTICS_SETTING_ID = "loramanager.usage_statistics";
|
||||||
|
const USAGE_STATISTICS_DEFAULT = true;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -124,6 +127,32 @@ const getTagSpaceReplacementPreference = (() => {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const getUsageStatisticsPreference = (() => {
|
||||||
|
let settingsUnavailableLogged = false;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const settingManager = app?.extensionManager?.setting;
|
||||||
|
if (!settingManager || typeof settingManager.get !== "function") {
|
||||||
|
if (!settingsUnavailableLogged) {
|
||||||
|
console.warn("LoRA Manager: settings API unavailable, using default usage statistics setting.");
|
||||||
|
settingsUnavailableLogged = true;
|
||||||
|
}
|
||||||
|
return USAGE_STATISTICS_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = settingManager.get(USAGE_STATISTICS_SETTING_ID);
|
||||||
|
return value ?? USAGE_STATISTICS_DEFAULT;
|
||||||
|
} catch (error) {
|
||||||
|
if (!settingsUnavailableLogged) {
|
||||||
|
console.warn("LoRA Manager: unable to read usage statistics setting, using default.", error);
|
||||||
|
settingsUnavailableLogged = true;
|
||||||
|
}
|
||||||
|
return USAGE_STATISTICS_DEFAULT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Register Extension with All Settings
|
// Register Extension with All Settings
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -168,6 +197,14 @@ app.registerExtension({
|
|||||||
tooltip: "When enabled, tag names with underscores will have them replaced with spaces when inserted (e.g., 'blonde_hair' becomes 'blonde hair').",
|
tooltip: "When enabled, tag names with underscores will have them replaced with spaces when inserted (e.g., 'blonde_hair' becomes 'blonde hair').",
|
||||||
category: ["LoRA Manager", "Autocomplete", "Tag Formatting"],
|
category: ["LoRA Manager", "Autocomplete", "Tag Formatting"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: USAGE_STATISTICS_SETTING_ID,
|
||||||
|
name: "Enable usage statistics tracking",
|
||||||
|
type: "boolean",
|
||||||
|
defaultValue: USAGE_STATISTICS_DEFAULT,
|
||||||
|
tooltip: "When enabled, LoRA Manager will track model usage statistics during workflow execution. Disabling this will prevent unnecessary disk writes.",
|
||||||
|
category: ["LoRA Manager", "Statistics", "Usage Tracking"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,4 +212,4 @@ app.registerExtension({
|
|||||||
// Exports
|
// Exports
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export { getWheelSensitivity, getAutoPathCorrectionPreference, getPromptTagAutocompletePreference, getTagSpaceReplacementPreference };
|
export { getWheelSensitivity, getAutoPathCorrectionPreference, getPromptTagAutocompletePreference, getTagSpaceReplacementPreference, getUsageStatisticsPreference };
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
import { showToast } from "./utils.js";
|
import { showToast } from "./utils.js";
|
||||||
import { getAutoPathCorrectionPreference } from "./settings.js";
|
import { getAutoPathCorrectionPreference, getUsageStatisticsPreference } from "./settings.js";
|
||||||
|
|
||||||
// Define target nodes and their widget configurations
|
// Define target nodes and their widget configurations
|
||||||
const PATH_CORRECTION_TARGETS = [
|
const PATH_CORRECTION_TARGETS = [
|
||||||
@@ -25,6 +25,11 @@ app.registerExtension({
|
|||||||
setup() {
|
setup() {
|
||||||
// Listen for successful executions
|
// Listen for successful executions
|
||||||
api.addEventListener("execution_success", ({ detail }) => {
|
api.addEventListener("execution_success", ({ detail }) => {
|
||||||
|
// Skip if usage statistics is disabled
|
||||||
|
if (!getUsageStatisticsPreference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (detail && detail.prompt_id) {
|
if (detail && detail.prompt_id) {
|
||||||
this.updateUsageStats(detail.prompt_id);
|
this.updateUsageStats(detail.prompt_id);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user