perf: optimize rust acceleration paths

Rust vs Python benchmark results:

- RSS: Rust 0.299 ms/loop vs Python 7.913 ms/loop, 26.47x faster

- Filter: Rust 12.740 ms/loop vs Python 57.187 ms/loop, 4.49x faster

- MetaInfo: Rust 64.680 ms/loop vs Python 316.158 ms/loop, 4.89x faster

- Indexer agsvpt: Rust 145.76 ms vs Python 3686.50 ms, 25.29x faster

- Indexer pttime: Rust 166.51 ms vs Python 4019.87 ms, 24.14x faster

- Indexer chdbits: Rust 161.17 ms vs Python 3604.28 ms, 22.36x faster

- Indexer iptorrents: Rust 77.82 ms vs Python 17615.52 ms, 226.36x faster

Validation:

- cargo fmt/check/test for rust/moviepilot_rust

- pytest Rust-related coverage: tests/test_rust_accel.py tests/test_torrent_filter.py tests/test_metainfo.py tests/test_indexer_spider_search_url.py tests/test_workflow_fetch_rss.py

- tests/run.py legacy suite

- pylint app/ --errors-only
This commit is contained in:
jxxghp
2026-05-23 19:35:55 +08:00
parent a6826e6a4e
commit 0bf228d29d
15 changed files with 1727 additions and 392 deletions

View File

@@ -0,0 +1,182 @@
import argparse
import statistics
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
from types import SimpleNamespace
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from app.core.context import MediaInfo, TorrentInfo
from app.modules.filter import FilterModule
from app.modules.filter.RuleParser import RuleParser
class StaticRuleHelper:
"""
为 benchmark 提供固定规则组,避免系统配置和数据库访问影响过滤链路测量。
"""
def __init__(self, groups):
self._groups = groups
def get_rule_group_by_media(self, media=None, group_names=None): # noqa: ARG002
"""
按规则名返回固定规则组。
"""
if not group_names:
return self._groups
return [group for group in self._groups if group.name in group_names]
def build_module() -> FilterModule:
"""
构造覆盖正则、标签、站点字段、促销、做种数、发布时间和 size_range 的过滤模块。
"""
module = FilterModule()
module.rulehelper = StaticRuleHelper(
[
SimpleNamespace(name="quality", rule_string="(HDR | DV) & !BLU > WEBDL & FREE"),
SimpleNamespace(name="site", rule_string="LABEL & SITE & SIZE & SEED & PUB & TMDB"),
]
)
module.rule_set = {
"HDR": {"include": r"[\s.]+HDR[\s.]+|HDR10|HDR10\+"},
"DV": {"include": r"DOVI|Dolby[\s.]+Vision"},
"BLU": {
"include": [r"\bBlu-?Ray\b"],
"exclude": [r"(?<!WEB-|HDTV)RIP"],
},
"WEBDL": {"include": r"WEB-?DL|WEB-?RIP"},
"FREE": {"downloadvolumefactor": 0},
"LABEL": {"include": "官方", "match": ["labels"]},
"SITE": {"include": "Alpha|Beta", "match": ["site_name"]},
"SIZE": {"size_range": "100-500"},
"SEED": {"seeders": "5"},
"PUB": {"publish_time": "0-2880"},
"TMDB": {"tmdb": {"original_language": "zh,cn"}},
}
return module
def build_torrents(count: int) -> list[TorrentInfo]:
"""
构造稳定的种子列表,让匹配和不匹配路径都进入完整过滤逻辑。
"""
pubdate = (datetime.now() - timedelta(minutes=90)).strftime("%Y-%m-%d %H:%M:%S")
torrents = []
for index in range(count):
matched = index % 3 != 0
title = f"Example Show S01E{index % 12 + 1:02d}-E{index % 12 + 2:02d} 1080p WEB-DL HDR10"
if not matched:
title = f"Example Show S01E{index % 12 + 1:02d} 1080p BluRay HDR"
torrents.append(
TorrentInfo(
site_name="Alpha" if index % 2 else "Beta",
title=title,
description="简繁中字",
labels=["官方"] if matched else ["转载"],
size=(700 if matched else 2400) * 1024 * 1024,
seeders=20 if matched else 1,
downloadvolumefactor=0 if matched else 1,
pubdate=pubdate,
)
)
return torrents
def build_mediainfo() -> MediaInfo:
"""
构造命中 TMDB 规则的媒体信息。
"""
mediainfo = MediaInfo()
mediainfo.original_language = "zh"
return mediainfo
def reset_priority(torrents: list[TorrentInfo]) -> None:
"""
清理上一轮过滤写入的优先级,避免重复测量间互相影响。
"""
for torrent in torrents:
torrent.pri_order = 0
def python_filter(module: FilterModule, rule_groups: list[str], torrent_list: list[TorrentInfo], mediainfo: MediaInfo):
"""
调用旧 Python 私有实现,作为同一入口语义下的基准对照。
"""
parser = RuleParser()
parsed_rule_cache = {}
groups = module.rulehelper.get_rule_group_by_media(media=mediainfo, group_names=rule_groups)
for group in groups:
torrent_list = module._FilterModule__filter_torrents(
rule_string=group.rule_string,
rule_name=group.name,
torrent_list=torrent_list,
mediainfo=mediainfo,
parser=parser,
parsed_rule_cache=parsed_rule_cache,
)
return torrent_list
def rust_filter(module: FilterModule, rule_groups: list[str], torrent_list: list[TorrentInfo], mediainfo: MediaInfo):
"""
调用当前生产入口,测量 Rust 完整过滤链路。
"""
return module.filter_torrents(rule_groups=rule_groups, torrent_list=torrent_list, mediainfo=mediainfo)
def measure(func, module: FilterModule, torrents: list[TorrentInfo], mediainfo: MediaInfo, loops: int, repeats: int):
"""
多轮测量过滤耗时并返回中位数。
"""
samples = []
filtered_count = 0
rule_groups = ["quality", "site"]
for _ in range(repeats):
start = time.perf_counter()
for _ in range(loops):
reset_priority(torrents)
filtered_count = len(func(module, rule_groups, torrents, mediainfo))
samples.append((time.perf_counter() - start) * 1000 / loops)
return statistics.median(samples), filtered_count
def parse_args():
"""
解析命令行参数。
"""
parser = argparse.ArgumentParser(description="Benchmark FilterModule.filter_torrents Rust entry")
parser.add_argument("--items", type=int, default=2000, help="Torrent count per loop")
parser.add_argument("--loops", type=int, default=20, help="Loops per repeat")
parser.add_argument("--repeats", type=int, default=5, help="Repeat count")
return parser.parse_args()
def main() -> int:
"""
运行 Filter Rust 与旧 Python 过滤链路基准测试。
"""
args = parse_args()
module = build_module()
torrents = build_torrents(args.items)
mediainfo = build_mediainfo()
rust_ms, rust_count = measure(rust_filter, module, torrents, mediainfo, args.loops, args.repeats)
python_ms, python_count = measure(python_filter, module, torrents, mediainfo, args.loops, args.repeats)
speedup = python_ms / rust_ms if rust_ms else 0
print(f"items_per_loop={len(torrents)} 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())