Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
symbol_coupling.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Module coupling measured at the SYMBOL level, from the linker's point of view.
3
4tools/include_graph.py measures what a file *reads* -- textual `#include` edges. This
5measures what actually *links*: for every compiled object, which symbols it defines and
6which it leaves undefined, resolved against the module that defines them. The result is
7the real API surface between subsystems.
8
9Why it is worth having both: an include edge can be an accident (a header pulled in for
10one typedef), and removing it changes nothing about the program. A symbol edge is a call
11that exists at runtime. When `include_graph.py` reports `common/ -> control/ : 104`, this
12tool answers the question that actually matters for modularisation -- WHICH functions,
13so the inversion becomes a work list rather than a number.
14
15Reads .o files from the build directory, so it needs a completed build and reflects the
16configuration that produced it (a `nofeatures` build will show fewer edges).
17
18Usage:
19 python3 tools/symbol_coupling.py # module x module summary
20 python3 tools/symbol_coupling.py --edge common develop # the symbols behind one edge
21 python3 tools/symbol_coupling.py --inversions # only upward edges (layer breaks)
22 python3 tools/symbol_coupling.py --build build-nofeatures
23"""
24import collections
25import os
26import re
27import subprocess
28import sys
29
30# Same layer order as tools/include_graph.py -- keep them in sync.
31LAYER = {'external': 0, 'win': 0, 'system': 0, 'common': 1, 'math': 1, 'colorprofiles': 1, 'pixel': 2, 'control': 3,
32 'gui': 4, 'widgets': 4, # widgets/ = reusable GTK widgets, no app state 'develop': 5,
33 'iop': 6, 'imageio': 6, 'libs': 7, 'views': 7, 'chart': 7,
34 'apps': 10, # executables link the orchestrator, so they sit ABOVE it 'app': 9}
35
36OBJ_RE = re.compile(r'\.dir/(.*)\.(?:c|cc|cpp)\.o$')
37
38
39def module_of(obj_path):
40 """The subsystem an object belongs to, from its path inside the build tree."""
41 m = OBJ_RE.search(obj_path)
42 rel = m.group(1) if m else obj_path
43 parts = [p for p in rel.split('/') if p not in ('.', '..')]
44 for p in parts:
45 if p in LAYER:
46 return p
47 # iop modules build as their own target, so the subsystem is not in the path
48 return 'iop' if '/iop/' in obj_path or obj_path.startswith('src/iop') else parts[0]
49
50
51def nm(obj, args):
52 r = subprocess.run(['nm', '-g'] + args + [obj], capture_output=True, text=True)
53 return {ln.split()[-1] for ln in r.stdout.splitlines() if ln.strip()}
54
55
56def collect(build):
57 objs = []
58 for root, _, names in os.walk(build):
59 for n in names:
60 if n.endswith('.o'):
61 objs.append(os.path.join(root, n))
62 if not objs:
63 print('no object files under %s -- build first' % build, file=sys.stderr)
64 raise SystemExit(2)
65
66 owner, undef = {}, {}
67 for o in objs:
68 mod = module_of(o)
69 for s in nm(o, ['--defined-only']):
70 owner.setdefault(s, mod)
71 undef[o] = (mod, nm(o, ['-u']))
72 return owner, undef, len(objs)
73
74
75def edges(owner, undef):
76 per_edge = collections.defaultdict(set)
77 for _, (src, syms) in undef.items():
78 for s in syms:
79 dst = owner.get(s)
80 if dst and dst != src:
81 per_edge[(src, dst)].add(s)
82 return per_edge
83
84
85def main():
86 build = 'build'
87 if '--build' in sys.argv:
88 build = sys.argv[sys.argv.index('--build') + 1]
89 owner, undef, n = collect(build)
90 per_edge = edges(owner, undef)
91 print('%d objects, %d exported symbols, %d cross-module edges\n' % (n, len(owner), len(per_edge)))
92
93 if '--edge' in sys.argv:
94 i = sys.argv.index('--edge')
95 a, b = sys.argv[i + 1], sys.argv[i + 2]
96 syms = sorted(per_edge.get((a, b), ()))
97 print('%s -> %s : %d symbols' % (a, b, len(syms)))
98 for s in syms:
99 print(' ', s)
100 return 0
101
102 rows = []
103 for (a, b), syms in per_edge.items():
104 la, lb = LAYER.get(a), LAYER.get(b)
105 inverted = la is not None and lb is not None and lb > la
106 if '--inversions' in sys.argv and not inverted:
107 continue
108 rows.append((len(syms), a, b, inverted))
109 rows.sort(reverse=True)
110
111 print('%-14s %-14s %8s' % ('from', 'to', 'symbols'))
112 for cnt, a, b, inverted in rows[:30]:
113 print('%-14s %-14s %8d %s' % (a, b, cnt, ' <-- LAYER INVERSION' if inverted else ''))
114
115 inv = [r for r in rows if r[3]]
116 if inv and '--inversions' not in sys.argv:
117 print('\n%d inverted edges carrying %d symbols in total.' % (len(inv), sum(r[0] for r in inv)))
118 print('Run --edge <from> <to> to list the functions behind one of them.')
119 return 0
120
121
122if __name__ == '__main__':
123 sys.exit(main())
edges(owner, undef)