| // 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. |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| import 'dart:isolate'; |
| |
| import 'package:analyzer/src/dart/analysis/byte_store.dart'; |
| import 'package:path/path.dart'; |
| |
| /** |
| * The request that is sent from the main isolate to the clean-up isolate. |
| */ |
| class CacheCleanUpRequest { |
| final String cachePath; |
| final int maxSizeBytes; |
| final SendPort replyTo; |
| |
| CacheCleanUpRequest(this.cachePath, this.maxSizeBytes, this.replyTo); |
| } |
| |
| /** |
| * [ByteStore] that stores values as files and performs cache eviction. |
| * |
| * Only the process that manages the cache, e.g. Analysis Server, should use |
| * this class. Other processes, e.g. Analysis Server plugins, should use |
| * [FileByteStore] instead and let the main process to perform eviction. |
| */ |
| class EvictingFileByteStore implements ByteStore { |
| static bool _cleanUpSendPortShouldBePrepared = true; |
| static SendPort _cleanUpSendPort; |
| |
| final String _cachePath; |
| final int _maxSizeBytes; |
| final FileByteStore _fileByteStore; |
| |
| int _bytesWrittenSinceCleanup = 0; |
| bool _evictionIsolateIsRunning = false; |
| |
| EvictingFileByteStore(this._cachePath, this._maxSizeBytes) |
| : _fileByteStore = new FileByteStore(_cachePath) { |
| _requestCacheCleanUp(); |
| } |
| |
| @override |
| List<int> get(String key) { |
| return _fileByteStore.get(key); |
| } |
| |
| @override |
| void put(String key, List<int> bytes) { |
| _fileByteStore.put(key, bytes); |
| // Update the current size. |
| _bytesWrittenSinceCleanup += bytes.length; |
| if (_bytesWrittenSinceCleanup > _maxSizeBytes ~/ 8) { |
| _requestCacheCleanUp(); |
| } |
| } |
| |
| /** |
| * If the cache clean up process has not been requested yet, request it. |
| */ |
| Future<Null> _requestCacheCleanUp() async { |
| if (_cleanUpSendPortShouldBePrepared) { |
| _cleanUpSendPortShouldBePrepared = false; |
| ReceivePort response = new ReceivePort(); |
| await Isolate.spawn(_cacheCleanUpFunction, response.sendPort); |
| _cleanUpSendPort = await response.first as SendPort; |
| } else { |
| while (_cleanUpSendPort == null) { |
| await new Future.delayed(new Duration(milliseconds: 100), () {}); |
| } |
| } |
| |
| if (!_evictionIsolateIsRunning) { |
| _evictionIsolateIsRunning = true; |
| try { |
| ReceivePort response = new ReceivePort(); |
| _cleanUpSendPort.send(new CacheCleanUpRequest( |
| _cachePath, _maxSizeBytes, response.sendPort)); |
| await response.first; |
| } finally { |
| _evictionIsolateIsRunning = false; |
| _bytesWrittenSinceCleanup = 0; |
| } |
| } |
| } |
| |
| /** |
| * This function is started in a new isolate, receives cache folder clean up |
| * requests and evicts older files from the folder. |
| */ |
| static void _cacheCleanUpFunction(SendPort initialReplyTo) { |
| ReceivePort port = new ReceivePort(); |
| initialReplyTo.send(port.sendPort); |
| port.listen((request) async { |
| if (request is CacheCleanUpRequest) { |
| await _cleanUpFolder(request.cachePath, request.maxSizeBytes); |
| // Let the client know that we're done. |
| request.replyTo.send(true); |
| } |
| }); |
| } |
| |
| static Future<Null> _cleanUpFolder(String cachePath, int maxSizeBytes) async { |
| // Prepare the list of files and their statistics. |
| List<File> files = <File>[]; |
| Map<File, FileStat> fileStatMap = {}; |
| int currentSizeBytes = 0; |
| List<FileSystemEntity> resources = new Directory(cachePath).listSync(); |
| for (FileSystemEntity resource in resources) { |
| if (resource is File) { |
| try { |
| FileStat fileStat = await resource.stat(); |
| files.add(resource); |
| fileStatMap[resource] = fileStat; |
| currentSizeBytes += fileStat.size; |
| } catch (_) {} |
| } |
| } |
| files.sort((a, b) { |
| return fileStatMap[a].accessed.millisecondsSinceEpoch - |
| fileStatMap[b].accessed.millisecondsSinceEpoch; |
| }); |
| |
| // Delete files until the current size is less than the max. |
| for (File file in files) { |
| if (currentSizeBytes < maxSizeBytes) { |
| break; |
| } |
| try { |
| await file.delete(); |
| } catch (_) {} |
| currentSizeBytes -= fileStatMap[file].size; |
| } |
| } |
| } |
| |
| /** |
| * [ByteStore] that stores values as files. |
| */ |
| class FileByteStore implements ByteStore { |
| final String _cachePath; |
| final String _tempName = 'temp_$pid'; |
| |
| FileByteStore(this._cachePath); |
| |
| @override |
| List<int> get(String key) { |
| try { |
| return _getFileForKey(key).readAsBytesSync(); |
| } catch (_) { |
| return null; |
| } |
| } |
| |
| @override |
| void put(String key, List<int> bytes) { |
| try { |
| File tempFile = _getFileForKey(_tempName); |
| tempFile.writeAsBytesSync(bytes); |
| File file = _getFileForKey(key); |
| tempFile.renameSync(file.path); |
| } catch (_) {} |
| } |
| |
| File _getFileForKey(String key) { |
| return new File(join(_cachePath, key)); |
| } |
| } |