Files
MoviePilot/scripts/perf/task_registry_ab.py
2026-08-25 06:43:37 +08:00

124 lines
3.9 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.
"""测量 TaskRegistry 跨线程 pending submission 的关停终态与提交吞吐。"""
from __future__ import annotations
import argparse
import asyncio
import inspect
import json
import statistics
import sys
import time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from app.runtime.tasks import TaskRegistry
async def _value(value: int) -> int:
"""返回输入值,提供最小可完成协程。"""
return value
def pending_shutdown_probe() -> dict[str, object]:
"""在目标 loop 尚未分发 callback 时关闭 Registry,并报告提交终态。"""
registry = TaskRegistry()
target_loop = asyncio.new_event_loop()
coroutine = _value(1)
completion = registry.submit_threadsafe(
coroutine,
loop=target_loop,
owner="probe.pending",
)
shutdown_result = asyncio.run(registry.shutdown(timeout_seconds=0.001))
result = {
"shutdown_result": shutdown_result,
"completion_done": completion.done(),
"completion_cancelled": completion.cancelled(),
"coroutine_state": inspect.getcoroutinestate(coroutine),
}
completion.cancel()
coroutine.close()
target_loop.close()
return result
async def throughput_sample(iterations: int) -> dict[str, float]:
"""从工作线程提交一组最小协程,并测量提交和完整完成时间。"""
registry = TaskRegistry()
loop = asyncio.get_running_loop()
started = time.perf_counter()
def submit_all():
"""在同一宿主线程连续提交,保持各轮工作负载一致。"""
return [
registry.submit_threadsafe(
_value(index),
loop=loop,
owner="probe.throughput",
)
for index in range(iterations)
]
completions = await asyncio.to_thread(submit_all)
submitted = time.perf_counter()
values = await asyncio.gather(
*(asyncio.wrap_future(completion) for completion in completions)
)
finished = time.perf_counter()
assert sum(values) == iterations * (iterations - 1) // 2
assert await registry.shutdown(timeout_seconds=1.0) is True
return {
"submit_ms": (submitted - started) * 1000,
"total_ms": (finished - started) * 1000,
}
async def run_samples(iterations: int, samples: int) -> list[dict[str, float]]:
"""顺序执行多轮样本,避免并行样本互相争抢事件循环。"""
return [await throughput_sample(iterations) for _ in range(samples)]
def parse_args() -> argparse.Namespace:
"""解析探针负载规模。"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--iterations", type=int, default=2000)
parser.add_argument("--samples", type=int, default=7)
return parser.parse_args()
def main() -> None:
"""运行终态探针和吞吐样本并输出 JSON。"""
args = parse_args()
if args.iterations < 1 or args.samples < 1:
raise SystemExit("iterations 和 samples 必须大于 0")
samples = asyncio.run(run_samples(args.iterations, args.samples))
print(
json.dumps(
{
"pending": pending_shutdown_probe(),
"throughput": {
"iterations": args.iterations,
"samples": args.samples,
"submit_ms": [sample["submit_ms"] for sample in samples],
"total_ms": [sample["total_ms"] for sample in samples],
"submit_median_ms": statistics.median(
sample["submit_ms"] for sample in samples
),
"total_median_ms": statistics.median(
sample["total_ms"] for sample in samples
),
},
},
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
main()