Devin Moore | bd13e63 | 2023-05-15 18:09:23 +0000 | [diff] [blame^] | 1 | #!/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 | |
| 5 | This tool only looks at the module-info.json for the current target. |
| 6 | |
| 7 | Example 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 | |
| 28 | None of the "SHARED_LIBRARIES/STATIC_LIBRARIES" are double counted in the |
| 29 | modules with one class |
| 30 | RLIB/ |
| 31 | |
| 32 | All 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 | |
| 44 | from collections import defaultdict |
| 45 | |
| 46 | import json, os, argparse |
| 47 | |
| 48 | ANDROID_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 |
| 52 | MAX_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 |
| 56 | MIN_STATIC_INCLUSIONS = 3 |
| 57 | |
| 58 | |
| 59 | def 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 | |
| 111 | class 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 | |
| 137 | def 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 | |
| 231 | if __name__ == "__main__": |
| 232 | main() |