import argparse
import statistics
import sys
import time
from contextlib import contextmanager
from pathlib import Path
from types import SimpleNamespace
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from app.helper import rss as rss_module
from app.helper.rss import RssHelper
from app.utils import rust_accel
class FakeRequestUtils:
"""
基准测试用 RequestUtils,固定返回内存中的 RSS 文本。
"""
xml_text = ""
def __init__(self, **_kwargs):
"""
保持与真实 RequestUtils 构造签名兼容。
"""
def get_res(self, _url):
"""
返回 RssHelper.parse 所需的最小响应对象。
"""
return SimpleNamespace(
status_code=200,
content=self.xml_text.encode("utf-8"),
text=self.xml_text,
apparent_encoding="utf-8",
encoding="utf-8",
)
def build_rss_xml(items: int) -> str:
"""
构造覆盖标题、描述、链接、enclosure、日期和 creator 的 RSS 文本。
"""
rows = []
for index in range(items):
rows.append(f"""
-
MoviePilot Benchmark {index}
tag]]>
https://example.com/details/{index}
Tue, 19 May 2026 08:30:00 GMT
bench-user-{index}
""")
return f"""
{''.join(rows)}
"""
@contextmanager
def patched_request_utils(xml_text: str):
"""
临时替换 RSS 请求层,让基准覆盖 RssHelper.parse 的实际解析链路。
"""
original_request_utils = rss_module.RequestUtils
FakeRequestUtils.xml_text = xml_text
rss_module.RequestUtils = FakeRequestUtils
try:
yield
finally:
rss_module.RequestUtils = original_request_utils
def disabled_rust_parse(_xml_text: str, _max_items: int = 1000):
"""
关闭 Rust 快路径,用同一条 RssHelper.parse 链路测量 Python lxml 兜底性能。
"""
return None
@contextmanager
def selected_rss_parser(use_rust: bool):
"""
在 Rust 快路径和 Python lxml 解析之间切换,保持请求与编码成本一致。
"""
original_parse = rss_module.rust_accel.parse_rss_items
if not use_rust:
rss_module.rust_accel.parse_rss_items = disabled_rust_parse
try:
yield
finally:
rss_module.rust_accel.parse_rss_items = original_parse
def parse_chain(xml_text: str, use_rust: bool):
"""
执行一次 RssHelper.parse,返回解析到的 RSS 条目。
"""
with patched_request_utils(xml_text), selected_rss_parser(use_rust):
return RssHelper().parse("https://example.com/rss")
def measure_chain(xml_text: str, use_rust: bool, loops: int, repeats: int):
"""
多轮测量 RssHelper.parse 平均耗时,并校验每轮解析数量稳定。
"""
samples = []
parsed_count = 0
for _ in range(repeats):
start = time.perf_counter()
for _ in range(loops):
parsed = parse_chain(xml_text, use_rust)
parsed_count = len(parsed)
samples.append((time.perf_counter() - start) * 1000 / loops)
return statistics.median(samples), parsed_count
def parse_args():
"""
解析命令行参数。
"""
parser = argparse.ArgumentParser(description="Benchmark RSS parsing through RssHelper.parse")
parser.add_argument("--items", type=int, default=200, help="RSS item count")
parser.add_argument("--loops", type=int, default=50, help="Loops per repeat")
parser.add_argument("--repeats", type=int, default=5, help="Repeat count")
return parser.parse_args()
def main() -> int:
"""
运行 Rust 与 Python RSS 解析链路基准测试。
"""
args = parse_args()
xml_text = build_rss_xml(args.items)
rust_ms, rust_count = measure_chain(xml_text, use_rust=True, loops=args.loops, repeats=args.repeats)
python_ms, python_count = measure_chain(xml_text, use_rust=False, loops=args.loops, repeats=args.repeats)
speedup = python_ms / rust_ms if rust_ms else 0
print(f"rust_available={rust_accel.is_available()}")
print(f"items={args.items} loops={args.loops} repeats={args.repeats}")
print(f"rust_items={rust_count} python_items={python_count}")
print(f"rust_chain_ms_per_loop={rust_ms:.3f}")
print(f"python_chain_ms_per_loop={python_ms:.3f}")
print(f"speedup={speedup:.2f}x")
return 0
if __name__ == "__main__":
raise SystemExit(main())