Skip to content

Commit 29b942c

Browse files
srawlinsCommit Queue
authored andcommitted
DAS plugins: Cache AOT snapshot
Fixes #61684 This caching is based largely on the `--depfile` feature offered by `dart compile`, which is based on a Ninja depfile concept (https://ninja-build.org/manual.html#_depfile), which spits out a file (`depfile.txt` here) which lists all of the input files which were required to build an AOT snapshot. The process is essentially: 1. If an AOT snapshot is found, maybe use it as a cached snapshot! a. If the `pubspec.yaml` modification timestamp is newer, re-compile! b. If the `.dart_tool/package_config.json` modification timestamp is newer, re-compile! c. If the `bin/plugin.dart` modification timestamp is newer, re-compile! d. If the `bin/depfile.txt` file is missing or malformed, re-compile! e. If any files mentioned in `bin/depfile.txt` have a newer modification timestamp, or don't exist, or are an otherwise bad path, re-compile! f. Otherwise, save a dozen seconds and use the cached snapshot. Change-Id: Icc747198f8af76d256ac915685473d6f529a3cef Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/464602 Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Samuel Rawlins <[email protected]>
1 parent 5cdde4a commit 29b942c

File tree

3 files changed

+416
-103
lines changed

3 files changed

+416
-103
lines changed

pkg/analysis_server/lib/src/plugin/plugin_manager.dart

Lines changed: 131 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -515,15 +515,16 @@ class PluginManager {
515515

516516
/// Compiles [entrypoint] to an AOT snapshot and records timing to the
517517
/// instrumentation log.
518-
ProcessResult _compileAotSnapshot(String entrypoint) {
518+
ProcessResult _compileAotSnapshot(File entrypoint) {
519519
instrumentationService.logInfo(
520520
'Running "dart compile aot-snapshot $entrypoint".',
521521
);
522522

523523
var stopwatch = Stopwatch()..start();
524+
var depfile = entrypoint.parent.getChildAssumingFile('depfile.txt');
524525
var result = _processRunner.runSync(
525526
sdk.dart,
526-
['compile', 'aot-snapshot', entrypoint],
527+
['compile', 'aot-snapshot', '--depfile', depfile.path, entrypoint.path],
527528
stderrEncoding: utf8,
528529
stdoutEncoding: utf8,
529530
);
@@ -539,9 +540,30 @@ class PluginManager {
539540
/// Compiles [pluginFile], in [pluginFolder], to an AOT snapshot, and returns
540541
/// the [File] for the snapshot.
541542
File _compileAsAot({required File pluginFile, required Folder pluginFolder}) {
543+
try {
544+
// Potentially use existing snapshot.
545+
var aotSnapshotFile = _existingAotSnapshot(
546+
resourceProvider: _resourceProvider,
547+
pluginFile: pluginFile,
548+
pluginFolder: pluginFolder,
549+
);
550+
if (aotSnapshotFile != null) {
551+
instrumentationService.logInfo(
552+
'Using existing plugin AOT snapshot at '
553+
"'${aotSnapshotFile.path}'",
554+
);
555+
return aotSnapshotFile;
556+
}
557+
} catch (error, stackTrace) {
558+
instrumentationService.logException(
559+
'Exception while checking an existing plugin AOT snapshot: '
560+
'"$error"\n$stackTrace',
561+
);
562+
}
563+
542564
// When the Dart Analysis Server is built as AOT, then all spawned
543565
// Isolates must also be built as AOT.
544-
var aotResult = _compileAotSnapshot(pluginFile.path);
566+
var aotResult = _compileAotSnapshot(pluginFile);
545567
if (aotResult.exitCode != 0) {
546568
var buffer = StringBuffer();
547569
buffer.writeln(
@@ -564,7 +586,7 @@ class PluginManager {
564586
/// Computes the plugin files, given that the plugin should exist in
565587
/// [pluginFolder].
566588
///
567-
/// Runs `pub` if [pubCommand] is not `null`.
589+
/// Runs `pub <pubCommand>` in [pluginFolder] if [pubCommand] is not `null`.
568590
PluginFiles _computeFiles(
569591
Folder pluginFolder, {
570592
required bool builtAsAot,
@@ -582,7 +604,10 @@ class PluginManager {
582604
.getChildAssumingFile(file_paths.packageConfigJson);
583605

584606
if (pubCommand != null) {
585-
var pubResult = _runPubCommand(pubCommand, pluginFolder);
607+
var pubResult = _runPubCommand(
608+
pubCommand,
609+
workingDirectory: pluginFolder,
610+
);
586611
String? exceptionReason;
587612
if (pubResult.exitCode != 0) {
588613
var buffer = StringBuffer();
@@ -722,6 +747,56 @@ class PluginManager {
722747
return packageConfigFile;
723748
}
724749

750+
/// Returns a viable existing plugin AOT snapshot, if it exists and its
751+
/// modification timestamp is newer than all of its dependencies, and `null`
752+
/// otherwise.
753+
///
754+
/// The dependencies of an AOT snapshot are the pubspec file, the
755+
/// entrypoint file, and all of the files referenced in the depfile which
756+
/// is generated by the `dart compile` command.
757+
File? _existingAotSnapshot({
758+
required ResourceProvider resourceProvider,
759+
required File pluginFile,
760+
required Folder pluginFolder,
761+
}) {
762+
var aotSnapshotFile = pluginFolder
763+
.getChildAssumingFolder('bin')
764+
.getChildAssumingFile('plugin.aot');
765+
if (!aotSnapshotFile.exists) return null;
766+
var snapshotModificationStamp = aotSnapshotFile.modificationStamp;
767+
768+
if (pluginFile.modificationStamp > snapshotModificationStamp) return null;
769+
var pubspecFile = pluginFolder.getChildAssumingFile(file_paths.pubspecYaml);
770+
if (pubspecFile.modificationStamp > snapshotModificationStamp) return null;
771+
772+
var depfile = pluginFolder
773+
.getChildAssumingFolder('bin')
774+
.getChildAssumingFile('depfile.txt');
775+
if (!depfile.exists) return null;
776+
777+
var content = depfile.readAsStringSync();
778+
var dependencies = parseDepfile(content);
779+
if (dependencies == null) {
780+
// Malformed depfile content.
781+
return null;
782+
}
783+
784+
for (var dependencyPath in dependencies) {
785+
var file = _resourceProvider.getFile(dependencyPath);
786+
if (!file.exists) {
787+
// Something has certainly changed on disk; do not use the cached
788+
// snapshot.
789+
return null;
790+
}
791+
if (file.modificationStamp > snapshotModificationStamp) {
792+
// Snapshot is stale.
793+
return null;
794+
}
795+
}
796+
797+
return aotSnapshotFile;
798+
}
799+
725800
void _notifyPluginsChanged() => _pluginsChanged.add(null);
726801

727802
/// Return the names of packages that are listed as dependencies in the given
@@ -742,17 +817,20 @@ class PluginManager {
742817
}
743818

744819
/// Runs (and records timing to the instrumentation log) a Pub command
745-
/// [pubCommand] in [folder].
746-
ProcessResult _runPubCommand(String pubCommand, Folder folder) {
820+
/// [pubCommand] in [workingDirectory].
821+
ProcessResult _runPubCommand(
822+
String pubCommand, {
823+
required Folder workingDirectory,
824+
}) {
747825
instrumentationService.logInfo(
748-
'Running "pub $pubCommand" in "${folder.path}".',
826+
'Running "pub $pubCommand" in "${workingDirectory.path}".',
749827
);
750828

751829
var stopwatch = Stopwatch()..start();
752830
var result = _processRunner.runSync(
753831
sdk.dart,
754832
['pub', pubCommand],
755-
workingDirectory: folder.path,
833+
workingDirectory: workingDirectory.path,
756834
environment: {_pubEnvironmentKey: _getPubEnvironmentValue()},
757835
stderrEncoding: utf8,
758836
stdoutEncoding: utf8,
@@ -772,6 +850,50 @@ class PluginManager {
772850
return hex.encode(bytes);
773851
}
774852

853+
/// Parses Ninja-style depfile content, returning a list of dependency paths.
854+
///
855+
/// Returns `null` if the text is not valid depfile content.
856+
///
857+
/// The format is:
858+
///
859+
/// target: dependency1 dependency2 ...
860+
///
861+
/// See https://ninja-build.org/manual.html#_depfile.
862+
@visibleForTesting
863+
static List<String>? parseDepfile(String content) {
864+
var colonIndex = content.indexOf(': ');
865+
if (colonIndex < 0) {
866+
// Not a valid depfile.
867+
return null;
868+
}
869+
var dependenciesString = content
870+
.substring(colonIndex + 1)
871+
.trimLeft()
872+
.replaceAll(RegExp(r'[\r\n]'), '');
873+
var dependencies = <String>[];
874+
var start = 0;
875+
while (start < dependenciesString.length) {
876+
var index = start;
877+
while (index < dependenciesString.length) {
878+
var char = dependenciesString[index];
879+
if (char == ' ') {
880+
break;
881+
} else if (char == r'\') {
882+
index++;
883+
}
884+
index++;
885+
}
886+
dependencies.add(
887+
dependenciesString
888+
.substring(start, index)
889+
.replaceAll(r'\\', r'\')
890+
.replaceAll(r'\ ', ' '),
891+
);
892+
start = index + 1;
893+
}
894+
return dependencies.where((p) => p.isNotEmpty).toList();
895+
}
896+
775897
/// Record the fact that the given [pluginIsolate] responded to a request with
776898
/// the given [method] in the given [time].
777899
static void recordResponseTime(

pkg/analysis_server/lib/src/plugin/plugin_watcher.dart

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,26 @@ class PluginWatcher implements DriverWatcher {
122122
"context root: '${contextRoot.root.path}'",
123123
);
124124
sharedPluginFolder.create();
125-
sharedPluginFolder
126-
.getChildAssumingFile(file_paths.pubspecYaml)
127-
.writeAsStringSync(packageGenerator.generatePubspec());
125+
var pubspecFile = sharedPluginFolder.getChildAssumingFile(
126+
file_paths.pubspecYaml,
127+
);
128+
var newPubspecContent = packageGenerator.generatePubspec();
129+
// Only update the file if the content is different, to avoid changing the
130+
// modification timestamp.
131+
if (!pubspecFile.exists ||
132+
newPubspecContent != pubspecFile.readAsStringSync()) {
133+
pubspecFile.writeAsStringSync(newPubspecContent);
134+
}
135+
128136
var binFolder = sharedPluginFolder.getChildAssumingFolder('bin')..create();
129-
binFolder
130-
.getChildAssumingFile('plugin.dart')
131-
.writeAsStringSync(packageGenerator.generateEntrypoint());
137+
var entrypointFile = binFolder.getChildAssumingFile('plugin.dart');
138+
var newEntrypointContent = packageGenerator.generateEntrypoint();
139+
// Only update the file if the content is different, to avoid changing the
140+
// modification timestamp.
141+
if (!entrypointFile.exists ||
142+
newEntrypointContent != entrypointFile.readAsStringSync()) {
143+
entrypointFile.writeAsStringSync(newEntrypointContent);
144+
}
132145
manager.instrumentationService.logInfo(
133146
'Adding ${pluginConfigurations.length} analyzer plugins for '
134147
"context root: '${contextRoot.root.path}'",

0 commit comments

Comments
 (0)