Files
Shan2L-ComfyUI-Simple-Profiler/exec_timer.py
T

376 lines
11 KiB
Python

import time
import server
import execution
import inspect
import os
import json
from aiohttp import web
import csv
import torch
API_PATH = "/exec-timer/stat"
ENABLE_CSV_GEN = os.environ.get("EXEC_TIMER_ENABLE_CSV_GEN", True)
TIME_PRECISION = int(os.environ.get("EXEC_TIMER_TIME_PRECISION", "2"))
OUTPUT_PATH = os.path.join(os.path.dirname(__file__), "outputs")
API_ALLOW_ORIGIN = "*"
NODE_STATS = {}
WORKFLOW_INFO = {
"start_time": None,
"end_time": None,
"prompt_id": None,
"is_running": False,
"peak_memory_usage": None,
}
GLOBAL_STEP = 1
def init_stats(prompt_id: str):
global NODE_STATS, WORKFLOW_INFO, GLOBAL_STEP
NODE_STATS = {}
WORKFLOW_INFO = {
"start_time": time.perf_counter(),
"end_time": None,
"prompt_id": prompt_id,
"is_running": True,
"peak_memory_usage": None,
}
GLOBAL_STEP = 1
torch.cuda.reset_max_memory_allocated()
print(
f"\n[Exec_Timer] Workflow begins, prompt_id: {prompt_id}, initialize status..."
)
def reset_stats():
global NODE_STATS, WORKFLOW_INFO, GLOBAL_STEP
NODE_STATS = {}
WORKFLOW_INFO = {
"start_time": None,
"end_time": None,
"prompt_id": None,
"is_running": False,
"peak_memory_usage": None,
}
GLOBAL_STEP = 1
torch.cuda.reset_max_memory_allocated()
def get_accurate_time(raw_time: float, precision: int):
if precision < 0:
raise ValueError("The precision can't be smaller than zero.")
if precision == 0:
return int(raw_time)
else:
return round(raw_time, TIME_PRECISION)
def generate_csv_file(prompt_id: str):
global WORKFLOW_INFO, NODE_STATS, OUTPUT_PATH
try:
prompt_id = WORKFLOW_INFO["prompt_id"]
csv_filepath = os.path.join(OUTPUT_PATH, f"{prompt_id}.csv")
with open(csv_filepath, "w") as file:
csv_writer = csv.writer(file, delimiter=" ")
csv_writer.writerow(
["GLOBAL_STEP", "CLASS TYPE", "Execution Time(ms)", "Unique ID"]
)
for global_step, node_stat in NODE_STATS.items():
csv_writer.writerow(
[
global_step,
node_stat["class_type"],
node_stat["exec_time_ms"],
node_stat["unique_id"],
]
)
workflow_execution_time_diff = (
WORKFLOW_INFO["end_time"] - WORKFLOW_INFO["start_time"]
) * 1000
workflow_execution_time_ms = get_accurate_time(
workflow_execution_time_diff, TIME_PRECISION
)
csv_writer.writerow(
["Total Execution Time(ms)", workflow_execution_time_ms]
)
csv_writer.writerow(
["Peak Memory Usage(Bytes)", WORKFLOW_INFO["peak_memory_usage"]]
)
print(f"The csv file has been generated: {csv_filepath}")
except Exception as e:
print(f"[Exec_Timer] Failed to save csv: {str(e)}")
def generate_json_file(prompt_id: str):
global NODE_STATS, WORKFLOW_INFO, OUTPUT_PATH
node_stats_with_mem = NODE_STATS.copy()
workflow_execution_time_diff = (
WORKFLOW_INFO["end_time"] - WORKFLOW_INFO["start_time"]
) * 1000
workflow_execution_time_ms = get_accurate_time(
workflow_execution_time_diff, TIME_PRECISION
)
node_stats_with_mem["peak_memory_usage"] = WORKFLOW_INFO["peak_memory_usage"]
node_stats_with_mem["total_time"] = workflow_execution_time_ms
try:
prompt_id = WORKFLOW_INFO["prompt_id"]
json_filepath = os.path.join(OUTPUT_PATH, f"{prompt_id}.json")
with open(json_filepath, "w") as file:
json.dump(node_stats_with_mem, file)
print(f"The json file has been generated: {json_filepath}")
except Exception as e:
print(f"[Exec_Timer] Failed to save json: {str(e)}")
async def send_workflow_timer_record(request):
global NODE_EXEC_LOG, WORKFLOW_INFO, OUTPUT_PATH
post_data = await request.json()
prompt_id = post_data.get("prompt_id")
if prompt_id == WORKFLOW_INFO["prompt_id"] and WORKFLOW_INFO["is_running"]:
return_code = 200
return_msg = "Succeed to find the target prompt, but it is still running."
node_stats = {}
else:
try:
json_filepath = os.path.join(OUTPUT_PATH, f"{prompt_id}.json")
with open(json_filepath, "r") as file:
node_stats = json.load(file)
return_code = 200
return_msg = "Succeed to get the workflow execution infomation."
except Exception as e:
print(f"Failed to open {json_filepath}: {str(e)}")
return_code = 500
return_msg = f"Failed to get status info for {prompt_id}"
node_stats = {}
response_data = {
"code": return_code,
"msg": return_msg,
"prompt_id": prompt_id,
"data": node_stats,
"timestamp": int(time.time() * 1000),
}
headers = {
"Access-Control-Allow-Origin": API_ALLOW_ORIGIN,
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
}
return web.json_response(response_data, headers=headers)
def register_api_route():
try:
server.PromptServer.instance.app.add_routes(
[
# web.get(API_PATH, get_workflow_timer_record),
web.post(API_PATH, send_workflow_timer_record),
# web.options(API_PATH, get_workflow_timer_record)
]
)
print(
f"[Exec_Timer] API registered | access address: http://0.0.0.0:8188{API_PATH}"
)
except Exception as e:
print(f"[Exec_Timer] Failed to register API: {str(e)}")
origin_execute = execution.execute
# async
if inspect.iscoroutinefunction(origin_execute):
async def hooked_execute(
server_inst,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
pending_async_nodes,
*args,
**kwargs,
):
global NODE_STATS, WORKFLOW_INFO, GLOBAL_STEP
unique_id = current_item
if not WORKFLOW_INFO["is_running"]:
init_stats(prompt_id)
try:
node_info = dynprompt.get_node(unique_id)
class_type = node_info.get("class_type", "Unknown Class")
display_id = dynprompt.get_display_node_id(unique_id)
except Exception:
class_type = "Unknown Class"
display_id = "Unknown Id"
node_start_time = time.perf_counter()
result = await origin_execute(
server_inst,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
pending_async_nodes,
*args,
**kwargs,
)
# Caculate the execution time
node_end_time = time.perf_counter()
exec_time_ms = (node_end_time - node_start_time) * 1000
exec_time_ms = get_accurate_time(exec_time_ms, TIME_PRECISION)
NODE_STATS[str(GLOBAL_STEP)] = {
"display_id": display_id,
"class_type": class_type,
"start_time": node_start_time,
"exec_time_ms": exec_time_ms,
"unique_id": unique_id,
}
GLOBAL_STEP += 1
print(
f"[Exec_Timer] Node Exec Finished | Class Type{class_type:25s} | Front ID: {display_id:5s} | Execution Time: {exec_time_ms} ms | Unique_id: {unique_id}"
)
return result
else:
def hooked_execute(
server_inst,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
*args,
**kwargs,
):
global NODE_STATS, WORKFLOW_INFO, GLOBAL_STEP
unique_id = current_item
if not WORKFLOW_INFO["is_running"]:
init_stats(prompt_id)
try:
node_info = dynprompt.get_node(unique_id)
class_type = node_info.get("class_type", "Unknown Class")
display_id = dynprompt.get_display_node_id(unique_id)
except Exception:
class_type = "Unknown Class"
display_id = "Unknown ID"
node_start_time = time.perf_counter()
result = origin_execute(
server_inst,
dynprompt,
caches,
current_item,
extra_data,
executed,
prompt_id,
execution_list,
pending_subgraph_results,
*args,
**kwargs,
)
node_end_time = time.perf_counter()
exec_time_ms = (node_end_time - node_start_time) * 1000
exec_time_ms = get_accurate_time(exec_time_ms, TIME_PRECISION)
# 存储统计数据
NODE_STATS[str(GLOBAL_STEP)] = {
"display_id": display_id,
"class_type": class_type,
"start_time": node_start_time,
"exec_time_ms": exec_time_ms,
"unique_id": unique_id,
}
GLOBAL_STEP += 1
print(
f"[Exec_Timer] Node Exec Finished | Class Type: {class_type:25s} | Front ID: {display_id:5s} | Execution Time: {exec_time_ms} ms | Unique_id: {unique_id}"
)
return result
execution.execute = hooked_execute
origin_send_sync = server.PromptServer.send_sync
def hooked_send_sync(self, event, data, sid=None):
global WORKFLOW_INFO, ENABLE_CSV_GEN
origin_send_sync(self, event, data, sid)
# The signal that indicate the workflow ends: event = 'executing' && data.node = None
if (
event == "executing"
and data
and data.get("node") is None
and WORKFLOW_INFO["is_running"]
):
WORKFLOW_INFO["end_time"] = time.perf_counter()
workflow_execution_time_tmsp = (
WORKFLOW_INFO["end_time"] - WORKFLOW_INFO["start_time"]
) * 1000
workflow_execution_time_ms = round(workflow_execution_time_tmsp, TIME_PRECISION)
print(
f"The total excution time of the workflow {WORKFLOW_INFO['prompt_id']} is {workflow_execution_time_ms}"
)
WORKFLOW_INFO["peak_memory_usage"] = torch.cuda.max_memory_allocated()
print(
f"[Exec-Timer] The max memory usage is : {WORKFLOW_INFO['peak_memory_usage']} MB"
)
prompt_id = WORKFLOW_INFO["prompt_id"]
generate_json_file(prompt_id)
if ENABLE_CSV_GEN:
generate_csv_file(prompt_id)
reset_stats()
server.PromptServer.send_sync = hooked_send_sync
register_api_route()
print(
"""
_____________ _____ _____ __
/ ___/ __/ __/ / ___/__ _______ / __/ | /| / /
/ /___\ \/ _/ / /__/ _ \/ __/ -_)\ \ | |/ |/ /
_____\___/___/___/__\___/\___/_/ _\__/___/_|__/|__/___ __
/_ __/ _/ |/ / __/ _ \ / _ \/ / / / / / ___/ _/ |/ /
/ / _/ // /|_/ / _// , _/ / ___/ /__/ /_/ / (_ // // /
/_/ /___/_/ /_/___/_/|_| /_/ /____/\____/\___/___/_/|_/
"""
)
print(
f"[Exec_Timer] The pure backend plugin Exec_Timer load successfully | Generate CSV File:{'True' if ENABLE_CSV_GEN else 'False'} | Time Precision: {TIME_PRECISION}"
)