Skip to content

Commit e1ba34f

Browse files
author
ryanontheinstide
committed
Squashed commit of the following:
This adds profiling of ComfyUI's execution. Disabled by default. commit eb8abf35ac0b0fd5e04781ca7bb0c4af923052e0 Author: ryanontheinstide <[email protected]> Date: Sun Jan 26 13:31:53 2025 -0500 Readme, disable by default commit 32696f9c0742b3f845ac361861a8035ada684ca7 Author: ryanontheinstide <[email protected]> Date: Sun Jan 26 13:01:35 2025 -0500 track more methods commit b6e5912969467986c2736123a0ba249c52a36d09 Author: ryanontheinstide <[email protected]> Date: Sun Jan 26 12:32:34 2025 -0500 working basic execution
1 parent c738081 commit e1ba34f

File tree

5 files changed

+418
-6
lines changed

5 files changed

+418
-6
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,33 @@ ProfilerX integrates directly with ComfyUI's execution system to collect perform
9393
- All data is collected automatically with minimal performance impact
9494
- Historical data is stored locally for trend analysis
9595

96+
## Execution Tracking
97+
98+
In addition to workflow profiling, this extension includes a detailed execution tracking system that monitors ComfyUI's internal method calls. This can be useful for:
99+
- Understanding the execution flow of your workflows
100+
- Identifying bottlenecks in specific operations
101+
- Debugging performance issues
102+
- Analyzing method call patterns and timing
103+
104+
### Enabling Execution Tracking
105+
106+
By default, execution tracking is disabled. To enable it:
107+
108+
1. Open `ComfyUI_ProfilerX/execution_core.py`
109+
2. Find the `ENABLED` flag at the top of the `ExecutionTracker` class:
110+
```python
111+
class ExecutionTracker:
112+
_instance = None
113+
_lock = threading.Lock()
114+
ENABLED = False # Change this to True to enable tracking
115+
```
116+
3. Change `ENABLED = False` to `ENABLED = True`
117+
4. Restart ComfyUI
118+
119+
When enabled, the tracker will record detailed timing information for internal ComfyUI operations in `ComfyUI_ProfilerX/data/method_traces.json`.
120+
121+
122+
96123
## Other Projects by RyanOnTheInside
97124

98125
Check out my other ComfyUI custom nodes:

__init__.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
1616

1717
logger.info("Initializing ComfyUI-ProfilerX...")
1818

19-
from .prestartup import inject_profiling, PROFILER_ENABLED # Import but don't auto-inject
19+
from .prestartup import inject_profiling, PROFILER_ENABLED, inject_tracking # Import but don't auto-inject
2020
from . import server # Register API routes
21+
from .execution_core import ExecutionTracker
2122

2223
# Set up web directory
2324
WEB_DIRECTORY = "./web"
2425

2526
# Try to inject profiling hooks
26-
if not inject_profiling():
27+
profiler_enabled = inject_profiling()
28+
if not profiler_enabled:
2729
# If injection fails, remove the profiler node from available nodes
2830
logger.warning("Disabling profiler nodes due to initialization failure")
2931
NODE_CLASS_MAPPINGS = {}
@@ -44,12 +46,20 @@
4446
"ProfilerX": "ProfilerX"
4547
}
4648

49+
# Try to inject execution tracking hooks
50+
execution_tracking_enabled = inject_tracking() if ExecutionTracker.ENABLED else False
51+
if execution_tracking_enabled:
52+
logger.info("Execution tracking is enabled")
53+
else:
54+
logger.warning("Execution tracking is disabled")
55+
4756
def setup_js():
4857
"""Register web extension with profiler status"""
49-
logger.debug(f"Setting up web extension (enabled={PROFILER_ENABLED})")
58+
logger.debug(f"Setting up web extension (profiler_enabled={PROFILER_ENABLED}, execution_tracking_enabled={execution_tracking_enabled})")
5059
return {
5160
"name": "ComfyUI-ProfilerX",
5261
"module": "index.js",
5362
"enabled": PROFILER_ENABLED,
54-
"status": "active" if PROFILER_ENABLED else "disabled"
63+
"status": "active" if PROFILER_ENABLED else "disabled",
64+
"execution_tracking": execution_tracking_enabled
5565
}

execution_core.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Core execution tracking for ComfyUI's internal methods"""
2+
import logging
3+
import time
4+
import json
5+
import os
6+
import threading
7+
from typing import Dict, List, Optional
8+
from collections import defaultdict
9+
import functools
10+
11+
logger = logging.getLogger('ComfyUI-ExecutionTracker')
12+
logger.setLevel(logging.ERROR)
13+
14+
class ExecutionTracker:
15+
_instance = None
16+
_lock = threading.Lock()
17+
ENABLED = False #this is a global variable that controls whether the execution tracker is enabled or not
18+
19+
def __init__(self):
20+
self.data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data")
21+
os.makedirs(self.data_dir, exist_ok=True)
22+
self.trace_file = os.path.join(self.data_dir, "method_traces.json")
23+
24+
# Load existing traces if any
25+
self.traces = self._load_traces()
26+
27+
# Track current execution
28+
self.call_stack = []
29+
self.current_execution = None
30+
31+
def _load_traces(self) -> Dict:
32+
"""Load existing traces from file"""
33+
if os.path.exists(self.trace_file):
34+
try:
35+
with open(self.trace_file, 'r') as f:
36+
return json.load(f)
37+
except Exception as e:
38+
logger.error(f"Failed to load traces: {e}")
39+
return {
40+
"executions": [],
41+
"method_stats": defaultdict(lambda: {
42+
"total_calls": 0,
43+
"total_time": 0,
44+
"min_time": float('inf'),
45+
"max_time": 0,
46+
"avg_time": 0
47+
})
48+
}
49+
50+
def _save_traces(self):
51+
"""Save traces to file"""
52+
try:
53+
with open(self.trace_file, 'w') as f:
54+
# Convert defaultdict to regular dict for JSON serialization
55+
traces_copy = {
56+
"executions": self.traces["executions"],
57+
"method_stats": {
58+
k: dict(v) for k, v in self.traces["method_stats"].items()
59+
}
60+
}
61+
json.dump(traces_copy, f, indent=2)
62+
except Exception as e:
63+
logger.error(f"Failed to save traces: {e}")
64+
65+
@classmethod
66+
def get_instance(cls) -> 'ExecutionTracker':
67+
if cls._instance is None:
68+
with cls._lock:
69+
if cls._instance is None:
70+
cls._instance = cls()
71+
return cls._instance
72+
73+
@classmethod
74+
def enable(cls):
75+
"""Enable execution tracking"""
76+
cls.ENABLED = True
77+
logger.info("ComfyUI method call tracking enabled")
78+
79+
@classmethod
80+
def disable(cls):
81+
"""Disable execution tracking"""
82+
cls.ENABLED = False
83+
logger.info("ComfyUI method call tracking disabled")
84+
85+
def start_execution(self, prompt_id: str):
86+
"""Start tracking a new execution"""
87+
if not self.ENABLED:
88+
return
89+
90+
with self._lock:
91+
self.current_execution = {
92+
"prompt_id": prompt_id,
93+
"start_time": time.time() * 1000,
94+
"method_calls": [],
95+
"total_time": 0
96+
}
97+
98+
def end_execution(self):
99+
"""End tracking current execution"""
100+
if not self.ENABLED or not self.current_execution:
101+
return
102+
103+
with self._lock:
104+
self.current_execution["end_time"] = time.time() * 1000
105+
self.current_execution["total_time"] = (
106+
self.current_execution["end_time"] - self.current_execution["start_time"]
107+
)
108+
self.traces["executions"].append(self.current_execution)
109+
self._save_traces()
110+
self.current_execution = None
111+
112+
def track_method_call(self, method_name: str, class_name: str = None):
113+
"""Decorator to track method execution time"""
114+
def decorator(func):
115+
@functools.wraps(func)
116+
def wrapper(*args, **kwargs):
117+
if not self.ENABLED:
118+
return func(*args, **kwargs)
119+
120+
full_name = f"{class_name}.{method_name}" if class_name else method_name
121+
start_time = time.time() * 1000
122+
123+
try:
124+
with self._lock:
125+
self.call_stack.append(full_name)
126+
127+
result = func(*args, **kwargs)
128+
129+
return result
130+
finally:
131+
end_time = time.time() * 1000
132+
duration = end_time - start_time
133+
134+
with self._lock:
135+
# Pop from call stack
136+
if self.call_stack:
137+
self.call_stack.pop()
138+
139+
# Update method stats
140+
stats = self.traces["method_stats"][full_name]
141+
stats["total_calls"] += 1
142+
stats["total_time"] += duration
143+
stats["min_time"] = min(stats.get("min_time", float('inf')), duration)
144+
stats["max_time"] = max(stats.get("max_time", 0), duration)
145+
stats["avg_time"] = stats["total_time"] / stats["total_calls"]
146+
147+
# Record call in current execution with enhanced context
148+
if self.current_execution:
149+
# Get queue size if available
150+
queue_size = None
151+
try:
152+
import execution
153+
if hasattr(execution, 'PromptServer') and hasattr(execution.PromptServer.instance, 'prompt_queue'):
154+
queue = execution.PromptServer.instance.prompt_queue.queue
155+
queue_size = len(queue) if queue else 0
156+
except:
157+
pass
158+
159+
# Determine if operation was cached
160+
is_cache_hit = False
161+
if 'caches' in kwargs and 'current_item' in kwargs:
162+
try:
163+
is_cache_hit = kwargs['caches'].outputs.get(kwargs['current_item']) is not None
164+
except:
165+
pass
166+
167+
call_info = {
168+
"method": full_name,
169+
"start_time": start_time,
170+
"duration": duration,
171+
"stack_depth": len(self.call_stack) + 1, # +1 since we already popped
172+
"parent": self.call_stack[-1] if self.call_stack else None,
173+
"queue_size": queue_size,
174+
"is_cache_hit": is_cache_hit
175+
}
176+
self.current_execution["method_calls"].append(call_info)
177+
return wrapper
178+
return decorator
179+
180+
def get_method_stats(self) -> Dict:
181+
"""Get statistics for all tracked methods"""
182+
return dict(self.traces["method_stats"]) if self.ENABLED else {}

0 commit comments

Comments
 (0)