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