/* * Copyright 2021 The Netty Project * * The Netty Project 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: * * https://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 io.netty.buffer.api.pool; import io.netty.buffer.api.AllocatorControl.UntetheredMemory; import io.netty.buffer.api.Buffer; import io.netty.buffer.api.BufferAllocator; import io.netty.buffer.api.MemoryManager; import io.netty.util.NettyRuntime; import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; import io.netty.util.internal.SystemPropertyUtil; import io.netty.util.internal.ThreadExecutorMap; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; import static java.util.Objects.requireNonNull; public class PooledBufferAllocator implements BufferAllocator, BufferAllocatorMetricProvider { private static final InternalLogger logger = InternalLoggerFactory.getInstance(PooledBufferAllocator.class); private static final int DEFAULT_NUM_HEAP_ARENA; private static final int DEFAULT_NUM_DIRECT_ARENA; private static final int DEFAULT_PAGE_SIZE; private static final int DEFAULT_MAX_ORDER; // 8192 << 9 = 4 MiB per chunk private static final int DEFAULT_SMALL_CACHE_SIZE; private static final int DEFAULT_NORMAL_CACHE_SIZE; static final int DEFAULT_MAX_CACHED_BUFFER_CAPACITY; private static final int DEFAULT_CACHE_TRIM_INTERVAL; private static final long DEFAULT_CACHE_TRIM_INTERVAL_MILLIS; private static final boolean DEFAULT_USE_CACHE_FOR_ALL_THREADS; private static final int DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT; static final int DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK; private static final int MIN_PAGE_SIZE = 4096; private static final int MAX_CHUNK_SIZE = (int) (((long) Integer.MAX_VALUE + 1) / 2); private final Runnable trimTask = this::trimCurrentThreadCache; static { int defaultAlignment = SystemPropertyUtil.getInt( "io.netty.allocator.directMemoryCacheAlignment", 0); int defaultPageSize = SystemPropertyUtil.getInt("io.netty.allocator.pageSize", 8192); Throwable pageSizeFallbackCause = null; try { validateAndCalculatePageShifts(defaultPageSize, defaultAlignment); } catch (Throwable t) { pageSizeFallbackCause = t; defaultPageSize = 8192; defaultAlignment = 0; } DEFAULT_PAGE_SIZE = defaultPageSize; DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT = defaultAlignment; int defaultMaxOrder = SystemPropertyUtil.getInt("io.netty.allocator.maxOrder", 9); Throwable maxOrderFallbackCause = null; try { validateAndCalculateChunkSize(DEFAULT_PAGE_SIZE, defaultMaxOrder); } catch (Throwable t) { maxOrderFallbackCause = t; defaultMaxOrder = 11; } DEFAULT_MAX_ORDER = defaultMaxOrder; // Determine reasonable default for nHeapArena and nDirectArena. // Assuming each arena has 3 chunks, the pool should not consume more than 50% of max memory. final Runtime runtime = Runtime.getRuntime(); /* * We use 2 * available processors by default to reduce contention as we use 2 * available processors for the * number of EventLoops in NIO and EPOLL as well. If we choose a smaller number we will run into hot spots as * allocation and de-allocation needs to be synchronized on the PoolArena. * * See https://github.com/netty/netty/issues/3888. */ final int defaultMinNumArena = NettyRuntime.availableProcessors() * 2; final int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER; DEFAULT_NUM_HEAP_ARENA = Math.max(0, SystemPropertyUtil.getInt( "io.netty.allocator.numArenas", (int) Math.min( defaultMinNumArena, runtime.maxMemory() / defaultChunkSize / 2 / 3))); DEFAULT_NUM_DIRECT_ARENA = Math.max(0, SystemPropertyUtil.getInt( "io.netty.allocator.numDirectArenas", (int) Math.min( defaultMinNumArena, PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3))); // cache sizes DEFAULT_SMALL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.smallCacheSize", 256); DEFAULT_NORMAL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.normalCacheSize", 64); // 32 kb is the default maximum capacity of the cached buffer. Similar to what is explained in // 'Scalable memory allocation using jemalloc' DEFAULT_MAX_CACHED_BUFFER_CAPACITY = SystemPropertyUtil.getInt( "io.netty.allocator.maxCachedBufferCapacity", 32 * 1024); // the number of threshold of allocations when cached entries will be freed up if not frequently used DEFAULT_CACHE_TRIM_INTERVAL = SystemPropertyUtil.getInt( "io.netty.allocator.cacheTrimInterval", 8192); DEFAULT_CACHE_TRIM_INTERVAL_MILLIS = SystemPropertyUtil.getLong( "io.netty.allocator.cacheTrimIntervalMillis", 0); DEFAULT_USE_CACHE_FOR_ALL_THREADS = SystemPropertyUtil.getBoolean( "io.netty.allocator.useCacheForAllThreads", false); // Use 1023 by default as we use an ArrayDeque as backing storage which will then allocate an internal array // of 1024 elements. Otherwise, we would allocate 2048 and only use 1024 which is wasteful. DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK = SystemPropertyUtil.getInt( "io.netty.allocator.maxCachedByteBuffersPerChunk", 1023); if (logger.isDebugEnabled()) { logger.debug("-Dio.netty.allocator.numArenas: {}", DEFAULT_NUM_HEAP_ARENA); logger.debug("-Dio.netty.allocator.numDirectArenas: {}", DEFAULT_NUM_DIRECT_ARENA); if (pageSizeFallbackCause == null) { logger.debug("-Dio.netty.allocator.pageSize: {}", DEFAULT_PAGE_SIZE); } else { logger.debug("-Dio.netty.allocator.pageSize: {}", DEFAULT_PAGE_SIZE, pageSizeFallbackCause); } if (maxOrderFallbackCause == null) { logger.debug("-Dio.netty.allocator.maxOrder: {}", DEFAULT_MAX_ORDER); } else { logger.debug("-Dio.netty.allocator.maxOrder: {}", DEFAULT_MAX_ORDER, maxOrderFallbackCause); } logger.debug("-Dio.netty.allocator.chunkSize: {}", DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER); logger.debug("-Dio.netty.allocator.smallCacheSize: {}", DEFAULT_SMALL_CACHE_SIZE); logger.debug("-Dio.netty.allocator.normalCacheSize: {}", DEFAULT_NORMAL_CACHE_SIZE); logger.debug("-Dio.netty.allocator.maxCachedBufferCapacity: {}", DEFAULT_MAX_CACHED_BUFFER_CAPACITY); logger.debug("-Dio.netty.allocator.cacheTrimInterval: {}", DEFAULT_CACHE_TRIM_INTERVAL); logger.debug("-Dio.netty.allocator.cacheTrimIntervalMillis: {}", DEFAULT_CACHE_TRIM_INTERVAL_MILLIS); logger.debug("-Dio.netty.allocator.useCacheForAllThreads: {}", DEFAULT_USE_CACHE_FOR_ALL_THREADS); logger.debug("-Dio.netty.allocator.maxCachedByteBuffersPerChunk: {}", DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK); } } private final MemoryManager manager; private final PoolArena[] arenas; private final int smallCacheSize; private final int normalCacheSize; private final List arenaMetrics; private final List arenaMetricsView; private final PoolThreadLocalCache threadCache; private final int chunkSize; private final PooledBufferAllocatorMetric metric; public PooledBufferAllocator(MemoryManager manager) { this(manager, manager.isNative()? DEFAULT_NUM_DIRECT_ARENA : DEFAULT_NUM_HEAP_ARENA, DEFAULT_PAGE_SIZE, DEFAULT_MAX_ORDER, DEFAULT_SMALL_CACHE_SIZE, DEFAULT_NORMAL_CACHE_SIZE, DEFAULT_USE_CACHE_FOR_ALL_THREADS, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT); } public PooledBufferAllocator(MemoryManager manager, int numArenas, int pageSize, int maxOrder) { this(manager, numArenas, pageSize, maxOrder, DEFAULT_SMALL_CACHE_SIZE, DEFAULT_NORMAL_CACHE_SIZE, DEFAULT_USE_CACHE_FOR_ALL_THREADS, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT); } public PooledBufferAllocator(MemoryManager manager, int numArenas, int pageSize, int maxOrder, int smallCacheSize, int normalCacheSize, boolean useCacheForAllThreads) { this(manager, numArenas, pageSize, maxOrder, smallCacheSize, normalCacheSize, useCacheForAllThreads, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT); } public PooledBufferAllocator(MemoryManager manager, int numArenas, int pageSize, int maxOrder, int smallCacheSize, int normalCacheSize, boolean useCacheForAllThreads, int directMemoryCacheAlignment) { this.manager = requireNonNull(manager, "MemoryManager"); threadCache = new PoolThreadLocalCache(useCacheForAllThreads); this.smallCacheSize = smallCacheSize; this.normalCacheSize = normalCacheSize; if (directMemoryCacheAlignment != 0) { if (!PlatformDependent.hasAlignDirectByteBuffer()) { throw new UnsupportedOperationException("Buffer alignment is not supported. " + "Either Unsafe or ByteBuffer.alignSlice() must be available."); } // Ensure page size is a whole multiple of the alignment, or bump it to the next whole multiple. pageSize = (int) PlatformDependent.align(pageSize, directMemoryCacheAlignment); } chunkSize = validateAndCalculateChunkSize(pageSize, maxOrder); checkPositiveOrZero(numArenas, "numArenas"); checkPositiveOrZero(directMemoryCacheAlignment, "directMemoryCacheAlignment"); if (directMemoryCacheAlignment > 0 && !isDirectMemoryCacheAlignmentSupported()) { throw new IllegalArgumentException("directMemoryCacheAlignment is not supported"); } if ((directMemoryCacheAlignment & -directMemoryCacheAlignment) != directMemoryCacheAlignment) { throw new IllegalArgumentException("directMemoryCacheAlignment: " + directMemoryCacheAlignment + " (expected: power of two)"); } int pageShifts = validateAndCalculatePageShifts(pageSize, directMemoryCacheAlignment); if (numArenas > 0) { arenas = newArenaArray(numArenas); List metrics = new ArrayList<>(arenas.length); for (int i = 0; i < arenas.length; i ++) { PoolArena arena = new PoolArena(this, manager, pageSize, pageShifts, chunkSize, directMemoryCacheAlignment); arenas[i] = arena; metrics.add(arena); } arenaMetrics = metrics; arenaMetricsView = Collections.unmodifiableList(metrics); } else { arenas = null; arenaMetrics = new ArrayList<>(1); arenaMetricsView = Collections.emptyList(); } metric = new PooledBufferAllocatorMetric(this); } private static PoolArena[] newArenaArray(int size) { return new PoolArena[size]; } private static int validateAndCalculatePageShifts(int pageSize, int alignment) { if (pageSize < MIN_PAGE_SIZE) { throw new IllegalArgumentException("pageSize: " + pageSize + " (expected: " + MIN_PAGE_SIZE + ')'); } if ((pageSize & pageSize - 1) != 0) { throw new IllegalArgumentException("pageSize: " + pageSize + " (expected: power of 2)"); } if (pageSize < alignment) { throw new IllegalArgumentException("Alignment cannot be greater than page size. " + "Alignment: " + alignment + ", page size: " + pageSize + '.'); } // Logarithm base 2. At this point we know that pageSize is a power of two. return Integer.SIZE - 1 - Integer.numberOfLeadingZeros(pageSize); } private static int validateAndCalculateChunkSize(int pageSize, int maxOrder) { if (maxOrder > 14) { throw new IllegalArgumentException("maxOrder: " + maxOrder + " (expected: 0-14)"); } // Ensure the resulting chunkSize does not overflow. int chunkSize = pageSize; for (int i = maxOrder; i > 0; i--) { if (chunkSize > MAX_CHUNK_SIZE / 2) { throw new IllegalArgumentException(String.format( "pageSize (%d) << maxOrder (%d) must not exceed %d", pageSize, maxOrder, MAX_CHUNK_SIZE)); } chunkSize <<= 1; } return chunkSize; } @Override public Buffer allocate(int size) { if (size < 1) { throw new IllegalArgumentException("Allocation size must be positive, but was " + size + '.'); } PooledAllocatorControl control = new PooledAllocatorControl(); control.parent = this; UntetheredMemory memory = allocate(control, size); Buffer buffer = manager.recoverMemory(control, memory.memory(), memory.drop()); return buffer.fill((byte) 0).order(ByteOrder.nativeOrder()); } UntetheredMemory allocate(PooledAllocatorControl control, int size) { PoolThreadCache cache = threadCache.get(); PoolArena arena = cache.arena; if (arena != null) { return arena.allocate(control, cache, size); } return allocateUnpooled(size); } private UntetheredMemory allocateUnpooled(int size) { return new UnpooledUnthetheredMemory(this, manager, size); } @Override public void close() { trimCurrentThreadCache(); threadCache.remove(); for (int i = 0, arenasLength = arenas.length; i < arenasLength; i++) { PoolArena arena = arenas[i]; if (arena != null) { arena.close(); arenas[i] = null; } } arenaMetrics.clear(); } /** * Default number of heap arenas - System Property: io.netty.allocator.numHeapArenas - default 2 * cores */ public static int defaultNumHeapArena() { return DEFAULT_NUM_HEAP_ARENA; } /** * Default number of direct arenas - System Property: io.netty.allocator.numDirectArenas - default 2 * cores */ public static int defaultNumDirectArena() { return DEFAULT_NUM_DIRECT_ARENA; } /** * Default buffer page size - System Property: io.netty.allocator.pageSize - default 8192 */ public static int defaultPageSize() { return DEFAULT_PAGE_SIZE; } /** * Default maximum order - System Property: io.netty.allocator.maxOrder - default 11 */ public static int defaultMaxOrder() { return DEFAULT_MAX_ORDER; } /** * Default thread caching behavior - System Property: io.netty.allocator.useCacheForAllThreads - default true */ public static boolean defaultUseCacheForAllThreads() { return DEFAULT_USE_CACHE_FOR_ALL_THREADS; } /** * Default prefer direct - System Property: io.netty.noPreferDirect - default false */ public static boolean defaultPreferDirect() { return PlatformDependent.directBufferPreferred(); } /** * Default small cache size - System Property: io.netty.allocator.smallCacheSize - default 256 */ public static int defaultSmallCacheSize() { return DEFAULT_SMALL_CACHE_SIZE; } /** * Default normal cache size - System Property: io.netty.allocator.normalCacheSize - default 64 */ public static int defaultNormalCacheSize() { return DEFAULT_NORMAL_CACHE_SIZE; } /** * Return {@code true} if direct memory cache alignment is supported, {@code false} otherwise. */ public static boolean isDirectMemoryCacheAlignmentSupported() { return PlatformDependent.hasUnsafe(); } public boolean isDirectBufferPooled() { return manager.isNative(); } public int numArenas() { return arenas.length; } final class PoolThreadLocalCache extends FastThreadLocal { private final boolean useCacheForAllThreads; PoolThreadLocalCache(boolean useCacheForAllThreads) { this.useCacheForAllThreads = useCacheForAllThreads; } @Override protected synchronized PoolThreadCache initialValue() { final PoolArena arena = leastUsedArena(arenas); final Thread current = Thread.currentThread(); if (useCacheForAllThreads || current instanceof FastThreadLocalThread) { final PoolThreadCache cache = new PoolThreadCache( arena, smallCacheSize, normalCacheSize, DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL); if (DEFAULT_CACHE_TRIM_INTERVAL_MILLIS > 0) { final EventExecutor executor = ThreadExecutorMap.currentExecutor(); if (executor != null) { executor.scheduleAtFixedRate(trimTask, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, TimeUnit.MILLISECONDS); } } return cache; } // No caching so just use 0 as sizes. return new PoolThreadCache(null, 0, 0, 0, 0); } @Override protected void onRemoval(PoolThreadCache threadCache) { threadCache.free(); } } static PoolArena leastUsedArena(PoolArena[] arenas) { if (arenas == null || arenas.length == 0) { return null; } PoolArena minArena = arenas[0]; for (int i = 1; i < arenas.length; i++) { PoolArena arena = arenas[i]; if (arena.numThreadCaches.get() < minArena.numThreadCaches.get()) { minArena = arena; } } return minArena; } @Override public PooledBufferAllocatorMetric metric() { return metric; } /** * Return a {@link List} of all heap {@link PoolArenaMetric}s that are provided by this pool. */ List arenaMetrics() { return arenaMetricsView; } /** * Return the number of thread local caches used by this {@link PooledBufferAllocator}. */ int numThreadLocalCaches() { if (arenas == null) { return 0; } int total = 0; for (PoolArena arena : arenas) { total += arena.numThreadCaches.get(); } return total; } /** * Return the size of the small cache. */ int smallCacheSize() { return smallCacheSize; } /** * Return the size of the normal cache. */ int normalCacheSize() { return normalCacheSize; } /** * Return the chunk size for an arena. */ final int chunkSize() { return chunkSize; } final long usedMemory() { return usedMemory(arenas); } private static long usedMemory(PoolArena[] arenas) { if (arenas == null) { return -1; } long used = 0; for (PoolArena arena : arenas) { used += arena.numActiveBytes(); if (used < 0) { return Long.MAX_VALUE; } } return used; } final PoolThreadCache threadCache() { PoolThreadCache cache = threadCache.get(); assert cache != null; return cache; } /** * Trim thread local cache for the current {@link Thread}, which will give back any cached memory that was not * allocated frequently since the last trim operation. * * Returns {@code true} if a cache for the current {@link Thread} exists and so was trimmed, false otherwise. */ public boolean trimCurrentThreadCache() { PoolThreadCache cache = threadCache.getIfExists(); if (cache != null) { cache.trim(); return true; } return false; } /** * Returns the status of the allocator (which contains all metrics) as string. Be aware this may be expensive * and so should not be called too frequently. */ public String dumpStats() { int heapArenasLen = arenas == null ? 0 : arenas.length; StringBuilder buf = new StringBuilder(512) .append(heapArenasLen) .append(" arena(s):") .append(StringUtil.NEWLINE); if (heapArenasLen > 0) { for (PoolArena a: arenas) { buf.append(a); } } return buf.toString(); } }