@@ -168,12 +168,15 @@ final class BucketSizeInfo {
168168 // Free bucket means it has space to allocate a block;
169169 // Completely free bucket means it has no block.
170170 private LinkedMap bucketList , freeBuckets , completelyFreeBuckets ;
171+ // only modified under synchronization, but also read outside it.
172+ private volatile long fragmentationBytes ;
171173 private int sizeIndex ;
172174
173175 BucketSizeInfo (int sizeIndex ) {
174176 bucketList = new LinkedMap ();
175177 freeBuckets = new LinkedMap ();
176178 completelyFreeBuckets = new LinkedMap ();
179+ fragmentationBytes = 0 ;
177180 this .sizeIndex = sizeIndex ;
178181 }
179182
@@ -193,7 +196,7 @@ public int sizeIndex() {
193196 * Find a bucket to allocate a block
194197 * @return the offset in the IOEngine
195198 */
196- public long allocateBlock () {
199+ public long allocateBlock (int blockSize ) {
197200 Bucket b = null ;
198201 if (freeBuckets .size () > 0 ) {
199202 // Use up an existing one first...
@@ -206,6 +209,9 @@ public long allocateBlock() {
206209 if (b == null ) return -1 ;
207210 long result = b .allocate ();
208211 blockAllocated (b );
212+ if (blockSize < b .getItemAllocationSize ()) {
213+ fragmentationBytes += b .getItemAllocationSize () - blockSize ;
214+ }
209215 return result ;
210216 }
211217
@@ -236,23 +242,38 @@ private synchronized void removeBucket(Bucket b) {
236242 completelyFreeBuckets .remove (b );
237243 }
238244
239- public void freeBlock (Bucket b , long offset ) {
245+ public void freeBlock (Bucket b , long offset , int length ) {
240246 assert bucketList .containsKey (b );
241247 // else we shouldn't have anything to free...
242248 assert (!completelyFreeBuckets .containsKey (b ));
243249 b .free (offset );
250+ if (length < b .getItemAllocationSize ()) {
251+ fragmentationBytes -= b .getItemAllocationSize () - length ;
252+ }
244253 if (!freeBuckets .containsKey (b )) freeBuckets .put (b , b );
245254 if (b .isCompletelyFree ()) completelyFreeBuckets .put (b , b );
246255 }
247256
248257 public synchronized IndexStatistics statistics () {
249258 long free = 0 , used = 0 ;
259+ int full = 0 ;
250260 for (Object obj : bucketList .keySet ()) {
251261 Bucket b = (Bucket ) obj ;
252262 free += b .freeCount ();
253263 used += b .usedCount ();
264+ if (!b .hasFreeSpace ()) {
265+ full ++;
266+ }
254267 }
255- return new IndexStatistics (free , used , bucketSizes [sizeIndex ]);
268+ int bucketObjectSize = bucketSizes [sizeIndex ];
269+ // this is most likely to always be 1 or 0
270+ int fillingBuckets = Math .max (0 , freeBuckets .size () - completelyFreeBuckets .size ());
271+ // if bucket capacity is not perfectly divisible by a bucket's object size, there will
272+ // be some left over per bucket. for some object sizes this may be large enough to be
273+ // non-trivial and worth tuning by choosing a more divisible object size.
274+ long wastedBytes = (bucketCapacity % bucketObjectSize ) * (full + fillingBuckets );
275+ return new IndexStatistics (free , used , bucketObjectSize , full , completelyFreeBuckets .size (),
276+ wastedBytes , fragmentationBytes );
256277 }
257278
258279 @ Override
@@ -434,7 +455,7 @@ public synchronized long allocateBlock(int blockSize)
434455 + "; adjust BucketCache sizes " + BlockCacheFactory .BUCKET_CACHE_BUCKETS_KEY
435456 + " to accomodate if size seems reasonable and you want it cached." );
436457 }
437- long offset = bsi .allocateBlock ();
458+ long offset = bsi .allocateBlock (blockSize );
438459
439460 // Ask caller to free up space and try again!
440461 if (offset < 0 ) throw new CacheFullException (blockSize , bsi .sizeIndex ());
@@ -455,11 +476,11 @@ private Bucket grabGlobalCompletelyFreeBucket() {
455476 * @param offset block's offset
456477 * @return size freed
457478 */
458- public synchronized int freeBlock (long offset ) {
479+ public synchronized int freeBlock (long offset , int length ) {
459480 int bucketNo = (int ) (offset / bucketCapacity );
460481 assert bucketNo >= 0 && bucketNo < buckets .length ;
461482 Bucket targetBucket = buckets [bucketNo ];
462- bucketSizeInfos [targetBucket .sizeIndex ()].freeBlock (targetBucket , offset );
483+ bucketSizeInfos [targetBucket .sizeIndex ()].freeBlock (targetBucket , offset , length );
463484 usedSize -= targetBucket .getItemAllocationSize ();
464485 return targetBucket .getItemAllocationSize ();
465486 }
@@ -478,77 +499,185 @@ public int sizeOfAllocation(long offset) {
478499 return targetBucket .getItemAllocationSize ();
479500 }
480501
502+ /**
503+ * Statistics to give a glimpse into the distribution of BucketCache objects. Each configured
504+ * bucket size, denoted by {@link BucketSizeInfo}, gets an IndexStatistic. A BucketSizeInfo
505+ * allocates blocks of a configured size from claimed buckets. If you have a bucket size of 512k,
506+ * the corresponding BucketSizeInfo will always allocate chunks of 512k at a time regardless of
507+ * actual request.
508+ * <p>
509+ * Over time, as a BucketSizeInfo gets more allocations, it will claim more buckets from the total
510+ * pool of completelyFreeBuckets. As blocks are freed from a BucketSizeInfo, those buckets may be
511+ * returned to the completelyFreeBuckets pool.
512+ * <p>
513+ * The IndexStatistics help visualize how these buckets are currently distributed, through counts
514+ * of items, bytes, and fullBuckets. Additionally, mismatches between block sizes and bucket sizes
515+ * can manifest in inefficient cache usage. These typically manifest in three ways:
516+ * <p>
517+ * 1. Allocation failures, because block size is larger than max bucket size. These show up in
518+ * logs and can be alleviated by adding larger bucket sizes if appropriate.<br>
519+ * 2. Memory fragmentation, because blocks are typically smaller than the bucket size. See
520+ * {@link #fragmentationBytes()} for details.<br>
521+ * 3. Memory waste, because a bucket's itemSize is not a perfect divisor of bucketCapacity. see
522+ * {@link #wastedBytes()} for details.<br>
523+ */
481524 static class IndexStatistics {
482- private long freeCount , usedCount , itemSize , totalCount ;
525+ private long freeCount , usedCount , itemSize , totalCount , wastedBytes , fragmentationBytes ;
526+ private int fullBuckets , completelyFreeBuckets ;
483527
528+ /**
529+ * How many more items can be allocated from the currently claimed blocks of this bucket size
530+ */
484531 public long freeCount () {
485532 return freeCount ;
486533 }
487534
535+ /**
536+ * How many items are currently taking up space in this bucket size's buckets
537+ */
488538 public long usedCount () {
489539 return usedCount ;
490540 }
491541
542+ /**
543+ * Combined {@link #freeCount()} + {@link #usedCount()}
544+ */
492545 public long totalCount () {
493546 return totalCount ;
494547 }
495548
549+ /**
550+ * How many more bytes can be allocated from the currently claimed blocks of this bucket size
551+ */
496552 public long freeBytes () {
497553 return freeCount * itemSize ;
498554 }
499555
556+ /**
557+ * How many bytes are currently taking up space in this bucket size's buckets Note: If your
558+ * items are less than the bucket size of this bucket, the actual used bytes by items will be
559+ * lower than this value. But since a bucket size can only allocate items of a single size, this
560+ * value is the true number of used bytes. The difference will be counted in
561+ * {@link #fragmentationBytes()}.
562+ */
500563 public long usedBytes () {
501564 return usedCount * itemSize ;
502565 }
503566
567+ /**
568+ * Combined {@link #totalCount()} * {@link #itemSize()}
569+ */
504570 public long totalBytes () {
505571 return totalCount * itemSize ;
506572 }
507573
574+ /**
575+ * This bucket size can only allocate items of this size, even if the requested allocation size
576+ * is smaller. The rest goes towards {@link #fragmentationBytes()}.
577+ */
508578 public long itemSize () {
509579 return itemSize ;
510580 }
511581
512- public IndexStatistics (long free , long used , long itemSize ) {
513- setTo (free , used , itemSize );
582+ /**
583+ * How many buckets have been completely filled by blocks for this bucket size. These buckets
584+ * can't accept any more blocks unless some existing are freed.
585+ */
586+ public int fullBuckets () {
587+ return fullBuckets ;
588+ }
589+
590+ /**
591+ * How many buckets are currently claimed by this bucket size but as yet totally unused. These
592+ * buckets are available for reallocation to other bucket sizes if those fill up.
593+ */
594+ public int completelyFreeBuckets () {
595+ return completelyFreeBuckets ;
596+ }
597+
598+ /**
599+ * If {@link #bucketCapacity} is not perfectly divisible by this {@link #itemSize()}, the
600+ * remainder will be unusable by in buckets of this size. A high value here may be optimized by
601+ * trying to choose bucket sizes which can better divide {@link #bucketCapacity}.
602+ */
603+ public long wastedBytes () {
604+ return wastedBytes ;
605+ }
606+
607+ /**
608+ * Every time you allocate blocks in these buckets where the block size is less than the bucket
609+ * size, fragmentation increases by that difference. You can reduce fragmentation by lowering
610+ * the bucket size so that it is closer to the typical block size. This may have the consequence
611+ * of bumping some blocks to the next larger bucket size, so experimentation may be needed.
612+ */
613+ public long fragmentationBytes () {
614+ return fragmentationBytes ;
615+ }
616+
617+ public IndexStatistics (long free , long used , long itemSize , int fullBuckets ,
618+ int completelyFreeBuckets , long wastedBytes , long fragmentationBytes ) {
619+ setTo (free , used , itemSize , fullBuckets , completelyFreeBuckets , wastedBytes ,
620+ fragmentationBytes );
514621 }
515622
516623 public IndexStatistics () {
517- setTo (-1 , -1 , 0 );
624+ setTo (-1 , -1 , 0 , 0 , 0 , 0 , 0 );
518625 }
519626
520- public void setTo (long free , long used , long itemSize ) {
627+ public void setTo (long free , long used , long itemSize , int fullBuckets ,
628+ int completelyFreeBuckets , long wastedBytes , long fragmentationBytes ) {
521629 this .itemSize = itemSize ;
522630 this .freeCount = free ;
523631 this .usedCount = used ;
524632 this .totalCount = free + used ;
633+ this .fullBuckets = fullBuckets ;
634+ this .completelyFreeBuckets = completelyFreeBuckets ;
635+ this .wastedBytes = wastedBytes ;
636+ this .fragmentationBytes = fragmentationBytes ;
525637 }
526638 }
527639
528640 public Bucket [] getBuckets () {
529641 return this .buckets ;
530642 }
531643
532- void logStatistics () {
644+ void logDebugStatistics () {
645+ if (!LOG .isDebugEnabled ()) {
646+ return ;
647+ }
648+
533649 IndexStatistics total = new IndexStatistics ();
534650 IndexStatistics [] stats = getIndexStatistics (total );
535- LOG .info ("Bucket allocator statistics follow:\n " );
536- LOG .info (" Free bytes=" + total .freeBytes () + "+; used bytes=" + total .usedBytes ()
537- + "; total bytes=" + total .totalBytes ());
651+ LOG .debug ("Bucket allocator statistics follow:" );
652+ LOG .debug (
653+ " Free bytes={}; used bytes={}; total bytes={}; wasted bytes={}; fragmentation bytes={}; "
654+ + "completelyFreeBuckets={}" ,
655+ total .freeBytes (), total .usedBytes (), total .totalBytes (), total .wastedBytes (),
656+ total .fragmentationBytes (), total .completelyFreeBuckets ());
538657 for (IndexStatistics s : stats ) {
539- LOG .info (" Object size " + s .itemSize () + " used=" + s .usedCount () + "; free="
540- + s .freeCount () + "; total=" + s .totalCount ());
658+ LOG .debug (
659+ " Object size {}; used={}; free={}; total={}; wasted bytes={}; fragmentation bytes={}, "
660+ + "full buckets={}" ,
661+ s .itemSize (), s .usedCount (), s .freeCount (), s .totalCount (), s .wastedBytes (),
662+ s .fragmentationBytes (), s .fullBuckets ());
541663 }
542664 }
543665
544666 IndexStatistics [] getIndexStatistics (IndexStatistics grandTotal ) {
545667 IndexStatistics [] stats = getIndexStatistics ();
546- long totalfree = 0 , totalused = 0 ;
668+ long totalfree = 0 , totalused = 0 , totalWasted = 0 , totalFragmented = 0 ;
669+ int fullBuckets = 0 , completelyFreeBuckets = 0 ;
670+
547671 for (IndexStatistics stat : stats ) {
548672 totalfree += stat .freeBytes ();
549673 totalused += stat .usedBytes ();
674+ totalWasted += stat .wastedBytes ();
675+ totalFragmented += stat .fragmentationBytes ();
676+ fullBuckets += stat .fullBuckets ();
677+ completelyFreeBuckets += stat .completelyFreeBuckets ();
550678 }
551- grandTotal .setTo (totalfree , totalused , 1 );
679+ grandTotal .setTo (totalfree , totalused , 1 , fullBuckets , completelyFreeBuckets , totalWasted ,
680+ totalFragmented );
552681 return stats ;
553682 }
554683
@@ -559,13 +688,6 @@ IndexStatistics[] getIndexStatistics() {
559688 return stats ;
560689 }
561690
562- public long freeBlock (long freeList []) {
563- long sz = 0 ;
564- for (int i = 0 ; i < freeList .length ; ++i )
565- sz += freeBlock (freeList [i ]);
566- return sz ;
567- }
568-
569691 public int getBucketIndex (long offset ) {
570692 return (int ) (offset / bucketCapacity );
571693 }
0 commit comments