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