Index: pkg/analysis_server/lib/src/source/caching_pub_package_map_provider.dart |
diff --git a/pkg/analysis_server/lib/src/source/caching_pub_package_map_provider.dart b/pkg/analysis_server/lib/src/source/caching_pub_package_map_provider.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a8289e412532d2b49ab51b5ef178a4418c317c52 |
--- /dev/null |
+++ b/pkg/analysis_server/lib/src/source/caching_pub_package_map_provider.dart |
@@ -0,0 +1,194 @@ |
+// Copyright (c) 2015, 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. |
+ |
+library source.caching_pub_package_map_provider; |
+ |
+import 'dart:convert'; |
+import 'dart:io' as io; |
+ |
+import 'package:analyzer/file_system/file_system.dart'; |
+import 'package:analyzer/source/package_map_provider.dart'; |
+import 'package:analyzer/source/pub_package_map_provider.dart'; |
+import 'package:analyzer/src/generated/engine.dart'; |
+import 'package:analyzer/src/generated/sdk_io.dart'; |
+import 'package:analyzer/src/generated/source.dart'; |
+ |
+/** |
+ * The function used to write the cache file. |
+ * Returns the modification timestamp for the newly written file. |
+ */ |
+typedef int WriteFile(File file, String content); |
+ |
+/** |
+ * [PubPackageMapProvider] extension which caches pub list results. |
+ * These results are cached in memory and in a single place on disk that is |
+ * shared cross session and between different simultaneous sessions. |
+ */ |
+class CachingPubPackageMapProvider extends PubPackageMapProvider { |
+ static const cacheKey = 'pub_list_cache'; |
+ static const cacheVersion = 1; |
+ static const cacheVersionKey = 'pub_list_cache_version'; |
+ static const pubListResultKey = 'pub_list_result'; |
+ static const modificationStampsKey = 'modification_stamps'; |
+ |
+ /** |
+ * A cache of folder path to pub list information as shown below |
+ * or `null` if the cache has not yet been initialized. |
+ * |
+ * { |
+ * "path/to/folder": { |
+ * "pub_list_result": { |
+ * "packages": { |
+ * "foo": "path/to/foo", |
+ * "bar": ["path/to/bar1", "path/to/bar2"], |
+ * "myapp": "path/to/myapp", // self link is included |
+ * }, |
+ * "input_files": [ |
+ * "path/to/myapp/pubspec.lock" |
+ * ] |
+ * }, |
+ * "modification_stamps": { |
+ * "path/to/myapp/pubspec.lock": 1424305309 |
+ * } |
+ * } |
+ * "path/to/another/folder": { |
+ * ... |
+ * } |
+ * ... |
+ * } |
+ */ |
+ Map<String, Map> _cache; |
+ |
+ /** |
+ * The modification time of the cache file when it was last read |
Paul Berry
2015/02/20 22:00:25
s/read/accessed/g
We update the modification time
danrubel
2015/02/21 03:09:30
Done.
|
+ * or `null` if it has not yet been read. |
+ */ |
+ int _cacheModificationTime; |
+ |
+ /** |
+ * The function used to write the cache file. |
+ */ |
+ WriteFile _writeFile; |
+ |
+ /** |
+ * Construct a new instance. |
+ * [RunPubList] and [WriteFile] implementations may be injected for testing |
+ */ |
+ CachingPubPackageMapProvider(ResourceProvider resourceProvider, |
+ DirectoryBasedDartSdk sdk, [RunPubList runPubList, this._writeFile]) |
+ : super(resourceProvider, sdk, runPubList) { |
+ if (_writeFile == null) { |
+ _writeFile = _writeFileDefault; |
+ } |
+ } |
+ |
+ File get _cacheFile => |
+ resourceProvider.getStateLocation('.pub-list').getChild('cache'); |
+ |
+ @override |
+ PackageMapInfo computePackageMap(Folder folder) { |
+ if (!folder.exists) { |
+ return computePackageMapError(folder); |
+ } |
+ // Ensure cache is up to date |
+ _readCache(); |
+ // Check for cached entry |
+ Map entry = _cache[folder.path]; |
+ if (entry != null) { |
+ Map<String, int> modificationStamps = entry[modificationStampsKey]; |
+ if (modificationStamps != null) { |
+ // |
+ // Check to see if any dependencies have changed |
+ // before returning cached result |
+ // |
+ bool dependencyChanged = false; |
+ modificationStamps.forEach((String path, int modificationStamp) { |
Paul Berry
2015/02/20 22:00:26
I'd recommend changing this to an ordinary "for" l
danrubel
2015/02/21 03:09:30
That would require a hash lookup on each iteration
Paul Berry
2015/02/23 18:54:58
Since calling res.exists and res.createSource().mo
danrubel
2015/02/24 02:29:45
Good point. Done.
|
+ Resource res = resourceProvider.getResource(path); |
+ if (res is File) { |
+ if (!res.exists || |
+ res.createSource().modificationStamp != modificationStamp) { |
+ dependencyChanged = true; |
+ } |
+ } else { |
+ dependencyChanged = true; |
+ } |
+ }); |
+ if (!dependencyChanged) { |
+ return parsePackageMap(entry[pubListResultKey], folder); |
+ } |
+ } |
+ } |
+ // Create a new entry if one does not already exist |
Paul Berry
2015/02/20 22:00:25
It's risky to do this here, since it leaves _cache
danrubel
2015/02/21 03:09:30
Done.
|
+ if (entry == null) { |
+ entry = new Map<String, Map>(); |
+ _cache[folder.path] = entry; |
+ } |
+ // computePackageMap calls parsePackageMap which caches the result |
+ PackageMapInfo info = super.computePackageMap(folder); |
+ _writeCache(); |
+ return info; |
+ } |
+ |
+ @override |
+ PackageMapInfo parsePackageMap(Map obj, Folder folder) { |
+ PackageMapInfo info = super.parsePackageMap(obj, folder); |
+ Map<String, int> modificationStamps = new Map<String, int>(); |
Paul Berry
2015/02/20 22:00:26
Nit: I believe the standard dart idiom for creati
Brian Wilkerson
2015/02/20 22:05:36
True, but what we really want, for efficiency's sa
|
+ for (String path in info.dependencies) { |
+ Resource res = resourceProvider.getResource(path); |
+ if (res is File && res.exists) { |
+ modificationStamps[path] = res.createSource().modificationStamp; |
+ } |
+ } |
+ // Assumes entry has been initialized by computePackageMap |
+ Map entry = _cache[folder.path]; |
Paul Berry
2015/02/20 22:00:25
These three lines can be replaced with a single at
danrubel
2015/02/21 03:09:30
Great suggestion. Done.
|
+ entry[pubListResultKey] = obj; |
+ entry[modificationStampsKey] = modificationStamps; |
+ return info; |
+ } |
+ |
+ /** |
+ * Read the cache from disk if it has not been read before. |
+ */ |
+ void _readCache() { |
+ // TODO(danrubel) This implementation assumes that |
+ // two separate processes are not accessing the cache file at the same time |
+ Source source = _cacheFile.createSource(); |
+ if (source.exists() && |
+ (_cache == null || _cacheModificationTime != source.modificationStamp)) { |
+ TimestampedData<String> data = source.contents; |
+ Map map = JSON.decode(data.data); |
+ if (map[cacheVersionKey] == cacheVersion) { |
+ _cache = map[cacheKey]; |
+ _cacheModificationTime = data.modificationTime; |
+ } |
+ } |
+ if (_cache == null) { |
+ _cache = new Map<String, Map>(); |
+ } |
+ } |
+ |
+ /** |
+ * Write the cache to disk. |
+ */ |
+ void _writeCache() { |
+ _cacheModificationTime = _writeFile(_cacheFile, JSON.encode({ |
+ cacheVersionKey: cacheVersion, |
+ cacheKey: _cache |
+ })); |
+ } |
+ |
+ /** |
+ * Update the given file with the specified content. |
+ */ |
+ int _writeFileDefault(File cacheFile, String content) { |
+ // TODO(danrubel) This implementation assumes that |
+ // two separate processes are not accessing the cache file at the same time |
+ io.File file = new io.File(cacheFile.path); |
+ if (!file.parent.existsSync()) { |
+ file.parent.createSync(recursive: true); |
+ } |
+ file.writeAsStringSync(content, flush: true); |
+ return file.lastModifiedSync().millisecondsSinceEpoch; |
+ } |
+} |