| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:blobstats/blob.dart'; |
| import 'package:blobstats/dart_package.dart'; |
| import 'package:blobstats/package.dart'; |
| import 'package:path/path.dart' as p; |
| |
| class BlobStats { |
| Directory buildDir; |
| Directory outputDir; |
| String suffix; |
| bool humanReadable; |
| Map<String, Blob> blobsByHash = <String, Blob>{}; |
| int duplicatedSize = 0; |
| int deduplicatedSize = 0; |
| List<File> pendingPackages = <File>[]; |
| List<Package> packages = <Package>[]; |
| |
| BlobStats(this.buildDir, this.outputDir, this.suffix, |
| {this.humanReadable = false}); |
| |
| Future addManifest(String dir, String name) async { |
| var lines = await File(p.join(buildDir.path, dir, name)).readAsLines(); |
| for (var line in lines) { |
| var parts = line.split('='); |
| var hash = parts[0]; |
| // Path entries are specified relative to the directory containing the manifest. |
| var entryPath = p.join(dir, parts[1]); |
| var file = File(entryPath); |
| if (entryPath.endsWith('meta.far')) { |
| pendingPackages.add(file); |
| } |
| |
| if (suffix != null && !entryPath.endsWith(suffix)) { |
| continue; |
| } |
| |
| var stat = file.statSync(); |
| if (stat.type == FileSystemEntityType.notFound) { |
| print('$entryPath does not exist'); |
| continue; |
| } |
| var blob = blobsByHash[hash]; |
| if (blob == null) { |
| var blob = Blob() |
| ..hash = hash |
| ..buildPath = entryPath |
| ..sizeOnHost = stat.size |
| ..count = 0; |
| blobsByHash[hash] = blob; |
| } |
| } |
| } |
| |
| Future addBlobSizes(String path) async { |
| var blobs = |
| json.decode(await File(p.join(buildDir.path, path)).readAsString()); |
| for (var blob in blobs) { |
| var b = blobsByHash[blob['merkle']]; |
| if (b != null) { |
| b.size = blob['size']; |
| } |
| } |
| } |
| |
| void printBlobList(List<Blob> blobs, int limit) { |
| print(' Size Share Prop Saved Path'); |
| var n = 0; |
| for (var blob in blobs) { |
| if (n++ > limit) return; |
| |
| var sb = StringBuffer() |
| ..write(formatSize(blob.size).padLeft(9)) |
| ..write(' ') |
| ..write(blob.count.toString().padLeft(5)) |
| ..write(' ') |
| ..write(formatSize(blob.proportional).padLeft(9)) |
| ..write(' ') |
| ..write(formatSize(blob.saved).padLeft(9)) |
| ..write(' ') |
| ..write(blob.sourcePath); |
| print(sb); |
| } |
| } |
| |
| void printBlobs(int limit) { |
| var blobs = blobsByHash.values.toList(); |
| print('Top blobs by size ($limit of ${blobs.length})'); |
| blobs.sort((a, b) => b.size.compareTo(a.size)); |
| printBlobList(blobs, limit); |
| |
| blobs.removeWhere((blob) => blob.count == 1); |
| |
| print(''); |
| print('Top deduplicated blobs by proportional ($limit of ${blobs.length})'); |
| blobs.sort((a, b) => b.proportional.compareTo(a.proportional)); |
| printBlobList(blobs, limit); |
| |
| print(''); |
| print('Top deduplicated blobs by saved ($limit of ${blobs.length})'); |
| blobs.sort((a, b) => b.saved.compareTo(a.saved)); |
| printBlobList(blobs, limit); |
| } |
| |
| void printOverallSavings() { |
| var percent = (duplicatedSize - deduplicatedSize) * 100 ~/ duplicatedSize; |
| print(''); |
| print('Total savings from deduplication:'); |
| print( |
| ' $percent% ${formatSize(deduplicatedSize)} / ${formatSize(duplicatedSize)}'); |
| } |
| |
| String metaFarToBlobsJson(String farPath) { |
| // Assumes details of //build/package.gni, namely that it generates |
| // <build-dir>/.../<package>/meta.far |
| // and puts a blobs.json file into |
| // <build-dir>/.../<package>/blobs.json |
| if (!farPath.endsWith('/meta.far')) { |
| throw ArgumentError('Build details have changed'); |
| } |
| String path = '${removeSuffix(farPath, 'meta.far')}blobs.json'; |
| if (!File(path).existsSync()) { |
| throw ArgumentError( |
| 'Build details have changed - path to blobs.json $path not found for $farPath'); |
| } |
| return path; |
| } |
| |
| Future computePackagesInParallel(int jobs) async { |
| var tasks = <Future>[]; |
| for (var i = 0; i < jobs; i++) { |
| tasks.add(computePackages()); |
| } |
| await Future.wait(tasks); |
| } |
| |
| Future computePackages() async { |
| while (pendingPackages.isNotEmpty) { |
| File far = pendingPackages.removeLast(); |
| |
| var package = Package()..path = far.path; |
| var parts = package.path.split('/'); |
| package |
| ..name = removeSuffix( |
| parts.length > 1 ? parts[parts.length - 2] : parts.last, '.meta') |
| ..size = 0 |
| ..proportional = 0 |
| ..private = 0 |
| ..blobCount = 0 |
| ..blobsByPath = <String, Blob>{}; |
| |
| var blobs = |
| json.decode(await File(metaFarToBlobsJson(far.path)).readAsString()); |
| |
| for (var blob in blobs) { |
| var hash = blob['merkle']; |
| var path = blob['path']; |
| var b = blobsByHash[hash]; |
| if (b == null) { |
| print( |
| '$path $hash is in a package manifest but not the final manifest'); |
| continue; |
| } |
| b.count++; |
| var sourcePath = blob['source_path']; |
| // If the source_path looks like <something>/blobs/<merkle>, it from a prebuilt package and has no |
| // meaningful source. Instead, use the path within the package as its identifier. |
| if (sourcePath.endsWith('/blobs/$hash')) { |
| sourcePath = path; |
| } |
| // We may see the same blob referenced from different packages with different source paths. |
| // If all references agree with each other, use that. |
| // Otherwise record the first observed path and append ' *' to denote that the path is only one of many. |
| if (b.sourcePath == 'Unknown') { |
| b.sourcePath = sourcePath; |
| } else if (b.sourcePath != sourcePath && !b.sourcePath.endsWith(' *')) { |
| b.sourcePath = '${b.sourcePath} *'; |
| } |
| package.blobsByPath[path] = b; |
| } |
| |
| packages.add(package); |
| } |
| } |
| |
| void computeStats() { |
| var filteredBlobs = <String, Blob>{}; |
| blobsByHash.forEach((hash, blob) { |
| if (blob.count == 0) { |
| print( |
| '${blob.hash} is in the final manifest but not any package manifest'); |
| } else { |
| filteredBlobs[hash] = blob; |
| } |
| }); |
| blobsByHash = filteredBlobs; |
| |
| for (var blob in blobsByHash.values) { |
| duplicatedSize += (blob.size * blob.count); |
| deduplicatedSize += blob.size; |
| } |
| |
| for (var package in packages) { |
| for (var blob in package.blobsByPath.values) { |
| package |
| ..size += blob.size |
| ..proportional += blob.proportional; |
| if (blob.count == 1) { |
| package.private += blob.size; |
| } |
| package.blobCount++; |
| } |
| } |
| } |
| |
| void printPackages() { |
| packages.sort((a, b) => b.proportional.compareTo(a.proportional)); |
| print(''); |
| print('Packages by proportional (${packages.length})'); |
| print(' Size Prop Private Name'); |
| for (var package in packages) { |
| var sb = StringBuffer() |
| ..write(formatSize(package.size).padLeft(9)) |
| ..write(' ') |
| ..write(formatSize(package.proportional).padLeft(9)) |
| ..write(' ') |
| ..write(formatSize(package.private).padLeft(9)) |
| ..write(' ') |
| ..write(package.name); |
| print(sb); |
| } |
| } |
| |
| Future packagesToChromiumBinarySizeTree() async { |
| var rootTree = {}; |
| rootTree['n'] = 'packages'; |
| rootTree['children'] = []; |
| rootTree['k'] = 'p'; // kind=path |
| for (var pkg in packages) { |
| var pkgTree = {}; |
| pkgTree['n'] = pkg.name; |
| pkgTree['children'] = []; |
| pkgTree['k'] = 'p'; // kind=path |
| rootTree['children'].add(pkgTree); |
| pkg.blobsByPath.forEach((path, blob) { |
| var blobName = path; //path.split('/').last; |
| var blobTree = {}; |
| blobTree['n'] = blobName; |
| blobTree['k'] = 's'; // kind=blob |
| var isUnique = blob.count == 1; |
| var isDart = |
| blobName.endsWith('.dilp') || blobName.endsWith('.aotsnapshot'); |
| if (isDart) { |
| if (isUnique) { |
| blobTree['t'] = 'uniDart'; |
| } else { |
| blobTree['t'] = 'dart'; // type=Shared Dart ('blue') |
| } |
| } else { |
| if (isUnique) { |
| blobTree['t'] = 'unique'; |
| } else { |
| blobTree['t'] = '?'; // type=Other ('red') |
| } |
| } |
| blobTree['c'] = blob.count; |
| blobTree['value'] = blob.proportional; |
| blobTree['originalSize'] = blob.sizeOnHost; |
| pkgTree['children'].add(blobTree); |
| }); |
| } |
| |
| var sink = File(p.join(outputDir.path, 'data.js')).openWrite() |
| ..write('var tree_data=') |
| ..write(json.encode(rootTree)); |
| await sink.close(); |
| |
| await Directory(p.join(outputDir.path, 'd3_v3')).create(recursive: true); |
| var d3Dir = p.join(buildDir.path, '../../scripts/third_party/d3_v3/'); |
| for (var file in ['LICENSE', 'd3.js']) { |
| await File(d3Dir + file).copy(p.join(outputDir.path, 'd3_v3', file)); |
| } |
| var templateDir = p.join(buildDir.path, '../../tools/blobstats/template/'); |
| for (var file in ['index.html', 'D3BlobTreeMap.js']) { |
| await File(templateDir + file).copy(p.join(outputDir.path, file)); |
| } |
| |
| print('Wrote visualization to ${p.join(outputDir.path, 'index.html')}'); |
| } |
| |
| void printDartPackages() async { |
| var dartPackagesMap = <String, DartPackage>{}; |
| for (var fuchsiaPackage in packages) { |
| fuchsiaPackage.blobsByPath.forEach((path, blob) { |
| if (!path.endsWith('.dilp')) return; |
| |
| var dartPackageName = removeSuffix(path.split('/').last, '.dilp'); |
| if (dartPackageName == 'main') return; |
| |
| var dartPackage = dartPackagesMap.putIfAbsent( |
| dartPackageName, () => DartPackage(dartPackageName)); |
| |
| dartPackage.blobs.putIfAbsent(blob, () => []).add(fuchsiaPackage.name); |
| }); |
| } |
| |
| var dartPackagesList = dartPackagesMap.values.toList() |
| ..sort((a, b) => a.name.compareTo(b.name)) |
| ..sort((a, b) => a.blobs.length.compareTo(b.blobs.length)); |
| |
| print(''); |
| print('Dart packages:'); |
| for (var dartPackage in dartPackagesList) { |
| print('package:${dartPackage.name} (${dartPackage.blobs.length} blobs)'); |
| if (dartPackage.blobs.length == 1) { |
| continue; |
| } |
| |
| for (var blob in dartPackage.blobs.keys) { |
| var fuchsiaPackages = dartPackage.blobs[blob]; |
| |
| var result = await Process.run(Platform.executable, [ |
| '../../prebuilt/third_party/flutter/x64/debug/jit/dart_binaries/list_libraries.snapshot', |
| blob.buildPath |
| ]); |
| if (result.exitCode != 0) { |
| print(result.stdout); |
| print(result.stderr); |
| throw Exception('Failed to list libraries in kernel file'); |
| } |
| var libraries = result.stdout.split('\n'); |
| libraries.remove(''); |
| |
| print(' ${blob.hash} ${blob.buildPath}'); |
| print(' ${fuchsiaPackages.join(' ')}'); |
| for (var library in libraries) { |
| print(' $library'); |
| } |
| } |
| print(''); |
| } |
| } |
| |
| Future<String> csvBlobs() async { |
| var path = p.join(outputDir.path, 'blobs.csv'); |
| var csv = File(path).openWrite()..writeln('Size,Share,Prop,Saved,Path'); |
| |
| var blobs = blobsByHash.values.toList() |
| ..sort((a, b) => b.size.compareTo(a.size)); |
| |
| for (var blob in blobs) { |
| var values = [ |
| blob.size, |
| blob.count, |
| blob.proportional, |
| blob.saved, |
| blob.sourcePath |
| ]; |
| csv.writeln(values.join(',')); |
| } |
| await csv.close(); |
| return path; |
| } |
| |
| Future<String> csvPackages() async { |
| var path = p.join(outputDir.path, 'packages.csv'); |
| var csv = File(path).openWrite()..writeln('Size,Prop,Private,Name'); |
| |
| packages.sort((a, b) => b.proportional.compareTo(a.proportional)); |
| |
| for (var package in packages) { |
| var values = [ |
| package.size, |
| package.proportional, |
| package.private, |
| package.name |
| ]; |
| csv.writeln(values.join(',')); |
| } |
| await csv.close(); |
| return path; |
| } |
| |
| String formatSize(num size) { |
| if (!humanReadable) return '$size'; |
| |
| var formattedSize = size; |
| if (formattedSize < 1024) return '$formattedSize'; |
| formattedSize /= 1024; |
| if (formattedSize < 1024) return '${formattedSize.toStringAsFixed(1)}K'; |
| formattedSize /= 1024.0; |
| if (formattedSize < 1024) return '${formattedSize.toStringAsFixed(1)}M'; |
| formattedSize /= 1024; |
| return '${formattedSize.toStringAsFixed(1)}G'; |
| } |
| |
| String removeSuffix(String s, String suffix) { |
| if (s.endsWith(suffix)) { |
| return s.substring(0, s.length - suffix.length); |
| } |
| return s; |
| } |
| } |