Files
MoviePilot/scripts/benchmark_metainfo_rust.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

404 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
通过生产识别入口比较影视与音乐的 Rust、Python 解析性能。
先验证完整稳定输出等价,再进行预热与计时;必须存在可用 Rust 运行能力。
"""
import argparse
import statistics
import sys
import time
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Callable, Optional
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from app.domain import metainfo as metainfo_module
from app.domain.meta.metaanime import MetaAnime
from app.domain.meta.metabase import MetaInfoSnapshot
from app.domain.meta.metamusic import MetaMusic
from app.domain.meta.runtime import (
configure_recognition_runtime,
get_audio_extensions,
get_metainfo_accelerator,
)
from app.domain.metainfo import MetaInfo, MetaInfoPath
from app.adapters.system import rust as rust_accel
from app.runtime.settings import get_runtime_setting
from tests.cases.meta import meta_cases
BenchmarkInput = tuple[str, str, Optional[str]]
ResultProjector = Callable[[Any], dict[str, Any]]
_MUSIC_CASES: tuple[BenchmarkInput, ...] = (
("music_query", "毛阿敏 - 永遠是朋友(2000) - ALAC [16B-44.1kHz]", None),
(
"music_query",
"VA-Once.Upon.a.Time.in.Hollywood.Original.Motion.Picture.Soundtrack."
"2019.FLAC.24bit.96kHz",
None,
),
("music_query", "李宗盛《理性与感性作品音乐会-CD2》2006-FLAC-分轨", None),
("music_query", "天国的情人-邓丽君作品全集1967-1995", None),
(
"music_query",
"S H E - S H E十七音乐会 2018 WEB-DL 1080P AVC AAC-FHDMv",
None,
),
("title", "周杰伦 - 晴天.flac", None),
("title", "01.我的地盘.wav", None),
(
"path",
"/benchmark/music/周杰伦 - 七里香 (2004) [FLAC 24bit-96kHz]/01.我的地盘.flac",
None,
),
(
"path",
"/benchmark/music/Daft Punk - Discovery (2001)/CD1/01 - One More Time.flac",
None,
),
(
"path",
"/benchmark/music/喜多郎 - 古事记 (1990) [SACD]/1-02 古事记.dsf",
None,
),
)
def configure_benchmark_runtime() -> None:
"""
注入独立基准所需的文件类型和 Rust 适配器,避免静默测量 Python 回退。
"""
configure_recognition_runtime(
media_extensions_provider=lambda: (
*get_runtime_setting("RMT_MEDIAEXT"),
*get_runtime_setting("RMT_SUBEXT"),
*get_runtime_setting("RMT_AUDIOEXT"),
),
audio_extensions_provider=lambda: get_runtime_setting("RMT_AUDIOEXT"),
accelerator=rust_accel,
)
def build_video_inputs(repeat: int) -> list[BenchmarkInput]:
"""
构造覆盖影视 MetaInfo 和 MetaInfoPath 生产入口的基准输入。
"""
inputs: list[BenchmarkInput] = []
for _ in range(repeat):
for item in meta_cases:
if item.get("path"):
inputs.append(("path", item["path"], item.get("subtitle")))
else:
inputs.append(("title", item["title"], item.get("subtitle")))
return inputs
def build_music_inputs(repeat: int) -> list[BenchmarkInput]:
"""
构造覆盖音乐查询、音频文件名和目录路径生产入口的基准输入。
"""
return list(_MUSIC_CASES) * repeat
def disabled_rust_parse(*_args, **_kwargs):
"""
关闭一个 Rust 快路径,使生产入口自然回退到 Python 实现。
"""
return None
@contextmanager
def selected_meta_parser(use_rust: bool):
"""
在 Rust 入口和 Python 回退链路之间切换,并在退出时恢复适配器。
:yields: 指定解析器生效的执行区间;退出时恢复原有适配器
"""
parser_names = (
"parse_metainfo",
"parse_metainfo_path",
"find_metainfo",
"parse_metamusic",
)
original_parsers = {
name: getattr(rust_accel, name)
for name in parser_names
}
if not use_rust:
for name in parser_names:
setattr(rust_accel, name, disabled_rust_parse)
try:
yield
finally:
for name, parser in original_parsers.items():
setattr(rust_accel, name, parser)
def parse_input(item: BenchmarkInput):
"""
按输入类型调用应用实际使用的公开识别入口。
"""
kind, value, subtitle = item
if kind == "path":
return MetaInfoPath(Path(value))
if kind == "music_query":
return MetaMusic.parse_query(value)
if kind == "title":
return MetaInfo(title=value, subtitle=subtitle, custom_words=["#"])
raise ValueError(f"未知基准输入类型:{kind}")
def parse_all(inputs: list[BenchmarkInput]) -> list[Any]:
"""
通过生产入口解析一轮完整输入。
"""
return [parse_input(item) for item in inputs]
def _enum_value(value: Any) -> Any:
"""
把枚举值归一为稳定的可比较值。
"""
return getattr(value, "value", value)
def project_video_result(meta: Any) -> dict[str, Any]:
"""
提取影视识别完整稳定契约,排除解析器的临时内部状态和派生展示字段。
"""
result = MetaInfoSnapshot.from_meta(meta).to_dict()
result["apply_words"] = list(result["apply_words"])
return result
def project_music_result(meta: Any) -> dict[str, Any]:
"""
提取音乐识别持久字段和派生音质字段,用于 Rust/Python 等价校验。
"""
return {
"type": _enum_value(meta.type),
"org_string": meta.org_string,
"title": meta.title,
"artists": list(meta.artists),
"album": meta.album,
"album_artist": meta.album_artist,
"year": meta.year,
"disc_number": meta.disc_number,
"track_number": meta.track_number,
"total_discs": meta.total_discs,
"total_tracks": meta.total_tracks,
"version": meta.version,
"audio_format": meta.audio_format,
"audio_lossless": meta.audio_lossless,
"bit_depth": meta.bit_depth,
"sample_rate": meta.sample_rate,
"bitrate": meta.bitrate,
"duration": meta.duration,
"isrc": meta.isrc,
"media_source": _enum_value(meta.media_source),
"media_id": meta.media_id,
"audio_quality": meta.audio_quality,
"audio_quality_score": meta.audio_quality_score,
"audio_specs": meta.audio_specs,
}
def assert_projected_results_equal(
inputs: list[BenchmarkInput],
rust_results: list[Any],
python_results: list[Any],
projector: ResultProjector,
) -> None:
"""
逐项校验 Rust/Python 稳定输出,首个差异携带输入和字段明细。
"""
if len(rust_results) != len(python_results):
raise AssertionError(
f"Rust/Python 结果数量不一致:{len(rust_results)} != {len(python_results)}"
)
for index, (rust_result, python_result) in enumerate(zip(rust_results, python_results)):
rust_projection = projector(rust_result)
python_projection = projector(python_result)
if rust_projection == python_projection:
continue
differences = {
key: (rust_projection.get(key), python_projection.get(key))
for key in sorted(set(rust_projection) | set(python_projection))
if rust_projection.get(key) != python_projection.get(key)
}
raise AssertionError(
f"Rust/Python 输出不等价:index={index} input={inputs[index]!r} "
f"differences={differences!r}"
)
def validate_equivalent_results(
inputs: list[BenchmarkInput],
projector: ResultProjector,
) -> None:
"""
分别通过 Rust/Python 生产链路解析并校验稳定输出等价。
"""
with selected_meta_parser(use_rust=True):
rust_results = parse_all(inputs)
with selected_meta_parser(use_rust=False):
python_results = parse_all(inputs)
assert_projected_results_equal(inputs, rust_results, python_results, projector)
def measure(
inputs: list[BenchmarkInput],
use_rust: bool,
loops: int,
repeats: int,
) -> tuple[float, int]:
"""
在一次解析器切换上下文中预热并多轮测量生产入口耗时。
"""
samples = []
parsed_count = 0
with selected_meta_parser(use_rust):
parse_all(inputs)
for _ in range(repeats):
start = time.perf_counter()
for _ in range(loops):
parsed_count = len(parse_all(inputs))
samples.append((time.perf_counter() - start) * 1000 / loops)
return statistics.median(samples), parsed_count
def benchmark_suite(
inputs: list[BenchmarkInput],
projector: ResultProjector,
loops: int,
repeats: int,
) -> dict[str, float | int]:
"""
先校验一个媒体域的结果等价,再返回 Rust/Python 独立性能指标。
"""
validate_equivalent_results(inputs, projector)
rust_ms, rust_count = measure(inputs, use_rust=True, loops=loops, repeats=repeats)
python_ms, python_count = measure(inputs, use_rust=False, loops=loops, repeats=repeats)
return {
"rust_ms": rust_ms,
"python_ms": python_ms,
"rust_count": rust_count,
"python_count": python_count,
"speedup": python_ms / rust_ms if rust_ms else 0,
}
def validate_rust_runtime() -> None:
"""
确认领域入口已注入可工作的 Rust 影视与音乐解析器,拒绝伪基准。
"""
if not rust_accel.is_enabled():
raise RuntimeError("Rust 加速未启用或 moviepilot-rust 扩展不可用")
if get_metainfo_accelerator() is not rust_accel:
raise RuntimeError("MetaInfo 领域入口未注入 Rust 加速器")
video_probe = rust_accel.parse_metainfo(
"Benchmark Movie 2026 1080p WEB-DL H265",
options={"media_exts": [".mkv"]},
)
if not isinstance(video_probe, dict) or video_probe.get("en_name") != "Benchmark Movie":
raise RuntimeError("Rust 影视解析探针未返回有效结果,拒绝测量 Python 回退")
if not callable(getattr(rust_accel, "parse_metamusic", None)):
raise RuntimeError("MoviePilot 后端缺少 rust_accel.parse_metamusic 适配器")
extension = getattr(rust_accel, "_moviepilot_rust", None)
if not callable(getattr(extension, "parse_metamusic_fast", None)):
raise RuntimeError("moviepilot-rust 版本过旧,缺少 parse_metamusic_fast")
probe = rust_accel.parse_metamusic("Daft Punk - Get Lucky 2013 FLAC")
if not isinstance(probe, dict):
raise RuntimeError("Rust 音乐解析探针未返回有效结果,拒绝测量 Python 回退")
def positive_int(value: str) -> int:
"""
解析命令行正整数,拒绝空循环和空样本配置。
"""
parsed = int(value)
if parsed <= 0:
raise argparse.ArgumentTypeError("必须为正整数")
return parsed
def parse_args():
"""
解析命令行参数。
"""
parser = argparse.ArgumentParser(
description="Benchmark video and music metadata through public entries"
)
parser.add_argument(
"--repeat-inputs",
type=positive_int,
default=20,
help="Repeat video and music cases per loop",
)
parser.add_argument("--loops", type=positive_int, default=10, help="Loops per repeat")
parser.add_argument("--repeats", type=positive_int, default=5, help="Repeat count")
return parser.parse_args()
def print_suite_result(
name: str,
inputs: list[BenchmarkInput],
result: dict[str, float | int],
loops: int,
repeats: int,
) -> None:
"""
按媒体域输出等价状态、耗时、单项耗时和性能提升倍数。
"""
rust_ms = float(result["rust_ms"])
python_ms = float(result["python_ms"])
print(f"{name}_items_per_loop={len(inputs)} loops={loops} repeats={repeats}")
print(
f"{name}_rust_items={result['rust_count']} "
f"{name}_python_items={result['python_count']}"
)
print(f"{name}_equivalent=true")
print(f"{name}_rust_ms_per_loop={rust_ms:.3f}")
print(f"{name}_python_ms_per_loop={python_ms:.3f}")
print(f"{name}_rust_us_per_item={rust_ms * 1000 / len(inputs):.3f}")
print(f"{name}_python_us_per_item={python_ms * 1000 / len(inputs):.3f}")
print(f"{name}_speedup={float(result['speedup']):.2f}x")
def main() -> int:
"""
运行影视与音乐 Rust/Python 生产入口基准测试。
"""
args = parse_args()
try:
configure_benchmark_runtime()
validate_rust_runtime()
video_inputs = build_video_inputs(args.repeat_inputs)
music_inputs = build_music_inputs(args.repeat_inputs)
video_result = benchmark_suite(
video_inputs,
project_video_result,
loops=args.loops,
repeats=args.repeats,
)
music_result = benchmark_suite(
music_inputs,
project_music_result,
loops=args.loops,
repeats=args.repeats,
)
except (AssertionError, RuntimeError) as err:
print(f"benchmark_error={err}", file=sys.stderr)
return 2
print_suite_result("video", video_inputs, video_result, args.loops, args.repeats)
print_suite_result("music", music_inputs, music_result, args.loops, args.repeats)
return 0
if __name__ == "__main__":
raise SystemExit(main())