Files
pyflowx/benchmarks/__main__.py
T

420 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.
"""PyFlowX 性能基准套件.
用法::
python -m benchmarks # 运行全部基准
python -m benchmarks graph # 仅图构建基准
python -m benchmarks execution # 仅执行基准
python -m benchmarks context # 仅上下文注入基准
python -m benchmarks storage # 仅状态后端基准
python -m benchmarks advanced # 仅高级基准
"""
from __future__ import annotations
import math
import sys
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import Any
from rich.console import Console
import pyflowx as px
from benchmarks import print_results, time_it
from benchmarks.bench_advanced import run_advanced
from pyflowx import Graph, GraphDefaults, RetryPolicy, TaskSpec
from pyflowx.context import build_call_args
from pyflowx.storage import JSONBackend, MemoryBackend, SQLiteBackend
# ============================================================================
# 图生成工具
# ============================================================================
def make_chain(n: int) -> list[TaskSpec]:
"""生成 n 个任务的链式 DAG。"""
specs = [TaskSpec(f"t{i}", cmd=["true"]) for i in range(n)]
for i in range(1, n):
specs[i] = TaskSpec(f"t{i}", cmd=["true"], depends_on=(f"t{i - 1}",))
return specs
def make_diamond(n: int) -> list[TaskSpec]:
"""生成 n 个任务的菱形 DAG(每层宽度约 sqrt(n))。"""
width = max(1, int(math.sqrt(n)))
specs: list[TaskSpec] = []
prev_layer: list[str] = []
layer = 0
count = 0
while count < n:
cur_layer: list[str] = []
for j in range(width):
if count >= n:
break
name = f"l{layer}_t{j}"
deps = tuple(prev_layer) if prev_layer else ()
specs.append(TaskSpec(name, cmd=["true"], depends_on=deps))
cur_layer.append(name)
count += 1
prev_layer = cur_layer
layer += 1
return specs
def make_wide(n: int) -> list[TaskSpec]:
"""生成 n 个独立任务(无依赖,最大并行度)。"""
return [TaskSpec(f"t{i}", cmd=["true"]) for i in range(n)]
# ============================================================================
# 基准:图构建
# ============================================================================
def bench_construction() -> None:
"""图构建(from_specs + validate)基准。"""
results = []
for n in (10, 100, 500, 1000):
specs = make_chain(n)
ms, _ = time_it(lambda s=specs: Graph.from_specs(s), iterations=20)
results.append((f"chain({n})", 20, ms, 1.0 / (ms / 1000) if ms > 0 else 0))
for n in (10, 100, 500, 1000):
specs = make_diamond(n)
ms, _ = time_it(lambda s=specs: Graph.from_specs(s), iterations=20)
results.append((f"diamond({n})", 20, ms, 1.0 / (ms / 1000) if ms > 0 else 0))
print_results("图构建 (from_specs + validate)", results)
def bench_layers() -> None:
"""拓扑分层基准(冷启动 vs 缓存命中)。"""
results = []
for n in (100, 500, 1000):
specs = make_diamond(n)
graph = Graph.from_specs(specs)
def _cold(g: Graph = graph) -> None:
g._layers_cache = None # type: ignore[attr-defined]
g.layers()
ms_cold, ops_cold = time_it(_cold, iterations=50, warmup=5)
results.append((f"layers(cold,{n})", 50, ms_cold, ops_cold))
ms_hot, ops_hot = time_it(lambda g=graph: g.layers(), iterations=200, warmup=10)
results.append((f"layers(cached,{n})", 200, ms_hot, ops_hot))
print_results("拓扑分层 (layers)", results)
def bench_resolved_spec() -> None:
"""resolved_spec 缓存命中基准。"""
results = []
for n in (100, 500, 1000):
specs = make_chain(n)
defaults = GraphDefaults(retry=RetryPolicy(max_attempts=2))
graph = Graph.from_specs(specs, defaults=defaults)
name = f"t{n // 2}"
ms, ops = time_it(lambda g=graph, nm=name: g.resolved_spec(nm), iterations=500, warmup=20)
results.append((f"resolved_spec(cached,{n})", 500, ms, ops))
print_results("resolved_spec (缓存命中)", results)
def run_graph() -> None:
"""运行全部图基准。"""
bench_construction()
bench_layers()
bench_resolved_spec()
# ============================================================================
# 基准:任务执行
# ============================================================================
def bench_sequential() -> None:
"""sequential 策略执行基准。"""
results = []
def noop() -> None:
pass
for n in (50, 200, 500):
specs = [TaskSpec(f"t{i}", fn=noop) for i in range(n)]
graph = Graph.from_specs(specs)
ms, ops = time_it(lambda g=graph: px.run(g, strategy="sequential"), iterations=10, warmup=2)
results.append((f"sequential({n})", 10, ms, ops))
print_results("执行策略: sequential", results)
def bench_thread() -> None:
"""thread 策略执行基准。"""
results = []
def noop() -> None:
pass
for n in (50, 200, 500):
specs = [TaskSpec(f"t{i}", fn=noop) for i in range(n)]
graph = Graph.from_specs(specs)
ms, ops = time_it(lambda g=graph: px.run(g, strategy="thread"), iterations=10, warmup=2)
results.append((f"thread({n})", 10, ms, ops))
print_results("执行策略: thread", results)
def bench_async() -> None:
"""async 策略执行基准。"""
results = []
def noop() -> None:
pass
for n in (50, 200, 500):
specs = [TaskSpec(f"t{i}", fn=noop) for i in range(n)]
graph = Graph.from_specs(specs)
ms, ops = time_it(lambda g=graph: px.run(g, strategy="async"), iterations=10, warmup=2)
results.append((f"async({n})", 10, ms, ops))
print_results("执行策略: async", results)
def bench_dependency() -> None:
"""dependency 策略执行基准。"""
results = []
def noop() -> None:
pass
for n in (50, 200, 500):
specs = [TaskSpec(f"t{i}", fn=noop) for i in range(n)]
graph = Graph.from_specs(specs)
ms, ops = time_it(lambda g=graph: px.run(g, strategy="dependency"), iterations=10, warmup=2)
results.append((f"dependency({n})", 10, ms, ops))
print_results("执行策略: dependency", results)
def bench_cmd_execution() -> None:
"""cmd 任务执行基准(真实子进程)。"""
results = []
for n in (10, 50, 100):
specs = [TaskSpec(f"t{i}", cmd=["true"]) for i in range(n)]
graph = Graph.from_specs(specs)
ms, ops = time_it(lambda g=graph: px.run(g, strategy="sequential"), iterations=5, warmup=1)
results.append((f"cmd-sequential({n})", 5, ms, ops))
for n in (10, 50, 100):
specs = [TaskSpec(f"t{i}", cmd=["true"]) for i in range(n)]
graph = Graph.from_specs(specs)
ms, ops = time_it(lambda g=graph: px.run(g, strategy="thread", max_workers=8), iterations=5, warmup=1)
results.append((f"cmd-thread({n})", 5, ms, ops))
print_results("cmd 任务执行 (['true'])", results)
def run_execution() -> None:
"""运行全部执行基准。"""
bench_sequential()
bench_thread()
bench_async()
bench_dependency()
bench_cmd_execution()
# ============================================================================
# 基准:上下文注入
# ============================================================================
def bench_context_no_deps() -> None:
"""无依赖 fn 任务的上下文注入基准。"""
results = []
def noop() -> None:
pass
spec = TaskSpec("noop", fn=noop)
context: dict[str, Any] = {}
ms, ops = time_it(lambda s=spec, c=context: build_call_args(s, c), iterations=2000, warmup=100)
results.append(("fn(no-deps)", 2000, ms, ops))
# cmd 任务快速路径
spec_cmd = TaskSpec("cmd", cmd=["true"])
ms, ops = time_it(lambda s=spec_cmd, c=context: build_call_args(s, c), iterations=2000, warmup=100)
results.append(("cmd(fast-path)", 2000, ms, ops))
print_results("上下文注入 (build_call_args)", results)
def bench_context_with_deps() -> None:
"""有依赖 fn 任务的上下文注入基准。"""
results = []
def consumer(a: int, b: int) -> int:
return a + b
spec = TaskSpec("consumer", fn=consumer, depends_on=("a", "b"))
context = {"a": 1, "b": 2, "c": 3}
ms, ops = time_it(lambda s=spec, c=context: build_call_args(s, c), iterations=2000, warmup=100)
results.append(("fn(2-deps)", 2000, ms, ops))
# Context 标注
from pyflowx.task import Context
def ctx_fn(ctx: Context) -> int:
return sum(ctx.values())
spec_ctx = TaskSpec("ctx", fn=ctx_fn, depends_on=("a", "b"))
ms, ops = time_it(lambda s=spec_ctx, c=context: build_call_args(s, c), iterations=2000, warmup=100)
results.append(("fn(Context-annotated)", 2000, ms, ops))
# **kwargs
def kwargs_fn(**kwargs: int) -> int:
return sum(kwargs.values())
spec_kw = TaskSpec("kw", fn=kwargs_fn, depends_on=("a", "b"))
ms, ops = time_it(lambda s=spec_kw, c=context: build_call_args(s, c), iterations=2000, warmup=100)
results.append(("fn(**kwargs)", 2000, ms, ops))
print_results("上下文注入 (有依赖)", results)
def run_context() -> None:
"""运行全部上下文注入基准。"""
bench_context_no_deps()
bench_context_with_deps()
# ============================================================================
# 基准:状态后端
# ============================================================================
def bench_storage() -> None:
"""状态后端 save/load 基准。"""
results = []
# MemoryBackend
mem = MemoryBackend()
ms, ops = time_it(lambda: mem.save("key", "value"), iterations=1000, warmup=50)
results.append(("MemoryBackend.save", 1000, ms, ops))
ms, ops = time_it(mem.load, iterations=1000, warmup=50)
results.append(("MemoryBackend.load", 1000, ms, ops))
# JSONBackendbatch 模式)
tmp_dir = tempfile.mkdtemp()
json_path = str(Path(tmp_dir) / "state.json")
json_backend = JSONBackend(json_path)
with json_backend.batch():
for i in range(100):
json_backend.save(f"task_{i}", f"result_{i}")
def _json_save() -> None:
jb = JSONBackend(json_path)
with jb.batch():
for i in range(10):
jb.save(f"bench_{i}", f"val_{i}")
ms, ops = time_it(_json_save, iterations=50, warmup=5)
results.append(("JSONBackend.save(batch=10)", 50, ms, ops))
# 复杂 value(嵌套 dict)—— 展示 batch 模式延迟验证的优化效果
complex_value = {
"output": {"path": "/data/result.json", "size": 1024, "checksum": "abc123"},
"metrics": {"accuracy": 0.95, "latency_ms": 120, "samples": 1000},
"artifacts": [f"artifact_{j}.bin" for j in range(10)],
"metadata": {"version": "1.0", "tags": ["trained", "validated"], "created_at": "2026-07-07"},
}
def _json_save_complex() -> None:
jb = JSONBackend(json_path)
with jb.batch():
for i in range(10):
jb.save(f"bench_complex_{i}", complex_value)
ms, ops = time_it(_json_save_complex, iterations=50, warmup=5)
results.append(("JSONBackend.save(batch=10,complex)", 50, ms, ops))
ms, ops = time_it(json_backend.load, iterations=200, warmup=10)
results.append(("JSONBackend.load", 200, ms, ops))
# SQLiteBackend
db_path = str(Path(tmp_dir) / "state.db")
sqlite_backend = SQLiteBackend(db_path)
with sqlite_backend.batch():
for i in range(100):
sqlite_backend.save(f"task_{i}", f"result_{i}")
def _sqlite_save() -> None:
sb = SQLiteBackend(db_path)
with sb.batch():
for i in range(10):
sb.save(f"bench_{i}", f"val_{i}")
ms, ops = time_it(_sqlite_save, iterations=50, warmup=5)
results.append(("SQLiteBackend.save(batch=10)", 50, ms, ops))
ms, ops = time_it(sqlite_backend.load, iterations=200, warmup=10)
results.append(("SQLiteBackend.load", 200, ms, ops))
print_results("状态后端 (save/load)", results)
# 清理临时目录
import shutil
shutil.rmtree(tmp_dir, ignore_errors=True)
def run_storage() -> None:
"""运行全部状态后端基准。"""
bench_storage()
# ============================================================================
# 主入口
# ============================================================================
BENCH_MODULES: dict[str, Callable[[], None]] = {
"graph": run_graph,
"execution": run_execution,
"context": run_context,
"storage": run_storage,
"advanced": run_advanced,
}
def main(argv: list[str] | None = None) -> int:
"""CLI 入口。"""
args = argv if argv is not None else sys.argv[1:]
console = Console()
console.print("[bold cyan]PyFlowX 性能基准套件[/bold cyan]\n")
if not args or args[0] in ("--all", "-a"):
for name, fn in BENCH_MODULES.items():
console.print(f"[bold]运行: {name}[/bold]")
fn()
elif args[0] in BENCH_MODULES:
BENCH_MODULES[args[0]]()
elif args[0] in ("--help", "-h"):
console.print("用法: python -m benchmarks [graph|execution|context|storage]")
console.print(" 无参数 = 运行全部基准")
else:
console.print(f"[red]未知基准模块: {args[0]}[/red]")
console.print(f"可用: {', '.join(BENCH_MODULES)}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())