mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-09 22:00:01 +08:00
性能优化 (#2126)
* 性能优化 * 代码改进 * 优化浏览器代际切换逻辑 * 统一缓存与生命周期 * 添加aiomysql依赖 * 优化插件路径处理逻辑,简化条件判断;在虚拟环境包管理器中添加编码和错误处理参数以增强稳定性 * 🚨 auto fix by pre-commit hooks * 优化Windows下的关闭逻辑 * 代码优化 * bugfix:修复配置重载问题 * bugfix:修复插件加载启动竞态问题 * 收敛事件入口和权限上下文 * 优化 Windows launcher 关闭重启兜底 --------- Co-authored-by: HibiKier <775757368@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
HibiKier
pre-commit-ci[bot]
parent
24c316cd2c
commit
5d92ccd3b0
@@ -20,6 +20,12 @@ from zhenxun.services.log import logger
|
||||
from .types import BaseScreenshotEngine
|
||||
|
||||
_PLAYWRIGHT_DISCONNECT_ERROR = "Connection closed while reading from the driver"
|
||||
_PLAYWRIGHT_TARGET_CLOSED_ERROR_MARKERS = (
|
||||
"TargetClosedError",
|
||||
"Target page, context or browser has been closed",
|
||||
"browser has been closed",
|
||||
"BrowserContext.new_page",
|
||||
)
|
||||
_UNRETRIEVED_FUTURE_MESSAGE = "Future exception was never retrieved"
|
||||
_LOOP_EXCEPTION_FILTER_STATE_ATTR = "_zhenxun_playwright_exception_filter_state"
|
||||
_DISCONNECT_SUPPRESSION_WINDOW_SECONDS = 10.0
|
||||
@@ -135,6 +141,14 @@ def _is_ignorable_playwright_disconnect(ctx: dict[str, Any]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _is_playwright_target_closed_error(exc: Exception) -> bool:
|
||||
exc_name = type(exc).__name__
|
||||
if exc_name == "TargetClosedError":
|
||||
return True
|
||||
message = str(exc)
|
||||
return any(marker in message for marker in _PLAYWRIGHT_TARGET_CLOSED_ERROR_MARKERS)
|
||||
|
||||
|
||||
def _get_loop_exception_filter_state(
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
) -> dict[str, Any] | None:
|
||||
@@ -1103,29 +1117,54 @@ class PlaywrightEngine(BaseScreenshotEngine):
|
||||
template_path: str,
|
||||
render_options: dict[str, Any],
|
||||
) -> bytes:
|
||||
generation, context = await self._acquire_context()
|
||||
page = None
|
||||
broken = False
|
||||
try:
|
||||
page = await context.new_page()
|
||||
page_options = self._build_page_options(render_options, pooled=True)
|
||||
viewport = page_options.get("viewport")
|
||||
if isinstance(viewport, dict):
|
||||
width = viewport.get("width")
|
||||
height = viewport.get("height")
|
||||
if isinstance(width, int) and isinstance(height, int):
|
||||
await page.set_viewport_size({"width": width, "height": height})
|
||||
return await self._render_with_page(
|
||||
page, html, template_path, render_options
|
||||
)
|
||||
except Exception:
|
||||
broken = True
|
||||
raise
|
||||
finally:
|
||||
if page is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await page.close()
|
||||
await self._release_context(generation, context, broken=broken)
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(2):
|
||||
generation, context = await self._acquire_context()
|
||||
page = None
|
||||
broken = False
|
||||
try:
|
||||
page = await context.new_page()
|
||||
page_options = self._build_page_options(render_options, pooled=True)
|
||||
viewport = page_options.get("viewport")
|
||||
if isinstance(viewport, dict):
|
||||
width = viewport.get("width")
|
||||
height = viewport.get("height")
|
||||
if isinstance(width, int) and isinstance(height, int):
|
||||
await page.set_viewport_size({"width": width, "height": height})
|
||||
return await self._render_with_page(
|
||||
page, html, template_path, render_options
|
||||
)
|
||||
except Exception as e:
|
||||
broken = True
|
||||
last_error = e
|
||||
if attempt == 0:
|
||||
if _is_playwright_target_closed_error(e):
|
||||
logger.warning(
|
||||
"截图引擎浏览器上下文代已失效,切换新代后重试一次。",
|
||||
"PlaywrightEngine",
|
||||
e=e,
|
||||
)
|
||||
try:
|
||||
await self._swap_generation("target_closed")
|
||||
except Exception:
|
||||
raise e
|
||||
else:
|
||||
logger.warning(
|
||||
"截图引擎上下文已失效,丢弃后重试一次。",
|
||||
"PlaywrightEngine",
|
||||
e=e,
|
||||
)
|
||||
continue
|
||||
raise
|
||||
finally:
|
||||
if page is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await page.close()
|
||||
await self._release_context(generation, context, broken=broken)
|
||||
|
||||
if last_error is not None:
|
||||
raise last_error
|
||||
raise RuntimeError("截图引擎上下文池渲染失败。")
|
||||
|
||||
async def _render_html(
|
||||
self,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
import hashlib
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.cache.bounded_ttl import BoundedTTLCache
|
||||
from zhenxun.utils.pydantic_compat import dump_json_safely
|
||||
|
||||
|
||||
@@ -23,9 +21,12 @@ class RenderResultMemoryCache:
|
||||
if isinstance(max_total_bytes, int) and max_total_bytes > 0
|
||||
else None
|
||||
)
|
||||
self._cache: OrderedDict[str, tuple[float, bytes]] = OrderedDict()
|
||||
self._total_bytes = 0
|
||||
self._lock = asyncio.Lock()
|
||||
self._cache = BoundedTTLCache[str, bytes](
|
||||
"RENDER_RESULT",
|
||||
ttl_seconds=self._ttl_seconds,
|
||||
max_items=self._max_items,
|
||||
max_total_bytes=self._max_total_bytes,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_key(payload: Any) -> str:
|
||||
@@ -37,55 +38,8 @@ class RenderResultMemoryCache:
|
||||
)
|
||||
return hashlib.sha256(payload_text.encode("utf-8")).hexdigest()
|
||||
|
||||
def _pop_oldest(self) -> None:
|
||||
if not self._cache:
|
||||
return
|
||||
_, (_, value) = self._cache.popitem(last=False)
|
||||
self._total_bytes -= len(value)
|
||||
if self._total_bytes < 0:
|
||||
self._total_bytes = 0
|
||||
|
||||
def _cleanup(self, now: float) -> None:
|
||||
while self._cache:
|
||||
expire_at, _ = next(iter(self._cache.values()))
|
||||
if expire_at > now:
|
||||
break
|
||||
self._pop_oldest()
|
||||
while len(self._cache) > self._max_items:
|
||||
self._pop_oldest()
|
||||
if self._max_total_bytes is not None:
|
||||
while self._total_bytes > self._max_total_bytes and self._cache:
|
||||
self._pop_oldest()
|
||||
|
||||
async def get(self, key: str) -> bytes | None:
|
||||
now = time.monotonic()
|
||||
async with self._lock:
|
||||
self._cleanup(now)
|
||||
item = self._cache.get(key)
|
||||
if item is None:
|
||||
return None
|
||||
expire_at, value = item
|
||||
if expire_at <= now:
|
||||
removed = self._cache.pop(key, None)
|
||||
if removed:
|
||||
self._total_bytes -= len(removed[1])
|
||||
if self._total_bytes < 0:
|
||||
self._total_bytes = 0
|
||||
return None
|
||||
self._cache.move_to_end(key)
|
||||
return value
|
||||
return await self._cache.get(key)
|
||||
|
||||
async def set(self, key: str, value: bytes) -> None:
|
||||
value_size = len(value)
|
||||
if self._max_total_bytes is not None and value_size > self._max_total_bytes:
|
||||
return
|
||||
now = time.monotonic()
|
||||
async with self._lock:
|
||||
if old := self._cache.pop(key, None):
|
||||
self._total_bytes -= len(old[1])
|
||||
if self._total_bytes < 0:
|
||||
self._total_bytes = 0
|
||||
self._cache[key] = (now + self._ttl_seconds, value)
|
||||
self._total_bytes += value_size
|
||||
self._cache.move_to_end(key)
|
||||
self._cleanup(now)
|
||||
await self._cache.set(key, value)
|
||||
|
||||
@@ -475,6 +475,18 @@ class RendererService:
|
||||
raise RuntimeError("ThemeManager尚未初始化。")
|
||||
return self._theme_manager.list_available_themes()
|
||||
|
||||
def clear_runtime_caches(self) -> dict[str, int]:
|
||||
cleared: dict[str, int] = {}
|
||||
if self._theme_manager:
|
||||
cleared.update(self._theme_manager.clear_runtime_caches())
|
||||
if self._template_engine and self._template_engine.env.cache:
|
||||
jinja_cache = self._template_engine.env.cache
|
||||
cache_size = len(jinja_cache)
|
||||
jinja_cache.clear()
|
||||
if cache_size:
|
||||
cleared["jinja_env"] = cache_size
|
||||
return cleared
|
||||
|
||||
async def switch_theme(self, theme_name: str) -> str:
|
||||
"""
|
||||
切换UI主题,加载新主题并持久化配置。
|
||||
|
||||
@@ -51,8 +51,10 @@ class ManifestRegistry:
|
||||
self._manifest_cache: dict[str, TemplateManifest] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
def clear_cache(self):
|
||||
def clear_cache(self) -> int:
|
||||
size = len(self._manifest_cache)
|
||||
self._manifest_cache.clear()
|
||||
return size
|
||||
|
||||
async def get_manifest(
|
||||
self, component_path: str, skin: str | None = None
|
||||
@@ -362,6 +364,21 @@ class ThemeManager:
|
||||
tuple[type, str, str | None], ComponentDependency
|
||||
] = OrderedDict()
|
||||
|
||||
def clear_runtime_caches(self) -> dict[str, int]:
|
||||
cleared = {
|
||||
"asset_resolution": len(self._asset_resolution_cache),
|
||||
"global_template": len(self._global_template_cache),
|
||||
"component_dependency": len(self._component_dependency_cache),
|
||||
}
|
||||
self._asset_resolution_cache.clear()
|
||||
self._global_template_cache.clear()
|
||||
self._component_dependency_cache.clear()
|
||||
if self.manifest_registry:
|
||||
manifest_count = self.manifest_registry.clear_cache()
|
||||
if manifest_count:
|
||||
cleared["manifest"] = manifest_count
|
||||
return {key: value for key, value in cleared.items() if value}
|
||||
|
||||
@staticmethod
|
||||
def _get_lru_entry(cache: OrderedDict, key: Any) -> Any:
|
||||
value = cache.get(key)
|
||||
|
||||
Reference in New Issue
Block a user