blob: 480255548a85fce9b9a992e29e847db790b5b16f [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import '../../deferred_load.dart' show OutputUnit;
import '../../js/js.dart' as js;
import '../../js/size_estimator.dart';
import '../../options.dart';
import '../model.dart';
class PreFragment {
final List<DeferredFragment> fragments = [];
final List<js.Statement> classPrototypes = [];
final List<js.Statement> closurePrototypes = [];
final List<js.Statement> inheritance = [];
final List<js.Statement> methodAliases = [];
final List<js.Statement> tearOffs = [];
final List<js.Statement> constants = [];
final List<js.Statement> typeRules = [];
final List<js.Statement> variances = [];
final List<js.Statement> staticNonFinalFields = [];
final List<js.Statement> lazyInitializers = [];
final List<js.Statement> nativeSupport = [];
final Set<PreFragment> successors = {};
final Set<PreFragment> predecessors = {};
int size = 0;
PreFragment(
Fragment fragment,
js.Statement classPrototypes,
js.Statement closurePrototypes,
js.Statement inheritance,
js.Statement methodAliases,
js.Statement tearOffs,
js.Statement constants,
js.Statement typeRules,
js.Statement variances,
js.Statement staticNonFinalFields,
js.Statement lazyInitializers,
js.Statement nativeSupport,
bool estimateSize) {
this.fragments.add(fragment);
this.classPrototypes.add(classPrototypes);
this.closurePrototypes.add(closurePrototypes);
this.inheritance.add(inheritance);
this.methodAliases.add(methodAliases);
this.tearOffs.add(tearOffs);
this.constants.add(constants);
this.typeRules.add(typeRules);
this.variances.add(variances);
this.staticNonFinalFields.add(staticNonFinalFields);
this.lazyInitializers.add(lazyInitializers);
this.nativeSupport.add(nativeSupport);
if (estimateSize) {
var estimator = SizeEstimator();
estimator.visit(classPrototypes);
estimator.visit(closurePrototypes);
estimator.visit(inheritance);
estimator.visit(methodAliases);
estimator.visit(tearOffs);
estimator.visit(constants);
estimator.visit(typeRules);
estimator.visit(variances);
estimator.visit(staticNonFinalFields);
estimator.visit(lazyInitializers);
estimator.visit(nativeSupport);
size = estimator.charCount;
}
}
PreFragment mergeAfter(PreFragment that) {
assert(this != that);
this.fragments.addAll(that.fragments);
this.classPrototypes.addAll(that.classPrototypes);
this.closurePrototypes.addAll(that.closurePrototypes);
this.inheritance.addAll(that.inheritance);
this.methodAliases.addAll(that.methodAliases);
this.tearOffs.addAll(that.tearOffs);
this.constants.addAll(that.constants);
this.typeRules.addAll(that.typeRules);
this.variances.addAll(that.variances);
this.staticNonFinalFields.addAll(that.staticNonFinalFields);
this.lazyInitializers.addAll(that.lazyInitializers);
this.nativeSupport.addAll(that.nativeSupport);
this.successors.remove(that);
that.successors.forEach((fragment) {
fragment.predecessors.remove(that);
fragment.predecessors.add(this);
});
this.successors.addAll(that.successors);
that.predecessors.remove(this);
that.predecessors.forEach((fragment) {
fragment.successors.remove(that);
fragment.successors.add(this);
});
this.predecessors.addAll(that.predecessors);
this.size += that.size;
return this;
}
FinalizedFragment finalize(
Program program, Map<Fragment, FinalizedFragment> fragmentMap) {
FinalizedFragment finalizedFragment;
var seedFragment = fragments.first;
// If we only have a single fragment, then wen just finalize it by itself.
// Otherwise, we finalize an entire group of fragments into a single
// merged and finalized fragment.
if (fragments.length == 1) {
finalizedFragment = FinalizedFragment(
seedFragment.outputFileName,
seedFragment.outputUnit,
seedFragment.libraries,
classPrototypes.first,
closurePrototypes.first,
inheritance.first,
methodAliases.first,
tearOffs.first,
constants.first,
typeRules.first,
variances.first,
staticNonFinalFields.first,
lazyInitializers.first,
nativeSupport.first,
program.metadataTypesForOutputUnit(seedFragment.outputUnit));
fragmentMap[seedFragment] = finalizedFragment;
} else {
List<Library> libraries = [];
for (var fragment in fragments) {
if (seedFragment.outputUnit != fragment.outputUnit) {
program.mergeOutputUnitMetadata(
seedFragment.outputUnit, fragment.outputUnit);
seedFragment.outputUnit.merge(fragment.outputUnit);
}
libraries.addAll(fragment.libraries);
}
finalizedFragment = FinalizedFragment(
seedFragment.outputFileName,
seedFragment.outputUnit,
libraries,
js.Block(classPrototypes),
js.Block(closurePrototypes),
js.Block(inheritance),
js.Block(methodAliases),
js.Block(tearOffs),
js.Block(constants),
js.Block(typeRules),
js.Block(variances),
js.Block(staticNonFinalFields),
js.Block(lazyInitializers),
js.Block(nativeSupport),
program.metadataTypesForOutputUnit(seedFragment.outputUnit));
for (var fragment in fragments) {
fragmentMap[fragment] = finalizedFragment;
}
}
return finalizedFragment;
}
@override
String toString() {
// This is not an efficient operation and should only be used for debugging.
var successors =
this.successors.map((fragment) => fragment.debugName()).join(',');
var predecessors =
this.predecessors.map((fragment) => fragment.debugName()).join(',');
var name = debugName();
return 'PreFragment(fragments=[$name], successors=[$successors], '
'predecessors=[$predecessors])';
}
String debugName() {
List<String> names = [];
this.fragments.forEach(
(fragment) => names.add(fragment.outputUnit.imports.toString()));
var outputUnitStrings = [];
for (var fragment in fragments) {
var importString = [];
for (var import in fragment.outputUnit.imports) {
importString.add(import.name);
}
outputUnitStrings.add('{${importString.join(', ')}}');
}
return "${outputUnitStrings.join('+')}";
}
}
class FinalizedFragment {
final String outputFileName;
final OutputUnit outputUnit;
final List<Library> libraries;
final js.Statement classPrototypes;
final js.Statement closurePrototypes;
final js.Statement inheritance;
final js.Statement methodAliases;
final js.Statement tearOffs;
final js.Statement constants;
final js.Statement typeRules;
final js.Statement variances;
final js.Statement staticNonFinalFields;
final js.Statement lazyInitializers;
final js.Statement nativeSupport;
final js.Expression deferredTypes;
FinalizedFragment(
this.outputFileName,
this.outputUnit,
this.libraries,
this.classPrototypes,
this.closurePrototypes,
this.inheritance,
this.methodAliases,
this.tearOffs,
this.constants,
this.typeRules,
this.variances,
this.staticNonFinalFields,
this.lazyInitializers,
this.nativeSupport,
this.deferredTypes);
bool isEmptyStatement(js.Statement statement) {
if (statement is js.Block) {
return statement.statements.isEmpty;
}
return statement is js.EmptyStatement;
}
bool get isEmpty {
// TODO(sra): How do we tell if [deferredTypes] is empty? It is filled-in
// later via the program finalizers. So we should defer the decision on the
// emptiness of the fragment until the finalizers have run. For now we seem
// to get away with the fact that type indexes are either (1) main unit or
// (2) local to the emitted unit, so there is no such thing as a type in a
// deferred unit that is referenced from another deferred unit. If we did
// not emit any functions, then we probably did not use the signature types
// in the OutputUnit's types, leaving them unused and tree-shaken.
// TODO(joshualitt): Currently, we ignore [typeRules] when determining
// emptiness because the type rules never seem to be empty.
return isEmptyStatement(classPrototypes) &&
isEmptyStatement(closurePrototypes) &&
isEmptyStatement(inheritance) &&
isEmptyStatement(methodAliases) &&
isEmptyStatement(tearOffs) &&
isEmptyStatement(constants) &&
isEmptyStatement(staticNonFinalFields) &&
isEmptyStatement(lazyInitializers) &&
isEmptyStatement(nativeSupport);
}
}
class _Partition {
int size = 0;
List<PreFragment> fragments = [];
void add(PreFragment that) {
size += that.size;
fragments.add(that);
}
}
class FragmentMerger {
final CompilerOptions _options;
FragmentMerger(this._options);
// Converts a map of (loadId, List<fragments>) to a map of
// (loadId, List<FinalizedFragment>).
static Map<String, List<FinalizedFragment>> processLoadMap(
Map<String, List<Fragment>> programLoadMap,
Map<Fragment, FinalizedFragment> fragmentMap) {
Map<String, List<FinalizedFragment>> loadMap = {};
programLoadMap.forEach((loadId, fragments) {
Set<FinalizedFragment> unique = {};
List<FinalizedFragment> finalizedFragments = [];
loadMap[loadId] = finalizedFragments;
for (var fragment in fragments) {
var finalizedFragment = fragmentMap[fragment];
if (unique.add(finalizedFragment)) {
finalizedFragments.add(finalizedFragment);
}
}
});
return loadMap;
}
// Attaches predecessors to each PreFragment. We only care about
// direct predecessors.
static void attachDependencies(Map<String, List<Fragment>> programLoadMap,
Map<Fragment, PreFragment> fragmentMap) {
programLoadMap.forEach((loadId, fragments) {
for (int i = 0; i < fragments.length - 1; i++) {
var fragment = fragmentMap[fragments[i]];
var nextFragment = fragmentMap[fragments[i + 1]];
fragment.successors.add(nextFragment);
nextFragment.predecessors.add(fragment);
}
});
}
/// A trivial greedy merge that uses the sorted order of the output units to
/// merge contiguous runs of fragments without creating cycles.
/// ie, if our sorted output units look like:
/// {a}, {b}, {c}, {a, b}, {b, c}, {a, b, c},
/// Assuming singletons have size 3, doubles have size 2, and triples have
/// size 1, total size would be 14. If we want 3 fragments, we have an ideal
/// fragment size of 5. Our final partitions would look like:
/// {a}, {b}, {c}+{a, b}, {b, c}+{a, b, c}.
List<PreFragment> mergeFragments(List<PreFragment> preDeferredFragments) {
// Sort PreFragments by their initial OutputUnit so they are in canonical
// order.
preDeferredFragments.sort((a, b) {
return a.fragments.single.outputUnit
.compareTo(b.fragments.single.outputUnit);
});
int desiredNumberOfFragment = _options.mergeFragmentsThreshold;
// TODO(joshualitt): Precalculate totalSize when computing dependencies.
int totalSize = 0;
for (var preFragment in preDeferredFragments) {
totalSize += preFragment.size;
}
int idealFragmentSize = (totalSize / desiredNumberOfFragment).ceil();
List<_Partition> partitions = [];
void add(PreFragment next) {
// Create a new partition if the current one grows too large, otherwise
// just add to the most recent partition.
if (partitions.isEmpty ||
partitions.last.size + next.size > idealFragmentSize) {
partitions.add(_Partition());
}
partitions.last.add(next);
}
// Greedily group fragments into partitions.
preDeferredFragments.forEach(add);
// Reduce fragments by merging fragments with fewer imports into fragments
// with more imports.
List<PreFragment> merged = [];
for (var partition in partitions) {
merged.add(partition.fragments.reduce((a, b) => b.mergeAfter(a)));
}
return merged;
}
}