blob: 2e50627cab82f029eb0628fcf2947bc8c176857a [file] [log] [blame]
Devin Moorebd13e632023-05-15 18:09:23 +00001#!/usr/bin/env python3
2
3"""Tool to find static libraries that maybe should be shared libraries and shared libraries that maybe should be static libraries.
4
5This tool only looks at the module-info.json for the current target.
6
7Example of "class" types for each of the modules in module-info.json
8 "EXECUTABLES": 2307,
9 "ETC": 9094,
10 "NATIVE_TESTS": 10461,
11 "APPS": 2885,
12 "JAVA_LIBRARIES": 5205,
13 "EXECUTABLES/JAVA_LIBRARIES": 119,
14 "FAKE": 553,
15 "SHARED_LIBRARIES/STATIC_LIBRARIES": 7591,
16 "STATIC_LIBRARIES": 11535,
17 "SHARED_LIBRARIES": 10852,
18 "HEADER_LIBRARIES": 1897,
19 "DYLIB_LIBRARIES": 1262,
20 "RLIB_LIBRARIES": 3413,
21 "ROBOLECTRIC": 39,
22 "PACKAGING": 5,
23 "PROC_MACRO_LIBRARIES": 36,
24 "RENDERSCRIPT_BITCODE": 17,
25 "DYLIB_LIBRARIES/RLIB_LIBRARIES": 8,
26 "ETC/FAKE": 1
27
28None of the "SHARED_LIBRARIES/STATIC_LIBRARIES" are double counted in the
29modules with one class
30RLIB/
31
32All of these classes have shared_libs and/or static_libs
33 "EXECUTABLES",
34 "SHARED_LIBRARIES",
35 "STATIC_LIBRARIES",
36 "SHARED_LIBRARIES/STATIC_LIBRARIES", # cc_library
37 "HEADER_LIBRARIES",
38 "NATIVE_TESTS", # test modules
39 "DYLIB_LIBRARIES", # rust
40 "RLIB_LIBRARIES", # rust
41 "ETC", # rust_bindgen
42"""
43
44from collections import defaultdict
45
46import json, os, argparse
47
48ANDROID_PRODUCT_OUT = os.environ.get("ANDROID_PRODUCT_OUT")
49# If a shared library is used less than MAX_SHARED_INCLUSIONS times in a target,
50# then it will likely save memory by changing it to a static library
51# This move will also use less storage
52MAX_SHARED_INCLUSIONS = 2
53# If a static library is used more than MAX_STATIC_INCLUSIONS times in a target,
54# then it will likely save memory by changing it to a shared library
55# This move will also likely use less storage
56MIN_STATIC_INCLUSIONS = 3
57
58
59def parse_args():
60 parser = argparse.ArgumentParser(
61 description=(
62 "Parse module-info.jso and display information about static and"
63 " shared library dependencies."
64 )
65 )
66 parser.add_argument(
67 "--module", dest="module", help="Print the info for the module."
68 )
69 parser.add_argument(
70 "--shared",
71 dest="print_shared",
72 action=argparse.BooleanOptionalAction,
73 help=(
74 "Print the list of libraries that are shared_libs for fewer than {}"
75 " modules.".format(MAX_SHARED_INCLUSIONS)
76 ),
77 )
78 parser.add_argument(
79 "--static",
80 dest="print_static",
81 action=argparse.BooleanOptionalAction,
82 help=(
83 "Print the list of libraries that are static_libs for more than {}"
84 " modules.".format(MIN_STATIC_INCLUSIONS)
85 ),
86 )
87 parser.add_argument(
88 "--recursive",
89 dest="recursive",
90 action=argparse.BooleanOptionalAction,
91 default=True,
92 help=(
93 "Gather all dependencies of EXECUTABLES recursvily before calculating"
94 " the stats. This eliminates duplicates from multiple libraries"
95 " including the same dependencies in a single binary."
96 ),
97 )
98 parser.add_argument(
99 "--both",
100 dest="both",
101 action=argparse.BooleanOptionalAction,
102 default=False,
103 help=(
104 "Print a list of libraries that are including libraries as both"
105 " static and shared"
106 ),
107 )
108 return parser.parse_args()
109
110
111class TransitiveHelper:
112
113 def __init__(self):
114 # keep a list of already expanded libraries so we don't end up in a cycle
115 self.visited = defaultdict(lambda: defaultdict(set))
116
117 # module is an object from the module-info dictionary
118 # module_info is the dictionary from module-info.json
119 # modify the module's shared_libs and static_libs with all of the transient
120 # dependencies required from all of the explicit dependencies
121 def flattenDeps(self, module, module_info):
Devin Moore621b02a2024-01-24 19:12:52 +0000122 libs_snapshot = dict(shared_libs = set(module.get("shared_libs",{})), static_libs = set(module.get("static_libs",{})))
Devin Moorebd13e632023-05-15 18:09:23 +0000123
124 for lib_class in ["shared_libs", "static_libs"]:
125 for lib in libs_snapshot[lib_class]:
Devin Moore621b02a2024-01-24 19:12:52 +0000126 if not lib or lib not in module_info or lib_class not in module:
Devin Moorebd13e632023-05-15 18:09:23 +0000127 continue
128 if lib in self.visited:
129 module[lib_class].update(self.visited[lib][lib_class])
130 else:
131 res = self.flattenDeps(module_info[lib], module_info)
Devin Moore621b02a2024-01-24 19:12:52 +0000132 module[lib_class].update(res.get(lib_class, {}))
133 self.visited[lib][lib_class].update(res.get(lib_class, {}))
Devin Moorebd13e632023-05-15 18:09:23 +0000134
135 return module
136
137def main():
138 module_info = json.load(open(ANDROID_PRODUCT_OUT + "/module-info.json"))
Devin Moorebd13e632023-05-15 18:09:23 +0000139
140 args = parse_args()
141
142 if args.module:
143 if args.module not in module_info:
144 print("Module {} does not exist".format(args.module))
145 exit(1)
146
Devin Moore621b02a2024-01-24 19:12:52 +0000147 # turn all of the static_libs and shared_libs lists into sets to make them
148 # easier to update
149 for _, module in module_info.items():
150 module["shared_libs"] = set(module.get("shared_libs", {}))
151 module["static_libs"] = set(module.get("static_libs", {}))
152
Devin Moorebd13e632023-05-15 18:09:23 +0000153 includedStatically = defaultdict(set)
154 includedSharedly = defaultdict(set)
155 includedBothly = defaultdict(set)
156 transitive = TransitiveHelper()
157 for name, module in module_info.items():
158 if args.recursive:
159 # in this recursive mode we only want to see what is included by the executables
160 if "EXECUTABLES" not in module["class"]:
161 continue
162 module = transitive.flattenDeps(module, module_info)
163 # filter out fuzzers by their dependency on clang
Devin Moore621b02a2024-01-24 19:12:52 +0000164 if "static_libs" in module:
165 if "libclang_rt.fuzzer" in module["static_libs"]:
166 continue
Devin Moorebd13e632023-05-15 18:09:23 +0000167 else:
168 if "NATIVE_TESTS" in module["class"]:
169 # We don't care about how tests are including libraries
170 continue
171
172 # count all of the shared and static libs included in this module
Devin Moore621b02a2024-01-24 19:12:52 +0000173 if "shared_libs" in module:
174 for lib in module["shared_libs"]:
175 includedSharedly[lib].add(name)
176 if "static_libs" in module:
177 for lib in module["static_libs"]:
178 includedStatically[lib].add(name)
Devin Moorebd13e632023-05-15 18:09:23 +0000179
Devin Moore621b02a2024-01-24 19:12:52 +0000180 if "shared_libs" in module and "static_libs" in module:
181 intersection = set(module["shared_libs"]).intersection(
182 module["static_libs"]
183 )
184 if intersection:
185 includedBothly[name] = intersection
Devin Moorebd13e632023-05-15 18:09:23 +0000186
187 if args.print_shared:
188 print(
189 "Shared libraries that are included by fewer than {} modules on a"
190 " device:".format(MAX_SHARED_INCLUSIONS)
191 )
192 for name, libs in includedSharedly.items():
193 if len(libs) < MAX_SHARED_INCLUSIONS:
194 print("{}: {} included by: {}".format(name, len(libs), libs))
195
196 if args.print_static:
197 print(
198 "Libraries that are included statically by more than {} modules on a"
199 " device:".format(MIN_STATIC_INCLUSIONS)
200 )
201 for name, libs in includedStatically.items():
202 if len(libs) > MIN_STATIC_INCLUSIONS:
203 print("{}: {} included by: {}".format(name, len(libs), libs))
204
205 if args.both:
206 allIncludedBothly = set()
207 for name, libs in includedBothly.items():
208 allIncludedBothly.update(libs)
209
210 print(
211 "List of libraries used both statically and shared in the same"
212 " processes:\n {}\n\n".format("\n".join(sorted(allIncludedBothly)))
213 )
214 print(
215 "List of libraries used both statically and shared in any processes:\n {}".format("\n".join(sorted(includedStatically.keys() & includedSharedly.keys()))))
216
217 if args.module:
218 print(json.dumps(module_info[args.module], default=list, indent=2))
219 print(
220 "{} is included in shared_libs {} times by these modules: {}".format(
221 args.module, len(includedSharedly[args.module]),
222 includedSharedly[args.module]
223 )
224 )
225 print(
226 "{} is included in static_libs {} times by these modules: {}".format(
227 args.module, len(includedStatically[args.module]),
228 includedStatically[args.module]
229 )
230 )
231 print("Shared libs included by this module that are used in fewer than {} processes:\n{}".format(
232 MAX_SHARED_INCLUSIONS, [x for x in module_info[args.module]["shared_libs"] if len(includedSharedly[x]) < MAX_SHARED_INCLUSIONS]))
233
234
235
236if __name__ == "__main__":
237 main()