/*******************************************************************************
* Copyright (c) 2003, 2015 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
* Bug 349224 Navigator content provider "appearsBefore" creates hard reference to named id - paul.fullbright@oracle.com
* C. Sean Young <csyoung@google.com> - Bug 436645
*******************************************************************************/
package org.eclipse.ui.internal.navigator.extensions;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.ui.internal.navigator.VisibilityAssistant;
import org.eclipse.ui.internal.navigator.VisibilityAssistant.VisibilityListener;
/**
* A cache for evaluated {@link NavigatorContentDescriptor}.
*/
public class EvaluationCache implements VisibilityListener {
// TODO Have an LRU cache with max size as well as SoftReferences, to help
// prevent pathological GC performance that can happen where there are a
// large number of softly reachable objects, as well as helping to reduce
// dependence on the GC from keeping this cache's size in line in the first
// place.
// TODO Counters for cache hits, misses, replacements, etc.
// TODO Either the overrides and not overrides case should "share" parts of
// their data structures (for example, this can be a map of key -> pair
// instead of two maps) OR not bother tracking "overrides or not" state here
// and instead let users of this class handle it with two instances of this
// class.
private final Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> evaluations = new HashMap<>();
private final Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> evaluationsWithOverrides = new HashMap<>();
private final ReferenceQueue<Object> evaluationsQueue = new ReferenceQueue<>();
private final ReferenceQueue<Object> evaluationsWithOverridesQueue = new ReferenceQueue<>();
/**
* @param anAssistant the VisisbilityAssistant to register with, must be non-null
*/
public EvaluationCache(VisibilityAssistant anAssistant) {
anAssistant.addListener(this);
}
private void cleanUpStaleEntries() {
// TODO Only clean up to a certain number of entries per call when merely accessing or setting?
// TODO Periodic task to run this every now and then, ala org.eclipse.core.runtime.jobs.Job?
// If this is done, will need to make this class thread safe.
// Not thread safe, but this whole class isn't, so that is fine.
Reference<?> r;
// Reference#poll thankfully does not block if there is nothing available.
while ((r = evaluationsQueue.poll()) != null) {
processStaleEntry(r, evaluations);
}
while ((r = evaluationsWithOverridesQueue.poll()) != null) {
processStaleEntry(r, evaluationsWithOverrides);
}
}
private static void processStaleEntry(Reference<?> r,
Map<? extends Reference<?>, ? extends Reference<?>> fromMap) {
if (r instanceof EvaluationReference) {
// Key has been collected; clear its entry.
EvaluationValueReference<?> oldVal = (EvaluationValueReference<?>) fromMap.remove(r);
if (oldVal != null) {
// Clear the key from the value so we don't try to prematurely
// remove any potential new mapping upon cleanUpStaleEntries()
oldVal.clear();
}
}
if (r instanceof EvaluationValueReference) {
// If the value has been collected, get its key, and then remove that entry.
EvaluationReference<?> key = ((EvaluationValueReference<?>) r).getKey();
if (key != null) {
fromMap.remove(key);
}
}
// All other Reference types we just leave alone.
}
private static NavigatorContentDescriptor[] getDescriptorsFromMap(Object anElement,
Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> map) {
// Need to wrap in the reference type before querying, else it won't be found by HashMap.
EvaluationReference<Object> key = new EvaluationReference<>(anElement);
NavigatorContentDescriptor[] cachedDescriptors = null;
Reference<NavigatorContentDescriptor[]> cache = map.get(key);
if (cache != null && (cachedDescriptors = cache.get()) == null) {
// There was an entry, but it has been collected; remove stale mapping.
EvaluationValueReference<NavigatorContentDescriptor[]> value = map.remove(key);
if (value != null) {
// Clear the key from the value so we don't try to prematurely remove any potential new mapping upon cleanUpStaleEntries()
value.clear();
}
}
return cachedDescriptors;
}
/**
* Finds the cached descriptors for the given key, or returns {@code null}
* if not currently in the cache.
*
* @param anElement
* the key to lookup
* @param toComputeOverrides
* whether overrides are to be considered
* @return the cached descriptors for the given key, or {@code null} if not
* currently in the cache
*/
public final NavigatorContentDescriptor[] getDescriptors(Object anElement, boolean toComputeOverrides) {
cleanUpStaleEntries();
if (anElement == null)
return null;
if (toComputeOverrides) {
return getDescriptorsFromMap(anElement, evaluations);
}
return getDescriptorsFromMap(anElement, evaluationsWithOverrides);
}
private static void setDescriptorsInMap(Object anElement, NavigatorContentDescriptor[] theDescriptors,
Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> map,
ReferenceQueue<Object> queue) {
// Ideally, we would use a WeakReference wrapper if the object given uses identity equality
// (we can test if the class uses Object's equals or has its own override), and only use a SoftReference
// if the object overrides equals, but that is a bit too unwieldy to check (it would require
// checking reflective data) to be worth it.
EvaluationReference<Object> key = new EvaluationReference<>(anElement, queue);
EvaluationValueReference<NavigatorContentDescriptor[]> newValue =
new EvaluationValueReference<>(theDescriptors, key, queue);
EvaluationValueReference<NavigatorContentDescriptor[]> oldValue = map.put(key, newValue);
if (oldValue != null) {
// "Swap" the correct key instance when swapping the value, or else the above, temporary
// lookup key will be collected too early (not the actual anElement, but the Reference object).
// Not truly needed, but it will help the point of this field not go to waste.
newValue.swapKey(oldValue);
// Clear the key so we don't try to prematurely remove the new mapping upon cleanUpStaleEntries()
oldValue.clear();
}
}
/**
* Caches the given descriptors with the given key.
*
* @param anElement
* the key to associate with the given descriptors
* @param theDescriptors
* the descriptors to cache against the given key
* @param toComputeOverrides
* whether overrides were considered in the computation of the
* given descriptors
*/
public final void setDescriptors(Object anElement, NavigatorContentDescriptor[] theDescriptors,
boolean toComputeOverrides) {
cleanUpStaleEntries();
if (anElement != null) {
if (toComputeOverrides) {
setDescriptorsInMap(anElement, theDescriptors, evaluations, evaluationsQueue);
} else {
setDescriptorsInMap(anElement, theDescriptors, evaluationsWithOverrides, evaluationsWithOverridesQueue);
}
}
}
/**
* {@inheritDoc}
*
* For an EvaluationCache, this means invalidating all cached descriptors.
*/
@Override
public void onVisibilityOrActivationChange() {
clear();
}
/**
* Clears the cache.
*/
public void clear() {
// Dump everything in the reference queues.
// Don't bother removing from the map based on references, we are about to clear everything anyways.
// This might lead to some premature removals because yet to be collected values are not clearing
// their key reference, but that is worth having a fast clearing of the maps.
// Thankfully, this should be rare, as when reference objects themselves are GCed before being added
// to a reference queue, they don't get added at all.
while (evaluationsQueue.poll() != null) {
// No need to do anything with the reference, we just need to drain
// the queue.
}
while (evaluationsWithOverridesQueue.poll() != null) {
// No need to do anything with the reference, we just need to drain
// the queue.
}
evaluations.clear();
evaluationsWithOverrides.clear();
}
}