tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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()