9f33810b01
Currently the 'load-ids' controls both how deferred modules are loaded and whether or not the compiler emits a deferred map JSON file. This change separates these 2 actions into 2 separate flags. 'deferred-map' now provides the URI for the JSON file. And 'use-load-ids' controls how deferred modules are loaded. This is needed for some tools that might not want to use load ids but still need to process the deferred map file. Change-Id: I6909307f058d8f97dd8987e421f693ee97d6572f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499520 Reviewed-by: Martin Kustermann <kustermann@google.com> Commit-Queue: Nate Biggs <natebiggs@google.com>
393 lines
12 KiB
Dart
393 lines
12 KiB
Dart
// Copyright (c) 2024, 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 'dart:convert' show JsonEncoder;
|
|
import 'dart:io';
|
|
|
|
import 'package:_fe_analyzer_shared/src/util/relativize.dart'
|
|
show relativizeUri;
|
|
import 'package:compiler/src/deferred_load/program_split_constraints/nodes.dart';
|
|
import 'package:compiler/src/deferred_load/program_split_constraints/parser.dart';
|
|
import 'package:kernel/ast.dart';
|
|
import 'package:kernel/class_hierarchy.dart';
|
|
import 'package:kernel/core_types.dart';
|
|
|
|
import 'await_transformer.dart' as await_transformer;
|
|
import 'compiler_options.dart';
|
|
import 'deferred_load/partition.dart';
|
|
import 'io_util.dart';
|
|
import 'modules.dart';
|
|
import 'target.dart';
|
|
import 'util.dart' show addPragma, getPragma;
|
|
|
|
class DeferredLoadingModuleStrategy extends ModuleStrategy {
|
|
final Component component;
|
|
final WasmCompilerOptions options;
|
|
final WasmTarget kernelTarget;
|
|
final CoreTypes coreTypes;
|
|
final CompilerPhaseInputOutputManager ioManager;
|
|
late final ModuleOutputData moduleOutputData;
|
|
|
|
DeferredLoadingModuleStrategy(
|
|
this.component,
|
|
this.options,
|
|
this.kernelTarget,
|
|
this.coreTypes,
|
|
this.ioManager,
|
|
);
|
|
|
|
@override
|
|
void addEntryPoints() {}
|
|
|
|
@override
|
|
void prepareComponent() {}
|
|
|
|
@override
|
|
Future<void> processComponentAfterTfa(
|
|
DeferredModuleLoadingMap loadingMap,
|
|
) async {
|
|
ConstraintData? constraints;
|
|
if (options.programSplitConstraintsUri != null) {
|
|
final json = await ioManager.readString(
|
|
options.programSplitConstraintsUri!,
|
|
);
|
|
constraints = Parser().read(json);
|
|
}
|
|
|
|
final partition = partitionAppplication(
|
|
coreTypes,
|
|
component,
|
|
options.translatorOptions.enableAsserts,
|
|
loadingMap,
|
|
findWasmRoots(coreTypes, component),
|
|
constraints: constraints,
|
|
);
|
|
|
|
final builder = ModuleMetadataBuilder(options);
|
|
final moduleMetadata = <Part, ModuleMetadata>{};
|
|
for (final part in partition.parts) {
|
|
moduleMetadata[part] = builder.buildModuleMetadata();
|
|
}
|
|
final referenceToModuleMetadata = <Reference, ModuleMetadata>{};
|
|
partition.referenceToPart.forEach((reference, output) {
|
|
referenceToModuleMetadata[reference] = moduleMetadata[output]!;
|
|
});
|
|
final constantToModuleMetadata = <Constant, ModuleMetadata>{};
|
|
partition.constantToPart.forEach((constant, output) {
|
|
constantToModuleMetadata[constant] = moduleMetadata[output]!;
|
|
});
|
|
partition.deferredImportToParts.forEach((deferredImport, parts) {
|
|
final wasmModules = [for (final o in parts) moduleMetadata[o]!];
|
|
loadingMap.addModuleToLibraryImport(
|
|
deferredImport.enclosingLibrary,
|
|
deferredImport.name!,
|
|
wasmModules,
|
|
);
|
|
});
|
|
|
|
// Some elements may not have gotten a module assigned in the above
|
|
// procedure. This can have a varity of reasons:
|
|
//
|
|
// - A class that's never really used but still in the AST because TFA
|
|
// left it there (this happens occasionally because we enable RTA before
|
|
// TFA, RTA is less precised and may mark a class as allocated but TFA
|
|
// later on optimizes usages away which leave the class as non-abstract
|
|
// but unused).
|
|
// - A class is only used in type expressions
|
|
// - ...
|
|
//
|
|
// The code generator still requires every library to have a corresponding
|
|
// module, so we make an artificial one here.
|
|
final dummyModule = builder.buildModuleMetadata();
|
|
|
|
moduleOutputData = ModuleOutputData.fineGrainedSplit(
|
|
[...moduleMetadata.values, dummyModule],
|
|
referenceToModuleMetadata,
|
|
constantToModuleMetadata,
|
|
dummyModule,
|
|
);
|
|
}
|
|
|
|
@override
|
|
ModuleOutputData buildModuleOutputData() => moduleOutputData;
|
|
}
|
|
|
|
class StressTestModuleStrategy extends ModuleStrategy {
|
|
final Component component;
|
|
final CoreTypes coreTypes;
|
|
final WasmTarget kernelTarget;
|
|
final ClassHierarchy classHierarchy;
|
|
final WasmCompilerOptions options;
|
|
late final ModuleOutputData moduleOutputData;
|
|
|
|
/// We load all 'dart:*' libraries since just doing the deferred load of modules
|
|
/// requires a significant portion of the SDK libraries.
|
|
late final Set<Library> _testModeMainLibraries = {
|
|
...component.libraries.where(
|
|
(l) => l.importUri.scheme == 'dart' || containsWasmExport(coreTypes, l),
|
|
),
|
|
};
|
|
|
|
StressTestModuleStrategy(
|
|
this.component,
|
|
this.coreTypes,
|
|
this.options,
|
|
this.kernelTarget,
|
|
this.classHierarchy,
|
|
);
|
|
|
|
@override
|
|
void addEntryPoints() {}
|
|
|
|
/// Augments the `_invokeMain` JS->WASM entry point with test mode setup.
|
|
///
|
|
/// Choosing to augment `_invokeMain` allows us to defer the user-defined
|
|
/// `main` into a second module ensuring that we always have at least 2
|
|
/// modules in test mode.
|
|
@override
|
|
void prepareComponent() {
|
|
final initLibraries = _testModeMainLibraries;
|
|
final internalLib = coreTypes.index.getLibrary('dart:_internal');
|
|
final invokeMain = coreTypes.index.getTopLevelProcedure(
|
|
'dart:_internal',
|
|
'_invokeMain',
|
|
);
|
|
|
|
final loadStatements = <Statement>[];
|
|
for (final library in getReachableLibraries(
|
|
component.mainMethod!.enclosingLibrary,
|
|
coreTypes,
|
|
kernelTarget,
|
|
)) {
|
|
if (initLibraries.contains(library)) continue;
|
|
final import = LibraryDependency.deferredImport(
|
|
library,
|
|
'${library.importUri}',
|
|
);
|
|
internalLib.addDependency(import);
|
|
loadStatements.add(
|
|
ExpressionStatement(AwaitExpression(LoadLibrary(import))),
|
|
);
|
|
}
|
|
|
|
invokeMain.function.asyncMarker = AsyncMarker.Async;
|
|
invokeMain.function.emittedValueType = const VoidType();
|
|
|
|
final oldBody = invokeMain.function.body!;
|
|
|
|
// Add print of 'unittest-suite-wait-for-done' to indicate to test harnesses
|
|
// that the test contains async work. Any test must therefore also include a
|
|
// concluding 'unittest-suite-done' message. Usually via calls to
|
|
// `asyncStart` and `asyncEnd` helpers.
|
|
final asyncStart = ExpressionStatement(
|
|
StaticInvocation(
|
|
coreTypes.printProcedure,
|
|
Arguments([StringLiteral('unittest-suite-wait-for-done')]),
|
|
),
|
|
);
|
|
invokeMain.function.body = Block([asyncStart, ...loadStatements, oldBody]);
|
|
|
|
// The await transformer runs modularly before this transform so we need to
|
|
// rerun it on the transformed `_invokeMain` method.
|
|
await_transformer.transformLibraries(
|
|
[invokeMain.enclosingLibrary],
|
|
classHierarchy,
|
|
coreTypes,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> processComponentAfterTfa(
|
|
DeferredModuleLoadingMap loadingMap,
|
|
) async {
|
|
final moduleBuilder = ModuleMetadataBuilder(options);
|
|
final mainModule = moduleBuilder.buildModuleMetadata();
|
|
final initLibraries = _testModeMainLibraries;
|
|
final modules = <ModuleMetadata>[];
|
|
final importMap = <String, List<ModuleMetadata>>{};
|
|
|
|
final internalLib = coreTypes.index.getLibrary('dart:_internal');
|
|
|
|
// Put each library in a separate module.
|
|
final libraryMap = <Library, ModuleMetadata>{};
|
|
for (final library in component.libraries) {
|
|
if (initLibraries.contains(library)) {
|
|
libraryMap[library] = mainModule;
|
|
continue;
|
|
}
|
|
final module = moduleBuilder.buildModuleMetadata();
|
|
modules.add(module);
|
|
libraryMap[library] = module;
|
|
final importName = '${library.importUri}';
|
|
importMap[importName] = [module];
|
|
loadingMap.addModuleToLibraryImport(internalLib, importName, [module]);
|
|
}
|
|
|
|
moduleOutputData = ModuleOutputData.librarySplit(
|
|
[mainModule, ...modules],
|
|
libraryMap,
|
|
null,
|
|
);
|
|
}
|
|
|
|
@override
|
|
ModuleOutputData buildModuleOutputData() => moduleOutputData;
|
|
}
|
|
|
|
Future<void> writeDeferredMapFile(
|
|
Component component,
|
|
CoreTypes coreTypes,
|
|
WasmCompilerOptions options,
|
|
DeferredModuleLoadingMap loadingMap,
|
|
) async {
|
|
final file = File.fromUri(options.deferredMapUri!);
|
|
await file.create(recursive: true);
|
|
await file.writeAsString(
|
|
_generateDeferredMapJson(
|
|
component,
|
|
component.mainMethod!.enclosingLibrary.importUri,
|
|
loadingMap,
|
|
),
|
|
);
|
|
}
|
|
|
|
String _generateDeferredMapJson(
|
|
Component component,
|
|
Uri rootLibraryUri,
|
|
DeferredModuleLoadingMap loadingMap,
|
|
) {
|
|
final output = <String, dynamic>{};
|
|
loadingMap.loadIds.forEach((tuple, loadId) {
|
|
final modules = loadingMap.moduleMap[loadId];
|
|
final (library, prefix) = tuple;
|
|
final libOutput =
|
|
output[relativizeUri(rootLibraryUri, library.importUri, false)] ??= {
|
|
'name': library.name ?? '<unnamed>',
|
|
'imports': <String, List<String>>{},
|
|
'importPrefixToLoadId': <String, String>{},
|
|
};
|
|
// For consistency with dart2js we use 1-based indexing in the generated
|
|
// json file.
|
|
final dart2jsLoadId = loadId + 1;
|
|
final dart2jsLoadIdStr = dart2jsLoadId.toString();
|
|
libOutput['imports']![dart2jsLoadIdStr] = modules
|
|
.map((m) => m.moduleName)
|
|
.toList();
|
|
libOutput['importPrefixToLoadId'][prefix] = dart2jsLoadIdStr;
|
|
});
|
|
|
|
return const JsonEncoder.withIndent(' ').convert(output);
|
|
}
|
|
|
|
class DeferredLoadingLowering extends Transformer {
|
|
final CoreTypes coreTypes;
|
|
final DeferredModuleLoadingMap loadingMap;
|
|
|
|
// These will only exist if the [Component] has actual deferred libraries. So
|
|
// access them lazily.
|
|
late final Procedure _loadLibraryFromLoadId = coreTypes.index
|
|
.getTopLevelProcedure('dart:_internal', 'loadLibraryFromLoadId');
|
|
late final Procedure _checkLibraryIsLoadedFromLoadId = coreTypes.index
|
|
.getTopLevelProcedure('dart:_internal', 'checkLibraryIsLoadedFromLoadId');
|
|
|
|
Map<LibraryDependency, int> _libraryLoadIds = {};
|
|
|
|
DeferredLoadingLowering(this.coreTypes, this.loadingMap);
|
|
|
|
static void markRuntimeFunctionsAsEntrypoints(CoreTypes coreTypes) {
|
|
addEntryPointPragma(
|
|
coreTypes,
|
|
coreTypes.index.getTopLevelProcedure(
|
|
'dart:_internal',
|
|
'loadLibraryFromLoadId',
|
|
),
|
|
);
|
|
addEntryPointPragma(
|
|
coreTypes,
|
|
coreTypes.index.getTopLevelProcedure(
|
|
'dart:_internal',
|
|
'checkLibraryIsLoadedFromLoadId',
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitComponent(Component node) {
|
|
// Assign a load ID to each deferred import.
|
|
_libraryLoadIds = {};
|
|
for (final library in node.libraries) {
|
|
for (final dep in library.dependencies) {
|
|
if (!dep.isDeferred) continue;
|
|
final name = dep.name!;
|
|
_libraryLoadIds[dep] = loadingMap.loadIds[(library, name)]!;
|
|
}
|
|
}
|
|
|
|
return super.visitComponent(node);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitLoadLibrary(LoadLibrary node) {
|
|
final loadId = _libraryLoadIds[node.import]!;
|
|
return StaticInvocation(
|
|
_loadLibraryFromLoadId,
|
|
Arguments([IntLiteral(loadId)]),
|
|
);
|
|
}
|
|
|
|
@override
|
|
TreeNode visitCheckLibraryIsLoaded(CheckLibraryIsLoaded node) {
|
|
final loadId = _libraryLoadIds[node.import]!;
|
|
return StaticInvocation(
|
|
_checkLibraryIsLoadedFromLoadId,
|
|
Arguments([IntLiteral(loadId)]),
|
|
);
|
|
}
|
|
|
|
static void addEntryPointPragma(CoreTypes coreTypes, Annotatable node) {
|
|
addPragma(node, 'wasm:entry-point', coreTypes, value: BoolConstant(true));
|
|
}
|
|
}
|
|
|
|
Set<Reference> findWasmRoots(CoreTypes coreTypes, Component component) {
|
|
final exports = <Reference>{};
|
|
final trueConstant = BoolConstant(true);
|
|
|
|
bool check(Annotatable node) {
|
|
if (getPragma<StringConstant>(coreTypes, node, 'wasm:export') != null ||
|
|
getPragma<Constant>(
|
|
coreTypes,
|
|
node,
|
|
'wasm:entry-point',
|
|
defaultValue: trueConstant,
|
|
) !=
|
|
null) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
for (final library in component.libraries) {
|
|
for (final member in library.members) {
|
|
if (check(member)) exports.add(member.reference);
|
|
}
|
|
for (final klass in library.classes) {
|
|
if (check(klass)) exports.add(klass.reference);
|
|
for (final member in klass.members) {
|
|
if (check(member)) {
|
|
if (member is Field) {
|
|
exports.add(member.getterReference);
|
|
if (member.hasSetter) {
|
|
exports.add(member.setterReference!);
|
|
}
|
|
} else {
|
|
exports.add(member.reference);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return exports;
|
|
}
|