/** * Copyright (C) 2014-2016 LinkedIn Corp. (pinot-core@linkedin.com) * * Licensed 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 com.linkedin.pinot.common.utils; import com.google.common.base.Joiner; import com.linkedin.pinot.common.Utils; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.Buffer; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.apache.avro.util.WeakIdentityHashMap; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utilities to deal with memory mapped buffers, which may not be portable. * */ public class MmapUtils { private static final Logger LOGGER = LoggerFactory.getLogger(MmapUtils.class); private static final AtomicLong DIRECT_BYTE_BUFFER_USAGE = new AtomicLong(0L); private static final AtomicLong MMAP_BUFFER_USAGE = new AtomicLong(0L); private static final AtomicInteger MMAP_BUFFER_COUNT = new AtomicInteger(0); private static final AtomicInteger ALLOCATION_FAILURE_COUNT = new AtomicInteger(0); private static final long BYTES_IN_MEGABYTE = 1024L * 1024L; private static final Map<ByteBuffer, AllocationContext> BUFFER_TO_CONTEXT_MAP = Collections.synchronizedMap(new WeakIdentityHashMap<ByteBuffer, AllocationContext>()); public enum AllocationType { MMAP, DIRECT_BYTE_BUFFER } public static class AllocationContext { private final String fileName; private final String parentFileName; private final String parentPathName; private final String context; private final AllocationType allocationType; public static final Joiner PATH_JOINER = Joiner.on(File.separatorChar).skipNulls(); AllocationContext(String context, AllocationType allocationType) { this.context = context; this.allocationType = allocationType; this.fileName = null; this.parentFileName = null; this.parentPathName = null; } AllocationContext(File file, String details, AllocationType allocationType) { context = details; this.allocationType = allocationType; // We separate the path into three components to lower the overall on-heap memory usage when there are lots of // redundant paths. Paths that are memory mapped are usually <storage directory>/<segment name>/<column name>.ext // and this allows sharing the same String instance if the storage directory and segment name is duplicated many // times. if (file != null) { fileName = file.getName().intern(); File parent = file.getParentFile(); if (parent != null) { File parentParent = parent.getParentFile(); if (parentParent != null) { String absolutePath = parentParent.getAbsolutePath(); if (absolutePath.equals("/")) absolutePath = ""; parentFileName = parent.getName().intern(); parentPathName = absolutePath.intern(); } else { String absolutePath = parent.getAbsolutePath(); if (absolutePath.equals("/")) absolutePath = ""; parentFileName = absolutePath.intern(); parentPathName = null; } } else { parentFileName = null; parentPathName = null; } } else { fileName = null; parentFileName = null; parentPathName = null; } } public String getContext() { if (fileName != null) { return PATH_JOINER.join(parentPathName, parentFileName, fileName) + " (" + context + ")"; } else { return context; } } public AllocationType getAllocationType() { return allocationType; } @Override public String toString() { return getContext(); } } /** * Unloads a byte buffer from memory * * @param buffer The buffer to unload, can be null */ public static void unloadByteBuffer(Buffer buffer) { if (null == buffer) return; // Non direct byte buffers do not require any special handling, since they're on heap if (!buffer.isDirect()) return; // Remove usage from direct byte buffer usage final int bufferSize = buffer.capacity(); final AllocationContext bufferContext = BUFFER_TO_CONTEXT_MAP.get(buffer); // A DirectByteBuffer can be cleaned up by doing buffer.cleaner().clean(), but this is not a public API. This is // probably not portable between JVMs. try { Method cleanerMethod = buffer.getClass().getMethod("cleaner"); Method cleanMethod = Class.forName("sun.misc.Cleaner").getMethod("clean"); cleanerMethod.setAccessible(true); cleanMethod.setAccessible(true); // buffer.cleaner().clean() Object cleaner = cleanerMethod.invoke(buffer); if (cleaner != null) { cleanMethod.invoke(cleaner); if (bufferContext != null) { switch (bufferContext.allocationType) { case DIRECT_BYTE_BUFFER: DIRECT_BYTE_BUFFER_USAGE.addAndGet(-bufferSize); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Releasing byte buffer of size {} with context {}, allocation after operation {}", bufferSize, bufferContext, getTrackedAllocationStatus()); } break; case MMAP: MMAP_BUFFER_USAGE.addAndGet(-bufferSize); MMAP_BUFFER_COUNT.decrementAndGet(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Unmapping byte buffer of size {} with context {}, allocation after operation {}", bufferSize, bufferContext, getTrackedAllocationStatus()); } break; } BUFFER_TO_CONTEXT_MAP.remove(buffer); } else { LOGGER.warn("Attempted to release byte buffer of size {} with no context, no deallocation performed.", bufferSize); if (LOGGER.isDebugEnabled()) { List<String> matchingAllocationContexts = new ArrayList<>(); synchronized (BUFFER_TO_CONTEXT_MAP) { clearSynchronizedMapEntrySetCache(); for (Map.Entry<ByteBuffer, AllocationContext> byteBufferAllocationContextEntry : BUFFER_TO_CONTEXT_MAP.entrySet()) { if (byteBufferAllocationContextEntry.getKey().capacity() == bufferSize) { matchingAllocationContexts.add(byteBufferAllocationContextEntry.getValue().toString()); } } // Clear the entry set cache afterwards so that we don't hang on to stale entries clearSynchronizedMapEntrySetCache(); } LOGGER.debug("Contexts with a size of {}: {}", bufferSize, matchingAllocationContexts); LOGGER.debug("Called by: {}", Utils.getCallingMethodDetails()); } } } } catch (Exception e) { LOGGER.warn("Caught (ignored) exception while unloading byte buffer", e); } } /** * Allocates a direct byte buffer, tracking usage information. * * @param capacity The capacity to allocate. * @param file The file that this byte buffer refers to * @param details Further details about the allocation * @return A new allocated byte buffer with the requested capacity */ public static ByteBuffer allocateDirectByteBuffer(final int capacity, final File file, final String details) { final String context; final AllocationContext allocationContext; if (file != null) { context = file.getAbsolutePath() + " (" + details + ")"; allocationContext = new AllocationContext(file, details, AllocationType.DIRECT_BYTE_BUFFER); } else { context = "no file (" + details + ")"; allocationContext = new AllocationContext(details, AllocationType.DIRECT_BYTE_BUFFER); } DIRECT_BYTE_BUFFER_USAGE.addAndGet(capacity); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Allocating byte buffer of size {} with context {}, allocation after operation {}", capacity, context, getTrackedAllocationStatus()); } ByteBuffer byteBuffer = null; try { byteBuffer = ByteBuffer.allocateDirect(capacity); } catch (OutOfMemoryError e) { ALLOCATION_FAILURE_COUNT.incrementAndGet(); DIRECT_BYTE_BUFFER_USAGE.addAndGet(-capacity); LOGGER.error("Ran out of direct memory while trying to allocate {} bytes (context {})", capacity, context, e); LOGGER.error("Allocation status {}", getTrackedAllocationStatus()); Utils.rethrowException(e); } BUFFER_TO_CONTEXT_MAP.put(byteBuffer, allocationContext); return byteBuffer; } /** * Memory maps a file, tracking usage information. * * @param randomAccessFile The random access file to mmap * @param mode The mmap mode * @param position The byte position to mmap * @param size The number of bytes to mmap * @param file The file that is mmap'ed * @param details Additional details about the allocation */ public static MappedByteBuffer mmapFile(RandomAccessFile randomAccessFile, FileChannel.MapMode mode, long position, long size, File file, String details) throws IOException { final String context; final AllocationContext allocationContext; if (file != null) { context = file.getAbsolutePath() + " (" + details + ")"; allocationContext = new AllocationContext(file, details, AllocationType.MMAP); } else { context = "no file (" + details + ")"; allocationContext = new AllocationContext(details, AllocationType.MMAP); } MMAP_BUFFER_USAGE.addAndGet(size); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Memory mapping file, mmap size {} with context {}, allocation after operation {}", size, context, getTrackedAllocationStatus()); } MappedByteBuffer byteBuffer = null; try { byteBuffer = randomAccessFile.getChannel().map(mode, position, size); MMAP_BUFFER_COUNT.incrementAndGet(); } catch (Exception e) { ALLOCATION_FAILURE_COUNT.incrementAndGet(); LOGGER.error("Failed to mmap file (size {}, context {})", size, context, e); LOGGER.error("Allocation status {}", getTrackedAllocationStatus()); Utils.rethrowException(e); } BUFFER_TO_CONTEXT_MAP.put(byteBuffer, allocationContext); return byteBuffer; } private static String getTrackedAllocationStatus() { long directByteBufferUsage = DIRECT_BYTE_BUFFER_USAGE.get(); long mmapBufferUsage = MMAP_BUFFER_USAGE.get(); long mmapBufferCount = MMAP_BUFFER_COUNT.get(); return "direct " + (directByteBufferUsage / BYTES_IN_MEGABYTE) + " MB, mmap " + (mmapBufferUsage / BYTES_IN_MEGABYTE) + " MB (" + mmapBufferCount + " files), total " + ((directByteBufferUsage + mmapBufferUsage) / BYTES_IN_MEGABYTE) + " MB"; } /** * Returns the number of bytes of direct buffers allocated. */ public static long getDirectByteBufferUsage() { return DIRECT_BYTE_BUFFER_USAGE.get(); } /** * Returns the number of bytes of memory mapped files. */ public static long getMmapBufferUsage() { return MMAP_BUFFER_USAGE.get(); } /** * Returns the number of memory mapped files. */ public static long getMmapBufferCount() { return MMAP_BUFFER_COUNT.get(); } /** * Returns the number of allocation failures since the application started. */ public static int getAllocationFailureCount() { return ALLOCATION_FAILURE_COUNT.get(); } private static void clearSynchronizedMapEntrySetCache() { // For some bizarre reason, Collections.synchronizedMap's implementation (at least on JDK 1.8.0.25) caches the // entry set, and will thus return stale (and incorrect) values if queried multiple times, as well as cause those // entries to not be garbage-collectable. This clears its cache. try { Class<?> clazz = BUFFER_TO_CONTEXT_MAP.getClass(); Field field = clazz.getDeclaredField("entrySet"); field.setAccessible(true); field.set(BUFFER_TO_CONTEXT_MAP, null); } catch (Exception e) { // Well, that didn't work. } } /** * Obtains the list of all allocations and their associated sizes. Only meant for debugging purposes. */ public static List<Pair<AllocationContext, Integer>> getAllocationsAndSizes() { List<Pair<AllocationContext, Integer>> returnValue = new ArrayList<Pair<AllocationContext, Integer>>(); synchronized (BUFFER_TO_CONTEXT_MAP) { clearSynchronizedMapEntrySetCache(); Set<Map.Entry<ByteBuffer, AllocationContext>> entries = BUFFER_TO_CONTEXT_MAP.entrySet(); // Clear the entry set cache afterwards so that we don't hang on to stale entries clearSynchronizedMapEntrySetCache(); for (Map.Entry<ByteBuffer, AllocationContext> bufferAndContext : entries) { returnValue.add(new ImmutablePair<AllocationContext, Integer>(bufferAndContext.getValue(), bufferAndContext.getKey().capacity())); } } return returnValue; } }