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:
Will Miao
2026-02-23 14:00:00 +08:00
parent ec76ac649b
commit 26b139884c
3 changed files with 93 additions and 24 deletions

View File

@@ -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()

View File

@@ -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 };

View File

@@ -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);
} }