mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-22 13:07:06 +08:00
feat: accelerate RSS parsing with Rust
This commit is contained in:
114
scripts/benchmark_rust_accel.py
Normal file
114
scripts/benchmark_rust_accel.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import argparse
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from app.utils import rust_accel
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
def _time_call(func: Callable[[], None], loops: int) -> float:
|
||||
"""
|
||||
执行指定函数多轮并返回总耗时。
|
||||
"""
|
||||
start = time.perf_counter()
|
||||
for _ in range(loops):
|
||||
func()
|
||||
return time.perf_counter() - start
|
||||
|
||||
|
||||
def _median_time(func: Callable[[], None], loops: int, repeat: int) -> float:
|
||||
"""
|
||||
重复测量指定函数并返回中位耗时。
|
||||
"""
|
||||
return statistics.median(_time_call(func, loops) for _ in range(repeat))
|
||||
|
||||
|
||||
def _rss_xml(item_count: int = 200) -> str:
|
||||
"""
|
||||
生成稳定的 RSS 测试数据,避免网络和站点波动影响结果。
|
||||
"""
|
||||
items = []
|
||||
for index in range(item_count):
|
||||
items.append(
|
||||
f"""
|
||||
<item>
|
||||
<title>Example Torrent {index}</title>
|
||||
<description><![CDATA[Example Desc {index}]]></description>
|
||||
<link>https://example.org/details/{index}</link>
|
||||
<enclosure url="https://example.org/download/{index}.torrent" length="{index + 1024}" />
|
||||
<pubDate>Tue, 19 May 2026 08:30:00 GMT</pubDate>
|
||||
<dc:creator>User {index}</dc:creator>
|
||||
</item>
|
||||
"""
|
||||
)
|
||||
return "<rss xmlns:dc=\"http://purl.org/dc/elements/1.1/\"><channel>" + "".join(items) + "</channel></rss>"
|
||||
|
||||
|
||||
def _python_rss_parse(xml_text: str) -> None:
|
||||
"""
|
||||
执行与 RssHelper 原 XPath 路径等价的 RSS 条目字段提取。
|
||||
"""
|
||||
root = etree.fromstring(
|
||||
xml_text.encode("utf-8"),
|
||||
parser=etree.XMLParser(recover=True, strip_cdata=False, resolve_entities=False, no_network=True),
|
||||
)
|
||||
parsed = []
|
||||
for item in root.xpath(".//item | .//entry")[:1000]:
|
||||
title_nodes = item.xpath(".//title")
|
||||
title = title_nodes[0].text if title_nodes and title_nodes[0].text else ""
|
||||
desc_nodes = item.xpath(".//description | .//summary")
|
||||
description = desc_nodes[0].text if desc_nodes and desc_nodes[0].text else ""
|
||||
link_nodes = item.xpath(".//link")
|
||||
link = link_nodes[0].text if link_nodes and link_nodes[0].text else ""
|
||||
enclosure_nodes = item.xpath(".//enclosure")
|
||||
enclosure = enclosure_nodes[0].get("url", "") if enclosure_nodes else link
|
||||
pubdate_nodes = item.xpath('./pubDate | ./published | ./updated')
|
||||
pubdate = StringUtils.get_time(pubdate_nodes[0].text) if pubdate_nodes and pubdate_nodes[0].text else ""
|
||||
parsed.append((title, description, link, enclosure, pubdate))
|
||||
root.clear()
|
||||
|
||||
|
||||
def _print_result(name: str, python_seconds: float, rust_seconds: float) -> None:
|
||||
"""
|
||||
输出单项基准耗时和提升倍数。
|
||||
"""
|
||||
speedup = python_seconds / rust_seconds if rust_seconds else 0
|
||||
print(f"{name}: Python {python_seconds:.4f}s, Rust {rust_seconds:.4f}s, speedup {speedup:.2f}x")
|
||||
|
||||
|
||||
def run_benchmark(loops: int, repeat: int) -> None:
|
||||
"""
|
||||
运行核心 Rust 加速模块的本地微基准。
|
||||
"""
|
||||
print(f"moviepilot_rust available: {rust_accel.is_available()}")
|
||||
|
||||
xml_text = _rss_xml()
|
||||
_print_result(
|
||||
"rss item parse",
|
||||
_median_time(lambda: _python_rss_parse(xml_text), loops, repeat),
|
||||
_median_time(lambda: rust_accel.parse_rss_items(xml_text, 1000), loops, repeat),
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
命令行入口。
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="Benchmark MoviePilot Rust acceleration paths.")
|
||||
parser.add_argument("--loops", type=int, default=20)
|
||||
parser.add_argument("--repeat", type=int, default=5)
|
||||
args = parser.parse_args()
|
||||
run_benchmark(max(args.loops, 1), max(args.repeat, 1))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -544,10 +544,17 @@ ensure_prereqs() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! ensure_base_tools || ! ensure_build_tools || ! ensure_python || ! ensure_uv || ! ensure_rust_toolchain; then
|
||||
if ! ensure_base_tools || ! ensure_python || ! ensure_uv; then
|
||||
python_install_hint
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! rust_accel_should_skip; then
|
||||
if ! ensure_build_tools || ! ensure_rust_toolchain; then
|
||||
export MOVIEPILOT_SKIP_RUST_ACCEL=1
|
||||
echo "==> Rust 加速扩展准备失败,已跳过;应用将继续使用 Python 实现"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_text() {
|
||||
|
||||
@@ -656,27 +656,28 @@ def _find_native_linker() -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def ensure_rust_accel_ready() -> None:
|
||||
def ensure_rust_accel_ready() -> bool:
|
||||
"""
|
||||
确认 Rust 加速扩展源码存在且本机具备 cargo 与链接器。
|
||||
"""
|
||||
if not RUST_ACCEL_MANIFEST.exists():
|
||||
return
|
||||
return False
|
||||
if _rust_accel_should_skip():
|
||||
print_step(f"已跳过 Rust 加速扩展构建:{RUST_ACCEL_SKIP_ENV}=1")
|
||||
return
|
||||
return False
|
||||
if not _find_cargo():
|
||||
raise RuntimeError(
|
||||
"未找到 Rust cargo,无法构建 MoviePilot Rust 加速扩展。"
|
||||
"请先安装 Rust toolchain 后重试,或临时设置 "
|
||||
f"{RUST_ACCEL_SKIP_ENV}=1 跳过加速扩展。"
|
||||
print_step(
|
||||
"未找到 Rust cargo,已跳过 Rust 加速扩展构建;"
|
||||
"应用将继续使用 Python 实现。"
|
||||
)
|
||||
return False
|
||||
if not _find_native_linker():
|
||||
raise RuntimeError(
|
||||
"未找到本机 C 编译器/链接器,无法构建 MoviePilot Rust 加速扩展。"
|
||||
"请先安装系统构建工具后重试,或临时设置 "
|
||||
f"{RUST_ACCEL_SKIP_ENV}=1 跳过加速扩展。"
|
||||
print_step(
|
||||
"未找到本机 C 编译器/链接器,已跳过 Rust 加速扩展构建;"
|
||||
"应用将继续使用 Python 实现。"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def install_rust_accel(venv_python: Path) -> None:
|
||||
@@ -688,22 +689,26 @@ def install_rust_accel(venv_python: Path) -> None:
|
||||
if _rust_accel_should_skip():
|
||||
return
|
||||
|
||||
ensure_rust_accel_ready()
|
||||
if not ensure_rust_accel_ready():
|
||||
return
|
||||
print_step("构建并安装 Rust 加速扩展")
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = _cargo_env_path()
|
||||
run(
|
||||
[
|
||||
str(venv_python),
|
||||
"-m",
|
||||
"maturin",
|
||||
"develop",
|
||||
"--release",
|
||||
"--manifest-path",
|
||||
str(RUST_ACCEL_MANIFEST),
|
||||
],
|
||||
env=env,
|
||||
)
|
||||
try:
|
||||
run(
|
||||
[
|
||||
str(venv_python),
|
||||
"-m",
|
||||
"maturin",
|
||||
"develop",
|
||||
"--release",
|
||||
"--manifest-path",
|
||||
str(RUST_ACCEL_MANIFEST),
|
||||
],
|
||||
env=env,
|
||||
)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print_step(f"Rust 加速扩展构建失败,已跳过;应用将继续使用 Python 实现:{exc}")
|
||||
|
||||
|
||||
def ensure_supported_python(python_bin: str) -> None:
|
||||
@@ -2732,7 +2737,6 @@ def install_deps(*, python_bin: str, venv_dir: Path, recreate: bool) -> Path:
|
||||
创建或复用本地虚拟环境,并安装后端依赖、Rust 扩展和浏览器运行时。
|
||||
"""
|
||||
ensure_supported_python(python_bin)
|
||||
ensure_rust_accel_ready()
|
||||
venv_dir = venv_dir.expanduser().resolve()
|
||||
venv_python = get_venv_python(venv_dir)
|
||||
venv_pip = get_venv_pip(venv_dir)
|
||||
|
||||
Reference in New Issue
Block a user