#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：performance_executor.py
模块描述：性能测试执行引擎 — 轻量级 asyncio + aiohttp 自研压测引擎

支持三种压测模式：
  - concurrent（固定并发数）
  - qps（固定 QPS）
  - step（阶梯加压）

新增两种场景模式（scenario_type）：
  - burst（瞬时并发测试）：集合点同步 + 循环次数/无限循环 + 失败停止策略
  - endurance（长稳压测）：多接口分线程组 + 思考时间 + CSV 参数化

核心组件：
  - MetricsCollector：线程安全指标采集器，每秒生成快照
  - ResultValidator：断言规则验证器
  - PerformanceExecutor：主引擎，协调 Token 获取、请求发送、指标采集

作者：czj
创建日期：2026-08-12
最后修改：2026-08-12
"""

import asyncio
import csv
import io
import json
import logging
import math
import random
import re
import threading
import time
from typing import Any, Callable, Dict, List, Optional

import aiohttp

from app.executors.http_client import HttpClient
from app.executors.template_resolver import TemplateResolver
from app.executors.resource_monitor import ResourceMonitor
from app.executors.target_resource_monitor import TargetResourceMonitor

logger = logging.getLogger(__name__)


# ==================== MetricsCollector ====================

class MetricsCollector:
    """
    指标采集器

    线程安全，支持每秒采集快照和最终汇总统计。
    使用 threading.Lock 保护共享状态。

    Attributes:
        snapshot_callback: 每秒快照回调函数（用于 WebSocket 推送）
    """

    def __init__(self, snapshot_callback: Optional[Callable[[dict], None]] = None):
        self.snapshot_callback = snapshot_callback
        self._lock = threading.Lock()
        self._start_time = time.time()
        self._reset()

    def _reset(self) -> None:
        """重置周期计数器（每秒重置）"""
        # 周期内（当前秒）
        self._period_success = 0
        self._period_fail = 0
        self._period_times: List[float] = []
        self._period_status_2xx = 0
        self._period_status_3xx = 0
        self._period_status_4xx = 0
        self._period_status_5xx = 0
        self._period_latencies: List[float] = []
        self._period_connect_times: List[float] = []
        self._period_sent_bytes = 0
        self._period_received_bytes = 0

        # 全局累计
        self.total_requests = 0
        self.success_count = 0
        self.fail_count = 0
        self.all_times: List[float] = []
        self.all_latencies: List[float] = []
        self.all_connect_times: List[float] = []
        self.total_sent_bytes = 0
        self.total_received_bytes = 0
        self.status_2xx = 0
        self.status_3xx = 0
        self.status_4xx = 0
        self.status_5xx = 0
        self._peak_tps = 0.0
        self._error_counts: Dict[str, int] = {
            "timeout": 0, "connect_error": 0, "client_error": 0,
            "http_4xx": 0, "http_5xx": 0, "assertion_fail": 0,
        }
        # 接口维度指标统计（长稳压测多接口场景）
        self._api_stats: Dict[str, dict] = {}
        self._api_stats_lock = threading.Lock()

    def record(self, status: int, elapsed_ms: float, success: bool, error: str = "",
               latency_ms: float = 0.0, connect_ms: float = 0.0,
               sent_bytes: int = 0, received_bytes: int = 0,
               error_type: str = "", api_name: str = "") -> None:
        """
        记录一次请求结果

        Args:
            status: HTTP 状态码（0 表示网络异常）
            elapsed_ms: 响应时间（毫秒）
            success: 是否成功（断言通过）
            error: 错误描述
            latency_ms: 首字节延迟（毫秒）
            connect_ms: TCP 连接时间（毫秒）
            sent_bytes: 发送字节数
            received_bytes: 接收字节数
            error_type: 错误类型（timeout/connect_error/client_error/assertion_fail）
            api_name: 接口名称（长稳压测多接口场景用于接口维度统计）
        """
        with self._lock:
            self.total_requests += 1
            if success:
                self.success_count += 1
                self._period_success += 1
            else:
                self.fail_count += 1
                self._period_fail += 1

            self._period_times.append(elapsed_ms)
            self.all_times.append(elapsed_ms)
            self._period_latencies.append(latency_ms)
            self.all_latencies.append(latency_ms)
            self._period_connect_times.append(connect_ms)
            self.all_connect_times.append(connect_ms)
            self._period_sent_bytes += sent_bytes
            self.total_sent_bytes += sent_bytes
            self._period_received_bytes += received_bytes
            self.total_received_bytes += received_bytes

            if 200 <= status < 300:
                self._period_status_2xx += 1
                self.status_2xx += 1
            elif 300 <= status < 400:
                self._period_status_3xx += 1
                self.status_3xx += 1
            elif 400 <= status < 500:
                self._period_status_4xx += 1
                self.status_4xx += 1
                self._error_counts["http_4xx"] += 1
            elif status >= 500:
                self._period_status_5xx += 1
                self.status_5xx += 1
                self._error_counts["http_5xx"] += 1

            # 错误分类统计
            if error_type:
                self._error_counts[error_type] = self._error_counts.get(error_type, 0) + 1
            elif not success and 200 <= status < 400:
                # 断言失败（状态码成功但断言未通过）
                self._error_counts["assertion_fail"] += 1

            # 接口维度统计（长稳压测多接口场景）
            if api_name:
                stats = self._api_stats.setdefault(api_name, {
                    "count": 0,
                    "success": 0,
                    "times": [],
                    "status_2xx": 0,
                    "status_3xx": 0,
                    "status_4xx": 0,
                    "status_5xx": 0,
                    "sent_bytes": 0,
                    "received_bytes": 0,
                })
                stats["count"] += 1
                if success:
                    stats["success"] += 1
                stats["times"].append(elapsed_ms)
                stats["sent_bytes"] += sent_bytes
                stats["received_bytes"] += received_bytes
                if 200 <= status < 300:
                    stats["status_2xx"] += 1
                elif 300 <= status < 400:
                    stats["status_3xx"] += 1
                elif 400 <= status < 500:
                    stats["status_4xx"] += 1
                elif status >= 500:
                    stats["status_5xx"] += 1

    def snapshot(self, peak_tps: float = 0.0, concurrent_users: int = 0) -> dict:
        """
        采集当前周期快照并重置周期计数器

        Args:
            peak_tps: 当前峰值 TPS（由执行器维护）
            concurrent_users: 实时并发数（在途请求数，由执行器维护）

        Returns:
            dict: 快照数据（含周期统计和全局累计）
        """
        with self._lock:
            elapsed = time.time() - self._start_time
            period_times = self._period_times
            period_success = self._period_success
            period_fail = self._period_fail
            period_latencies = self._period_latencies
            period_connect_times = self._period_connect_times

            # 计算周期统计
            tps = period_success / 1.0 if period_success > 0 else 0
            avg = sum(period_times) / len(period_times) if period_times else 0
            p50 = self._percentile(period_times, 50) if period_times else 0
            p90 = self._percentile(period_times, 90) if period_times else 0
            p95 = self._percentile(period_times, 95) if period_times else 0
            p99 = self._percentile(period_times, 99) if period_times else 0

            snapshot = {
                "elapsed": round(elapsed, 1),
                "tps": round(tps, 2),
                "avg_response_time": round(avg, 2),
                "min_response_time": round(min(period_times), 2) if period_times else 0,
                "max_response_time": round(max(period_times), 2) if period_times else 0,
                "p50": round(p50, 2),
                "p90": round(p90, 2),
                "p95": round(p95, 2),
                "p99": round(p99, 2),
                "std_dev": round(self._std_dev(period_times), 2),
                "latency_avg": round(sum(period_latencies) / len(period_latencies), 2) if period_latencies else 0,
                "connect_time_avg": round(sum(period_connect_times) / len(period_connect_times), 2) if period_connect_times else 0,
                "sent_bytes": self._period_sent_bytes,
                "received_bytes": self._period_received_bytes,
                "peak_tps": round(peak_tps, 2),
                "success_count": period_success,
                "fail_count": period_fail,
                "status_2xx": self._period_status_2xx,
                "status_3xx": self._period_status_3xx,
                "status_4xx": self._period_status_4xx,
                "status_5xx": self._period_status_5xx,
                "total_requests": self.total_requests,
                "concurrent_users": concurrent_users,
            }

            # 重置周期计数器
            self._period_success = 0
            self._period_fail = 0
            self._period_times = []
            self._period_status_2xx = 0
            self._period_status_3xx = 0
            self._period_status_4xx = 0
            self._period_status_5xx = 0
            self._period_latencies = []
            self._period_connect_times = []
            self._period_sent_bytes = 0
            self._period_received_bytes = 0

            return snapshot

    def summary(self) -> dict:
        """
        最终汇总统计

        Returns:
            dict: 完整汇总数据
        """
        times = self.all_times
        latencies = self.all_latencies
        connect_times = self.all_connect_times
        elapsed = max(time.time() - self._start_time, 1)
        return {
            "total_requests": self.total_requests,
            "success_count": self.success_count,
            "fail_count": self.fail_count,
            "error_rate": round(self.fail_count / max(self.total_requests, 1) * 100, 2),
            "min_response_time": round(min(times), 2) if times else 0,
            "max_response_time": round(max(times), 2) if times else 0,
            "avg_response_time": round(sum(times) / len(times), 2) if times else 0,
            "p50_response_time": round(self._percentile(times, 50), 2) if times else 0,
            "p90_response_time": round(self._percentile(times, 90), 2) if times else 0,
            "p95_response_time": round(self._percentile(times, 95), 2) if times else 0,
            "p99_response_time": round(self._percentile(times, 99), 2) if times else 0,
            "std_dev": round(self._std_dev(times), 2) if times else 0,
            "latency_avg": round(sum(latencies) / len(latencies), 2) if latencies else 0,
            "latency_min": round(min(latencies), 2) if latencies else 0,
            "latency_max": round(max(latencies), 2) if latencies else 0,
            "connect_time_avg": round(sum(connect_times) / len(connect_times), 2) if connect_times else 0,
            "connect_time_max": round(max(connect_times), 2) if connect_times else 0,
            "peak_tps": round(self._peak_tps, 2),
            "total_sent_bytes": self.total_sent_bytes,
            "total_received_bytes": self.total_received_bytes,
            "sent_kb_per_sec": round(self.total_sent_bytes / 1024 / elapsed, 2),
            "received_kb_per_sec": round(self.total_received_bytes / 1024 / elapsed, 2),
            "apdex": round(self._apdex(times), 4),
            "actual_tps": round(self.success_count / elapsed, 2),
            "status_2xx": self.status_2xx,
            "status_3xx": self.status_3xx,
            "status_4xx": self.status_4xx,
            "status_5xx": self.status_5xx,
            "error_type_timeout": self._error_counts.get("timeout", 0),
            "error_type_connect_error": self._error_counts.get("connect_error", 0),
            "error_type_client_error": self._error_counts.get("client_error", 0),
            "error_type_http_4xx": self._error_counts.get("http_4xx", 0),
            "error_type_http_5xx": self._error_counts.get("http_5xx", 0),
            "error_type_assertion": self._error_counts.get("assertion_fail", 0),
        }

    def _build_api_summary(self) -> Optional[list]:
        """
        构建接口维度指标汇总（长稳压测多接口场景，执行结束后调用）

        Returns:
            list: [{name, totalRequests, successCount, failCount, avgResponseTime,
                    minResponseTime, maxResponseTime, p50ResponseTime, p90ResponseTime,
                    p95ResponseTime, p99ResponseTime, stdDev, errorRate, actualTps,
                    status2xx, status3xx, status4xx, status5xx, sentBytes, receivedBytes}]
            无接口数据返回 None
        """
        if not self._api_stats:
            return None
        result = []
        elapsed = max(time.time() - self._start_time, 1)
        for api_name, stats in self._api_stats.items():
            times = stats["times"]
            if not times:
                continue
            sorted_t = sorted(times)
            n = len(times)
            p50 = sorted_t[int(n * 0.5)] if n > 1 else sorted_t[0]
            p90 = sorted_t[int(n * 0.9)] if n > 1 else sorted_t[0]
            p95 = sorted_t[int(n * 0.95)] if n > 1 else sorted_t[0]
            p99 = sorted_t[int(n * 0.99)] if n > 1 else sorted_t[0]
            mean = sum(times) / n
            std_dev = math.sqrt(sum((x - mean) ** 2 for x in times) / n) if n > 1 else 0
            fail_count = stats["count"] - stats["success"]
            result.append({
                "name": api_name,
                "totalRequests": stats["count"],
                "successCount": stats["success"],
                "failCount": fail_count,
                "avgResponseTime": round(mean, 2),
                "minResponseTime": round(min(times), 2) if n else 0,
                "maxResponseTime": round(max(times), 2) if n else 0,
                "p50ResponseTime": round(p50, 2),
                "p90ResponseTime": round(p90, 2),
                "p95ResponseTime": round(p95, 2),
                "p99ResponseTime": round(p99, 2),
                "stdDev": round(std_dev, 2),
                "errorRate": round(fail_count / max(stats["count"], 1), 4),
                "actualTps": round(stats["success"] / elapsed, 2),
                "status2xx": stats["status_2xx"],
                "status3xx": stats["status_3xx"],
                "status4xx": stats["status_4xx"],
                "status5xx": stats["status_5xx"],
                "sentBytes": stats["sent_bytes"],
                "receivedBytes": stats["received_bytes"],
            })
        return result

    @staticmethod
    def _std_dev(data: List[float]) -> float:
        """
        计算总体标准差

        Args:
            data: 数据列表

        Returns:
            float: 标准差
        """
        if len(data) < 2:
            return 0.0
        mean = sum(data) / len(data)
        variance = sum((x - mean) ** 2 for x in data) / len(data)
        return math.sqrt(variance)

    @staticmethod
    def _apdex(times: List[float], threshold_ms: float = 500) -> float:
        """
        计算 Apdex（应用性能指数）

        Apdex = (满意数 + 容忍数 / 2) / 总请求数
        满意: 响应时间 <= threshold_ms
        容忍: 响应时间 <= 4 * threshold_ms

        Args:
            times: 响应时间列表（毫秒）
            threshold_ms: 满意阈值（默认 500ms）

        Returns:
            float: Apdex 值（0.0 ~ 1.0）
        """
        if not times:
            return 1.0
        tolerated = 4 * threshold_ms
        satisfied = sum(1 for t in times if t <= threshold_ms)
        tolerating = sum(1 for t in times if threshold_ms < t <= tolerated)
        total = len(times)
        return (satisfied + tolerating / 2) / total

    @staticmethod
    def _percentile(data: List[float], p: float) -> float:
        """
        计算百分位数（线性插值）

        Args:
            data: 排序前的数据列表
            p: 百分位（0-100）

        Returns:
            float: 百分位数值
        """
        if not data:
            return 0.0
        sorted_data = sorted(data)
        k = (len(sorted_data) - 1) * p / 100
        f = int(k)
        c = min(f + 1, len(sorted_data) - 1)
        if f == c:
            return sorted_data[f]
        return sorted_data[f] + (sorted_data[c] - sorted_data[f]) * (k - f)


# ==================== ResultValidator ====================

class ResultValidator:
    """
    结果验证器

    执行断言规则列表，判断一次请求是否符合预期。
    支持三种断言类型：
      - status_code: HTTP 状态码比较
      - response_time: 响应时间比较（ms）
      - response_body: 响应体内容检查（JSON Path / 文本包含）
    """

    @staticmethod
    def validate(assertions: list, status_code: int, body: str, response_time: float) -> bool:
        """
        执行所有断言，全部通过返回 True

        Args:
            assertions: 断言规则列表
            status_code: HTTP 状态码
            body: 响应体文本
            response_time: 响应时间（ms）

        Returns:
            bool: 所有断言通过返回 True
        """
        if not assertions:
            return True

        for assertion in assertions:
            if not ResultValidator._evaluate(assertion, status_code, body, response_time):
                return False
        return True

    @staticmethod
    def _evaluate(assertion: dict, status_code: int, body: str, response_time: float) -> bool:
        """
        评估单条断言

        Args:
            assertion: 断言规则字典
            status_code: HTTP 状态码
            body: 响应体文本
            response_time: 响应时间（ms）

        Returns:
            bool: 断言通过返回 True
        """
        atype = assertion.get("type", "")
        operator = assertion.get("operator", "equals")
        value = assertion.get("value")

        if atype == "status_code":
            return ResultValidator._compare(status_code, operator, value)
        elif atype == "response_time":
            return ResultValidator._compare(response_time, operator, value)
        elif atype == "response_body":
            return ResultValidator._check_body(assertion, body)
        else:
            # 未知断言类型默认通过
            return True

    @staticmethod
    def _compare(actual: Any, operator: str, expected: Any) -> bool:
        """
        比较运算符

        Args:
            actual: 实际值
            operator: 运算符
            expected: 期望值

        Returns:
            bool: 比较结果
        """
        try:
            if operator == "equals":
                return actual == expected
            elif operator == "not_equals":
                return actual != expected
            elif operator == "less_than":
                return float(actual) < float(expected)
            elif operator == "less_or_equal":
                return float(actual) <= float(expected)
            elif operator == "greater_than":
                return float(actual) > float(expected)
            elif operator == "greater_or_equal":
                return float(actual) >= float(expected)
            elif operator == "json_path":
                return ResultValidator._check_json_path(actual, expected)
            return True
        except (TypeError, ValueError) as e:
            logger.warning(f"断言比较异常: {e}")
            return False

    @staticmethod
    def _check_body(assertion: dict, body: str) -> bool:
        """
        检查响应体

        Args:
            assertion: 断言规则（含 path 和 value）
            body: 响应体文本

        Returns:
            bool: 检查通过返回 True
        """
        path = assertion.get("path", "")
        expected = assertion.get("value", "")
        operator = assertion.get("operator", "contains")

        if not path:
            # 无 path 时直接检查文本包含
            if operator == "contains":
                return str(expected) in body
            elif operator == "not_contains":
                return str(expected) not in body
            return True

        # 有 path 时尝试 JSON Path 解析
        try:
            data = json.loads(body)
            actual = data
            for key in path.strip("$.").split("."):
                if isinstance(actual, dict):
                    actual = actual.get(key)
                elif isinstance(actual, list):
                    try:
                        idx = int(key)
                        actual = actual[idx] if 0 <= idx < len(actual) else None
                    except ValueError:
                        actual = None
                else:
                    actual = None
                    break
            return ResultValidator._compare(actual, operator, expected)
        except (json.JSONDecodeError, KeyError, IndexError, TypeError):
            return False

    @staticmethod
    def _check_json_path(actual: Any, expected: Any) -> bool:
        """
        检查 JSON Path 值（占位，预留扩展）

        Args:
            actual: 实际值
            expected: 期望值

        Returns:
            bool: 匹配返回 True
        """
        return str(actual) == str(expected)


# ==================== _BurstBarrier ====================

class _BurstBarrier:
    """
    Burst 集合点同步屏障（兼容 Python 3.10）

    使用 asyncio.Condition + wait_for 实现超时等待，
    替代 Python 3.11+ 的 asyncio.Barrier(timeout=...)。

    Usage:
        >>> barrier = _BurstBarrier(party_count=10, timeout=30.0)
        >>> # 在多个协程中调用
        >>> await barrier.wait()
    """

    def __init__(self, party_count: int, timeout: float):
        """
        Args:
            party_count: 需要等待的协程数
            timeout: 超时秒数（超过此时间未到齐的协程继续执行）
        """
        self._party_count = party_count
        self._timeout = timeout
        self._condition = asyncio.Condition()
        self._arrived = 0
        self._phase = 0  # 用于区分轮次

    async def wait(self) -> None:
        """
        等待所有协程到达集合点

        当到达的协程数达到 party_count 时，所有协程同时释放。
        超时后同样释放（不阻塞）。
        """
        async with self._condition:
            self._arrived += 1
            current_phase = self._phase

            if self._arrived >= self._party_count:
                # 已到齐，唤醒所有等待者
                self._condition.notify_all()
                self._phase += 1
                self._arrived = 0
            else:
                # 等待直到到齐或超时
                try:
                    await asyncio.wait_for(
                        self._condition.wait_for(
                            lambda: self._phase != current_phase
                            or self._arrived >= self._party_count
                        ),
                        timeout=self._timeout,
                    )
                except asyncio.TimeoutError:
                    # 超时：当前线程继续执行
                    self._arrived -= 1
                    raise

    def reset(self) -> None:
        """重置屏障（下一轮使用）"""
        self._arrived = 0
        self._phase += 1


# ==================== PerformanceExecutor ====================

class PerformanceExecutor:
    """
    性能测试执行器

    轻量级 asyncio + aiohttp 自研引擎，支持三种压测模式：
      - concurrent: 固定并发数
      - qps: 固定 QPS（每秒请求数）
      - step: 阶梯并发（逐步递增）

    与 PlaywrightExecutor(UI测试) 和 SecurityExecutor(安全测试) 并列。

    Usage:
        >>> executor = PerformanceExecutor(http_client_config)
        >>> result = await executor.execute(task)
    """

    def __init__(self, http_client_config: dict, ref_provider: Optional[Callable[[str, str], list]] = None):
        """
        初始化执行器

        Args:
            http_client_config: HttpClient 配置字典
            ref_provider: 跨任务引用取值回调 (task_id, key) -> list
        """
        self.http_client_config = http_client_config
        self._http_client: Optional[HttpClient] = None
        self._token: Optional[str] = None
        self._running = False
        self._metrics: Optional[MetricsCollector] = None
        self._validator = ResultValidator()
        # 实时并发数（在途请求数）
        self._inflight = 0
        # 峰值 TPS（跨周期比较）
        self._peak_tps = 0.0
        # 执行机资源监控器（psutil 优先，标准库兜底）
        self._resource_monitor: Optional[ResourceMonitor] = None
        # 最新一次资源采样（供快照合并推送）
        self._latest_resource: Optional[dict] = None
        # 资源采样序列缓存（执行结束写入 resource_summary）
        self._resource_samples: List[Dict[str, Any]] = []
        self._resource_samples_lock = threading.Lock()
        # 目标机资源监控器（SSH 方式采集被测系统资源）
        self._target_resource_monitor: Optional[TargetResourceMonitor] = None
        # 最新一次目标机资源采样（供快照合并推送）
        self._latest_target_resource: Optional[dict] = None
        # 目标机资源采样序列缓存（执行结束写入 target_resource_summary）
        self._target_resource_samples: List[Dict[str, Any]] = []
        self._target_resource_samples_lock = threading.Lock()
        # 事务上下文（每个虚拟用户共享，{{captured.key}} 变量跨步骤传递）
        self._transaction_stats: List[Dict[str, Any]] = []
        self._transaction_stats_lock = threading.Lock()
        # 事务上下文（每个虚拟用户共享，{{captured.key}} 变量跨步骤传递）
        self._transaction_stats: List[Dict[str, Any]] = []
        self._transaction_stats_lock = threading.Lock()
        # 登录接口压测时复用的验证码 UUID（首次获取，后续复用）
        self._login_uuid: Optional[str] = None
        # 模板变量解析器（REF 占位符通过 ref_provider 跨任务取值）
        self._template_resolver = TemplateResolver(ref_provider=ref_provider)
        # 响应捕获缓冲区：{key: [values]}（压测结束后统一写库）
        self._captured: Dict[str, List[Any]] = {}
        # 捕获值数量上限，超出丢弃并告警
        self._capture_max = 10000

    def _get_http_client(self) -> HttpClient:
        """获取或创建 HttpClient 实例"""
        if not self._http_client:
            self._http_client = HttpClient(self.http_client_config)
        return self._http_client

    def _ensure_token(self, task) -> str:
        """
        确保有有效的 Token（复用 HttpClient.login）

        Args:
            task: PerformanceTask 任务对象

        Returns:
            str: Bearer Token
        """
        if not self._http_client:
            self._http_client = HttpClient(self.http_client_config)

        if not self._token:
            self._token = self._http_client.login(task.account_key)

        return self._token or ""

    def _is_login_target(self, task) -> bool:
        """
        判断目标任务是否为登录接口

        登录接口需要特殊处理：请求体包含 username/password/code/uuid，
        且请求头需要签名（X-RANDOM/X-TIMESTAMP/X-SIGN/RandomCode）。
        """
        login_path = self.http_client_config.get("auth", {}).get(
            "login_path", "/platform/api/auth/login"
        )
        url = (task.target_url or "").lower()
        return login_path in url

    def _get_login_body(self, task, csv_records: Optional[list] = None,
                        csv_fields: Optional[list] = None,
                        global_thread_idx: int = 0,
                        csv_variable_mapping: Optional[dict] = None) -> Dict[str, Any]:
        """
        构造登录接口请求体

        使用加密后的密码和验证码 UUID，确保每次压测请求的登录请求合法。
        支持 CSV 参数化：启用后按线程索引取当前虚拟用户的用户名/密码
        （通过 csv_variable_mapping 映射 CSV 列名）。

        Args:
            task: PerformanceTask 任务对象
            csv_records: CSV 参数化记录列表
            csv_fields: CSV 字段名列表
            global_thread_idx: 全局线程索引（用于 CSV 分配）
            csv_variable_mapping: CSV 列名→变量名映射，如 {"username": "{{csv.username}}", ...}

        Returns:
            dict: 登录请求体
        """
        client = self._get_http_client()
        accounts = self.http_client_config.get("accounts", {})
        account = accounts.get(task.account_key, {})
        username = account.get("username", "admin@xty")
        password = account.get("password", "Ubains@13579")
        captcha = accounts.get("captcha", "csba")

        # CSV 参数化：按线程索引取当前虚拟用户的用户名/密码并加密
        if csv_records and csv_fields:
            csv_idx = global_thread_idx % len(csv_records)
            csv_row = csv_records[csv_idx]
            mapping = csv_variable_mapping or {}
            csv_username_col = self._resolve_csv_column_name(mapping.get("username", ""))
            csv_password_col = self._resolve_csv_column_name(mapping.get("password", ""))
            if csv_username_col and csv_row.get(csv_username_col):
                username = csv_row[csv_username_col]
            if csv_password_col and csv_row.get(csv_password_col):
                password = csv_row[csv_password_col]

        # 加密密码
        encrypted_pwd = HttpClient.encrypt_password(password)

        # 获取验证码 UUID（复用，避免每次压测请求都请求验证码接口）
        if not self._login_uuid:
            self._login_uuid = client.get_captcha_uuid() or ""

        return {
            "username": username,
            "password": encrypted_pwd,
            "code": captcha,
            "uuid": self._login_uuid,
        }

    @staticmethod
    def _resolve_csv_column_name(mapping_value: Any) -> str:
        """
        从 CSV 变量映射值（如 {{csv.username}}）中提取 CSV 列名

        Args:
            mapping_value: 映射值，如 "{{csv.username}}"

        Returns:
            str: CSV 列名（匹配失败返回原字符串）
        """
        if not mapping_value:
            return ""
        match = re.search(r"\{\{csv\.([^}]+)\}\}", str(mapping_value))
        return match.group(1) if match else str(mapping_value)

    def _build_headers(self, task, body_for_sign: Any = None) -> Dict[str, str]:
        """
        构造请求头

        Args:
            task: PerformanceTask 任务对象
            body_for_sign: 用于签名的实际请求体（替换模板后），
                          None 时使用 task.body

        Returns:
            dict: 请求头字典
        """
        headers = {}
        if task.headers:
            headers.update(task.headers)

        headers.setdefault("Content-Type", "application/json")
        headers.setdefault("Accept", "application/json, text/plain, */*")

        # 登录接口需要签名（X-RANDOM/X-TIMESTAMP/X-SIGN/RandomCode）
        # 注意：登录接口不能带 Authorization 头，否则签名校验会失败
        # （签名是用 bearer_token="" 生成的，带 Authorization 会导致签名不匹配）
        if self._is_login_target(task):
            # 优先使用已解析的 body_for_sign（含 CSV 参数化后的用户名/密码）
            # 确保签名与实际发送的请求体一致
            body = body_for_sign if body_for_sign is not None else self._get_login_body(task)
            sign_headers = self._get_http_client()._generate_sign(
                body_data=body, bearer_token=""
            )
            headers.update(sign_headers)
        elif task.auth_required and self._token:
            headers["Authorization"] = f"Bearer {self._token}"

        # 非登录接口但需要签名（如创建会议接口，sign_request=True）
        if task.sign_request and not self._is_login_target(task) and self._token:
            if body_for_sign is None:
                body_for_sign = task.body if task.method.upper() in ("POST", "PUT", "PATCH") else None
            sign_headers = self._get_http_client()._generate_sign(
                body_data=body_for_sign, bearer_token=f"Bearer {self._token}"
            )
            headers.update(sign_headers)

        return headers

    def _resolve_body(self, task, request_index: int) -> Optional[Any]:
        """
        解析请求体（模板变量替换）

        优先使用 body_template（含 {__XXX__} 占位符），否则使用 body。

        Args:
            task: PerformanceTask 任务对象
            request_index: 当前请求序号

        Returns:
            Any: 替换后的请求体（dict/list/str/None）
        """
        method = task.method.upper()
        if method not in ("POST", "PUT", "PATCH"):
            return None

        template = task.body_template if getattr(task, "body_template", None) else task.body
        if template is None:
            return None

        resolved = self._template_resolver.resolve(template, request_index)

        # REF 引用为空时记录警告，方便定位
        if isinstance(resolved, (dict, list, str)):
            text = str(resolved)
            if "{__REF:" in text:
                logger.warning(f"任务 {task.id} 存在未解析的 REF 引用（上游任务可能无输出）: {text}")

        return resolved

    def _resolve_body_with_config(self, api_config: dict, request_index: int,
                                   fallback_body: Any = None) -> Optional[Any]:
        """
        解析长稳压测接口配置的请求体（模板变量替换）

        Args:
            api_config: 接口配置字典
            request_index: 当前请求序号
            fallback_body: 任务级 body 兜底

        Returns:
            Any: 替换后的请求体
        """
        method = api_config.get("method", "GET").upper()
        if method not in ("POST", "PUT", "PATCH"):
            return None

        body = api_config.get("body", fallback_body)
        if body is None:
            return None

        resolved = self._template_resolver.resolve(body, request_index)
        return resolved

    def _capture_response(self, capture_rules: Optional[list], body_text: str, status_code: int) -> None:
        """
        按捕获规则从响应体中提取字段，存入 _captured 缓冲区

        Args:
            capture_rules: 捕获规则列表 [{key, json_path, multiple}]
            body_text: 响应体文本
            status_code: HTTP 状态码
        """
        if not capture_rules:
            return
        if status_code < 200 or status_code >= 300:
            return  # 仅成功响应参与捕获

        try:
            data = json.loads(body_text)
        except (json.JSONDecodeError, TypeError):
            return  # 非 JSON 响应不捕获

        for rule in capture_rules:
            key = (rule.get("key") if isinstance(rule, dict) else None) or getattr(rule, "key", None)
            json_path = (rule.get("json_path") if isinstance(rule, dict) else None) or getattr(rule, "json_path", None)
            if not key or not json_path:
                continue

            values = self._extract_json_path(data, json_path)
            if values:
                buf = self._captured.setdefault(key, [])
                # 超出上限丢弃并告警（避免内存膨胀）
                if len(buf) >= self._capture_max:
                    logger.warning(f"捕获 key={key} 超过上限 {self._capture_max}，丢弃新值")
                    continue
                buf.extend(values)

    @staticmethod
    def _extract_json_path(data: Any, path: str) -> list:
        """
        从 JSON 数据中按路径提取字段值（简化版 JSONPath）

        支持：
        - $.a.b       → 嵌套字段
        - $.a[0].b    → 数组下标
        - $.*.name    → 通配（取所有子元素的 name）
        - $.items[*].id → 数组通配

        Args:
            data: 解析后的 JSON 数据（dict/list）
            path: JSONPath 表达式，如 "$.data.roomNo"

        Returns:
            list: 提取到的所有值
        """
        if not path or not path.startswith("$"):
            return []

        # 去掉 $ 前缀，按 . 和 [ ] 分词
        tokens = re.findall(r"\.([^.\[\]]+)|\[([^\[\]]*)\]", path[1:])
        results = [data]

        for dot_token, bracket_token in tokens:
            token = dot_token if dot_token else bracket_token
            next_results = []
            for item in results:
                if isinstance(item, dict):
                    if token == "*":
                        next_results.extend(item.values())
                    elif token in item:
                        next_results.append(item[token])
                elif isinstance(item, list):
                    if token == "*":
                        next_results.extend(item)
                    elif token.isdigit():
                        idx = int(token)
                        if 0 <= idx < len(item):
                            next_results.append(item[idx])
                    elif token == "":
                        next_results.extend(item)
            results = next_results
            if not results:
                break

        return results

    async def execute(self, task, snapshot_callback: Optional[Callable[[dict], None]] = None) -> dict:
        """
        执行性能测试任务

        Args:
            task: PerformanceTask 任务对象
            snapshot_callback: 每秒快照回调（用于 WebSocket 推送）

        Returns:
            dict: 执行结果汇总
        """
        self._metrics = MetricsCollector(snapshot_callback=snapshot_callback)
        self._running = True

        # 1. 获取 Token
        if task.auth_required:
            token = self._ensure_token(task)
            if not token:
                self._running = False
                return {
                    "error": True,
                    "error_message": f"登录失败，无法获取 Token (account: {task.account_key})",
                }

        # 2. 启动资源监控协程（与 _snapshot_loop 并行）
        from app.config import settings
        resource_task: Optional[asyncio.Task] = None
        target_resource_task: Optional[asyncio.Task] = None
        if settings.RESOURCE_MONITOR_ENABLED:
            # 执行机资源监控（本地资源）
            self._resource_monitor = ResourceMonitor(
                sampling_interval=settings.RESOURCE_MONITOR_INTERVAL
            )
            resource_task = asyncio.create_task(self._resource_loop())
            # 目标机资源监控（SSH 方式采集被测系统资源）
            if settings.TARGET_MONITOR_ENABLED:
                self._target_resource_monitor = TargetResourceMonitor(
                    host=settings.TARGET_MONITOR_HOST,
                    port=settings.TARGET_MONITOR_PORT,
                    username=settings.TARGET_MONITOR_USERNAME,
                    password=settings.TARGET_MONITOR_PASSWORD,
                    sampling_interval=settings.TARGET_MONITOR_INTERVAL,
                    max_retries=settings.TARGET_MONITOR_MAX_RETRIES,
                )
                target_resource_task = asyncio.create_task(self._target_resource_loop())

        # 3. 根据 mode 选择执行策略（场景模式优先于压测模式）
        try:
            scenario_type = getattr(task, "scenario_type", None)
            if scenario_type == "burst":
                result = await self._run_burst(task)
            elif scenario_type == "endurance":
                result = await self._run_endurance(task)
            elif task.mode == "concurrent":
                result = await self._run_concurrent(task)
            elif task.mode == "qps":
                result = await self._run_qps(task)
            elif task.mode == "step":
                result = await self._run_step(task)
            else:
                result = {"error": True, "error_message": f"未知压测模式: {task.mode}"}
        except Exception as e:
            logger.exception(f"压测执行异常: {e}")
            result = {"error": True, "error_message": str(e)}
        finally:
            self._running = False
            # 停止资源监控
            if resource_task:
                resource_task.cancel()
                try:
                    await resource_task
                except asyncio.CancelledError:
                    pass
            if self._resource_monitor:
                self._resource_monitor.dispose()
                self._resource_monitor = None
            # 停止目标机资源监控
            if target_resource_task:
                target_resource_task.cancel()
                try:
                    await target_resource_task
                except asyncio.CancelledError:
                    pass
            if self._target_resource_monitor:
                self._target_resource_monitor.dispose()
                self._target_resource_monitor = None

        # 4. 附加响应捕获结果（由服务层统一写库）
        if isinstance(result, dict):
            result["captured"] = self._captured
            # 资源监控汇总（执行机资源监控功能）
            result["resource_summary"] = self._build_resource_summary()
            # 事务处理时间汇总（多步骤事务功能）
            result["transaction_summary"] = self._build_transaction_summary()
            # 目标机资源监控汇总（SSH 方式采集被测系统资源）
            result["target_resource_summary"] = self._build_target_resource_summary()
            # 接口维度指标汇总（长稳压测多接口场景）
            result["api_summary"] = self._metrics._build_api_summary() if self._metrics else None

        return result

    async def _run_concurrent(self, task) -> dict:
        """
        并发模式：固定并发数，持续压测

        Args:
            task: PerformanceTask 任务对象

        Returns:
            dict: 汇总统计
        """
        semaphore = asyncio.Semaphore(task.concurrency)
        connector = aiohttp.TCPConnector(limit=task.concurrency, limit_per_host=task.concurrency, ssl=False)

        # 启动快照采集协程
        sampler_task = asyncio.create_task(self._snapshot_loop(task))

        # 解析 CSV 参数化数据（支持并发模式多账号登录）
        csv_records = []
        csv_fields = None
        if getattr(task, "csv_parameterization_enabled", False):
            csv_records, csv_fields = self._parse_csv(task)

        try:
            async with aiohttp.ClientSession(connector=connector) as session:
                # 预热阶段：逐步增加并发
                if task.ramp_up > 0:
                    ramp_start = time.time()
                    ramp_workers = []
                    for i in range(task.concurrency):
                        if not self._running:
                            break
                        ramp_workers.append(
                            asyncio.create_task(self._worker(session, task, semaphore, i, csv_records, csv_fields))
                        )
                        # 在预热时间内均匀启动
                        await asyncio.sleep(task.ramp_up / max(task.concurrency, 1))
                    ramp_elapsed = time.time() - ramp_start
                    logger.info(f"预热完成: {len(ramp_workers)} 个协程已启动, 耗时 {ramp_elapsed:.1f}s")
                else:
                    # 无预热，直接启动所有协程
                    ramp_workers = [
                        asyncio.create_task(self._worker(session, task, semaphore, i, csv_records, csv_fields))
                        for i in range(task.concurrency)
                    ]

                # 压测阶段：等待指定时长（分段检查停止）
                await self._sleep_with_check(task.duration)
                self._running = False

                # 等待所有 worker 结束
                await asyncio.gather(*ramp_workers, return_exceptions=True)
        finally:
            sampler_task.cancel()
            try:
                await sampler_task
            except asyncio.CancelledError:
                pass

        return self._metrics.summary()

    async def _run_qps(self, task) -> dict:
        """
        QPS 模式：控制每秒请求速率

        通过令牌桶算法控制请求速率，确保每秒请求数不超过 qps_target。

        Args:
            task: PerformanceTask 任务对象

        Returns:
            dict: 汇总统计
        """
        connector = aiohttp.TCPConnector(limit=200, limit_per_host=200, ssl=False)
        sampler_task = asyncio.create_task(self._snapshot_loop(task))

        # 解析 CSV 参数化数据（支持 QPS 模式多账号登录）
        csv_records = []
        csv_fields = None
        if getattr(task, "csv_parameterization_enabled", False):
            csv_records, csv_fields = self._parse_csv(task)

        # 令牌桶参数
        rate = task.qps_target  # 每秒令牌数
        tokens = rate           # 初始令牌数
        last_refill = time.time()
        min_interval = 1.0 / max(rate, 1)  # 最小请求间隔

        # 并发请求控制
        semaphore = asyncio.Semaphore(task.concurrency if task.concurrency > 0 else 50)

        try:
            async with aiohttp.ClientSession(connector=connector) as session:
                end_time = time.time() + task.duration
                pending = []

                while self._running and time.time() < end_time:
                    # 补充令牌
                    now = time.time()
                    elapsed = now - last_refill
                    tokens = min(rate, tokens + elapsed * rate)
                    last_refill = now

                    # 消耗令牌并发起请求
                    qps_index = 0
                    while tokens >= 1.0 and self._running:
                        tokens -= 1.0
                        task_obj = asyncio.create_task(
                            self._send_request(session, task, qps_index,
                                               csv_records=csv_records, csv_fields=csv_fields,
                                               global_thread_idx=qps_index)
                        )
                        qps_index += 1
                        pending.append(task_obj)
                        await asyncio.sleep(min_interval * 0.5)  # 微间隔避免突发

                    # 清理已完成的任务
                    pending = [p for p in pending if not p.done()]

                    # 等待一小段时间再检查令牌（分段检查停止）
                    if tokens < 1.0:
                        await self._sleep_with_check(min_interval * 0.5)

                self._running = False

                # 等待剩余请求完成
                if pending:
                    await asyncio.gather(*pending, return_exceptions=True)
        finally:
            sampler_task.cancel()
            try:
                await sampler_task
            except asyncio.CancelledError:
                pass

        return self._metrics.summary()

    async def _run_step(self, task) -> dict:
        """
        阶梯模式：逐步递增并发数

        step_concurrency = [10, 50, 100, 200] 表示 4 个阶梯
        step_duration = 60 表示每阶梯持续 60 秒

        Args:
            task: PerformanceTask 任务对象

        Returns:
            dict: 汇总统计
        """
        step_concurrency = task.step_concurrency or [task.concurrency]
        step_duration = task.step_duration or task.duration

        if not step_concurrency:
            step_concurrency = [task.concurrency]

        connector = aiohttp.TCPConnector(limit=200, limit_per_host=200, ssl=False)
        sampler_task = asyncio.create_task(self._snapshot_loop(task))

        # 解析 CSV 参数化数据（支持阶梯模式多账号登录）
        csv_records = []
        csv_fields = None
        if getattr(task, "csv_parameterization_enabled", False):
            csv_records, csv_fields = self._parse_csv(task)

        try:
            async with aiohttp.ClientSession(connector=connector) as session:
                for step_idx, concurrent in enumerate(step_concurrency):
                    if not self._running:
                        break

                    logger.info(f"阶梯 {step_idx + 1}/{len(step_concurrency)}: 并发数={concurrent}, 时长={step_duration}s")
                    semaphore = asyncio.Semaphore(concurrent)

                    # 启动当前阶梯的 workers
                    workers = [
                        asyncio.create_task(self._worker(session, task, semaphore, i, csv_records, csv_fields))
                        for i in range(concurrent)
                    ]

                    # 持续当前阶梯时长（分段检查停止）
                    await self._sleep_with_check(step_duration)

                    # 停止当前阶梯的 workers
                    self._running = False
                    await asyncio.gather(*workers, return_exceptions=True)
                    self._running = True  # 为下一阶梯重置

        finally:
            self._running = False
            sampler_task.cancel()
            try:
                await sampler_task
            except asyncio.CancelledError:
                pass

        return self._metrics.summary()

    async def _run_burst(self, task) -> dict:
        """
        瞬时并发（burst/spike）模式：集合点同步 + 循环次数/无限循环

        所有虚拟用户在集合点（Synchronizing Timer）等待，直到到达指定数量的线程，
        然后同时释放发起请求。适用于瞬间高并发场景如秒杀、签到。

        关键参数：
        - loop_type: 'finite'（有限次）| 'infinite'（无限循环直至时长结束）
        - loop_count: 有限循环次数
        - synchronizing_timer_enabled/count/timeout: 集合点配置
        - error_action: 'continue' | 'stop'（失败是否停止整个场景）

        Args:
            task: PerformanceTask 任务对象

        Returns:
            dict: 汇总统计
        """
        loop_type = getattr(task, "loop_type", "finite") or "finite"
        loop_count = getattr(task, "loop_count", 1) or 1
        sync_enabled = getattr(task, "synchronizing_timer_enabled", False) or False
        sync_count = getattr(task, "synchronizing_timer_count", task.concurrency) or task.concurrency
        sync_timeout = getattr(task, "synchronizing_timer_timeout", 30000) or 30000
        error_action = getattr(task, "error_action", "continue") or "continue"

        burst_duration = task.duration if loop_type == "infinite" else 0
        burst_end = time.time() + burst_duration if burst_duration > 0 else None

        connector = aiohttp.TCPConnector(limit=task.concurrency, limit_per_host=task.concurrency, ssl=False)
        sampler_task = asyncio.create_task(self._snapshot_loop(task))

        # 集合点同步屏障（asyncio.Condition 实现，兼容 Python 3.10）
        barrier = _BurstBarrier(
            party_count=min(sync_count, task.concurrency),
            timeout=sync_timeout / 1000.0,  # ms → s
        )
        # 失败停止信号（error_action=stop 时，任一 worker 失败触发）
        stop_event = asyncio.Event()

        # 解析 CSV 参数化数据（支持 burst 场景多账号登录）
        csv_records = []
        csv_fields = None
        if getattr(task, "csv_parameterization_enabled", False):
            csv_records, csv_fields = self._parse_csv(task)

        try:
            async with aiohttp.ClientSession(connector=connector) as session:
                iteration = 0
                while self._running:
                    if loop_type == "finite" and iteration >= loop_count:
                        break
                    if loop_type == "infinite" and burst_end and time.time() >= burst_end:
                        break
                    iteration += 1

                    logger.info(
                        f"Burst 迭代 {iteration}/{loop_count if loop_type == 'finite' else '∞'}: "
                        f"启动 {task.concurrency} 个并发协程"
                    )

                    # 启动所有 worker（每个 worker 在集合点等待）
                    workers = [
                        asyncio.create_task(
                            self._burst_worker(session, task, barrier, error_action, iteration, stop_event, i,
                                               csv_records, csv_fields)
                        )
                        for i in range(task.concurrency)
                    ]

                    # 等待所有 worker 完成（集合点同步+请求+返回）
                    await asyncio.gather(*workers, return_exceptions=True)

                    # 重置 barrier 供下一轮使用
                    barrier.reset()

                    # error_action=stop 且有 worker 失败 → 终止整个场景
                    if stop_event.is_set():
                        logger.warning(f"Burst 场景因请求失败提前终止 (error_action=stop, iteration={iteration})")
                        break

                    # 有限循环：一轮结束后检查是否还有剩余时长
                    if loop_type == "finite" and iteration < loop_count:
                        # 两轮之间间隔 1s 喘息
                        await self._sleep_with_check(1.0)

                self._running = False
        finally:
            sampler_task.cancel()
            try:
                await sampler_task
            except asyncio.CancelledError:
                pass

        return self._metrics.summary()

    async def _burst_worker(self, session: aiohttp.ClientSession, task,
                            barrier: "_BurstBarrier", error_action: str,
                            iteration: int, stop_event: asyncio.Event,
                            thread_idx: int,
                            csv_records: Optional[list] = None,
                            csv_fields: Optional[list] = None) -> None:
        """
        Burst 工作协程：在集合点等待 → 同时发送请求 → 检查结果

        Args:
            session: aiohttp 会话
            task: PerformanceTask 任务对象
            barrier: 集合点同步屏障
            error_action: 失败策略（continue/stop）
            iteration: 当前迭代轮次
            stop_event: 失败停止信号（error_action=stop 时使用）
            thread_idx: 当前线程在迭代内的索引
            csv_records: CSV 参数化记录列表（多账号登录时按线程分配）
            csv_fields: CSV 字段名列表
        """
        request_index = (iteration - 1) * task.concurrency + thread_idx

        # 集合点等待（所有线程在此同步）
        try:
            await barrier.wait()
        except asyncio.TimeoutError:
            # 超时后继续执行（不阻塞）
            logger.debug("Burst barrier 超时，继续执行")

        # 失败停止信号已触发，跳过本次请求
        if stop_event.is_set():
            return

        # 全局线程索引（跨迭代累计，确保 CSV 账号不重复分配）
        global_thread_idx = (iteration - 1) * task.concurrency + thread_idx

        # 发送请求
        before_fail = self._metrics.fail_count
        await self._send_request(
            session, task, request_index,
            csv_records=csv_records, csv_fields=csv_fields,
            global_thread_idx=global_thread_idx,
        )
        after_fail = self._metrics.fail_count

        # error_action=stop 且本次请求失败 → 通知其他线程停止
        if error_action == "stop" and after_fail > before_fail:
            stop_event.set()

    async def _run_endurance(self, task) -> dict:
        """
        长稳压测（endurance）模式：多接口分线程组 + 思考时间 + CSV 参数化

        支持多接口（scenario_apis）配置，每个接口分配独立的线程数，
        总并发数 = Σ(各接口 thread_count)。各接口独立运行，互不干扰。

        关键参数：
        - scenario_apis: [{name, method, url, headers, body, thread_count, assertions, unique_fields}]
        - think_time_enabled/min/max/distribution: 思考时间（constant/gaussian）
        - csv_parameterization_enabled/content/variable_mapping: CSV 参数化

        Args:
            task: PerformanceTask 任务对象

        Returns:
            dict: 汇总统计（含 api_summary 接口维度指标）
        """
        scenario_apis = getattr(task, "scenario_apis", None) or []
        if not scenario_apis:
            return {"error": True, "error_message": "长稳压测未配置接口列表 (scenario_apis)"}

        total_concurrency = sum(api.get("thread_count", 1) for api in scenario_apis)
        connector = aiohttp.TCPConnector(
            limit=max(total_concurrency, 200),
            limit_per_host=max(total_concurrency, 200),
            ssl=False,
        )
        sampler_task = asyncio.create_task(self._snapshot_loop(task))

        # 解析 CSV 参数化数据
        csv_records = []
        csv_fields = None
        if getattr(task, "csv_parameterization_enabled", False):
            csv_records, csv_fields = self._parse_csv(task)

        try:
            async with aiohttp.ClientSession(connector=connector) as session:
                # 按接口创建线程组
                api_workers = []
                global_thread_idx = 0
                for api_idx, api_config in enumerate(scenario_apis):
                    thread_count = api_config.get("thread_count", 1)
                    api_name = api_config.get("name", f"api_{api_idx}")

                    for t in range(thread_count):
                        worker = asyncio.create_task(
                            self._endurance_worker(
                                session, task, api_config, api_name,
                                global_thread_idx, csv_records, csv_fields,
                            )
                        )
                        api_workers.append(worker)
                        global_thread_idx += 1

                # 压测阶段：持续指定时长（分段检查停止）
                await self._sleep_with_check(task.duration)
                self._running = False

                # 等待所有 worker 结束
                await asyncio.gather(*api_workers, return_exceptions=True)
        finally:
            sampler_task.cancel()
            try:
                await sampler_task
            except asyncio.CancelledError:
                pass

        return self._metrics.summary()

    async def _endurance_worker(self, session: aiohttp.ClientSession, task,
                                api_config: dict, api_name: str,
                                global_thread_idx: int,
                                csv_records: list, csv_fields: list) -> None:
        """
        长稳压测工作协程：持续发送指定接口的请求，含思考时间和 CSV 参数化

        Args:
            session: aiohttp 会话
            task: PerformanceTask 任务对象
            api_config: 接口配置字典
            api_name: 接口名称（用于指标统计）
            global_thread_idx: 全局线程索引（用于 CSV 分配）
            csv_records: CSV 参数字段列表
            csv_fields: CSV 字段名列表
        """
        request_index = 0
        while self._running:
            await self._send_request(
                session, task, request_index,
                api_config=api_config, api_name=api_name,
                csv_records=csv_records, csv_fields=csv_fields,
                global_thread_idx=global_thread_idx,
            )
            request_index += 1

            # 思考时间（Think Time）
            if getattr(task, "think_time_enabled", False):
                await self._think_time(task)

    async def _think_time(self, task) -> None:
        """
        思考时间等待

        在请求之间模拟用户思考/操作延迟。
        - constant: 固定时间 = (min + max) / 2
        - gaussian: 高斯分布随机时间，范围 [min, max]

        Args:
            task: PerformanceTask 任务对象
        """
        tt_min = getattr(task, "think_time_min", 1000) or 1000
        tt_max = getattr(task, "think_time_max", 3000) or 3000
        tt_dist = getattr(task, "think_time_distribution", "constant") or "constant"

        if tt_dist == "gaussian":
            mu = (tt_min + tt_max) / 2.0
            sigma = (tt_max - tt_min) / 6.0  # 99.7% 落在 [min, max]
            delay = random.gauss(mu, sigma)
            delay = max(tt_min, min(tt_max, delay))
        else:
            # constant: 取中间值
            delay = (tt_min + tt_max) / 2.0

        await asyncio.sleep(delay / 1000.0)  # ms → s

    @staticmethod
    def _parse_csv(task) -> tuple:
        """
        解析 CSV 参数化内容

        Args:
            task: PerformanceTask 任务对象

        Returns:
            tuple: (records, fieldnames)
                records: [{field: value, ...}, ...] 字典列表
                fieldnames: [str, ...] 字段名列表
        """
        csv_content = getattr(task, "csv_content", None) or ""
        if not csv_content.strip():
            return [], []

        try:
            reader = csv.DictReader(io.StringIO(csv_content))
            records = list(reader)
            fieldnames = reader.fieldnames or []
            return records, fieldnames
        except Exception as e:
            logger.warning(f"CSV 解析失败: {e}")
            return [], []

    @staticmethod
    def _resolve_csv_vars(data: Any, csv_record: dict, csv_fields: list) -> Any:
        """
        递归替换 CSV 参数化变量 {{csv.字段名}} 为 CSV 记录中的实际值

        Args:
            data: 待替换的数据（dict/list/str）
            csv_record: 当前 CSV 行记录
            csv_fields: CSV 字段名列表

        Returns:
            Any: 替换后的数据
        """
        if not csv_record or not csv_fields:
            return data

        if isinstance(data, str):
            def _replace(match):
                key = match.group(1)
                val = csv_record.get(key)
                return str(val) if val is not None else match.group(0)
            return re.sub(r"\{\{csv\.([^}]+)\}\}", _replace, data)

        if isinstance(data, dict):
            return {
                k: PerformanceExecutor._resolve_csv_vars(v, csv_record, csv_fields)
                for k, v in data.items()
            }

        if isinstance(data, list):
            return [
                PerformanceExecutor._resolve_csv_vars(item, csv_record, csv_fields)
                for item in data
            ]

        return data


    async def _worker(self, session: aiohttp.ClientSession, task,
                      semaphore: asyncio.Semaphore,
                      thread_idx: int = 0,
                      csv_records: Optional[list] = None,
                      csv_fields: Optional[list] = None) -> None:
        """
        工作协程：循环发送请求直到停止

        多步骤事务模式（task.steps 非空）走 _send_transaction，
        单接口模式走 _send_request（向后兼容）。

        Args:
            session: aiohttp 会话
            task: PerformanceTask 任务对象
            semaphore: 并发信号量
            thread_idx: 线程索引（用于 CSV 分配）
            csv_records: CSV 参数化记录列表
            csv_fields: CSV 字段名列表
        """
        request_index = 0
        steps = self._get_transaction_steps(task)
        while self._running:
            async with semaphore:
                if steps:
                    await self._send_transaction(session, task, steps, request_index)
                else:
                    await self._send_request(
                        session, task, request_index,
                        csv_records=csv_records, csv_fields=csv_fields,
                        global_thread_idx=thread_idx,
                    )
                request_index += 1

    @staticmethod
    def _get_transaction_steps(task) -> list:
        """
        获取事务步骤配置（steps 非空列表 → 事务模式，None/空 → 单接口模式）

        Args:
            task: PerformanceTask 任务对象

        Returns:
            list: 事务步骤列表（空列表表示单接口模式）
        """
        steps = getattr(task, "steps", None) or []
        return [s for s in steps if isinstance(s, dict) and s.get("url")]

    async def _send_request(self, session: aiohttp.ClientSession, task, request_index: int = 0,
                            api_config: Optional[dict] = None, api_name: str = "",
                            csv_records: Optional[list] = None, csv_fields: Optional[list] = None,
                            global_thread_idx: int = 0) -> None:
        """
        发送单个 HTTP 请求并记录指标

        Args:
            session: aiohttp 会话
            task: PerformanceTask 任务对象
            request_index: 请求序号（用于 {__INDEX__} / REF 轮询）
            api_config: 长稳压测接口配置（覆盖 method/url/headers/body/assertions/unique_fields）
            api_name: 接口名称（用于接口维度指标统计）
            csv_records: CSV 参数化记录列表
            csv_fields: CSV 字段名列表
            global_thread_idx: 全局线程索引（用于 CSV 分配）
        """
        start = time.time()
        status = 0
        body = ""
        elapsed_ms = 0.0
        latency_ms = 0.0
        connect_ms = 0.0
        sent_bytes = 0
        received_bytes = 0

        # 实时并发数（在途请求数）统计
        self._inflight += 1
        try:
            # 长稳压测：使用 api_config 覆盖 method/url/headers/body/assertions/unique_fields
            if api_config:
                method = api_config.get("method", "GET").upper()
                url = api_config.get("url", task.target_url)
                api_headers = dict(task.headers or {})
                api_headers.update(api_config.get("headers", {}) or {})
                api_body = api_config.get("body", task.body)
                api_assertions = api_config.get("assertions", task.assertions) or []
                api_unique_fields = api_config.get("unique_fields", getattr(task, "unique_fields", []) or [])
            else:
                method = task.method.upper()
                url = task.target_url
                api_headers = task.headers
                api_body = task.body
                api_assertions = task.assertions or []
                api_unique_fields = getattr(task, "unique_fields", []) or []

            # 登录接口使用自动构造的请求体（含加密密码 + 验证码UUID）
            # 支持 CSV 参数化：启用后按线程索引取不同虚拟用户的用户名/密码
            if self._is_login_target(task):
                json_data = self._get_login_body(
                    task, csv_records, csv_fields, global_thread_idx,
                    getattr(task, "csv_variable_mapping", None) or {}
                )
            else:
                # 长稳压测：使用 api_config 提供的 body 模板解析
                if api_config:
                    json_data = self._resolve_body_with_config(api_config, request_index, api_body)
                else:
                    json_data = self._resolve_body(task, request_index)

            # ⭐ 唯一性字段处理：对已解析的 body 按配置生成唯一值
            if json_data is not None and api_unique_fields:
                json_data = TemplateResolver.apply_unique_fields(
                    json_data, api_unique_fields, request_index
                )

            # CSV 参数化替换（长稳压测）
            if csv_records and csv_fields and json_data is not None:
                csv_idx = global_thread_idx % len(csv_records)
                csv_record = csv_records[csv_idx]
                json_data = self._resolve_csv_vars(json_data, csv_record, csv_fields)

            headers = self._build_headers(task, body_for_sign=json_data)
            # 长稳压测：合并 api_config 级请求头
            if api_config:
                api_extra_headers = api_config.get("headers", {}) or {}
                headers.update(api_extra_headers)

            # ⚠️ 防御性修复：字符串 body 反序列化回 JSON 对象
            # 防止前端直传字符串导致 json=str 双重序列化 -> API 接收字符串字面量 -> 4XX
            if isinstance(json_data, str):
                try:
                    json_data = json.loads(json_data)
                except (json.JSONDecodeError, TypeError):
                    logger.warning(f"任务 {task.id} body 不是合法 JSON 字符串，原样发送: {str(json_data)[:80]}")
                    pass

            # 发送字节数估算（GET/无请求体时为 0）
            if json_data is not None:
                sent_bytes = len(json.dumps(json_data, ensure_ascii=False).encode("utf-8"))

            connect_start = time.time()
            async with session.request(
                method, url,
                headers=headers,
                json=json_data,
                ssl=False,
                timeout=aiohttp.ClientTimeout(total=30),
            ) as resp:
                # 进入上下文：已收到响应头 → 计算首字节延迟与 TCP 连接时间
                headers_received_time = time.time()
                latency_ms = (headers_received_time - start) * 1000
                connect_ms = (headers_received_time - connect_start) * 1000

                status = resp.status
                body = await resp.text()
                received_bytes = len(body.encode("utf-8"))
                elapsed_ms = (time.time() - start) * 1000

                # 执行断言
                assert_ok = self._validator.validate(
                    api_assertions, status, body, elapsed_ms
                )

                # 记录指标（长稳压测带 api_name 用于接口维度统计）
                self._metrics.record(
                    status, elapsed_ms, assert_ok,
                    latency_ms=latency_ms, connect_ms=connect_ms,
                    sent_bytes=sent_bytes, received_bytes=received_bytes,
                    api_name=api_name,
                )

                # 响应捕获（从响应中提取字段保存到 _captured）
                self._capture_response(task.capture_rules, body, status)

        except asyncio.TimeoutError:
            elapsed_ms = (time.time() - start) * 1000
            self._metrics.record(
                0, elapsed_ms, False, "timeout",
                sent_bytes=sent_bytes, error_type="timeout",
                api_name=api_name,
            )
        except aiohttp.ClientConnectorError as e:
            elapsed_ms = (time.time() - start) * 1000
            self._metrics.record(
                0, elapsed_ms, False, str(e),
                sent_bytes=sent_bytes, error_type="connect_error",
                api_name=api_name,
            )
        except aiohttp.ClientError as e:
            elapsed_ms = (time.time() - start) * 1000
            self._metrics.record(
                0, elapsed_ms, False, str(e),
                sent_bytes=sent_bytes, error_type="client_error",
                api_name=api_name,
            )
        except Exception as e:
            elapsed_ms = (time.time() - start) * 1000
            self._metrics.record(
                0, elapsed_ms, False, str(e),
                sent_bytes=sent_bytes, error_type="client_error",
                api_name=api_name,
            )
        finally:
            self._inflight -= 1

    # ==================== 事务模式（多步骤） ====================

    async def _send_transaction(self, session: aiohttp.ClientSession, task, steps: list, request_index: int) -> None:
        """
        发送一个完整事务（多步骤顺序执行）

        每个虚拟用户拥有独立的 shared_context，步骤间通过 {{captured.key}} 传参。
        失败策略：fail_fast（步骤失败即终止事务）/ continue（继续后续步骤）

        Args:
            session: aiohttp 会话
            task: PerformanceTask 任务对象
            steps: 事务步骤配置列表
            request_index: 请求序号
        """
        shared_context: Dict[str, Any] = {}
        step_results: List[Dict[str, Any]] = []
        transaction_start = time.time()
        transaction_timeout = getattr(task, "transaction_timeout", 60) or 60
        fail_policy = getattr(task, "transaction_fail_policy", "fail_fast") or "fail_fast"
        overall_success = True

        for step_idx, step in enumerate(steps):
            # 事务超时检查
            if time.time() - transaction_start > transaction_timeout:
                overall_success = False
                step_results.append({
                    "step_name": step.get("name", f"step_{step_idx}"),
                    "step_index": step_idx,
                    "status": 0,
                    "success": False,
                    "elapsed_ms": 0.0,
                    "error_message": "事务超时",
                })
                break

            step_start = time.time()
            status = 0
            body = ""
            elapsed_ms = 0.0
            step_error = ""

            try:
                method = step.get("method", "GET").upper()
                url = step.get("url", "")

                # 解析步骤级的请求体（支持模板变量 + {{captured.key}} 变量）
                step_body = self._resolve_step_body(step, request_index, shared_context)

                # 步骤级请求头（合并任务级 + 步骤级，步骤级优先级更高）
                step_headers = dict(getattr(task, "headers", {}) or {})
                step_headers.update(step.get("headers", {}) or {})

                # 登录 Token 继承（任务级已获取的 token）
                if task.auth_required and self._token:
                    step_headers["Authorization"] = f"Bearer {self._token}"

                # 签名处理（如果任务或步骤要求签名）
                if getattr(task, "sign_request", False) and self._token:
                    sign_headers = self._get_http_client()._generate_sign(
                        body_data=step_body, bearer_token=f"Bearer {self._token}"
                    )
                    step_headers.update(sign_headers)

                step_headers.setdefault("Content-Type", "application/json")

                # 发送请求
                async with session.request(
                    method, url,
                    headers=step_headers,
                    json=step_body,
                    ssl=False,
                    timeout=aiohttp.ClientTimeout(total=step.get("timeout", 30) or 30),
                ) as resp:
                    status = resp.status
                    body = await resp.text()
                    elapsed_ms = (time.time() - step_start) * 1000

                # 步骤级断言验证
                step_assertions = step.get("assertions", []) or []
                step_assert_ok = self._validator.validate(
                    step_assertions, status, body, elapsed_ms
                ) if step_assertions else True

                # 步骤级响应捕获（存入 shared_context 供后续步骤 {{captured.key}} 使用）
                step_captured: Dict[str, Any] = {}
                step_capture_rules = step.get("capture_rules", []) or []
                if step_capture_rules and 200 <= status < 300:
                    try:
                        data = json.loads(body)
                    except (json.JSONDecodeError, TypeError):
                        data = None
                    if data:
                        for rule in step_capture_rules:
                            key = rule.get("key", "") if isinstance(rule, dict) else ""
                            json_path = rule.get("json_path", "") if isinstance(rule, dict) else ""
                            if key and json_path:
                                values = self._extract_json_path(data, json_path)
                                if values:
                                    step_captured[key] = values[0]  # 只取第一个值

                # 合并到 shared_context
                shared_context.update(step_captured)

                step_success = step_assert_ok and (200 <= status < 400)
                step_results.append({
                    "step_name": step.get("name", f"step_{step_idx}"),
                    "step_index": step_idx,
                    "status": status,
                    "success": step_success,
                    "elapsed_ms": round(elapsed_ms, 2),
                    "latency_ms": 0.0,
                    "connect_ms": 0.0,
                    "sent_bytes": len(json.dumps(step_body or {}, ensure_ascii=False).encode("utf-8")) if step_body else 0,
                    "received_bytes": len(body.encode("utf-8")),
                    "error_message": "",
                    "captured": step_captured,
                })

                if not step_success:
                    overall_success = False
                    if fail_policy == "fail_fast":
                        break  # 失败即终止整个事务

            except asyncio.TimeoutError:
                elapsed_ms = (time.time() - step_start) * 1000
                step_error = "timeout"
                overall_success = False
                step_results.append({
                    "step_name": step.get("name", f"step_{step_idx}"),
                    "step_index": step_idx,
                    "status": 0,
                    "success": False,
                    "elapsed_ms": round(elapsed_ms, 2),
                    "error_message": "响应超时",
                })
                if fail_policy == "fail_fast":
                    break
            except aiohttp.ClientError as e:
                elapsed_ms = (time.time() - step_start) * 1000
                step_error = str(e)
                overall_success = False
                step_results.append({
                    "step_name": step.get("name", f"step_{step_idx}"),
                    "step_index": step_idx,
                    "status": 0,
                    "success": False,
                    "elapsed_ms": round(elapsed_ms, 2),
                    "error_message": str(e),
                })
                if fail_policy == "fail_fast":
                    break
            except Exception as e:
                elapsed_ms = (time.time() - step_start) * 1000
                step_error = str(e)
                overall_success = False
                step_results.append({
                    "step_name": step.get("name", f"step_{step_idx}"),
                    "step_index": step_idx,
                    "status": 0,
                    "success": False,
                    "elapsed_ms": round(elapsed_ms, 2),
                    "error_message": str(e),
                })
                if fail_policy == "fail_fast":
                    break

        transaction_elapsed_ms = (time.time() - transaction_start) * 1000

        # 记录事务总体指标到 MetricsCollector（简化：按事务整体记录一条记录）
        first_step = step_results[0] if step_results else {}
        self._metrics.record(
            status=first_step.get("status", 0) if step_results else 0,
            elapsed_ms=transaction_elapsed_ms,
            success=overall_success,
            error=step_error,
            sent_bytes=sum(s.get("sent_bytes", 0) for s in step_results),
            received_bytes=sum(s.get("received_bytes", 0) for s in step_results),
            error_type="client_error" if not overall_success else "",
        )

        # 保存事务步骤详情到 _transaction_stats（供汇总报告）
        with self._transaction_stats_lock:
            self._transaction_stats.append({
                "transactionIndex": request_index,
                "success": overall_success,
                "totalElapsedMs": round(transaction_elapsed_ms, 2),
                "steps": step_results,
            })

    def _resolve_step_body(self, step: dict, request_index: int, shared_context: Dict[str, Any]) -> Optional[Any]:
        """
        解析事务步骤的请求体（支持模板变量 + {{captured.key}} 变量）

        {{captured.key}} 优先级：高于 {__INDEX__} 等模板变量

        Args:
            step: 步骤配置字典
            request_index: 请求序号
            shared_context: 当前虚拟用户的共享上下文（包含前置步骤捕获值）

        Returns:
            Any: 解析后的请求体（dict/list/str/None）
        """
        method = step.get("method", "GET").upper()
        if method not in ("POST", "PUT", "PATCH"):
            return None

        template = step.get("body_template") or step.get("body")
        if template is None:
            return None

        # 1. 先解析模板变量（{__XXX__} 占位符）
        resolved = self._template_resolver.resolve(template, request_index)

        # 2. 再解析 {{captured.key}} 变量（从 shared_context 取值）
        resolved = self._resolve_captured_vars(resolved, shared_context)

        # 字符串反序列化（防御性修复）
        if isinstance(resolved, str):
            try:
                resolved = json.loads(resolved)
            except (json.JSONDecodeError, TypeError):
                pass

        return resolved

    @staticmethod
    def _resolve_captured_vars(data: Any, shared_context: Dict[str, Any]) -> Any:
        """
        递归替换 {{captured.key}} 占位符为 shared_context 中的实际值

        Args:
            data: 待替换的数据（dict/list/str）
            shared_context: 共享上下文（前置步骤捕获值）

        Returns:
            Any: 替换后的数据
        """
        if not shared_context:
            return data

        if isinstance(data, str):
            # 替换 {{captured.xxx}} 为 shared_context[xxx] 的值
            def _replace(match):
                key = match.group(1)
                val = shared_context.get(key)
                if val is None:
                    logger.warning(f"{{{{captured.{key}}}}} 未找到，保留原样")
                    return match.group(0)
                return str(val)

            return re.sub(r"\{\{captured\.([^}]+)\}\}", _replace, data)

        if isinstance(data, dict):
            return {
                k: PerformanceExecutor._resolve_captured_vars(v, shared_context)
                for k, v in data.items()
            }

        if isinstance(data, list):
            return [
                PerformanceExecutor._resolve_captured_vars(item, shared_context)
                for item in data
            ]

        return data

    def _build_transaction_summary(self) -> Optional[dict]:
        """
        构建事务处理时间汇总（执行结束后调用）

        Returns:
            dict: {
                "totalTransactions": int,
                "successCount": int,
                "failCount": int,
                "avgElapsedMs": float,
                "maxElapsedMs": float,
                "stepSummaries": [{stepName, avgElapsedMs, maxElapsedMs, successCount, failCount, sampleCount}]
            }
            无事务数据返回 None
        """
        with self._transaction_stats_lock:
            stats = list(self._transaction_stats)

        if not stats:
            return None

        total = len(stats)
        success_count = sum(1 for s in stats if s.get("success", False))
        fail_count = total - success_count
        elapsed_vals = [s.get("totalElapsedMs", 0.0) for s in stats]

        # 按步骤名聚合
        step_map: Dict[str, Dict[str, Any]] = {}
        for txn in stats:
            for step in txn.get("steps", []):
                step_name = step.get("step_name", "unknown")
                entry = step_map.setdefault(step_name, {
                    "stepName": step_name,
                    "elapsedList": [],
                    "successCount": 0,
                    "failCount": 0,
                })
                entry["elapsedList"].append(step.get("elapsed_ms", 0.0))
                if step.get("success", False):
                    entry["successCount"] += 1
                else:
                    entry["failCount"] += 1

        step_summaries = []
        for step_name, entry in step_map.items():
            el = entry["elapsedList"]
            step_summaries.append({
                "stepName": step_name,
                "sampleCount": len(el),
                "avgElapsedMs": round(sum(el) / len(el), 2) if el else 0.0,
                "maxElapsedMs": round(max(el), 2) if el else 0.0,
                "minElapsedMs": round(min(el), 2) if el else 0.0,
                "successCount": entry["successCount"],
                "failCount": entry["failCount"],
            })

        return {
            "totalTransactions": total,
            "successCount": success_count,
            "failCount": fail_count,
            "avgElapsedMs": round(sum(elapsed_vals) / len(elapsed_vals), 2) if elapsed_vals else 0.0,
            "maxElapsedMs": round(max(elapsed_vals), 2) if elapsed_vals else 0.0,
            "minElapsedMs": round(min(elapsed_vals), 2) if elapsed_vals else 0.0,
            "stepSummaries": step_summaries,
        }

    async def _sleep_with_check(self, seconds: float) -> None:
        """
        分段睡眠，期间持续检查停止标志，保证 stop() 能快速生效

        Args:
            seconds: 总睡眠时长（秒）
        """
        remaining = float(seconds)
        while remaining > 0 and self._running:
            # 分 0.5s 一段检查，停止信号最长 0.5s 内响应
            chunk = min(remaining, 0.5)
            await asyncio.sleep(chunk)
            remaining -= chunk

    async def _snapshot_loop(self, task) -> None:
        """
        快照采集循环：每秒采集一次指标并回调

        资源监控数据（_latest_resource）合并进同一快照推送，
        前端 MonitorPanel 直接读取 data.resource 字段。

        Args:
            task: PerformanceTask 任务对象
        """
        while self._running:
            await asyncio.sleep(1)
            try:
                # 更新峰值 TPS（比较上一周期的 tps）
                snapshot = self._metrics.snapshot(
                    peak_tps=self._peak_tps,
                    concurrent_users=self._inflight,
                )
                if snapshot.get("tps", 0) > self._peak_tps:
                    self._peak_tps = snapshot["tps"]
                # 合并执行机资源指标（资源监控功能）
                with self._resource_samples_lock:
                    latest_resource = dict(self._latest_resource) if self._latest_resource else None
                if latest_resource:
                    snapshot["resource"] = latest_resource
                # 合并目标机资源指标（SSH 目标机资源监控功能）
                with self._target_resource_samples_lock:
                    latest_target = dict(self._latest_target_resource) if self._latest_target_resource else None
                if latest_target:
                    snapshot["targetResource"] = latest_target
                if self._metrics.snapshot_callback:
                    self._metrics.snapshot_callback(snapshot)
            except Exception as e:
                logger.error(f"快照采集异常: {e}")

    async def _resource_loop(self) -> None:
        """
        资源监控采集循环：每采样间隔采集一次执行机资源指标

        与 _snapshot_loop 并行运行（同一事件循环内），
        采样结果存入 _latest_resource（供快照合并）和 _resource_samples（供汇总）。
        """
        from app.config import settings
        interval = settings.RESOURCE_MONITOR_INTERVAL
        while self._running:
            await asyncio.sleep(interval)
            if not self._running:
                break
            try:
                if self._resource_monitor:
                    sample = self._resource_monitor.sample()
                    sample["timestamp"] = time.time()
                    with self._resource_samples_lock:
                        self._latest_resource = sample
                        # 序列上限 3000 点（约 50 分钟 @1s），超出丢弃最旧
                        self._resource_samples.append(sample)
                        if len(self._resource_samples) > 3000:
                            # 降采样：每 2 点取 1（PRD 数据量控制策略）
                            self._resource_samples = self._resource_samples[::2]
            except asyncio.CancelledError:
                raise
            except Exception as e:
                logger.debug(f"资源采样异常: {e}")

    def _build_resource_summary(self) -> Optional[dict]:
        """
        构建资源监控汇总（执行结束后调用）

        Returns:
            dict: {
                "stats": {avg/max CPU、内存、网络速率},
                "series": [{elapsed, cpuPercent, memPercent, netSentKbS, netRecvKbS}]
            }
            无采样数据返回 None（报告页据此隐藏资源区块）
        """
        with self._resource_samples_lock:
            samples = list(self._resource_samples)

        if not samples:
            return None

        cpu_vals = [s.get("cpuPercent", 0.0) for s in samples]
        mem_vals = [s.get("memPercent", 0.0) for s in samples]
        sent_vals = [s.get("netSentKbS", 0.0) for s in samples]
        recv_vals = [s.get("netRecvKbS", 0.0) for s in samples]

        # 序列（带相对时间轴，供趋势图渲染）
        first_ts = samples[0].get("timestamp", 0.0)
        series = []
        for s in samples:
            series.append({
                "elapsed": round(s.get("timestamp", 0.0) - first_ts, 1) if first_ts else 0.0,
                "cpuPercent": s.get("cpuPercent", 0.0),
                "memPercent": s.get("memPercent", 0.0),
                "netSentKbS": s.get("netSentKbS", 0.0),
                "netRecvKbS": s.get("netRecvKbS", 0.0),
            })

        return {
            "stats": {
                "sampleCount": len(samples),
                "avgCpuPercent": round(sum(cpu_vals) / len(cpu_vals), 1),
                "maxCpuPercent": round(max(cpu_vals), 1),
                "avgMemPercent": round(sum(mem_vals) / len(mem_vals), 1),
                "maxMemPercent": round(max(mem_vals), 1),
                "avgSentKbS": round(sum(sent_vals) / len(sent_vals), 1),
                "avgRecvKbS": round(sum(recv_vals) / len(recv_vals), 1),
                "maxSentKbS": round(max(sent_vals), 1),
                "maxRecvKbS": round(max(recv_vals), 1),
            },
            "series": series,
        }

    async def _target_resource_loop(self) -> None:
        """
        目标机资源监控采集循环：每采样间隔通过 SSH 采集一次目标机资源指标

        与 _resource_loop 并行运行（同一事件循环内），
        采样结果存入 _latest_target_resource（供快照合并）和
        _target_resource_samples（供汇总）。

        SSH 采集为同步阻塞调用（含网络 IO），单次耗时通常 < 50ms，
        在 1s 采样间隔下对压测事件循环影响可忽略。
        """
        from app.config import settings
        interval = settings.TARGET_MONITOR_INTERVAL
        while self._running:
            await asyncio.sleep(interval)
            if not self._running:
                break
            try:
                if self._target_resource_monitor:
                    sample = self._target_resource_monitor.sample()
                    with self._target_resource_samples_lock:
                        self._latest_target_resource = sample
                        self._target_resource_samples.append(sample)
                        if len(self._target_resource_samples) > 3000:
                            # 降采样：每 2 点取 1（数据量控制策略）
                            self._target_resource_samples = self._target_resource_samples[::2]
            except asyncio.CancelledError:
                raise
            except Exception as e:
                logger.debug(f"目标机资源采样异常: {e}")

    def _build_target_resource_summary(self) -> Optional[dict]:
        """
        构建目标机资源监控汇总（执行结束后调用）

        Returns:
            dict: {
                "host": str, 目标机地址
                "stats": {sampleCount, avg/max CPU、内存、负载},
                "series": [{elapsed, cpuPercent, memPercent, loadAvg1}],
                "javaProcessStats": [        # 每个匹配 Java 进程的统计汇总
                    {
                        "processName": str,
                        "avgCpuPercent": float,
                        "maxCpuPercent": float,
                        "avgMemPercent": float,
                        "maxMemPercent": float,
                        "avgRssMb": float,
                        "maxRssMb": float,
                    }, ...
                ],
                "javaProcessSeries": [       # 每个匹配 Java 进程随时间变化序列
                    {
                        "processName": str,
                        "data": [{elapsed, cpuPercent, memPercent, rssMb}, ...]
                    }, ...
                ]
            }
            无采样数据返回 None（报告页据此隐藏目标机资源区块）
        """
        with self._target_resource_samples_lock:
            samples = list(self._target_resource_samples)

        if not samples:
            return None

        # 过滤不可用样本（SSH 断开期间 available=False 的采样）
        usable = [s for s in samples if s.get("available", False)]
        if not usable:
            return None

        cpu_vals = [s.get("cpuPercent", 0.0) for s in usable]
        mem_vals = [s.get("memPercent", 0.0) for s in usable]
        load_vals = [s.get("loadAvg1", 0.0) for s in usable]

        first_ts = usable[0].get("timestamp", 0.0)
        series = []
        for s in usable:
            series.append({
                "elapsed": round(s.get("timestamp", 0.0) - first_ts, 1) if first_ts else 0.0,
                "cpuPercent": s.get("cpuPercent", 0.0),
                "memPercent": s.get("memPercent", 0.0),
                "loadAvg1": s.get("loadAvg1", 0.0),
            })

        # ====== Java 进程汇总 ======
        # 收集所有样本中出现的 Java 进程按 processName 组织
        java_process_map: dict = {}  # processName -> {cpu[], mem[], rss[], data[{elapsed,...}]}
        for s in usable:
            ts = s.get("timestamp", 0.0)
            elapsed = round(ts - first_ts, 1) if first_ts else 0.0
            for proc in s.get("javaProcesses", []):
                pname = proc["processName"]
                if pname not in java_process_map:
                    java_process_map[pname] = {
                        "cpu": [],
                        "mem": [],
                        "rss": [],
                        "data": [],
                    }
                java_process_map[pname]["cpu"].append(proc.get("cpuPercent", 0.0))
                java_process_map[pname]["mem"].append(proc.get("memPercent", 0.0))
                java_process_map[pname]["rss"].append(proc.get("rssMb", 0.0))
                java_process_map[pname]["data"].append({
                    "elapsed": elapsed,
                    "cpuPercent": proc.get("cpuPercent", 0.0),
                    "memPercent": proc.get("memPercent", 0.0),
                    "rssMb": proc.get("rssMb", 0.0),
                })

        java_process_stats = []
        java_process_series = []
        for pname, vals in java_process_map.items():
            cpu_v = vals["cpu"]
            mem_v = vals["mem"]
            rss_v = vals["rss"]
            java_process_stats.append({
                "processName": pname,
                "avgCpuPercent": round(sum(cpu_v) / len(cpu_v), 1) if cpu_v else 0.0,
                "maxCpuPercent": round(max(cpu_v), 1) if cpu_v else 0.0,
                "avgMemPercent": round(sum(mem_v) / len(mem_v), 1) if mem_v else 0.0,
                "maxMemPercent": round(max(mem_v), 1) if mem_v else 0.0,
                "avgRssMb": round(sum(rss_v) / len(rss_v), 1) if rss_v else 0.0,
                "maxRssMb": round(max(rss_v), 1) if rss_v else 0.0,
            })
            java_process_series.append({
                "processName": pname,
                "data": vals["data"],
            })

        from app.config import settings
        result = {
            "host": settings.TARGET_MONITOR_HOST,
            "stats": {
                "sampleCount": len(usable),
                "avgCpuPercent": round(sum(cpu_vals) / len(cpu_vals), 1),
                "maxCpuPercent": round(max(cpu_vals), 1),
                "avgMemPercent": round(sum(mem_vals) / len(mem_vals), 1),
                "maxMemPercent": round(max(mem_vals), 1),
                "avgLoadAvg1": round(sum(load_vals) / len(load_vals), 2),
                "maxLoadAvg1": round(max(load_vals), 2),
            },
            "series": series,
        }
        if java_process_stats:
            result["javaProcessStats"] = java_process_stats
            result["javaProcessSeries"] = java_process_series
        return result

    def stop(self) -> None:
        """停止压测"""
        self._running = False
        logger.info("性能测试执行器已停止")