/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.hive.llap.cache; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.hadoop.hive.common.io.Allocator; import org.apache.hadoop.hive.common.io.DataCache.BooleanRef; import org.apache.hadoop.hive.common.io.DataCache.DiskRangeListFactory; import org.apache.hadoop.hive.common.io.encoded.MemoryBuffer; import org.apache.hadoop.hive.llap.DebugUtils; import org.apache.hadoop.hive.llap.cache.LowLevelCache.Priority; import org.apache.hadoop.hive.llap.io.api.impl.LlapIoImpl; import org.apache.hadoop.hive.llap.metrics.LlapDaemonCacheMetrics; import org.apache.hive.common.util.Ref; import org.apache.orc.OrcProto; import org.apache.orc.OrcProto.ColumnEncoding; import com.google.common.base.Function; public class SerDeLowLevelCacheImpl implements LlapOomDebugDump { private static final int DEFAULT_CLEANUP_INTERVAL = 600; private final Allocator allocator; private final AtomicInteger newEvictions = new AtomicInteger(0); private Thread cleanupThread = null; private final ConcurrentHashMap<Object, FileCache<FileData>> cache = new ConcurrentHashMap<>(); private final LowLevelCachePolicy cachePolicy; private final long cleanupInterval; private final LlapDaemonCacheMetrics metrics; private static final class StripeInfoComparator implements Comparator<StripeData> { @Override public int compare(StripeData o1, StripeData o2) { int starts = Long.compare(o1.knownTornStart, o2.knownTornStart); if (starts != 0) return starts; starts = Long.compare(o1.firstStart, o2.firstStart); if (starts != 0) return starts; assert (o1.lastStart == o2.lastStart) == (o1.lastEnd == o2.lastEnd); return Long.compare(o1.lastStart, o2.lastStart); } } public static class FileData { /** * RW lock ensures we have a consistent view of the file data, which is important given that * we generate "stripe" boundaries arbitrarily. Reading buffer data itself doesn't require * that this lock is held; however, everything else in stripes list does. * TODO: make more granular? We only care that each one reader sees consistent boundaries. * So, we could shallow-copy the stripes list, then have individual locks inside each. */ private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Object fileKey; private final int colCount; private ArrayList<StripeData> stripes; public FileData(Object fileKey, int colCount) { this.fileKey = fileKey; this.colCount = colCount; } public void toString(StringBuilder sb) { sb.append("File data for ").append(fileKey).append(" with ").append(colCount) .append(" columns: ").append(stripes); } public int getColCount() { return colCount; } public ArrayList<StripeData> getData() { return stripes; } public void addStripe(StripeData sd) { if (stripes == null) { stripes = new ArrayList<>(); } stripes.add(sd); } @Override public String toString() { return "[fileKey=" + fileKey + ", colCount=" + colCount + ", stripes=" + stripes + "]"; } } public static final class StripeData { // In LRR case, if we just store 2 boundaries (which could be split boundaries or reader // positions), we wouldn't be able to account for torn rows correctly because the semantics of // our "exact" reader positions, and inexact split boundaries, are different. We cannot even // tell LRR to use exact boundaries, as there can be a mismatch in an original mid-file split // wrt first row when caching - we may produce incorrect result if we adjust the split // boundary, and also if we don't adjust it, depending where it falls. At best, we'd end up // with spurious disk reads if we cache on row boundaries but splits include torn rows. // This structure implies that when reading a split, we skip the first torn row but fully // read the last torn row (as LineRecordReader does). If we want to support a different scheme, // we'd need to store more offsets and make logic account for that. private long knownTornStart; // This can change based on new splits. private final long firstStart, lastStart, lastEnd; // TODO: we can actually consider storing ALL the delta encoded row offsets - not a lot of // overhead compared to the data itself, and with row offsets, we could use columnar // blocks for inconsistent splits. We are not optimizing for inconsistent splits for now. private final long rowCount; private final OrcProto.ColumnEncoding[] encodings; private LlapDataBuffer[][][] data; // column index, stream type, buffers public StripeData(long knownTornStart, long firstStart, long lastStart, long lastEnd, long rowCount, ColumnEncoding[] encodings) { this.knownTornStart = knownTornStart; this.firstStart = firstStart; this.lastStart = lastStart; this.lastEnd = lastEnd; this.encodings = encodings; this.rowCount = rowCount; this.data = encodings == null ? null : new LlapDataBuffer[encodings.length][][]; } @Override public String toString() { return toCoordinateString() + " with encodings [" + Arrays.toString(encodings) .replace('\n', ' ') + "] and data " + SerDeLowLevelCacheImpl.toString(data); } public long getKnownTornStart() { return knownTornStart; } public long getFirstStart() { return firstStart; } public long getLastStart() { return lastStart; } public long getLastEnd() { return lastEnd; } public long getRowCount() { return rowCount; } public OrcProto.ColumnEncoding[] getEncodings() { return encodings; } public LlapDataBuffer[][][] getData() { return data; } public String toCoordinateString() { return "stripe kts " + knownTornStart + " from " + firstStart + " to [" + lastStart + ", " + lastEnd + ")"; } public static StripeData duplicateStructure(StripeData s) { return new StripeData(s.knownTornStart, s.firstStart, s.lastStart, s.lastEnd, s.rowCount, new OrcProto.ColumnEncoding[s.encodings.length]); } public void setKnownTornStart(long value) { knownTornStart = value; } } public static String toString(LlapDataBuffer[][][] data) { if (data == null) return "null"; StringBuilder sb = new StringBuilder("["); for (int i = 0; i < data.length; ++i) { LlapDataBuffer[][] colData = data[i]; if (colData == null) { sb.append("null, "); continue; } sb.append("colData ["); for (int j = 0; j < colData.length; ++j) { LlapDataBuffer[] streamData = colData[j]; if (streamData == null) { sb.append("null, "); continue; } sb.append("buffers ["); for (int k = 0; k < streamData.length; ++k) { sb.append(streamData[k]); } sb.append("], "); } sb.append("], "); } sb.append("]"); return sb.toString(); } public static String toString(LlapDataBuffer[][] data) { if (data == null) return "null"; StringBuilder sb = new StringBuilder("["); for (int j = 0; j < data.length; ++j) { LlapDataBuffer[] streamData = data[j]; if (streamData == null) { sb.append("null, "); continue; } sb.append("["); for (int k = 0; k < streamData.length; ++k) { LlapDataBuffer s = streamData[k]; sb.append(LlapDataBuffer.toDataString(s)); } sb.append("], "); } sb.append("]"); return sb.toString(); } public SerDeLowLevelCacheImpl( LlapDaemonCacheMetrics metrics, LowLevelCachePolicy cachePolicy, Allocator allocator) { this.cachePolicy = cachePolicy; this.allocator = allocator; this.cleanupInterval = DEFAULT_CLEANUP_INTERVAL; this.metrics = metrics; LlapIoImpl.LOG.info("SerDe low-level level cache; cleanup interval {} sec", cleanupInterval); } public void startThreads() { if (cleanupInterval < 0) return; cleanupThread = new CleanupThread(cache, newEvictions, cleanupInterval); cleanupThread.start(); } public FileData getFileData(Object fileKey, long start, long end, boolean[] includes, DiskRangeListFactory factory, LowLevelCacheCounters qfCounters, BooleanRef gotAllData) throws IOException { FileCache<FileData> subCache = cache.get(fileKey); if (subCache == null || !subCache.incRef()) { if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) { LlapIoImpl.CACHE_LOGGER.trace("Cannot find cache for " + fileKey + " in " + cache); } markAllAsMissed(start, end, qfCounters, gotAllData); return null; } try { FileData cached = subCache.getCache(); cached.rwLock.readLock().lock(); if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) { LlapIoImpl.CACHE_LOGGER.trace("Cache for " + fileKey + " is " + subCache.getCache()); } try { if (cached.stripes == null) { LlapIoImpl.CACHE_LOGGER.debug("Cannot find any stripes for " + fileKey); markAllAsMissed(start, end, qfCounters, gotAllData); return null; } if (includes.length > cached.colCount) { throw new IOException("Includes " + DebugUtils.toString(includes) + " for " + cached.colCount + " columns"); } FileData result = new FileData(cached.fileKey, cached.colCount); if (gotAllData != null) { gotAllData.value = true; } // We will adjust start and end so that we could record the metrics; save the originals. long origStart = start, origEnd = end; // startIx is inclusive, endIx is exclusive. int startIx = Integer.MIN_VALUE, endIx = Integer.MIN_VALUE; LlapIoImpl.CACHE_LOGGER.debug("Looking for data between " + start + " and " + end); for (int i = 0; i < cached.stripes.size() && endIx == Integer.MIN_VALUE; ++i) { StripeData si = cached.stripes.get(i); if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) { LlapIoImpl.CACHE_LOGGER.trace("Looking at " + si.toCoordinateString()); } if (startIx == i) { // The start of the split was in the middle of the previous slice. start = si.knownTornStart; } else if (startIx == Integer.MIN_VALUE) { // Determine if we need to read this slice for the split. if (si.lastEnd <= start) continue; // Slice before the start of the split. // Start of the split falls somewhere within or before this slice. // Note the ">=" - LineRecordReader will skip the first row even if we start // directly at its start, because it cannot know if it's the start or not. // Unless it's 0; note that we DO give 0 special treatment here, unlike the EOF below, // because zero is zero. Need to mention it in Javadoc. if (start == 0 && si.firstStart == 0) { startIx = i; } else if (start >= si.firstStart) { // If the start of the split points into the middle of the cached slice, we cannot // use the cached block - it's encoded and columnar, so we cannot map the file // offset to some "offset" in "middle" of the slice (but see TODO for firstStart). startIx = i + 1; // continue; } else { // Start of the split is before this slice. startIx = i; // Simple case - we will read cache from the split start offset. start = si.knownTornStart; } } // Determine if this (or previous) is the last slice we need to read for this split. if (startIx != Integer.MIN_VALUE && endIx == Integer.MIN_VALUE) { if (si.lastEnd <= end) { // The entire current slice is part of the split. Note that if split end EQUALS // lastEnd, the split would also read the next row, so we do need to look at the // next slice, if any (although we'd probably find we cannot use it). // Note also that we DO NOT treat end-of-file differently here, cause we do not know // of any such thing. The caller must handle lastEnd vs end of split vs end of file // match correctly in terms of how LRR handles them. See above for start-of-file. if (i + 1 != cached.stripes.size()) continue; endIx = i + 1; end = si.lastEnd; } else if (si.lastStart <= end) { // The split ends within (and would read) the last row of this slice. Exact match. endIx = i + 1; end = si.lastEnd; } else { // Either the slice comes entirely after the end of split (following a gap in cached // data); or the split ends in the middle of the slice, so it's the same as in the // startIx logic w.r.t. the partial match; so, we either don't want to, or cannot, // use this. There's no need to distinguish these two cases for now. endIx = i; end = (endIx > 0) ? cached.stripes.get(endIx - 1).lastEnd : start; } } } LlapIoImpl.CACHE_LOGGER.debug("Determined stripe indexes " + startIx + ", " + endIx); if (endIx <= startIx) { if (gotAllData != null) { gotAllData.value = false; } return null; // No data for the split, or it fits in the middle of one or two slices. } if (start > origStart || end < origEnd) { if (gotAllData != null) { gotAllData.value = false; } long totalMiss = Math.max(0, origEnd - end) + Math.max(0, start - origStart); metrics.incrCacheRequestedBytes(totalMiss); if (qfCounters != null) { qfCounters.recordCacheMiss(totalMiss); } } result.stripes = new ArrayList<>(endIx - startIx); for (int stripeIx = startIx; stripeIx < endIx; ++stripeIx) { getCacheDataForOneSlice(stripeIx, cached, result, gotAllData, includes, qfCounters); } return result; } finally { cached.rwLock.readLock().unlock(); } } finally { subCache.decRef(); } } private void getCacheDataForOneSlice(int stripeIx, FileData cached, FileData result, BooleanRef gotAllData, boolean[] includes, LowLevelCacheCounters qfCounters) { StripeData cStripe = cached.stripes.get(stripeIx); if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) { LlapIoImpl.CACHE_LOGGER.trace("Got stripe in cache " + cStripe); } StripeData stripe = StripeData.duplicateStructure(cStripe); result.stripes.add(stripe); boolean isMissed = false; for (int colIx = 0; colIx < cached.colCount; ++colIx) { if (!includes[colIx]) continue; if (cStripe.encodings[colIx] == null || cStripe.data[colIx] == null) { if (cStripe.data[colIx] != null) { throw new AssertionError(cStripe); // No encoding => must have no data. } isMissed = true; if (gotAllData != null) { gotAllData.value = false; } continue; } stripe.encodings[colIx] = cStripe.encodings[colIx]; LlapDataBuffer[][] cColData = cStripe.data[colIx]; assert cColData != null; for (int streamIx = 0; cColData != null && streamIx < cColData.length; ++streamIx) { LlapDataBuffer[] streamData = cColData[streamIx]; // Note: this relies on the fact that we always evict the entire column, so if // we have the column data, we assume we have all the streams we need. if (streamData == null) continue; for (int i = 0; i < streamData.length; ++i) { // Finally, we are going to use "i"! if (!lockBuffer(streamData[i], true)) { LlapIoImpl.CACHE_LOGGER.info("Couldn't lock data for stripe at " + stripeIx + ", colIx " + colIx + ", stream type " + streamIx); handleRemovedColumnData(cColData); cColData = null; isMissed = true; if (gotAllData != null) { gotAllData.value = false; } break; } } } // At this point, we have arrived at the level where we need all the data, and the // arrays never change. So we will just do a shallow assignment here instead of copy. stripe.data[colIx] = cColData; if (cColData == null) { stripe.encodings[colIx] = null; } } doMetricsStuffForOneSlice(qfCounters, stripe, isMissed); } private void doMetricsStuffForOneSlice( LowLevelCacheCounters qfCounters, StripeData stripe, boolean isMissed) { // Slice boundaries may not match split boundaries due to torn rows in either direction, // so this counter may not be consistent with splits. This is also why we increment // requested bytes here, instead of based on the split - we don't want the metrics to be // inconsistent with each other. No matter what we determine here, at least we'll account // for both in the same manner. long bytes = stripe.lastEnd - stripe.knownTornStart; metrics.incrCacheRequestedBytes(bytes); if (!isMissed) { metrics.incrCacheHitBytes(bytes); } if (qfCounters != null) { if (isMissed) { qfCounters.recordCacheMiss(bytes); } else { qfCounters.recordCacheHit(bytes); } } } private void markAllAsMissed(long from, long to, LowLevelCacheCounters qfCounters, BooleanRef gotAllData) { if (qfCounters != null) { metrics.incrCacheRequestedBytes(to - from); qfCounters.recordCacheMiss(to - from); } if (gotAllData != null) { gotAllData.value = false; } } private boolean lockBuffer(LlapDataBuffer buffer, boolean doNotifyPolicy) { int rc = buffer.incRef(); if (rc > 0) { metrics.incrCacheNumLockedBuffers(); } if (doNotifyPolicy && rc == 1) { // We have just locked a buffer that wasn't previously locked. cachePolicy.notifyLock(buffer); } return rc > 0; } public void putFileData(final FileData data, Priority priority, LowLevelCacheCounters qfCounters) { // TODO: buffers are accounted for at allocation time, but ideally we should report the memory // overhead from the java objects to memory manager and remove it when discarding file. if (data.stripes == null || data.stripes.isEmpty()) { LlapIoImpl.LOG.warn("Trying to cache FileData with no data for " + data.fileKey); return; } FileCache<FileData> subCache = null; FileData cached = null; data.rwLock.writeLock().lock(); try { subCache = FileCache.getOrAddFileSubCache( cache, data.fileKey, new Function<Void, FileData>() { @Override public FileData apply(Void input) { return data; // If we don't have a file cache, we will add this one as is. } }); cached = subCache.getCache(); } finally { if (data != cached) { data.rwLock.writeLock().unlock(); } } try { if (data != cached) { cached.rwLock.writeLock().lock(); } try { for (StripeData si : data.stripes) { lockAllBuffersForPut(si, priority); } if (data == cached) { if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) { LlapIoImpl.CACHE_LOGGER.trace("Cached new data " + data); } return; } if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) { LlapIoImpl.CACHE_LOGGER.trace("Merging old " + cached + " and new " + data); } ArrayList<StripeData> combined = new ArrayList<>( cached.stripes.size() + data.stripes.size()); combined.addAll(cached.stripes); combined.addAll(data.stripes); Collections.sort(combined, new StripeInfoComparator()); int lastIx = combined.size() - 1; for (int ix = 0; ix < lastIx; ++ix) { StripeData cur = combined.get(ix), next = combined.get(ix + 1); if (cur.lastEnd <= next.firstStart) continue; // All good. if (cur.firstStart == next.firstStart && cur.lastEnd == next.lastEnd) { mergeStripeInfos(cur, next); combined.remove(ix + 1); --lastIx; // Don't recheck with next, only 2 lists each w/o collisions. continue; } // The original lists do not contain collisions, so only one is 'old'. boolean isCurOriginal = cached.stripes.contains(cur); handleRemovedStripeInfo(combined.remove(isCurOriginal ? ix : ix + 1)); --ix; --lastIx; } cached.stripes = combined; if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) { LlapIoImpl.CACHE_LOGGER.trace("New cache data is " + combined); } } finally { cached.rwLock.writeLock().unlock(); } } finally { subCache.decRef(); } } private void lockAllBuffersForPut(StripeData si, Priority priority) { for (int i = 0; i < si.data.length; ++i) { LlapDataBuffer[][] colData = si.data[i]; if (colData == null) continue; for (int j = 0; j < colData.length; ++j) { LlapDataBuffer[] streamData = colData[j]; if (streamData == null) continue; for (int k = 0; k < streamData.length; ++k) { boolean canLock = lockBuffer(streamData[k], false); // false - not in cache yet assert canLock; cachePolicy.cache(streamData[k], priority); streamData[k].declaredCachedLength = streamData[k].getByteBufferRaw().remaining(); } } } } private void handleRemovedStripeInfo(StripeData removed) { for (LlapDataBuffer[][] colData : removed.data) { handleRemovedColumnData(colData); } } private void handleRemovedColumnData(LlapDataBuffer[][] removed) { // TODO: could we tell the policy that we don't care about these and have them evicted? or we // could just deallocate them when unlocked, and free memory + handle that in eviction. // For now, just abandon the blocks - eventually, they'll get evicted. } private void mergeStripeInfos(StripeData to, StripeData from) { if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) { LlapIoImpl.CACHE_LOGGER.trace("Merging slices data: old " + to + " and new " + from); } to.knownTornStart = Math.min(to.knownTornStart, from.knownTornStart); if (from.encodings.length != to.encodings.length) { throw new RuntimeException("Different encodings " + from + "; " + to); } for (int colIx = 0; colIx < from.encodings.length; ++colIx) { if (to.encodings[colIx] == null) { to.encodings[colIx] = from.encodings[colIx]; } else if (from.encodings[colIx] != null && !to.encodings[colIx].equals(from.encodings[colIx])) { throw new RuntimeException("Different encodings at " + colIx + ": " + from + "; " + to); } LlapDataBuffer[][] fromColData = from.data[colIx]; if (fromColData != null) { if (to.data[colIx] != null) { // Note: we assume here that the data that was returned to the caller from cache will not // be passed back in via put. Right now it's safe since we don't do anything. But if we // evict proactively, we will have to compare objects all the way down. handleRemovedColumnData(to.data[colIx]); } to.data[colIx] = fromColData; } } } private void unlockBuffer(LlapDataBuffer buffer, boolean handleLastDecRef) { boolean isLastDecref = (buffer.decRef() == 0); if (handleLastDecRef && isLastDecref) { // This is kind of not pretty, but this is how we detect whether buffer was cached. // We would always set this for lookups at put time. if (buffer.declaredCachedLength != LlapDataBuffer.UNKNOWN_CACHED_LENGTH) { cachePolicy.notifyUnlock(buffer); } else { if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) { LlapIoImpl.CACHE_LOGGER.trace("Deallocating {} that was not cached", buffer); } allocator.deallocate(buffer); } } metrics.decrCacheNumLockedBuffers(); } private static final ByteBuffer fakeBuf = ByteBuffer.wrap(new byte[1]); public static LlapDataBuffer allocateFake() { LlapDataBuffer fake = new LlapDataBuffer(); fake.initialize(-1, fakeBuf, 0, 1); return fake; } public final void notifyEvicted(MemoryBuffer buffer) { newEvictions.incrementAndGet(); } private final class CleanupThread extends FileCacheCleanupThread<FileData> { public CleanupThread(ConcurrentHashMap<Object, FileCache<FileData>> fileMap, AtomicInteger newEvictions, long cleanupInterval) { super("Llap serde low level cache cleanup thread", fileMap, newEvictions, cleanupInterval); } @Override protected int getCacheSize(FileCache<FileData> fc) { return 1; // Each iteration cleans the file cache as a single unit (unlike the ORC cache). } @Override public int cleanUpOneFileCache(FileCache<FileData> fc, int leftToCheck, long endTime, Ref<Boolean> isPastEndTime) throws InterruptedException { FileData fd = fc.getCache(); fd.rwLock.writeLock().lock(); try { for (StripeData sd : fd.stripes) { for (int colIx = 0; colIx < sd.data.length; ++colIx) { LlapDataBuffer[][] colData = sd.data[colIx]; if (colData == null) continue; boolean hasAllData = true; for (int j = 0; (j < colData.length) && hasAllData; ++j) { LlapDataBuffer[] streamData = colData[j]; if (streamData == null) continue; for (int k = 0; k < streamData.length; ++k) { LlapDataBuffer buf = streamData[k]; hasAllData = hasAllData && lockBuffer(buf, false); if (!hasAllData) break; unlockBuffer(buf, true); } } if (!hasAllData) { handleRemovedColumnData(colData); sd.data[colIx] = null; } } } } finally { fd.rwLock.writeLock().unlock(); } return leftToCheck - 1; } } @Override public String debugDumpForOom() { StringBuilder sb = new StringBuilder("File cache state "); for (Map.Entry<Object, FileCache<FileData>> e : cache.entrySet()) { if (!e.getValue().incRef()) continue; try { sb.append("\n file " + e.getKey()); sb.append("\n ["); e.getValue().getCache().toString(sb); sb.append("]"); } finally { e.getValue().decRef(); } } return sb.toString(); } @Override public void debugDumpShort(StringBuilder sb) { sb.append("\nSerDe cache state "); int allLocked = 0, allUnlocked = 0, allEvicted = 0; for (Map.Entry<Object, FileCache<FileData>> e : cache.entrySet()) { if (!e.getValue().incRef()) continue; try { FileData fd = e.getValue().getCache(); int fileLocked = 0, fileUnlocked = 0, fileEvicted = 0; sb.append(fd.colCount).append(" columns, ").append(fd.stripes.size()).append(" stripes; "); for (StripeData stripe : fd.stripes) { if (stripe.data == null) continue; for (int i = 0; i < stripe.data.length; ++i) { LlapDataBuffer[][] colData = stripe.data[i]; if (colData == null) continue; for (int j = 0; j < colData.length; ++j) { LlapDataBuffer[] streamData = colData[j]; if (streamData == null) continue; for (int k = 0; k < streamData.length; ++k) { int newRc = streamData[k].incRef(); if (newRc < 0) { ++fileEvicted; continue; } try { if (newRc > 1) { // We hold one refcount. ++fileLocked; } else { ++fileUnlocked; } } finally { streamData[k].decRef(); } } } } } allLocked += fileLocked; allUnlocked += fileUnlocked; allEvicted += fileEvicted; sb.append("\n file " + e.getKey() + ": " + fileLocked + " locked, " + fileUnlocked + " unlocked, " + fileEvicted + " evicted"); } finally { e.getValue().decRef(); } } sb.append("\nSerDe cache summary: " + allLocked + " locked, " + allUnlocked + " unlocked, " + allEvicted + " evicted"); } }