blob: 6e09080420999a8f5ef1951776f61c35486eab09 [file] [log] [blame]
// 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 [Make sure the build is up to date]');
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;
}
}