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 |