footprint.py (3227B)
1 #!/usr/bin/env python3 2 """Compare installed Linux artifacts; Nix and tar are measurement-time tools only.""" 3 import argparse 4 import json 5 from pathlib import Path 6 import subprocess 7 import tempfile 8 import zlib 9 10 11 def compressed_bytes(command): 12 """Count a gzip level-6 stream without retaining/exporting the archive.""" 13 compressor = zlib.compressobj(level=6, wbits=31) 14 size = 0 15 with tempfile.TemporaryFile() as errors: 16 with subprocess.Popen(command, stdout=subprocess.PIPE, stderr=errors) as process: 17 while chunk := process.stdout.read(1024 * 1024): 18 size += len(compressor.compress(chunk)) 19 size += len(compressor.flush()) 20 if process.wait(): 21 errors.seek(0) 22 raise RuntimeError(errors.read().decode('utf-8', errors='replace')) 23 return size 24 25 26 def measure(path, name): 27 path = path.resolve(strict=True) 28 library = path / 'lib' / name 29 assert library.is_file(), library 30 closure = json.loads(subprocess.check_output( 31 ['nix', 'path-info', '--json', '--json-format', '1', '--recursive', str(path)], text=True)) 32 if isinstance(closure, list): 33 closure = {row['path']: row for row in closure} 34 haskell = sorted((path / 'lib').glob('libHS*.so')) 35 return { 36 'store_path': str(path), 37 'build': json.loads((path / 'share/tricu/build.json').read_text()) if (path / 'share/tricu/build.json').exists() else None, 38 'main_library_bytes': library.stat().st_size, 39 'package_file_bytes': sum(p.stat().st_size for p in path.rglob('*') if p.is_file()), 40 'package_nar_bytes': closure[str(path)]['narSize'], 41 'closure_nar_bytes': sum(row['narSize'] for row in closure.values()), 42 'package_tar_gzip6_bytes': compressed_bytes([ 43 'tar', '--sort=name', '--mtime=@1', '--owner=0', '--group=0', 44 '--numeric-owner', '-C', str(path), '-cf', '-', '.']), 45 'closure_nix_export_gzip6_bytes': compressed_bytes([ 46 'nix-store', '--export', *sorted(closure)]), 47 'haskell_libraries': {p.name: p.stat().st_size for p in haskell}, 48 'native_closure': {p: row['narSize'] for p, row in closure.items() if p != str(path)}, 49 } 50 51 52 def main(): 53 parser = argparse.ArgumentParser(description=__doc__) 54 parser.add_argument('--host', required=True, type=Path) 55 parser.add_argument('--host-archive', required=True, type=Path) 56 parser.add_argument('--output', required=True, type=Path) 57 args = parser.parse_args() 58 report = { 59 'method': 'installed files; Nix NAR sizes; deterministic tar and Nix export gzip level 6', 60 'host': measure(args.host, 'libtricu-haskell.so'), 61 } 62 report['host_archive'] = measure(args.host_archive, 'libtricu-haskell.so') 63 args.output.write_text(json.dumps(report, indent=2, sort_keys=True) + '\n') 64 for role in ('host', 'host_archive'): 65 row = report[role] 66 print(role + ': ' + ', '.join( 67 f'{key}={row[key] / 1048576:.2f} MiB' for key in ( 68 'main_library_bytes', 'package_nar_bytes', 'closure_nar_bytes', 69 'package_tar_gzip6_bytes', 'closure_nix_export_gzip6_bytes'))) 70 71 72 if __name__ == '__main__': 73 main()