blob: 65349a0d8c612e9d557ca6112b95320be3ac4496 [file] [log] [blame]
// 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:collection';
import 'package:analyzer/exception/exception.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/utilities_collection.dart';
import 'package:analyzer/src/task/api/model.dart';
import 'package:analyzer/src/task/model.dart';
/**
* The cache results visiting function type.
*/
typedef void CacheResultVisitor(AnalysisTarget target, ResultData data);
/**
* Return `true` if the [result] of the [target] should be flushed.
*/
typedef bool FlushResultFilter<V>(
AnalysisTarget target, ResultDescriptor<V> result);
/**
* Return `true` if the given [target] is a priority one.
*/
typedef bool IsPriorityAnalysisTarget(AnalysisTarget target);
/**
* An LRU cache of results produced by analysis.
*/
class AnalysisCache {
/**
* A flag used to control whether trace information should be produced when
* the content of the cache is modified.
*/
static bool _TRACE_CHANGES = false;
/**
* An array containing the partitions of which this cache is comprised.
*/
final List<CachePartition> _partitions;
/**
* The [StreamController] reporting [InvalidatedResult]s.
*/
final ReentrantSynchronousStream<InvalidatedResult> onResultInvalidated =
new ReentrantSynchronousStream<InvalidatedResult>();
final List<ReentrantSynchronousStreamSubscription>
onResultInvalidatedPartitionSubscriptions =
<ReentrantSynchronousStreamSubscription>[];
/**
* Initialize a newly created cache to have the given [_partitions]. The
* partitions will be searched in the order in which they appear in the array,
* so the most specific partition (usually an [SdkCachePartition]) should be
* first and the most general (usually a [UniversalCachePartition]) last.
*/
AnalysisCache(this._partitions) {
for (CachePartition partition in _partitions) {
partition.containingCaches.add(this);
ReentrantSynchronousStreamSubscription<InvalidatedResult> subscription =
partition.onResultInvalidated.listen((InvalidatedResult event) {
onResultInvalidated.add(event);
});
onResultInvalidatedPartitionSubscriptions.add(subscription);
}
}
/**
* Return an iterator returning all of the [Source] targets.
*/
Iterable<Source> get sources {
return _partitions
.map((CachePartition partition) => partition.sources)
.expand((Iterable<Source> sources) => sources);
}
// TODO(brianwilkerson) Implement or delete this.
// /**
// * Return information about each of the partitions in this cache.
// */
// List<AnalysisContextStatistics_PartitionData> get partitionData {
// int count = _partitions.length;
// List<AnalysisContextStatistics_PartitionData> data =
// new List<AnalysisContextStatistics_PartitionData>(count);
// for (int i = 0; i < count; i++) {
// CachePartition partition = _partitions[i];
// data[i] = new AnalysisContextStatisticsImpl_PartitionDataImpl(
// partition.astSize,
// partition.map.length);
// }
// return data;
// }
/**
* Free any allocated resources and references.
*/
void dispose() {
for (ReentrantSynchronousStreamSubscription subscription
in onResultInvalidatedPartitionSubscriptions) {
subscription.cancel();
}
for (CachePartition partition in _partitions) {
partition.containingCaches.remove(this);
}
}
/**
* Flush results that satisfy the given [filter].
*/
void flush(FlushResultFilter filter) {
for (CachePartition partition in _partitions) {
partition.flush(filter);
}
}
/**
* Return the entry associated with the given [target].
*/
CacheEntry get(AnalysisTarget target) {
int count = _partitions.length;
for (int i = 0; i < count; i++) {
CachePartition partition = _partitions[i];
if (partition.isResponsibleFor(target)) {
return partition.get(target);
}
}
//
// We should never get to this point because the last partition should
// always be a universal partition, except in the case of the SDK context,
// in which case the target should always be part of the SDK.
//
return null;
}
/**
* Return the context to which the given [target] was explicitly added.
*/
InternalAnalysisContext getContextFor(AnalysisTarget target) {
int count = _partitions.length;
for (int i = 0; i < count; i++) {
CachePartition partition = _partitions[i];
if (partition.isResponsibleFor(target)) {
return partition.context;
}
}
//
// We should never get to this point because the last partition should
// always be a universal partition, except in the case of the SDK context,
// in which case the target should always be part of the SDK.
//
// TODO(brianwilkerson) Throw an exception here.
AnalysisEngine.instance.logger.logInformation(
'Could not find context for $target',
new CaughtException(new AnalysisException(), null));
return null;
}
/**
* Return [Source]s whose full path is equal to the given [path].
* Maybe empty, but not `null`.
*/
List<Source> getSourcesWithFullName(String path) {
List<Source> sources = <Source>[];
for (CachePartition partition in _partitions) {
List<Source> partitionSources = partition.getSourcesWithFullName(path);
sources.addAll(partitionSources);
}
return sources;
}
/**
* Return the state of the given [result] for the given [target].
*
* It does not update the cache, if the corresponding [CacheEntry] does not
* exist, then [CacheState.INVALID] is returned.
*/
CacheState getState(AnalysisTarget target, ResultDescriptor result) {
CacheEntry entry = get(target);
if (entry == null) {
return CacheState.INVALID;
}
return entry.getState(result);
}
/**
* Return the value of the given [result] for the given [target].
*
* It does not update the cache, if the corresponding [CacheEntry] does not
* exist, then the default value is returned.
*/
V getValue<V>(AnalysisTarget target, ResultDescriptor<V> result) {
CacheEntry entry = get(target);
if (entry == null) {
return result.defaultValue;
}
return entry.getValue(result);
}
/**
* Return an iterator returning all of the map entries mapping targets to
* cache entries. If the [context] is not `null`, then only entries that are
* owned by the given context will be returned.
*/
MapIterator<AnalysisTarget, CacheEntry> iterator(
{InternalAnalysisContext context: null}) {
List<Map<AnalysisTarget, CacheEntry>> maps =
<Map<AnalysisTarget, CacheEntry>>[];
for (CachePartition partition in _partitions) {
if (context == null || partition.context == context) {
maps.add(partition.entryMap);
}
}
return new MultipleMapIterator<AnalysisTarget, CacheEntry>(maps);
}
/**
* Puts the given [entry] into the cache.
*/
void put(CacheEntry entry) {
AnalysisTarget target = entry.target;
entry.fixExceptionState();
int count = _partitions.length;
for (int i = 0; i < count; i++) {
CachePartition partition = _partitions[i];
if (partition.isResponsibleFor(target)) {
if (_TRACE_CHANGES) {
CacheEntry oldEntry = partition.get(target);
if (oldEntry == null) {
AnalysisEngine.instance.logger
.logInformation('Added a cache entry for $target.');
} else {
AnalysisEngine.instance.logger
.logInformation('Modified the cache entry for $target.');
// 'Diff = ${entry.getDiff(oldEntry)}');
}
}
partition.put(entry);
return;
}
}
// TODO(brianwilkerson) Handle the case where no partition was found,
// possibly by throwing an exception.
}
/**
* Remove all information related to the given [target] from this cache.
* Return the entry associated with the target, or `null` if there was cache
* entry for the target.
*/
CacheEntry remove(AnalysisTarget target) {
int count = _partitions.length;
for (int i = 0; i < count; i++) {
CachePartition partition = _partitions[i];
if (partition.isResponsibleFor(target)) {
if (_TRACE_CHANGES) {
AnalysisEngine.instance.logger
.logInformation('Removed the cache entry for $target.');
}
CacheEntry entry = partition.remove(target);
if (entry != null) {
entry.dispose();
}
return entry;
}
}
return null;
}
/**
* Return the number of targets that are mapped to cache entries.
*/
int size() {
int size = 0;
int count = _partitions.length;
for (int i = 0; i < count; i++) {
size += _partitions[i].size();
}
return size;
}
}
/**
* The information cached by an analysis context about an individual target.
*/
class CacheEntry {
/**
* The index of the flag indicating whether the source was explicitly added to
* the context or whether the source was implicitly added because it was
* referenced by another source.
*/
static int _EXPLICITLY_ADDED_FLAG = 0;
/**
* The next visit process identifier.
*/
static int nextVisitId = 0;
/**
* A table containing the number of times the value of a result descriptor was
* recomputed after having been flushed.
*/
static final Map<ResultDescriptor, int> recomputedCounts =
new HashMap<ResultDescriptor, int>();
/**
* The target this entry is about.
*/
final AnalysisTarget target;
/**
* The partition that is responsible for this entry.
*/
CachePartition _partition;
/**
* The most recent time at which the state of the target matched the state
* represented by this entry, `-1` if the target does not exist.
*/
int modificationTime = -1;
/**
* The exception that caused one or more values to have a state of
* [CacheState.ERROR].
*/
CaughtException _exception;
/**
* A bit-encoding of boolean flags associated with this entry's target.
*/
int _flags = 0;
/**
* A table mapping result descriptors to the cached values of those results.
*/
Map<ResultDescriptor, ResultData> _resultMap =
new HashMap<ResultDescriptor, ResultData>();
CacheEntry(this.target);
/**
* The exception that caused one or more values to have a state of
* [CacheState.ERROR].
*/
CaughtException get exception => _exception;
/**
* Return `true` if the source was explicitly added to the context or `false`
* if the source was implicitly added because it was referenced by another
* source.
*/
bool get explicitlyAdded => _getFlag(_EXPLICITLY_ADDED_FLAG);
/**
* Set whether the source was explicitly added to the context to match the
* [explicitlyAdded] flag.
*/
void set explicitlyAdded(bool explicitlyAdded) {
_setFlag(_EXPLICITLY_ADDED_FLAG, explicitlyAdded);
}
/**
* Return a list of result descriptors for results whose state is not
* [CacheState.INVALID].
*/
List<ResultDescriptor> get nonInvalidResults => _resultMap.keys.toList();
/**
* Notifies the entry that the client is going to stop using it.
*/
void dispose() {
_resultMap.forEach((ResultDescriptor descriptor, ResultData data) {
TargetedResult result = new TargetedResult(target, descriptor);
for (TargetedResult dependedOnResult in data.dependedOnResults) {
for (AnalysisCache cache in _partition.containingCaches) {
CacheEntry entry = cache.get(dependedOnResult.target);
if (entry != null) {
ResultData data =
entry.getResultDataOrNull(dependedOnResult.result);
if (data != null) {
data.dependentResults.remove(result);
}
}
}
}
});
_resultMap.clear();
}
/**
* Fix the state of the [exception] to match the current state of the entry.
*/
void fixExceptionState() {
if (!hasErrorState()) {
_exception = null;
}
}
/**
* Flush results that satisfy the given [filter].
*/
void flush(FlushResultFilter filter) {
_resultMap.forEach((ResultDescriptor result, ResultData data) {
if (filter(target, result)) {
data.flush();
}
});
}
/**
* Return the result data associated with the [descriptor], creating one if it
* isn't there.
*/
ResultData getResultData(ResultDescriptor descriptor) {
return _resultMap.putIfAbsent(descriptor, () => new ResultData(descriptor));
}
/**
* Return the result data associated with the [descriptor], or `null` if there
* is no data currently associated with the descriptor.
*/
ResultData getResultDataOrNull(ResultDescriptor descriptor) =>
_resultMap[descriptor];
/**
* Return the state of the result represented by the given [descriptor].
*/
CacheState getState(ResultDescriptor descriptor) {
ResultData data = _resultMap[descriptor];
if (data == null) {
return CacheState.INVALID;
}
return data.state;
}
/**
* Return the value of the result represented by the given [descriptor], or
* the default value for the result if this entry does not have a valid value.
*/
V getValue<V>(ResultDescriptor<V> descriptor) {
ResultData data = _resultMap[descriptor];
if (data == null) {
return descriptor.defaultValue;
}
if (_partition != null) {
_partition.resultAccessed(target, descriptor);
}
return data.value as V;
}
/**
* Return `true` if the state of any data value is [CacheState.ERROR].
*/
bool hasErrorState() {
for (ResultData data in _resultMap.values) {
if (data.state == CacheState.ERROR) {
return true;
}
}
return false;
}
/**
* Invalidate all of the information associated with this entry's target.
*/
void invalidateAllInformation() {
_resultMap.clear();
_exception = null;
}
/**
* Return `true` if the state of the result represented by the given
* [descriptor] is [CacheState.INVALID].
*/
bool isInvalid(ResultDescriptor descriptor) =>
getState(descriptor) == CacheState.INVALID;
/**
* Return `true` if the state of the result represented by the given
* [descriptor] is [CacheState.VALID].
*/
bool isValid(ResultDescriptor descriptor) =>
getState(descriptor) == CacheState.VALID;
/**
* For each of the given [descriptors], set their states to
* [CacheState.ERROR], their values to the corresponding default values, and
* remember the [exception] that caused this state.
*/
void setErrorState(
CaughtException exception, List<ResultDescriptor> descriptors) {
if (descriptors == null || descriptors.isEmpty) {
throw new ArgumentError('at least one descriptor is expected');
}
if (exception == null) {
throw new ArgumentError('an exception is expected');
}
this._exception = exception;
for (ResultDescriptor descriptor in descriptors) {
_setErrorState(descriptor, exception);
}
}
/**
* Set the state of the result represented by the given [descriptor] to the
* given [state].
*/
void setState(ResultDescriptor descriptor, CacheState state, {Delta delta}) {
if (state == CacheState.ERROR) {
throw new ArgumentError('use setErrorState() to set the state to ERROR');
}
if (state == CacheState.VALID) {
throw new ArgumentError('use setValue() to set the state to VALID');
}
_validateStateChange(descriptor, state);
if (state == CacheState.INVALID) {
ResultData data = _resultMap[descriptor];
if (data != null) {
bool canUseDelta =
_gatherResultsInvalidatedByDelta(descriptor, delta, 0);
if (!canUseDelta) {
delta = null;
}
_invalidate(nextVisitId++, descriptor, delta, 0);
}
} else {
ResultData data = getResultData(descriptor);
data.state = state;
if (state != CacheState.IN_PROCESS) {
//
// If the state is in-process, we can leave the current value in the
// cache for any 'get' methods to access.
//
data.value = descriptor.defaultValue;
}
}
}
/**
* Set the value of the result represented by the given [descriptor] to the
* given [value].
*/
void setValue<V>(ResultDescriptor<V> descriptor, V value,
List<TargetedResult> dependedOn) {
// {
// String valueStr = '$value';
// if (valueStr.length > 20) {
// valueStr = valueStr.substring(0, 20) + '...';
// }
// valueStr = valueStr.replaceAll('\n', '\\n');
// print(
// 'setValue $descriptor for $target value=$valueStr dependedOn=$dependedOn');
// }
_validateStateChange(descriptor, CacheState.VALID);
TargetedResult thisResult = new TargetedResult(target, descriptor);
if (_partition != null) {
_partition.resultStored(thisResult, value);
}
ResultData data = getResultData(descriptor);
_setDependedOnResults(data, thisResult, dependedOn);
if (data.state == CacheState.FLUSHED) {
int count = recomputedCounts[descriptor] ?? 0;
recomputedCounts[descriptor] = count + 1;
}
data.state = CacheState.VALID;
data.value = value ?? descriptor.defaultValue;
}
/**
* If the result represented by the given [descriptor] is valid, set
* it to the given [value], keep its dependency, and if [invalidateDependent]
* invalidate all the dependent result.
*/
void setValueIncremental(
ResultDescriptor descriptor, dynamic value, bool invalidateDependent) {
ResultData data = getResultData(descriptor);
if (data.state == CacheState.VALID || data.state == CacheState.FLUSHED) {
data.value = value;
}
if (invalidateDependent) {
_invalidateDependentResults(nextVisitId++, data, null, 0);
}
}
@override
String toString() {
StringBuffer buffer = new StringBuffer();
_writeOn(buffer);
return buffer.toString();
}
/**
* Visit the given [result] and all results that depend on it, and
* ask [delta] to gather changes. Return `true` if the [delta] can be used
* to perform limited invalidation, or `false` if the changes collection
* process does not stop (should not happen).
*/
bool _gatherResultsInvalidatedByDelta(
ResultDescriptor result, Delta delta, int level) {
if (delta == null) {
return false;
}
if (!delta.shouldGatherChanges) {
return true;
}
for (int i = 0; i < 64; i++) {
bool hasVisitChanges = false;
_visitResults(nextVisitId++, result,
(AnalysisTarget target, ResultData data) {
bool hasDeltaChanges = delta.gatherChanges(
_partition.context, target, data.descriptor, data.value);
if (hasDeltaChanges) {
hasVisitChanges = true;
}
});
delta.gatherEnd();
if (!hasVisitChanges) {
return true;
}
}
return false;
}
/**
* Return the value of the flag with the given [index].
*/
bool _getFlag(int index) => BooleanArray.get(_flags, index);
/**
* Invalidate the result represented by the given [descriptor] and propagate
* invalidation to other results that depend on it.
*/
void _invalidate(
int id, ResultDescriptor descriptor, Delta delta, int level) {
ResultData thisData = _resultMap[descriptor];
if (thisData == null) {
return;
}
// Stop if already validated.
if (delta != null) {
if (thisData.visitId == id) {
return;
}
thisData.visitId = id;
}
// Ask the delta to validate.
DeltaResult deltaResult = null;
if (delta != null) {
deltaResult = delta.validate(
_partition.context, target, descriptor, thisData.value);
if (deltaResult == DeltaResult.STOP) {
return;
}
}
// if (deltaResult != null) {
// String indent = ' ' * level;
// String deltaResultName = deltaResult.toString().split('.').last;
// print('[$id]$indent$deltaResultName $descriptor for $target');
// }
if (deltaResult == DeltaResult.INVALIDATE_NO_DELTA) {
delta = null;
}
if (deltaResult == DeltaResult.INVALIDATE_KEEP_DEPENDENCIES) {
thisData.value = descriptor.defaultValue;
thisData.state = CacheState.INVALID;
} else if (deltaResult == null ||
deltaResult == DeltaResult.INVALIDATE ||
deltaResult == DeltaResult.INVALIDATE_NO_DELTA) {
_resultMap.remove(descriptor);
// Stop depending on other results.
{
TargetedResult thisResult = new TargetedResult(target, descriptor);
List<AnalysisCache> caches = _partition.containingCaches;
int cacheLength = caches.length;
List<TargetedResult> results = thisData.dependedOnResults;
int resultLength = results.length;
for (int i = 0; i < resultLength; i++) {
TargetedResult dependedOnResult = results[i];
for (int j = 0; j < cacheLength; j++) {
AnalysisCache cache = caches[j];
CacheEntry entry = cache.get(dependedOnResult.target);
if (entry != null) {
ResultData data =
entry.getResultDataOrNull(dependedOnResult.result);
if (data != null) {
data.dependentResults.remove(thisResult);
}
}
}
}
}
// if (deltaResult == null) {
// String indent = ' ' * level;
// print('[$id]$indent invalidate $descriptor for $target');
// }
}
// Invalidate results that depend on this result.
_invalidateDependentResults(id, thisData, delta, level + 1);
// If empty and not explicitly added, remove the entry altogether.
if (_resultMap.isEmpty && !explicitlyAdded) {
CacheEntry entry = _partition.entryMap.remove(target);
if (entry != null) {
entry.dispose();
}
_partition._removeIfSource(target);
}
// Notify controller.
if (deltaResult != DeltaResult.KEEP_CONTINUE) {
_partition.onResultInvalidated
.add(new InvalidatedResult(this, descriptor, thisData.value));
}
}
/**
* Invalidates all the results of this entry, with propagation.
*/
void _invalidateAll() {
List<ResultDescriptor> results = _resultMap.keys.toList();
int length = results.length;
for (int i = 0; i < length; i++) {
ResultDescriptor result = results[i];
_invalidate(nextVisitId++, result, null, 0);
}
}
/**
* Invalidate results that depend on [thisData].
*/
void _invalidateDependentResults(
int id, ResultData thisData, Delta delta, int level) {
// It is necessary to copy the results to a list to avoid a concurrent
// modification of the set of dependent results.
List<AnalysisCache> caches = _partition.containingCaches;
int cacheLength = caches.length;
List<TargetedResult> dependentResults = thisData.dependentResults.toList();
int resultLength = dependentResults.length;
for (int i = 0; i < resultLength; i++) {
TargetedResult dependentResult = dependentResults[i];
for (int j = 0; j < cacheLength; j++) {
AnalysisCache cache = caches[j];
CacheEntry entry = cache.get(dependentResult.target);
if (entry != null) {
entry._invalidate(id, dependentResult.result, delta, level);
}
}
}
}
/**
* Set the [dependedOn] on which this result depends.
*/
void _setDependedOnResults(ResultData thisData, TargetedResult thisResult,
List<TargetedResult> dependedOn) {
List<AnalysisCache> caches = _partition.containingCaches;
int cacheLength = caches.length;
List<TargetedResult> oldResults = thisData.dependedOnResults;
int oldLength = oldResults.length;
for (int i = 0; i < oldLength; i++) {
TargetedResult dependedOnResult = oldResults[i];
for (int j = 0; j < cacheLength; j++) {
AnalysisCache cache = caches[j];
CacheEntry entry = cache.get(dependedOnResult.target);
if (entry != null) {
ResultData data = entry.getResultDataOrNull(dependedOnResult.result);
if (data != null) {
data.dependentResults.remove(thisResult);
}
}
}
}
thisData.dependedOnResults = dependedOn;
int newLength = dependedOn.length;
for (int i = 0; i < newLength; i++) {
TargetedResult dependedOnResult = dependedOn[i];
for (int j = 0; j < cacheLength; j++) {
AnalysisCache cache = caches[j];
CacheEntry entry = cache.get(dependedOnResult.target);
if (entry != null) {
ResultData data = entry.getResultData(dependedOnResult.result);
data.dependentResults.add(thisResult);
}
}
}
}
/**
* Set states of the given and dependent results to [CacheState.ERROR] and
* their values to the corresponding default values
*/
void _setErrorState(ResultDescriptor descriptor, CaughtException exception) {
ResultData thisData = getResultData(descriptor);
// Set the error state.
_exception = exception;
thisData.state = CacheState.ERROR;
thisData.value = descriptor.defaultValue;
// Propagate the error state.
List<AnalysisCache> caches = _partition.containingCaches;
int cacheLength = caches.length;
thisData.dependentResults.forEach((TargetedResult dependentResult) {
for (int i = 0; i < cacheLength; i++) {
AnalysisCache cache = caches[i];
CacheEntry entry = cache.get(dependentResult.target);
if (entry != null) {
entry._setErrorState(dependentResult.result, exception);
}
}
});
}
/**
* Set the value of the flag with the given [index] to the given [value].
*/
void _setFlag(int index, bool value) {
_flags = BooleanArray.set(_flags, index, value);
}
/**
* If the state of the value described by the given [descriptor] is changing
* from ERROR to anything else, capture the information. This is an attempt to
* discover the underlying cause of a long-standing bug.
*/
void _validateStateChange(ResultDescriptor descriptor, CacheState newState) {
// TODO(brianwilkerson) Decide whether we still want to capture this data.
// if (descriptor != CONTENT) {
// return;
// }
// ResultData data = resultMap[CONTENT];
// if (data != null && data.state == CacheState.ERROR) {
// String message =
// 'contentState changing from ${data.state} to $newState';
// InstrumentationBuilder builder =
// Instrumentation.builder2('CacheEntry-validateStateChange');
// builder.data3('message', message);
// //builder.data('source', source.getFullName());
// builder.record(new CaughtException(new AnalysisException(message), null));
// builder.log();
// }
}
/**
* Call [visitor] for the result described by the given [descriptor] and all
* results that depend on directly or indirectly. Each result is visited
* only once.
*/
void _visitResults(
int id, ResultDescriptor descriptor, CacheResultVisitor visitor) {
ResultData thisData = _resultMap[descriptor];
if (thisData == null) {
return;
}
// Stop if already visited.
if (thisData.visitId == id) {
return;
}
thisData.visitId = id;
// Visit this result.
visitor(target, thisData);
// Visit results that depend on this result.
List<AnalysisCache> caches = _partition.containingCaches;
int cacheLength = caches.length;
List<TargetedResult> dependentResults = thisData.dependentResults.toList();
int resultLength = dependentResults.length;
for (int i = 0; i < resultLength; i++) {
TargetedResult dependentResult = dependentResults[i];
for (int j = 0; j < cacheLength; j++) {
AnalysisCache cache = caches[j];
CacheEntry entry = cache.get(dependentResult.target);
if (entry != null) {
entry._visitResults(id, dependentResult.result, visitor);
}
}
}
}
/**
* Write a textual representation of this entry to the given [buffer]. The
* result should only be used for debugging purposes.
*/
void _writeOn(StringBuffer buffer) {
buffer.write('time = ');
buffer.write(modificationTime);
List<ResultDescriptor> results = _resultMap.keys.toList();
results.sort((ResultDescriptor first, ResultDescriptor second) =>
first.toString().compareTo(second.toString()));
for (ResultDescriptor result in results) {
ResultData data = _resultMap[result];
buffer.write('; ');
buffer.write(result.toString());
buffer.write(' = ');
buffer.write(data.state);
}
}
}
/**
* An object that controls flushing of analysis results from the cache.
*/
class CacheFlushManager<T> {
final IsPriorityAnalysisTarget isPriorityAnalysisTarget;
final ResultCachingPolicy policy;
final int maxActiveSize;
final int maxIdleSize;
/**
* A map of the stored [TargetedResult] to their sizes.
*/
final HashMap<TargetedResult, int> resultSizeMap =
new HashMap<TargetedResult, int>();
/**
* A linked set containing the most recently accessed results with the most
* recently used at the end of the list. When more results are added than the
* maximum size allowed then the least recently used results will be flushed
* from the cache.
*/
final LinkedHashSet<TargetedResult> recentlyUsed =
new LinkedHashSet<TargetedResult>();
/**
* The current size of stored results.
*/
int currentSize = 0;
/**
* The current maximum cache size.
*/
int maxSize;
CacheFlushManager(ResultCachingPolicy policy, this.isPriorityAnalysisTarget)
: policy = policy,
maxActiveSize = policy.maxActiveSize,
maxIdleSize = policy.maxIdleSize,
maxSize = policy.maxActiveSize;
/**
* If [currentSize] is already less than [maxSize], returns an empty list.
* Otherwise returns [TargetedResult]s to flush from the cache to make
* [currentSize] less or equal to [maxSize].
*
* Results for priority files are never flushed, so this method might leave
* [currentSize] greater than [maxSize].
*/
List<TargetedResult> flushToSize() {
// If still under the cap, done.
if (currentSize <= maxSize) {
return const <TargetedResult>[];
}
// Flush results until we are under the cap.
List<TargetedResult> resultsToFlush = <TargetedResult>[];
for (TargetedResult result in recentlyUsed) {
if (isPriorityAnalysisTarget(result.target)) {
continue;
}
resultsToFlush.add(result);
int size = resultSizeMap.remove(result);
assert(size != null);
currentSize -= size;
if (currentSize <= maxSize) {
break;
}
}
recentlyUsed.removeAll(resultsToFlush);
return resultsToFlush;
}
/**
* Notifies this manager that the corresponding analysis context is active.
*/
void madeActive() {
maxSize = maxActiveSize;
}
/**
* Notifies this manager that the corresponding analysis context is idle.
* Returns [TargetedResult]s that should be flushed from the cache.
*/
List<TargetedResult> madeIdle() {
maxSize = maxIdleSize;
return flushToSize();
}
/**
* Records that the given [result] was just read from the cache.
*/
void resultAccessed(TargetedResult result) {
if (recentlyUsed.remove(result)) {
recentlyUsed.add(result);
}
}
/**
* Records that the given [newResult] and [newValue] were stored to the cache.
* Returns [TargetedResult]s that should be flushed from the cache.
*/
List<TargetedResult> resultStored(TargetedResult newResult, T newValue) {
if (!recentlyUsed.remove(newResult)) {
int size = policy.measure(newValue);
resultSizeMap[newResult] = size;
currentSize += size;
}
recentlyUsed.add(newResult);
return flushToSize();
}
/**
* Records that the given [target] was just removed from to the cache.
*/
void targetRemoved(AnalysisTarget target) {
List<TargetedResult> resultsToRemove = <TargetedResult>[];
for (TargetedResult result in recentlyUsed) {
if (result.target == target) {
resultsToRemove.add(result);
int size = resultSizeMap.remove(result);
assert(size != null);
currentSize -= size;
}
}
recentlyUsed.removeAll(resultsToRemove);
}
}
/**
* A single partition in an LRU cache of information related to analysis.
*/
abstract class CachePartition {
/**
* The context that owns this partition. Multiple contexts can reference a
* partition, but only one context can own it.
*/
final InternalAnalysisContext context;
/**
* A list of the caches that contain this partition. This includes the cache
* associated with the context that owns this partition.
*/
final List<AnalysisCache> containingCaches = <AnalysisCache>[];
/**
* A table mapping caching policies to the cache flush managers.
*/
final HashMap<ResultCachingPolicy, CacheFlushManager> _flushManagerMap =
new HashMap<ResultCachingPolicy, CacheFlushManager>();
/**
* The [StreamController] reporting [InvalidatedResult]s.
*/
final ReentrantSynchronousStream<InvalidatedResult> onResultInvalidated =
new ReentrantSynchronousStream<InvalidatedResult>();
/**
* A table mapping the targets belonging to this partition to the information
* known about those targets.
*/
final HashMap<AnalysisTarget, CacheEntry> entryMap =
new HashMap<AnalysisTarget, CacheEntry>();
/**
* A set of the [Source] targets.
*/
final HashSet<Source> sources = new HashSet<Source>();
/**
* A table mapping full paths to lists of [Source]s with these full paths.
*/
final Map<String, List<Source>> pathToSource = <String, List<Source>>{};
/**
* Initialize a newly created cache partition, belonging to the given
* [context].
*/
CachePartition(this.context);
/**
* Specify whether a context that uses this partition is being analyzed.
*/
set isActive(bool active) {
for (CacheFlushManager manager in _flushManagerMap.values) {
if (active) {
manager.madeActive();
} else {
List<TargetedResult> resultsToFlush = manager.madeIdle();
_flushResults(resultsToFlush);
}
}
}
/**
* Notifies the partition that the client is going to stop using it.
*/
void dispose() {
for (CacheEntry entry in entryMap.values) {
entry.dispose();
}
entryMap.clear();
sources.clear();
pathToSource.clear();
}
/**
* Flush results that satisfy the given [filter].
*/
void flush(FlushResultFilter filter) {
for (CacheEntry entry in entryMap.values) {
entry.flush(filter);
}
}
/**
* Return the entry associated with the given [target].
*/
CacheEntry get(AnalysisTarget target) => entryMap[target];
/**
* Return [Source]s whose full path is equal to the given [path].
* Maybe empty, but not `null`.
*/
List<Source> getSourcesWithFullName(String path) {
List<Source> sources = pathToSource[path];
return sources ?? const <Source>[];
}
/**
* Return `true` if this partition is responsible for the given [target].
*/
bool isResponsibleFor(AnalysisTarget target);
/**
* Return an iterator returning all of the map entries mapping targets to
* cache entries.
*/
MapIterator<AnalysisTarget, CacheEntry> iterator() =>
new SingleMapIterator<AnalysisTarget, CacheEntry>(entryMap);
/**
* Puts the given [entry] into the partition.
*/
void put(CacheEntry entry) {
AnalysisTarget target = entry.target;
if (entry._partition != null) {
throw new StateError(
'The entry for $target is already in ${entry._partition}');
}
entry._partition = this;
entry.fixExceptionState();
entryMap[target] = entry;
_addIfSource(target);
}
/**
* Remove all information related to the given [target] from this partition.
* Return the entry associated with the target, or `null` if there was cache
* entry for the target.
*/
CacheEntry remove(AnalysisTarget target) {
for (CacheFlushManager flushManager in _flushManagerMap.values) {
flushManager.targetRemoved(target);
}
CacheEntry entry = entryMap.remove(target);
if (entry != null) {
entry._invalidateAll();
}
_removeIfSource(target);
return entry;
}
/**
* Records that a value of the result described by the given [descriptor]
* for the given [target] was just read from the cache.
*/
void resultAccessed(AnalysisTarget target, ResultDescriptor descriptor) {
CacheFlushManager flushManager = _getFlushManager(descriptor);
TargetedResult result = new TargetedResult(target, descriptor);
flushManager.resultAccessed(result);
}
/**
* Records that the given [result] was just stored into the cache.
*/
void resultStored(TargetedResult result, Object value) {
CacheFlushManager flushManager = _getFlushManager(result.result);
List<TargetedResult> resultsToFlush =
flushManager.resultStored(result, value);
_flushResults(resultsToFlush);
}
/**
* Return the number of targets that are mapped to cache entries.
*/
int size() => entryMap.length;
/**
* If the given [target] is a [Source], adds it to [sources].
*/
void _addIfSource(AnalysisTarget target) {
if (target is Source) {
sources.add(target);
String fullName = target.fullName;
pathToSource.putIfAbsent(fullName, () => <Source>[]).add(target);
}
}
/**
* Flush the given [resultsToFlush].
*/
void _flushResults(List<TargetedResult> resultsToFlush) {
for (TargetedResult result in resultsToFlush) {
CacheEntry entry = get(result.target);
if (entry != null) {
ResultData data = entry._resultMap[result.result];
if (data != null) {
data.flush();
}
}
}
}
/**
* Return the [CacheFlushManager] for the given [descriptor], not `null`.
*/
CacheFlushManager _getFlushManager(ResultDescriptor descriptor) {
ResultCachingPolicy policy = descriptor.cachingPolicy;
if (identical(policy, DEFAULT_CACHING_POLICY) ||
context.analysisOptions.disableCacheFlushing) {
return UnlimitedCacheFlushManager.INSTANCE;
}
CacheFlushManager manager = _flushManagerMap[policy];
if (manager == null) {
manager = new CacheFlushManager(policy, _isPriorityAnalysisTarget);
_flushManagerMap[policy] = manager;
}
return manager;
}
bool _isPriorityAnalysisTarget(AnalysisTarget target) {
Source source = target.source;
return source != null && context.prioritySources.contains(source);
}
/**
* If the given [target] is a [Source], remove it from the list of [sources].
*/
void _removeIfSource(AnalysisTarget target) {
if (target is Source) {
sources.remove(target);
String path = target.fullName;
List<Source> pathSources = pathToSource[path];
if (pathSources != null) {
pathSources.remove(target);
if (pathSources.isEmpty) {
pathToSource.remove(path);
}
}
}
}
}
/**
* The description for a change.
*/
class Delta {
final Source source;
Delta(this.source);
/**
* Return `true` if this delta needs cache walking to gather additional
* changes before it can be used to [validate]. In this case [gatherChanges]
* is invoked for every targeted result in transitive dependencies, and
* [gatherEnd] is invoked after cache walking is done.
*/
bool get shouldGatherChanges => false;
/**
* This method is called during a cache walk, so that the delta can gather
* additional changes to which are caused by the changes it already knows
* about. Return `true` if a new change was added, so that one more cache
* walk will be performed (to include changes that depend on results which we
* decided to be changed later in the previous cache walk).
*/
bool gatherChanges(InternalAnalysisContext context, AnalysisTarget target,
ResultDescriptor descriptor, Object value) {
return false;
}
/**
* The current cache results visit is done.
*/
void gatherEnd() {}
/**
* Check whether this delta affects the result described by the given
* [descriptor] and [target]. The current [value] of the result is provided.
*/
DeltaResult validate(InternalAnalysisContext context, AnalysisTarget target,
ResultDescriptor descriptor, Object value) {
return DeltaResult.INVALIDATE;
}
}
/**
* The possible results of validating analysis results against a [Delta].
*/
enum DeltaResult {
/**
* Invalidate this result and continue visiting dependent results
* with this [Delta]. Remove the result and all its dependencies.
*/
INVALIDATE,
/**
* Invalidate this result and continue visiting dependent results
* with this [Delta]. Keep the dependencies of this result.
*/
INVALIDATE_KEEP_DEPENDENCIES,
/**
* Invalidate this result and stop using this [Delta], so unconditionally
* invalidate all the dependent results.
*/
INVALIDATE_NO_DELTA,
/**
* Keep this result and continue validating dependent results
* with this [Delta].
*/
KEEP_CONTINUE,
/**
* Keep this result and stop visiting results that depend on this one.
*/
STOP
}
/**
* [InvalidatedResult] describes an invalidated result.
*/
class InvalidatedResult<V> {
/**
* The target in which the result was invalidated.
*/
final CacheEntry entry;
/**
* The descriptor of the result which was invalidated.
*/
final ResultDescriptor<V> descriptor;
/**
* The value of the result before it was invalidated, may be the default
* value if the result was flushed.
*/
final V value;
InvalidatedResult(this.entry, this.descriptor, this.value);
@override
String toString() => '$descriptor of ${entry.target}';
}
/**
* A cache partition that contains all of the targets in a single package.
*/
class PackageCachePartition extends CachePartition {
/**
* The root of the directory representing the package.
*/
final Folder packageRoot;
/**
* Initialize a newly created cache partition, belonging to the given
* [context].
*/
PackageCachePartition(InternalAnalysisContext context, this.packageRoot)
: super(context);
@override
bool isResponsibleFor(AnalysisTarget target) {
Source source = target.source;
return source != null && packageRoot.contains(source.fullName);
}
}
/**
* A Stream-like interface, which broadcasts events synchronously.
* If a second event is fired while delivering a first event, then the second
* event will be delivered first, and then delivering of the first will be
* continued.
*/
class ReentrantSynchronousStream<T> {
final List<Function> listeners = <Function>[];
/**
* Send the given [event] to the stream.
*/
void add(T event) {
List<Function> listeners = this.listeners.toList();
for (Function listener in listeners) {
listener(event);
}
}
/**
* Listen for the events in this stream.
* Note that if the [listener] fires a new event, then the [listener] will be
* invoked again before returning from the [add] invocation.
*/
ReentrantSynchronousStreamSubscription<T> listen(void listener(T event)) {
listeners.add(listener);
return new ReentrantSynchronousStreamSubscription<T>(this, listener);
}
}
/**
* A subscription on events from a [ReentrantSynchronousStream].
*/
class ReentrantSynchronousStreamSubscription<T> {
final ReentrantSynchronousStream<T> _stream;
final Function _listener;
ReentrantSynchronousStreamSubscription(this._stream, this._listener);
/**
* Cancels this subscription.
* It will no longer receive events.
*/
void cancel() {
_stream.listeners.remove(_listener);
}
}
/**
* The data about a single analysis result that is stored in a [CacheEntry].
*/
// TODO(brianwilkerson) Consider making this a generic class so that the value
// can be typed.
class ResultData {
/**
* The [ResultDescriptor] this result is for.
*/
final ResultDescriptor descriptor;
/**
* The state of the cached value.
*/
CacheState state;
/**
* The value being cached, or the default value for the result if there is no
* value (for example, when the [state] is [CacheState.INVALID]).
*/
Object value;
/**
* The identifier of the most recent visiting process. We use it to visit
* every result only once.
*/
int visitId = -1;
/**
* A list of the results on which this result depends.
*/
List<TargetedResult> dependedOnResults = <TargetedResult>[];
/**
* A list of the results that depend on this result.
*/
Set<TargetedResult> dependentResults = new Set<TargetedResult>();
/**
* Initialize a newly created result holder to represent the value of data
* described by the given [descriptor].
*/
ResultData(this.descriptor) {
state = CacheState.INVALID;
value = descriptor.defaultValue;
}
/**
* Flush this value.
*/
void flush() {
state = CacheState.FLUSHED;
value = descriptor.defaultValue;
}
}
/**
* A cache partition that contains all of the targets in the SDK.
*/
class SdkCachePartition extends CachePartition {
/**
* Initialize a newly created cache partition, belonging to the given
* [context].
*/
SdkCachePartition(InternalAnalysisContext context) : super(context);
@override
bool isResponsibleFor(AnalysisTarget target) {
if (target is AnalysisContextTarget) {
return true;
}
Source source = target.source;
return source != null && source.isInSystemLibrary;
}
}
/**
* A cache partition that contains all targets not contained in other partitions.
*/
class UniversalCachePartition extends CachePartition {
/**
* Initialize a newly created cache partition, belonging to the given
* [context].
*/
UniversalCachePartition(InternalAnalysisContext context) : super(context);
@override
bool isResponsibleFor(AnalysisTarget target) => true;
}
/**
* [CacheFlushManager] that does nothing, results are never flushed.
*/
class UnlimitedCacheFlushManager extends CacheFlushManager {
static final CacheFlushManager INSTANCE = new UnlimitedCacheFlushManager();
UnlimitedCacheFlushManager() : super(DEFAULT_CACHING_POLICY, (_) => false);
@override
void resultAccessed(TargetedResult result) {}
@override
List<TargetedResult> resultStored(TargetedResult newResult, newValue) {
return const <TargetedResult>[];
}
@override
void targetRemoved(AnalysisTarget target) {}
}