提交 7f4e853f authored 作者: 陈泽健's avatar 陈泽健

feat(performance): 批量执行进度WebSocket + 增强指标数据模型 + 合并报告PRD

- 新增 /ws/batch-progress WebSocket 固定分组(perf_batch)实时推送批量进度,
  须注册在 /ws/{task_id} 之前, 字段统一 snake_case(current_index/total_count/
  current_task_name/success_count/fail_count), 支持 ping/pong 保活
- frontend ProjectDetail.vue 批量进度字段 camelCase -> snake_case 对齐
- PerformanceTask 新增 16 项增强指标(p95/std_dev/latency/connect_time/peak_tps/
  apdex/错误分类计数)
- 新增 PerformanceBatchExecution / PerformanceProjectReport 模型 + 合并报告
  PRD 与执行计划文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 d0757333
# PRD - 批量执行合并报告
> **文档版本**: v1.0
> **创建日期**: 2026-08-20
> **模块类型**: 性能测试
> **优先级**: P0
---
## 一、背景与目标
### 1.1 背景
当前性能测试模块已实现**项目管理与批量执行**功能:
- 用户可将任务归入项目(文件夹)
- 支持「执行全部」和「勾选批量执行」
- 串行逐个执行任务,每任务出一份独立报告
但问题是:**批量执行后,每个任务各出一份报告,用户需要逐个查看,无法从整体上评估项目维度的性能表现。** 例如「会议预约」项目下有 3 个接口(新建/修改/取消会议),批量执行后用户希望看到整个项目的总体性能概况,而不是分别翻看 3 份报告。
### 1.2 目标
1. **批量执行后生成合并报告**:将批次内所有任务的执行结果聚合为一份综合报告
2. **项目级报告查看**:项目管理页面增加「查看报告」入口,展示项目整体性能
3. **保留逐任务明细**:合并报告不替代原有单任务报告,两者共存
4. **兼容单任务执行**:单任务执行行为不变,只合并批量执行的场景
---
## 二、功能范围
| 功能 | 描述 | 优先级 |
|------|------|--------|
| 存储批量执行记录 | 保存 batch_id、所属项目、任务列表、执行时间 | P0 |
| 合并报告数据模型 | 存储聚合后的项目级性能指标 | P0 |
| 批量执行后自动生成合并报告 | 所有任务完成后,自动聚合指标生成合并报告 | P0 |
| 合并报告 API | 按 batch_id 或 project_id 查询合并报告 | P0 |
| 合并报告前端页面 | 展示聚合指标 + 各任务明细列表 | P0 |
| 项目详情页跳转合并报告 | 批量执行完成后可点击查看合并报告 | P0 |
| 单任务报告保持独立 | 不影响现有单任务报告查看功能 | P0 |
---
## 三、数据模型设计
### 3.1 PerformanceBatchExecution(已有结构扩展)
当前 `batch_run_tasks` 方法已生成 `batch_id`,但未持久化存储。需要新增表:
```sql
CREATE TABLE performance_batch_executions (
id VARCHAR(64) PRIMARY KEY, -- 批量执行ID (batch_xxx)
project_id VARCHAR(64) DEFAULT NULL, -- 关联项目ID(可选,直接批量执行可能无项目)
task_ids TEXT NOT NULL, -- 执行的任务ID列表(JSON数组)
status VARCHAR(20) DEFAULT 'running', -- running/completed/partially_failed
total_count INT DEFAULT 0, -- 总任务数
success_count INT DEFAULT 0, -- 成功数
fail_count INT DEFAULT 0, -- 失败数
started_at DATETIME, -- 开始时间
completed_at DATETIME, -- 完成时间
created_at DATETIME -- 创建时间
);
```
### 3.2 PerformanceProjectReport(新表)
存储项目维度的合并报告:
```sql
CREATE TABLE performance_project_reports (
id VARCHAR(64) PRIMARY KEY, -- 报告唯一标识 (rpt_xxx)
project_id VARCHAR(64) NOT NULL, -- 关联项目ID
batch_id VARCHAR(64) DEFAULT NULL, -- 关联批量执行ID(可选)
task_ids TEXT NOT NULL, -- 包含的任务ID列表(JSON数组)
-- 聚合指标(与单任务指标对齐,但数值为聚合值)
total_requests INT DEFAULT 0, -- 总请求数
success_count INT DEFAULT 0, -- 总成功数
fail_count INT DEFAULT 0, -- 总失败数
error_rate FLOAT DEFAULT 0.0, -- 整体错误率
actual_tps FLOAT DEFAULT 0.0, -- 整体 TPS(总请求/总实际时长)
avg_response_time FLOAT DEFAULT 0.0, -- 平均响应时间(所有请求加权平均)
min_response_time FLOAT DEFAULT 0.0, -- 最小响应时间
max_response_time FLOAT DEFAULT 0.0, -- 最大响应时间
p50_response_time FLOAT DEFAULT 0.0, -- P50
p90_response_time FLOAT DEFAULT 0.0, -- P90
p95_response_time FLOAT DEFAULT 0.0, -- P95
p99_response_time FLOAT DEFAULT 0.0, -- P99
std_dev FLOAT DEFAULT 0.0, -- 标准差
apdex FLOAT DEFAULT 0.0, -- Apdex 用户满意度
status_2xx INT DEFAULT 0,
status_3xx INT DEFAULT 0,
status_4xx INT DEFAULT 0,
status_5xx INT DEFAULT 0,
total_sent_bytes INT DEFAULT 0,
total_received_bytes INT DEFAULT 0,
peak_tps FLOAT DEFAULT 0.0, -- 所有任务中最高峰值 TPS
duration_actual FLOAT DEFAULT 0.0, -- 总实际耗时(从第一个任务开始到最后一个任务结束)
-- 任务明细(JSON)
task_summaries TEXT, -- 各任务摘要的 JSON 数组
-- 时间
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
### 3.3 聚合算法
合并报告的核心是将 N 个任务的指标**合理聚合**,而非简单相加:
| 指标 | 聚合方式 | 说明 |
|------|---------|------|
| total_requests | 求和 | 所有任务请求数相加 |
| success_count | 求和 | 所有任务成功数相加 |
| fail_count | 求和 | 所有任务失败数相加 |
| error_rate | 重算 | `fail_count / total_requests` |
| actual_tps | 重算 | `total_requests / 所有任务实际耗时之和` |
| avg_response_time | 加权平均 | `Σ(avg_rt_i × request_count_i) / total_requests` |
| min_response_time | 取最小值 | 所有任务中最小的 |
| max_response_time | 取最大值 | 所有任务中最大的 |
| p50/p90/p95/p99 | 加权近似 | 基于各任务分位数加权估算 |
| std_dev | 重算 | 基于聚合后的均值和方差计算 |
| apdex | 加权平均 | `Σ(apdex_i × request_count_i) / total_requests` |
| peak_tps | 取最大值 | 各任务 peak_tps 的最大值 |
| status_2xx/4xx/5xx | 求和 | 各任务状态码计数相加 |
| sent/received_bytes | 求和 | 各任务收发字节数相加 |
| duration_actual | 求和 | 各任务实际耗时相加 |
---
## 四、API 设计
### 4.1 合并报告 API
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/api/performance/projects/{project_id}/report` | 获取项目最新合并报告 |
| GET | `/api/performance/batch-reports/{batch_id}` | 按 batch_id 获取合并报告 |
| GET | `/api/performance/projects/{project_id}/reports` | 获取项目历史合并报告列表 |
### 4.2 批量执行响应扩展
现有 `POST /api/performance/tasks/batch-run``POST /api/performance/projects/{id}/run-all` 的响应增加 `report_id` 字段:
```json
{
"message": "批量执行完成: 3 成功, 0 失败",
"batch_id": "batch_xxx",
"task_count": 3,
"report_id": "rpt_xxx" // 新增
}
```
### 4.3 合并报告响应结构
```json
{
"id": "rpt_xxx",
"project_id": "proj_xxx",
"batch_id": "batch_xxx",
"created_at": "2026-08-20T12:00:00",
"summary": {
"total_requests": 10000,
"success_count": 9950,
"fail_count": 50,
"error_rate": 0.005,
"actual_tps": 150.5,
"peak_tps": 320.0,
"avg_response_time": 185.2,
"min_response_time": 45.0,
"max_response_time": 2500.0,
"p50_response_time": 120.0,
"p90_response_time": 350.0,
"p95_response_time": 500.0,
"p99_response_time": 1200.0,
"std_dev": 85.3,
"apdex": 0.92,
"status_2xx": 9800,
"status_4xx": 150,
"status_5xx": 50,
"total_sent_bytes": 512000,
"total_received_bytes": 10240000,
"duration_actual": 66.5
},
"task_summaries": [
{
"task_id": "perf_xxx",
"task_name": "新建会议",
"status": "completed",
"total_requests": 3000,
"success_count": 2980,
"fail_count": 20,
"avg_response_time": 150.0,
"actual_tps": 120.0
},
{
"task_id": "perf_yyy",
"task_name": "修改会议",
"status": "completed",
"total_requests": 4000,
"success_count": 3990,
"fail_count": 10,
"avg_response_time": 200.0,
"actual_tps": 160.0
}
]
}
```
---
## 五、前端页面设计
### 5.1 合并报告页面
新增 `/performance/project-report` 路由,复用现有 `ReportPanel.vue` 的布局风格:
- **顶部**:项目名称 + 批量执行时间 + 返回项目详情按钮
- **聚合指标卡片**(与现有单任务报告风格一致):
- 第一行:总请求数 / 总成功数 / 总失败数 / 错误率
- 第二行:整体 TPS / 峰值 TPS / 平均响应时间 / P95
- 第三行:标准差 / Apdex / 最小/最大响应时间
- **分位数表**:P50 / P90 / P95 / P99
- **状态码分布表**:2xx / 4xx / 5xx
- **各任务明细表**:列出每个任务的名称、状态、请求数、TPS、平均响应时间
- **导出 JSON** 功能
### 5.2 项目详情页改动
- 批量执行完成后,进度条下方增加「**查看合并报告**」按钮
- 项目头部增加「历史报告」按钮,查看该项目所有历史合并报告列表
### 5.3 路由
```
/performance/project-report?projectId=xxx&reportId=xxx ← 合并报告页(新增)
```
---
## 六、实施计划
| 阶段 | 内容 | 预估工时 |
|------|------|---------|
| Phase 1 | 数据模型 + 后端聚合逻辑 + API | 1 天 |
| Phase 2 | 前端合并报告页面 | 0.5 天 |
| Phase 3 | 项目详情页适配 + 联调 | 0.5 天 |
---
## 七、边界与约束
### 7.1 兼容性
- 单任务执行不变,报告格式不变
- 批量执行生成的合并报告不影响原有单任务报告
- 旧批量执行记录无合并报告(不回溯)
### 7.2 性能约束
- 合并报告聚合在任务执行完成后同步计算(串行,不影响执行性能)
- 快照数据不合并(各任务快照独立存储),合并报告仅包含聚合指标
### 7.3 注意事项
- 如果某个任务执行失败(status=failed),仍计入总请求数,但该任务的指标可能不完整
- 合并报告的 `error_rate` 基于所有任务的总失败数/总请求数重新计算,而非各任务 error_rate 的平均值
---
*本文档完成后,将使用 prd-plan skill 生成计划执行文档,再使用 prd-code skill 实现代码。*
\ No newline at end of file
# 执行计划 - 批量执行合并报告
> **关联 PRD**:`_PRD_批量执行合并报告.md`
> **创建日期**:2026-08-20
> **预计总工时**:2 天
---
## 一、执行概述
本计划将 PRD 中的「批量执行合并报告」功能分为 3 个 Phase 实施:
1. **Phase 1**:数据模型 + 后端聚合逻辑 + API(1 天)
2. **Phase 2**:前端合并报告页面(0.5 天)
3. **Phase 3**:项目详情页适配 + 联调部署(0.5 天)
每个 Phase 完成后可独立验收,Phase 1 是后续所有 Phase 的基础。
---
## 二、任务分解
### Phase 1:数据模型 + 后端聚合逻辑 + API
**目标**:新增 PerformanceBatchExecution(持久化)和 PerformanceProjectReport 模型,实现批量执行后的自动聚合,提供合并报告查询 API。
| # | 任务 | 关键文件 | 验收标准 |
|---|------|---------|---------|
| 1.1 | 新建 `PerformanceBatchExecution` ORM 模型 | `backend/app/models/performance.py` | 表字段与 PRD 3.1 一致(id/project_id/task_ids/status/total_count/success_count/fail_count/started_at/completed_at/created_at) |
| 1.2 | 新建 `PerformanceProjectReport` ORM 模型 | `backend/app/models/performance.py` | 表字段与 PRD 3.2 一致(id/project_id/batch_id/task_ids + 所有聚合指标字段 + task_summaries JSON) |
| 1.3 | 定义合并报告 Pydantic Schema | `backend/app/schemas/performance.py` | `ProjectReportResponse`(含 summary + task_summaries)+ `ProjectReportListResponse` |
| 1.4 | 数据库迁移(SQLite ALTER TABLE) | `backend/app/database.py` | 启动时自动建表(`_ensure_columns``Base.metadata.create_all`) |
| 1.5 | 实现 `_aggregate_task_results()` 聚合方法 | `backend/app/services/performance_service.py` | 接收 N 个任务 ID,按 PRD 3.3 聚合算法计算合并指标,返回报告字典 |
| 1.6 | 改造 `batch_run_tasks()` 持久化 batch 记录 + 执行完自动聚合 | `backend/app/services/performance_service.py` | 开始执行时写入 `performance_batch_executions`(status=running),完成时更新 status + 调用 `_aggregate_task_results()` 生成合并报告并写入 `performance_project_reports` |
| 1.7 | 改造 `run_project_all()` 同样触发合并报告生成 | `backend/app/services/performance_service.py` | 调用 `batch_run_tasks()` 已自然继承 |
| 1.8 | 新增合并报告查询 API | `backend/app/routers/performance.py` | 3 条路由:GET `/projects/{project_id}/report`(最新)、GET `/batch-reports/{batch_id}`、GET `/projects/{project_id}/reports`(历史列表) |
| 1.9 | 批量执行响应扩展 `report_id` | `backend/app/routers/performance.py` | `BatchRunResponse` 新增 `report_id` 字段 |
| 1.10 | 更新 `BatchRunResponse` Schema | `backend/app/schemas/performance.py` | 新增 `report_id: Optional[str] = None` |
**验收方式**:通过 Swagger UI 手动测试——批量执行一组任务 → 确认 `performance_batch_executions` 表有记录 → 调用合并报告 API 返回聚合指标。
---
### Phase 2:前端合并报告页面
**目标**:新建合并报告页面,展示聚合指标卡片 + 各任务明细列表。
| # | 任务 | 关键文件 | 验收标准 |
|---|------|---------|---------|
| 2.1 | 新增合并报告类型定义 | `frontend/src/types/performance.ts` | `ProjectReportResponse``ProjectReportSummary``TaskSummaryItem` 接口 |
| 2.2 | 新增合并报告 API 调用 | `frontend/src/api/performance.ts` | `getProjectReport(projectId)``getProjectReports(projectId)``getBatchReport(batchId)` |
| 2.3 | 新建合并报告页面 `ProjectReport.vue` | `frontend/src/views/performance/ProjectReport.vue` | 复用 ReportPanel.vue 风格,展示聚合指标卡片(4行16卡)+ 分位数表 + 状态码分布 + 各任务明细表 |
| 2.4 | 注册合并报告路由 | `frontend/src/router/index.ts` | `/performance/project-report` 可访问,支持 query 参数 `projectId``reportId` |
| 2.5 | 批量执行完成后的跳转入口 | `frontend/src/views/performance/ProjectDetail.vue` | 批量执行完成后进度条下方出现「查看合并报告」按钮 |
**验收方式**:批量执行完成后点击「查看合并报告」→ 跳转到合并报告页面,看到聚合指标和各任务明细。
---
### Phase 3:项目详情页适配 + 联调
**目标**:完善项目详情页与合并报告的联动,整体联调验证。
| # | 任务 | 关键文件 | 验收标准 |
|---|------|---------|---------|
| 3.1 | 项目详情页批量执行反馈 | `ProjectDetail.vue` | 批量执行完成后取消 loading,显示「查看合并报告」按钮,点击跳转 `ProjectReport.vue` |
| 3.2 | 前端构建验证 | `npm run build` | 无 TypeScript 错误,构建通过 |
| 3.3 | 端到端联调 | 全部相关文件 | 创建项目 → 创建 2 个任务归入项目 → 批量执行 → 查看合并报告 → 看到聚合指标 + 各任务明细 |
| 3.4 | 部署到 5.60 服务器 | 后端 5 文件 + 前端 dist | 容器重启后功能正常 |
**验收方式**:完整端到端流程验证通过。
---
## 三、聚合算法实现细节
### 3.1 `_aggregate_task_results(tasks_summary, snapshots_all)` 伪代码
```
输入:tasks_data = [task1.to_dict(), task2.to_dict(), ...]
输出:合并指标字典
1. 基础求和
total_requests = Σ task.total_requests
success_count = Σ task.success_count
fail_count = Σ task.fail_count
status_2xx = Σ task.status_2xx (来自快照汇总)
status_4xx = Σ task.status_4xx
status_5xx = Σ task.status_5xx
total_sent_bytes = Σ task.total_sent_bytes
total_received_bytes = Σ task.total_received_bytes
2. 重算比率
error_rate = fail_count / total_requests
duration_actual = Σ task.duration_actual
actual_tps = total_requests / duration_actual (如果 > 0)
3. 响应时间
min_response_time = min(task.min_response_time)
max_response_time = max(task.max_response_time)
# 加权平均
avg_response_time = Σ(task.avg_response_time × task.total_requests) / total_requests
# 百分位数(加权近似)
p50 = 加权百分位数(p50_i, request_count_i)
p90 = 加权百分位数(p90_i, request_count_i)
p95 = 加权百分位数(p95_i, request_count_i)
p99 = 加权百分位数(p99_i, request_count_i)
4. 其他
peak_tps = max(task.peak_tps)
apdex = Σ(task.apdex × task.total_requests) / total_requests
std_dev = 基于聚合均值和方差计算(近似:√(Σ((std_dev_i² + mean_i²) × n_i) / N - 总均值²))
5. 任务明细
task_summaries = [{task_id, task_name, status, total_requests, success_count,
fail_count, avg_response_time, actual_tps, error_rate}]
```
### 3.2 加权百分位数算法
由于各任务的分位数是独立计算的,合并时无法精确计算全局百分位数,采用**加权近似法**
```python
def weighted_percentile(percentiles, counts, p):
"""
percentiles: [p50_1, p50_2, ...] 各任务的指定分位数
counts: 各任务请求数
p: 目标分位数 (50/90/95/99)
"""
# 按分位数值排序
pairs = sorted(zip(percentiles, counts))
sorted_vals, sorted_counts = zip(*pairs)
total = sum(sorted_counts)
target = total * p / 100
cumulative = 0
for val, cnt in zip(sorted_vals, sorted_counts):
cumulative += cnt
if cumulative >= target:
return val
return sorted_vals[-1]
```
---
## 四、验收标准总览
| 验收项 | 对应 Phase |
|--------|-----------|
| `performance_batch_executions` 表有数据 | Phase 1 |
| `performance_project_reports` 表有数据 | Phase 1 |
| 合并报告 API 返回聚合指标 | Phase 1 |
| 批量执行响应含 `report_id` | Phase 1 |
| 合并报告页面展示聚合指标卡片 | Phase 2 |
| 合并报告页面展示各任务明细 | Phase 2 |
| 批量执行完成后可跳转合并报告 | Phase 3 |
| 单任务报告不受影响 | Phase 3 |
| 前端构建通过 | Phase 3 |
---
## 五、测试计划
### 单元测试
- `_aggregate_task_results()` 聚合逻辑测试(验证加权平均、百分位数、求和等)
- 批量执行记录持久化测试
### 集成测试
- 创建 2 个任务 → 批量执行 → 验证合并报告指标正确性
- 单任务执行 → 验证不生成合并报告
### 手动测试
- Phase 1 完成后通过 Swagger UI 验证 API
- Phase 2 完成后通过浏览器验证前端页面
---
## 六、风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 加权百分位数不精确 | 合并报告的 P95/P99 与真实值有偏差 | 文档说明是近似值,标注算法;如需精确值需原始数据全量排序 |
| 批量执行后聚合计算耗时 | 用户等待时间长 | 聚合计算在内存中完成(O(N) 复杂度),通常 < 100ms |
| 大数据量下快照内存占用 | 大批量任务聚合时快照数据量大 | 聚合时不加载快照明细,仅从 task 表汇总指标 |
| 数据库锁(SQLite) | 并发写入合并报告失败 | 批量执行本身已串行,写库在 finally 块中 |
---
## 七、实施顺序建议
### 推荐顺序(前端 + 后端可并行)
**Step 1(后端核心)**:新建模型 + Schema → database.py 迁移 → `_aggregate_task_results()` 实现 → 改造 `batch_run_tasks()` 持久化 + 聚合 → 合并报告查询 API
**Step 2(前端核心)**:类型定义 → API 封装 → ProjectReport.vue 页面 → 路由注册
**Step 3(联调)**:ProjectDetail.vue 适配 → 端到端验证 → 构建 → 部署
---
## 八、后续工作
- [ ] Phase 1-3 实施完成后更新 CLAUDE.md 当前进度
- [ ] 更新 HANDOFF.md 交接文档
- [ ] Git 提交(Conventional Commits)
- [ ] 部署到 5.60 服务器
---
## 九、附录
### 现有文件参考
| 文件 | 用途 |
|------|------|
| `backend/app/services/performance_service.py` | `batch_run_tasks()` 约 998 行,需要改造持久化 + 聚合 |
| `backend/app/routers/performance.py` | 批量执行路由约 506 行,需要扩展 `report_id` |
| `backend/app/models/performance.py` | 现有模型约 380 行,新增 2 个模型 |
| `backend/app/schemas/performance.py` | 现有 Schema 约 377 行,新增合并报告 Schema |
| `frontend/src/views/performance/ReportPanel.vue` | 626 行,合并报告页面布局参考 |
| `frontend/src/views/performance/ProjectDetail.vue` | 439 行,需适配批量执行后的跳转 |
### 关键数据流
```
批量执行请求 → POST /api/performance/tasks/batch-run
batch_run_tasks() 开始
创建 performance_batch_executions 记录 (status=running)
串行逐个执行任务(已有的 run_task 逻辑不变)
所有任务执行完毕
_aggregate_task_results() 聚合所有任务指标
创建 performance_project_reports 记录
更新 performance_batch_executions (status=completed)
返回 { batch_id, report_id, ... }
前端跳转合并报告页 → GET /api/performance/projects/{id}/report
```
---
*本文档由 prd-plan skill 生成,关联 PRD:`_PRD_批量执行合并报告.md`*
\ No newline at end of file
......@@ -200,6 +200,25 @@ class PerformanceTask(Base):
actual_tps: Mapped[float] = mapped_column(Float, default=0.0, comment="实际吞吐量")
error_rate: Mapped[float] = mapped_column(Float, default=0.0, comment="错误率(%)")
# 增强指标
p95_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P95响应时间(ms)")
std_dev: Mapped[float] = mapped_column(Float, default=0.0, comment="响应时间标准差(ms)")
latency_avg: Mapped[float] = mapped_column(Float, default=0.0, comment="平均首字节延迟(ms)")
latency_min: Mapped[float] = mapped_column(Float, default=0.0, comment="最小首字节延迟(ms)")
latency_max: Mapped[float] = mapped_column(Float, default=0.0, comment="最大首字节延迟(ms)")
connect_time_avg: Mapped[float] = mapped_column(Float, default=0.0, comment="平均TCP连接时间(ms)")
connect_time_max: Mapped[float] = mapped_column(Float, default=0.0, comment="最大TCP连接时间(ms)")
peak_tps: Mapped[float] = mapped_column(Float, default=0.0, comment="峰值TPS")
total_sent_bytes: Mapped[int] = mapped_column(Integer, default=0, comment="总发送字节数")
total_received_bytes: Mapped[int] = mapped_column(Integer, default=0, comment="总接收字节数")
apdex: Mapped[float] = mapped_column(Float, default=0.0, comment="Apdex用户满意度")
error_type_timeout: Mapped[int] = mapped_column(Integer, default=0, comment="超时错误数")
error_type_connect_error: Mapped[int] = mapped_column(Integer, default=0, comment="连接错误数")
error_type_client_error: Mapped[int] = mapped_column(Integer, default=0, comment="客户端错误数")
error_type_http_4xx: Mapped[int] = mapped_column(Integer, default=0, comment="HTTP 4xx错误数")
error_type_http_5xx: Mapped[int] = mapped_column(Integer, default=0, comment="HTTP 5xx错误数")
error_type_assertion: Mapped[int] = mapped_column(Integer, default=0, comment="断言失败数")
# 时间
start_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="开始时间")
end_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="结束时间")
......@@ -392,3 +411,178 @@ class PerformanceSnapshot(Base):
"status_4xx": self.status_4xx,
"status_5xx": self.status_5xx,
}
class PerformanceBatchExecution(Base):
"""
批量执行记录数据库模型
持久化存储批量执行记录,用于追踪批量执行的状态和结果。
Attributes:
id (str): 批量执行ID (batch_xxx)
project_id (str): 关联项目ID(可选)
task_ids (str): 执行的任务ID列表(JSON数组)
status (str): running/completed/partially_failed
total_count (int): 总任务数
success_count (int): 成功数
fail_count (int): 失败数
started_at (datetime): 开始时间
completed_at (datetime): 完成时间
created_at (datetime): 创建时间
"""
__tablename__ = "performance_batch_executions"
id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="批量执行ID")
project_id: Mapped[Optional[str]] = mapped_column(
String(64), nullable=True, default=None, comment="关联项目ID"
)
task_ids: Mapped[str] = mapped_column(Text, default="[]", comment="任务ID列表(JSON)")
status: Mapped[str] = mapped_column(
String(20), default="running", comment="状态: running/completed/partially_failed"
)
total_count: Mapped[int] = mapped_column(Integer, default=0, comment="总任务数")
success_count: Mapped[int] = mapped_column(Integer, default=0, comment="成功数")
fail_count: Mapped[int] = mapped_column(Integer, default=0, comment="失败数")
started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="开始时间")
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, comment="完成时间")
created_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, comment="创建时间"
)
def __repr__(self) -> str:
return f"<PerformanceBatchExecution(id={self.id}, status={self.status})>"
def to_dict(self) -> dict:
return {
"id": self.id,
"project_id": self.project_id,
"task_ids": self.task_ids,
"status": self.status,
"total_count": self.total_count,
"success_count": self.success_count,
"fail_count": self.fail_count,
"started_at": self.started_at.isoformat() if self.started_at else None,
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
class PerformanceProjectReport(Base):
"""
项目合并报告数据库模型
存储项目维度的合并报告,将批次内所有任务的执行结果聚合为一份综合报告。
Attributes:
id (str): 报告唯一标识 (rpt_xxx)
project_id (str): 关联项目ID
batch_id (str): 关联批量执行ID(可选)
task_ids (str): 包含的任务ID列表(JSON数组)
# 聚合指标
total_requests (int): 总请求数
success_count (int): 总成功数
fail_count (int): 总失败数
error_rate (float): 整体错误率
actual_tps (float): 整体 TPS
avg_response_time (float): 平均响应时间(加权平均)
min_response_time (float): 最小响应时间
max_response_time (float): 最大响应时间
p50_response_time (float): P50
p90_response_time (float): P90
p95_response_time (float): P95
p99_response_time (float): P99
std_dev (float): 标准差
apdex (float): Apdex 用户满意度
status_2xx (int): 2xx 状态码计数
status_3xx (int): 3xx 状态码计数
status_4xx (int): 4xx 状态码计数
status_5xx (int): 5xx 状态码计数
total_sent_bytes (int): 总发送字节数
total_received_bytes (int): 总接收字节数
peak_tps (float): 所有任务中最高峰值 TPS
duration_actual (float): 总实际耗时
# 任务明细
task_summaries (str): 各任务摘要的 JSON 数组
created_at (datetime): 创建时间
"""
__tablename__ = "performance_project_reports"
id: Mapped[str] = mapped_column(String(64), primary_key=True, comment="报告唯一标识")
project_id: Mapped[str] = mapped_column(String(64), default="", comment="关联项目ID")
batch_id: Mapped[Optional[str]] = mapped_column(
String(64), nullable=True, default=None, comment="关联批量执行ID"
)
task_ids: Mapped[str] = mapped_column(Text, default="[]", comment="任务ID列表(JSON)")
# 聚合指标
total_requests: Mapped[int] = mapped_column(Integer, default=0, comment="总请求数")
success_count: Mapped[int] = mapped_column(Integer, default=0, comment="总成功数")
fail_count: Mapped[int] = mapped_column(Integer, default=0, comment="总失败数")
error_rate: Mapped[float] = mapped_column(Float, default=0.0, comment="整体错误率")
actual_tps: Mapped[float] = mapped_column(Float, default=0.0, comment="整体TPS")
avg_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="平均响应时间(ms)")
min_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="最小响应时间(ms)")
max_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="最大响应时间(ms)")
p50_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P50响应时间(ms)")
p90_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P90响应时间(ms)")
p95_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P95响应时间(ms)")
p99_response_time: Mapped[float] = mapped_column(Float, default=0.0, comment="P99响应时间(ms)")
std_dev: Mapped[float] = mapped_column(Float, default=0.0, comment="标准差(ms)")
apdex: Mapped[float] = mapped_column(Float, default=0.0, comment="Apdex用户满意度")
status_2xx: Mapped[int] = mapped_column(Integer, default=0, comment="2xx计数")
status_3xx: Mapped[int] = mapped_column(Integer, default=0, comment="3xx计数")
status_4xx: Mapped[int] = mapped_column(Integer, default=0, comment="4xx计数")
status_5xx: Mapped[int] = mapped_column(Integer, default=0, comment="5xx计数")
total_sent_bytes: Mapped[int] = mapped_column(Integer, default=0, comment="总发送字节数")
total_received_bytes: Mapped[int] = mapped_column(Integer, default=0, comment="总接收字节数")
peak_tps: Mapped[float] = mapped_column(Float, default=0.0, comment="峰值TPS")
duration_actual: Mapped[float] = mapped_column(Float, default=0.0, comment="总实际耗时(秒)")
# 任务明细
task_summaries: Mapped[str] = mapped_column(Text, default="[]", comment="任务摘要JSON")
# 时间
created_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, comment="创建时间"
)
def __repr__(self) -> str:
return f"<PerformanceProjectReport(id={self.id}, project_id={self.project_id})>"
def to_dict(self) -> dict:
return {
"id": self.id,
"project_id": self.project_id,
"batch_id": self.batch_id,
"task_ids": self.task_ids,
"total_requests": self.total_requests,
"success_count": self.success_count,
"fail_count": self.fail_count,
"error_rate": self.error_rate,
"actual_tps": self.actual_tps,
"avg_response_time": self.avg_response_time,
"min_response_time": self.min_response_time,
"max_response_time": self.max_response_time,
"p50_response_time": self.p50_response_time,
"p90_response_time": self.p90_response_time,
"p95_response_time": self.p95_response_time,
"p99_response_time": self.p99_response_time,
"std_dev": self.std_dev,
"apdex": self.apdex,
"status_2xx": self.status_2xx,
"status_3xx": self.status_3xx,
"status_4xx": self.status_4xx,
"status_5xx": self.status_5xx,
"total_sent_bytes": self.total_sent_bytes,
"total_received_bytes": self.total_received_bytes,
"peak_tps": self.peak_tps,
"duration_actual": self.duration_actual,
"task_summaries": self.task_summaries,
"created_at": self.created_at.isoformat() if self.created_at else None,
}
\ No newline at end of file
......@@ -291,6 +291,50 @@ async def get_task_results(
# ==================== Phase 3: WebSocket 实时监控 ====================
@router.websocket("/ws/batch-progress")
async def websocket_batch_progress(websocket: WebSocket):
"""
批量执行进度 WebSocket(固定分组,须注册在 /ws/{task_id} 之前)
客户端连接后订阅 perf_batch 分组,实时收到批量执行的进度广播。
消息格式(perf_batch_progress):
```json
{
"type": "perf_batch_progress",
"batch_id": "...",
"project_id": "...",
"current_index": 1,
"total_count": 5,
"current_task_id": "...",
"current_task_name": "任务名",
"current_status": "running",
"success_count": 1,
"fail_count": 0
}
```
current_status: running / completed / partially_failed / completed
"""
ws_group = "perf_batch"
await manager.connect(websocket, ws_group)
try:
# 保持连接,等待推送
while True:
try:
data = await websocket.receive_text()
msg = json.loads(data)
if msg.get("type") == "ping":
await manager.send_to(websocket, {"type": "pong"})
except json.JSONDecodeError:
pass
except WebSocketDisconnect:
logger.info("批量进度 WebSocket 断开连接")
except Exception as e:
logger.error(f"批量进度 WebSocket 异常: {e}")
finally:
await manager.disconnect(websocket, ws_group)
@router.websocket("/ws/{task_id}")
async def websocket_monitor(websocket: WebSocket, task_id: str):
"""
......
......@@ -377,11 +377,11 @@ function connectBatchWs() {
try {
const msg = JSON.parse(event.data)
if (msg.type === 'perf_batch_progress') {
batchCurrentIndex.value = msg.currentIndex
batchTotalCount.value = msg.totalCount
batchCurrentTaskName.value = msg.currentTaskName || ''
batchSuccessCount.value = msg.successCount || 0
batchFailCount.value = msg.failCount || 0
batchCurrentIndex.value = msg.current_index
batchTotalCount.value = msg.total_count
batchCurrentTaskName.value = msg.current_task_name || ''
batchSuccessCount.value = msg.success_count || 0
batchFailCount.value = msg.fail_count || 0
}
} catch {
// 忽略解析错误
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论