blob: c531ba2198f8c33962c6d29f0d8a5a372590e72c [file] [log] [blame]
// Copyright (c) 2014, 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.
/// *Overview of deferred loading*
///
/// Deferred loading allows developers to specify deferred imports. These
/// imports represent explicit asynchronous splits of the application that
/// allows code to be delivered in pieces.
///
/// The initial download of an application will exclude code used only by
/// deferred imports. As the application reaches a
/// `deferred_import.loadLibrary()` instruction, it will download and initialize
/// any code needed by that deferred import.
///
/// Very often separate deferred imports access common code. When that happens,
/// the compiler places the shared code in separate files. At runtime, the
/// application will only download shared code once when the first deferred
/// import that needs that code gets loaded. To achieve this, the compiler
/// generates _load lists_: a list of JavaScript files that need to be
/// downloaded for every deferred import in the program.
///
/// Each generated JavaScript file has an initialzation within it. The files can
/// be concatenated together in a bundle without affecting the initialization
/// logic. This is used by customers to reduce the download latency when they
/// know that multiple files will be loaded at once.
///
/// *The code splitting algorithm*
///
/// The goal of this library and the [DeferredLoadingTask] is to determine how
/// to best split code in multiple files according to the principles described
/// above.
///
/// We do so by partitioning code into output units ([OutputUnit]s in our
/// implementation). The partitioning reflects how code is shared between
/// different deferred imports. Each output unit is associated a set of deferred
/// imports (an [ImportSet] in our implementation). These are the deferred
/// imports that need the code that is stored in that output unit. Code that is
/// needed by a single deferred import, will be associated with a set containing
/// that deferred import only (a singleton set), but code that is shared by 10
/// deferred imports will be associated with a set containing all of those
/// imports instead. We determine whether code is shared based on how code is
/// accessed in the program. An element is considered to be accessed by a
/// deferred import if it is either loaded and invoked from that import or
/// transitively accessed by an element that was invoked by that import.
///
/// In theory, there could be an exponential number of output units: one per
/// subset of deferred imports in the program. In practice, large apps do have a
/// large number of output units, but the result is not exponential. This is
/// both because not all deferred imports have code in common and because many
/// deferred imports end up having the same code in common.
///
/// *Main output unit*:
///
/// The main output unit contains any code accessed directly from main. Such
/// code may be accessed by deferred imports too, but because it is accessed
/// from the main entrypoint of the program, possibly synchronously, we do not
/// split out the code or defer it. Our current implementation uses an empty
/// import-set as a sentinel value to represent this output unit.
///
/// *Dependency graph*:
///
/// We use the element model to discover dependencies between elements.
/// We distinguish two kinds of dependencies: deferred or direct (aka.
/// non-deferred):
///
/// * Deferred dependencies are only used to discover root elements. Roots
/// are elements immediately loaded and used from deferred import prefixes in
/// a program.
///
/// * Direct dependencies are used to recursively update which output unit
/// should be associated with an element.
///
/// *Algorithm Principle*:
///
/// Conceptually the algorithm consists of associating an element with an
/// import-set. When we discover a root, we mark it and everything it can reach
/// as being used by that import. Marking elements as used by that import
/// consists of adding the import to the import-set of all those reachable
/// elements.
///
/// An earlier version of this algorithm was implemented with this simple
/// approach: we kept a map from entities to a [Set] of imports and updated the
/// sets iteratively. However, as customer applications grew, we needed a more
/// specialized and efficient implementation.
///
/// *ImportSet representation and related optimizations*:
///
/// The most important change to scale the algorithm was to use an efficient
/// representation of entity to import-set associations. For large apps there
/// are a lot of entities, and the simple representation of having a [Set] per
/// entity was too expensive. We observed that many of such sets had the same
/// imports (which makes sense given that many elements ended up together in the
/// same output units). This led us to design the [ImportSet] abstraction: a
/// representation of import-sets that guarantees that each import-set has a
/// canonical representation. Memory-wise this was a big win: we now bounded
/// the heap utilization to one [ImportSet] instance per unique import-set.
///
/// This representation is not perfect. Simple operations, like adding an import
/// to an import-set, are now worse-case linear. So it was important to add a
/// few optimizations in the algorithm in order to adapt to the new
/// representation.
///
/// The principle of our optimizations is to make bulk updates. Rather than
/// adding an import at a time for all reachable elements, we changed the
/// algorithm to make updates in bulk in two ways:
///
/// * Batch unions: when possible add more than one import at once, and
///
/// * Update elements in segments: when an element and its reachable
/// dependencies would change in the same way, update them all together.
///
/// To achieve these bulk updates, the algorithm uses a two tier algorithm:
///
/// * The top tier uses a worklist to track the start of a bulk update, either
/// from a root (entities that dominate code used by a single deferred import)
/// or from a merge point in the dependency graph (entities that dominate
/// shared code between multiple imports).
///
/// * The second tier is where bulk updates are made, these don't use a
/// worklist, but simply a DFS recursive traversal of the dependency graph.
/// The DFS traversal stops at merge points and makes note of them by
/// updating the top tier worklist.
///
///
/// *Example*:
///
/// Consider this dependency graph (ignoring elements in the main output unit):
///
/// deferred import A: a1 ---> s1 ---> s2 -> s3
/// ^ ^
/// | |
/// deferred import B: b1 -----+ |
/// |
/// deferred import C: c1 ---> c2 ---> c3
///
/// Here a1, b1, and c1 are roots, while s1 and s2 are merge points. The
/// algorithm will compute a result with 5 deferred output units:
//
/// * unit {A}: contains a1
/// * unit {B}: contains b1
/// * unit {C}: contains c1, c2, and c3
/// * unit {A, B}: contains s1
/// * unit {A, B, C}: contains s2, and s3
///
/// After marking everything reachable from main as part of the main output
/// unit, our algorithm will work as follows:
///
/// * Initially all deferred elements have no mapping.
/// * We make note of work to do, initially to mark the root of each
/// deferred import:
/// * a1 with A, and recurse from there.
/// * b1 with B, and recurse from there.
/// * c1 with C, and recurse from there.
/// * We update a1, s1, s2, s3 in bulk, from no mapping to {A}.
/// * We update b1 from no mapping to {B}, and when we find s1 we notice
/// that s1 is already associated with another import set {A}. This is a
/// merge point that can't be updated in bulk, so we make
/// note of additional work for later to mark s1 with {A, B}
/// * We update in bulk c1, c2, c3 to {C}, and make a note to update s2 with
/// {A, C} (another merge point).
/// * We update s1 to {A, B}, and update the existing note to update s2, now
/// with {A, B, C}
/// * Finally we update s2 and s3 with {A, B, C} in bulk, without ever
/// updating them to the intermediate state {A, C}.
///
/// *How bulk segment updates work?*
///
/// The principle of the bulk segment update is similar to memoizing the result
/// of a union operation. We replace a union operation with a cached result if
/// we can tell that the inputs to the operation are the same.
///
/// Our implementation doesn't use a cache table to memoize arbitrary unions.
/// Instead it only memoizes one union at a time: it tries to reuse the result
/// of a union applied to one entity, when updating the import-sets of its
/// transitive dependencies.
///
/// Consider a modification of the example above where we add s4 and s5 as
/// additional dependencies of s3. Conceptually, we are applying this sequence
/// of union operations:
///
/// importSet[s2] = importSet[s2] UNION {B, C}
/// importSet[s3] = importSet[s3] UNION {B, C}
/// importSet[s4] = importSet[s4] UNION {B, C}
/// importSet[s5] = importSet[s5] UNION {B, C}
///
/// When the algorithm is updating s2, it checks whether any of the entities
/// reachable from s2 also have the same import-set as s2, and if so, we know
/// that the union result is the same.
///
/// Our implementation uses the term `oldSet` to represent the first input of
/// the memoized union operation, and `newSet` to represent the result:
///
/// oldSet = importSet[s2] // = A
/// newSet = oldSet UNION {B, C} // = {A, B, C}
///
/// Then the updates are encoded as:
///
/// update(s2, oldSet, newSet);
/// update(s3, oldSet, newSet);
/// update(s4, oldSet, newSet);
/// update(s5, oldSet, newSet);
///
/// where:
///
/// update(s5, oldSet, newSet) {
/// var currentSet = importSet[s];
/// if (currentSet == oldSet) {
/// // Use the memoized result, whohoo!
/// importSet[s] = newSet;
/// } else {
/// // Don't use the memoized result, instead use the worklist to later
/// // update `s` with the appropriate union operation.
/// }
/// }
///
/// As a result of this, the update to the import set for s2, s3, s4 and s5
/// becomes a single if-check and an assignment, but the union operation was
/// only executed once.
///
/// *Constraints*:
///
/// By default our algorithm considers all deferred imports equally and
/// potentially occurring at any time in the application lifetime. In practice,
/// apps use deferred imports to layer the load of their application and, often,
/// developers know how imports will be loaded over time.
///
/// Dart2js accepts a configuration file to specify constraints about deferred
/// imports. There are many kinds of constraints that help developers encode how
/// their applications work.
///
/// To model constraints, the deferred loading algorithm was changed to include
/// _set transitions_: these are changes made to import-sets to effectively
/// encode the constraints.
///
/// Consider, for example, a program with two deferred imports `A` and `B`. Our
/// unconstrained algorithm will split the code in 3 files:
///
/// * code unique to `A` (represented by the import set `{A}`)
///
/// * code unique to `B` (represented by the import set `{B}`)
///
/// * code shared between `A and `B (represented by the import set `{A, B}`)
///
/// When an end-user loads the user journey corresponding to `A`, the code for
/// `{A}` and `{A,B}` gets loaded. When they load the user journey corresponding
/// to `B`, `{B}` and `{A, B}` gets loaded.
///
/// An ordering constraint saying that `B` always loads after `A` tells our
/// algorithm that, even though there exists code that is unique to `A`, we
/// could merge it together with the shared code between `A` and `B`, since the
/// user never intends to load `B` first. The result would be to have two files
/// instead:
///
/// * code unique to `B` (represented by the import set `{B}`)
///
/// * code unique to A and code shared between A and B (represented by the
/// import set `{A, B}`)
///
///
/// In this example, the set transition is to convert any set containing `{A}`
/// into a set containing `{A, B}`.
///
// TODO(joshualitt): update doc above when main is represented by a set
// containing an implict import corresponding to `main`.
// TODO(sigmund): investigate different heuristics for how to select the next
// work item (e.g. we might converge faster if we pick first the update that
// contains a bigger delta.)
library deferred_load;
import 'dart:collection' show Queue;
import 'package:kernel/ast.dart' as ir;
import 'dependencies.dart';
import 'import_set.dart';
import 'output_unit.dart';
import '../../compiler_new.dart' show OutputType;
import '../common/metrics.dart' show Metric, Metrics, CountMetric, DurationMetric;
import '../common/tasks.dart' show CompilerTask;
import '../common.dart';
import '../common_elements.dart' show CommonElements, KElementEnvironment;
import '../compiler.dart' show Compiler;
import '../constants/values.dart'
show
ConstantValue,
ConstructedConstantValue,
InstantiationConstantValue;
import '../elements/types.dart';
import '../elements/entities.dart';
import '../kernel/kelements.dart' show KLocalFunction;
import '../kernel/element_map.dart';
import '../universe/use.dart';
import '../universe/world_impact.dart'
show ImpactUseCase, WorldImpact, WorldImpactVisitorImpl;
import '../util/util.dart' show makeUnique;
import '../world.dart' show KClosedWorld;
// TODO(joshualitt): Refactor logic out of DeferredLoadTask so work_queue.dart
// can be its own independent library.
part 'work_queue.dart';
class _DeferredLoadTaskMetrics implements Metrics {
@override
String get namespace => 'deferred_load';
DurationMetric time = DurationMetric('time');
CountMetric outputUnitElements = CountMetric('outputUnitElements');
@override
Iterable<Metric> get primary => [time];
@override
Iterable<Metric> get secondary => [outputUnitElements];
}
/// For each deferred import, find elements and constants to be loaded when that
/// import is loaded. Elements that are used by several deferred imports are in
/// shared OutputUnits.
class DeferredLoadTask extends CompilerTask {
@override
String get name => 'Deferred Loading';
/// The OutputUnit that will be loaded when the program starts.
OutputUnit _mainOutputUnit;
/// A set containing (eventually) all output units that will result from the
/// program.
final List<OutputUnit> _allOutputUnits = [];
/// Will be `true` if the program contains deferred libraries.
bool isProgramSplit = false;
static const ImpactUseCase IMPACT_USE = ImpactUseCase('Deferred load');
/// A cache of the result of calling `computeImportDeferName` on the keys of
/// this map.
final Map<ImportEntity, String> importDeferName = {};
/// A mapping from classes to their import set.
Map<ClassEntity, ImportSet> _classToSet = {};
/// A mapping from interface types (keyed by classes) to their import set.
Map<ClassEntity, ImportSet> _classTypeToSet = {};
/// A mapping from members to their import set.
Map<MemberEntity, ImportSet> _memberToSet = {};
/// A mapping from local functions to their import set.
Map<Local, ImportSet> _localFunctionToSet = {};
/// A mapping from constants to their import set.
Map<ConstantValue, ImportSet> _constantToSet = {};
Iterable<ImportEntity> get _allDeferredImports =>
_deferredImportDescriptions.keys;
/// Because the token-stream is forgotten later in the program, we cache a
/// description of each deferred import.
final Map<ImportEntity, ImportDescription> _deferredImportDescriptions = {};
/// A lattice to compactly represent multiple subsets of imports.
ImportSetLattice importSets = ImportSetLattice();
final Compiler compiler;
KernelToElementMap _elementMap;
@override
final _DeferredLoadTaskMetrics metrics = _DeferredLoadTaskMetrics();
bool get disableProgramSplit => compiler.options.disableProgramSplit;
DeferredLoadTask(this.compiler, this._elementMap) : super(compiler.measurer) {
_mainOutputUnit = OutputUnit(true, 'main', {});
importSets.mainSet.unit = _mainOutputUnit;
_allOutputUnits.add(_mainOutputUnit);
}
KElementEnvironment get elementEnvironment =>
compiler.frontendStrategy.elementEnvironment;
CommonElements get commonElements => compiler.frontendStrategy.commonElements;
DartTypes get dartTypes => commonElements.dartTypes;
DiagnosticReporter get reporter => compiler.reporter;
/// Collects all direct dependencies of [element].
///
/// The collected dependent elements and constants are are added to
/// [elements] and [constants] respectively.
void _collectDirectMemberDependencies(KClosedWorld closedWorld,
MemberEntity element, Dependencies dependencies) {
// TODO(sigurdm): We want to be more specific about this - need a better
// way to query "liveness".
if (!closedWorld.isMemberUsed(element)) {
return;
}
_collectDependenciesFromImpact(closedWorld, element, dependencies);
collectConstantsInBody(element, dependencies);
}
/// Finds all elements and constants that [element] depends directly on.
/// (not the transitive closure.)
///
/// Adds the results to [elements] and [constants].
void _collectAllElementsAndConstantsResolvedFromClass(
KClosedWorld closedWorld,
ClassEntity element,
Dependencies dependencies) {
// If we see a class, add everything its live instance members refer
// to. Static members are not relevant, unless we are processing
// extra dependencies due to mirrors.
void addLiveInstanceMember(MemberEntity member) {
if (!closedWorld.isMemberUsed(member)) return;
if (!member.isInstanceMember) return;
dependencies.addMember(member);
_collectDirectMemberDependencies(closedWorld, member, dependencies);
}
void addClassAndMaybeAddEffectiveMixinClass(ClassEntity cls) {
dependencies.addClass(cls);
if (elementEnvironment.isMixinApplication(cls)) {
dependencies.addClass(elementEnvironment.getEffectiveMixinClass(cls));
}
}
ClassEntity cls = element;
elementEnvironment.forEachLocalClassMember(cls, addLiveInstanceMember);
elementEnvironment.forEachSupertype(cls, (InterfaceType type) {
_collectTypeDependencies(type, dependencies);
});
elementEnvironment.forEachSuperClass(cls, (superClass) {
addClassAndMaybeAddEffectiveMixinClass(superClass);
_collectTypeDependencies(
elementEnvironment.getThisType(superClass), dependencies);
});
addClassAndMaybeAddEffectiveMixinClass(cls);
}
/// Finds all elements and constants that [element] depends directly on.
/// (not the transitive closure.)
///
/// Adds the results to [elements] and [constants].
void _collectAllElementsAndConstantsResolvedFromMember(
KClosedWorld closedWorld,
MemberEntity element,
Dependencies dependencies) {
if (element is FunctionEntity) {
_collectTypeDependencies(
elementEnvironment.getFunctionType(element), dependencies);
}
if (element.isStatic || element.isTopLevel || element.isConstructor) {
dependencies.addMember(element);
_collectDirectMemberDependencies(closedWorld, element, dependencies);
}
if (element is ConstructorEntity && element.isGenerativeConstructor) {
// When instantiating a class, we record a reference to the
// constructor, not the class itself. We must add all the
// instance members of the constructor's class.
ClassEntity cls = element.enclosingClass;
_collectAllElementsAndConstantsResolvedFromClass(
closedWorld, cls, dependencies);
}
// Other elements, in particular instance members, are ignored as
// they are processed as part of the class.
}
/// Extract the set of constants that are used in the body of [element].
void collectConstantsInBody(MemberEntity element, Dependencies dependencies) {
ir.Member node = _elementMap.getMemberNode(element);
// Fetch the internal node in order to skip annotations on the member.
// TODO(sigmund): replace this pattern when the kernel-ast provides a better
// way to skip annotations (issue 31565).
var visitor = ConstantCollector(
_elementMap, _elementMap.getStaticTypeContext(element), dependencies);
if (node is ir.Field) {
node.initializer?.accept(visitor);
return;
}
if (node is ir.Constructor) {
node.initializers.forEach((i) => i.accept(visitor));
}
node.function?.accept(visitor);
}
/// Recursively collects all the dependencies of [type].
void _collectTypeDependencies(DartType type, Dependencies dependencies,
[ImportEntity import]) {
TypeDependencyVisitor(dependencies, import, commonElements).visit(type);
}
void _collectTypeArgumentDependencies(
Iterable<DartType> typeArguments, Dependencies dependencies,
[ImportEntity import]) {
if (typeArguments == null) return;
TypeDependencyVisitor(dependencies, import, commonElements)
.visitList(typeArguments);
}
/// Extract any dependencies that are known from the impact of [element].
void _collectDependenciesFromImpact(KClosedWorld closedWorld,
MemberEntity element, Dependencies dependencies) {
WorldImpact worldImpact = compiler.impactCache[element];
compiler.impactStrategy.visitImpact(
element,
worldImpact,
WorldImpactVisitorImpl(
visitStaticUse: (MemberEntity member, StaticUse staticUse) {
void processEntity() {
Entity usedEntity = staticUse.element;
if (usedEntity is MemberEntity) {
dependencies.addMember(usedEntity, staticUse.deferredImport);
} else {
assert(usedEntity is KLocalFunction,
failedAt(usedEntity, "Unexpected static use $staticUse."));
KLocalFunction localFunction = usedEntity;
// TODO(sra): Consult KClosedWorld to see if signature is needed.
_collectTypeDependencies(
localFunction.functionType, dependencies);
dependencies.localFunctions.add(localFunction);
}
}
switch (staticUse.kind) {
case StaticUseKind.CONSTRUCTOR_INVOKE:
case StaticUseKind.CONST_CONSTRUCTOR_INVOKE:
// The receiver type of generative constructors is a dependency of
// the constructor (handled by `addMember` above) and not a
// dependency at the call site.
// Factory methods, on the other hand, are like static methods so
// the target type is not relevant.
// TODO(johnniwinther): Use rti need data to skip unneeded type
// arguments.
_collectTypeArgumentDependencies(
staticUse.type.typeArguments, dependencies);
processEntity();
break;
case StaticUseKind.STATIC_INVOKE:
case StaticUseKind.CLOSURE_CALL:
case StaticUseKind.DIRECT_INVOKE:
// TODO(johnniwinther): Use rti need data to skip unneeded type
// arguments.
_collectTypeArgumentDependencies(
staticUse.typeArguments, dependencies);
processEntity();
break;
case StaticUseKind.STATIC_TEAR_OFF:
case StaticUseKind.CLOSURE:
case StaticUseKind.STATIC_GET:
case StaticUseKind.STATIC_SET:
processEntity();
break;
case StaticUseKind.SUPER_TEAR_OFF:
case StaticUseKind.SUPER_FIELD_SET:
case StaticUseKind.SUPER_GET:
case StaticUseKind.SUPER_SETTER_SET:
case StaticUseKind.SUPER_INVOKE:
case StaticUseKind.INSTANCE_FIELD_GET:
case StaticUseKind.INSTANCE_FIELD_SET:
case StaticUseKind.FIELD_INIT:
case StaticUseKind.FIELD_CONSTANT_INIT:
// These static uses are not relevant for this algorithm.
break;
case StaticUseKind.CALL_METHOD:
case StaticUseKind.INLINING:
failedAt(element, "Unexpected static use: $staticUse.");
break;
}
}, visitTypeUse: (MemberEntity member, TypeUse typeUse) {
void addClassIfInterfaceType(DartType t, [ImportEntity import]) {
var typeWithoutNullability = t.withoutNullability;
if (typeWithoutNullability is InterfaceType) {
dependencies.addClass(typeWithoutNullability.element, import);
}
}
DartType type = typeUse.type;
switch (typeUse.kind) {
case TypeUseKind.TYPE_LITERAL:
_collectTypeDependencies(
type, dependencies, typeUse.deferredImport);
break;
case TypeUseKind.CONST_INSTANTIATION:
addClassIfInterfaceType(type, typeUse.deferredImport);
_collectTypeDependencies(
type, dependencies, typeUse.deferredImport);
break;
case TypeUseKind.INSTANTIATION:
case TypeUseKind.NATIVE_INSTANTIATION:
addClassIfInterfaceType(type);
_collectTypeDependencies(type, dependencies);
break;
case TypeUseKind.IS_CHECK:
case TypeUseKind.CATCH_TYPE:
_collectTypeDependencies(type, dependencies);
break;
case TypeUseKind.AS_CAST:
if (closedWorld.annotationsData
.getExplicitCastCheckPolicy(element)
.isEmitted) {
_collectTypeDependencies(type, dependencies);
}
break;
case TypeUseKind.IMPLICIT_CAST:
if (closedWorld.annotationsData
.getImplicitDowncastCheckPolicy(element)
.isEmitted) {
_collectTypeDependencies(type, dependencies);
}
break;
case TypeUseKind.PARAMETER_CHECK:
case TypeUseKind.TYPE_VARIABLE_BOUND_CHECK:
if (closedWorld.annotationsData
.getParameterCheckPolicy(element)
.isEmitted) {
_collectTypeDependencies(type, dependencies);
}
break;
case TypeUseKind.RTI_VALUE:
case TypeUseKind.TYPE_ARGUMENT:
case TypeUseKind.NAMED_TYPE_VARIABLE_NEW_RTI:
case TypeUseKind.CONSTRUCTOR_REFERENCE:
failedAt(element, "Unexpected type use: $typeUse.");
break;
}
}, visitDynamicUse: (MemberEntity member, DynamicUse dynamicUse) {
// TODO(johnniwinther): Use rti need data to skip unneeded type
// arguments.
_collectTypeArgumentDependencies(
dynamicUse.typeArguments, dependencies);
}),
DeferredLoadTask.IMPACT_USE);
}
/// Update the import set of all constants reachable from [constant], as long
/// as they had the [oldSet]. As soon as we see a constant with a different
/// import set, we stop and enqueue a new recursive update in [queue].
///
/// Invariants: oldSet is either null or a subset of newSet.
void _updateConstantRecursive(
KClosedWorld closedWorld,
ConstantValue constant,
ImportSet oldSet,
ImportSet newSet,
WorkQueue queue) {
if (constant == null) return;
var currentSet = _constantToSet[constant];
// Already visited.
if (currentSet == newSet) return;
// Elements in the main output unit always remain there.
if (currentSet == importSets.mainSet) return;
if (currentSet == oldSet) {
_constantToSet[constant] = newSet;
if (constant is ConstructedConstantValue) {
ClassEntity cls = constant.type.element;
_updateClassRecursive(closedWorld, cls, oldSet, newSet, queue);
}
if (constant is InstantiationConstantValue) {
for (DartType type in constant.typeArguments) {
type = type.withoutNullability;
if (type is InterfaceType) {
_updateClassRecursive(
closedWorld, type.element, oldSet, newSet, queue);
}
}
}
constant.getDependencies().forEach((ConstantValue dependency) {
// Constants are not allowed to refer to deferred constants, so
// no need to check for a deferred type literal here.
_updateConstantRecursive(
closedWorld, dependency, oldSet, newSet, queue);
});
} else {
assert(
// Invariant: we must mark main before we mark any deferred import.
newSet != importSets.mainSet || oldSet != null,
failedAt(
NO_LOCATION_SPANNABLE,
"Tried to assign ${constant.toDartText(closedWorld.dartTypes)} "
"to the main output unit, but it was assigned to $currentSet."));
queue.addConstant(constant, newSet);
}
}
void _updateClassRecursive(KClosedWorld closedWorld, ClassEntity element,
ImportSet oldSet, ImportSet newSet, WorkQueue queue) {
if (element == null) return;
ImportSet currentSet = _classToSet[element];
// Already visited. We may visit some root nodes a second time with
// [isMirrorUsage] in order to mark static members used reflectively.
if (currentSet == newSet) return;
// Elements in the main output unit always remain there.
if (currentSet == importSets.mainSet) return;
if (currentSet == oldSet) {
// Continue recursively updating from [oldSet] to [newSet].
_classToSet[element] = newSet;
Dependencies dependencies = Dependencies();
_collectAllElementsAndConstantsResolvedFromClass(
closedWorld, element, dependencies);
LibraryEntity library = element.library;
_processDependencies(
closedWorld, library, dependencies, oldSet, newSet, queue, element);
} else {
queue.addClass(element, newSet);
}
}
void _updateClassTypeRecursive(KClosedWorld closedWorld, ClassEntity element,
ImportSet oldSet, ImportSet newSet, WorkQueue queue) {
if (element == null) return;
ImportSet currentSet = _classTypeToSet[element];
// Already visited. We may visit some root nodes a second time with
// [isMirrorUsage] in order to mark static members used reflectively.
if (currentSet == newSet) return;
// Elements in the main output unit always remain there.
if (currentSet == importSets.mainSet) return;
if (currentSet == oldSet) {
// Continue recursively updating from [oldSet] to [newSet].
_classTypeToSet[element] = newSet;
Dependencies dependencies = Dependencies();
dependencies.addClassType(element);
LibraryEntity library = element.library;
_processDependencies(
closedWorld, library, dependencies, oldSet, newSet, queue, element);
} else {
queue.addClassType(element, newSet);
}
}
void _updateMemberRecursive(KClosedWorld closedWorld, MemberEntity element,
ImportSet oldSet, ImportSet newSet, WorkQueue queue) {
if (element == null) return;
ImportSet currentSet = _memberToSet[element];
// Already visited. We may visit some root nodes a second time with
// [isMirrorUsage] in order to mark static members used reflectively.
if (currentSet == newSet) return;
// Elements in the main output unit always remain there.
if (currentSet == importSets.mainSet) return;
if (currentSet == oldSet) {
// Continue recursively updating from [oldSet] to [newSet].
_memberToSet[element] = newSet;
Dependencies dependencies = Dependencies();
_collectAllElementsAndConstantsResolvedFromMember(
closedWorld, element, dependencies);
LibraryEntity library = element.library;
_processDependencies(
closedWorld, library, dependencies, oldSet, newSet, queue, element);
} else {
queue.addMember(element, newSet);
}
}
void _updateLocalFunction(
Local localFunction, ImportSet oldSet, ImportSet newSet) {
ImportSet currentSet = _localFunctionToSet[localFunction];
if (currentSet == newSet) return;
// Elements in the main output unit always remain there.
if (currentSet == importSets.mainSet) return;
if (currentSet == oldSet) {
_localFunctionToSet[localFunction] = newSet;
} else {
_localFunctionToSet[localFunction] = importSets.union(currentSet, newSet);
}
// Note: local functions are not updated recursively because the
// dependencies are already visited as dependencies of the enclosing member.
}
/// Whether to enqueue a deferred dependency.
///
/// Due to the nature of the algorithm, some dependencies may be visited more
/// than once. However, we know that new deferred-imports are only discovered
/// when we are visiting the main output unit (size == 0) or code reachable
/// from a deferred import (size == 1). After that, we are rediscovering the
/// same nodes we have already seen.
_shouldAddDeferredDependency(ImportSet newSet) => newSet.length <= 1;
void _processDependencies(
KClosedWorld closedWorld,
LibraryEntity library,
Dependencies dependencies,
ImportSet oldSet,
ImportSet newSet,
WorkQueue queue,
Spannable context) {
dependencies.classes.forEach((ClassEntity cls, DependencyInfo info) {
if (info.isDeferred) {
if (_shouldAddDeferredDependency(newSet)) {
for (ImportEntity deferredImport in info.imports) {
queue.addClass(cls, importSets.singleton(deferredImport));
}
}
} else {
_updateClassRecursive(closedWorld, cls, oldSet, newSet, queue);
}
});
dependencies.classType.forEach((ClassEntity cls, DependencyInfo info) {
if (info.isDeferred) {
if (_shouldAddDeferredDependency(newSet)) {
for (ImportEntity deferredImport in info.imports) {
queue.addClassType(cls, importSets.singleton(deferredImport));
}
}
} else {
_updateClassTypeRecursive(closedWorld, cls, oldSet, newSet, queue);
}
});
dependencies.members.forEach((MemberEntity member, DependencyInfo info) {
if (info.isDeferred) {
if (_shouldAddDeferredDependency(newSet)) {
for (ImportEntity deferredImport in info.imports) {
queue.addMember(member, importSets.singleton(deferredImport));
}
}
} else {
_updateMemberRecursive(closedWorld, member, oldSet, newSet, queue);
}
});
for (Local localFunction in dependencies.localFunctions) {
_updateLocalFunction(localFunction, oldSet, newSet);
}
dependencies.constants
.forEach((ConstantValue constant, DependencyInfo info) {
if (info.isDeferred) {
if (_shouldAddDeferredDependency(newSet)) {
for (ImportEntity deferredImport in info.imports) {
queue.addConstant(constant, importSets.singleton(deferredImport));
}
}
} else {
_updateConstantRecursive(closedWorld, constant, oldSet, newSet, queue);
}
});
}
/// Computes a unique string for the name field for each outputUnit.
void _createOutputUnits() {
int counter = 1;
void addUnit(ImportSet importSet) {
if (importSet.unit != null) return;
var unit = OutputUnit(false, '$counter',
importSet.collectImports().map((i) => i.declaration).toSet());
counter++;
importSet.unit = unit;
_allOutputUnits.add(unit);
metrics.outputUnitElements.add(1);
}
// Generate an output unit for all import sets that are associated with an
// element or constant.
_classToSet.values.forEach(addUnit);
_classTypeToSet.values.forEach(addUnit);
_memberToSet.values.forEach(addUnit);
_localFunctionToSet.values.forEach(addUnit);
_constantToSet.values.forEach(addUnit);
// Sort output units to make the output of the compiler more stable.
_allOutputUnits.sort();
}
void _setupImportNames() {
// If useSimpleLoadIds is true then we use a monotonically increasing number
// to generate loadIds. Otherwise, we will use the user provided names.
bool useIds = compiler.options.useSimpleLoadIds;
var allDeferredImports = _allDeferredImports.toList();
if (useIds) {
// Sort for a canonical order of [ImportEntity]s.
allDeferredImports.sort(compareImportEntities);
}
int nextDeferId = 0;
Set<String> usedImportNames = {};
for (ImportEntity import in allDeferredImports) {
String result = computeImportDeferName(import, compiler);
assert(result != null);
if (useIds) {
importDeferName[import] = (++nextDeferId).toString();
} else {
// Note: tools that process the json file to build multi-part initial load
// bundles depend on the fact that makeUnique appends only digits, or a
// period followed by digits.
importDeferName[import] = makeUnique(result, usedImportNames, '.');
}
}
}
/// Returns a name for a deferred import.
String computeImportDeferName(ImportEntity declaration, Compiler compiler) {
assert(declaration.isDeferred);
if (declaration.name != null) {
return declaration.name;
} else {
// This happens when the deferred import isn't declared with a prefix.
assert(compiler.compilationFailed);
return '';
}
}
/// Performs the deferred loading algorithm.
///
/// See the top-level library comment for details.
OutputUnitData run(FunctionEntity main, KClosedWorld closedWorld) {
return metrics.time.measure(() => _run(main, closedWorld));
}
OutputUnitData _run(FunctionEntity main, KClosedWorld closedWorld) {
if (!isProgramSplit || main == null || disableProgramSplit) {
return _buildResult();
}
work() {
var queue = WorkQueue(this.importSets);
// Add `main` and their recursive dependencies to the main output unit.
// We do this upfront to avoid wasting time visiting these elements when
// analyzing deferred imports.
queue.addMember(main, importSets.mainSet);
// Also add "global" dependencies to the main output unit. These are
// things that the backend needs but cannot associate with a particular
// element. This set also contains elements for which we lack precise
// information.
for (MemberEntity element
in closedWorld.backendUsage.globalFunctionDependencies) {
queue.addMember(element, importSets.mainSet);
}
for (ClassEntity element
in closedWorld.backendUsage.globalClassDependencies) {
queue.addClass(element, importSets.mainSet);
}
void emptyQueue() {
while (queue.isNotEmpty) {
WorkItem item = queue.nextItem();
item.update(this, closedWorld, queue);
}
}
emptyQueue();
}
reporter.withCurrentElement(main.library, () => measure(work));
// Notify that we no longer need impacts for deferred load, so they can be
// discarded at this time.
compiler.impactStrategy.onImpactUsed(DeferredLoadTask.IMPACT_USE);
return _buildResult();
}
// Dumps a graph as a list of strings of 0 and 1. There is one 'bit' for each
// import entity in the graph, and each string in the list represents an
// output unit.
void _dumpDeferredGraph() {
int id = 0;
Map<ImportEntity, int> importMap = {};
var entities = _deferredImportDescriptions.keys.toList();
entities.sort(compareImportEntities);
entities = entities.reversed.toList();
for (var key in entities) {
importMap[key] = id++;
}
List<String> graph = [];
for (var outputUnit in _allOutputUnits) {
if (!outputUnit.isMainOutput) {
List<int> representation = List.filled(id, 0);
for (var entity in outputUnit.imports) {
representation[importMap[entity]] = 1;
}
graph.add(representation.join());
}
}
compiler.outputProvider.createOutputSink(
compiler.options.deferredGraphUri.path, '', OutputType.debug)
..add(graph.join('\n'))
..close();
}
OutputUnitData _buildResult() {
_createOutputUnits();
_setupImportNames();
if (compiler.options.deferredGraphUri != null) {
_dumpDeferredGraph();
}
Map<ClassEntity, OutputUnit> classMap = {};
Map<ClassEntity, OutputUnit> classTypeMap = {};
Map<MemberEntity, OutputUnit> memberMap = {};
Map<Local, OutputUnit> localFunctionMap = {};
Map<ConstantValue, OutputUnit> constantMap = {};
_classToSet.forEach((cls, s) => classMap[cls] = s.unit);
_classTypeToSet.forEach((cls, s) => classTypeMap[cls] = s.unit);
_memberToSet.forEach((member, s) => memberMap[member] = s.unit);
_localFunctionToSet.forEach(
(localFunction, s) => localFunctionMap[localFunction] = s.unit);
_constantToSet.forEach((constant, s) => constantMap[constant] = s.unit);
_classToSet = null;
_classTypeToSet = null;
_memberToSet = null;
_localFunctionToSet = null;
_constantToSet = null;
importSets = null;
return OutputUnitData(
this.isProgramSplit && !disableProgramSplit,
this._mainOutputUnit,
classMap,
classTypeMap,
memberMap,
localFunctionMap,
constantMap,
_allOutputUnits,
importDeferName,
_deferredImportDescriptions);
}
void beforeResolution(Uri rootLibraryUri, Iterable<Uri> libraries) {
measureSubtask('prepare', () {
for (Uri uri in libraries) {
LibraryEntity library = elementEnvironment.lookupLibrary(uri);
reporter.withCurrentElement(library, () {
for (ImportEntity import in elementEnvironment.getImports(library)) {
if (import.isDeferred) {
_deferredImportDescriptions[import] =
ImportDescription(import, library, rootLibraryUri);
isProgramSplit = true;
}
}
});
}
});
}
bool ignoreEntityInDump(Entity element) => false;
/// Creates a textual representation of the output unit content.
String dump() {
Map<OutputUnit, List<String>> elementMap = {};
Map<OutputUnit, List<String>> constantMap = {};
_classToSet.forEach((ClassEntity element, ImportSet importSet) {
if (ignoreEntityInDump(element)) return;
var elements = elementMap.putIfAbsent(importSet.unit, () => <String>[]);
var id = element.name ?? '$element';
id = '$id cls';
elements.add(id);
});
_classTypeToSet.forEach((ClassEntity element, ImportSet importSet) {
if (ignoreEntityInDump(element)) return;
var elements = elementMap.putIfAbsent(importSet.unit, () => <String>[]);
var id = element.name ?? '$element';
id = '$id type';
elements.add(id);
});
_memberToSet.forEach((MemberEntity element, ImportSet importSet) {
if (ignoreEntityInDump(element)) return;
var elements = elementMap.putIfAbsent(importSet.unit, () => []);
var id = element.name ?? '$element';
var cls = element.enclosingClass?.name;
if (cls != null) id = '$cls.$id';
if (element.isSetter) id = '$id=';
id = '$id member';
elements.add(id);
});
_localFunctionToSet.forEach((Local element, ImportSet importSet) {
if (ignoreEntityInDump(element)) return;
var elements = elementMap.putIfAbsent(importSet.unit, () => []);
var id = element.name ?? '$element';
var context = (element as dynamic).memberContext.name;
id = element.name == null || element.name == '' ? '<anonymous>' : id;
id = '$context.$id';
id = '$id local';
elements.add(id);
});
_constantToSet.forEach((ConstantValue value, ImportSet importSet) {
// Skip primitive values: they are not stored in the constant tables and
// if they are shared, they end up duplicated anyways across output units.
if (value.isPrimitive) return;
constantMap
.putIfAbsent(importSet.unit, () => [])
.add(value.toStructuredText(dartTypes));
});
Map<OutputUnit, String> text = {};
for (OutputUnit outputUnit in _allOutputUnits) {
StringBuffer unitText = StringBuffer();
if (outputUnit.isMainOutput) {
unitText.write(' <MAIN UNIT>');
} else {
unitText.write(' imports:');
var imports = outputUnit.imports
.map((i) => '${i.enclosingLibraryUri.resolveUri(i.uri)}')
.toList();
for (var i in imports..sort()) {
unitText.write('\n $i:');
}
}
List<String> elements = elementMap[outputUnit];
if (elements != null) {
unitText.write('\n elements:');
for (String element in elements..sort()) {
unitText.write('\n $element');
}
}
List<String> constants = constantMap[outputUnit];
if (constants != null) {
unitText.write('\n constants:');
for (String value in constants..sort()) {
unitText.write('\n $value');
}
}
text[outputUnit] = '$unitText';
}
StringBuffer sb = StringBuffer();
for (OutputUnit outputUnit in _allOutputUnits.toList()
..sort((a, b) => text[a].compareTo(text[b]))) {
sb.write('\n\n-------------------------------\n');
sb.write('Output unit: ${outputUnit.name}');
sb.write('\n ${text[outputUnit]}');
}
return sb.toString();
}
}