mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 12:56:34 +08:00
ci(architecture): close complexity and concurrency blind spots
This commit is contained in:
7
.github/workflows/test.yml
vendored
7
.github/workflows/test.yml
vendored
@@ -63,7 +63,12 @@ jobs:
|
||||
run: uv run --locked --no-sync mypy --config-file mypy.ini
|
||||
|
||||
- name: Check complexity ratchet
|
||||
run: uv run --locked --no-sync python scripts/architecture/complexity.py
|
||||
run: |
|
||||
uv run --locked --no-sync python scripts/architecture/complexity.py
|
||||
uv run --locked --no-sync python scripts/architecture/complexity.py --v2
|
||||
|
||||
- name: Check native concurrency ownership
|
||||
run: uv run --locked --no-sync python scripts/architecture/concurrency.py
|
||||
|
||||
- name: Check async blocking ratchet
|
||||
run: uv run --locked --no-sync python scripts/architecture/async_blocking.py
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
| RES-003 | `PLANNED`(R3) | Outbox after_commit 失败只有内存 pending 标记,重启不可恢复 | 持久 intent、唯一 handler、claim/fencing、重启回放、幂等和失败观测完整闭环 |
|
||||
| RES-004 | `PLANNED`(R4) | Startup initializer 与插件市场存在多套 Transport/Adapter/Manager 构造 | 所有构造回收到 composition root,插件兼容只经 SDK/Compat,canonical 不保留重复正式入口 |
|
||||
| RES-005 | `PLANNED`(R5) | 审计声明、机器门禁、CI 与远端交付状态需要重新校准 | 文档与事实源一致;专项、锁定全量、Pylint、架构/兼容门禁和 GitHub CI 闭环,远端 `0/0` |
|
||||
R2 已落地门禁基础:`complexity.py --v2` 覆盖私有/嵌套方法、类、文件和
|
||||
`app/scheduler/`;`concurrency.py` 覆盖宿主原生并发原语并冻结调用点与词法 owner。
|
||||
当前基线只允许删除或收缩,新增调用点和 owner 漂移均阻断 CI;现有历史热点仍须在后续
|
||||
叶子中按真实所有权逐项收口,不得以生成基线伪装为债务清零。
|
||||
|
||||
## 1. 结论摘要
|
||||
|
||||
|
||||
@@ -216,6 +216,18 @@ only the historical `Scheduler` and `SchedulerChain` identities. New plugins use
|
||||
the narrow `app.sdk.scheduler` facade; internal Scheduler owners must not be
|
||||
re-exported from the package root, SDK or Compat.
|
||||
|
||||
Complexity and concurrency governance covers the complete canonical execution
|
||||
surface rather than only public methods. `scripts/architecture/complexity.py --v2`
|
||||
ratchets private and nested methods, class/file hotspots, and the
|
||||
`app/scheduler/` package; its generated baseline is an evidence ledger, not a
|
||||
permission to add another oversized owner. `scripts/architecture/concurrency.py`
|
||||
enumerates native Thread, Timer, thread/process executors, Process,
|
||||
asyncio task/thread helpers, and executor hand-off calls across host roots. Each
|
||||
fact must have a stable lexical owner and remain in the baseline with no new
|
||||
call site or owner drift. `app/plugins/**`, `app/sdk/**`, and
|
||||
`app/runtime/compat/**` are excluded because their compatibility/runtime
|
||||
contracts are governed by their own exact manifests.
|
||||
|
||||
`app.schemas` and the `app.db` package root are compatibility facades, not
|
||||
implementation dependency hubs. Host code imports concrete schema submodules; the schema root
|
||||
resolves its generated export manifest lazily for plugins and legacy callers.
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Iterable
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/complexity-baseline.json"
|
||||
V2_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/complexity-v2-baseline.json"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -30,6 +31,85 @@ RULES = (
|
||||
)
|
||||
|
||||
|
||||
def _iter_python_files(root: Path, roots: tuple[str, ...]) -> Iterable[Path]:
|
||||
"""遍历 canonical 宿主源码,明确排除运行时插件副本。"""
|
||||
for scan_root in roots:
|
||||
path = root / scan_root
|
||||
if path.is_file() and path.suffix == ".py":
|
||||
yield path
|
||||
continue
|
||||
if not path.is_dir():
|
||||
continue
|
||||
for candidate in sorted(path.rglob("*.py")):
|
||||
relative = candidate.relative_to(root).as_posix()
|
||||
if relative == "app/plugins" or relative.startswith("app/plugins/"):
|
||||
continue
|
||||
yield candidate
|
||||
|
||||
|
||||
def _walk_functions(
|
||||
nodes: Iterable[ast.AST],
|
||||
prefix: str = "",
|
||||
) -> Iterable[tuple[str, ast.FunctionDef | ast.AsyncFunctionDef]]:
|
||||
"""递归收集函数、方法和嵌套编排器,避免只检查公开入口。"""
|
||||
for node in nodes:
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
qualname = f"{prefix}.{node.name}" if prefix else node.name
|
||||
if not node.name.startswith("__"):
|
||||
yield qualname, node
|
||||
yield from _walk_functions(node.body, qualname)
|
||||
elif isinstance(node, ast.ClassDef):
|
||||
qualname = f"{prefix}.{node.name}" if prefix else node.name
|
||||
yield from _walk_functions(node.body, qualname)
|
||||
elif isinstance(node, ast.If):
|
||||
yield from _walk_functions(node.body, prefix)
|
||||
yield from _walk_functions(node.orelse, prefix)
|
||||
elif isinstance(node, (ast.For, ast.AsyncFor, ast.While, ast.With, ast.AsyncWith)):
|
||||
yield from _walk_functions(node.body, prefix)
|
||||
yield from _walk_functions(node.orelse, prefix) if isinstance(node, (ast.For, ast.AsyncFor)) else ()
|
||||
elif isinstance(node, ast.Try):
|
||||
yield from _walk_functions(node.body, prefix)
|
||||
yield from _walk_functions(node.orelse, prefix)
|
||||
yield from _walk_functions(node.finalbody, prefix)
|
||||
for handler in node.handlers:
|
||||
yield from _walk_functions(handler.body, prefix)
|
||||
|
||||
|
||||
def _walk_classes(
|
||||
nodes: Iterable[ast.AST],
|
||||
prefix: str = "",
|
||||
) -> Iterable[tuple[str, ast.ClassDef]]:
|
||||
"""递归收集类体长度,覆盖 Scheduler 和私有 owner。"""
|
||||
for node in nodes:
|
||||
if isinstance(node, ast.ClassDef):
|
||||
qualname = f"{prefix}.{node.name}" if prefix else node.name
|
||||
yield qualname, node
|
||||
yield from _walk_classes(node.body, qualname)
|
||||
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
yield from _walk_classes(node.body, prefix)
|
||||
|
||||
|
||||
def collect_complexity_v2(root: Path = PROJECT_ROOT) -> dict[str, dict[str, int]]:
|
||||
"""收集私有方法、类、文件和 Scheduler 的可审计复杂度事实。"""
|
||||
roots = ("app/api/endpoints", "app/application", "app/chain", "app/scheduler")
|
||||
report: dict[str, dict[str, int]] = {"method": {}, "class": {}, "file": {}}
|
||||
for path in _iter_python_files(root, roots):
|
||||
relative = path.relative_to(root).as_posix()
|
||||
source_lines = path.read_text(encoding="utf-8").splitlines()
|
||||
if len(source_lines) > 1000:
|
||||
report["file"][relative] = len(source_lines)
|
||||
tree = ast.parse("\n".join(source_lines), filename=str(path))
|
||||
for qualname, node in _walk_functions(tree.body):
|
||||
line_count = (node.end_lineno or node.lineno) - node.lineno + 1
|
||||
if line_count > 150:
|
||||
report["method"][f"{relative}:{qualname}"] = line_count
|
||||
for qualname, node in _walk_classes(tree.body):
|
||||
line_count = (node.end_lineno or node.lineno) - node.lineno + 1
|
||||
if line_count > 500:
|
||||
report["class"][f"{relative}:{qualname}"] = line_count
|
||||
return report
|
||||
|
||||
|
||||
def _is_endpoint(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
||||
"""识别带常见 HTTP method decorator 的 API endpoint。"""
|
||||
methods = {"get", "post", "put", "patch", "delete", "options", "head"}
|
||||
@@ -92,12 +172,48 @@ def compare_complexity(
|
||||
return problems
|
||||
|
||||
|
||||
def compare_complexity_v2(
|
||||
baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]]
|
||||
) -> list[str]:
|
||||
"""返回 v2 私有方法、类和文件事实的新增或增长问题。"""
|
||||
problems: list[str] = []
|
||||
for category in baseline.keys() | current.keys():
|
||||
previous = baseline.get(category, {})
|
||||
for entry, line_count in current.get(category, {}).items():
|
||||
if entry not in previous:
|
||||
problems.append(f"{category}: 新增超限 {entry}={line_count}")
|
||||
elif line_count > previous[entry]:
|
||||
problems.append(
|
||||
f"{category}: 既有超限增长 {entry}={line_count}>{previous[entry]}"
|
||||
)
|
||||
return problems
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""执行复杂度 baseline check 或显式 write。"""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--write", action="store_true", help="写入当前超限基线")
|
||||
parser.add_argument("--v2", action="store_true", help="检查私有方法、类、文件和 Scheduler")
|
||||
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
|
||||
args = parser.parse_args()
|
||||
if args.v2:
|
||||
baseline_path = args.baseline if args.baseline != DEFAULT_BASELINE else V2_BASELINE
|
||||
current_v2 = collect_complexity_v2()
|
||||
if args.write:
|
||||
baseline_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
baseline_path.write_text(
|
||||
json.dumps(current_v2, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"已写入 {baseline_path.relative_to(PROJECT_ROOT)}")
|
||||
return 0
|
||||
baseline_v2 = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
problems_v2 = compare_complexity_v2(baseline_v2, current_v2)
|
||||
if problems_v2:
|
||||
print("\n".join(problems_v2))
|
||||
return 1
|
||||
print("复杂度 v2 ratchet 通过")
|
||||
return 0
|
||||
current = collect_complexity()
|
||||
if args.write:
|
||||
args.baseline.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
200
scripts/architecture/concurrency.py
Normal file
200
scripts/architecture/concurrency.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""枚举宿主原生并发原语并维护稳定 owner 事实。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/concurrency-baseline.json"
|
||||
SCAN_ROOTS = (
|
||||
"app/adapters",
|
||||
"app/api",
|
||||
"app/agent",
|
||||
"app/application",
|
||||
"app/chain",
|
||||
"app/command.py",
|
||||
"app/db",
|
||||
"app/domain",
|
||||
"app/foundation",
|
||||
"app/main.py",
|
||||
"app/monitor",
|
||||
"app/modules",
|
||||
"app/runtime",
|
||||
"app/scheduler",
|
||||
"app/startup",
|
||||
"app/workflow",
|
||||
)
|
||||
|
||||
_CALL_SUFFIXES = frozenset({
|
||||
"create_task", "ensure_future", "to_thread", "run_in_executor", "run_in_threadpool",
|
||||
})
|
||||
_CONSTRUCTOR_NAMES = frozenset({
|
||||
"Thread", "Timer", "ThreadPoolExecutor", "ProcessPoolExecutor", "Process",
|
||||
})
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ConcurrencyFact:
|
||||
"""描述一个原生并发调用及其静态 owner。"""
|
||||
|
||||
path: str
|
||||
line: int
|
||||
target: str
|
||||
owner: str
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""返回跨运行稳定的事实键。"""
|
||||
return f"{self.path}:{self.line}:{self.target}"
|
||||
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
"""转换为基线文件中的稳定记录。"""
|
||||
return {"owner": self.owner, "target": self.target}
|
||||
|
||||
|
||||
def _iter_python_files(root: Path, scan_roots: Iterable[str]) -> Iterable[Path]:
|
||||
"""遍历宿主源码并排除插件副本、SDK 和兼容实现。"""
|
||||
for scan_root in scan_roots:
|
||||
path = root / scan_root
|
||||
if path.is_file():
|
||||
candidates = [path]
|
||||
elif path.is_dir():
|
||||
candidates = sorted(path.rglob("*.py"))
|
||||
else:
|
||||
candidates = []
|
||||
for candidate in candidates:
|
||||
relative = candidate.relative_to(root).as_posix()
|
||||
if any(
|
||||
relative == excluded or relative.startswith(f"{excluded}/")
|
||||
for excluded in ("app/plugins", "app/sdk", "app/runtime/compat")
|
||||
):
|
||||
continue
|
||||
yield candidate
|
||||
|
||||
|
||||
def _target_name(node: ast.Call) -> str | None:
|
||||
"""识别约定的原生并发 API,避免按普通方法名误报。"""
|
||||
if not isinstance(node.func, (ast.Name, ast.Attribute)):
|
||||
return None
|
||||
target = ast.unparse(node.func)
|
||||
terminal = target.rsplit(".", 1)[-1]
|
||||
if terminal in _CALL_SUFFIXES or terminal in _CONSTRUCTOR_NAMES:
|
||||
return terminal
|
||||
return None
|
||||
|
||||
|
||||
class _ConcurrencyVisitor(ast.NodeVisitor):
|
||||
"""收集函数、类和模块级并发调用的词法 owner。"""
|
||||
|
||||
def __init__(self, relative_path: str) -> None:
|
||||
"""初始化文件位置和 owner 栈。"""
|
||||
self._relative_path = relative_path
|
||||
self._owners: list[str] = []
|
||||
self.facts: list[ConcurrencyFact] = []
|
||||
|
||||
@property
|
||||
def _owner(self) -> str:
|
||||
"""返回最具体的可定位 owner。"""
|
||||
return ".".join(self._owners) if self._owners else self._relative_path
|
||||
|
||||
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
||||
"""把类名纳入 owner 路径后继续扫描方法体。"""
|
||||
self._owners.append(node.name)
|
||||
self.generic_visit(node)
|
||||
self._owners.pop()
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
||||
"""把同步函数纳入 owner 路径后扫描其嵌套并发。"""
|
||||
self._owners.append(node.name)
|
||||
self.generic_visit(node)
|
||||
self._owners.pop()
|
||||
|
||||
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
||||
"""把异步函数纳入 owner 路径后扫描其嵌套并发。"""
|
||||
self._owners.append(node.name)
|
||||
self.generic_visit(node)
|
||||
self._owners.pop()
|
||||
|
||||
def visit_Call(self, node: ast.Call) -> None:
|
||||
"""记录已识别原语并继续访问参数中的嵌套调用。"""
|
||||
target = _target_name(node)
|
||||
if target:
|
||||
self.facts.append(
|
||||
ConcurrencyFact(
|
||||
path=self._relative_path,
|
||||
line=node.lineno,
|
||||
target=target,
|
||||
owner=self._owner,
|
||||
)
|
||||
)
|
||||
self.generic_visit(node)
|
||||
|
||||
|
||||
def collect_concurrency(
|
||||
root: Path = PROJECT_ROOT,
|
||||
*,
|
||||
scan_roots: Iterable[str] = SCAN_ROOTS,
|
||||
) -> dict[str, dict[str, str]]:
|
||||
"""收集全部宿主原生并发调用及静态 owner。"""
|
||||
facts: dict[str, dict[str, str]] = {}
|
||||
for path in _iter_python_files(root, scan_roots):
|
||||
relative = path.relative_to(root).as_posix()
|
||||
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
|
||||
visitor = _ConcurrencyVisitor(relative)
|
||||
visitor.visit(tree)
|
||||
for fact in visitor.facts:
|
||||
facts[fact.key] = fact.as_dict()
|
||||
return dict(sorted(facts.items()))
|
||||
|
||||
|
||||
def compare_concurrency(
|
||||
baseline: dict[str, dict[str, str]],
|
||||
current: dict[str, dict[str, str]],
|
||||
) -> list[str]:
|
||||
"""拒绝新增原语、owner 丢失或事实 owner 漂移。"""
|
||||
problems: list[str] = []
|
||||
for key, record in current.items():
|
||||
if not record.get("owner"):
|
||||
problems.append(f"并发事实缺少 owner:{key}")
|
||||
continue
|
||||
previous = baseline.get(key)
|
||||
if previous is None:
|
||||
problems.append(f"新增原生并发事实:{key}")
|
||||
elif previous.get("owner") != record.get("owner"):
|
||||
problems.append(
|
||||
f"并发 owner 漂移:{key}={record.get('owner')}(基线为 {previous.get('owner')})"
|
||||
)
|
||||
return problems
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""执行原生并发事实 ratchet。"""
|
||||
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_concurrency()
|
||||
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_concurrency(baseline, current)
|
||||
if problems:
|
||||
print("\n".join(problems))
|
||||
return 1
|
||||
print("原生并发事实 ratchet 通过")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
79
tests/fixtures/architecture/complexity-v2-baseline.json
vendored
Normal file
79
tests/fixtures/architecture/complexity-v2-baseline.json
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"class": {
|
||||
"app/application/formatting.py:EpisodeFormatRuleHelper": 1280,
|
||||
"app/application/messaging/site.py:SiteInteractionHandler": 579,
|
||||
"app/application/messaging/skill.py:SkillInteractionHandler": 1101,
|
||||
"app/application/messaging/subscribe.py:SubscribeInteractionHandler": 694,
|
||||
"app/application/plugin/install.py:PluginInstallCommand": 739,
|
||||
"app/application/rss.py:RssHelper": 531,
|
||||
"app/application/security/url.py:SecurityUtils": 771,
|
||||
"app/application/torrent/download.py:TorrentHelper": 738,
|
||||
"app/application/transfer/workflow.py:JobManager": 754,
|
||||
"app/chain/_recognition.py:RecognitionMixin": 570,
|
||||
"app/chain/base.py:ChainBase": 1161,
|
||||
"app/chain/download/batch.py:DownloadBatchOwner": 515,
|
||||
"app/chain/download/subtitle.py:DownloadSubtitleOwner": 538,
|
||||
"app/chain/interaction.py:MediaInteractionChain": 1540,
|
||||
"app/chain/mediaserver.py:MediaServerChain": 568,
|
||||
"app/chain/message.py:MessageChain": 1876,
|
||||
"app/chain/recommend.py:RecommendChain": 742,
|
||||
"app/chain/scraping.py:ScrapingChain": 1802,
|
||||
"app/chain/search/media.py:SearchMediaOwner": 522,
|
||||
"app/chain/search/provider.py:SearchProviderOwner": 662,
|
||||
"app/chain/search/subtitle.py:SearchSubtitleOwner": 528,
|
||||
"app/chain/site.py:SiteChain": 1003,
|
||||
"app/chain/subscribe/create.py:SubscribeCreateOwner": 646,
|
||||
"app/chain/subscribe/match.py:SubscribeMatchOwner": 571,
|
||||
"app/chain/subscribe/refresh.py:SubscribeRefreshOwner": 760,
|
||||
"app/chain/torrents.py:TorrentsChain": 792,
|
||||
"app/chain/transfer/plan.py:TransferPlanningOwner": 909,
|
||||
"app/chain/transfer/queue.py:TransferQueueOwner": 1572,
|
||||
"app/chain/transfer/settlement.py:TransferSettlementOwner": 648,
|
||||
"app/chain/transfer/workflow.py:TransferWorkflowOwner": 1061,
|
||||
"app/chain/workflow.py:WorkflowExecutor": 1143
|
||||
},
|
||||
"file": {
|
||||
"app/api/endpoints/system.py": 1438,
|
||||
"app/application/formatting.py": 1523,
|
||||
"app/application/history.py": 1505,
|
||||
"app/application/messaging/agent.py": 2195,
|
||||
"app/application/messaging/message.py": 1314,
|
||||
"app/application/messaging/skill.py": 1300,
|
||||
"app/application/security/url.py": 1079,
|
||||
"app/application/transfer/workflow.py": 1957,
|
||||
"app/chain/base.py": 1201,
|
||||
"app/chain/interaction.py": 1571,
|
||||
"app/chain/message.py": 1984,
|
||||
"app/chain/scraping.py": 2065,
|
||||
"app/chain/site.py": 1187,
|
||||
"app/chain/transfer/queue.py": 1631,
|
||||
"app/chain/transfer/workflow.py": 1106,
|
||||
"app/chain/workflow.py": 1391
|
||||
},
|
||||
"method": {
|
||||
"app/api/endpoints/transfer.py:_execute_manual_transfer": 296,
|
||||
"app/application/formatting.py:EpisodeFormatRuleHelper._auto_recommend": 174,
|
||||
"app/application/messaging/site.py:SiteInteractionHandler._handle_text_interaction": 227,
|
||||
"app/application/messaging/skill.py:SkillInteractionHandler._handle_callback_interaction": 158,
|
||||
"app/application/messaging/skill.py:SkillInteractionHandler._handle_text_interaction": 296,
|
||||
"app/application/messaging/subscribe.py:SubscribeInteractionHandler._handle_text_interaction": 205,
|
||||
"app/application/network.py:NetworkTestService._build_rules": 206,
|
||||
"app/application/rss.py:RssHelper._parse_impl": 213,
|
||||
"app/application/security/cookie.py:CookieHelper._get_site_cookie_ua_impl": 241,
|
||||
"app/chain/download/batch.py:DownloadBatchOwner._execute_batch_download": 478,
|
||||
"app/chain/download/submission.py:DownloadSubmissionOwner._execute_download_single": 176,
|
||||
"app/chain/message.py:MessageChain._handle_ai_message": 202,
|
||||
"app/chain/message.py:MessageChain._handle_message_core": 151,
|
||||
"app/chain/subscribe/match.py:SubscribeMatchOwner._execute_match": 377,
|
||||
"app/chain/transfer/execution.py:TransferExecutionOwner._TransferChain__perform_transfer": 209,
|
||||
"app/chain/transfer/queue.py:TransferQueueOwner._TransferChain__replay_pending": 171,
|
||||
"app/chain/transfer/queue.py:TransferQueueOwner._TransferChain__start_transfer": 180,
|
||||
"app/chain/transfer/request.py:_TransferCandidatePlanner._plan_file_items": 173,
|
||||
"app/chain/transfer/settlement.py:TransferSettlementOwner._TransferChain__default_callback": 261,
|
||||
"app/chain/transfer/workflow.py:TransferWorkflowOwner._build_transfer_tasks": 283,
|
||||
"app/chain/transfer/workflow.py:TransferWorkflowOwner._execute_transfer_tasks": 172,
|
||||
"app/chain/transfer/workflow.py:TransferWorkflowOwner._run_transfer_workflow": 232,
|
||||
"app/scheduler/catalog.py:SchedulerCatalogOwner._initialize_catalog": 336,
|
||||
"app/scheduler/execution.py:SchedulerExecutionOwner.start": 153
|
||||
}
|
||||
}
|
||||
622
tests/fixtures/architecture/concurrency-baseline.json
vendored
Normal file
622
tests/fixtures/architecture/concurrency-baseline.json
vendored
Normal file
@@ -0,0 +1,622 @@
|
||||
{
|
||||
"app/adapters/external/plugin/client.py:1372:to_thread": {
|
||||
"owner": "PluginMarketTransport.async_get_plugin_system_version_check_message",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/adapters/external/server.py:1005:to_thread": {
|
||||
"owner": "MoviePilotServerHelper.sub_reg_async",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/adapters/external/server.py:1019:to_thread": {
|
||||
"owner": "MoviePilotServerHelper.sub_done_async",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/adapters/network/browser.py:461:ThreadPoolExecutor": {
|
||||
"owner": "BrowserSessionHelper._get_session_executor",
|
||||
"target": "ThreadPoolExecutor"
|
||||
},
|
||||
"app/adapters/network/http.py:224:create_task": {
|
||||
"owner": "_get_shared_async_transport",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/adapters/system/fsproxy.py:399:Thread": {
|
||||
"owner": "FileSystemProxy._read_line_windows",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/adapters/system/host.py:1012:Process": {
|
||||
"owner": "SystemUtils.memory_usage",
|
||||
"target": "Process"
|
||||
},
|
||||
"app/adapters/system/host.py:255:Process": {
|
||||
"owner": "SystemUtils._process_tree",
|
||||
"target": "Process"
|
||||
},
|
||||
"app/adapters/system/host.py:329:create_task": {
|
||||
"owner": "SystemUtils.execute_with_subprocess_async",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/adapters/system/host.py:950:Process": {
|
||||
"owner": "SystemUtils.dashboard_system_info",
|
||||
"target": "Process"
|
||||
},
|
||||
"app/adapters/system/plugin/dependency.py:414:to_thread": {
|
||||
"owner": "PluginDependencyInstaller.async_find_missing",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/adapters/system/plugin/health.py:403:create_task": {
|
||||
"owner": "PluginRuntimeHealth.__async_create_runtime_constraints_file",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/adapters/system/plugin/health.py:404:to_thread": {
|
||||
"owner": "PluginRuntimeHealth.__async_create_runtime_constraints_file",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/adapters/system/plugin/health.py:417:to_thread": {
|
||||
"owner": "PluginRuntimeHealth.__async_create_runtime_constraints_file.cleanup_created_file",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/adapters/system/plugin/health.py:419:create_task": {
|
||||
"owner": "PluginRuntimeHealth.__async_create_runtime_constraints_file",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/adapters/system/plugin/package.py:2130:to_thread": {
|
||||
"owner": "PluginPackageManager.__async_install_from_release",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/agent/callback/__init__.py:239:create_task": {
|
||||
"owner": "StreamingHandler._start_streaming",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/callback/__init__.py:271:run_in_threadpool": {
|
||||
"owner": "StreamingHandler._stop_streaming",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/agent/callback/__init__.py:606:run_in_threadpool": {
|
||||
"owner": "StreamingHandler._flush",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/agent/callback/__init__.py:653:run_in_threadpool": {
|
||||
"owner": "StreamingHandler._flush",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/agent/callback/__init__.py:691:run_in_threadpool": {
|
||||
"owner": "StreamingHandler._flush",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/agent/capabilities/adapter.py:117:to_thread": {
|
||||
"owner": "AgentServiceAdapter.materialize",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/agent/lifecycle.py:104:create_task": {
|
||||
"owner": "AgentLifecycleOwner.close",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/lifecycle.py:33:create_task": {
|
||||
"owner": "AgentLifecycleOwner.initialize",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/llm/discovery.py:195:to_thread": {
|
||||
"owner": "_ProviderDiscovery._load_models_dev_from_disk",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/agent/llm/discovery.py:223:to_thread": {
|
||||
"owner": "_ProviderDiscovery._write_models_dev_to_disk",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/agent/llm/discovery.py:684:to_thread": {
|
||||
"owner": "_ProviderDiscovery._list_models_from_bedrock",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/agent/llm/discovery.py:691:to_thread": {
|
||||
"owner": "_ProviderDiscovery._list_models_from_bedrock",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/agent/mcp.py:215:create_task": {
|
||||
"owner": "_StdioMcpSession.__aenter__",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/memory/__init__.py:64:create_task": {
|
||||
"owner": "MemoryManager.initialize",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/middleware/subagents.py:1091:create_task": {
|
||||
"owner": "SubAgentTaskControlMiddleware._run_pipeline",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/middleware/subagents.py:841:create_task": {
|
||||
"owner": "SubAgentTaskControlMiddleware._start_tasks",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/orchestrator.py:1050:run_in_threadpool": {
|
||||
"owner": "MoviePilotAgent._deliver_private_channel_message",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/agent/session.py:374:create_task": {
|
||||
"owner": "AgentSessionOwner.process_message",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/session.py:655:create_task": {
|
||||
"owner": "AgentSessionOwner._stop_current_task_locked",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/session.py:71:run_in_threadpool": {
|
||||
"owner": "_async_start_processing_status",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/agent/session.py:755:create_task": {
|
||||
"owner": "AgentSessionOwner._schedule_deferred_session_cleanup",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/session.py:83:run_in_threadpool": {
|
||||
"owner": "_async_finish_processing_status",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/agent/tools/base.py:199:ThreadPoolExecutor": {
|
||||
"owner": "_submit_blocking_call",
|
||||
"target": "ThreadPoolExecutor"
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py:257:create_task": {
|
||||
"owner": "_TerminalSessionManager.start",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py:302:create_task": {
|
||||
"owner": "_TerminalSessionManager._start_pty_session",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py:303:create_task": {
|
||||
"owner": "_TerminalSessionManager._start_pty_session",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py:326:create_task": {
|
||||
"owner": "_TerminalSessionManager._start_pipe_session",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py:330:create_task": {
|
||||
"owner": "_TerminalSessionManager._start_pipe_session",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py:332:create_task": {
|
||||
"owner": "_TerminalSessionManager._start_pipe_session",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py:372:to_thread": {
|
||||
"owner": "_TerminalSessionManager._wait_pty_process",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py:477:to_thread": {
|
||||
"owner": "_TerminalSessionManager.write",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/agent/tools/impl/execute_command.py:475:create_task": {
|
||||
"owner": "ExecuteCommandTool._run_once",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/tools/impl/execute_command.py:477:create_task": {
|
||||
"owner": "ExecuteCommandTool._run_once",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/agent/tools/impl/execute_command.py:478:create_task": {
|
||||
"owner": "ExecuteCommandTool._run_once",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/api/endpoints/dashboard.py:183:run_in_threadpool": {
|
||||
"owner": "schedule",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/api/endpoints/dashboard.py:214:run_in_threadpool": {
|
||||
"owner": "schedule2",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/api/endpoints/history.py:111:to_thread": {
|
||||
"owner": "_request_batch_durable_retries",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/api/endpoints/history.py:447:to_thread": {
|
||||
"owner": "ai_redo_transfer_history",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/api/endpoints/search.py:218:create_task": {
|
||||
"owner": "_iter_batched_search_events",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/agenttask.py:286:create_task": {
|
||||
"owner": "AgentTaskExecutionService.claim",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/agenttask.py:293:create_task": {
|
||||
"owner": "AgentTaskExecutionService.claim",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/agenttask.py:359:create_task": {
|
||||
"owner": "AgentTaskExecutionService._finalize",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/messaging/agent.py:1023:run_in_threadpool": {
|
||||
"owner": "get_web_agent_upload_dir",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/application/messaging/agent.py:1047:run_in_threadpool": {
|
||||
"owner": "save_web_agent_upload",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/application/messaging/agent.py:1263:run_in_threadpool": {
|
||||
"owner": "prepare_web_agent_audio_attachment_path_async",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/application/messaging/agent.py:1271:run_in_threadpool": {
|
||||
"owner": "prepare_web_agent_audio_attachment_path_async",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/application/messaging/agent.py:1278:run_in_threadpool": {
|
||||
"owner": "prepare_web_agent_audio_attachment_path_async",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/application/messaging/agent.py:1289:run_in_threadpool": {
|
||||
"owner": "prepare_web_agent_audio_attachment_path_async.cleanup_conversion_output",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/application/messaging/agent.py:1311:run_in_threadpool": {
|
||||
"owner": "prepare_web_agent_audio_attachment_path_async",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/application/messaging/agent.py:1417:to_thread": {
|
||||
"owner": "transcribe_web_agent_audio_input",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/application/messaging/agent.py:1772:run_in_threadpool": {
|
||||
"owner": "collect_web_agent_traditional_events",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/application/messaging/agent.py:1802:to_thread": {
|
||||
"owner": "collect_web_agent_traditional_events",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/application/messaging/agent.py:1875:create_task": {
|
||||
"owner": "_build_traditional_web_agent_stream.event_generator",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/messaging/agent.py:715:create_task": {
|
||||
"owner": "WebAgentEventPublisher.__init__",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/messaging/message.py:892:Thread": {
|
||||
"owner": "MessageQueueManager.start",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/application/plugin/catalog.py:195:ThreadPoolExecutor": {
|
||||
"owner": "PluginCatalogService.collect",
|
||||
"target": "ThreadPoolExecutor"
|
||||
},
|
||||
"app/application/plugin/catalog.py:255:create_task": {
|
||||
"owner": "PluginCatalogService.async_collect",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/plugin/catalog.py:260:create_task": {
|
||||
"owner": "PluginCatalogService.async_collect",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/plugin/install.py:661:create_task": {
|
||||
"owner": "PluginInstallCommand.__resolve_journal_created",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/plugin/install.py:696:create_task": {
|
||||
"owner": "PluginInstallCommand.__resolve_commit_outcome",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/plugin/install.py:851:create_task": {
|
||||
"owner": "PluginInstallCommand.__rollback_cancelled",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/plugin/install.py:871:ensure_future": {
|
||||
"owner": "PluginInstallCommand.__await_side_effect",
|
||||
"target": "ensure_future"
|
||||
},
|
||||
"app/application/plugin/inventory.py:116:create_task": {
|
||||
"owner": "PluginCandidateInventoryReader.async_load",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/application/plugin/inventory.py:131:to_thread": {
|
||||
"owner": "PluginCandidateInventoryReader.async_load",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/application/plugin/inventory.py:181:to_thread": {
|
||||
"owner": "PluginCandidateInventoryReader._async_read_market_generation",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/chain/_recognition.py:527:run_in_threadpool": {
|
||||
"owner": "RecognitionMixin._async_run_recognition_steps",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/media/album.py:180:run_in_threadpool": {
|
||||
"owner": "MediaAlbumOwner._async_match_music_album_directory",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/media/album.py:216:run_in_threadpool": {
|
||||
"owner": "MediaAlbumOwner.async_recognize_music_album_directory",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/media/album.py:218:run_in_threadpool": {
|
||||
"owner": "MediaAlbumOwner.async_recognize_music_album_directory",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/media/path.py:381:run_in_threadpool": {
|
||||
"owner": "MediaPathOwner._async_music_album_dir_fallback",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/media/path.py:427:run_in_threadpool": {
|
||||
"owner": "MediaPathOwner.async_recognize_music_by_path",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/search/media.py:734:run_in_threadpool": {
|
||||
"owner": "SearchMediaOwner._run_media_process_async",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/search/media.py:936:run_in_threadpool": {
|
||||
"owner": "SearchMediaOwner.async_process_stream",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/search/music.py:236:run_in_threadpool": {
|
||||
"owner": "SearchMusicOwner._async_process_music",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/search/music.py:282:run_in_threadpool": {
|
||||
"owner": "SearchMusicOwner._async_process_music_stream",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/search/subtitle.py:429:run_in_threadpool": {
|
||||
"owner": "SearchSubtitleOwner._async_search_subtitles_for_media",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/search/subtitle.py:526:run_in_threadpool": {
|
||||
"owner": "SearchSubtitleOwner._async_search_subtitles_for_media_stream",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/search/title.py:248:run_in_threadpool": {
|
||||
"owner": "SearchTitleOwner._run_title_search_async",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/search/title.py:382:run_in_threadpool": {
|
||||
"owner": "SearchTitleOwner.async_search_by_title_stream",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/chain/transfer/queue.py:149:Thread": {
|
||||
"owner": "TransferQueueOwner._TransferChain__init",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/chain/transfer/queue.py:317:to_thread": {
|
||||
"owner": "TransferQueueOwner.close",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/chain/transfer/queue.py:405:Thread": {
|
||||
"owner": "TransferQueueOwner._TransferChain__ensure_recovery_scheduler",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/chain/transfer/queue.py:469:Thread": {
|
||||
"owner": "TransferQueueOwner._TransferChain__start_lease_heartbeat_owner_locked",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/chain/transfer/queue.py:717:Thread": {
|
||||
"owner": "TransferQueueOwner._TransferChain__start_lease_release_owner_locked",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/db/worker.py:82:ThreadPoolExecutor": {
|
||||
"owner": "DatabaseWorker.start",
|
||||
"target": "ThreadPoolExecutor"
|
||||
},
|
||||
"app/main.py:179:Thread": {
|
||||
"owner": "start_tray",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/modules/acoustid/__init__.py:148:run_in_threadpool": {
|
||||
"owner": "AcoustIdModule.async_identify_music_by_fingerprint",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/modules/discord/discord.py:220:create_task": {
|
||||
"owner": "Discord._start.runner",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/modules/discord/discord.py:248:Thread": {
|
||||
"owner": "Discord._start",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/modules/discord/discord.py:731:create_task": {
|
||||
"owner": "Discord._start_typing_task",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/modules/discord/discord.py:924:run_in_threadpool": {
|
||||
"owner": "Discord._send_file",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/modules/feishu/feishu.py:215:Thread": {
|
||||
"owner": "Feishu._start_ws_client",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/modules/indexer/spider/__init__.py:400:run_in_threadpool": {
|
||||
"owner": "SiteSpider.async_get_torrents",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/modules/qqbot/gateway.py:134:Thread": {
|
||||
"owner": "run_gateway.start_heartbeat",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/modules/qqbot/qqbot.py:144:Thread": {
|
||||
"owner": "QQBot._start_gateway",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/modules/telegram/telegram.py:221:Thread": {
|
||||
"owner": "Telegram.__init__",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/modules/telegram/telegram.py:544:Thread": {
|
||||
"owner": "Telegram._start_typing_task",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/modules/thetvdb/__init__.py:291:run_in_threadpool": {
|
||||
"owner": "TheTvDbModule._async_load_auxiliary_candidates",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/modules/wechat/wechatbot.py:122:Thread": {
|
||||
"owner": "WeChatBot._start_gateway",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/modules/wechat/wechatbot.py:124:Thread": {
|
||||
"owner": "WeChatBot._start_gateway",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/modules/wechatclawbot/wechatclawbot.py:1732:Thread": {
|
||||
"owner": "WechatClawBot._start_polling",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/monitor/monitor.py:1111:Thread": {
|
||||
"owner": "Monitor.__request_scheduler_stop",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/monitor/monitor.py:735:Thread": {
|
||||
"owner": "Monitor.__start_compensation",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/monitor/recovery.py:168:Thread": {
|
||||
"owner": "RecoveryExecutor._start",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/monitor/watcher.py:126:Thread": {
|
||||
"owner": "LocalDirectoryWatcher.start",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/runtime/capabilities/runtime.py:1514:to_thread": {
|
||||
"owner": "CapabilityRuntime.shutdown_async",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/runtime/coalesce.py:171:create_task": {
|
||||
"owner": "EventCoalescer._on_flush_timer",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/runtime/debounce.py:136:Timer": {
|
||||
"owner": "Debouncer._call_leading",
|
||||
"target": "Timer"
|
||||
},
|
||||
"app/runtime/debounce.py:161:Timer": {
|
||||
"owner": "Debouncer._call_trailing",
|
||||
"target": "Timer"
|
||||
},
|
||||
"app/runtime/debounce.py:221:create_task": {
|
||||
"owner": "AsyncDebouncer._call_leading",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/runtime/debounce.py:241:create_task": {
|
||||
"owner": "AsyncDebouncer._call_trailing",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/runtime/event/dispatch.py:260:run_in_threadpool": {
|
||||
"owner": "EventDispatcher.invoke_async",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/runtime/event/dispatch.py:292:run_in_threadpool": {
|
||||
"owner": "EventDispatcher.invoke_async_strict",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/runtime/events.py:229:Thread": {
|
||||
"owner": "EventManager.start",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/runtime/events.py:262:to_thread": {
|
||||
"owner": "EventManager.stop_async",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/runtime/execution.py:153:create_task": {
|
||||
"owner": "run_in_threadpool_to_completion",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/runtime/execution.py:153:run_in_threadpool": {
|
||||
"owner": "run_in_threadpool_to_completion",
|
||||
"target": "run_in_threadpool"
|
||||
},
|
||||
"app/runtime/extensions/plugin/monitor.py:55:Thread": {
|
||||
"owner": "PluginMonitorController.start",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/runtime/extensions/plugin/sync.py:108:ThreadPoolExecutor": {
|
||||
"owner": "PluginSyncService.sync",
|
||||
"target": "ThreadPoolExecutor"
|
||||
},
|
||||
"app/runtime/extensions/resource.py:129:to_thread": {
|
||||
"owner": "AsyncManagedResourceAdapter.materialize",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/runtime/gc.py:78:Process": {
|
||||
"owner": "get_memory_usage",
|
||||
"target": "Process"
|
||||
},
|
||||
"app/runtime/log.py:143:Thread": {
|
||||
"owner": "NonBlockingFileHandler.__init__",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/runtime/log.py:303:Thread": {
|
||||
"owner": "NonBlockingFileHandler._close_handlers_bounded",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/runtime/resources.py:133:to_thread": {
|
||||
"owner": "acquire_managed_resource_async",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/runtime/resources.py:189:to_thread": {
|
||||
"owner": "stop_managed_resource_async",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/runtime/state.py:335:Thread": {
|
||||
"owner": "SystemHelper._start_graceful_shutdown_monitor",
|
||||
"target": "Thread"
|
||||
},
|
||||
"app/runtime/state.py:99:Process": {
|
||||
"owner": "SystemHelper._is_local_cli_managed",
|
||||
"target": "Process"
|
||||
},
|
||||
"app/runtime/tasks.py:63:create_task": {
|
||||
"owner": "TaskRegistry.create",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/runtime/tasks.py:80:to_thread": {
|
||||
"owner": "TaskRegistry.create_sync",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/scheduler/bridge.py:186:create_task": {
|
||||
"owner": "SchedulerBridgeOwner._submit_cross_thread.start_on_target_loop",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/scheduler/bridge.py:248:create_task": {
|
||||
"owner": "SchedulerBridgeOwner._submit_to_loop",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/scheduler/catalog.py:187:ThreadPoolExecutor": {
|
||||
"owner": "SchedulerCatalogOwner._initialize_catalog",
|
||||
"target": "ThreadPoolExecutor"
|
||||
},
|
||||
"app/scheduler/execution.py:224:create_task": {
|
||||
"owner": "SchedulerExecutionOwner.start.__start_coro",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/scheduler/execution.py:301:Process": {
|
||||
"owner": "SchedulerExecutionOwner.start",
|
||||
"target": "Process"
|
||||
},
|
||||
"app/scheduler/lifecycle.py:126:to_thread": {
|
||||
"owner": "SchedulerLifecycleOwner.stop_async",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/scheduler/lifecycle.py:29:to_thread": {
|
||||
"owner": "SchedulerLifecycleOwner.on_config_changed",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/startup/composition/subscription.py:93:to_thread": {
|
||||
"owner": "async_rule_group_mutation_scope",
|
||||
"target": "to_thread"
|
||||
},
|
||||
"app/startup/lifecycle/__init__.py:161:create_task": {
|
||||
"owner": "run_shutdown_step",
|
||||
"target": "create_task"
|
||||
},
|
||||
"app/startup/lifecycle/__init__.py:365:create_task": {
|
||||
"owner": "schedule_plugin_settlement",
|
||||
"target": "create_task"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"""复杂度只降不增 ratchet 测试。"""
|
||||
|
||||
from scripts.architecture.complexity import compare_complexity
|
||||
from scripts.architecture.complexity import compare_complexity, compare_complexity_v2
|
||||
|
||||
|
||||
def test_complexity_ratchet_allows_removal_and_reduction() -> None:
|
||||
@@ -39,3 +39,18 @@ def test_complexity_ratchet_rejects_growth_and_new_oversize() -> None:
|
||||
|
||||
assert any("既有超限增长" in problem for problem in problems)
|
||||
assert any("新增超限" in problem for problem in problems)
|
||||
|
||||
|
||||
def test_complexity_v2_ratchet_checks_categories_outside_public_rules() -> None:
|
||||
"""v2 基线必须实际比较私有方法、类和文件类别。"""
|
||||
baseline = {"method": {"app/scheduler/a.py:Runner.run": 160}, "class": {}, "file": {}}
|
||||
current = {
|
||||
"method": {"app/scheduler/a.py:Runner.run": 161},
|
||||
"class": {"app/scheduler/a.py:Runner": 600},
|
||||
"file": {"app/scheduler/a.py": 1200},
|
||||
}
|
||||
|
||||
problems = compare_complexity_v2(baseline, current)
|
||||
assert any("method: 既有超限增长" in problem for problem in problems)
|
||||
assert any("class: 新增超限" in problem for problem in problems)
|
||||
assert any("file: 新增超限" in problem for problem in problems)
|
||||
|
||||
62
tests/test_concurrency_gate.py
Normal file
62
tests/test_concurrency_gate.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""原生并发事实门禁回归测试。"""
|
||||
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.architecture.concurrency import collect_concurrency, compare_concurrency
|
||||
|
||||
|
||||
def test_concurrency_gate_tracks_nested_owners_and_known_primitives(tmp_path: Path) -> None:
|
||||
"""嵌套函数、类方法和常见原生并发入口都必须被枚举。"""
|
||||
source_path = tmp_path / "app/scheduler/sample.py"
|
||||
source_path.parent.mkdir(parents=True)
|
||||
source_path.write_text(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
import asyncio
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
class Runner:
|
||||
async def run(self):
|
||||
asyncio.create_task(self.child())
|
||||
threading.Timer(1, self.child).start()
|
||||
with ThreadPoolExecutor() as executor:
|
||||
executor.submit(self.child)
|
||||
|
||||
async def child(self):
|
||||
await asyncio.to_thread(lambda: None)
|
||||
"""
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
facts = collect_concurrency(tmp_path, scan_roots=("app/scheduler",))
|
||||
assert facts["app/scheduler/sample.py:8:create_task"]["owner"] == "Runner.run"
|
||||
assert facts["app/scheduler/sample.py:9:Timer"]["owner"] == "Runner.run"
|
||||
assert facts["app/scheduler/sample.py:10:ThreadPoolExecutor"]["owner"] == "Runner.run"
|
||||
assert facts["app/scheduler/sample.py:14:to_thread"]["owner"] == "Runner.child"
|
||||
|
||||
|
||||
def test_concurrency_ratchet_rejects_new_facts_and_owner_drift() -> None:
|
||||
"""新增并发调用和 owner 漂移必须阻断 ratchet。"""
|
||||
baseline = {
|
||||
"app/runtime/tasks.py:10:create_task": {
|
||||
"owner": "Runtime.run",
|
||||
"target": "create_task",
|
||||
},
|
||||
}
|
||||
current = {
|
||||
"app/runtime/tasks.py:10:create_task": {
|
||||
"owner": "Runtime.changed",
|
||||
"target": "create_task",
|
||||
},
|
||||
"app/api/system.py:20:Thread": {
|
||||
"owner": "system",
|
||||
"target": "Thread",
|
||||
},
|
||||
}
|
||||
|
||||
problems = compare_concurrency(baseline, current)
|
||||
assert any("owner 漂移" in problem for problem in problems)
|
||||
assert any("新增原生并发事实" in problem for problem in problems)
|
||||
Reference in New Issue
Block a user