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.AtomicLong;
23 import java.util.concurrent.atomic.AtomicReferenceArray;
24 import java.util.concurrent.atomic.LongAdder;
25 import java.util.concurrent.locks.ReentrantLock;
26 import java.util.stream.Collectors;
27
28 import org.eclipse.jgit.annotations.NonNull;
29 import org.eclipse.jgit.internal.JGitText;
30 import org.eclipse.jgit.storage.file.WindowCacheConfig;
31 import org.eclipse.jgit.storage.file.WindowCacheStats;
32 import org.eclipse.jgit.util.Monitoring;
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 public class WindowCache {
109
110
111
112
113 static interface StatsRecorder {
114
115
116
117
118
119
120 void recordHits(int count);
121
122
123
124
125
126
127
128
129 void recordMisses(int count);
130
131
132
133
134
135
136
137 void recordLoadSuccess(long loadTimeNanos);
138
139
140
141
142
143
144
145 void recordLoadFailure(long loadTimeNanos);
146
147
148
149
150
151
152
153 void recordEvictions(int count);
154
155
156
157
158
159
160
161 void recordOpenFiles(int delta);
162
163
164
165
166
167
168
169
170
171
172 void recordOpenBytes(PackFile pack, int delta);
173
174
175
176
177
178
179
180 @NonNull
181 WindowCacheStats getStats();
182 }
183
184 static class StatsRecorderImpl
185 implements StatsRecorder, WindowCacheStats {
186 private final LongAdder hitCount;
187 private final LongAdder missCount;
188 private final LongAdder loadSuccessCount;
189 private final LongAdder loadFailureCount;
190 private final LongAdder totalLoadTime;
191 private final LongAdder evictionCount;
192 private final LongAdder openFileCount;
193 private final LongAdder openByteCount;
194 private final Map<String, LongAdder> openByteCountPerRepository;
195
196
197
198
199 public StatsRecorderImpl() {
200 hitCount = new LongAdder();
201 missCount = new LongAdder();
202 loadSuccessCount = new LongAdder();
203 loadFailureCount = new LongAdder();
204 totalLoadTime = new LongAdder();
205 evictionCount = new LongAdder();
206 openFileCount = new LongAdder();
207 openByteCount = new LongAdder();
208 openByteCountPerRepository = new ConcurrentHashMap<>();
209 }
210
211 @Override
212 public void recordHits(int count) {
213 hitCount.add(count);
214 }
215
216 @Override
217 public void recordMisses(int count) {
218 missCount.add(count);
219 }
220
221 @Override
222 public void recordLoadSuccess(long loadTimeNanos) {
223 loadSuccessCount.increment();
224 totalLoadTime.add(loadTimeNanos);
225 }
226
227 @Override
228 public void recordLoadFailure(long loadTimeNanos) {
229 loadFailureCount.increment();
230 totalLoadTime.add(loadTimeNanos);
231 }
232
233 @Override
234 public void recordEvictions(int count) {
235 evictionCount.add(count);
236 }
237
238 @Override
239 public void recordOpenFiles(int delta) {
240 openFileCount.add(delta);
241 }
242
243 @Override
244 public void recordOpenBytes(PackFile pack, int delta) {
245 openByteCount.add(delta);
246 String repositoryId = repositoryId(pack);
247 LongAdder la = openByteCountPerRepository
248 .computeIfAbsent(repositoryId, k -> new LongAdder());
249 la.add(delta);
250 if (delta < 0) {
251 openByteCountPerRepository.computeIfPresent(repositoryId,
252 (k, v) -> v.longValue() == 0 ? null : v);
253 }
254 }
255
256 private static String repositoryId(PackFile pack) {
257
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 WindowCacheal/storage/file/WindowCache.html#WindowCache">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;
380 }
381
382 static final ByteWindow get(PackFile 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) {
387
388
389
390
391
392 c.removeAll();
393 }
394 return r;
395 }
396
397 static final void purge(PackFile 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 boolean useStrongRefs;
437
438 private WindowCache(WindowCacheConfig cfg) {
439 tableSize = tableSize(cfg);
440 final int lockCount = lockCount(cfg);
441 if (tableSize < 1)
442 throw new IllegalArgumentException(JGitText.get().tSizeMustBeGreaterOrEqual1);
443 if (lockCount < 1)
444 throw new IllegalArgumentException(JGitText.get().lockCountMustBeGreaterOrEqual1);
445
446 clock = new AtomicLong(1);
447 table = new AtomicReferenceArray<>(tableSize);
448 locks = new Lock[lockCount];
449 for (int i = 0; i < locks.length; i++)
450 locks[i] = new Lock();
451 evictLock = new ReentrantLock();
452
453 int eb = (int) (tableSize * .1);
454 if (64 < eb)
455 eb = 64;
456 else if (eb < 4)
457 eb = 4;
458 if (tableSize < eb)
459 eb = tableSize;
460 evictBatch = eb;
461
462 maxFiles = cfg.getPackedGitOpenFiles();
463 maxBytes = cfg.getPackedGitLimit();
464 mmap = cfg.isPackedGitMMAP();
465 windowSizeShift = bits(cfg.getPackedGitWindowSize());
466 windowSize = 1 << windowSizeShift;
467 useStrongRefs = cfg.isPackedGitUseStrongRefs();
468 queue = useStrongRefs ? new StrongCleanupQueue(this)
469 : new SoftCleanupQueue(this);
470
471 mbean = new StatsRecorderImpl();
472 statsRecorder = mbean;
473 if (cfg.getExposeStatsViaJmx()) {
474 Monitoring.registerMBean(mbean, "block_cache");
475 }
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
484
485
486 public WindowCacheStats getStats() {
487 return statsRecorder.getStats();
488 }
489
490
491
492
493 public void resetStats() {
494 mbean.resetCounters();
495 }
496
497 private int hash(int packHash, long off) {
498 return packHash + (int) (off >>> windowSizeShift);
499 }
500
501 private ByteWindow load(PackFile pack, long offset) throws IOException {
502 long startTime = System.nanoTime();
503 if (pack.beginWindowCache())
504 statsRecorder.recordOpenFiles(1);
505 try {
506 if (mmap)
507 return pack.mmap(offset, windowSize);
508 ByteArrayWindow w = pack.read(offset, windowSize);
509 statsRecorder.recordLoadSuccess(System.nanoTime() - startTime);
510 return w;
511 } catch (IOException | RuntimeException | Error e) {
512 close(pack);
513 statsRecorder.recordLoadFailure(System.nanoTime() - startTime);
514 throw e;
515 } finally {
516 statsRecorder.recordMisses(1);
517 }
518 }
519
520 private PageRef<ByteWindow> createRef(PackFile p, long o, ByteWindow v) {
521 final PageRef<ByteWindow> ref = useStrongRefs
522 ? new StrongRef(p, o, v, queue)
523 : new SoftRef(p, o, v, (SoftCleanupQueue) queue);
524 statsRecorder.recordOpenBytes(ref.getPack(), ref.getSize());
525 return ref;
526 }
527
528 private void clear(PageRef<ByteWindow> ref) {
529 statsRecorder.recordOpenBytes(ref.getPack(), -ref.getSize());
530 statsRecorder.recordEvictions(1);
531 close(ref.getPack());
532 }
533
534 private void close(PackFile pack) {
535 if (pack.endWindowCache()) {
536 statsRecorder.recordOpenFiles(-1);
537 }
538 }
539
540 private boolean isFull() {
541 return maxFiles < mbean.getOpenFileCount()
542 || maxBytes < mbean.getOpenByteCount();
543 }
544
545 private long toStart(long offset) {
546 return (offset >>> windowSizeShift) << windowSizeShift;
547 }
548
549 private static int tableSize(WindowCacheConfig cfg) {
550 final int wsz = cfg.getPackedGitWindowSize();
551 final long limit = cfg.getPackedGitLimit();
552 if (wsz <= 0)
553 throw new IllegalArgumentException(JGitText.get().invalidWindowSize);
554 if (limit < wsz)
555 throw new IllegalArgumentException(JGitText.get().windowSizeMustBeLesserThanLimit);
556 return (int) Math.min(5 * (limit / wsz) / 2, 2000000000);
557 }
558
559 private static int lockCount(WindowCacheConfig cfg) {
560 return Math.max(cfg.getPackedGitOpenFiles(), 32);
561 }
562
563
564
565
566
567
568
569
570
571
572
573
574
575 private ByteWindow getOrLoad(PackFile pack, long position)
576 throws IOException {
577 final int slot = slot(pack, position);
578 final Entry e1 = table.get(slot);
579 ByteWindow v = scan(e1, pack, position);
580 if (v != null) {
581 statsRecorder.recordHits(1);
582 return v;
583 }
584
585 synchronized (lock(pack, position)) {
586 Entry e2 = table.get(slot);
587 if (e2 != e1) {
588 v = scan(e2, pack, position);
589 if (v != null) {
590 statsRecorder.recordHits(1);
591 return v;
592 }
593 }
594
595 v = load(pack, position);
596 final PageRef<ByteWindow> ref = createRef(pack, position, v);
597 hit(ref);
598 for (;;) {
599 final Entry n = new Entry(clean(e2), ref);
600 if (table.compareAndSet(slot, e2, n))
601 break;
602 e2 = table.get(slot);
603 }
604 }
605
606 if (evictLock.tryLock()) {
607 try {
608 gc();
609 evict();
610 } finally {
611 evictLock.unlock();
612 }
613 }
614
615 return v;
616 }
617
618 private ByteWindow scan(Entry n, PackFile pack, long position) {
619 for (; n != null; n = n.next) {
620 final PageRef<ByteWindow> r = n.ref;
621 if (r.getPack() == pack && r.getPosition() == position) {
622 final ByteWindow v = r.get();
623 if (v != null) {
624 hit(r);
625 return v;
626 }
627 n.kill();
628 break;
629 }
630 }
631 return null;
632 }
633
634 private void hit(PageRef r) {
635
636
637
638
639
640
641
642 final long c = clock.get();
643 clock.compareAndSet(c, c + 1);
644 r.setLastAccess(c);
645 }
646
647 private void evict() {
648 while (isFull()) {
649 int ptr = rng.nextInt(tableSize);
650 Entry old = null;
651 int slot = 0;
652 for (int b = evictBatch - 1; b >= 0; b--, ptr++) {
653 if (tableSize <= ptr)
654 ptr = 0;
655 for (Entry e = table.get(ptr); e != null; e = e.next) {
656 if (e.dead)
657 continue;
658 if (old == null || e.ref.getLastAccess() < old.ref
659 .getLastAccess()) {
660 old = e;
661 slot = ptr;
662 }
663 }
664 }
665 if (old != null) {
666 old.kill();
667 gc();
668 final Entry e1 = table.get(slot);
669 table.compareAndSet(slot, e1, clean(e1));
670 }
671 }
672 }
673
674
675
676
677
678
679
680
681
682
683
684 private void removeAll() {
685 for (int s = 0; s < tableSize; s++) {
686 Entry e1;
687 do {
688 e1 = table.get(s);
689 for (Entry e = e1; e != null; e = e.next)
690 e.kill();
691 } while (!table.compareAndSet(s, e1, null));
692 }
693 gc();
694 }
695
696
697
698
699
700
701
702
703
704
705
706
707 private void removeAll(PackFile pack) {
708 for (int s = 0; s < tableSize; s++) {
709 final Entry e1 = table.get(s);
710 boolean hasDead = false;
711 for (Entry e = e1; e != null; e = e.next) {
712 if (e.ref.getPack() == pack) {
713 e.kill();
714 hasDead = true;
715 } else if (e.dead)
716 hasDead = true;
717 }
718 if (hasDead)
719 table.compareAndSet(s, e1, clean(e1));
720 }
721 gc();
722 }
723
724 private void gc() {
725 queue.gc();
726 }
727
728 private int slot(PackFile pack, long position) {
729 return (hash(pack.hash, position) >>> 1) % tableSize;
730 }
731
732 private Lock lock(PackFile pack, long position) {
733 return locks[(hash(pack.hash, position) >>> 1) % locks.length];
734 }
735
736 private static Entry clean(Entry top) {
737 while (top != null && top.dead) {
738 top.ref.kill();
739 top = top.next;
740 }
741 if (top == null)
742 return null;
743 final Entry n = clean(top.next);
744 return n == top.next ? top : new Entry(n, top.ref);
745 }
746
747 private static class Entry {
748
749 final Entry next;
750
751
752 final PageRef<ByteWindow> ref;
753
754
755
756
757
758
759
760
761 volatile boolean dead;
762
763 Entry(Entry n, PageRef<ByteWindow> r) {
764 next = n;
765 ref = r;
766 }
767
768 final void kill() {
769 dead = true;
770 ref.kill();
771 }
772 }
773
774 private static interface PageRef<T> {
775
776
777
778
779
780
781
782
783 T get();
784
785
786
787
788
789
790
791 boolean kill();
792
793
794
795
796
797
798 PackFile getPack();
799
800
801
802
803
804
805 long getPosition();
806
807
808
809
810
811
812 int getSize();
813
814
815
816
817
818
819 long getLastAccess();
820
821
822
823
824
825
826
827 void setLastAccess(long time);
828
829
830
831
832
833 boolean isStrongRef();
834 }
835
836
837 private static class SoftRef extends SoftReference<ByteWindow>
838 implements PageRef<ByteWindow> {
839 private final PackFile pack;
840
841 private final long position;
842
843 private final int size;
844
845 private long lastAccess;
846
847 protected SoftRef(final PackFile pack, final long position,
848 final ByteWindow v, final SoftCleanupQueue queue) {
849 super(v, queue);
850 this.pack = pack;
851 this.position = position;
852 this.size = v.size();
853 }
854
855 @Override
856 public PackFile getPack() {
857 return pack;
858 }
859
860 @Override
861 public long getPosition() {
862 return position;
863 }
864
865 @Override
866 public int getSize() {
867 return size;
868 }
869
870 @Override
871 public long getLastAccess() {
872 return lastAccess;
873 }
874
875 @Override
876 public void setLastAccess(long time) {
877 this.lastAccess = time;
878 }
879
880 @Override
881 public boolean kill() {
882 return enqueue();
883 }
884
885 @Override
886 public boolean isStrongRef() {
887 return false;
888 }
889 }
890
891
892 private static class StrongRef implements PageRef<ByteWindow> {
893 private ByteWindow referent;
894
895 private final PackFile pack;
896
897 private final long position;
898
899 private final int size;
900
901 private long lastAccess;
902
903 private CleanupQueue queue;
904
905 protected StrongRef(final PackFile pack, final long position,
906 final ByteWindow v, final CleanupQueue queue) {
907 this.pack = pack;
908 this.position = position;
909 this.referent = v;
910 this.size = v.size();
911 this.queue = queue;
912 }
913
914 @Override
915 public PackFile getPack() {
916 return pack;
917 }
918
919 @Override
920 public long getPosition() {
921 return position;
922 }
923
924 @Override
925 public int getSize() {
926 return size;
927 }
928
929 @Override
930 public long getLastAccess() {
931 return lastAccess;
932 }
933
934 @Override
935 public void setLastAccess(long time) {
936 this.lastAccess = time;
937 }
938
939 @Override
940 public ByteWindow get() {
941 return referent;
942 }
943
944 @Override
945 public boolean kill() {
946 if (referent == null) {
947 return false;
948 }
949 referent = null;
950 return queue.enqueue(this);
951 }
952
953 @Override
954 public boolean isStrongRef() {
955 return true;
956 }
957 }
958
959 private static interface CleanupQueue {
960 boolean enqueue(PageRef<ByteWindow> r);
961 void gc();
962 }
963
964 private static class SoftCleanupQueue extends ReferenceQueue<ByteWindow>
965 implements CleanupQueue {
966 private final WindowCache wc;
967
968 SoftCleanupQueue(WindowCache cache) {
969 this.wc = cache;
970 }
971
972 @Override
973 public boolean enqueue(PageRef<ByteWindow> r) {
974
975
976 return false;
977 }
978
979 @Override
980 public void gc() {
981 SoftRef r;
982 while ((r = (SoftRef) poll()) != null) {
983 wc.clear(r);
984
985 final int s = wc.slot(r.getPack(), r.getPosition());
986 final Entry e1 = wc.table.get(s);
987 for (Entry n = e1; n != null; n = n.next) {
988 if (n.ref == r) {
989 n.dead = true;
990 wc.table.compareAndSet(s, e1, clean(e1));
991 break;
992 }
993 }
994 }
995 }
996 }
997
998 private static class StrongCleanupQueue implements CleanupQueue {
999 private final WindowCache wc;
1000
1001 private final ConcurrentLinkedQueue<PageRef<ByteWindow>> queue = new ConcurrentLinkedQueue<>();
1002
1003 StrongCleanupQueue(WindowCache wc) {
1004 this.wc = wc;
1005 }
1006
1007 @Override
1008 public boolean enqueue(PageRef<ByteWindow> r) {
1009 if (queue.contains(r)) {
1010 return false;
1011 }
1012 return queue.add(r);
1013 }
1014
1015 @Override
1016 public void gc() {
1017 PageRef<ByteWindow> r;
1018 while ((r = queue.poll()) != null) {
1019 wc.clear(r);
1020
1021 final int s = wc.slot(r.getPack(), r.getPosition());
1022 final Entry e1 = wc.table.get(s);
1023 for (Entry n = e1; n != null; n = n.next) {
1024 if (n.ref == r) {
1025 n.dead = true;
1026 wc.table.compareAndSet(s, e1, clean(e1));
1027 break;
1028 }
1029 }
1030 }
1031 }
1032 }
1033
1034 private static final class Lock {
1035
1036 }
1037 }