#!/usr/bin/env python3 """ 使用 MoviePilot-Build 的真实站点配置比较 Rust 与 Python 索引解析耗时。 输入为本地构造的 HTML,不向站点发起请求;结果取多轮运行的耗时中位数。 """ import argparse import contextlib import copy import io import statistics import sys import time from pathlib import Path from typing import Callable, Dict, List import yaml PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from app.modules.indexer import spider as spider_module from app.modules.indexer.spider import SiteSpider def _load_site_config(sites_dir: Path, site_id: str) -> dict: """ 从 MoviePilot-Build sites 目录读取指定站点配置。 """ candidates = list(sites_dir.rglob(f"{site_id}.yml")) if not candidates: raise FileNotFoundError(f"找不到站点配置:{site_id}") with candidates[0].open("r", encoding="utf-8") as file: return yaml.safe_load(file) def _nexus_row( torrent_id: int, title: str, category: str = "402", date_text: str = "2025-05-01 12:13:14", free_class: str = "pro_free", ) -> str: """ 生成 NexusPhp 类站点配置可解析的单行 HTML。 """ return f""" cat
{title}.fallback download Desc {torrent_id} remove 标签{torrent_id}
{date_text} 1.5 GB {torrent_id + 10} {torrent_id % 7} {torrent_id % 11} """ def _build_nexus_html(rows: int) -> str: """ 生成多行 NexusPhp 表格 HTML,用于驱动真实 SiteSpider 解析链路。 """ body = "\n".join( _nexus_row(1000 + index, f"Benchmark.Title.{index}") for index in range(rows) ) return f"{body}
" def _build_ipt_html(rows: int) -> str: """ 生成 IPT 配置使用的表格 HTML,覆盖 lstrip 和英文相对时间过滤器。 """ body = "\n".join( f""" Benchmark.IPT.{index} downloadFree
Uploaded | {index % 5 + 1} hours ago
{1 + index % 9}.2 GB {index % 13} {index + 20} {index % 7} """ for index in range(rows) ) return f"{body}
" def _prepare_cases(sites_dir: Path, rows: int) -> List[Dict[str, object]]: """ 构造使用真实站点配置的 基准测试 用例。 """ cases = [] for site_id in ["agsvpt", "pttime", "chdbits"]: config = _load_site_config(sites_dir, site_id) cases.append({ "site": site_id, "indexer": config, "html": _build_nexus_html(rows), }) cases.append({ "site": "iptorrents", "indexer": _load_site_config(sites_dir, "iptorrents"), "html": _build_ipt_html(rows), }) return cases def _parse_with_rust(indexer: dict, html: str) -> List[dict]: """ 使用 SiteSpider 默认入口解析,Rust 扩展可用时会命中 Rust 加速路径。 """ return SiteSpider(copy.deepcopy(indexer)).parse(html) def _parse_with_python(indexer: dict, html: str) -> List[dict]: """ 临时禁用 Rust 加速路径,复用同一个 SiteSpider.parse 入口测 PyQuery 路径。 """ original = spider_module.rust_accel.parse_indexer_torrents spider_module.rust_accel.parse_indexer_torrents = lambda *args, **kwargs: None try: return SiteSpider(copy.deepcopy(indexer)).parse(html) finally: spider_module.rust_accel.parse_indexer_torrents = original def _time_parser(parser: Callable[[dict, str], List[dict]], indexer: dict, html: str, loops: int) -> float: """ 多次执行解析函数并返回总耗时。 """ buffer = io.StringIO() with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer): started = time.perf_counter() for _ in range(loops): parser(indexer, html) return time.perf_counter() - started def _median_time(parser: Callable[[dict, str], List[dict]], indexer: dict, html: str, loops: int, repeats: int) -> float: """ 重复测量并取中位数,降低单次抖动对结论的影响。 """ samples = [_time_parser(parser, indexer, html, loops) for _ in range(repeats)] return statistics.median(samples) def run_benchmark(sites_dir: Path, rows: int, loops: int, repeats: int) -> None: """ 执行真实 SiteSpider.parse 链路 基准测试 并打印 Rust/PyQuery 对比。 """ cases = _prepare_cases(sites_dir, rows) print(f"sites_dir={sites_dir}") print(f"rows={rows} loops={loops} repeats={repeats}") print("site, rows, rust_ms, python_ms, speedup") for case in cases: buffer = io.StringIO() with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer): rust_result = _parse_with_rust(case["indexer"], case["html"]) python_result = _parse_with_python(case["indexer"], case["html"]) if not rust_result or not python_result: raise RuntimeError(f"{case['site']} benchmark fixture did not parse any rows") rust_time = _median_time(_parse_with_rust, case["indexer"], case["html"], loops, repeats) python_time = _median_time(_parse_with_python, case["indexer"], case["html"], loops, repeats) speedup = python_time / rust_time if rust_time else 0 print( f"{case['site']}, {len(rust_result)}, " f"{rust_time * 1000:.2f}, {python_time * 1000:.2f}, {speedup:.2f}x" ) def parse_args() -> argparse.Namespace: """ 解析命令行参数。 """ parser = argparse.ArgumentParser(description="Benchmark MoviePilot Rust indexer parser") parser.add_argument( "--sites-dir", type=Path, default=Path("/Users/jxxghp/PycharmProjects/MoviePilot-Build/sites"), help="MoviePilot-Build sites directory", ) parser.add_argument("--rows", type=int, default=80, help="Rows per fixture page") parser.add_argument("--loops", type=int, default=50, help="Loops per timing sample") parser.add_argument("--repeats", type=int, default=5, help="Timing samples per parser") return parser.parse_args() def main() -> None: """ 解析采样参数并输出真实站点配置的索引解析性能对照。 """ args = parse_args() run_benchmark(args.sites_dir, args.rows, args.loops, args.repeats) if __name__ == "__main__": main()