Files
ryanontheinside-ComfyUI_Rea…/tests/test_profiling.py
2025-01-01 11:18:07 -05:00

297 lines
12 KiB
Python

import cProfile
import pstats
import numpy as np
import cv2
import sys
import os
import json
import time
import torch
from functools import wraps
from pathlib import Path
import line_profiler
import tracemalloc
from io import StringIO
# Add parent directory to path to import our modules
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(project_root)
# Import our modules
from detectors.detectors import MotionDetector, BrightnessDetector
from detectors.detector_nodes import MotionDetectorNode, BrightnessDetectorNode, RegionOfInterest
from controls.detector_controls import FloatDetectionControl, IntDetectionControl, StringDetectionControl
from controls.sequence_controls import FloatSequence
from base.detector_base import SharedProcessing
class ProfilingResults:
def __init__(self, output_dir="profiling_results"):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
self.results = {
"timing": {},
"memory": {},
"line_profiles": {},
"summary": {}
}
def add_timing(self, section, data):
self.results["timing"][section] = data
def add_memory(self, section, data):
self.results["memory"][section] = data
def add_line_profile(self, section, stats):
# Capture line profiler output
output = StringIO()
stats.print_stats(output)
self.results["line_profiles"][section] = output.getvalue()
def add_summary(self, section, data):
self.results["summary"][section] = data
def save(self):
# Save JSON-compatible data
with open(self.output_dir / "profiling_summary.json", "w") as f:
summary_data = {
"timing": self.results["timing"],
"memory": self.results["memory"],
"summary": self.results["summary"]
}
json.dump(summary_data, f, indent=2)
# Save detailed line profiles
with open(self.output_dir / "line_profiles.txt", "w") as f:
for section, profile in self.results["line_profiles"].items():
f.write(f"\n{'='*80}\n")
f.write(f"Profile for: {section}\n")
f.write(f"{'='*80}\n")
f.write(profile)
def create_test_images(size=(512, 512)):
"""Create test images that will exercise different code paths"""
# Create RGB test images (HWC format)
images = {
"uniform": np.ones((*size, 3), dtype=np.uint8) * 128, # 3-channel RGB
"random": np.random.randint(0, 255, (*size, 3), dtype=np.uint8), # 3-channel RGB
"moving": []
}
# Create sequence of images with simulated motion
base = np.zeros((*size, 3), dtype=np.uint8) # 3-channel RGB
for i in range(30):
frame = base.copy()
x1 = int(size[1] * (i / 30))
x2 = int(size[1] * ((30-i) / 30))
# Draw in RGB order since we're not using cv2.imshow
cv2.rectangle(frame, (x1, 100), (x1 + 50, 150), (255, 255, 255), -1) # White
cv2.rectangle(frame, (x2, 300), (x2 + 50, 350), (128, 128, 128), -1) # Gray
cv2.circle(frame, (x1, x2), 30, (200, 200, 200), -1) # Light gray
images["moving"].append(frame)
# Debug first frame
print(f"First moving frame shape: {images['moving'][0].shape}")
print(f"First moving frame dtype: {images['moving'][0].dtype}")
print(f"First moving frame range: {images['moving'][0].min()}-{images['moving'][0].max()}")
print(f"First moving frame channels: {images['moving'][0][0,0]}") # Show first pixel's channels
return images
class CodeProfiler:
def __init__(self):
self.line_profiler = line_profiler.LineProfiler()
self.test_images = create_test_images()
self.iterations = 1000
self.results = ProfilingResults()
# Set up line profiling for critical functions
self.setup_profiling()
def setup_profiling(self):
"""Set up line profiling for critical code paths"""
# Core detection logic
self.line_profiler.add_function(MotionDetector.detect)
self.line_profiler.add_function(BrightnessDetector.detect)
self.line_profiler.add_function(SharedProcessing.get_shared_data)
# Control node processing
self.line_profiler.add_function(FloatDetectionControl.process_detection_base)
self.line_profiler.add_function(FloatDetectionControl._process_action)
# ROI processing
self.line_profiler.add_function(RegionOfInterest.update)
def profile_detector_nodes(self):
"""Profile detector node operations"""
print(f"Profiling detector nodes...")
# Create test inputs that mimic ComfyUI node inputs
image = self.test_images["moving"][0]
print(f"Original image shape: {image.shape}")
print(f"Original image dtype: {image.dtype}")
print(f"Original image range: {image.min()}-{image.max()}")
print(f"Original image channels: {image[0,0]}") # Show first pixel's channels
# ComfyUI IMAGE inputs are BHWC format, float32, range [0,1]
image_tensor = torch.from_numpy(image).float() / 255.0 # Keep HWC format
print(f"Before batch dim: {image_tensor.shape}")
print(f"Before batch dtype: {image_tensor.dtype}")
print(f"Before batch range: {image_tensor.min()}-{image_tensor.max()}")
print(f"Before batch channels: {image_tensor[0,0]}")
image_tensor = image_tensor.unsqueeze(0) # Add batch dimension -> BHWC
print(f"After batch dim: {image_tensor.shape}")
print(f"After batch dtype: {image_tensor.dtype}")
print(f"After batch range: {image_tensor.min()}-{image_tensor.max()}")
print(f"After batch channels: {image_tensor[0,0,0]}")
# Debug numpy conversion
test_frame = (image_tensor[0] * 255).cpu().numpy().astype(np.uint8)
print(f"Test numpy conversion shape: {test_frame.shape}")
print(f"Test numpy conversion dtype: {test_frame.dtype}")
print(f"Test numpy conversion range: {test_frame.min()}-{test_frame.max()}")
print(f"Test numpy conversion channels: {test_frame[0,0]}") # Show first pixel's channels
# Create mask tensor - ComfyUI expects BHW format
mask = np.ones((512, 512), dtype=np.float32)
mask_tensor = torch.from_numpy(mask).unsqueeze(0) # Add batch dimension -> BHW
# Profile motion detector node
motion_node = MotionDetectorNode()
motion_detector = motion_node.update(threshold=0.1, blur_size=5, always_execute=True)[0]
# Profile brightness detector node
brightness_node = BrightnessDetectorNode()
brightness_detector = brightness_node.update(threshold=0.5, use_average=True, always_execute=True)[0]
# Create ROI chain
roi_node = RegionOfInterest()
roi_inputs = {
"mask": mask_tensor,
"detector": motion_detector,
"action": "add",
"value": 0.1,
"always_execute": True
}
roi_chain = roi_node.update(**roi_inputs)[0]
# Profile detection controls
controls = [
(FloatDetectionControl(), "float_control", {
"minimum_value": 0.0,
"maximum_value": 1.0,
"starting_value": 0.5
})
]
self.line_profiler.enable()
tracemalloc.start()
timing_data = {}
for control, name, params in controls:
start_time = time.perf_counter()
for _ in range(self.iterations):
inputs = {
"image": image_tensor,
"roi_chain": roi_chain,
"always_execute": True,
**params
}
control.update(**inputs)
timing_data[name] = time.perf_counter() - start_time
snapshot = tracemalloc.take_snapshot()
tracemalloc.stop()
self.line_profiler.disable()
# Save results
self.results.add_timing("detector_nodes", timing_data)
self.results.add_memory("detector_nodes", [str(stat) for stat in snapshot.statistics('lineno')[:10]])
self.results.add_line_profile("detector_nodes", self.line_profiler)
def profile_core_detection(self):
"""Profile core detection algorithms"""
print(f"Profiling core detection...")
motion_detector = MotionDetector()
motion_detector.setup(threshold=0.1, blur_size=5)
brightness_detector = BrightnessDetector()
brightness_detector.setup(threshold=0.5, use_average=True)
mask = np.ones((512, 512), dtype=np.float32)
self.line_profiler.enable()
tracemalloc.start()
timing_data = {"motion": 0, "brightness": 0}
# Profile motion detection
start_time = time.perf_counter()
for _ in range(self.iterations // len(self.test_images["moving"])):
state = {}
for i in range(len(self.test_images["moving"]) - 1):
current = self.test_images["moving"][i]
# Convert to RGB for OpenCV
current = cv2.cvtColor(current, cv2.COLOR_BGR2RGB)
shared_data = SharedProcessing.get_shared_data(current)
preprocessed = motion_detector.preprocess(current, shared_data)
state = {
"preprocessed": preprocessed,
"shared": shared_data,
"y_offset": 0,
"x_offset": 0
}
motion_detector.detect(current, mask, state)
timing_data["motion"] = time.perf_counter() - start_time
# Profile brightness detection
start_time = time.perf_counter()
for img_type in ["uniform", "random"]:
image = self.test_images[img_type]
# Convert to RGB for OpenCV
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
for _ in range(self.iterations // 2):
shared_data = SharedProcessing.get_shared_data(image)
preprocessed = brightness_detector.preprocess(image, shared_data)
state = {
"preprocessed": preprocessed,
"shared": shared_data,
"y_offset": 0,
"x_offset": 0
}
brightness_detector.detect(image, mask, state)
timing_data["brightness"] = time.perf_counter() - start_time
snapshot = tracemalloc.take_snapshot()
tracemalloc.stop()
self.line_profiler.disable()
# Save results
self.results.add_timing("core_detection", timing_data)
self.results.add_memory("core_detection", [str(stat) for stat in snapshot.statistics('lineno')[:10]])
self.results.add_line_profile("core_detection", self.line_profiler)
def run_profiling(self):
"""Run all profiling tests"""
start_time = time.time()
self.profile_core_detection()
self.profile_detector_nodes()
total_time = time.time() - start_time
self.results.add_summary("execution", {
"total_time": total_time,
"iterations": self.iterations,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
})
# Save all results
self.results.save()
print(f"\nProfiling complete. Results saved to {self.results.output_dir}/")
if __name__ == "__main__":
profiler = CodeProfiler()
profiler.run_profiling()