blob: e86a812166eeae925366814a04caaf4d138f272d [file] [log] [blame]
// Copyright (c) 2020, 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';
import 'package:async/async.dart';
import 'package:dwds/src/debugging/metadata/module_metadata.dart';
import 'package:dwds/src/readers/asset_reader.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
/// A provider of metadata in which data is collected through DDC outputs.
class MetadataProvider {
final AssetReader _assetReader;
final _logger = Logger('MetadataProvider');
final String entrypoint;
final Set<String> _libraries = {};
final Map<String, String> _scriptToModule = {};
final Map<String, String> _moduleToSourceMap = {};
final Map<String, String> _modulePathToModule = {};
final Map<String, String> _moduleToModulePath = {};
final Map<String, Set<String>> _moduleToLibraries = {};
final Map<String, List<String>> _scripts = {};
final _metadataMemoizer = AsyncMemoizer();
/// Implicitly imported libraries in any DDC component.
///
/// Currently dart_sdk module does not come with the metadata.
/// To allow evaluation of expressions that use libraries and
/// types from the SDK (such as a dart Type object), add the
/// metadata for dart_sdk manually.
///
/// TODO: Generate sdk module metadata to be consumed by debugger.
/// Issue: https://github.com/dart-lang/sdk/issues/45477
List<String> get sdkLibraries => const [
'dart:_runtime',
'dart:_debugger',
'dart:_foreign_helper',
'dart:_interceptors',
'dart:_internal',
'dart:_isolate_helper',
'dart:_js_helper',
'dart:_js_primitives',
'dart:_metadata',
'dart:_native_typed_data',
'dart:_rti',
'dart:async',
'dart:collection',
'dart:convert',
'dart:core',
'dart:developer',
'dart:io',
'dart:isolate',
'dart:js',
'dart:js_util',
'dart:math',
'dart:typed_data',
'dart:indexed_db',
'dart:html',
'dart:html_common',
'dart:svg',
'dart:web_audio',
'dart:web_gl',
'dart:ui',
];
MetadataProvider(this.entrypoint, this._assetReader);
/// A list of all libraries in the Dart application.
///
/// Example:
///
/// [
/// dart:web_gl,
/// dart:math,
/// org-dartlang-app:///web/main.dart
/// ]
///
Future<List<String>> get libraries async {
await _initialize();
return _libraries.toList();
}
/// A map of library uri to dart scripts.
///
/// Example:
///
/// {
/// org-dartlang-app:///web/main.dart :
/// { web/main.dart }
/// }
///
Future<Map<String, List<String>>> get scripts async {
await _initialize();
return _scripts;
}
/// A map of script to containing module.
///
/// Example:
///
/// {
/// org-dartlang-app:///web/main.dart :
/// web/main
/// }
Future<Map<String, String>> get scriptToModule async {
await _initialize();
return _scriptToModule;
}
/// A map of module name to source map path.
///
/// Example:
///
/// {
/// org-dartlang-app:///web/main.dart :
/// web/main.ddc.js.map
/// }
Future<Map<String, String>> get moduleToSourceMap async {
await _initialize();
return _moduleToSourceMap;
}
/// A map of module path to module name.
///
/// Example:
///
/// {
/// web/main.ddc.js :
/// web/main
/// }
Future<Map<String, String>> get modulePathToModule async {
await _initialize();
return _modulePathToModule;
}
/// A map of module to module path.
///
/// Example:
///
/// {
/// web/main
/// web/main.ddc.js :
/// }
Future<Map<String, String>> get moduleToModulePath async {
await _initialize();
return _moduleToModulePath;
}
/// A list of module ids.
///
/// Example:
///
/// [
/// web/main,
/// web/foo/bar
/// ]
Future<List<String>> get modules async {
await _initialize();
return _moduleToModulePath.keys.toList();
}
/// Compute metadata information after reading the metadata contents and
/// return a map from module names to their [ModuleMetadata].
Future<Map<String, ModuleMetadata>> _processMetadata() async {
final modules = <String, ModuleMetadata>{};
// The merged metadata resides next to the entrypoint.
// Assume that <name>.bootstrap.js has <name>.ddc_merged_metadata
if (entrypoint.endsWith('.bootstrap.js')) {
_logger.info('Loading debug metadata...');
final serverPath = entrypoint.replaceAll(
'.bootstrap.js',
'.ddc_merged_metadata',
);
final merged = await _assetReader.metadataContents(serverPath);
if (merged != null) {
for (final contents in merged.split('\n')) {
try {
if (contents.isEmpty ||
contents.startsWith('// intentionally empty:')) {
continue;
}
final moduleJson = json.decode(contents);
final metadata = ModuleMetadata.fromJson(
moduleJson as Map<String, dynamic>,
);
final moduleName = metadata.name;
modules[moduleName] = metadata;
_logger.fine('Loaded debug metadata for module: $moduleName');
} catch (e) {
_logger.warning('Failed to read metadata: $e');
rethrow;
}
}
}
}
return modules;
}
/// Process all metadata, including SDK metadata, and compute caches once.
Future<void> _initialize() async {
await _metadataMemoizer.runOnce(() async {
final metadata = await _processMetadata();
_addSdkMetadata();
metadata.values.forEach(_addMetadata);
});
}
/// Given a map of hot reloaded modules mapped to their respective libraries,
/// determines deleted and invalidated libraries and modules, invalidates them
/// in any caches, and recomputes the necessary information.
///
/// Returns a [ModifiedModuleReport] that can be used to invalidate other
/// caches after a hot reload.
Future<ModifiedModuleReport> reinitializeAfterHotReload(
Map<String, List> reloadedModulesToLibraries,
) async {
final modules = await _processMetadata();
final invalidatedLibraries = <String>{};
void invalidateLibrary(String libraryImportUri) {
invalidatedLibraries.add(libraryImportUri);
_libraries.remove(libraryImportUri);
_scriptToModule.remove(libraryImportUri);
_scripts[libraryImportUri]?.forEach(_scriptToModule.remove);
_scripts.remove(libraryImportUri);
}
final deletedModules = <String>{};
for (final module in _moduleToLibraries.keys) {
final deletedModule = !modules.containsKey(module);
final invalidatedModule = reloadedModulesToLibraries.containsKey(module);
assert(!(deletedModule && invalidatedModule));
// If the module was either deleted or reloaded, invalidate all previous
// information both about the module and its libraries.
if (deletedModule || invalidatedModule) {
_modulePathToModule.remove(module);
_moduleToLibraries[module]?.forEach(invalidateLibrary);
_moduleToModulePath.remove(module);
_moduleToSourceMap.remove(module);
}
if (deletedModule) deletedModules.add(module);
}
final reloadedModules = <String>{};
final reloadedLibraries = <String>{};
for (final module in reloadedModulesToLibraries.keys) {
reloadedModules.add(module);
reloadedLibraries.addAll(
reloadedModulesToLibraries[module]!.cast<String>(),
);
_addMetadata(modules[module]!);
}
// The libraries that are removed from the program are those that we
// invalidated but were never added again.
final deletedLibraries =
invalidatedLibraries
.where((library) => !_libraries.contains(library))
.toSet();
return ModifiedModuleReport(
deletedModules: deletedModules,
deletedLibraries: deletedLibraries,
reloadedModules: reloadedModules,
reloadedLibraries: reloadedLibraries,
);
}
void _addMetadata(ModuleMetadata metadata) {
final modulePath = stripLeadingSlashes(metadata.moduleUri);
final sourceMapPath = stripLeadingSlashes(metadata.sourceMapUri);
final moduleName = metadata.name;
_moduleToSourceMap[moduleName] = sourceMapPath;
_modulePathToModule[modulePath] = moduleName;
_moduleToModulePath[moduleName] = modulePath;
final moduleLibraries = <String>{};
for (final library in metadata.libraries.values) {
if (library.importUri.startsWith('file:/')) {
throw AbsoluteImportUriException(library.importUri);
}
moduleLibraries.add(library.importUri);
_libraries.add(library.importUri);
_scripts[library.importUri] = [];
_scriptToModule[library.importUri] = moduleName;
for (final path in library.partUris) {
// Parts in metadata are relative to the library Uri directory.
final partPath = p.url.join(p.dirname(library.importUri), path);
_scripts[library.importUri]!.add(partPath);
_scriptToModule[partPath] = moduleName;
}
}
_moduleToLibraries[moduleName] = moduleLibraries;
}
void _addSdkMetadata() {
final moduleName = 'dart_sdk';
for (final lib in sdkLibraries) {
_libraries.add(lib);
_scripts[lib] = [];
// TODO(srujzs): It feels weird that we add this mapping to only this map
// and not any of the other module maps. We should maybe handle this
// differently. This will become relevant if we ever support hot reload
// for the Dart SDK.
_scriptToModule[lib] = moduleName;
}
}
}
class AbsoluteImportUriException implements Exception {
final String importUri;
AbsoluteImportUriException(this.importUri);
@override
String toString() => "AbsoluteImportUriError: '$importUri'";
}
/// Computed after a hot reload using
/// [MetadataProvider.reinitializeAfterHotReload], represents the modules and
/// libraries in the program that were deleted, reloaded, and therefore,
/// modified.
///
/// Used to recompute caches throughout DWDS.
class ModifiedModuleReport {
/// Module names that are no longer in the program.
final Set<String> deletedModules;
/// Library uris that are no longer in the program.
final Set<String> deletedLibraries;
/// Module names that were loaded during the hot reload.
final Set<String> reloadedModules;
/// Library uris that were loaded during the hot reload.
final Set<String> reloadedLibraries;
/// Module names that were either removed or modified, including additions.
final Set<String> modifiedModules;
/// Library uris that were either removed or modified, including additions.
final Set<String> modifiedLibraries;
ModifiedModuleReport({
required this.deletedModules,
required this.deletedLibraries,
required this.reloadedModules,
required this.reloadedLibraries,
}) : modifiedModules = deletedModules.union(reloadedModules),
modifiedLibraries = deletedLibraries.union(reloadedLibraries);
}