| // 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/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() { |
| _options.ticker.reset(); |
| 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(_options.ticker, 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(); |
| } |
| } |