Files
filament/tools/zbloat/zbloat.py
Philip Rideout 101a59138c Update the zbloat tool
The tool now works with Python 3 and handles binaries that have multiple
embedded material archives.
2022-03-21 16:13:49 -07:00

233 lines
7.8 KiB
Python
Executable File

#!/usr/bin/env python3
#
# Copyright 2020 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
For usage help, see README.md
"""
import argparse
import os
import struct
import tempfile
import zipfile
import json
from pathlib import Path
TEMPDIR = tempfile.gettempdir()
SCRIPTDIR = Path(os.path.realpath(__file__)).parent
DEBUG = False
RESGEN_MAGIC = b'__RESGEN__'
def format_bytes(bytes, prec=1):
"""Pretty-print a number of bytes."""
if bytes > 1e6:
bytes = bytes / 1.0e6
return f'%.{prec}fm' % bytes
if bytes > 1e3:
bytes = bytes / 1.0e3
return f'%.{prec}fk' % bytes
return str(bytes)
def get_hint_marker(filename):
HINTS = ['renderer', 'libfilament-jni']
hints = [hint in filename for hint in HINTS]
return '*' if True in hints else ' '
def choose_from_dir(path: Path):
paths = list(path.glob('**/*.so'))
has_dso = False
for index, dso in enumerate(paths):
has_dso = True
size = dso.stat().st_size / 1000
filename = str(dso)
marker = get_hint_marker(filename)
print(f"{index:3} {marker} {size:8.0f} kB {filename}")
has_dso or quit("No .so file found in the specified path")
val = input("Which dso should be analyzed? Type a number. ")
return str(paths[int(val)])
def choose_from_zip(path: Path):
z = zipfile.ZipFile(path)
paths = []
for info in z.infolist():
if info.filename.endswith('.so'):
paths.append((info.filename, info.compress_size, info.file_size))
has_dso = False
for index, dso in enumerate(paths):
has_dso = True
filename, csize, usize = dso
csize = csize / 1000
usize = usize / 1000
marker = get_hint_marker(filename)
print(f"{index:3} {marker} {usize:8.0f} kB ({csize:8.0f} kB) {filename}")
has_dso or quit("No .so file found in the specified path")
val = input("Which dso should be analyzed? Type a number. ")
chosen_filename = paths[int(val)][0]
result_path = z.extract(chosen_filename, TEMPDIR)
if DEBUG: os.system(f'cp {result_path} .')
return result_path
def extract_single_blob(content, index, jsons, blobs):
offset = content.find(RESGEN_MAGIC, index)
if offset < 0:
return 0
idx = offset + len(RESGEN_MAGIC)
if chr(content[idx]) == '_':
return idx
size_string = ""
while chr(content[idx]) != '{':
size_string += chr(content[idx])
idx += 1
if len(size_string) >= 10: quit('Bad JSON chunk: ' + size_string)
json_string = content[idx:idx+int(size_string)]
if DEBUG: print(json_string)
resgen_json = json.loads(json_string)
total_size = 0
for mat in resgen_json:
total_size += resgen_json[mat]
resgen_blob = content[offset - total_size:offset]
jsons.append(resgen_json)
blobs.append(resgen_blob)
return idx+int(size_string)
def extract_resgen(path: Path):
with open(path, mode='rb') as file:
content = file.read()
index, jsons, blobs = 0, [], []
while True:
index = extract_single_blob(content, index, jsons, blobs)
if index == 0:
return jsons, blobs
def get_compressed_size(blob):
tmpzip = Path(f"{TEMPDIR}/tmp.zip")
with zipfile.ZipFile(tmpzip, "w") as tmp:
tmp.writestr("tmp", blob, zipfile.ZIP_DEFLATED)
return tmpzip.stat().st_size
def treeify_single_blob(resgen_json, resgen_blob):
csize = format_bytes(get_compressed_size(resgen_blob))
result = {}
total_size = 0
materials = []
offset = 0
for mat in resgen_json:
material_size = resgen_json[mat]
total_size += material_size
material_blob = resgen_blob[offset:offset + material_size]
offset += material_size
chunk_offset = 0
chunks = []
while chunk_offset < len(material_blob):
chunk_name = material_blob[chunk_offset:chunk_offset + 8][::-1].decode("utf-8")
chunk_size = struct.unpack_from('i', material_blob[chunk_offset + 8:chunk_offset + 12])[0]
chunks.append({
"data": {"$area": chunk_size},
"name": chunk_name + " " + format_bytes(chunk_size),
"detail": chunk_name + " " + format_bytes(chunk_size, 3),
})
chunk_offset += 12 + chunk_size
materials.append({
"data": {"$area": material_size, "$symbol": "read-only data"},
"name": mat + " " + format_bytes(material_size),
"children": chunks,
})
result["data"] = {"$area": total_size}
result["name"] = f"materials {format_bytes(total_size)} ({csize})"
result["children"] = materials
return result
def treeify_resgen(resgen_jsons, resgen_blobs):
result = []
for resgen_json, resgen_blob in zip(resgen_jsons, resgen_blobs):
result.append(treeify_single_blob(resgen_json, resgen_blob))
return result
def main(args):
path = Path(args.input)
path.exists() or quit("No file or folder at the specified path.")
if path.is_dir():
path = choose_from_dir(path)
elif path.suffix in ['.zip', '.aar', '.apk']:
path = choose_from_zip(path)
dsopath = Path(path)
size = format_bytes(dsopath.stat().st_size, 2)
csize = format_bytes(get_compressed_size(open(dsopath, 'rb').read()), 2)
info = f'{size} ({csize})'
print('Scanning for resgen resources...')
resgen_jsons, resgen_blobs = extract_resgen(dsopath)
print('Running nm... (this might take a while)')
os.system(f'nm -C -S {path} > {TEMPDIR}/nm.out')
if DEBUG: os.system(f'cp {TEMPDIR}/nm.out .')
print('Running objdump...')
os.system(f'objdump -h {path} > {TEMPDIR}/objdump.out')
if DEBUG: os.system(f'cp {TEMPDIR}/objdump.out .')
print('Generating treemap JSON...')
os.system(f'cd {TEMPDIR} ; python3 {SCRIPTDIR}/evmar_bloat.py syms > syms.json')
os.system(f'cd {TEMPDIR} ; python3 {SCRIPTDIR}/evmar_bloat.py sections > sections.json')
# Splice the materials JSON into the sections JSON.
sections_json = json.loads(open(f'{TEMPDIR}/sections.json').read())
trimmed_json = sections_json
for child in sections_json['children']:
if child["name"].startswith('sections '):
trimmed_json = child
for section in trimmed_json['children']:
if section["name"].startswith('rodata'):
section["children"] = treeify_resgen(resgen_jsons, resgen_blobs)
sections_json = "const kSections = " + json.dumps(trimmed_json) + ";\n"
symbols_json = open(f'{TEMPDIR}/syms.json').read()
symbols_json = "const kSymbols = " + symbols_json + ";\n"
if DEBUG: open('syms.json', 'wt').write(symbols_json)
if DEBUG: open('sections.json', 'wt').write(sections_json)
print(f'Generating {args.output}...')
template = open(f'{SCRIPTDIR}/template.html').read()
template = template.replace('$TITLE$', dsopath.name)
template = template.replace('$INFO$', info)
template = template.replace('$SYMBOLS_JSON$', symbols_json)
template = template.replace('$SECTIONS_JSON$', sections_json)
open(args.output, 'w').write(template)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--output", default="index.html", help="generated HTML file name")
parser.add_argument("input", help="path to folder, zip, aar, apk, or so")
args = parser.parse_args()
main(args)