The SDL pump did the translation itself, so it had to know about keyboard, joystick, touch, window, graphics, audio and ui: eight upward dependencies from one of the engine's lowest modules, and the source of every remaining dependency cycle. Each module now registers an IPlatformEventSink for the events it owns and the pump only dispatches. Key and text events move to keyboard/sdl, joystick and game-controller events (including the hotplug device table) to joystick/sdl, finger events to touch/sdl, resize and the mobile background/foreground surface watch to window/sdl, the ImGui observer to ui, and Audio::pump to an end-of-pump hook in audio. The pump keeps only quit and window-close, which are about the event loop rather than any one module. Sinks run in priority order -- observer, then surface, then input -- so ImGui still sees events first and the window is resized before anything reads its size. The mobile surface teardown keeps using SDL_AddEventWatch rather than the poll path, since by the time SDL_PollEvent returns the native surface is gone; the watch is now installed by the window module on its first pump. Also removes exceptionIfInRenderPass, whose body has been commented out since it was written and whose only remaining effect was a graphics include. The engine dependency graph now has zero cycles, down from 26, and the back-edge whitelist is down to 2 (window -> graphics, window -> image). Layering is also right way up: event sits at L0 with the input modules above it at L1/L2. Verified: unit tests for event/input/ui/audio/thread pass, and the example smoke suite gives the same 19 pass / 9 fail as the pre-change baseline (those 9 fail on main too, from script API drift in the examples themselves). Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>
179 lines
6.8 KiB
Python
Executable File
179 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Cross-module dependency report for src/modules.
|
|
|
|
Scans #include directives and reports the module dependency graph, its
|
|
topological layering and any dependency cycles. See
|
|
docs/dev/模块编排与裁剪架构.md for how the output is interpreted.
|
|
|
|
python3 scripts/module_depgraph.py # graph + layers
|
|
python3 scripts/module_depgraph.py --cycles # cycles only
|
|
python3 scripts/module_depgraph.py --check # exit 1 on unlisted back-edge
|
|
|
|
Only sub-path includes ("map/Map.h") count as a cross-module dependency;
|
|
a bare <map> / <thread> / <filesystem> is a standard library header.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
MODULES_DIR = os.path.join(REPO, "src", "modules")
|
|
|
|
INCLUDE_RE = re.compile(r'^\s*#\s*include\s*[<"]([^">]+)[">]', re.M)
|
|
SOURCE_EXT = (".cpp", ".cc", ".h", ".hpp")
|
|
|
|
# Upward edges that break the layering. Each entry is a known debt item; --check
|
|
# fails on anything outside this list so no new back-edge slips in, and reports
|
|
# entries that are no longer needed so the list shrinks as debt is paid off.
|
|
KNOWN_BACK_EDGES = {
|
|
# window/Window.cpp, window/sdl/Window.cpp: the window owns the Graphics
|
|
# pointer and decodes its own icon image.
|
|
("window", "graphics"),
|
|
("window", "image"),
|
|
}
|
|
|
|
|
|
def scan():
|
|
"""Return (modules, edges, header_edges).
|
|
|
|
edges[a][b] is the set of b's headers included anywhere in a;
|
|
header_edges[a][b] narrows that to includes in a's own headers, which
|
|
leak into a's public API and are harder to decouple.
|
|
"""
|
|
modules = sorted(
|
|
d for d in os.listdir(MODULES_DIR) if os.path.isdir(os.path.join(MODULES_DIR, d))
|
|
)
|
|
known = set(modules)
|
|
edges = defaultdict(lambda: defaultdict(set))
|
|
header_edges = defaultdict(lambda: defaultdict(set))
|
|
|
|
for module in modules:
|
|
for dirpath, _, files in os.walk(os.path.join(MODULES_DIR, module)):
|
|
for name in files:
|
|
if not name.endswith(SOURCE_EXT):
|
|
continue
|
|
path = os.path.join(dirpath, name)
|
|
with open(path, encoding="utf-8", errors="ignore") as handle:
|
|
text = handle.read()
|
|
for include in INCLUDE_RE.findall(text):
|
|
head, _, _ = include.partition("/")
|
|
if not _:
|
|
continue
|
|
if head not in known or head == module:
|
|
continue
|
|
edges[module][head].add(include)
|
|
if name.endswith((".h", ".hpp")):
|
|
header_edges[module][head].add(include)
|
|
return modules, edges, header_edges
|
|
|
|
|
|
def layers(modules, edges, ignore):
|
|
"""Longest-path depth per module once `ignore` edges are dropped."""
|
|
graph = {a: {b for b in deps if (a, b) not in ignore} for a, deps in edges.items()}
|
|
depth = {}
|
|
|
|
def visit(node, stack):
|
|
if node in depth:
|
|
return depth[node]
|
|
if node in stack:
|
|
return 0
|
|
depth[node] = 0
|
|
depth[node] = max((visit(d, stack | {node}) + 1 for d in graph.get(node, ())), default=0)
|
|
return depth[node]
|
|
|
|
for module in modules:
|
|
visit(module, set())
|
|
return depth
|
|
|
|
|
|
def cycles(edges, ignore=frozenset()):
|
|
graph = {a: {b for b in deps if (a, b) not in ignore} for a, deps in edges.items()}
|
|
found = []
|
|
|
|
def walk(start, node, path, seen):
|
|
for nxt in sorted(graph.get(node, ())):
|
|
if nxt == start and len(path) > 1:
|
|
found.append(list(path))
|
|
# Only extend through nodes ordered after `start` so each cycle is
|
|
# reported once, from its alphabetically first member.
|
|
elif nxt not in seen and nxt > start:
|
|
walk(start, nxt, path + [nxt], seen | {nxt})
|
|
|
|
for start in sorted(graph):
|
|
walk(start, start, [start], {start})
|
|
return found
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--cycles", action="store_true", help="report dependency cycles only")
|
|
parser.add_argument("--check", action="store_true",
|
|
help="fail when a back-edge outside KNOWN_BACK_EDGES appears")
|
|
args = parser.parse_args()
|
|
|
|
modules, edges, header_edges = scan()
|
|
|
|
if args.cycles:
|
|
found = cycles(edges)
|
|
print(f"dependency cycles: {len(found)}")
|
|
for cycle in sorted(found, key=len):
|
|
print(" " + " -> ".join(cycle) + " -> " + cycle[0])
|
|
remaining = cycles(edges, KNOWN_BACK_EDGES)
|
|
print(f"\nafter removing {len(KNOWN_BACK_EDGES)} known back-edges: {len(remaining)}")
|
|
return 0
|
|
|
|
depth = layers(modules, edges, KNOWN_BACK_EDGES)
|
|
|
|
if args.check:
|
|
actual = {(a, b) for a, deps in edges.items() for b in deps}
|
|
# A back-edge points from a lower layer up to a higher one.
|
|
offenders = sorted(
|
|
(a, b) for (a, b) in actual
|
|
if depth[a] <= depth[b] and (a, b) not in KNOWN_BACK_EDGES
|
|
)
|
|
stale = sorted(KNOWN_BACK_EDGES - actual)
|
|
for a, b in offenders:
|
|
print(f"error: layering violation {a}(L{depth[a]}) -> {b}(L{depth[b]})", file=sys.stderr)
|
|
for a, b in stale:
|
|
print(f"note: {a} -> {b} is fixed; drop it from KNOWN_BACK_EDGES", file=sys.stderr)
|
|
if offenders:
|
|
return 1
|
|
print(f"ok: no new back-edges ({len(KNOWN_BACK_EDGES)} known, {len(stale)} now stale)")
|
|
return 0
|
|
|
|
by_layer = defaultdict(list)
|
|
for module in modules:
|
|
by_layer[depth[module]].append(module)
|
|
print("=== layers (back-edges removed) ===")
|
|
for layer in sorted(by_layer):
|
|
print(f"L{layer} ({len(by_layer[layer])}): " + ", ".join(sorted(by_layer[layer])))
|
|
|
|
print("\n=== dependencies (* = leaks into the module's own headers) ===")
|
|
for module in sorted(modules, key=lambda m: (-len(edges[m]), m)):
|
|
if not edges[module]:
|
|
continue
|
|
deps = [b + ("*" if b in header_edges[module] else "") for b in sorted(edges[module])]
|
|
print(f"{module:16s} L{depth[module]} ({len(deps)}): " + ", ".join(deps))
|
|
|
|
reverse = defaultdict(set)
|
|
for a, deps in edges.items():
|
|
for b in deps:
|
|
reverse[b].add(a)
|
|
print("\n=== dependents ===")
|
|
for module in sorted(modules, key=lambda m: (-len(reverse[m]), m)):
|
|
if reverse[module]:
|
|
print(f"{module:16s} ({len(reverse[module])}): " + ", ".join(sorted(reverse[module])))
|
|
|
|
isolated = [m for m in modules if not edges[m] and not reverse[m]]
|
|
print("\n=== isolated (trimmable with no graph impact) ===")
|
|
print(", ".join(isolated) if isolated else "(none)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|