1 /*******************************************************************************
2  * Copyright (c) 2003, 2015 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *     Bug 349224 Navigator content provider "appearsBefore" creates hard reference to named id - paul.fullbright@oracle.com
14  *     C. Sean Young <csyoung@google.com> - Bug 436645
15  *******************************************************************************/
16 package org.eclipse.ui.internal.navigator.extensions;
17 
18 import java.lang.ref.Reference;
19 import java.lang.ref.ReferenceQueue;
20 import java.util.HashMap;
21 import java.util.Map;
22 
23 import org.eclipse.ui.internal.navigator.VisibilityAssistant;
24 import org.eclipse.ui.internal.navigator.VisibilityAssistant.VisibilityListener;
25 
26 /**
27  * A cache for evaluated {@link NavigatorContentDescriptor}.
28  */
29 public class EvaluationCache implements VisibilityListener {
30 	// TODO Have an LRU cache with max size as well as SoftReferences, to help
31 	// prevent pathological GC performance that can happen where there are a
32 	// large number of softly reachable objects, as well as helping to reduce
33 	// dependence on the GC from keeping this cache's size in line in the first
34 	// place.
35 
36 	// TODO Counters for cache hits, misses, replacements, etc.
37 
38 	// TODO Either the overrides and not overrides case should "share" parts of
39 	// their data structures (for example, this can be a map of key -> pair
40 	// instead of two maps) OR not bother tracking "overrides or not" state here
41 	// and instead let users of this class handle it with two instances of this
42 	// class.
43 	private final Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> evaluations = new HashMap<>();
44 	private final Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> evaluationsWithOverrides = new HashMap<>();
45 
46 	private final ReferenceQueue<Object> evaluationsQueue = new ReferenceQueue<>();
47 	private final ReferenceQueue<Object> evaluationsWithOverridesQueue = new ReferenceQueue<>();
48 
49 	/**
50 	 * @param anAssistant the VisisbilityAssistant to register with, must be non-null
51 	 */
EvaluationCache(VisibilityAssistant anAssistant)52 	public EvaluationCache(VisibilityAssistant anAssistant) {
53 		anAssistant.addListener(this);
54 	}
55 
cleanUpStaleEntries()56 	private void cleanUpStaleEntries() {
57 		// TODO Only clean up to a certain number of entries per call when merely accessing or setting?
58 		// TODO Periodic task to run this every now and then, ala org.eclipse.core.runtime.jobs.Job?
59 		// If this is done, will need to make this class thread safe.
60 
61 		// Not thread safe, but this whole class isn't, so that is fine.
62 		Reference<?> r;
63 		// Reference#poll thankfully does not block if there is nothing available.
64 		while ((r = evaluationsQueue.poll()) != null) {
65 			processStaleEntry(r, evaluations);
66 		}
67 		while ((r = evaluationsWithOverridesQueue.poll()) != null) {
68 			processStaleEntry(r, evaluationsWithOverrides);
69 		}
70 	}
71 
processStaleEntry(Reference<?> r, Map<? extends Reference<?>, ? extends Reference<?>> fromMap)72 	private static void processStaleEntry(Reference<?> r,
73 			Map<? extends Reference<?>, ? extends Reference<?>> fromMap) {
74 		if (r instanceof EvaluationReference) {
75 			// Key has been collected; clear its entry.
76 			EvaluationValueReference<?> oldVal = (EvaluationValueReference<?>) fromMap.remove(r);
77 			if (oldVal != null) {
78 				// Clear the key from the value so we don't try to prematurely
79 				// remove any potential new mapping upon cleanUpStaleEntries()
80 				oldVal.clear();
81 			}
82 		}
83 		if (r instanceof EvaluationValueReference) {
84 			// If the value has been collected, get its key, and then remove that entry.
85 			EvaluationReference<?> key = ((EvaluationValueReference<?>) r).getKey();
86 			if (key != null) {
87 				fromMap.remove(key);
88 			}
89 		}
90 		// All other Reference types we just leave alone.
91 	}
92 
getDescriptorsFromMap(Object anElement, Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> map)93 	private static NavigatorContentDescriptor[] getDescriptorsFromMap(Object anElement,
94 			Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> map) {
95 		// Need to wrap in the reference type before querying, else it won't be found by HashMap.
96 		EvaluationReference<Object> key = new EvaluationReference<>(anElement);
97 		NavigatorContentDescriptor[] cachedDescriptors = null;
98 		Reference<NavigatorContentDescriptor[]> cache = map.get(key);
99 		if (cache != null && (cachedDescriptors = cache.get()) == null) {
100 			// There was an entry, but it has been collected; remove stale mapping.
101 			EvaluationValueReference<NavigatorContentDescriptor[]> value = map.remove(key);
102 			if (value != null) {
103 				// Clear the key from the value so we don't try to prematurely remove any potential new mapping upon cleanUpStaleEntries()
104 				value.clear();
105 			}
106 		}
107 		return cachedDescriptors;
108 	}
109 
110 	/**
111 	 * Finds the cached descriptors for the given key, or returns {@code null}
112 	 * if not currently in the cache.
113 	 *
114 	 * @param anElement
115 	 *            the key to lookup
116 	 * @param toComputeOverrides
117 	 *            whether overrides are to be considered
118 	 * @return the cached descriptors for the given key, or {@code null} if not
119 	 *         currently in the cache
120 	 */
getDescriptors(Object anElement, boolean toComputeOverrides)121 	public final NavigatorContentDescriptor[] getDescriptors(Object anElement, boolean toComputeOverrides) {
122 		cleanUpStaleEntries();
123 		if (anElement == null)
124 			return null;
125 
126 		if (toComputeOverrides) {
127 			return getDescriptorsFromMap(anElement, evaluations);
128 		}
129 		return getDescriptorsFromMap(anElement, evaluationsWithOverrides);
130 	}
131 
setDescriptorsInMap(Object anElement, NavigatorContentDescriptor[] theDescriptors, Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> map, ReferenceQueue<Object> queue)132 	private static void setDescriptorsInMap(Object anElement, NavigatorContentDescriptor[] theDescriptors,
133 			Map<EvaluationReference<Object>, EvaluationValueReference<NavigatorContentDescriptor[]>> map,
134 			ReferenceQueue<Object> queue) {
135 		// Ideally, we would use a WeakReference wrapper if the object given uses identity equality
136 		// (we can test if the class uses Object's equals or has its own override), and only use a SoftReference
137 		// if the object overrides equals, but that is a bit too unwieldy to check (it would require
138 		// checking reflective data) to be worth it.
139 		EvaluationReference<Object> key = new EvaluationReference<>(anElement, queue);
140 		EvaluationValueReference<NavigatorContentDescriptor[]> newValue =
141 				new EvaluationValueReference<>(theDescriptors, key, queue);
142 		EvaluationValueReference<NavigatorContentDescriptor[]> oldValue = map.put(key, newValue);
143 		if (oldValue != null) {
144 			// "Swap" the correct key instance when swapping the value, or else the above, temporary
145 			// lookup key will be collected too early (not the actual anElement, but the Reference object).
146 			// Not truly needed, but it will help the point of this field not go to waste.
147 			newValue.swapKey(oldValue);
148 			// Clear the key so we don't try to prematurely remove the new mapping upon cleanUpStaleEntries()
149 			oldValue.clear();
150 		}
151 	}
152 
153 	/**
154 	 * Caches the given descriptors with the given key.
155 	 *
156 	 * @param anElement
157 	 *            the key to associate with the given descriptors
158 	 * @param theDescriptors
159 	 *            the descriptors to cache against the given key
160 	 * @param toComputeOverrides
161 	 *            whether overrides were considered in the computation of the
162 	 *            given descriptors
163 	 */
setDescriptors(Object anElement, NavigatorContentDescriptor[] theDescriptors, boolean toComputeOverrides)164 	public final void setDescriptors(Object anElement, NavigatorContentDescriptor[] theDescriptors,
165 			boolean toComputeOverrides) {
166 		cleanUpStaleEntries();
167 		if (anElement != null) {
168 			if (toComputeOverrides) {
169 				setDescriptorsInMap(anElement, theDescriptors, evaluations, evaluationsQueue);
170 			} else {
171 				setDescriptorsInMap(anElement, theDescriptors, evaluationsWithOverrides, evaluationsWithOverridesQueue);
172 			}
173 		}
174 	}
175 
176 	/**
177 	 * {@inheritDoc}
178 	 *
179 	 * For an EvaluationCache, this means invalidating all cached descriptors.
180 	 */
181 	@Override
onVisibilityOrActivationChange()182 	public void onVisibilityOrActivationChange() {
183 		clear();
184 	}
185 
186 	/**
187 	 * Clears the cache.
188 	 */
clear()189 	public void clear() {
190 		// Dump everything in the reference queues.
191 		// Don't bother removing from the map based on references, we are about to clear everything anyways.
192 		// This might lead to some premature removals because yet to be collected values are not clearing
193 		// their key reference, but that is worth having a fast clearing of the maps.
194 		// Thankfully, this should be rare, as when reference objects themselves are GCed before being added
195 		// to a reference queue, they don't get added at all.
196 		while (evaluationsQueue.poll() != null) {
197 			// No need to do anything with the reference, we just need to drain
198 			// the queue.
199 		}
200 		while (evaluationsWithOverridesQueue.poll() != null) {
201 			// No need to do anything with the reference, we just need to drain
202 			// the queue.
203 		}
204 		evaluations.clear();
205 		evaluationsWithOverrides.clear();
206 	}
207 }