OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 library source.caching_pub_package_map_provider; |
| 6 |
| 7 import 'dart:convert'; |
| 8 import 'dart:io' as io; |
| 9 |
| 10 import 'package:analyzer/file_system/file_system.dart'; |
| 11 import 'package:analyzer/source/package_map_provider.dart'; |
| 12 import 'package:analyzer/source/pub_package_map_provider.dart'; |
| 13 import 'package:analyzer/src/generated/engine.dart'; |
| 14 import 'package:analyzer/src/generated/sdk_io.dart'; |
| 15 import 'package:analyzer/src/generated/source.dart'; |
| 16 |
| 17 /** |
| 18 * The function used to write the cache file. |
| 19 * Returns the modification stamp for the newly written file. |
| 20 */ |
| 21 typedef int WriteFile(File file, String content); |
| 22 |
| 23 /** |
| 24 * [PubPackageMapProvider] extension which caches pub list results. |
| 25 * These results are cached in memory and in a single place on disk that is |
| 26 * shared cross session and between different simultaneous sessions. |
| 27 */ |
| 28 class CachingPubPackageMapProvider extends PubPackageMapProvider { |
| 29 static const cacheKey = 'pub_list_cache'; |
| 30 static const cacheVersion = 1; |
| 31 static const cacheVersionKey = 'pub_list_cache_version'; |
| 32 static const pubListResultKey = 'pub_list_result'; |
| 33 static const modificationStampsKey = 'modification_stamps'; |
| 34 |
| 35 /** |
| 36 * A cache of folder path to pub list information as shown below |
| 37 * or `null` if the cache has not yet been initialized. |
| 38 * |
| 39 * { |
| 40 * "path/to/folder": { |
| 41 * "pub_list_result": { |
| 42 * "packages": { |
| 43 * "foo": "path/to/foo", |
| 44 * "bar": ["path/to/bar1", "path/to/bar2"], |
| 45 * "myapp": "path/to/myapp", // self link is included |
| 46 * }, |
| 47 * "input_files": [ |
| 48 * "path/to/myapp/pubspec.lock" |
| 49 * ] |
| 50 * }, |
| 51 * "modification_stamps": { |
| 52 * "path/to/myapp/pubspec.lock": 1424305309 |
| 53 * } |
| 54 * } |
| 55 * "path/to/another/folder": { |
| 56 * ... |
| 57 * } |
| 58 * ... |
| 59 * } |
| 60 */ |
| 61 Map<String, Map> _cache; |
| 62 |
| 63 /** |
| 64 * The modification time of the cache file |
| 65 * or `null` if it has not yet been read. |
| 66 */ |
| 67 int _cacheModificationTime; |
| 68 |
| 69 /** |
| 70 * The function used to write the cache file. |
| 71 */ |
| 72 WriteFile _writeFile; |
| 73 |
| 74 /** |
| 75 * Construct a new instance. |
| 76 * [RunPubList] and [WriteFile] implementations may be injected for testing |
| 77 */ |
| 78 CachingPubPackageMapProvider(ResourceProvider resourceProvider, |
| 79 DirectoryBasedDartSdk sdk, [RunPubList runPubList, this._writeFile]) |
| 80 : super(resourceProvider, sdk, runPubList) { |
| 81 if (_writeFile == null) { |
| 82 _writeFile = _writeFileDefault; |
| 83 } |
| 84 } |
| 85 |
| 86 File get cacheFile => _cacheDir.getChild('cache'); |
| 87 Folder get _cacheDir => resourceProvider.getStateLocation('.pub-list'); |
| 88 File get _touchFile => _cacheDir.getChild('touch'); |
| 89 |
| 90 @override |
| 91 PackageMapInfo computePackageMap(Folder folder) { |
| 92 // |
| 93 // Return error if folder does not exist, but don't remove previously |
| 94 // cached result because folder may be only temporarily inaccessible |
| 95 // |
| 96 if (!folder.exists) { |
| 97 return computePackageMapError(folder); |
| 98 } |
| 99 // Ensure cache is up to date |
| 100 _readCache(); |
| 101 // Check for cached entry |
| 102 Map entry = _cache[folder.path]; |
| 103 if (entry != null) { |
| 104 Map<String, int> modificationStamps = entry[modificationStampsKey]; |
| 105 if (modificationStamps != null) { |
| 106 // |
| 107 // Check to see if any dependencies have changed |
| 108 // before returning cached result |
| 109 // |
| 110 if (!_haveDependenciesChanged(modificationStamps)) { |
| 111 return parsePackageMap(entry[pubListResultKey], folder); |
| 112 } |
| 113 } |
| 114 } |
| 115 int runCount = 0; |
| 116 PackageMapInfo info; |
| 117 while (true) { |
| 118 // Capture the current time so that we can tell if an input file |
| 119 // has changed while running pub list. This is done |
| 120 // by writing to a file rather than getting millisecondsSinceEpoch |
| 121 // because file modification time has different granularity |
| 122 // on diferent systems. |
| 123 int startStamp; |
| 124 try { |
| 125 startStamp = _writeFile(_touchFile, 'touch'); |
| 126 } catch (exception, stackTrace) { |
| 127 AnalysisEngine.instance.logger.logInformation( |
| 128 'Exception writing $_touchFile\n$exception\n$stackTrace'); |
| 129 startStamp = new DateTime.now().millisecondsSinceEpoch; |
| 130 } |
| 131 // computePackageMap calls parsePackageMap which caches the result |
| 132 info = super.computePackageMap(folder); |
| 133 ++runCount; |
| 134 if (!_haveDependenciesChangedSince(info, startStamp)) { |
| 135 // If no dependencies have changed while running pub then finished |
| 136 break; |
| 137 } |
| 138 if (runCount == 4) { |
| 139 // Don't run forever |
| 140 AnalysisEngine.instance.logger.logInformation( |
| 141 'pub list called $runCount times: $folder'); |
| 142 break; |
| 143 } |
| 144 } |
| 145 _writeCache(); |
| 146 return info; |
| 147 } |
| 148 |
| 149 @override |
| 150 PackageMapInfo parsePackageMap(Map obj, Folder folder) { |
| 151 PackageMapInfo info = super.parsePackageMap(obj, folder); |
| 152 Map<String, int> modificationStamps = new Map<String, int>(); |
| 153 for (String path in info.dependencies) { |
| 154 Resource res = resourceProvider.getResource(path); |
| 155 if (res is File && res.exists) { |
| 156 modificationStamps[path] = res.createSource().modificationStamp; |
| 157 } |
| 158 } |
| 159 // Assumes entry has been initialized by computePackageMap |
| 160 _cache[folder.path] = <String, Map>{ |
| 161 pubListResultKey: obj, |
| 162 modificationStampsKey: modificationStamps |
| 163 }; |
| 164 return info; |
| 165 } |
| 166 |
| 167 /** |
| 168 * Determine if any of the dependencies have changed. |
| 169 */ |
| 170 bool _haveDependenciesChanged(Map<String, int> modificationStamps) { |
| 171 for (String path in modificationStamps.keys) { |
| 172 Resource res = resourceProvider.getResource(path); |
| 173 if (res is File) { |
| 174 if (!res.exists || |
| 175 res.createSource().modificationStamp != modificationStamps[path]) { |
| 176 return true; |
| 177 } |
| 178 } else { |
| 179 return true; |
| 180 } |
| 181 } |
| 182 return false; |
| 183 } |
| 184 |
| 185 /** |
| 186 * Determine if any of the dependencies have changed since the given time. |
| 187 */ |
| 188 bool _haveDependenciesChangedSince(PackageMapInfo info, int startStamp) { |
| 189 for (String path in info.dependencies) { |
| 190 Resource res = resourceProvider.getResource(path); |
| 191 if (res is File) { |
| 192 int modStamp = res.createSource().modificationStamp; |
| 193 if (modStamp != null && modStamp >= startStamp) { |
| 194 return true; |
| 195 } |
| 196 } |
| 197 } |
| 198 return false; |
| 199 } |
| 200 |
| 201 /** |
| 202 * Read the cache from disk if it has not been read before. |
| 203 */ |
| 204 void _readCache() { |
| 205 // TODO(danrubel) This implementation assumes that |
| 206 // two separate processes are not accessing the cache file at the same time |
| 207 Source source = cacheFile.createSource(); |
| 208 if (source.exists() && |
| 209 (_cache == null || _cacheModificationTime != source.modificationStamp))
{ |
| 210 try { |
| 211 TimestampedData<String> data = source.contents; |
| 212 Map map = JSON.decode(data.data); |
| 213 if (map[cacheVersionKey] == cacheVersion) { |
| 214 _cache = map[cacheKey]; |
| 215 _cacheModificationTime = data.modificationTime; |
| 216 } |
| 217 } catch (exception, stackTrace) { |
| 218 AnalysisEngine.instance.logger.logInformation( |
| 219 'Exception reading $cacheFile\n$exception\n$stackTrace'); |
| 220 } |
| 221 } |
| 222 if (_cache == null) { |
| 223 _cache = new Map<String, Map>(); |
| 224 } |
| 225 } |
| 226 |
| 227 /** |
| 228 * Write the cache to disk. |
| 229 */ |
| 230 void _writeCache() { |
| 231 try { |
| 232 _cacheModificationTime = _writeFile(cacheFile, JSON.encode({ |
| 233 cacheVersionKey: cacheVersion, |
| 234 cacheKey: _cache |
| 235 })); |
| 236 } catch (exception, stackTrace) { |
| 237 AnalysisEngine.instance.logger.logInformation( |
| 238 'Exception writing $cacheFile\n$exception\n$stackTrace'); |
| 239 } |
| 240 } |
| 241 |
| 242 /** |
| 243 * Update the given file with the specified content. |
| 244 */ |
| 245 int _writeFileDefault(File cacheFile, String content) { |
| 246 // TODO(danrubel) This implementation assumes that |
| 247 // two separate processes are not accessing the cache file at the same time |
| 248 io.File file = new io.File(cacheFile.path); |
| 249 if (!file.parent.existsSync()) { |
| 250 file.parent.createSync(recursive: true); |
| 251 } |
| 252 file.writeAsStringSync(content, flush: true); |
| 253 return file.lastModifiedSync().millisecondsSinceEpoch; |
| 254 } |
| 255 } |
OLD | NEW |