Skip to content

Commit b4ac0a1

Browse files
MarkzipanCommit Queue
authored andcommitted
Adding support for non-hot-reloadable packages in DDC.
These packages can be specified with the `non-hot-reloadable-package` flag and will be compiled without their members being properly hot reloadable. In exchange, we can perform additional optimizations (such as avoiding levels of indirection/checks in anticipation of their being updated). This additionally overhauls our memory compiler to use the incremental compiler for proper state-passing hot reload testing + adds tests for this functionality. Change-Id: Ief65896410f32655f064bd15728703629faa4051 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/428343 Reviewed-by: Nate Biggs <[email protected]> Reviewed-by: Nicholas Shahan <[email protected]> Commit-Queue: Mark Zhou <[email protected]>
1 parent e3e82c9 commit b4ac0a1

File tree

6 files changed

+270
-22
lines changed

6 files changed

+270
-22
lines changed

pkg/dev_compiler/lib/src/command/command.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,8 @@ Future<CompilerResult> _compile(List<String> args,
363363
final lastAcceptedComponent = Component();
364364
kernel.BinaryBuilder((await File(reloadLastAcceptedKernel).readAsBytes()))
365365
.readComponent(lastAcceptedComponent);
366-
final deltaInspector = HotReloadDeltaInspector();
366+
final deltaInspector = HotReloadDeltaInspector(
367+
nonHotReloadablePackages: options.nonHotReloadablePackages);
367368
final rejectionReasons = deltaInspector.compareGenerations(
368369
lastAcceptedComponent, compiledLibraries);
369370
if (rejectionReasons.isNotEmpty) {

pkg/dev_compiler/lib/src/command/options.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ class Options {
6363

6464
final Map<String, String> summaryModules;
6565

66+
/// Packages that should not be hot reloaded. DDC can perform special
67+
/// optimizations on these packages and will throw if they are hot reloaded.
68+
final Set<String> nonHotReloadablePackages;
69+
6670
final List<ModuleFormat> moduleFormats;
6771

6872
/// The name of the module.
@@ -117,6 +121,7 @@ class Options {
117121
this.reloadLastAcceptedKernel,
118122
this.reloadDeltaKernel,
119123
this.summaryModules = const {},
124+
this.nonHotReloadablePackages = const {},
120125
this.moduleFormats = const [],
121126
required this.moduleName,
122127
this.multiRootScheme = 'org-dartlang-app',

pkg/dev_compiler/lib/src/kernel/compiler_new.dart

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,19 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
758758

759759
bool _inFunctionExpression = false;
760760

761+
/// Returns whether or not [uri] can be hot reloaded.
762+
///
763+
/// These packages are not updated during the runtime of a standard program,
764+
/// allowing us to make the following optimizations:
765+
/// - methods, getters, and setters are emitted without additional levels
766+
/// of indirection
767+
/// - fields are emitted without additional lazy initialization checks.
768+
bool _isNonHotReloadableResource(Uri uri) {
769+
return (uri.isScheme('dart')) ||
770+
(uri.isScheme('package') &&
771+
_options.nonHotReloadablePackages.contains(uri.pathSegments[0]));
772+
}
773+
761774
/// Module can be emitted only once, and the compiler can be reused after
762775
/// only in incremental mode, for expression compilation only.
763776
js_ast.Program emitLibrary(Library library) {
@@ -2761,11 +2774,13 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
27612774
var virtualFieldSymbol = _emitFieldValueAccessor(field);
27622775
var name = _declareMemberName(field);
27632776
var initializer = _visitInitializer(field.initializer, field.annotations);
2764-
var getter = _emitLazyInitializingFunction(
2765-
js.call('this.#', virtualFieldSymbol),
2766-
initializer,
2767-
field,
2768-
);
2777+
var getter = _isNonHotReloadableResource(_currentLibrary!.importUri)
2778+
? js.fun('function() { return this[#]; }', [virtualFieldSymbol])
2779+
: _emitLazyInitializingFunction(
2780+
js.call('this.#', virtualFieldSymbol),
2781+
initializer,
2782+
field,
2783+
);
27692784
var jsGetter = js_ast.Method(name, getter, isGetter: true)
27702785
..sourceInformation = _nodeStart(field);
27712786

@@ -3124,8 +3139,21 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
31243139

31253140
var initializer =
31263141
_visitInitializer(member.initializer, member.annotations);
3127-
var getter = _emitLazyInitializingFunction(
3128-
js.call('this.#', memberValueStore), initializer, member);
3142+
3143+
js_ast.Fun getter;
3144+
if (_isNonHotReloadableResource(_currentLibrary!.importUri)) {
3145+
getter = js.fun(
3146+
'function() { return this[#] === void 0 ? this[#] = # : this[#]; }',
3147+
[
3148+
memberValueStore,
3149+
memberValueStore,
3150+
initializer,
3151+
memberValueStore
3152+
]);
3153+
} else {
3154+
getter = _emitLazyInitializingFunction(
3155+
js.call('this.#', memberValueStore), initializer, member);
3156+
}
31293157
properties.add(js_ast.Method(access, getter,
31303158
isGetter: true,
31313159
isStatic: member.isStatic && member.enclosingClass != null)
@@ -3134,7 +3162,6 @@ class LibraryCompiler extends ComputeOnceConstantVisitor<js_ast.Expression>
31343162
member.fileOffset,
31353163
member.name.text.length,
31363164
));
3137-
31383165
if (!member.isFinal && !member.isConst) {
31393166
var body = <js_ast.Statement>[];
31403167
var param = _emitIdentifier('v');

pkg/dev_compiler/lib/src/kernel/hot_reload_delta_inspector.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ class HotReloadDeltaInspector {
1818
/// generation.
1919
final _rejectionMessages = <String>[];
2020

21+
/// A set of packages that should not be hot reloaded.
22+
///
23+
/// The delta inspector will reject any component that modifies any such
24+
/// packages (while allowing them to be introduced).
25+
final Set<String> nonHotReloadablePackages;
26+
27+
HotReloadDeltaInspector({this.nonHotReloadablePackages = const {}});
28+
2129
/// Returns all hot reload rejection errors discovered while comparing [delta]
2230
/// against the [lastAccepted] version.
2331
///
@@ -42,6 +50,12 @@ class HotReloadDeltaInspector {
4250
// No previous version of the library to compare with.
4351
continue;
4452
}
53+
if (_shouldNotCompileWithHotReload(deltaLibrary.importUri)) {
54+
_rejectionMessages
55+
.add('Attempting to hot reload a modified library from a package '
56+
'marked as non-hot-reloadable: '
57+
"Library: '${deltaLibrary.importUri}'");
58+
}
4559
var libraryMetadata = metadataRepo.mapping
4660
.putIfAbsent(deltaLibrary, HotReloadLibraryMetadata.new);
4761
// TODO(60281): Handle members when an entire library has been deleted
@@ -158,6 +172,16 @@ class HotReloadDeltaInspector {
158172
acceptedProcedure.name.text,
159173
];
160174
}
175+
176+
/// Returns `true` if the resource at [uri] should not be compiled with hot
177+
/// reload.
178+
///
179+
/// No 'dart:' libraries will be compiled with hot reload support.
180+
bool _shouldNotCompileWithHotReload(Uri uri) {
181+
return (uri.isScheme('dart')) ||
182+
(uri.isScheme('package') &&
183+
nonHotReloadablePackages.contains(uri.pathSegments[0]));
184+
}
161185
}
162186

163187
const hotReloadLibraryMetadataTag = 'ddc.hot-reload-library.metadata';

pkg/dev_compiler/test/hot_reload_delta_inspector_test.dart

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,24 +348,150 @@ Future<void> main() async {
348348
orderedEquals(['deletedSetter']));
349349
});
350350
});
351+
352+
group('Non-hot-reloadable packages ', () {
353+
final packageName = 'test_package';
354+
355+
final deltaInspector =
356+
HotReloadDeltaInspector(nonHotReloadablePackages: {packageName});
357+
test('reject reloads when a member is added.', () async {
358+
final initialAndDeltaSource = '''
359+
import 'package:$packageName/file.dart';
360+
main() {}
361+
''';
362+
final initialPackageSource = 'class Foo {}';
363+
final deltaPackageSource = 'class Foo { int member = 100; }';
364+
final (:initial, :delta) = await compileComponents(
365+
initialAndDeltaSource,
366+
initialAndDeltaSource,
367+
initialPackageSource: initialPackageSource,
368+
deltaPackageSource: deltaPackageSource,
369+
packageName: packageName,
370+
);
371+
expect(
372+
deltaInspector.compareGenerations(initial, delta),
373+
unorderedEquals([
374+
'Attempting to hot reload a modified library from a package '
375+
'marked as non-hot-reloadable: '
376+
"Library: 'package:$packageName/file.dart'"
377+
]));
378+
});
379+
test('reject reloads when a member is removed.', () async {
380+
final initialAndDeltaSource = '''
381+
import 'package:$packageName/file.dart';
382+
main() {}
383+
''';
384+
final initialPackageSource = 'class Foo { int member = 100; }';
385+
final deltaPackageSource = 'class Foo {}';
386+
final (:initial, :delta) = await compileComponents(
387+
initialAndDeltaSource,
388+
initialAndDeltaSource,
389+
initialPackageSource: initialPackageSource,
390+
deltaPackageSource: deltaPackageSource,
391+
packageName: packageName,
392+
);
393+
expect(
394+
deltaInspector.compareGenerations(initial, delta),
395+
unorderedEquals([
396+
'Attempting to hot reload a modified library from a package '
397+
'marked as non-hot-reloadable: '
398+
"Library: 'package:$packageName/file.dart'"
399+
]));
400+
});
401+
test('accept reloads when introduced but not modified.', () async {
402+
final initialSource = '''
403+
main() {}
404+
''';
405+
final initialAndDeltaPackageSource = 'class Foo { int member = 100; }';
406+
final deltaSource = '''
407+
import 'package:$packageName/file.dart';
408+
main() {}
409+
''';
410+
final (:initial, :delta) = await compileComponents(
411+
initialSource,
412+
deltaSource,
413+
initialPackageSource: initialAndDeltaPackageSource,
414+
deltaPackageSource: initialAndDeltaPackageSource,
415+
packageName: packageName,
416+
);
417+
expect(() => deltaInspector.compareGenerations(initial, delta),
418+
returnsNormally);
419+
});
420+
});
351421
}
352422

353423
/// Test only helper compiles [initialSource] and [deltaSource] and returns two
354424
/// kernel components.
425+
///
426+
/// Auto-generates a fake package_config.json if [packageName] is provided.
427+
/// Supports a single package named [packageName] containing a single file
428+
/// whose source contents across one generation are [initialPackageSource] and
429+
/// [deltaPackageSource].
355430
Future<({Component initial, Component delta})> compileComponents(
356-
String initialSource, String deltaSource) async {
431+
String initialSource, String deltaSource,
432+
{Uri? baseUri,
433+
String? packageName,
434+
String initialPackageSource = '',
435+
String deltaPackageSource = ''}) async {
436+
baseUri ??= memoryDirectory;
437+
357438
final fileName = 'main.dart';
358-
final fileUri = Uri(scheme: 'memory', host: '', path: fileName);
439+
final packageFileName = 'lib/file.dart';
440+
final fileUri = Uri(scheme: baseUri.scheme, host: '', path: fileName);
359441
final memoryFileMap = {fileName: initialSource};
360-
final initialResult = await componentFromMemory(memoryFileMap, fileUri);
442+
443+
// Generate a fake package_config.json and package.
444+
Uri? packageConfigUri;
445+
if (packageName != null) {
446+
packageConfigUri = baseUri.resolve('package_config.json');
447+
memoryFileMap['package_config.json'] =
448+
generateFakePackagesFile(packageName: packageName);
449+
memoryFileMap[packageFileName] = initialPackageSource;
450+
}
451+
final initialResult = await incrementalComponentFromMemory(
452+
memoryFileMap,
453+
fileUri,
454+
baseUri: baseUri,
455+
packageConfigUri: packageConfigUri,
456+
);
361457
expect(initialResult.errors, isEmpty,
362458
reason: 'Initial source produced compile time errors.');
459+
363460
memoryFileMap[fileName] = deltaSource;
364-
final deltaResult = await componentFromMemory(memoryFileMap, fileUri);
461+
if (packageName != null) {
462+
memoryFileMap[packageFileName] = initialPackageSource;
463+
}
464+
final deltaResult = await incrementalComponentFromMemory(
465+
memoryFileMap,
466+
fileUri,
467+
baseUri: baseUri,
468+
packageConfigUri: packageConfigUri,
469+
initialCompilerState: initialResult.initialCompilerState,
470+
);
365471
expect(deltaResult.errors, isEmpty,
366472
reason: 'Delta source produced compile time errors.');
367473
return (
368474
initial: initialResult.ddcResult.component,
369475
delta: deltaResult.ddcResult.component
370476
);
371477
}
478+
479+
String generateFakePackagesFile({
480+
required String packageName,
481+
String rootUri = '/',
482+
String packageUri = 'lib/',
483+
}) {
484+
return '''
485+
{
486+
"configVersion": 0,
487+
"packages": [
488+
{
489+
"name": "$packageName",
490+
"rootUri": "$rootUri",
491+
"packageUri": "$packageUri",
492+
"languageVersion": "3.4"
493+
}
494+
]
495+
}
496+
''';
497+
}

0 commit comments

Comments
 (0)