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 Monitoring.registerMBean(mbean, "block_cache");
474
475 if (maxFiles < 1)
476 throw new IllegalArgumentException(JGitText.get().openFilesMustBeAtLeast1);
477 if (maxBytes < windowSize)
478 throw new IllegalArgumentException(JGitText.get().windowSizeMustBeLesserThanLimit);
479 }
480
481
482
483
484 public WindowCacheStats getStats() {
485 return statsRecorder.getStats();
486 }
487
488
489
490
491 public void resetStats() {
492 mbean.resetCounters();
493 }
494
495 private int hash(int packHash, long off) {
496 return packHash + (int) (off >>> windowSizeShift);
497 }
498
499 private ByteWindow load(PackFile pack, long offset) throws IOException {
500 long startTime = System.nanoTime();
501 if (pack.beginWindowCache())
502 statsRecorder.recordOpenFiles(1);
503 try {
504 if (mmap)
505 return pack.mmap(offset, windowSize);
506 ByteArrayWindow w = pack.read(offset, windowSize);
507 statsRecorder.recordLoadSuccess(System.nanoTime() - startTime);
508 return w;
509 } catch (IOException | RuntimeException | Error e) {
510 close(pack);
511 statsRecorder.recordLoadFailure(System.nanoTime() - startTime);
512 throw e;
513 } finally {
514 statsRecorder.recordMisses(1);
515 }
516 }
517
518 private PageRef<ByteWindow> createRef(PackFile p, long o, ByteWindow v) {
519 final PageRef<ByteWindow> ref = useStrongRefs
520 ? new StrongRef(p, o, v, queue)
521 : new SoftRef(p, o, v, (SoftCleanupQueue) queue);
522 statsRecorder.recordOpenBytes(ref.getPack(), ref.getSize());
523 return ref;
524 }
525
526 private void clear(PageRef<ByteWindow> ref) {
527 statsRecorder.recordOpenBytes(ref.getPack(), -ref.getSize());
528 statsRecorder.recordEvictions(1);
529 close(ref.getPack());
530 }
531
532 private void close(PackFile pack) {
533 if (pack.endWindowCache()) {
534 statsRecorder.recordOpenFiles(-1);
535 }
536 }
537
538 private boolean isFull() {
539 return maxFiles < mbean.getOpenFileCount()
540 || maxBytes < mbean.getOpenByteCount();
541 }
542
543 private long toStart(long offset) {
544 return (offset >>> windowSizeShift) << windowSizeShift;
545 }
546
547 private static int tableSize(WindowCacheConfig cfg) {
548 final int wsz = cfg.getPackedGitWindowSize();
549 final long limit = cfg.getPackedGitLimit();
550 if (wsz <= 0)
551 throw new IllegalArgumentException(JGitText.get().invalidWindowSize);
552 if (limit < wsz)
553 throw new IllegalArgumentException(JGitText.get().windowSizeMustBeLesserThanLimit);
554 return (int) Math.min(5 * (limit / wsz) / 2, 2000000000);
555 }
556
557 private static int lockCount(WindowCacheConfig cfg) {
558 return Math.max(cfg.getPackedGitOpenFiles(), 32);
559 }
560
561
562
563
564
565
566
567
568
569
570
571
572
573 private ByteWindow getOrLoad(PackFile pack, long position)
574 throws IOException {
575 final int slot = slot(pack, position);
576 final Entry e1 = table.get(slot);
577 ByteWindow v = scan(e1, pack, position);
578 if (v != null) {
579 statsRecorder.recordHits(1);
580 return v;
581 }
582
583 synchronized (lock(pack, position)) {
584 Entry e2 = table.get(slot);
585 if (e2 != e1) {
586 v = scan(e2, pack, position);
587 if (v != null) {
588 statsRecorder.recordHits(1);
589 return v;
590 }
591 }
592
593 v = load(pack, position);
594 final PageRef<ByteWindow> ref = createRef(pack, position, v);
595 hit(ref);
596 for (;;) {
597 final Entry n = new Entry(clean(e2), ref);
598 if (table.compareAndSet(slot, e2, n))
599 break;
600 e2 = table.get(slot);
601 }
602 }
603
604 if (evictLock.tryLock()) {
605 try {
606 gc();
607 evict();
608 } finally {
609 evictLock.unlock();
610 }
611 }
612
613 return v;
614 }
615
616 private ByteWindow scan(Entry n, PackFile pack, long position) {
617 for (; n != null; n = n.next) {
618 final PageRef<ByteWindow> r = n.ref;
619 if (r.getPack() == pack && r.getPosition() == position) {
620 final ByteWindow v = r.get();
621 if (v != null) {
622 hit(r);
623 return v;
624 }
625 n.kill();
626 break;
627 }
628 }
629 return null;
630 }
631
632 private void hit(PageRef r) {
633
634
635
636
637
638
639
640 final long c = clock.get();
641 clock.compareAndSet(c, c + 1);
642 r.setLastAccess(c);
643 }
644
645 private void evict() {
646 while (isFull()) {
647 int ptr = rng.nextInt(tableSize);
648 Entry old = null;
649 int slot = 0;
650 for (int b = evictBatch - 1; b >= 0; b--, ptr++) {
651 if (tableSize <= ptr)
652 ptr = 0;
653 for (Entry e = table.get(ptr); e != null; e = e.next) {
654 if (e.dead)
655 continue;
656 if (old == null || e.ref.getLastAccess() < old.ref
657 .getLastAccess()) {
658 old = e;
659 slot = ptr;
660 }
661 }
662 }
663 if (old != null) {
664 old.kill();
665 gc();
666 final Entry e1 = table.get(slot);
667 table.compareAndSet(slot, e1, clean(e1));
668 }
669 }
670 }
671
672
673
674
675
676
677
678
679
680
681
682 private void removeAll() {
683 for (int s = 0; s < tableSize; s++) {
684 Entry e1;
685 do {
686 e1 = table.get(s);
687 for (Entry e = e1; e != null; e = e.next)
688 e.kill();
689 } while (!table.compareAndSet(s, e1, null));
690 }
691 gc();
692 }
693
694
695
696
697
698
699
700
701
702
703
704
705 private void removeAll(PackFile pack) {
706 for (int s = 0; s < tableSize; s++) {
707 final Entry e1 = table.get(s);
708 boolean hasDead = false;
709 for (Entry e = e1; e != null; e = e.next) {
710 if (e.ref.getPack() == pack) {
711 e.kill();
712 hasDead = true;
713 } else if (e.dead)
714 hasDead = true;
715 }
716 if (hasDead)
717 table.compareAndSet(s, e1, clean(e1));
718 }
719 gc();
720 }
721
722 private void gc() {
723 queue.gc();
724 }
725
726 private int slot(PackFile pack, long position) {
727 return (hash(pack.hash, position) >>> 1) % tableSize;
728 }
729
730 private Lock lock(PackFile pack, long position) {
731 return locks[(hash(pack.hash, position) >>> 1) % locks.length];
732 }
733
734 private static Entry clean(Entry top) {
735 while (top != null && top.dead) {
736 top.ref.kill();
737 top = top.next;
738 }
739 if (top == null)
740 return null;
741 final Entry n = clean(top.next);
742 return n == top.next ? top : new Entry(n, top.ref);
743 }
744
745 private static class Entry {
746
747 final Entry next;
748
749
750 final PageRef<ByteWindow> ref;
751
752
753
754
755
756
757
758
759 volatile boolean dead;
760
761 Entry(Entry n, PageRef<ByteWindow> r) {
762 next = n;
763 ref = r;
764 }
765
766 final void kill() {
767 dead = true;
768 ref.kill();
769 }
770 }
771
772 private static interface PageRef<T> {
773
774
775
776
777
778
779
780
781 T get();
782
783
784
785
786
787
788
789 boolean kill();
790
791
792
793
794
795
796 PackFile getPack();
797
798
799
800
801
802
803 long getPosition();
804
805
806
807
808
809
810 int getSize();
811
812
813
814
815
816
817 long getLastAccess();
818
819
820
821
822
823
824
825 void setLastAccess(long time);
826
827
828
829
830
831 boolean isStrongRef();
832 }
833
834
835 private static class SoftRef extends SoftReference<ByteWindow>
836 implements PageRef<ByteWindow> {
837 private final PackFile pack;
838
839 private final long position;
840
841 private final int size;
842
843 private long lastAccess;
844
845 protected SoftRef(final PackFile pack, final long position,
846 final ByteWindow v, final SoftCleanupQueue queue) {
847 super(v, queue);
848 this.pack = pack;
849 this.position = position;
850 this.size = v.size();
851 }
852
853 @Override
854 public PackFile getPack() {
855 return pack;
856 }
857
858 @Override
859 public long getPosition() {
860 return position;
861 }
862
863 @Override
864 public int getSize() {
865 return size;
866 }
867
868 @Override
869 public long getLastAccess() {
870 return lastAccess;
871 }
872
873 @Override
874 public void setLastAccess(long time) {
875 this.lastAccess = time;
876 }
877
878 @Override
879 public boolean kill() {
880 return enqueue();
881 }
882
883 @Override
884 public boolean isStrongRef() {
885 return false;
886 }
887 }
888
889
890 private static class StrongRef implements PageRef<ByteWindow> {
891 private ByteWindow referent;
892
893 private final PackFile pack;
894
895 private final long position;
896
897 private final int size;
898
899 private long lastAccess;
900
901 private CleanupQueue queue;
902
903 protected StrongRef(final PackFile pack, final long position,
904 final ByteWindow v, final CleanupQueue queue) {
905 this.pack = pack;
906 this.position = position;
907 this.referent = v;
908 this.size = v.size();
909 this.queue = queue;
910 }
911
912 @Override
913 public PackFile getPack() {
914 return pack;
915 }
916
917 @Override
918 public long getPosition() {
919 return position;
920 }
921
922 @Override
923 public int getSize() {
924 return size;
925 }
926
927 @Override
928 public long getLastAccess() {
929 return lastAccess;
930 }
931
932 @Override
933 public void setLastAccess(long time) {
934 this.lastAccess = time;
935 }
936
937 @Override
938 public ByteWindow get() {
939 return referent;
940 }
941
942 @Override
943 public boolean kill() {
944 if (referent == null) {
945 return false;
946 }
947 referent = null;
948 return queue.enqueue(this);
949 }
950
951 @Override
952 public boolean isStrongRef() {
953 return true;
954 }
955 }
956
957 private static interface CleanupQueue {
958 boolean enqueue(PageRef<ByteWindow> r);
959 void gc();
960 }
961
962 private static class SoftCleanupQueue extends ReferenceQueue<ByteWindow>
963 implements CleanupQueue {
964 private final WindowCache wc;
965
966 SoftCleanupQueue(WindowCache cache) {
967 this.wc = cache;
968 }
969
970 @Override
971 public boolean enqueue(PageRef<ByteWindow> r) {
972
973
974 return false;
975 }
976
977 @Override
978 public void gc() {
979 SoftRef r;
980 while ((r = (SoftRef) poll()) != null) {
981 wc.clear(r);
982
983 final int s = wc.slot(r.getPack(), r.getPosition());
984 final Entry e1 = wc.table.get(s);
985 for (Entry n = e1; n != null; n = n.next) {
986 if (n.ref == r) {
987 n.dead = true;
988 wc.table.compareAndSet(s, e1, clean(e1));
989 break;
990 }
991 }
992 }
993 }
994 }
995
996 private static class StrongCleanupQueue implements CleanupQueue {
997 private final WindowCache wc;
998
999 private final ConcurrentLinkedQueue<PageRef<ByteWindow>> queue = new ConcurrentLinkedQueue<>();
1000
1001 StrongCleanupQueue(WindowCache wc) {
1002 this.wc = wc;
1003 }
1004
1005 @Override
1006 public boolean enqueue(PageRef<ByteWindow> r) {
1007 if (queue.contains(r)) {
1008 return false;
1009 }
1010 return queue.add(r);
1011 }
1012
1013 @Override
1014 public void gc() {
1015 PageRef<ByteWindow> r;
1016 while ((r = queue.poll()) != null) {
1017 wc.clear(r);
1018
1019 final int s = wc.slot(r.getPack(), r.getPosition());
1020 final Entry e1 = wc.table.get(s);
1021 for (Entry n = e1; n != null; n = n.next) {
1022 if (n.ref == r) {
1023 n.dead = true;
1024 wc.table.compareAndSet(s, e1, clean(e1));
1025 break;
1026 }
1027 }
1028 }
1029 }
1030 }
1031
1032 private static final class Lock {
1033
1034 }
1035 }