blob: 7511b36ab2f47d8f0f0b65d52a9bb133ed1238f7 [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):
122 libs_snapshot = dict(shared_libs = set(module["shared_libs"]), static_libs = set(module["static_libs"]))
123
124 for lib_class in ["shared_libs", "static_libs"]:
125 for lib in libs_snapshot[lib_class]:
126 if not lib or lib not in module_info:
127 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)
132 module[lib_class].update(res[lib_class])
133 self.visited[lib][lib_class].update(res[lib_class])
134
135 return module
136
137def main():
138 module_info = json.load(open(ANDROID_PRODUCT_OUT + "/module-info.json"))
139 # turn all of the static_libs and shared_libs lists into sets to make them
140 # easier to update
141 for _, module in module_info.items():
142 module["shared_libs"] = set(module["shared_libs"])
143 module["static_libs"] = set(module["static_libs"])
144
145 args = parse_args()
146
147 if args.module:
148 if args.module not in module_info:
149 print("Module {} does not exist".format(args.module))
150 exit(1)
151
152 includedStatically = defaultdict(set)
153 includedSharedly = defaultdict(set)
154 includedBothly = defaultdict(set)
155 transitive = TransitiveHelper()
156 for name, module in module_info.items():
157 if args.recursive:
158 # in this recursive mode we only want to see what is included by the executables
159 if "EXECUTABLES" not in module["class"]:
160 continue
161 module = transitive.flattenDeps(module, module_info)
162 # filter out fuzzers by their dependency on clang
163 if "libclang_rt.fuzzer" in module["static_libs"]:
164 continue
165 else:
166 if "NATIVE_TESTS" in module["class"]:
167 # We don't care about how tests are including libraries
168 continue
169
170 # count all of the shared and static libs included in this module
171 for lib in module["shared_libs"]:
172 includedSharedly[lib].add(name)
173 for lib in module["static_libs"]:
174 includedStatically[lib].add(name)
175
176 intersection = set(module["shared_libs"]).intersection(
177 module["static_libs"]
178 )
179 if intersection:
180 includedBothly[name] = intersection
181
182 if args.print_shared:
183 print(
184 "Shared libraries that are included by fewer than {} modules on a"
185 " device:".format(MAX_SHARED_INCLUSIONS)
186 )
187 for name, libs in includedSharedly.items():
188 if len(libs) < MAX_SHARED_INCLUSIONS:
189 print("{}: {} included by: {}".format(name, len(libs), libs))
190
191 if args.print_static:
192 print(
193 "Libraries that are included statically by more than {} modules on a"
194 " device:".format(MIN_STATIC_INCLUSIONS)
195 )
196 for name, libs in includedStatically.items():
197 if len(libs) > MIN_STATIC_INCLUSIONS:
198 print("{}: {} included by: {}".format(name, len(libs), libs))
199
200 if args.both:
201 allIncludedBothly = set()
202 for name, libs in includedBothly.items():
203 allIncludedBothly.update(libs)
204
205 print(
206 "List of libraries used both statically and shared in the same"
207 " processes:\n {}\n\n".format("\n".join(sorted(allIncludedBothly)))
208 )
209 print(
210 "List of libraries used both statically and shared in any processes:\n {}".format("\n".join(sorted(includedStatically.keys() & includedSharedly.keys()))))
211
212 if args.module:
213 print(json.dumps(module_info[args.module], default=list, indent=2))
214 print(
215 "{} is included in shared_libs {} times by these modules: {}".format(
216 args.module, len(includedSharedly[args.module]),
217 includedSharedly[args.module]
218 )
219 )
220 print(
221 "{} is included in static_libs {} times by these modules: {}".format(
222 args.module, len(includedStatically[args.module]),
223 includedStatically[args.module]
224 )
225 )
226 print("Shared libs included by this module that are used in fewer than {} processes:\n{}".format(
227 MAX_SHARED_INCLUSIONS, [x for x in module_info[args.module]["shared_libs"] if len(includedSharedly[x]) < MAX_SHARED_INCLUSIONS]))
228
229
230
231if __name__ == "__main__":
232 main()