"""批量压测结果实时更新脚本(out_res/批量压测结果.xlsx)
每跑完一个接口的一个场景压测(预检/调试/全量各算一条记录),立即调用本脚本更新一次对应行。
openpyxl + 文件锁实现,多卡并行写安全(N 个 turing_sec 实例同时更新互不冲突)。
用法(与 AGENTS.md "批量多接口压测调度策略" 章节引用保持一致):
python scripts/update_batch_results.py \
--api aclnnAbs --scenario normal \
--total 10000 --success 9987 --fail 13 \
--fail_reason "OOM/shape过大 8条; 161001 dtype不支持 5条" \
--has_bug 不是bug --has_aicore 否 --card 0 \
--analyzed 否 --issue ""
# 预检/调试记录使用带 _precheck 后缀的 scenario(如 normal_precheck / multithread_precheck / consistent_precheck)
# 场景行已存在则更新(upsert),不存在则追加
表格列(8 个强制维度 + 4 个辅助/分析维度):
1.接口名 2.测试场景 3.压测总条数 4.实际执行成功条数 5.实际执行失败条数
6.执行失败原因 7.是否存在bug 8.是否存在aicore 9.执行卡号(辅) 10.更新时间(辅)
11.是否分析完成 12.问题单
"是否存在bug"为三态列(强制规范): 是bug / 不是bug / 需要人工确认
- 是bug:已确认问题(aicore_error 提单 / hash_mismatch>0 提单 / error 场景资料描述场景
doc_based=true 的意外成功(错误码不符不判问题、不记录) / 修复循环耗尽等)
- 不是bug:确认非问题(成功率达标且无 aicore_error / CPP 构造问题已修复 /
资料描述场景外不校验(error 模式 doc_based=false 场景不做错误码校验)等)
- 需要人工确认:暂无法自动判定(接口行为与资料描述不符待复核 / 根因存疑等)
"是否分析完成": 是/否(该行结果是否已完成人工/Agent 分析)
"问题单": 问题单号或链接(无则留空)
远程结果数据库(123.60.114.238:/home/30039325/fuzz_result)同步见 scripts/sync_result_server.py。
GitCode 仓实时同步(Alice_zhou/issue):本地表格每次更新成功后自动调用
scripts/sync_batch_results_git.py 将结果合并推送到仓内 `批量压测结果.xlsx`
(多机安全合并:行键=接口名+测试场景,本机行优先、他机行保留,禁止整表覆盖)——
不论压测任务由哪台机器拉起,仓内始终汇聚各机器的最新结果。
--no_git_sync 跳过本次同步;--git_sync 强制同步;全局开关见 config/issue_tracker.json
的 batch_result_git_sync(默认 true),环境变量 BATCH_RESULT_GIT_SYNC=0/1 可覆盖。
"""
import argparse
import contextlib
import os
import socket
import sys
import time
try:
import openpyxl
except ImportError:
print("[ERROR] openpyxl 未安装,请先执行: pip install openpyxl", file=sys.stderr)
sys.exit(1)
DEFAULT_XLSX = os.path.join("out_res", "批量压测结果.xlsx")
HEADERS = [
"接口名",
"测试场景",
"压测总条数",
"实际执行成功条数",
"实际执行失败条数",
"执行失败原因",
"是否存在bug",
"是否存在aicore",
"执行卡号",
"更新时间",
"是否分析完成",
"问题单",
]
BUG_STATES = ("是bug", "不是bug", "需要人工确认")
BUG_STATE_ALIASES = {
"是bug": "是bug", "是": "是bug", "yes": "是bug", "true": "是bug", "y": "是bug", "1": "是bug",
"不是bug": "不是bug", "否": "不是bug", "no": "不是bug", "false": "不是bug", "n": "不是bug", "0": "不是bug",
"需要人工确认": "需要人工确认", "人工确认": "需要人工确认", "待确认": "需要人工确认",
"需人工确认": "需要人工确认", "unknown": "需要人工确认", "uncertain": "需要人工确认",
}
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
class CrossPlatformLock:
"""跨平台文件锁(Linux: fcntl.flock / Windows: msvcrt.locking),带重试。"""
def __init__(self, path, timeout=60, retries=50):
self.path = path
self.timeout = timeout
self.retries = retries
self.fd = None
def __enter__(self):
self.fd = open(self.path, "a+")
last_err = None
for i in range(self.retries):
try:
try:
import fcntl
fcntl.flock(self.fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
return self
except ImportError:
import msvcrt
self.fd.seek(0)
msvcrt.locking(self.fd.fileno(), msvcrt.LK_NBLCK, 1)
return self
except OSError as err:
last_err = err
time.sleep(min(0.1 * (i + 1), 1.0))
raise TimeoutError(
"获取结果表格锁超时({}s): {} {}".format(self.timeout, self.path, last_err)
)
def __exit__(self, exc_type, exc_val, exc_tb):
with contextlib.suppress(Exception):
try:
import fcntl
fcntl.flock(self.fd.fileno(), fcntl.LOCK_UN)
except ImportError:
import msvcrt
self.fd.seek(0)
msvcrt.locking(self.fd.fileno(), msvcrt.LK_UNLCK, 1)
with contextlib.suppress(Exception):
self.fd.close()
return False
def normalize_int(value, default=0):
try:
return int(str(value).strip())
except (TypeError, ValueError):
return default
def load_or_create(xlsx_path):
"""加载或新建工作簿,返回 (workbook, worksheet)。"""
if os.path.isfile(xlsx_path):
wb = openpyxl.load_workbook(xlsx_path)
ws = wb.active
header_row = [str(c.value) if c.value is not None else "" for c in ws[1]] if ws.max_row >= 1 else []
if header_row[: len(HEADERS)] != HEADERS:
for col_idx, name in enumerate(HEADERS, start=1):
ws.cell(row=1, column=col_idx, value=name)
else:
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "批量压测结果"
for col_idx, name in enumerate(HEADERS, start=1):
ws.cell(row=1, column=col_idx, value=name)
widths = [22, 20, 12, 16, 16, 50, 30, 40, 10, 20, 12, 28]
for col_idx, width in enumerate(widths, start=1):
ws.column_dimensions[openpyxl.utils.get_column_letter(col_idx)].width = width
return wb, ws
def find_row(ws, api, scenario):
"""按 (接口名, 测试场景) 查找已有行号(upsert 语义),不存在返回 None。"""
for row in range(2, ws.max_row + 1):
cell_api = ws.cell(row=row, column=1).value
cell_scenario = ws.cell(row=row, column=2).value
if cell_api is not None and cell_scenario is not None:
if str(cell_api).strip() == api and str(cell_scenario).strip() == scenario:
return row
return None
def normalize_bug_state(value):
"""是否是bug 三态归一化:是bug / 不是bug / 需要人工确认(无法识别时默认需要人工确认)。
支持复合值(三态+标准理由,如 "不是bug(环境问题)" / "是bug(aicore_error提单)"):
前缀为规范三态时保留完整复合值(理由写入 xlsx 列),仅宽松输入(是/否/待确认等)做归一。
"""
if value is None:
return "需要人工确认"
raw = str(value).strip().rstrip("。.!!")
key = raw.lower()
if key in BUG_STATE_ALIASES:
return BUG_STATE_ALIASES[key]
for state in BUG_STATES:
if key.startswith(state):
return raw
return "需要人工确认({})".format(raw) if raw else "需要人工确认"
def git_sync_after_update(xlsx_path, api, scenario, force=None):
"""本地表格更新后实时同步 GitCode 仓(Alice_zhou/issue,多机合并推送)。
失败仅告警不阻塞本地更新(下次场景更新自动重试);force=None 时按
环境变量 BATCH_RESULT_GIT_SYNC > config batch_result_git_sync 判定开关。
"""
try:
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from sync_batch_results_git import DEFAULT_CONFIG, git_sync_enabled, sync_local_xlsx
if force is None:
enabled, reason = git_sync_enabled(DEFAULT_CONFIG)
elif force:
enabled, reason = True, "cli --git_sync"
else:
enabled, reason = False, "cli --no_git_sync"
if not enabled:
print("[GIT_SYNC] skip ({})".format(reason))
return
ok, url, detail = sync_local_xlsx(
xlsx_path,
commit_msg="results: update {}/{} from {}".format(api, scenario, socket.gethostname()))
if ok:
print("[GIT_SYNC] {}/{} 已实时同步至 GitCode 仓: {} ({})".format(
api, scenario, url or "Alice_zhou/issue", detail))
else:
print("[GIT_SYNC] WARN 同步失败(不阻塞本地更新): {}".format(detail))
except Exception as exc:
print("[GIT_SYNC] WARN 同步异常(不阻塞本地更新): {}".format(exc))
def issue_stats_sync_after_update(xlsx_path, api, scenario):
"""批量压测结果更新后实时同步 问题统计/接口压测结果与提单汇总.xlsx(GitCode 仓 Alice_zhou/issue)。
"每次压测结果依次填充即可,不需要另外建立表格"——按 (接口名, 压测日期) upsert 累积;
失败仅告警不阻塞本地更新(下次场景更新/定时兜底自动重试);独立开关
ISSUE_STATS_GIT_SYNC/config issue_stats_git_sync(默认 true)。"""
try:
from sync_issue_stats_git import issue_stats_enabled, sync_api_summary
enabled, reason = issue_stats_enabled()
if not enabled:
print("[STATS_SYNC] skip ({})".format(reason))
return
ok, url, detail = sync_api_summary(xlsx_path)
if ok and url:
print("[STATS_SYNC] {}/{} 接口压测结果与提单汇总已实时填充 GitCode 仓: {} ({})".format(
api, scenario, url, detail))
elif ok:
print("[STATS_SYNC] skip: {}".format(detail))
else:
print("[STATS_SYNC] WARN 同步失败(不阻塞本地更新): {}".format(detail))
except Exception as exc:
print("[STATS_SYNC] WARN 同步异常(不阻塞本地更新): {}".format(exc))
def main():
parser = argparse.ArgumentParser(
description="批量压测结果实时更新(out_res/批量压测结果.xlsx,多卡并行写安全)"
)
parser.add_argument("--api", required=True, help="aclnn 接口名,如 aclnnAbs")
parser.add_argument("--scenario", required=True,
help="测试场景:normal/non_contig/multithread/consistent/error/sample"
"(预检/调试记录:normal_precheck/multithread_precheck/consistent_precheck 等)")
parser.add_argument("--total", required=True, help="压测总条数(计划总条数)")
parser.add_argument("--success", required=True, help="实际执行成功条数(aclnn 实际调用 ret==ACL_SUCCESS)")
parser.add_argument("--fail", default=None,
help="实际执行失败条数(缺省时自动 = total - success,未执行起来的用例也计入失败)")
parser.add_argument("--fail_reason", default="无", help="执行失败原因摘要(TOP 原因 + plog 错误码),无失败填 无")
parser.add_argument("--has_bug", default="需要人工确认",
help="是否存在bug(三态,强制规范):是bug / 不是bug / 需要人工确认;"
"宽松输入自动归一(是→是bug,否→不是bug,待确认→需要人工确认)")
parser.add_argument("--has_aicore", default="否", help="是否存在aicore:是/否,是时附 kernel_name 与命中文件")
parser.add_argument("--card", default="", help="执行卡号 deviceId(多卡并行追溯用)")
parser.add_argument("--analyzed", default="否", help="是否分析完成:是/否(该行结果是否已完成分析)")
parser.add_argument("--issue", default="", help="问题单号或链接(无则留空)")
parser.add_argument("--note", default="", help="备注(可选,将合并进执行失败原因列)")
parser.add_argument("--xlsx", default=DEFAULT_XLSX, help="xlsx 路径,默认 out_res/批量压测结果.xlsx")
parser.add_argument("--no_git_sync", action="store_true",
help="跳过本次 GitCode 仓(Alice_zhou/issue)实时同步")
parser.add_argument("--git_sync", action="store_true",
help="强制执行 GitCode 仓实时同步(覆盖 config/环境变量开关)")
args = parser.parse_args()
api = str(args.api).strip()
scenario = str(args.scenario).strip()
total = normalize_int(args.total)
success = normalize_int(args.success)
fail = normalize_int(args.fail, default=-1)
if fail < 0:
fail = total - success
fail_reason = str(args.fail_reason).strip() or "无"
if args.note:
fail_reason = "{} | note: {}".format(fail_reason, str(args.note).strip())
values = [
api,
scenario,
total,
success,
fail,
fail_reason,
normalize_bug_state(args.has_bug),
str(args.has_aicore).strip() or "否",
str(args.card).strip(),
time.strftime("%Y-%m-%d %H:%M:%S"),
"是" if str(args.analyzed).strip().lower() in ("是", "yes", "true", "y", "1") else "否",
str(args.issue).strip(),
]
xlsx_path = args.xlsx
os.makedirs(os.path.dirname(os.path.abspath(xlsx_path)), exist_ok=True)
lock_path = xlsx_path + ".lock"
os.makedirs(os.path.dirname(os.path.abspath(lock_path)), exist_ok=True)
with CrossPlatformLock(lock_path):
wb, ws = load_or_create(xlsx_path)
row = find_row(ws, api, scenario)
action = "update"
if row is None:
row = ws.max_row + 1 if ws.max_row >= 1 else 2
if ws.max_row == 1 and ws.cell(row=1, column=1).value is None:
row = 1
action = "append"
for col_idx, value in enumerate(values, start=1):
cell = ws.cell(row=row, column=col_idx, value=value)
if col_idx == 12 and value and str(value).startswith("http"):
first_url = str(value).split("\n")[0].strip()
if first_url.startswith("http"):
cell.hyperlink = first_url
wb.save(xlsx_path)
success_rate = (success * 100.0 / total) if total > 0 else 0.0
print("[BATCH_RESULT] {} row={} api={} scenario={} total={} success={} fail={} rate={:.2f}% -> {}".format(
action, row, api, scenario, total, success, fail, success_rate, xlsx_path))
if args.git_sync:
git_sync_after_update(xlsx_path, api, scenario, force=True)
elif args.no_git_sync:
git_sync_after_update(xlsx_path, api, scenario, force=False)
else:
git_sync_after_update(xlsx_path, api, scenario, force=None)
issue_stats_sync_after_update(xlsx_path, api, scenario)
return 0
if __name__ == "__main__":
sys.exit(main())