blob: 576f4eea12e0c6855e50bffc85c9deb8993c8cc3 [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;
}
}
void mergeAfter(PreFragment that) {
assert(this != that);
assert(
(that.predecessors.length == 1 && that.predecessors.single == this) ||
(this.successors.length == 1 && this.successors.single == 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;
}
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.name));
return names.join(',');
}
static int compare(PreFragment l, PreFragment r) {
return l.size.compareTo(r.size);
}
}
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 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);
}
});
}
// Iterates through preDeferredFragments making as many merges as possible
// until either there are no more valid merges to make, or until there are
// only mergeFragmentsThreshold remaining.
List<PreFragment> mergeFragments(List<PreFragment> preDeferredFragments) {
Set<PreFragment> fragmentsBySize = {};
// We greedily look for a valid merge which results in the smallest
// possible increase in size. Currently, we only merge fragments in two
// cases:
// 1) We will merge two fragments A and B if B is A's single dependent.
// 2) We will merge two fragments C and D if C is D's single dependency.
bool mergeTwo() {
PreFragment aFragment = null;
PreFragment bFragment = null;
PreFragment cFragment = null;
PreFragment dFragment = null;
for (var fragment in fragmentsBySize) {
if (fragment.successors.length == 1 &&
(aFragment == null && bFragment == null ||
(fragment.size + fragment.successors.single.size <
aFragment.size + bFragment.size))) {
aFragment = fragment;
bFragment = fragment.successors.single;
}
if (fragment.predecessors.length == 1 &&
(cFragment == null && dFragment == null ||
(fragment.size + fragment.predecessors.single.size <
cFragment.size + dFragment.size))) {
cFragment = fragment.predecessors.single;
dFragment = fragment;
}
}
assert((aFragment != null &&
bFragment != null &&
aFragment != bFragment &&
aFragment.successors.single == bFragment) ||
(cFragment != null &&
dFragment != null &&
cFragment != dFragment &&
dFragment.predecessors.single == cFragment) ||
(aFragment == null &&
bFragment == null &&
cFragment == null &&
dFragment == null));
int mergeSentinel = 0x10000000000;
bool abCanMerge = aFragment != null && bFragment != null;
bool cdCanMerge = cFragment != null && dFragment != null;
int abMergeSize =
abCanMerge ? aFragment.size + bFragment.size : mergeSentinel;
int cdMergeSize =
cdCanMerge ? cFragment.size + dFragment.size : mergeSentinel;
bool abShouldMerge() => abCanMerge && abMergeSize <= cdMergeSize;
bool cdShouldMerge() => cdCanMerge && cdMergeSize <= abMergeSize;
void innerMerge(PreFragment a, PreFragment b) {
fragmentsBySize.remove(a);
fragmentsBySize.remove(b);
a.mergeAfter(b);
fragmentsBySize.add(a);
}
bool merged = abShouldMerge() || cdShouldMerge();
if (abShouldMerge()) {
innerMerge(aFragment, bFragment);
} else if (cdShouldMerge()) {
innerMerge(cFragment, dFragment);
} else {
assert(aFragment == null &&
bFragment == null &&
cFragment == null &&
dFragment == null);
}
return merged;
}
fragmentsBySize.addAll(preDeferredFragments);
var numFragments = preDeferredFragments.length;
while (numFragments-- > _options.mergeFragmentsThreshold) {
if (!mergeTwo()) {
// No further valid merges can be made.
break;
}
}
return fragmentsBySize.toList();
}
}