blob: 8c5a5b638189661d43b09f1dd8796f84c29e3557 [file] [log] [blame]
// Copyright (c) 2017, 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:async';
import 'dart:convert';
import 'package:front_end/src/api_prototype/byte_store.dart';
import 'package:front_end/src/api_prototype/file_system.dart';
import 'package:front_end/src/api_prototype/incremental_kernel_generator.dart';
import 'package:front_end/src/base/api_signature.dart';
import 'package:front_end/src/base/performance_logger.dart';
import 'package:front_end/src/base/processed_options.dart';
import 'package:front_end/src/fasta/compiler_context.dart';
import 'package:front_end/src/fasta/dill/dill_target.dart';
import 'package:front_end/src/fasta/kernel/kernel_target.dart';
import 'package:front_end/src/fasta/ticker.dart';
import 'package:front_end/src/fasta/uri_translator.dart';
import 'package:front_end/src/incremental/file_state.dart';
import 'package:front_end/src/incremental/reference_index.dart';
import 'package:kernel/kernel.dart';
import 'package:meta/meta.dart';
/// Implementation of [IncrementalKernelGenerator].
///
/// The initial compilation of the entry point is performed not incrementally.
///
/// Each file that is transitively referenced from the entry point is read,
/// its API signature is computed. Then full compilation is performed, without
/// any incrementality, to get the initial program. When a file is invalidated,
/// it is read again, and its API signature is recomputed. If the API signature
/// is the same as it was before, then only the library of the file is
/// recompiled, and the current program is updated. If the API signature of
/// a file is different, all libraries that transitively use the changed file
/// are removed from the current program, and recompiled using the remaining
/// libraries.
class IncrementalKernelGeneratorImpl implements IncrementalKernelGenerator {
static const MSG_PENDING_COMPUTE =
'A computeDelta() invocation is still executing.';
static const MSG_NO_LAST_DELTA =
'The last delta has been already accepted or rejected.';
static const MSG_HAS_LAST_DELTA =
'The last delta must be either accepted or rejected.';
/// The version of data format, should be incremented on every format change.
static const int DATA_VERSION = 1;
/// Options used by the kernel compiler.
final ProcessedOptions options;
/// The optional SDK outline as a serialized program.
/// If provided, the driver will not attempt to read SDK files.
final List<int> _sdkOutlineBytes;
/// The [FileSystem] which should be used by the front end to access files.
final FileSystem _fileSystem;
/// The logger to report compilation progress.
final PerformanceLog _logger;
/// The [ByteStore] used to cache results.
final ByteStore _byteStore;
/// The object that knows how to resolve "package:" and "dart:" URIs.
final UriTranslator uriTranslator;
/// The URI of the program entry point.
final Uri _entryPoint;
/// The function to notify when files become used or unused, or `null`.
final WatchUsedFilesFn _watchFn;
/// The salt to mix into all hashes used as keys for serialized data.
List<int> _salt;
/// The current file system state.
FileSystemState _fsState;
/// The list of absolute file URIs that were reported through [invalidate]
/// and not checked for actual changes yet.
List<Uri> _invalidatedFiles = [];
/// The set of libraries for which the content of the library file, or
/// one of its parts, changed using [invalidate], and during
/// [_refreshInvalidatedFiles] it was found that that changes are only in
/// method bodies.
///
/// If any library had an API change, this set will be empty.
final Set<FileState> _changedLibrariesWithSameApi = new Set<FileState>();
/// The [Program] with currently valid libraries. When a file is invalidated,
/// we remove the file, its library, and everything affected from [_program].
Program _program = new Program();
/// The [DillTarget] that represents the current [Program] state.
DillTarget _dillTarget;
/// Each key is the file system URI of a library.
/// Each value is the libraries that directly depend on the key library.
Map<Uri, Set<Uri>> _directLibraryDependencies = {};
/// Each key is the file system URI of a library.
/// Each value is the [Library] that is still in the [_program].
Map<Uri, Library> _uriToLibrary = {};
/// Each key is the file system URI of a part.
/// Each value is the file system URI of the library that sources the part.
Map<Uri, Uri> _partToLibrary = {};
/// The index that keeps track of references and nodes that use them,
/// and allows fast reference replacement on a single library compilation.
final ReferenceIndex _referenceIndex = new ReferenceIndex();
/// Whether [computeDelta] is executing.
bool _isComputeDeltaExecuting = false;
/// The current signatures for libraries.
final Map<Uri, String> _currentSignatures = {};
/// The signatures for libraries produced by the last [computeDelta], or
/// `null` if the last delta was either accepted or rejected.
Map<Uri, String> _lastSignatures;
/// The object that provides additional information for tests.
final _TestView _testView = new _TestView();
IncrementalKernelGeneratorImpl(this.options, this.uriTranslator,
List<int> sdkOutlineBytes, this._entryPoint,
{WatchUsedFilesFn watch})
: _sdkOutlineBytes = sdkOutlineBytes,
_fileSystem = options.fileSystem,
_logger = options.logger,
_byteStore = options.byteStore,
_watchFn = watch {
_computeSalt();
Future<Null> onFileAdded(Uri uri) {
if (_watchFn != null) {
return _watchFn(uri, true);
}
return new Future.value();
}
_fsState = new FileSystemState(_byteStore, _fileSystem, options.target,
uriTranslator, _salt, onFileAdded);
// Pre-populate the Program with SDK.
_loadSdkOutline();
_createDillTarget();
}
/// Return the object that provides additional information for tests.
@visibleForTesting
_TestView get test => _testView;
@override
void acceptLastDelta() {
_throwIfNoLastDelta();
_currentSignatures.addAll(_lastSignatures);
_lastSignatures = null;
}
@override
Future<DeltaProgram> computeDelta() {
if (_isComputeDeltaExecuting) {
throw new StateError(MSG_PENDING_COMPUTE);
}
if (_lastSignatures != null) {
throw new StateError(MSG_HAS_LAST_DELTA);
}
_lastSignatures = {};
_isComputeDeltaExecuting = true;
return _runWithFrontEndContext('Compute delta', () async {
try {
await _refreshInvalidatedFiles();
_testView.compiledUris.clear();
// Ensure that the graph starting at the entry point is ready.
await _logger.runAsync('Build graph of files', () async {
return await _fsState.getFile(_entryPoint);
});
// The file graph might have changed, perform GC.
await _gc();
// Compile just libraries with changes to function bodies, or
// compile multiple libraries because of API changes.
if (_changedLibrariesWithSameApi.isNotEmpty) {
await _logger.runAsync('Compile libraries with body changes',
() async {
await _compileLibrariesWithBodyChanges();
});
} else {
_createDillTarget();
// Append all libraries what we still have in the current program.
Set<Uri> validLibraries =
_program.libraries.map((library) => library.importUri).toSet();
var validDillCount = validLibraries.length;
await _logger.runAsync('Load $validDillCount dill libraries',
() async {
_dillTarget.loader.appendLibraries(_program);
await _dillTarget.buildOutlines();
});
// Configure KernelTarget to compile the entry point.
var kernelTarget =
new KernelTarget(_fileSystem, false, _dillTarget, uriTranslator);
kernelTarget.read(_entryPoint);
// Compile the entry point.
await _logger.runAsync('Compile', () async {
await kernelTarget.buildOutlines(nameRoot: _program.root);
_program = await kernelTarget.buildProgram() ?? _program;
});
_program.computeCanonicalNames();
_logger.run('Compute dependencies', _computeDependencies);
// Append new libraries to the DillTarget.
int newDillCount = _program.libraries.length - validDillCount;
await _logger.runAsync('Append $newDillCount dill libraries',
() async {
_dillTarget.loader.appendLibraries(_program,
filter: (uri) => !validLibraries.contains(uri));
await _dillTarget.buildOutlines();
});
}
_logger.run('Index references', () {
_referenceIndex.indexNewLibraries(_program);
});
// Prepare libraries that changed relatively to the current state.
var changedLibraries = new Set<Uri>();
for (var library in _program.libraries) {
var uri = library.importUri;
var file = _fsState.getFileOrNull(uri);
if (file != null && _currentSignatures[uri] != file.signatureStr) {
changedLibraries.add(library.fileUri);
_lastSignatures[uri] = file.signatureStr;
_testView.compiledUris.add(uri);
}
}
// The set of affected library cycles (have different signatures),
// or libraries that import or export affected libraries (so VM might
// have inlined some code from affected libraries into them).
final vmRequiredLibraries = new Set<Uri>();
void gatherVmRequiredLibraries(Uri libraryUri) {
if (vmRequiredLibraries.add(libraryUri)) {
var directUsers = _directLibraryDependencies[libraryUri];
directUsers?.forEach(gatherVmRequiredLibraries);
}
}
changedLibraries.forEach(gatherVmRequiredLibraries);
// Compose the resulting program with new libraries.
var program = new Program(nameRoot: _program.root);
for (var library in _program.libraries) {
if (_sdkOutlineBytes != null && library.importUri.isScheme('dart')) {
continue;
}
if (vmRequiredLibraries.contains(library.fileUri)) {
program.uriToSource[library.fileUri] =
_program.uriToSource[library.fileUri];
for (var part in library.parts) {
program.uriToSource[part.fileUri] =
_program.uriToSource[part.fileUri];
}
program.libraries.add(library);
library.parent = program;
}
}
program.mainMethod = _program.mainMethod;
_logger.writeln('Returning ${program.libraries.length} libraries.');
_logger.writeln('There are ${_dillTarget.loader.libraries.length} '
'libraries in DillTarget.');
var stateString = _ExternalState.asString(_lastSignatures);
return new DeltaProgram(stateString, program);
} finally {
_isComputeDeltaExecuting = false;
}
});
}
@override
void invalidate(Uri uri) {
_invalidatedFiles.add(uri);
}
@override
void rejectLastDelta() {
_throwIfNoLastDelta();
_lastSignatures = null;
}
@override
void reset() {
_currentSignatures.clear();
_lastSignatures = null;
}
@override
void setState(String state) {
if (_isComputeDeltaExecuting) {
throw new StateError(MSG_PENDING_COMPUTE);
}
var signatures = _ExternalState.fromString(state);
_currentSignatures.clear();
_currentSignatures.addAll(signatures);
}
/// The [_program] is almost valid, there are [_changedLibrariesWithSameApi]
/// which should be recompiled, but all other libraries are fine.
///
/// Compile the changed libraries and update referenced in other libraries.
Future<Null> _compileLibrariesWithBodyChanges() async {
if (_changedLibrariesWithSameApi.isNotEmpty) {
var kernelTarget =
new KernelTarget(_fileSystem, false, _dillTarget, uriTranslator);
// Schedule URIs of changed libraries for compilation.
for (var changedLibrary in _changedLibrariesWithSameApi) {
_testView.compiledUris.add(changedLibrary.uri);
// Detach the old library.
var oldLibrary = _uriToLibrary[changedLibrary.fileUri];
_program.root.removeChild(changedLibrary.uriStr);
_program.libraries.remove(oldLibrary);
_dillTarget.loader.builders.remove(changedLibrary.uri);
_dillTarget.loader.libraries.remove(oldLibrary);
_referenceIndex.removeLibrary(oldLibrary);
// Schedule the library for compilation.
kernelTarget.read(changedLibrary.uri);
}
var mainReference = _program.mainMethodName;
await _logger.runAsync('Compile', () async {
await kernelTarget.buildOutlines(nameRoot: _program.root);
await kernelTarget.buildProgram();
});
// Attach the new library and replace references.
_logger.run('Replace references', () {
var builders = kernelTarget.loader.builders;
for (var changedLibrary in _changedLibrariesWithSameApi) {
Library oldLibrary = _uriToLibrary[changedLibrary.fileUri];
Library newLibrary = builders[changedLibrary.uri].target;
_program.root
.getChildFromUri(newLibrary.importUri)
.bindTo(newLibrary.reference);
newLibrary.computeCanonicalNames();
_program.root.adoptChild(newLibrary.canonicalName);
_program.libraries.add(newLibrary);
_uriToLibrary[changedLibrary.fileUri] = newLibrary;
_referenceIndex.replaceLibrary(oldLibrary, newLibrary);
// Schedule the new outline for loading.
// TODO(scheglov): Add a more efficient API to add one library.
_dillTarget.loader.appendLibraries(_program,
filter: (uri) => uri == newLibrary.importUri);
// If main() was defined in the recompiled library, replace it.
if (mainReference?.asProcedure?.enclosingLibrary == oldLibrary) {
mainReference = newLibrary.procedures
.singleWhere((p) => p.name.name == 'main')
.reference;
}
}
});
// Load outlines of replaced libraries.
await _dillTarget.buildOutlines();
// Restore the main() procedure reference.
_program.mainMethodName = mainReference;
}
}
/// Recompute [_directLibraryDependencies] for the current [_program].
void _computeDependencies() {
_directLibraryDependencies.clear();
_uriToLibrary.clear();
_partToLibrary.clear();
var processedLibraries = new Set<Library>();
void processLibrary(Library library) {
if (!processedLibraries.add(library)) return;
_uriToLibrary[library.fileUri] = library;
// Remember libraries for parts.
for (var part in library.parts) {
_partToLibrary[part.fileUri] = library.fileUri;
}
// Record reverse dependencies.
for (LibraryDependency dependency in library.dependencies) {
Library targetLibrary = dependency.targetLibrary;
_directLibraryDependencies
.putIfAbsent(targetLibrary.fileUri, () => new Set<Uri>())
.add(library.fileUri);
processLibrary(targetLibrary);
}
}
var entryPointLibrary =
_program.libraries.singleWhere((lib) => lib.importUri == _entryPoint);
processLibrary(entryPointLibrary);
}
/// Compute salt and put into [_salt].
void _computeSalt() {
var saltBuilder = new ApiSignature();
saltBuilder.addInt(DATA_VERSION);
saltBuilder.addBool(options.strongMode);
if (_sdkOutlineBytes != null) {
saltBuilder.addBytes(_sdkOutlineBytes);
}
_salt = saltBuilder.toByteList();
}
/// Create a new, empty [_dillTarget].
void _createDillTarget() {
_dillTarget = new DillTarget(
new Ticker(isVerbose: false), uriTranslator, options.target);
}
/// Find files which are not referenced from the entry point and report
/// them to the watch function.
Future<Null> _gc() async {
List<FileState> removedFiles = _fsState.gc(_entryPoint);
if (removedFiles.isNotEmpty && _watchFn != null) {
for (var removedFile in removedFiles) {
// If a library, remove it from the program.
Library library = _uriToLibrary.remove(removedFile.fileUri);
if (library != null) {
_currentSignatures.remove(library.importUri);
_program.libraries.remove(library);
_program.root.removeChild(library.importUri.toString());
_program.uriToSource.remove(library.fileUri);
for (var part in library.parts) {
_program.uriToSource.remove(part.fileUri);
}
}
// Notify the client.
await _watchFn(removedFile.fileUri, false);
}
}
}
/// If SDK outline bytes are provided, load it and configure the file system
/// state to skip SDK library files.
void _loadSdkOutline() {
if (_sdkOutlineBytes != null) {
_logger.run('Load SDK outline from bytes', () {
loadProgramFromBytes(_sdkOutlineBytes, _program);
// Configure the file system state to skip the outline libraries.
for (var outlineLibrary in _program.libraries) {
_fsState.skipSdkLibraries.add(outlineLibrary.importUri);
}
});
}
}
/// Refresh all the invalidated files and update dependencies.
Future<Null> _refreshInvalidatedFiles() async {
await _logger.runAsync('Refresh invalidated files', () async {
// Replace the list to avoid concurrent modifications.
List<Uri> invalidatedFiles = _invalidatedFiles;
_invalidatedFiles = <Uri>[];
// Refresh the files.
_changedLibrariesWithSameApi.clear();
var filesWithDifferentApiSignature = <FileState>[];
for (var fileUri in invalidatedFiles) {
var file = _fsState.getFileByFileUri(fileUri);
if (file != null) {
_logger.writeln('Refresh $fileUri');
bool apiSignatureChanged = await file.refresh();
if (apiSignatureChanged) {
filesWithDifferentApiSignature.add(file);
} else {
FileState libraryFile = file;
Uri libraryFileUri = _partToLibrary[file.fileUri];
if (libraryFileUri != null) {
libraryFile = _fsState.getFileByFileUri(libraryFileUri);
}
_changedLibrariesWithSameApi.add(libraryFile);
}
}
}
if (filesWithDifferentApiSignature.isNotEmpty) {
_logger.writeln('API changed in $filesWithDifferentApiSignature.');
_changedLibrariesWithSameApi.clear();
/// Invalidate the library with the given [libraryUri],
/// and recursively all its clients.
void invalidateLibrary(Uri libraryUri) {
Library library = _uriToLibrary.remove(libraryUri);
if (library == null) return;
// Invalidate the library.
_program.libraries.remove(library);
_program.root.removeChild(library.importUri.toString());
_program.uriToSource.remove(libraryUri);
_currentSignatures.remove(library.importUri);
_referenceIndex.removeLibrary(library);
// Recursively invalidate clients.
Set<Uri> directDependencies =
_directLibraryDependencies.remove(libraryUri);
directDependencies?.forEach(invalidateLibrary);
}
// TODO(scheglov): Some changes still might be incremental.
for (var uri in invalidatedFiles) {
Uri libraryUri = _partToLibrary.remove(uri) ?? uri;
invalidateLibrary(libraryUri);
}
}
});
}
Future<T> _runWithFrontEndContext<T>(String msg, Future<T> f()) async {
return await CompilerContext.runWithOptions(options, (context) {
context.disableColors();
return _logger.runAsync(msg, f);
});
}
/// Throw [StateError] if [_lastSignatures] is `null`, i.e. there is no
/// last delta - it either has not been computed yet, or has been already
/// accepted or rejected.
void _throwIfNoLastDelta() {
if (_isComputeDeltaExecuting) {
throw new StateError(MSG_PENDING_COMPUTE);
}
if (_lastSignatures == null) {
throw new StateError(MSG_NO_LAST_DELTA);
}
}
}
class _ExternalState {
/// Return the JSON encoding of the [signatures].
static String asString(Map<Uri, String> signatures) {
var json = <String, String>{};
signatures.forEach((uri, signature) {
json[uri.toString()] = signature;
});
return JSON.encode(json);
}
/// Decode the given JSON [state] into the program state.
static Map<Uri, String> fromString(String state) {
var signatures = <Uri, String>{};
Map<String, String> json = JSON.decode(state);
json.forEach((uriStr, signature) {
var uri = Uri.parse(uriStr);
signatures[uri] = signature;
});
return signatures;
}
}
@visibleForTesting
class _TestView {
/// The list of [Uri]s compiled for the last delta.
/// It does not include libraries which were reused from the last program.
final Set<Uri> compiledUris = new Set<Uri>();
}