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

848 lines
30 KiB
Python
Raw Permalink 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.
"""
检测关键 async 路径中新引入的直接阻塞调用。
"""
from __future__ import annotations
import argparse
import ast
import json
from collections import Counter
from collections.abc import Iterator, Sequence
from dataclasses import dataclass
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/async-blocking-baseline.json"
SCAN_ROOTS = ("app",)
EXCLUDED_ROOTS = ("app/plugins",)
# RequestUtils 中直接执行同步 HTTP 请求或管理同步响应的方法。
_SYNC_HTTP_METHODS = {
"delete_res",
"get",
"get_json",
"get_res",
"get_stream",
"post",
"post_json",
"post_res",
"put",
"put_res",
"request",
"response_manager",
}
_REQUESTS_METHODS = {
"delete",
"get",
"head",
"patch",
"post",
"put",
"request",
}
# 这些 pathlib 操作会访问文件系统,不能视为纯路径计算。
_PATH_IO_METHODS = {
"exists",
"glob",
"is_dir",
"is_file",
"iterdir",
"mkdir",
"open",
"read_bytes",
"read_text",
"rename",
"rglob",
"stat",
"unlink",
"write_bytes",
"write_text",
}
_SHUTIL_METHODS = {
"copy",
"copyfile",
"copytree",
"make_archive",
"move",
"rmtree",
"unpack_archive",
"which",
}
_SUBPROCESS_METHODS = {
"Popen",
"call",
"check_call",
"check_output",
"run",
}
_OS_IO_METHODS = {"listdir", "scandir", "walk"}
# SystemConfigOper 的这些读取由内存快照提供,不计入同步数据库阻塞。
_SYSTEM_CONFIG_MEMORY_READS = {
"app.db.oper.SystemConfigOper.all",
"app.db.oper.SystemConfigOper.get",
"app.db.oper.systemconfig.SystemConfigOper.all",
"app.db.oper.systemconfig.SystemConfigOper.get",
}
@dataclass(frozen=True)
class _Binding:
"""
描述由明确导入或局部赋值建立的符号来源。
:param family: 能力来源分类,区分同步或异步 HTTP、路径、数据库和普通模块;unknown 表示来源不明
:param qualified_name: 已解析的完整模块、类或方法路径;来源不明时为空字符串
:param kind: 符号形态,区分模块、类、实例、可调用对象、集合和未知来源;默认 module
"""
family: str
qualified_name: str
kind: str = "module"
# 显式未知绑定用于遮蔽外层同名导入,避免错误继承其能力来源。
_UNKNOWN_BINDING = _Binding("unknown", "", "unknown")
# 控制流合并时优先保留这些同步能力,防止分支遮蔽已知阻塞风险。
_BLOCKING_FAMILIES = {
"os",
"requests",
"shutil",
"subprocess",
"sync_http",
"sync_oper",
"sync_path",
"time",
}
def _binding_for_qualified(qualified_name: str) -> _Binding:
"""
按稳定模块路径识别门禁关心的符号族。
"""
if qualified_name == "app.adapters.network.http.RequestUtils":
return _Binding("sync_http", qualified_name, "class")
if qualified_name == "app.adapters.network.http.AsyncRequestUtils":
return _Binding("async_http", qualified_name, "class")
if qualified_name in {"requests.Session", "requests.sessions.Session"}:
return _Binding("sync_http", qualified_name, "class")
if qualified_name == "pathlib.Path":
return _Binding("sync_path", qualified_name, "class")
if qualified_name == "anyio.Path":
return _Binding("async_path", qualified_name, "class")
if qualified_name.startswith("app.db.oper.") and qualified_name.endswith("Oper"):
return _Binding("sync_oper", qualified_name, "class")
if qualified_name.startswith("requests."):
return _Binding("requests", qualified_name, "callable")
if qualified_name.startswith("shutil."):
return _Binding("shutil", qualified_name, "callable")
if qualified_name.startswith("subprocess."):
return _Binding("subprocess", qualified_name, "callable")
if qualified_name.startswith("os."):
return _Binding("os", qualified_name, "callable")
if qualified_name.startswith("time."):
return _Binding("time", qualified_name, "callable")
return _Binding("module", qualified_name)
def _resolve_binding(
expression: ast.expr,
bindings: dict[str, _Binding],
) -> _Binding | None:
"""
解析明确导入、构造、属性访问和简单路径派生。
"""
if isinstance(expression, ast.Name):
return bindings.get(expression.id)
if isinstance(expression, ast.Attribute):
base = _resolve_binding(expression.value, bindings)
if not base or base.kind in {"collection", "unknown"}:
return None
qualified_name = f"{base.qualified_name}.{expression.attr}"
if base.kind == "module":
return _binding_for_qualified(qualified_name)
return _Binding(base.family, qualified_name, "callable")
if isinstance(expression, ast.Call):
target = _resolve_binding(expression.func, bindings)
if target and target.kind == "class":
return _Binding(target.family, target.qualified_name, "instance")
return None
if isinstance(expression, ast.BinOp) and isinstance(expression.op, ast.Div):
left = _resolve_binding(expression.left, bindings)
if left and left.family in {"async_path", "sync_path"}:
return left
if isinstance(expression, ast.IfExp):
return _merge_binding_options(
"",
(
_resolve_binding(expression.body, bindings),
_resolve_binding(expression.orelse, bindings),
),
)
return None
def _merge_binding_options(
name: str,
options: Sequence[_Binding | None],
) -> _Binding | None:
"""
控制流合流时保留任一分支可能进入的同步阻塞类型。
"""
if options and all(option == options[0] for option in options):
return options[0]
blocking = sorted(
(
option
for option in options
if option and option.family in _BLOCKING_FAMILIES
),
key=lambda item: (item.family, item.qualified_name, item.kind),
)
if blocking:
return blocking[0]
if name == "open" and any(option is None for option in options):
return None
return _UNKNOWN_BINDING
def _merge_binding_states(
states: Sequence[dict[str, _Binding]],
) -> dict[str, _Binding]:
"""
合并互斥控制流的局部符号表。
"""
names = set().union(*(state.keys() for state in states))
merged: dict[str, _Binding] = {}
for name in names:
binding = _merge_binding_options(name, tuple(state.get(name) for state in states))
if binding:
merged[name] = binding
return merged
def _load_oper_methods(root: Path) -> dict[str, set[str]]:
"""
从 Oper 源码建立同步方法索引,避免按方法名猜测数据库调用。
"""
methods: dict[str, set[str]] = {}
oper_root = root / "app/db/oper"
if not oper_root.is_dir():
return methods
for path in sorted(oper_root.glob("*.py")):
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
module = ".".join(path.relative_to(root).with_suffix("").parts)
for node in tree.body:
if not isinstance(node, ast.ClassDef) or not node.name.endswith("Oper"):
continue
qualified_name = f"{module}.{node.name}"
sync_methods = {
child.name
for child in node.body
if isinstance(child, ast.FunctionDef)
and not child.name.startswith("__")
}
methods[qualified_name] = sync_methods
methods[f"app.db.oper.{node.name}"] = sync_methods
return methods
class _ImportCollector(ast.NodeVisitor):
"""
收集模块级导入,作为每个 async 函数的初始符号表。
"""
def __init__(self) -> None:
"""
初始化模块级符号来源表。
"""
self.bindings: dict[str, _Binding] = {}
def visit_Import(self, node: ast.Import) -> None:
"""
记录模块导入及别名对应的完整来源。
"""
for alias in node.names:
local_name = alias.asname or alias.name.split(".", 1)[0]
qualified_name = alias.name if alias.asname else local_name
self.bindings[local_name] = _binding_for_qualified(qualified_name)
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
"""
记录绝对导入的符号来源,跳过相对导入和通配导入。
"""
if node.level or not node.module:
return
for alias in node.names:
if alias.name == "*":
continue
local_name = alias.asname or alias.name
qualified_name = f"{node.module}.{alias.name}"
self.bindings[local_name] = _binding_for_qualified(qualified_name)
def _bind_target(self, target: ast.expr, binding: _Binding | None) -> None:
"""
传播简单赋值来源;解包目标标记为未知,避免猜测各元素类型。
"""
if isinstance(target, ast.Name):
self.bindings[target.id] = binding or _UNKNOWN_BINDING
elif isinstance(target, (ast.Tuple, ast.List)):
for element in target.elts:
self._bind_target(element, None)
def visit_Assign(self, node: ast.Assign) -> None:
"""
传播模块级简单别名,并清除无法证明来源的旧绑定。
"""
binding = _resolve_binding(node.value, self.bindings)
for target in node.targets:
self._bind_target(target, binding)
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
"""
按赋值右值传播模块级来源;没有右值时标记为未知。
"""
binding = _resolve_binding(node.value, self.bindings) if node.value else None
self._bind_target(node.target, binding)
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
"""
跳过函数体,避免把局部导入泄漏到模块符号表。
"""
return
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
"""
跳过异步函数体,保持模块级导入收集边界。
"""
return
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""
跳过类体,避免将类内绑定当作模块全局来源。
"""
return
class _AsyncCallVisitor(ast.NodeVisitor):
"""
用局部符号传播识别一个 async 函数直接执行的阻塞调用。
"""
def __init__(
self,
function: ast.FunctionDef | ast.AsyncFunctionDef,
module_bindings: dict[str, _Binding],
oper_methods: dict[str, set[str]],
*,
record_calls: bool,
) -> None:
"""
复制可见符号来源并绑定参数,为单个函数建立调用统计。
:param function: 本次分析的函数定义
:param module_bindings: 定义处可见的外层符号来源,不原地修改
:param oper_methods: Oper 完整类名到同步方法名集合的索引
:param record_calls: 是否统计当前函数中的阻塞调用;关闭时仍追踪嵌套函数
"""
self.calls: Counter[str] = Counter()
self.nested_functions: list[
tuple[ast.FunctionDef | ast.AsyncFunctionDef, dict[str, _Binding]]
] = []
self._bindings = dict(module_bindings)
self._oper_methods = oper_methods
self._record_calls = record_calls
self._bind_arguments(function)
def _bind_arguments(
self,
function: ast.FunctionDef | ast.AsyncFunctionDef,
) -> None:
"""
用参数遮蔽外层同名绑定,并从显式注解恢复已知能力来源。
"""
arguments = (
*function.args.posonlyargs,
*function.args.args,
*function.args.kwonlyargs,
)
for argument in arguments:
binding = self._annotation_binding(argument.annotation)
if binding:
self._bindings[argument.arg] = binding
else:
self._bindings[argument.arg] = _UNKNOWN_BINDING
def _annotation_binding(self, annotation: ast.expr | None) -> _Binding | None:
"""
从联合、可选和容器注解中解析实例或集合元素来源。
:return: 可证明的实例或集合绑定;未识别时返回 None
"""
if annotation is None:
return None
if isinstance(annotation, ast.Subscript):
container = ast.unparse(annotation.value).rsplit(".", 1)[-1]
elements = (
annotation.slice.elts
if isinstance(annotation.slice, ast.Tuple)
else (annotation.slice,)
)
for element in elements:
binding = self._annotation_binding(element)
if not binding:
continue
if container in {"list", "List", "Sequence", "set", "tuple"}:
return _Binding(
binding.family,
binding.qualified_name,
"collection",
)
if container in {"Annotated", "Optional", "Union"}:
return binding
return None
if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
return self._annotation_binding(annotation.left) or self._annotation_binding(
annotation.right
)
binding = _resolve_binding(annotation, self._bindings)
if binding and binding.kind == "class":
return _Binding(binding.family, binding.qualified_name, "instance")
return None
def _resolve(self, expression: ast.expr) -> _Binding | None:
return _resolve_binding(expression, self._bindings)
@staticmethod
def _call_label(binding: _Binding) -> str:
"""
把完整调用路径归一为基线使用的稳定名称。
"""
parts = binding.qualified_name.split(".")
if binding.family == "sync_oper" and len(parts) >= 2:
return ".".join(parts[-2:])
if binding.family in {"async_path", "sync_path"} and parts:
return f"Path.{parts[-1]}"
if binding.family == "sync_http" and len(parts) >= 2:
return ".".join(parts[-2:])
if binding.family in {"os", "requests", "shutil", "subprocess", "time"}:
return ".".join(parts[-2:])
return binding.qualified_name
def _record_call(self, node: ast.Call, binding: _Binding | None) -> None:
"""
仅在启用统计且来源命中同步能力规则时累计调用次数。
未被局部符号遮蔽的 open 按内置阻塞调用处理;系统配置内存读取不计入数据库阻塞。
"""
if not self._record_calls:
return
if not binding:
if isinstance(node.func, ast.Name) and node.func.id == "open":
self.calls["open"] += 1
return
method = binding.qualified_name.rsplit(".", 1)[-1]
blocked = False
if binding.family == "sync_http":
blocked = binding.kind == "callable" and method in _SYNC_HTTP_METHODS
elif binding.family == "requests":
blocked = method in _REQUESTS_METHODS
elif binding.family == "sync_path":
blocked = binding.kind == "callable" and method in _PATH_IO_METHODS
elif binding.family == "shutil":
blocked = method in _SHUTIL_METHODS
elif binding.family == "subprocess":
blocked = method in _SUBPROCESS_METHODS
elif binding.family == "os":
blocked = method in _OS_IO_METHODS
elif binding.family == "time":
blocked = method == "sleep"
elif binding.family == "sync_oper" and binding.kind == "callable":
class_name, method_name = binding.qualified_name.rsplit(".", 1)
blocked = (
method_name in self._oper_methods.get(class_name, set())
and binding.qualified_name not in _SYSTEM_CONFIG_MEMORY_READS
)
if blocked:
self.calls[self._call_label(binding)] += 1
def _bind_target(self, target: ast.expr, binding: _Binding | None) -> None:
"""
传播简单目标的已知来源;解包赋值将各目标标记为未知。
"""
if isinstance(target, ast.Name):
self._bindings[target.id] = binding or _UNKNOWN_BINDING
elif isinstance(target, (ast.Tuple, ast.List)):
for element in target.elts:
self._bind_target(element, None)
def visit_Import(self, node: ast.Import) -> None:
"""
把函数内的模块导入加入当前符号表。
"""
collector = _ImportCollector()
collector.visit_Import(node)
self._bindings.update(collector.bindings)
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
"""
把函数内可解析的绝对符号导入加入当前符号表。
"""
collector = _ImportCollector()
collector.visit_ImportFrom(node)
self._bindings.update(collector.bindings)
def visit_Assign(self, node: ast.Assign) -> None:
"""
先统计右值调用,再将可证明来源传播给赋值目标。
"""
self.visit(node.value)
binding = self._resolve(node.value)
for target in node.targets:
self._bind_target(target, binding)
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
"""
统计右值调用,并依次使用右值来源和类型注解绑定目标。
"""
if node.value:
self.visit(node.value)
binding = self._resolve(node.value) if node.value else None
binding = binding or self._annotation_binding(node.annotation)
self._bind_target(node.target, binding)
def visit_AugAssign(self, node: ast.AugAssign) -> None:
"""
统计增量赋值右值,并使目标的旧来源失效。
"""
self.visit(node.value)
self._bind_target(node.target, None)
def _visit_branch(
self,
statements: Sequence[ast.stmt],
initial: dict[str, _Binding],
) -> dict[str, _Binding]:
"""
在指定符号副本中分析分支,并恢复调用方的符号表。
:return: 分支执行后的来源映射;调用统计仍累计在当前访问器中
"""
saved = self._bindings
self._bindings = dict(initial)
for statement in statements:
self.visit(statement)
result = self._bindings
self._bindings = saved
return result
def visit_If(self, node: ast.If) -> None:
"""
分别分析互斥分支并合并来源,保留任一路径可能存在的阻塞能力。
"""
self.visit(node.test)
initial = dict(self._bindings)
body = self._visit_branch(node.body, initial)
alternate = self._visit_branch(node.orelse, initial) if node.orelse else initial
self._bindings = _merge_binding_states((body, alternate))
def visit_For(self, node: ast.For | ast.AsyncFor) -> None:
"""
从集合来源推断循环元素,并合并零次执行与循环完成路径。
"""
self.visit(node.iter)
initial = dict(self._bindings)
collection = self._resolve(node.iter)
element = (
_Binding(collection.family, collection.qualified_name, "instance")
if collection and collection.kind == "collection"
else None
)
self._bind_target(node.target, element)
for statement in node.body:
self.visit(statement)
iterated = dict(self._bindings)
completed = self._visit_branch(node.orelse, iterated)
self._bindings = _merge_binding_states((initial, completed))
def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
"""
按相同的元素传播和零次执行规则分析异步循环。
"""
self.visit_For(node)
def _visit_comprehension(
self,
generators: Sequence[ast.comprehension],
outputs: Sequence[ast.expr],
) -> None:
"""
在独立符号副本中分析推导式的迭代、条件和输出表达式。
"""
saved = self._bindings
self._bindings = dict(saved)
for generator in generators:
self.visit(generator.iter)
collection = self._resolve(generator.iter)
element = (
_Binding(collection.family, collection.qualified_name, "instance")
if collection and collection.kind == "collection"
else None
)
self._bind_target(generator.target, element)
for condition in generator.ifs:
self.visit(condition)
for output in outputs:
self.visit(output)
self._bindings = saved
def visit_ListComp(self, node: ast.ListComp) -> None:
"""
隔离列表推导式的目标绑定,并统计其表达式中的阻塞调用。
"""
self._visit_comprehension(node.generators, (node.elt,))
def visit_SetComp(self, node: ast.SetComp) -> None:
"""
隔离集合推导式的目标绑定,并统计其表达式中的阻塞调用。
"""
self._visit_comprehension(node.generators, (node.elt,))
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
"""
在独立符号表中扫描生成器表达式,记录其中可证明的阻塞调用。
"""
self._visit_comprehension(node.generators, (node.elt,))
def visit_DictComp(self, node: ast.DictComp) -> None:
"""
隔离字典推导式的目标绑定,同时扫描键和值表达式。
"""
self._visit_comprehension(node.generators, (node.key, node.value))
def visit_NamedExpr(self, node: ast.NamedExpr) -> None:
"""
统计赋值表达式的右值,并传播其来源到当前作用域。
"""
self.visit(node.value)
self._bind_target(node.target, self._resolve(node.value))
def visit_Call(self, node: ast.Call) -> None:
"""
记录已知阻塞调用,并继续检查参数和立即调用的匿名函数体。
"""
binding = self._resolve(node.func)
self._record_call(node, binding)
if isinstance(node.func, ast.Lambda):
self._visit_lambda_defaults(node.func)
for argument in node.args:
self.visit(argument)
for keyword in node.keywords:
self.visit(keyword.value)
self.visit(node.func.body)
return
self.generic_visit(node)
def _visit_definition_expressions(
self,
node: ast.FunctionDef | ast.AsyncFunctionDef,
) -> None:
"""
扫描函数定义时执行的装饰器和默认参数,不进入函数体。
"""
for decorator in node.decorator_list:
self.visit(decorator)
for default in node.args.defaults:
self.visit(default)
for keyword_default in node.args.kw_defaults:
if keyword_default:
self.visit(keyword_default)
def _visit_nested_function(
self,
node: ast.FunctionDef | ast.AsyncFunctionDef,
) -> None:
"""
记录嵌套函数及定义处的来源快照,交由独立分析统计函数体。
"""
self._visit_definition_expressions(node)
self.nested_functions.append((node, dict(self._bindings)))
self._bindings[node.name] = _UNKNOWN_BINDING
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
"""
扫描定义期表达式,并将同步嵌套函数加入后续分析队列。
"""
self._visit_nested_function(node)
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
"""
扫描定义期表达式,并将异步嵌套函数加入后续分析队列。
"""
self._visit_nested_function(node)
def _visit_lambda_defaults(self, node: ast.Lambda) -> None:
"""
统计匿名函数创建时执行的默认参数表达式。
"""
for default in node.args.defaults:
self.visit(default)
for keyword_default in node.args.kw_defaults:
if keyword_default:
self.visit(keyword_default)
def visit_Lambda(self, node: ast.Lambda) -> None:
"""
仅扫描匿名函数默认参数,函数体由直接调用路径另行访问。
"""
self._visit_lambda_defaults(node)
def _root_functions(
tree: ast.Module,
) -> Iterator[tuple[str, ast.AsyncFunctionDef]]:
"""
产出模块顶层和类直接拥有的 async 入口。
"""
for node in tree.body:
if isinstance(node, ast.AsyncFunctionDef):
yield node.name, node
elif isinstance(node, ast.ClassDef):
for method in node.body:
if isinstance(method, ast.AsyncFunctionDef):
yield f"{node.name}.{method.name}", method
def _is_excluded_path(root: Path, path: Path) -> bool:
"""
判断源码路径是否位于明确排除的插件目录。
"""
relative = path.relative_to(root).as_posix()
return any(
relative == excluded or relative.startswith(f"{excluded}/")
for excluded in EXCLUDED_ROOTS
)
def _scan_paths(root: Path, scan_roots: Sequence[str | Path]) -> Iterator[Path]:
"""
按稳定顺序产出存在且不属于插件目录的扫描目标。
"""
for scan_root in scan_roots:
target = root / scan_root
if target.is_file():
if not _is_excluded_path(root, target):
yield target
elif target.is_dir():
yield from (
path
for path in sorted(target.rglob("*.py"))
if not _is_excluded_path(root, path)
)
def collect_async_blocking(
root: Path = PROJECT_ROOT,
scan_roots: Sequence[str | Path] = SCAN_ROOTS,
) -> dict[str, int]:
"""
扫描关键目录并以文件、函数、调用名聚合存量次数。
:param root: 仓库根目录,同时用于读取 Oper 同步方法声明
:param scan_roots: 相对于仓库根目录的文件或目录;插件路径始终排除
:return: 文件、函数限定名与调用名组成的键到静态调用次数的映射
"""
debt: Counter[str] = Counter()
oper_methods = _load_oper_methods(root)
for path in _scan_paths(root, scan_roots):
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
imports = _ImportCollector()
for statement in tree.body:
imports.visit(statement)
relative = path.relative_to(root).as_posix()
pending: list[
tuple[
str,
ast.FunctionDef | ast.AsyncFunctionDef,
dict[str, _Binding],
]
] = [
(qualname, function, imports.bindings)
for qualname, function in _root_functions(tree)
]
pending_index = 0
while pending_index < len(pending):
qualname, function, lexical_bindings = pending[pending_index]
pending_index += 1
is_async = isinstance(function, ast.AsyncFunctionDef)
visitor = _AsyncCallVisitor(
function,
lexical_bindings,
oper_methods,
record_calls=is_async,
)
for statement in function.body:
visitor.visit(statement)
if is_async:
for call_name, count in visitor.calls.items():
debt[f"{relative}:{qualname}:{call_name}"] += count
pending.extend(
(
f"{qualname}.{nested.name}",
nested,
bindings,
)
for nested, bindings in visitor.nested_functions
)
return dict(sorted(debt.items()))
def compare_async_blocking(
baseline: dict[str, int], current: dict[str, int]
) -> list[str]:
"""
允许存量减少或删除,拒绝新增阻塞调用和调用次数增长。
:param baseline: 已审查的阻塞调用次数上限
:param current: 当前源码收集出的阻塞调用次数
:return: 新增或增长条目的诊断列表;下降不构成问题
"""
problems = []
for entry, count in current.items():
previous = baseline.get(entry)
if previous is None:
problems.append(f"新增 async 阻塞调用:{entry} x{count}")
elif count > previous:
problems.append(f"async 阻塞调用增长:{entry} x{count}>{previous}")
return problems
def main() -> int:
"""
执行 async 阻塞基线检查或显式写入。
"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--write", action="store_true", help="写入当前阻塞债务")
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
args = parser.parse_args()
current = collect_async_blocking()
if args.write:
args.baseline.parent.mkdir(parents=True, exist_ok=True)
args.baseline.write_text(
json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(f"已写入 {args.baseline.relative_to(PROJECT_ROOT)}")
return 0
baseline = json.loads(args.baseline.read_text(encoding="utf-8"))
problems = compare_async_blocking(baseline, current)
if problems:
print("\n".join(problems))
return 1
print("async 阻塞调用 ratchet 通过")
return 0
if __name__ == "__main__":
raise SystemExit(main())