perf: 量化商品页打开耗时并去重检查 (#109)
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""任务领取到商品页的脱敏分阶段计时。
|
||||
|
||||
日志字段使用白名单,只包含任务编号、稳定操作名、毫秒耗时和结果分类。
|
||||
不要把 URL、控件树、异常正文或业务数据传入本模块。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
import time
|
||||
from typing import Callable, Iterator, Optional
|
||||
|
||||
from .db import data_dir
|
||||
|
||||
|
||||
PERFORMANCE_LOG_NAME = "task_performance.jsonl"
|
||||
_LOGGER_NAME = "cmautobuy.performance"
|
||||
_ACTIVE_TRACE: ContextVar[Optional["TaskPerformanceTrace"]] = ContextVar(
|
||||
"cmautobuy_task_performance_trace", default=None
|
||||
)
|
||||
|
||||
|
||||
def _default_sink(record: dict[str, object]) -> None:
|
||||
logging.getLogger(_LOGGER_NAME).info(
|
||||
json.dumps(record, ensure_ascii=False, separators=(",", ":"))
|
||||
)
|
||||
|
||||
|
||||
def configure_performance_logging(
|
||||
directory: Optional[Path] = None,
|
||||
) -> Path:
|
||||
"""把性能事件写入可轮转 JSONL 文件,重复调用不会增加处理器。"""
|
||||
|
||||
log_directory = directory or data_dir() / "logs"
|
||||
log_directory.mkdir(parents=True, exist_ok=True)
|
||||
log_path = log_directory / PERFORMANCE_LOG_NAME
|
||||
logger = logging.getLogger(_LOGGER_NAME)
|
||||
if not any(
|
||||
getattr(handler, "baseFilename", None) == str(log_path.resolve())
|
||||
for handler in logger.handlers
|
||||
):
|
||||
handler = RotatingFileHandler(
|
||||
log_path,
|
||||
maxBytes=2 * 1024 * 1024,
|
||||
backupCount=3,
|
||||
encoding="utf-8",
|
||||
)
|
||||
handler.setFormatter(logging.Formatter("%(message)s"))
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
logger.propagate = False
|
||||
return log_path
|
||||
|
||||
|
||||
class TaskPerformanceTrace:
|
||||
"""收集一条任务的阶段耗时,并在绑定任务编号后输出记录。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
monotonic: Callable[[], float] = time.monotonic,
|
||||
sink: Callable[[dict[str, object]], None] = _default_sink,
|
||||
) -> None:
|
||||
self._monotonic = monotonic
|
||||
self._sink = sink
|
||||
self._started_at = monotonic()
|
||||
self._task_id = ""
|
||||
self._pending: list[tuple[str, int, str]] = []
|
||||
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
return self._task_id
|
||||
|
||||
def bind_task(self, task_id: str) -> None:
|
||||
"""绑定稳定任务编号,并输出绑定前缓存的领取阶段记录。"""
|
||||
|
||||
value = str(task_id or "").strip()
|
||||
if not value:
|
||||
return
|
||||
if self._task_id and self._task_id != value:
|
||||
raise RuntimeError("性能计时不能跨任务复用")
|
||||
self._task_id = value
|
||||
pending, self._pending = self._pending, []
|
||||
for operation, duration_ms, result in pending:
|
||||
self._emit(operation, duration_ms, result)
|
||||
|
||||
@contextmanager
|
||||
def stage(self, operation: str) -> Iterator[None]:
|
||||
"""记录一个阶段;异常时只写 failed,不记录异常正文。"""
|
||||
|
||||
started_at = self._monotonic()
|
||||
try:
|
||||
yield
|
||||
except BaseException:
|
||||
self.record(operation, self._monotonic() - started_at, "failed")
|
||||
raise
|
||||
else:
|
||||
self.record(operation, self._monotonic() - started_at, "succeeded")
|
||||
|
||||
def record(self, operation: str, seconds: float, result: str) -> None:
|
||||
"""记录白名单阶段数据,负耗时会安全归零。"""
|
||||
|
||||
checked_operation = str(operation or "").strip()
|
||||
checked_result = str(result or "").strip()
|
||||
if not checked_operation or not checked_result:
|
||||
raise ValueError("性能操作名和结果分类不能为空")
|
||||
duration_ms = max(0, round(float(seconds) * 1000))
|
||||
if self._task_id:
|
||||
self._emit(checked_operation, duration_ms, checked_result)
|
||||
else:
|
||||
self._pending.append(
|
||||
(checked_operation, duration_ms, checked_result)
|
||||
)
|
||||
|
||||
def checkpoint(self, operation: str, result: str = "succeeded") -> None:
|
||||
"""记录从本轮调度开始到当前时刻的端到端耗时。"""
|
||||
|
||||
self.record(operation, self._monotonic() - self._started_at, result)
|
||||
|
||||
@contextmanager
|
||||
def activate(self) -> Iterator["TaskPerformanceTrace"]:
|
||||
"""让同一工作线程内的设备和页面适配层取得本计时对象。"""
|
||||
|
||||
token = _ACTIVE_TRACE.set(self)
|
||||
try:
|
||||
yield self
|
||||
finally:
|
||||
_ACTIVE_TRACE.reset(token)
|
||||
|
||||
def _emit(self, operation: str, duration_ms: int, result: str) -> None:
|
||||
self._sink(
|
||||
{
|
||||
"task_id": self._task_id,
|
||||
"operation": operation,
|
||||
"duration_ms": duration_ms,
|
||||
"result": result,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def current_performance_trace() -> Optional[TaskPerformanceTrace]:
|
||||
"""返回当前工作线程的任务计时对象;普通调用返回 None。"""
|
||||
|
||||
return _ACTIVE_TRACE.get()
|
||||
Reference in New Issue
Block a user