1
2
3
4
5
6
7
8
9
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
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 public class WindowCache {
110
111
112
113
114 static interface StatsRecorder {
115
116
117
118
119
120
121 void recordHits(int count);
122
123
124
125
126
127
128
129
130 void recordMisses(int count);
131
132
133
134
135
136
137
138 void recordLoadSuccess(long loadTimeNanos);
139
140
141
142
143
144
145
146 void recordLoadFailure(long loadTimeNanos);
147
148
149
150
151
152
153
154 void recordEvictions(int count);
155
156
157
158
159
160
161
162 void recordOpenFiles(int delta);
163
164
165
166
167
168
169
170
171
172
173 void recordOpenBytes(Pack pack, int delta);
174
175
176
177
178
179
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
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
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
348
349
350
351
352
353
354
355
356
357
358
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
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
388
389
390
391
392 c.removeAll();
393 }
394 return r;
395 }
396
397 static final void purge(Pack pack) {
398 cache.removeAll(pack);
399 }
400
401
402 private final CleanupQueue queue;
403
404
405 private final int tableSize;
406
407
408 private final AtomicLong clock;
409
410
411 private final AtomicReferenceArray<Entry> table;
412
413
414 private final Lock[] locks;
415
416
417 private final ReentrantLock evictLock;
418
419
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");
486 }
487 return this;
488 }
489
490
491
492
493 public WindowCacheStats getStats() {
494 return statsRecorder.getStats();
495 }
496
497
498
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
572
573
574
575
576
577
578
579
580
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
643
644
645
646
647
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
683
684
685
686
687
688
689
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
705
706
707
708
709
710
711
712
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
756 final Entry next;
757
758
759 final PageRef<ByteWindow> ref;
760
761
762
763
764
765
766
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
784
785
786
787
788
789
790 T get();
791
792
793
794
795
796
797
798 boolean kill();
799
800
801
802
803
804
805
806
807 Pack getPack();
808
809
810
811
812
813
814
815
816 long getPosition();
817
818
819
820
821
822
823 int getSize();
824
825
826
827
828
829
830 long getLastAccess();
831
832
833
834
835
836
837
838 void setLastAccess(long time);
839
840
841
842
843
844 boolean isStrongRef();
845 }
846
847
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
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
986
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
1047 }
1048 }