View Javadoc
1   /*
2    * Copyright (C) 2008, 2009 Google Inc.
3    * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
4    *
5    * This program and the accompanying materials are made available under the
6    * terms of the Eclipse Distribution License v. 1.0 which is available at
7    * https://www.eclipse.org/org/documents/edl-v10.php.
8    *
9    * SPDX-License-Identifier: BSD-3-Clause
10   */
11  
12  package org.eclipse.jgit.internal.storage.file;
13  
14  import java.io.IOException;
15  import java.lang.ref.ReferenceQueue;
16  import java.lang.ref.SoftReference;
17  import java.util.Collections;
18  import java.util.Map;
19  import java.util.Random;
20  import java.util.concurrent.ConcurrentHashMap;
21  import java.util.concurrent.ConcurrentLinkedQueue;
22  import java.util.concurrent.atomic.AtomicBoolean;
23  import java.util.concurrent.atomic.AtomicLong;
24  import java.util.concurrent.atomic.AtomicReferenceArray;
25  import java.util.concurrent.atomic.LongAdder;
26  import java.util.concurrent.locks.ReentrantLock;
27  import java.util.stream.Collectors;
28  
29  import org.eclipse.jgit.annotations.NonNull;
30  import org.eclipse.jgit.internal.JGitText;
31  import org.eclipse.jgit.storage.file.WindowCacheConfig;
32  import org.eclipse.jgit.storage.file.WindowCacheStats;
33  import org.eclipse.jgit.util.Monitoring;
34  
35  /**
36   * Caches slices of a {@link org.eclipse.jgit.internal.storage.file.Pack} in
37   * memory for faster read access.
38   * <p>
39   * The WindowCache serves as a Java based "buffer cache", loading segments of a
40   * PackFile into the JVM heap prior to use. As JGit often wants to do reads of
41   * only tiny slices of a file, the WindowCache tries to smooth out these tiny
42   * reads into larger block-sized IO operations.
43   * <p>
44   * Whenever a cache miss occurs, {@link #load(Pack, long)} is invoked by
45   * exactly one thread for the given <code>(PackFile,position)</code> key tuple.
46   * This is ensured by an array of locks, with the tuple hashed to a lock
47   * instance.
48   * <p>
49   * During a miss, older entries are evicted from the cache so long as
50   * {@link #isFull()} returns true.
51   * <p>
52   * Its too expensive during object access to be 100% accurate with a least
53   * recently used (LRU) algorithm. Strictly ordering every read is a lot of
54   * overhead that typically doesn't yield a corresponding benefit to the
55   * application.
56   * <p>
57   * This cache implements a loose LRU policy by randomly picking a window
58   * comprised of roughly 10% of the cache, and evicting the oldest accessed entry
59   * within that window.
60   * <p>
61   * Entities created by the cache are held under SoftReferences if option
62   * {@code core.packedGitUseStrongRefs} is set to {@code false} in the git config
63   * (this is the default) or by calling
64   * {@link WindowCacheConfig#setPackedGitUseStrongRefs(boolean)}, permitting the
65   * Java runtime's garbage collector to evict entries when heap memory gets low.
66   * Most JREs implement a loose least recently used algorithm for this eviction.
67   * When this option is set to {@code true} strong references are used which
68   * means that Java gc cannot evict the WindowCache to reclaim memory. On the
69   * other hand this provides more predictable performance since the cache isn't
70   * flushed when used heap comes close to the maximum heap size.
71   * <p>
72   * The internal hash table does not expand at runtime, instead it is fixed in
73   * size at cache creation time. The internal lock table used to gate load
74   * invocations is also fixed in size.
75   * <p>
76   * The key tuple is passed through to methods as a pair of parameters rather
77   * than as a single Object, thus reducing the transient memory allocations of
78   * callers. It is more efficient to avoid the allocation, as we can't be 100%
79   * sure that a JIT would be able to stack-allocate a key tuple.
80   * <p>
81   * This cache has an implementation rule such that:
82   * <ul>
83   * <li>{@link #load(Pack, long)} is invoked by at most one thread at a time
84   * for a given <code>(PackFile,position)</code> tuple.</li>
85   * <li>For every <code>load()</code> invocation there is exactly one
86   * {@link #createRef(Pack, long, ByteWindow)} invocation to wrap a
87   * SoftReference or a StrongReference around the cached entity.</li>
88   * <li>For every Reference created by <code>createRef()</code> there will be
89   * exactly one call to {@link #clear(PageRef)} to cleanup any resources associated
90   * with the (now expired) cached entity.</li>
91   * </ul>
92   * <p>
93   * Therefore, it is safe to perform resource accounting increments during the
94   * {@link #load(Pack, long)} or
95   * {@link #createRef(Pack, long, ByteWindow)} methods, and matching
96   * decrements during {@link #clear(PageRef)}. Implementors may need to override
97   * {@link #createRef(Pack, long, ByteWindow)} in order to embed additional
98   * accounting information into an implementation specific
99   * {@link org.eclipse.jgit.internal.storage.file.WindowCache.PageRef} subclass, as
100  * the cached entity may have already been evicted by the JRE's garbage
101  * collector.
102  * <p>
103  * To maintain higher concurrency workloads, during eviction only one thread
104  * performs the eviction work, while other threads can continue to insert new
105  * objects in parallel. This means that the cache can be temporarily over limit,
106  * especially if the nominated eviction thread is being starved relative to the
107  * other threads.
108  */
109 public class WindowCache {
110 
111 	/**
112 	 * Record statistics for a cache
113 	 */
114 	static interface StatsRecorder {
115 		/**
116 		 * Record cache hits. Called when cache returns a cached entry.
117 		 *
118 		 * @param count
119 		 *            number of cache hits to record
120 		 */
121 		void recordHits(int count);
122 
123 		/**
124 		 * Record cache misses. Called when the cache returns an entry which had
125 		 * to be loaded.
126 		 *
127 		 * @param count
128 		 *            number of cache misses to record
129 		 */
130 		void recordMisses(int count);
131 
132 		/**
133 		 * Record a successful load of a cache entry
134 		 *
135 		 * @param loadTimeNanos
136 		 *            time to load a cache entry
137 		 */
138 		void recordLoadSuccess(long loadTimeNanos);
139 
140 		/**
141 		 * Record a failed load of a cache entry
142 		 *
143 		 * @param loadTimeNanos
144 		 *            time used trying to load a cache entry
145 		 */
146 		void recordLoadFailure(long loadTimeNanos);
147 
148 		/**
149 		 * Record cache evictions due to the cache evictions strategy
150 		 *
151 		 * @param count
152 		 *            number of evictions to record
153 		 */
154 		void recordEvictions(int count);
155 
156 		/**
157 		 * Record files opened by cache
158 		 *
159 		 * @param delta
160 		 *            delta of number of files opened by cache
161 		 */
162 		void recordOpenFiles(int delta);
163 
164 		/**
165 		 * Record cached bytes
166 		 *
167 		 * @param pack
168 		 *            pack file the bytes are read from
169 		 *
170 		 * @param delta
171 		 *            delta of cached bytes
172 		 */
173 		void recordOpenBytes(Pack pack, int delta);
174 
175 		/**
176 		 * Returns a snapshot of this recorder's stats. Note that this may be an
177 		 * inconsistent view, as it may be interleaved with update operations.
178 		 *
179 		 * @return a snapshot of this recorder's stats
180 		 */
181 		@NonNull
182 		WindowCacheStats getStats();
183 	}
184 
185 	static class StatsRecorderImpl
186 			implements StatsRecorder, WindowCacheStats {
187 		private final LongAdder hitCount;
188 		private final LongAdder missCount;
189 		private final LongAdder loadSuccessCount;
190 		private final LongAdder loadFailureCount;
191 		private final LongAdder totalLoadTime;
192 		private final LongAdder evictionCount;
193 		private final LongAdder openFileCount;
194 		private final LongAdder openByteCount;
195 		private final Map<String, LongAdder> openByteCountPerRepository;
196 
197 		/**
198 		 * Constructs an instance with all counts initialized to zero.
199 		 */
200 		public StatsRecorderImpl() {
201 			hitCount = new LongAdder();
202 			missCount = new LongAdder();
203 			loadSuccessCount = new LongAdder();
204 			loadFailureCount = new LongAdder();
205 			totalLoadTime = new LongAdder();
206 			evictionCount = new LongAdder();
207 			openFileCount = new LongAdder();
208 			openByteCount = new LongAdder();
209 			openByteCountPerRepository = new ConcurrentHashMap<>();
210 		}
211 
212 		@Override
213 		public void recordHits(int count) {
214 			hitCount.add(count);
215 		}
216 
217 		@Override
218 		public void recordMisses(int count) {
219 			missCount.add(count);
220 		}
221 
222 		@Override
223 		public void recordLoadSuccess(long loadTimeNanos) {
224 			loadSuccessCount.increment();
225 			totalLoadTime.add(loadTimeNanos);
226 		}
227 
228 		@Override
229 		public void recordLoadFailure(long loadTimeNanos) {
230 			loadFailureCount.increment();
231 			totalLoadTime.add(loadTimeNanos);
232 		}
233 
234 		@Override
235 		public void recordEvictions(int count) {
236 			evictionCount.add(count);
237 		}
238 
239 		@Override
240 		public void recordOpenFiles(int delta) {
241 			openFileCount.add(delta);
242 		}
243 
244 		@Override
245 		public void recordOpenBytes(Pack pack, int delta) {
246 			openByteCount.add(delta);
247 			String repositoryId = repositoryId(pack);
248 			LongAdder la = openByteCountPerRepository
249 					.computeIfAbsent(repositoryId, k -> new LongAdder());
250 			la.add(delta);
251 			if (delta < 0) {
252 				openByteCountPerRepository.computeIfPresent(repositoryId,
253 						(k, v) -> v.longValue() == 0 ? null : v);
254 			}
255 		}
256 
257 		private static String repositoryId(Pack pack) {
258 			// use repository's gitdir since Pack doesn't know its repository
259 			return pack.getPackFile().getParentFile().getParentFile()
260 					.getParent();
261 		}
262 
263 		@Override
264 		public WindowCacheStats getStats() {
265 			return this;
266 		}
267 
268 		@Override
269 		public long getHitCount() {
270 			return hitCount.sum();
271 		}
272 
273 		@Override
274 		public long getMissCount() {
275 			return missCount.sum();
276 		}
277 
278 		@Override
279 		public long getLoadSuccessCount() {
280 			return loadSuccessCount.sum();
281 		}
282 
283 		@Override
284 		public long getLoadFailureCount() {
285 			return loadFailureCount.sum();
286 		}
287 
288 		@Override
289 		public long getEvictionCount() {
290 			return evictionCount.sum();
291 		}
292 
293 		@Override
294 		public long getTotalLoadTime() {
295 			return totalLoadTime.sum();
296 		}
297 
298 		@Override
299 		public long getOpenFileCount() {
300 			return openFileCount.sum();
301 		}
302 
303 		@Override
304 		public long getOpenByteCount() {
305 			return openByteCount.sum();
306 		}
307 
308 		@Override
309 		public void resetCounters() {
310 			hitCount.reset();
311 			missCount.reset();
312 			loadSuccessCount.reset();
313 			loadFailureCount.reset();
314 			totalLoadTime.reset();
315 			evictionCount.reset();
316 		}
317 
318 		@Override
319 		public Map<String, Long> getOpenByteCountPerRepository() {
320 			return Collections.unmodifiableMap(
321 					openByteCountPerRepository.entrySet().stream()
322 							.collect(Collectors.toMap(Map.Entry::getKey,
323 									e -> Long.valueOf(e.getValue().sum()),
324 									(u, v) -> v)));
325 		}
326 	}
327 
328 	private static final int bits(int newSize) {
329 		if (newSize < 4096)
330 			throw new IllegalArgumentException(JGitText.get().invalidWindowSize);
331 		if (Integer.bitCount(newSize) != 1)
332 			throw new IllegalArgumentException(JGitText.get().windowSizeMustBePowerOf2);
333 		return Integer.numberOfTrailingZeros(newSize);
334 	}
335 
336 	private static final Random rng = new Random();
337 
338 	private static volatile WindowCache cache;
339 
340 	private static volatile int streamFileThreshold;
341 
342 	static {
343 		reconfigure(new WindowCacheConfig());
344 	}
345 
346 	/**
347 	 * Modify the configuration of the window cache.
348 	 * <p>
349 	 * The new configuration is applied immediately. If the new limits are
350 	 * smaller than what is currently cached, older entries will be purged
351 	 * as soon as possible to allow the cache to meet the new limit.
352 	 *
353 	 * @deprecated use {@code cfg.install()} to avoid internal reference.
354 	 * @param cfg
355 	 *            the new window cache configuration.
356 	 * @throws java.lang.IllegalArgumentException
357 	 *             the cache configuration contains one or more invalid
358 	 *             settings, usually too low of a limit.
359 	 */
360 	@Deprecated
361 	public static void reconfigure(WindowCacheConfig cfg) {
362 		final WindowCache nc = new WindowCache(cfg);
363 		final WindowCache oc = cache;
364 		if (oc != null)
365 			oc.removeAll();
366 		cache = nc;
367 		streamFileThreshold = cfg.getStreamFileThreshold();
368 		DeltaBaseCache.reconfigure(cfg);
369 	}
370 
371 	static int getStreamFileThreshold() {
372 		return streamFileThreshold;
373 	}
374 
375 	/**
376 	 * @return the cached instance.
377 	 */
378 	public static WindowCache getInstance() {
379 		return cache.publishMBeanIfNeeded();
380 	}
381 
382 	static final ByteWindow get(Pack pack, long offset)
383 			throws IOException {
384 		final WindowCache c = cache;
385 		final ByteWindow r = c.getOrLoad(pack, c.toStart(offset));
386 		if (c != cache.publishMBeanIfNeeded()) {
387 			// The cache was reconfigured while we were using the old one
388 			// to load this window. The window is still valid, but our
389 			// cache may think its still live. Ensure the window is removed
390 			// from the old cache so resources can be released.
391 			//
392 			c.removeAll();
393 		}
394 		return r;
395 	}
396 
397 	static final void purge(Pack pack) {
398 		cache.removeAll(pack);
399 	}
400 
401 	/** cleanup released and/or garbage collected windows. */
402 	private final CleanupQueue queue;
403 
404 	/** Number of entries in {@link #table}. */
405 	private final int tableSize;
406 
407 	/** Access clock for loose LRU. */
408 	private final AtomicLong clock;
409 
410 	/** Hash bucket directory; entries are chained below. */
411 	private final AtomicReferenceArray<Entry> table;
412 
413 	/** Locks to prevent concurrent loads for same (PackFile,position). */
414 	private final Lock[] locks;
415 
416 	/** Lock to elect the eviction thread after a load occurs. */
417 	private final ReentrantLock evictLock;
418 
419 	/** Number of {@link #table} buckets to scan for an eviction window. */
420 	private final int evictBatch;
421 
422 	private final int maxFiles;
423 
424 	private final long maxBytes;
425 
426 	private final boolean mmap;
427 
428 	private final int windowSizeShift;
429 
430 	private final int windowSize;
431 
432 	private final StatsRecorder statsRecorder;
433 
434 	private final StatsRecorderImpl mbean;
435 
436 	private final AtomicBoolean publishMBean = new AtomicBoolean();
437 
438 	private boolean useStrongRefs;
439 
440 	private WindowCache(WindowCacheConfig cfg) {
441 		tableSize = tableSize(cfg);
442 		final int lockCount = lockCount(cfg);
443 		if (tableSize < 1)
444 			throw new IllegalArgumentException(JGitText.get().tSizeMustBeGreaterOrEqual1);
445 		if (lockCount < 1)
446 			throw new IllegalArgumentException(JGitText.get().lockCountMustBeGreaterOrEqual1);
447 
448 		clock = new AtomicLong(1);
449 		table = new AtomicReferenceArray<>(tableSize);
450 		locks = new Lock[lockCount];
451 		for (int i = 0; i < locks.length; i++)
452 			locks[i] = new Lock();
453 		evictLock = new ReentrantLock();
454 
455 		int eb = (int) (tableSize * .1);
456 		if (64 < eb)
457 			eb = 64;
458 		else if (eb < 4)
459 			eb = 4;
460 		if (tableSize < eb)
461 			eb = tableSize;
462 		evictBatch = eb;
463 
464 		maxFiles = cfg.getPackedGitOpenFiles();
465 		maxBytes = cfg.getPackedGitLimit();
466 		mmap = cfg.isPackedGitMMAP();
467 		windowSizeShift = bits(cfg.getPackedGitWindowSize());
468 		windowSize = 1 << windowSizeShift;
469 		useStrongRefs = cfg.isPackedGitUseStrongRefs();
470 		queue = useStrongRefs ? new StrongCleanupQueue(this)
471 				: new SoftCleanupQueue(this);
472 
473 		mbean = new StatsRecorderImpl();
474 		statsRecorder = mbean;
475 		publishMBean.set(cfg.getExposeStatsViaJmx());
476 
477 		if (maxFiles < 1)
478 			throw new IllegalArgumentException(JGitText.get().openFilesMustBeAtLeast1);
479 		if (maxBytes < windowSize)
480 			throw new IllegalArgumentException(JGitText.get().windowSizeMustBeLesserThanLimit);
481 	}
482 
483 	private WindowCache publishMBeanIfNeeded() {
484 		if (publishMBean.getAndSet(false)) {
485 			Monitoring.registerMBean(mbean, "block_cache"); //$NON-NLS-1$
486 		}
487 		return this;
488 	}
489 
490 	/**
491 	 * @return cache statistics for the WindowCache
492 	 */
493 	public WindowCacheStats getStats() {
494 		return statsRecorder.getStats();
495 	}
496 
497 	/**
498 	 * Reset stats. Does not reset open bytes and open files stats.
499 	 */
500 	public void resetStats() {
501 		mbean.resetCounters();
502 	}
503 
504 	private int hash(int packHash, long off) {
505 		return packHash + (int) (off >>> windowSizeShift);
506 	}
507 
508 	private ByteWindow load(Pack pack, long offset) throws IOException {
509 		long startTime = System.nanoTime();
510 		if (pack.beginWindowCache())
511 			statsRecorder.recordOpenFiles(1);
512 		try {
513 			if (mmap)
514 				return pack.mmap(offset, windowSize);
515 			ByteArrayWindow w = pack.read(offset, windowSize);
516 			statsRecorder.recordLoadSuccess(System.nanoTime() - startTime);
517 			return w;
518 		} catch (IOException | RuntimeException | Error e) {
519 			close(pack);
520 			statsRecorder.recordLoadFailure(System.nanoTime() - startTime);
521 			throw e;
522 		} finally {
523 			statsRecorder.recordMisses(1);
524 		}
525 	}
526 
527 	private PageRef<ByteWindow> createRef(Pack p, long o, ByteWindow v) {
528 		final PageRef<ByteWindow> ref = useStrongRefs
529 				? new StrongRef(p, o, v, queue)
530 				: new SoftRef(p, o, v, (SoftCleanupQueue) queue);
531 		statsRecorder.recordOpenBytes(ref.getPack(), ref.getSize());
532 		return ref;
533 	}
534 
535 	private void clear(PageRef<ByteWindow> ref) {
536 		statsRecorder.recordOpenBytes(ref.getPack(), -ref.getSize());
537 		statsRecorder.recordEvictions(1);
538 		close(ref.getPack());
539 	}
540 
541 	private void close(Pack pack) {
542 		if (pack.endWindowCache()) {
543 			statsRecorder.recordOpenFiles(-1);
544 		}
545 	}
546 
547 	private boolean isFull() {
548 		return maxFiles < mbean.getOpenFileCount()
549 				|| maxBytes < mbean.getOpenByteCount();
550 	}
551 
552 	private long toStart(long offset) {
553 		return (offset >>> windowSizeShift) << windowSizeShift;
554 	}
555 
556 	private static int tableSize(WindowCacheConfig cfg) {
557 		final int wsz = cfg.getPackedGitWindowSize();
558 		final long limit = cfg.getPackedGitLimit();
559 		if (wsz <= 0)
560 			throw new IllegalArgumentException(JGitText.get().invalidWindowSize);
561 		if (limit < wsz)
562 			throw new IllegalArgumentException(JGitText.get().windowSizeMustBeLesserThanLimit);
563 		return (int) Math.min(5 * (limit / wsz) / 2, 2000000000);
564 	}
565 
566 	private static int lockCount(WindowCacheConfig cfg) {
567 		return Math.max(cfg.getPackedGitOpenFiles(), 32);
568 	}
569 
570 	/**
571 	 * Lookup a cached object, creating and loading it if it doesn't exist.
572 	 *
573 	 * @param pack
574 	 *            the pack that "contains" the cached object.
575 	 * @param position
576 	 *            offset within <code>pack</code> of the object.
577 	 * @return the object reference.
578 	 * @throws IOException
579 	 *             the object reference was not in the cache and could not be
580 	 *             obtained by {@link #load(Pack, long)}.
581 	 */
582 	private ByteWindow getOrLoad(Pack pack, long position)
583 			throws IOException {
584 		final int slot = slot(pack, position);
585 		final Entry e1 = table.get(slot);
586 		ByteWindow v = scan(e1, pack, position);
587 		if (v != null) {
588 			statsRecorder.recordHits(1);
589 			return v;
590 		}
591 
592 		synchronized (lock(pack, position)) {
593 			Entry e2 = table.get(slot);
594 			if (e2 != e1) {
595 				v = scan(e2, pack, position);
596 				if (v != null) {
597 					statsRecorder.recordHits(1);
598 					return v;
599 				}
600 			}
601 
602 			v = load(pack, position);
603 			final PageRef<ByteWindow> ref = createRef(pack, position, v);
604 			hit(ref);
605 			for (;;) {
606 				final Entry n = new Entry(clean(e2), ref);
607 				if (table.compareAndSet(slot, e2, n))
608 					break;
609 				e2 = table.get(slot);
610 			}
611 		}
612 
613 		if (evictLock.tryLock()) {
614 			try {
615 				gc();
616 				evict();
617 			} finally {
618 				evictLock.unlock();
619 			}
620 		}
621 
622 		return v;
623 	}
624 
625 	private ByteWindow scan(Entry n, Pack pack, long position) {
626 		for (; n != null; n = n.next) {
627 			final PageRef<ByteWindow> r = n.ref;
628 			if (r.getPack() == pack && r.getPosition() == position) {
629 				final ByteWindow v = r.get();
630 				if (v != null) {
631 					hit(r);
632 					return v;
633 				}
634 				n.kill();
635 				break;
636 			}
637 		}
638 		return null;
639 	}
640 
641 	private void hit(PageRef r) {
642 		// We don't need to be 100% accurate here. Its sufficient that at least
643 		// one thread performs the increment. Any other concurrent access at
644 		// exactly the same time can simply use the same clock value.
645 		//
646 		// Consequently we attempt the set, but we don't try to recover should
647 		// it fail. This is why we don't use getAndIncrement() here.
648 		//
649 		final long c = clock.get();
650 		clock.compareAndSet(c, c + 1);
651 		r.setLastAccess(c);
652 	}
653 
654 	private void evict() {
655 		while (isFull()) {
656 			int ptr = rng.nextInt(tableSize);
657 			Entry old = null;
658 			int slot = 0;
659 			for (int b = evictBatch - 1; b >= 0; b--, ptr++) {
660 				if (tableSize <= ptr)
661 					ptr = 0;
662 				for (Entry e = table.get(ptr); e != null; e = e.next) {
663 					if (e.dead)
664 						continue;
665 					if (old == null || e.ref.getLastAccess() < old.ref
666 							.getLastAccess()) {
667 						old = e;
668 						slot = ptr;
669 					}
670 				}
671 			}
672 			if (old != null) {
673 				old.kill();
674 				gc();
675 				final Entry e1 = table.get(slot);
676 				table.compareAndSet(slot, e1, clean(e1));
677 			}
678 		}
679 	}
680 
681 	/**
682 	 * Clear every entry from the cache.
683 	 * <p>
684 	 * This is a last-ditch effort to clear out the cache, such as before it
685 	 * gets replaced by another cache that is configured differently. This
686 	 * method tries to force every cached entry through {@link #clear(PageRef)} to
687 	 * ensure that resources are correctly accounted for and cleaned up by the
688 	 * subclass. A concurrent reader loading entries while this method is
689 	 * running may cause resource accounting failures.
690 	 */
691 	private void removeAll() {
692 		for (int s = 0; s < tableSize; s++) {
693 			Entry e1;
694 			do {
695 				e1 = table.get(s);
696 				for (Entry e = e1; e != null; e = e.next)
697 					e.kill();
698 			} while (!table.compareAndSet(s, e1, null));
699 		}
700 		gc();
701 	}
702 
703 	/**
704 	 * Clear all entries related to a single file.
705 	 * <p>
706 	 * Typically this method is invoked during {@link Pack#close()}, when we
707 	 * know the pack is never going to be useful to us again (for example, it no
708 	 * longer exists on disk). A concurrent reader loading an entry from this
709 	 * same pack may cause the pack to become stuck in the cache anyway.
710 	 *
711 	 * @param pack
712 	 *            the file to purge all entries of.
713 	 */
714 	private void removeAll(Pack pack) {
715 		for (int s = 0; s < tableSize; s++) {
716 			final Entry e1 = table.get(s);
717 			boolean hasDead = false;
718 			for (Entry e = e1; e != null; e = e.next) {
719 				if (e.ref.getPack() == pack) {
720 					e.kill();
721 					hasDead = true;
722 				} else if (e.dead)
723 					hasDead = true;
724 			}
725 			if (hasDead)
726 				table.compareAndSet(s, e1, clean(e1));
727 		}
728 		gc();
729 	}
730 
731 	private void gc() {
732 		queue.gc();
733 	}
734 
735 	private int slot(Pack pack, long position) {
736 		return (hash(pack.hash, position) >>> 1) % tableSize;
737 	}
738 
739 	private Lock lock(Pack pack, long position) {
740 		return locks[(hash(pack.hash, position) >>> 1) % locks.length];
741 	}
742 
743 	private static Entry clean(Entry top) {
744 		while (top != null && top.dead) {
745 			top.ref.kill();
746 			top = top.next;
747 		}
748 		if (top == null)
749 			return null;
750 		final Entry n = clean(top.next);
751 		return n == top.next ? top : new Entry(n, top.ref);
752 	}
753 
754 	private static class Entry {
755 		/** Next entry in the hash table's chain list. */
756 		final Entry next;
757 
758 		/** The referenced object. */
759 		final PageRef<ByteWindow> ref;
760 
761 		/**
762 		 * Marked true when ref.get() returns null and the ref is dead.
763 		 * <p>
764 		 * A true here indicates that the ref is no longer accessible, and that
765 		 * we therefore need to eventually purge this Entry object out of the
766 		 * bucket's chain.
767 		 */
768 		volatile boolean dead;
769 
770 		Entry(Entry n, PageRef<ByteWindow> r) {
771 			next = n;
772 			ref = r;
773 		}
774 
775 		final void kill() {
776 			dead = true;
777 			ref.kill();
778 		}
779 	}
780 
781 	private static interface PageRef<T> {
782 		/**
783 		 * Returns this reference object's referent. If this reference object
784 		 * has been cleared, either by the program or by the garbage collector,
785 		 * then this method returns <code>null</code>.
786 		 *
787 		 * @return The object to which this reference refers, or
788 		 *         <code>null</code> if this reference object has been cleared
789 		 */
790 		T get();
791 
792 	    /**
793 		 * Kill this ref
794 		 *
795 		 * @return <code>true</code> if this reference object was successfully
796 		 *         killed; <code>false</code> if it was already killed
797 		 */
798 		boolean kill();
799 
800 		/**
801 		 * Get the {@link org.eclipse.jgit.internal.storage.file.Pack} the
802 		 * referenced cache page is allocated for
803 		 *
804 		 * @return the {@link org.eclipse.jgit.internal.storage.file.Pack} the
805 		 *         referenced cache page is allocated for
806 		 */
807 		Pack getPack();
808 
809 		/**
810 		 * Get the position of the referenced cache page in the
811 		 * {@link org.eclipse.jgit.internal.storage.file.Pack}
812 		 *
813 		 * @return the position of the referenced cache page in the
814 		 *         {@link org.eclipse.jgit.internal.storage.file.Pack}
815 		 */
816 		long getPosition();
817 
818 		/**
819 		 * Get size of cache page
820 		 *
821 		 * @return size of cache page
822 		 */
823 		int getSize();
824 
825 		/**
826 		 * Get pseudo time of last access to this cache page
827 		 *
828 		 * @return pseudo time of last access to this cache page
829 		 */
830 		long getLastAccess();
831 
832 		/**
833 		 * Set pseudo time of last access to this cache page
834 		 *
835 		 * @param time
836 		 *            pseudo time of last access to this cache page
837 		 */
838 		void setLastAccess(long time);
839 
840 		/**
841 		 * Whether this is a strong reference.
842 		 * @return {@code true} if this is a strong reference
843 		 */
844 		boolean isStrongRef();
845 	}
846 
847 	/** A soft reference wrapped around a cached object. */
848 	private static class SoftRef extends SoftReference<ByteWindow>
849 			implements PageRef<ByteWindow> {
850 		private final Pack pack;
851 
852 		private final long position;
853 
854 		private final int size;
855 
856 		private long lastAccess;
857 
858 		protected SoftRef(final Pack pack, final long position,
859 				final ByteWindow v, final SoftCleanupQueue queue) {
860 			super(v, queue);
861 			this.pack = pack;
862 			this.position = position;
863 			this.size = v.size();
864 		}
865 
866 		@Override
867 		public Pack getPack() {
868 			return pack;
869 		}
870 
871 		@Override
872 		public long getPosition() {
873 			return position;
874 		}
875 
876 		@Override
877 		public int getSize() {
878 			return size;
879 		}
880 
881 		@Override
882 		public long getLastAccess() {
883 			return lastAccess;
884 		}
885 
886 		@Override
887 		public void setLastAccess(long time) {
888 			this.lastAccess = time;
889 		}
890 
891 		@Override
892 		public boolean kill() {
893 			return enqueue();
894 		}
895 
896 		@Override
897 		public boolean isStrongRef() {
898 			return false;
899 		}
900 	}
901 
902 	/** A strong reference wrapped around a cached object. */
903 	private static class StrongRef implements PageRef<ByteWindow> {
904 		private ByteWindow referent;
905 
906 		private final Pack pack;
907 
908 		private final long position;
909 
910 		private final int size;
911 
912 		private long lastAccess;
913 
914 		private CleanupQueue queue;
915 
916 		protected StrongRef(final Pack pack, final long position,
917 				final ByteWindow v, final CleanupQueue queue) {
918 			this.pack = pack;
919 			this.position = position;
920 			this.referent = v;
921 			this.size = v.size();
922 			this.queue = queue;
923 		}
924 
925 		@Override
926 		public Pack getPack() {
927 			return pack;
928 		}
929 
930 		@Override
931 		public long getPosition() {
932 			return position;
933 		}
934 
935 		@Override
936 		public int getSize() {
937 			return size;
938 		}
939 
940 		@Override
941 		public long getLastAccess() {
942 			return lastAccess;
943 		}
944 
945 		@Override
946 		public void setLastAccess(long time) {
947 			this.lastAccess = time;
948 		}
949 
950 		@Override
951 		public ByteWindow get() {
952 			return referent;
953 		}
954 
955 		@Override
956 		public boolean kill() {
957 			if (referent == null) {
958 				return false;
959 			}
960 			referent = null;
961 			return queue.enqueue(this);
962 		}
963 
964 		@Override
965 		public boolean isStrongRef() {
966 			return true;
967 		}
968 	}
969 
970 	private static interface CleanupQueue {
971 		boolean enqueue(PageRef<ByteWindow> r);
972 		void gc();
973 	}
974 
975 	private static class SoftCleanupQueue extends ReferenceQueue<ByteWindow>
976 			implements CleanupQueue {
977 		private final WindowCache wc;
978 
979 		SoftCleanupQueue(WindowCache cache) {
980 			this.wc = cache;
981 		}
982 
983 		@Override
984 		public boolean enqueue(PageRef<ByteWindow> r) {
985 			// no need to explicitly add soft references which are enqueued by
986 			// the JVM
987 			return false;
988 		}
989 
990 		@Override
991 		public void gc() {
992 			SoftRef r;
993 			while ((r = (SoftRef) poll()) != null) {
994 				wc.clear(r);
995 
996 				final int s = wc.slot(r.getPack(), r.getPosition());
997 				final Entry e1 = wc.table.get(s);
998 				for (Entry n = e1; n != null; n = n.next) {
999 					if (n.ref == r) {
1000 						n.dead = true;
1001 						wc.table.compareAndSet(s, e1, clean(e1));
1002 						break;
1003 					}
1004 				}
1005 			}
1006 		}
1007 	}
1008 
1009 	private static class StrongCleanupQueue implements CleanupQueue {
1010 		private final WindowCache wc;
1011 
1012 		private final ConcurrentLinkedQueue<PageRef<ByteWindow>> queue = new ConcurrentLinkedQueue<>();
1013 
1014 		StrongCleanupQueue(WindowCache wc) {
1015 			this.wc = wc;
1016 		}
1017 
1018 		@Override
1019 		public boolean enqueue(PageRef<ByteWindow> r) {
1020 			if (queue.contains(r)) {
1021 				return false;
1022 			}
1023 			return queue.add(r);
1024 		}
1025 
1026 		@Override
1027 		public void gc() {
1028 			PageRef<ByteWindow> r;
1029 			while ((r = queue.poll()) != null) {
1030 				wc.clear(r);
1031 
1032 				final int s = wc.slot(r.getPack(), r.getPosition());
1033 				final Entry e1 = wc.table.get(s);
1034 				for (Entry n = e1; n != null; n = n.next) {
1035 					if (n.ref == r) {
1036 						n.dead = true;
1037 						wc.table.compareAndSet(s, e1, clean(e1));
1038 						break;
1039 					}
1040 				}
1041 			}
1042 		}
1043 	}
1044 
1045 	private static final class Lock {
1046 		// Used only for its implicit monitor.
1047 	}
1048 }