blob: 0029ef2d9b555b54f01c67b3d6cc1839885219a7 [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 '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/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:kernel/kernel.dart';
import 'package:meta/meta.dart';
/// Implementation of [IncrementalKernelGenerator].
///
/// This implementation uses simplified approach to tracking dependencies.
/// When a change happens to a file, we invalidate this file, its library,
/// and then transitive closure of all libraries that reference it.
class MinimalIncrementalKernelGenerator 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.';
/// Options used by the kernel compiler.
final ProcessedOptions _options;
/// The object that knows how to resolve "package:" and "dart:" URIs.
final UriTranslator uriTranslator;
/// The logger to report compilation progress.
final PerformanceLog _logger;
/// 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;
/// A [FileSystem] or [_WatchingFileSystem] instance.
final FileSystem _fileSystem;
/// 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();
/// 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 = {};
/// Whether [computeDelta] is executing.
bool _isComputeDeltaExecuting = false;
/// The set of libraries (file system URIs) accepted by the client, and not
/// yet invalidated explicitly or implicitly via a transitive dependency.
///
/// When we produce a new delta, we put newly compiled libraries into
/// the [_lastLibraries] field, so [_currentLibraries] and [_lastLibraries]
/// don't intersect.
final Set<Uri> _currentLibraries = new Set<Uri>();
/// The set of new libraries (file system URIs) returned to the client by the
/// last [computeDelta], or `null` if the last delta was either accepted or
/// rejected.
Set<Uri> _lastLibraries;
/// The object that provides additional information for tests.
_TestView _testView;
MinimalIncrementalKernelGenerator(this._options, this.uriTranslator,
List<int> sdkOutlineBytes, this._entryPoint,
{WatchUsedFilesFn watch})
: _logger = _options.logger,
_watchFn = watch,
_fileSystem = watch == null
? _options.fileSystem
: new _WatchingFileSystem(_options.fileSystem, watch) {
_testView = new _TestView();
// Pre-populate the Program with SDK.
if (sdkOutlineBytes != null) {
loadProgramFromBytes(sdkOutlineBytes, _program);
for (var sdkLibrary in _program.libraries) {
_currentLibraries.add(sdkLibrary.fileUri);
}
}
}
/// Return the object that provides additional information for tests.
@visibleForTesting
_TestView get test => _testView;
@override
void acceptLastDelta() {
_throwIfNoLastDelta();
_currentLibraries.addAll(_lastLibraries);
_lastLibraries = null;
}
@override
Future<DeltaProgram> computeDelta() {
if (_isComputeDeltaExecuting) {
throw new StateError(MSG_PENDING_COMPUTE);
}
if (_lastLibraries != null) {
throw new StateError(MSG_HAS_LAST_DELTA);
}
_lastLibraries = new Set<Uri>();
_isComputeDeltaExecuting = true;
return _runWithFrontEndContext('Compute delta', () async {
try {
var dillTarget = new DillTarget(new Ticker(isVerbose: _options.verbose),
uriTranslator, _options.target);
// Append all libraries what we still have in the current program.
await _logger.runAsync('Load 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 into the new program.
_program = await _logger.runAsync('Compile', () async {
await kernelTarget.buildOutlines(nameRoot: _program.root);
return await kernelTarget.buildProgram() ?? _program;
});
await _unwatchFiles();
_logger.run('Compute dependencies', _computeDependencies);
// Compose the resulting program with new libraries.
var program = new Program(nameRoot: _program.root);
_testView.compiledUris.clear();
for (var library in _program.libraries) {
Uri uri = library.fileUri;
if (_currentLibraries.contains(uri)) continue;
_lastLibraries.add(uri);
_testView.compiledUris.add(library.importUri);
program.uriToSource[uri] = _program.uriToSource[uri];
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;
return new DeltaProgram('', program);
} finally {
_isComputeDeltaExecuting = false;
}
});
}
@override
void invalidate(Uri uri) {
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}");
_program.uriToSource.remove(libraryUri);
_currentLibraries.remove(libraryUri);
// Recursively invalidate dependencies.
Set<Uri> directDependencies =
_directLibraryDependencies.remove(libraryUri);
directDependencies?.forEach(invalidateLibrary);
}
Uri libraryUri = _partToLibrary.remove(uri) ?? uri;
invalidateLibrary(libraryUri);
}
@override
void rejectLastDelta() {
_throwIfNoLastDelta();
_lastLibraries = null;
}
@override
void reset() {
_currentLibraries.clear();
_lastLibraries = null;
}
@override
void setState(String state) {
// TODO(scheglov): Do we need this at all?
// If we don't know the previous state, we will give the client all
// libraries to reload. While this is suboptimal, this should not affect
// correctness.
// Note that even if we don't give the client all libraries, we will have
// to compile them all anyway, because this implementation does not use
// any persistent caching.
}
/// 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 = _getEntryPointLibrary();
processLibrary(entryPointLibrary);
}
Library _getEntryPointLibrary() =>
_program.libraries.singleWhere((lib) => lib.importUri == _entryPoint);
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 [_lastLibraries] 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 (_lastLibraries == null) {
throw new StateError(MSG_NO_LAST_DELTA);
}
}
/// Compute the set of of files transitively referenced from the entry point,
/// remove all other files from [_program], and call [_watchFn] to unwatch
/// known files that are not longer referenced.
Future<Null> _unwatchFiles() async {
var entryPointFiles = new Set<Uri>();
// Don't remove SDK libraries.
for (var library in _program.libraries) {
if (library.importUri.isScheme('dart')) {
entryPointFiles.add(library.fileUri);
for (var part in library.parts) {
entryPointFiles.add(part.fileUri);
}
}
}
void appendTransitiveFiles(Library library) {
if (entryPointFiles.add(library.fileUri)) {
for (var part in library.parts) {
entryPointFiles.add(part.fileUri);
}
for (var dependency in library.dependencies) {
appendTransitiveFiles(dependency.targetLibrary);
}
}
}
// Append files transitively referenced from the entry point.
var entryPointLibrary = _getEntryPointLibrary();
appendTransitiveFiles(entryPointLibrary);
// Remove not loaded files from the set of known files.
if (_fileSystem is _WatchingFileSystem) {
_WatchingFileSystem fileSystem = _fileSystem;
for (Uri knownUri in fileSystem.knownFiles.toList()) {
if (!entryPointFiles.contains(knownUri)) {
await _watchFn(knownUri, false);
fileSystem.knownFiles.remove(knownUri);
_program.uriToSource.remove(knownUri);
}
}
}
// Remove libraries that are no longer referenced.
_program.libraries
.removeWhere((library) => !entryPointFiles.contains(library.fileUri));
}
}
@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>();
}
/// [FileSystem] that notifies [WatchUsedFilesFn] about new files.
class _WatchingFileSystem implements FileSystem {
final FileSystem fileSystem;
final WatchUsedFilesFn watchFn;
final Set<Uri> knownFiles = new Set<Uri>();
_WatchingFileSystem(this.fileSystem, this.watchFn);
@override
FileSystemEntity entityForUri(Uri uri) {
var entity = fileSystem.entityForUri(uri);
return new _WatchingFileSystemEntity(this, entity, watchFn);
}
}
/// [FileSystemEntity] that notifies the [WatchUsedFilesFn] about new files.
class _WatchingFileSystemEntity implements FileSystemEntity {
final _WatchingFileSystem fileSystem;
final FileSystemEntity entity;
final WatchUsedFilesFn watchFn;
_WatchingFileSystemEntity(this.fileSystem, this.entity, this.watchFn);
@override
Uri get uri => entity.uri;
@override
Future<bool> exists() {
return entity.exists();
}
@override
Future<List<int>> readAsBytes() async {
if (fileSystem.knownFiles.add(uri)) {
await watchFn(uri, true);
}
return entity.readAsBytes();
}
@override
Future<String> readAsString() async {
if (fileSystem.knownFiles.add(uri)) {
await watchFn(uri, true);
}
return entity.readAsString();
}
}