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..964e7dec57871bac973570ad62e4869d9b13ec1f |
--- /dev/null |
+++ b/pkg/analysis_server/lib/src/source/caching_pub_package_map_provider.dart |
@@ -0,0 +1,260 @@ |
+// 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:core'; |
+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/dart/sdk/sdk.dart'; |
+import 'package:analyzer/src/generated/engine.dart'; |
+import 'package:analyzer/src/generated/source.dart'; |
+ |
+/** |
+ * The function used to write the cache file. |
+ * Returns the modification stamp 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. |
+ * |
+ * TODO(paulberry): before this class is used again, it should be ported over |
+ * to extend OptimizingPubPackageMapProvider instead of PubPackageMapProvider. |
+ */ |
+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 |
+ * 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, FolderBasedDartSdk sdk, |
+ [RunPubList runPubList, this._writeFile]) |
+ : super(resourceProvider, sdk, runPubList) { |
+ if (_writeFile == null) { |
+ _writeFile = _writeFileDefault; |
+ } |
+ } |
+ |
+ File get cacheFile => _cacheDir.getChild('cache'); |
+ Folder get _cacheDir => resourceProvider.getStateLocation('.pub-list'); |
+ File get _touchFile => _cacheDir.getChild('touch'); |
+ |
+ @override |
+ PackageMapInfo computePackageMap(Folder folder) { |
+ // |
+ // Return error if folder does not exist, but don't remove previously |
+ // cached result because folder may be only temporarily inaccessible |
+ // |
+ 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] as Map<String, int>; |
+ if (modificationStamps != null) { |
+ // |
+ // Check to see if any dependencies have changed |
+ // before returning cached result |
+ // |
+ if (!_haveDependenciesChanged(modificationStamps)) { |
+ return parsePackageMap(entry[pubListResultKey], folder); |
+ } |
+ } |
+ } |
+ int runCount = 0; |
+ PackageMapInfo info; |
+ while (true) { |
+ // Capture the current time so that we can tell if an input file |
+ // has changed while running pub list. This is done |
+ // by writing to a file rather than getting millisecondsSinceEpoch |
+ // because file modification time has different granularity |
+ // on diferent systems. |
+ int startStamp; |
+ try { |
+ startStamp = _writeFile(_touchFile, 'touch'); |
+ } catch (exception, stackTrace) { |
+ AnalysisEngine.instance.logger.logInformation( |
+ 'Exception writing $_touchFile\n$exception\n$stackTrace'); |
+ startStamp = new DateTime.now().millisecondsSinceEpoch; |
+ } |
+ // computePackageMap calls parsePackageMap which caches the result |
+ info = super.computePackageMap(folder); |
+ ++runCount; |
+ if (!_haveDependenciesChangedSince(info, startStamp)) { |
+ // If no dependencies have changed while running pub then finished |
+ break; |
+ } |
+ if (runCount == 4) { |
+ // Don't run forever |
+ AnalysisEngine.instance.logger |
+ .logInformation('pub list called $runCount times: $folder'); |
+ break; |
+ } |
+ } |
+ _writeCache(); |
+ return info; |
+ } |
+ |
+ @override |
+ PackageMapInfo parsePackageMap(Map obj, Folder folder) { |
+ PackageMapInfo info = super.parsePackageMap(obj, folder); |
+ Map<String, int> modificationStamps = new Map<String, int>(); |
+ 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 |
+ _cache[folder.path] = <String, Map>{ |
+ pubListResultKey: obj, |
+ modificationStampsKey: modificationStamps |
+ }; |
+ return info; |
+ } |
+ |
+ /** |
+ * Determine if any of the dependencies have changed. |
+ */ |
+ bool _haveDependenciesChanged(Map<String, int> modificationStamps) { |
+ for (String path in modificationStamps.keys) { |
+ Resource res = resourceProvider.getResource(path); |
+ if (res is File) { |
+ if (!res.exists || |
+ res.createSource().modificationStamp != modificationStamps[path]) { |
+ return true; |
+ } |
+ } else { |
+ return true; |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ /** |
+ * Determine if any of the dependencies have changed since the given time. |
+ */ |
+ bool _haveDependenciesChangedSince(PackageMapInfo info, int startStamp) { |
+ for (String path in info.dependencies) { |
+ Resource res = resourceProvider.getResource(path); |
+ if (res is File) { |
+ int modStamp = res.createSource().modificationStamp; |
+ if (modStamp != null && modStamp >= startStamp) { |
+ return true; |
+ } |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ /** |
+ * 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)) { |
+ try { |
+ TimestampedData<String> data = source.contents; |
+ Map map = JSON.decode(data.data); |
+ if (map[cacheVersionKey] == cacheVersion) { |
+ _cache = map[cacheKey] as Map<String, Map>; |
+ _cacheModificationTime = data.modificationTime; |
+ } |
+ } catch (exception, stackTrace) { |
+ AnalysisEngine.instance.logger.logInformation( |
+ 'Exception reading $cacheFile\n$exception\n$stackTrace'); |
+ } |
+ } |
+ if (_cache == null) { |
+ _cache = new Map<String, Map>(); |
+ } |
+ } |
+ |
+ /** |
+ * Write the cache to disk. |
+ */ |
+ void _writeCache() { |
+ try { |
+ _cacheModificationTime = _writeFile(cacheFile, |
+ JSON.encode({cacheVersionKey: cacheVersion, cacheKey: _cache})); |
+ } catch (exception, stackTrace) { |
+ AnalysisEngine.instance.logger.logInformation( |
+ 'Exception writing $cacheFile\n$exception\n$stackTrace'); |
+ } |
+ } |
+ |
+ /** |
+ * 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; |
+ } |
+} |