This commit is contained in:
SengokuCola
2025-06-22 18:15:34 +08:00
11 changed files with 264 additions and 298 deletions

View File

@@ -151,7 +151,7 @@ class HeartFChatting:
# 存储回调函数
self.on_stop_focus_chat = on_stop_focus_chat
# 初始化性能记录器
# 如果没有指定版本号,则使用全局版本管理器的版本号
actual_version = performance_version or get_hfc_version()
@@ -407,17 +407,17 @@ class HeartFChatting:
+ (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "")
+ processor_time_log
)
# 记录性能数据
try:
action_result = self._current_cycle_detail.loop_plan_info.get('action_result', {})
action_result = self._current_cycle_detail.loop_plan_info.get("action_result", {})
cycle_performance_data = {
"cycle_id": self._current_cycle_detail.cycle_id,
"action_type": action_result.get('action_type', 'unknown'),
"action_type": action_result.get("action_type", "unknown"),
"total_time": self._current_cycle_detail.end_time - self._current_cycle_detail.start_time,
"step_times": cycle_timers.copy(),
"reasoning": action_result.get('reasoning', ''),
"success": self._current_cycle_detail.loop_action_info.get('action_taken', False),
"reasoning": action_result.get("reasoning", ""),
"success": self._current_cycle_detail.loop_action_info.get("action_taken", False),
}
self.performance_logger.record_cycle(cycle_performance_data)
except Exception as perf_e:

View File

@@ -1,8 +1,6 @@
import json
import os
import time
from datetime import datetime
from typing import Dict, List, Any, Optional
from typing import Dict, List, Any
from pathlib import Path
from src.common.logger import get_logger
@@ -11,32 +9,34 @@ logger = get_logger("hfc_performance")
class HFCPerformanceLogger:
"""HFC性能记录管理器"""
# 版本号常量,可在启动时修改
INTERNAL_VERSION = "v1.0.0"
def __init__(self, chat_id: str, version: str = None):
self.chat_id = chat_id
self.version = version or self.INTERNAL_VERSION
self.log_dir = Path("log/hfc_loop")
self.data_dir = Path("data/hfc")
self.session_start_time = datetime.now()
# 确保目录存在
self.log_dir.mkdir(parents=True, exist_ok=True)
self.data_dir.mkdir(parents=True, exist_ok=True)
# 当前会话的日志文件,包含版本号
version_suffix = self.version.replace(".", "_")
self.session_file = self.log_dir / f"{chat_id}_{version_suffix}_{self.session_start_time.strftime('%Y%m%d_%H%M%S')}.json"
self.session_file = (
self.log_dir / f"{chat_id}_{version_suffix}_{self.session_start_time.strftime('%Y%m%d_%H%M%S')}.json"
)
self.current_session_data = []
# 统计数据文件
self.stats_file = self.data_dir / "time.json"
# 初始化时计算历史统计数据
self._update_historical_stats()
def record_cycle(self, cycle_data: Dict[str, Any]):
"""记录单次循环数据"""
try:
@@ -50,112 +50,110 @@ class HFCPerformanceLogger:
"total_time": cycle_data.get("total_time", 0),
"step_times": cycle_data.get("step_times", {}),
"reasoning": cycle_data.get("reasoning", ""),
"success": cycle_data.get("success", False)
"success": cycle_data.get("success", False),
}
# 添加到当前会话数据
self.current_session_data.append(record)
# 立即写入文件(防止数据丢失)
self._write_session_data()
logger.debug(f"记录HFC循环数据: cycle_id={record['cycle_id']}, action={record['action_type']}, time={record['total_time']:.2f}s")
logger.debug(
f"记录HFC循环数据: cycle_id={record['cycle_id']}, action={record['action_type']}, time={record['total_time']:.2f}s"
)
except Exception as e:
logger.error(f"记录HFC循环数据失败: {e}")
def _write_session_data(self):
"""写入当前会话数据到文件"""
try:
with open(self.session_file, 'w', encoding='utf-8') as f:
with open(self.session_file, "w", encoding="utf-8") as f:
json.dump(self.current_session_data, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"写入会话数据失败: {e}")
def _update_historical_stats(self):
"""更新历史统计数据"""
try:
# 读取所有历史会话文件
all_records = []
# 读取当前chat_id的所有历史文件包括不同版本
for file_path in self.log_dir.glob(f"{self.chat_id}_*.json"):
if file_path == self.session_file:
continue # 跳过当前会话文件
try:
with open(file_path, 'r', encoding='utf-8') as f:
with open(file_path, "r", encoding="utf-8") as f:
records = json.load(f)
if isinstance(records, list):
all_records.extend(records)
except Exception as e:
logger.warning(f"读取历史文件 {file_path} 失败: {e}")
if not all_records:
logger.info(f"没有找到 chat_id={self.chat_id} 的历史数据")
return
# 计算统计数据
stats = self._calculate_stats(all_records)
# 更新统计文件
self._update_stats_file(stats)
logger.info(f"更新了 chat_id={self.chat_id} 的历史统计数据,共 {len(all_records)} 条记录")
except Exception as e:
logger.error(f"更新历史统计数据失败: {e}")
def _calculate_stats(self, records: List[Dict[str, Any]]) -> Dict[str, Any]:
"""计算统计数据"""
if not records:
return {}
# 按动作类型分组
action_groups = {}
total_times = []
step_time_totals = {}
for record in records:
action_type = record.get("action_type", "unknown")
total_time = record.get("total_time", 0)
step_times = record.get("step_times", {})
if action_type not in action_groups:
action_groups[action_type] = {
"count": 0,
"total_times": [],
"step_times": {}
}
action_groups[action_type] = {"count": 0, "total_times": [], "step_times": {}}
action_groups[action_type]["count"] += 1
action_groups[action_type]["total_times"].append(total_time)
total_times.append(total_time)
# 记录步骤时间
for step_name, step_time in step_times.items():
if step_name not in action_groups[action_type]["step_times"]:
action_groups[action_type]["step_times"][step_name] = []
action_groups[action_type]["step_times"][step_name].append(step_time)
if step_name not in step_time_totals:
step_time_totals[step_name] = []
step_time_totals[step_name].append(step_time)
# 计算各种平均值和比例
total_records = len(records)
# 整体统计
overall_stats = {
"total_records": total_records,
"avg_total_time": sum(total_times) / len(total_times) if total_times else 0,
"avg_step_times": {}
"avg_step_times": {},
}
# 各步骤平均时间
for step_name, times in step_time_totals.items():
overall_stats["avg_step_times"][step_name] = sum(times) / len(times) if times else 0
# 按动作类型统计
action_stats = {}
for action_type, data in action_groups.items():
@@ -163,76 +161,78 @@ class HFCPerformanceLogger:
"count": data["count"],
"percentage": (data["count"] / total_records) * 100,
"avg_total_time": sum(data["total_times"]) / len(data["total_times"]) if data["total_times"] else 0,
"avg_step_times": {}
"avg_step_times": {},
}
# 该动作各步骤平均时间
for step_name, times in data["step_times"].items():
action_stats[action_type]["avg_step_times"][step_name] = sum(times) / len(times) if times else 0
return {
"chat_id": self.chat_id,
"version": self.version,
"last_updated": datetime.now().isoformat(),
"overall": overall_stats,
"by_action": action_stats
"by_action": action_stats,
}
def _update_stats_file(self, new_stats: Dict[str, Any]):
"""更新统计文件"""
try:
# 读取现有统计数据
existing_stats = {}
if self.stats_file.exists():
with open(self.stats_file, 'r', encoding='utf-8') as f:
with open(self.stats_file, "r", encoding="utf-8") as f:
existing_stats = json.load(f)
# 更新当前chat_id和版本的统计数据
stats_key = f"{self.chat_id}_{self.version}"
existing_stats[stats_key] = new_stats
# 写回文件
with open(self.stats_file, 'w', encoding='utf-8') as f:
with open(self.stats_file, "w", encoding="utf-8") as f:
json.dump(existing_stats, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"更新统计文件失败: {e}")
def get_current_session_stats(self) -> Dict[str, Any]:
"""获取当前会话的统计数据"""
if not self.current_session_data:
return {}
return self._calculate_stats(self.current_session_data)
def finalize_session(self):
"""结束会话,进行最终统计"""
try:
if self.current_session_data:
# 计算当前会话统计数据
current_stats = self._calculate_stats(self.current_session_data)
self._calculate_stats(self.current_session_data)
# 合并历史数据重新计算总体统计
all_records = self.current_session_data[:]
# 读取历史数据
for file_path in self.log_dir.glob(f"{self.chat_id}_*.json"):
if file_path == self.session_file:
continue
try:
with open(file_path, 'r', encoding='utf-8') as f:
with open(file_path, "r", encoding="utf-8") as f:
records = json.load(f)
if isinstance(records, list):
all_records.extend(records)
except Exception as e:
logger.warning(f"读取历史文件 {file_path} 失败: {e}")
# 重新计算总体统计
total_stats = self._calculate_stats(all_records)
self._update_stats_file(total_stats)
logger.info(f"完成会话统计,当前会话 {len(self.current_session_data)} 条记录,总共 {len(all_records)} 条记录")
logger.info(
f"完成会话统计,当前会话 {len(self.current_session_data)} 条记录,总共 {len(all_records)} 条记录"
)
except Exception as e:
logger.error(f"结束会话统计失败: {e}")
logger.error(f"结束会话统计失败: {e}")

View File

@@ -18,21 +18,21 @@ logger = get_logger("hfc_version")
class HFCVersionManager:
"""HFC版本号管理器"""
# 默认版本号
DEFAULT_VERSION = "v1.0.0"
# 当前运行时版本号
_current_version: Optional[str] = None
@classmethod
def set_version(cls, version: str) -> bool:
"""
设置当前运行时版本号
参数:
version: 版本号字符串,格式如 v1.0.0 或 1.0.0
返回:
bool: 设置是否成功
"""
@@ -48,103 +48,103 @@ class HFCVersionManager:
except Exception as e:
logger.error(f"设置版本号失败: {e}")
return False
@classmethod
def get_version(cls) -> str:
"""
获取当前版本号
返回:
str: 当前版本号
"""
if cls._current_version:
return cls._current_version
# 尝试从环境变量获取
env_version = os.getenv("HFC_PERFORMANCE_VERSION")
if env_version:
if cls.set_version(env_version):
return cls._current_version
# 返回默认版本号
return cls.DEFAULT_VERSION
@classmethod
def auto_generate_version(cls, base_version: str = None) -> str:
"""
自动生成版本号(基于时间戳)
参数:
base_version: 基础版本号,如果不提供则使用默认版本
返回:
str: 生成的版本号
"""
if not base_version:
base_version = cls.DEFAULT_VERSION
# 提取基础版本号的主要部分
base_match = re.match(r'v?(\d+\.\d+)', base_version)
base_match = re.match(r"v?(\d+\.\d+)", base_version)
if base_match:
base_part = base_match.group(1)
else:
base_part = "1.0"
# 添加时间戳
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
generated_version = f"v{base_part}.{timestamp}"
cls.set_version(generated_version)
logger.info(f"自动生成版本号: {generated_version}")
return generated_version
@classmethod
def _validate_version(cls, version: str) -> Optional[str]:
"""
验证版本号格式
参数:
version: 待验证的版本号
返回:
Optional[str]: 验证后的版本号失败返回None
"""
if not version or not isinstance(version, str):
return None
version = version.strip()
# 支持的格式:
# v1.0.0, 1.0.0, v1.0, 1.0, v1.0.0.20241222_1530 等
patterns = [
r'^v?(\d+\.\d+\.\d+)$', # v1.0.0 或 1.0.0
r'^v?(\d+\.\d+)$', # v1.0 或 1.0
r'^v?(\d+\.\d+\.\d+\.\w+)$', # v1.0.0.build 或 1.0.0.build
r'^v?(\d+\.\d+\.\w+)$', # v1.0.build 或 1.0.build
r"^v?(\d+\.\d+\.\d+)$", # v1.0.0 或 1.0.0
r"^v?(\d+\.\d+)$", # v1.0 或 1.0
r"^v?(\d+\.\d+\.\d+\.\w+)$", # v1.0.0.build 或 1.0.0.build
r"^v?(\d+\.\d+\.\w+)$", # v1.0.build 或 1.0.build
]
for pattern in patterns:
match = re.match(pattern, version)
if match:
# 确保版本号以v开头
if not version.startswith('v'):
version = 'v' + version
if not version.startswith("v"):
version = "v" + version
return version
return None
@classmethod
def reset_version(cls):
"""重置版本号为默认值"""
cls._current_version = None
logger.info("HFC版本号已重置为默认值")
@classmethod
def get_version_info(cls) -> dict:
"""
获取版本信息
返回:
dict: 版本相关信息
"""
@@ -154,7 +154,7 @@ class HFCVersionManager:
"default_version": cls.DEFAULT_VERSION,
"is_custom": current != cls.DEFAULT_VERSION,
"env_version": os.getenv("HFC_PERFORMANCE_VERSION"),
"timestamp": datetime.now().isoformat()
"timestamp": datetime.now().isoformat(),
}
@@ -182,4 +182,4 @@ def reset_hfc_version():
# 在模块加载时显示当前版本信息
if __name__ != "__main__":
current_version = HFCVersionManager.get_version()
logger.debug(f"HFC性能记录模块已加载当前版本: {current_version}")
logger.debug(f"HFC性能记录模块已加载当前版本: {current_version}")

View File

@@ -345,6 +345,7 @@ class ChatManager:
async def load_all_streams(self):
"""从数据库加载所有聊天流"""
logger.info("正在从数据库加载所有聊天流")
def _db_load_all_streams_sync():
loaded_streams_data = []
for model_instance in ChatStreams.select():

View File

@@ -197,36 +197,30 @@ class StatisticOutputTask(AsyncTask):
logger.info("\n" + "\n".join(output))
async def run(self):
try:
try:
now = datetime.now()
# 使用线程池并行执行耗时操作
loop = asyncio.get_event_loop()
# 在线程池中并行执行数据收集和之前的HTML生成如果存在
with concurrent.futures.ThreadPoolExecutor() as executor:
logger.info("正在收集统计数据...")
# 数据收集任务
collect_task = loop.run_in_executor(
executor, self._collect_all_statistics, now
)
collect_task = loop.run_in_executor(executor, self._collect_all_statistics, now)
# 等待数据收集完成
stats = await collect_task
logger.info("统计数据收集完成")
# 并行执行控制台输出和HTML报告生成
console_task = loop.run_in_executor(
executor, self._statistic_console_output, stats, now
)
html_task = loop.run_in_executor(
executor, self._generate_html_report, stats, now
)
console_task = loop.run_in_executor(executor, self._statistic_console_output, stats, now)
html_task = loop.run_in_executor(executor, self._generate_html_report, stats, now)
# 等待两个输出任务完成
await asyncio.gather(console_task, html_task)
logger.info("统计数据输出完成")
except Exception as e:
logger.exception(f"输出统计数据过程中发生异常,错误信息:{e}")
@@ -236,41 +230,38 @@ class StatisticOutputTask(AsyncTask):
备选方案:完全异步后台运行统计输出
使用此方法可以让统计任务完全非阻塞
"""
async def _async_collect_and_output():
try:
import concurrent.futures
now = datetime.now()
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor() as executor:
logger.info("正在后台收集统计数据...")
# 创建后台任务,不等待完成
collect_task = asyncio.create_task(
loop.run_in_executor(executor, self._collect_all_statistics, now)
)
stats = await collect_task
logger.info("统计数据收集完成")
# 创建并发的输出任务
output_tasks = [
asyncio.create_task(
loop.run_in_executor(executor, self._statistic_console_output, stats, now)
),
asyncio.create_task(
loop.run_in_executor(executor, self._generate_html_report, stats, now)
)
asyncio.create_task(loop.run_in_executor(executor, self._statistic_console_output, stats, now)),
asyncio.create_task(loop.run_in_executor(executor, self._generate_html_report, stats, now)),
]
# 等待所有输出任务完成
await asyncio.gather(*output_tasks)
logger.info("统计数据后台输出完成")
except Exception as e:
logger.exception(f"后台统计数据输出过程中发生异常:{e}")
# 创建后台任务,立即返回
asyncio.create_task(_async_collect_and_output())
@@ -1619,7 +1610,7 @@ class AsyncStatisticOutputTask(AsyncTask):
def __init__(self, record_file_path: str = "maibot_statistics.html"):
# 延迟0秒启动运行间隔300秒
super().__init__(task_name="Async Statistics Data Output Task", wait_before_start=0, run_interval=300)
# 直接复用 StatisticOutputTask 的初始化逻辑
temp_stat_task = StatisticOutputTask(record_file_path)
self.name_mapping = temp_stat_task.name_mapping
@@ -1628,49 +1619,46 @@ class AsyncStatisticOutputTask(AsyncTask):
async def run(self):
"""完全异步执行统计任务"""
async def _async_collect_and_output():
try:
now = datetime.now()
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor() as executor:
logger.info("正在后台收集统计数据...")
# 数据收集任务
collect_task = asyncio.create_task(
loop.run_in_executor(executor, self._collect_all_statistics, now)
)
stats = await collect_task
logger.info("统计数据收集完成")
# 创建并发的输出任务
output_tasks = [
asyncio.create_task(
loop.run_in_executor(executor, self._statistic_console_output, stats, now)
),
asyncio.create_task(
loop.run_in_executor(executor, self._generate_html_report, stats, now)
)
asyncio.create_task(loop.run_in_executor(executor, self._statistic_console_output, stats, now)),
asyncio.create_task(loop.run_in_executor(executor, self._generate_html_report, stats, now)),
]
# 等待所有输出任务完成
await asyncio.gather(*output_tasks)
logger.info("统计数据后台输出完成")
except Exception as e:
logger.exception(f"后台统计数据输出过程中发生异常:{e}")
# 创建后台任务,立即返回
asyncio.create_task(_async_collect_and_output())
# 复用 StatisticOutputTask 的所有方法
def _collect_all_statistics(self, now: datetime):
return StatisticOutputTask._collect_all_statistics(self, now)
def _statistic_console_output(self, stats: Dict[str, Any], now: datetime):
return StatisticOutputTask._statistic_console_output(self, stats, now)
def _generate_html_report(self, stats: dict[str, Any], now: datetime):
return StatisticOutputTask._generate_html_report(self, stats, now)
@@ -1678,11 +1666,11 @@ class AsyncStatisticOutputTask(AsyncTask):
@staticmethod
def _collect_model_request_for_period(collect_period: List[Tuple[str, datetime]]) -> Dict[str, Any]:
return StatisticOutputTask._collect_model_request_for_period(collect_period)
@staticmethod
@staticmethod
def _collect_online_time_for_period(collect_period: List[Tuple[str, datetime]], now: datetime) -> Dict[str, Any]:
return StatisticOutputTask._collect_online_time_for_period(collect_period, now)
def _collect_message_count_for_period(self, collect_period: List[Tuple[str, datetime]]) -> Dict[str, Any]:
return StatisticOutputTask._collect_message_count_for_period(self, collect_period)
@@ -1699,13 +1687,13 @@ class AsyncStatisticOutputTask(AsyncTask):
def _format_chat_stat(self, stats: Dict[str, Any]) -> str:
return StatisticOutputTask._format_chat_stat(self, stats)
def _generate_chart_data(self, stat: dict[str, Any]) -> dict:
return StatisticOutputTask._generate_chart_data(self, stat)
def _collect_interval_data(self, now: datetime, hours: int, interval_minutes: int) -> dict:
return StatisticOutputTask._collect_interval_data(self, now, hours, interval_minutes)
def _generate_chart_tab(self, chart_data: dict) -> str:
return StatisticOutputTask._generate_chart_tab(self, chart_data)