Files
ryanontheinside-ComfyUI_Pro…/execution_core.py
RyanOnTheInside 4f2151e780 Squashed commit of the following:
This adds profiling of ComfyUI's execution. Disabled by default.
commit eb8abf35ac0b0fd5e04781ca7bb0c4af923052e0
Author: ryanontheinstide <ryanfosdick87@gmail.com>
Date:   Sun Jan 26 13:31:53 2025 -0500

    Readme, disable by default

commit 32696f9c0742b3f845ac361861a8035ada684ca7
Author: ryanontheinstide <ryanfosdick87@gmail.com>
Date:   Sun Jan 26 13:01:35 2025 -0500

    track more methods

commit b6e5912969467986c2736123a0ba249c52a36d09
Author: ryanontheinstide <ryanfosdick87@gmail.com>
Date:   Sun Jan 26 12:32:34 2025 -0500

    working basic  execution
2025-01-26 13:32:36 -05:00

182 lines
7.1 KiB
Python

"""Core execution tracking for ComfyUI's internal methods"""
import logging
import time
import json
import os
import threading
from typing import Dict, List, Optional
from collections import defaultdict
import functools
logger = logging.getLogger('ComfyUI-ExecutionTracker')
logger.setLevel(logging.ERROR)
class ExecutionTracker:
_instance = None
_lock = threading.Lock()
ENABLED = False #this is a global variable that controls whether the execution tracker is enabled or not
def __init__(self):
self.data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
os.makedirs(self.data_dir, exist_ok=True)
self.trace_file = os.path.join(self.data_dir, "method_traces.json")
# Load existing traces if any
self.traces = self._load_traces()
# Track current execution
self.call_stack = []
self.current_execution = None
def _load_traces(self) -> Dict:
"""Load existing traces from file"""
if os.path.exists(self.trace_file):
try:
with open(self.trace_file, 'r') as f:
return json.load(f)
except Exception as e:
logger.error(f"Failed to load traces: {e}")
return {
"executions": [],
"method_stats": defaultdict(lambda: {
"total_calls": 0,
"total_time": 0,
"min_time": float('inf'),
"max_time": 0,
"avg_time": 0
})
}
def _save_traces(self):
"""Save traces to file"""
try:
with open(self.trace_file, 'w') as f:
# Convert defaultdict to regular dict for JSON serialization
traces_copy = {
"executions": self.traces["executions"],
"method_stats": {
k: dict(v) for k, v in self.traces["method_stats"].items()
}
}
json.dump(traces_copy, f, indent=2)
except Exception as e:
logger.error(f"Failed to save traces: {e}")
@classmethod
def get_instance(cls) -> 'ExecutionTracker':
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
@classmethod
def enable(cls):
"""Enable execution tracking"""
cls.ENABLED = True
logger.info("ComfyUI method call tracking enabled")
@classmethod
def disable(cls):
"""Disable execution tracking"""
cls.ENABLED = False
logger.info("ComfyUI method call tracking disabled")
def start_execution(self, prompt_id: str):
"""Start tracking a new execution"""
if not self.ENABLED:
return
with self._lock:
self.current_execution = {
"prompt_id": prompt_id,
"start_time": time.time() * 1000,
"method_calls": [],
"total_time": 0
}
def end_execution(self):
"""End tracking current execution"""
if not self.ENABLED or not self.current_execution:
return
with self._lock:
self.current_execution["end_time"] = time.time() * 1000
self.current_execution["total_time"] = (
self.current_execution["end_time"] - self.current_execution["start_time"]
)
self.traces["executions"].append(self.current_execution)
self._save_traces()
self.current_execution = None
def track_method_call(self, method_name: str, class_name: str = None):
"""Decorator to track method execution time"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not self.ENABLED:
return func(*args, **kwargs)
full_name = f"{class_name}.{method_name}" if class_name else method_name
start_time = time.time() * 1000
try:
with self._lock:
self.call_stack.append(full_name)
result = func(*args, **kwargs)
return result
finally:
end_time = time.time() * 1000
duration = end_time - start_time
with self._lock:
# Pop from call stack
if self.call_stack:
self.call_stack.pop()
# Update method stats
stats = self.traces["method_stats"][full_name]
stats["total_calls"] += 1
stats["total_time"] += duration
stats["min_time"] = min(stats.get("min_time", float('inf')), duration)
stats["max_time"] = max(stats.get("max_time", 0), duration)
stats["avg_time"] = stats["total_time"] / stats["total_calls"]
# Record call in current execution with enhanced context
if self.current_execution:
# Get queue size if available
queue_size = None
try:
import execution
if hasattr(execution, 'PromptServer') and hasattr(execution.PromptServer.instance, 'prompt_queue'):
queue = execution.PromptServer.instance.prompt_queue.queue
queue_size = len(queue) if queue else 0
except:
pass
# Determine if operation was cached
is_cache_hit = False
if 'caches' in kwargs and 'current_item' in kwargs:
try:
is_cache_hit = kwargs['caches'].outputs.get(kwargs['current_item']) is not None
except:
pass
call_info = {
"method": full_name,
"start_time": start_time,
"duration": duration,
"stack_depth": len(self.call_stack) + 1, # +1 since we already popped
"parent": self.call_stack[-1] if self.call_stack else None,
"queue_size": queue_size,
"is_cache_hit": is_cache_hit
}
self.current_execution["method_calls"].append(call_info)
return wrapper
return decorator
def get_method_stats(self) -> Dict:
"""Get statistics for all tracked methods"""
return dict(self.traces["method_stats"]) if self.ENABLED else {}