All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.activej.bytebuf.ByteBufPool Maven / Gradle / Ivy

Go to download

Fast and memory-efficient byte buffer, an optimized version of Java's ByteBuffer class. Useful for fast low-level I/O operations like working with files or transferring data over the internet.

There is a newer version: 6.0-beta2
Show newest version
/*
 * Copyright (C) 2020 ActiveJ LLC.
 *
 * 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 io.activej.bytebuf;

import io.activej.bytebuf.ByteBuf.ByteBufSlice;
import io.activej.common.ApplicationSettings;
import io.activej.common.MemSize;
import io.activej.common.concurrent.ObjectPool;

import java.lang.StackWalker.StackFrame;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import static io.activej.common.Checks.checkArgument;
import static java.lang.Integer.numberOfLeadingZeros;
import static java.lang.Math.max;
import static java.lang.System.currentTimeMillis;
import static java.util.Arrays.stream;
import static java.util.Comparator.comparingLong;
import static java.util.stream.Collectors.toList;

/**
 * Represents a pool of ByteBufs with 33 slabs. Each of these slabs
 * is a {@code ByteBufConcurrentStack} which stores ByteBufs of a
 * particular capacity which is a power of two.
 * 

* When you need a new ByteBuf, it is either created (if a ByteBuf of * such capacity hasn't been used and recycled yet) or popped from the * appropriate slabs' stack. */ @SuppressWarnings({"WeakerAccess", "unused"}) public final class ByteBufPool { private static final int NUMBER_OF_SLABS = 33; /** * Defines the minimal size of ByteBufs in this ByteBufPool. * A constant value, by default set at 0. */ private static final int MIN_SIZE = ApplicationSettings.getMemSize(ByteBufPool.class, "minSize", MemSize.ZERO).toInt(); /** * Defines the maximum size of ByteBufs in this ByteBufPool. * A constant value, by default set at 0. */ private static final int MAX_SIZE = ApplicationSettings.getMemSize(ByteBufPool.class, "maxSize", MemSize.ZERO).toInt(); private static final boolean MIN_MAX_CHECKS = MIN_SIZE != 0 || MAX_SIZE != 0; /** * Allows getting trace stack about {@code ByteBufs} of this {@code ByteBufPool} * while debugging if set at value {@code true} (note that this is resource intensive). * By default, set at value {@code false}. If changed, significantly * influences the performance and workflow of {@link #allocate(int)} operation. */ static final boolean REGISTRY = ApplicationSettings.getBoolean(ByteBufPool.class, "registry", false); /** * Allows getting statistics about this ByteBufPool while debugging * if set at value {@code true} (note that this is resource intensive). * By default, set at value {@code false}. If changed, significantly * influences the performance and workflow of {@link #allocate(int)} operation. */ static final boolean STATS = ApplicationSettings.getBoolean(ByteBufPool.class, "stats", false); /** * Allows clearing byte bufs when being returned to the pool * if set at value {@code true} (note that this is resource intensive). * By default, set at value {@code false}. If changed, * influences the performance of {@link #recycle(ByteBuf)} operation. * Should only be used for testing to catch bugs with premature {@link ByteBuf} recycling, * should not be used in production code */ static final boolean CLEAR_ON_RECYCLE = ApplicationSettings.getBoolean(ByteBufPool.class, "clearOnRecycle", false); static final boolean USE_WATCHDOG = ApplicationSettings.getBoolean(ByteBufPool.class, "useWatchdog", false); static final Duration WATCHDOG_INTERVAL = ApplicationSettings.getDuration(ByteBufPool.class, "watchdogInterval", Duration.ofSeconds(2)); static final Duration WATCHDOG_SMOOTHING_WINDOW = ApplicationSettings.getDuration(ByteBufPool.class, "watchdogSmoothingWindow", Duration.ofSeconds(10)); static final double WATCHDOG_ERROR_MARGIN = ApplicationSettings.getDouble(ByteBufPool.class, "watchdogErrorMargin", 4.0); private static final double SMOOTHING_COEFF = 1.0 - Math.pow(0.5, (double) WATCHDOG_INTERVAL.toMillis() / WATCHDOG_SMOOTHING_WINDOW.toMillis()); /** * {@code ByteBufConcurrentStack} allows working with slabs and their ByteBufs. * Basically, it is a singly linked list with basic stack operations: * {@code push, pop, peek, clear, isEmpty, size}. *

* The implementation of {@code ByteBufConcurrentStack} is highly efficient * due to utilizing {@link java.util.concurrent.atomic.AtomicReference}. * Moreover, such approach allows working with slabs concurrently safely. */ static final ObjectPool[] slabs; static final SlabStats[] slabStats; static final AtomicInteger[] created; static final AtomicInteger[] reused; private static final ByteBufPoolStats stats = new ByteBufPoolStats(); /** * Stores information about ByteBufs for stats. *

* It is a helper class which contains size, * timestamp and stackTrace which represent * information about the ByteBufs. */ public static final class Entry { final int size; final long timestamp; final Thread thread; final List stackTrace; Entry(int size, long timestamp, Thread thread, List stackTrace) { this.size = size; this.timestamp = timestamp; this.thread = thread; this.stackTrace = stackTrace; } public int getSize() { return size; } public long getTimestamp() { return timestamp; } public String getAge() { return Duration.ofMillis(System.currentTimeMillis() - timestamp).toString(); } public String getThread() { return thread.toString(); } public List getStackTrace() { return stackTrace.stream().map(Object::toString).toList(); } @Override public String toString() { return "{" + "size=" + size + ", timestamp=" + timestamp + ", thread=" + thread + ", stackTrace=" + stackTrace + '}'; } } private static final Map allocateRegistry = Collections.synchronizedMap(new WeakHashMap<>()); private static final Map recycleRegistry = Collections.synchronizedMap(new WeakHashMap<>()); static { slabs = new ObjectPool[NUMBER_OF_SLABS]; slabStats = new SlabStats[NUMBER_OF_SLABS]; created = new AtomicInteger[NUMBER_OF_SLABS]; reused = new AtomicInteger[NUMBER_OF_SLABS]; for (int i = 0; i < NUMBER_OF_SLABS; i++) { slabs[i] = new ObjectPool<>(); created[i] = new AtomicInteger(); reused[i] = new AtomicInteger(); } if (USE_WATCHDOG) { for (int i = 0; i < NUMBER_OF_SLABS; i++) { slabStats[i] = new SlabStats(); } Thread watchdogThread = new Thread(() -> { while (true) { updateStats(); evict(); try { //noinspection BusyWait Thread.sleep(WATCHDOG_INTERVAL.toMillis()); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); break; } } }, "bytebufpool-watchdog-thread"); watchdogThread.setDaemon(true); watchdogThread.setPriority(Thread.MIN_PRIORITY); watchdogThread.start(); } } private ByteBufPool() {} /** * Allocates byte buffer from the pool with size of * ceil(log2(size))2 * (rounds up to the nearest power of 2) bytes. *

* Note that resource intensive {@link #buildRegistryEntry(ByteBuf)} will be executed * only if {{@code #REGISTRY}} is set {@code true}. Also, such parameters as * {@code STATS}, {@code MIN_MAX_CHECKS}, {@code MIN_SIZE}, {@code MAX_SIZE} * significantly influence the workflow of the {@code allocate} operation. * * @param size returned byte buffer size is guaranteed to be bigger or equal to requested size * @return byte buffer from this pool */ public static ByteBuf allocate(int size) { assert size >= 0 : "Allocating ByteBuf with negative size"; if (MIN_MAX_CHECKS) { if ((MIN_SIZE != 0 && size < MIN_SIZE) || (MAX_SIZE != 0 && size >= MAX_SIZE)) { // not willing to register in pool return ByteBuf.wrapForWriting(new byte[size]); } } int index = 32 - numberOfLeadingZeros(size - 1); // index==32 for size==0 ObjectPool slab = slabs[index]; ByteBuf buf = slab.poll(); if (buf != null) { if (USE_WATCHDOG) { int slabSize = slab.size(); //noinspection NonAtomicOperationOnVolatileField slabStats[index].min = Math.min(slabStats[index].min, slabSize); } if (ByteBuf.CHECK_RECYCLE && buf.refs != -1) throw onByteBufRecycled(buf); buf.tail = 0; buf.head = 0; buf.refs = 1; if (STATS) recordReuse(index); } else { if (USE_WATCHDOG) slabStats[index].min = 0; buf = ByteBuf.wrapForWriting(new byte[index == 32 ? 0 : 1 << index]); buf.refs = 1; if (STATS) recordNew(index); } if (REGISTRY) allocateRegistry.put(buf, buildRegistryEntry(buf)); return buf; } private static void recordNew(int index) { created[index].incrementAndGet(); } private static void recordReuse(int index) { reused[index].incrementAndGet(); } private static Entry buildRegistryEntry(ByteBuf buf) { return new Entry(buf.array.length, currentTimeMillis(), Thread.currentThread(), StackWalker.getInstance().walk(frames -> frames.skip(3).toList())); } static AssertionError onByteBufRecycled(ByteBuf buf) { int slab = 32 - numberOfLeadingZeros(buf.array.length - 1); ObjectPool pool = slabs[slab]; pool.clear(); if (USE_WATCHDOG) slabStats[slab].clear(); return new AssertionError( "Attempt to use recycled ByteBuf" + (REGISTRY ? ByteBufPool.getByteBufTrace(buf) : "")); } static String getByteBufTrace(ByteBuf buf) { Entry allocated = allocateRegistry.get(buf); Entry recycled = recycleRegistry.get(buf); if (allocated == null && recycled == null) return ""; return "\nAllocated: " + allocated + "\nRecycled: " + recycled; } /** * Allocates byte buffer in the same way as {@link #allocate(int)} does, but * sets its positions so that write-remaining is equal to requested size. *

* For example, if you need a {@code ByteBuf} of size 21, * a {@code ByteBuf} of size 32 is allocated. (|______|)
* But its read/write positions are set to 11 so that only last 21 are writable (|__####|) *

* * @param size requested size * @return byte buffer from this pool with appropriate positions set */ public static ByteBuf allocateExact(int size) { ByteBuf buf = allocate(size); int d = buf.writeRemaining() - size; buf.tail(d); buf.head(d); return buf; } public static ByteBuf allocate(MemSize size) { return allocate(size.toInt()); } public static ByteBuf allocateExact(MemSize size) { return allocateExact(size.toInt()); } /** * Returns provided ByteBuf to the ByteBufPool to the appropriate slab. * * @param buf the ByteBuf to be recycled */ static void recycle(ByteBuf buf) { int slab = 32 - numberOfLeadingZeros(buf.array.length - 1); ObjectPool pool = slabs[slab]; if (CLEAR_ON_RECYCLE) Arrays.fill(buf.array(), (byte) 0); if (REGISTRY) { recycleRegistry.put(buf, buildRegistryEntry(buf)); allocateRegistry.remove(buf); } pool.offer(buf); } public static ByteBuf ensureWriteRemaining(ByteBuf buf, int newWriteRemaining) { return ensureWriteRemaining(buf, 0, newWriteRemaining); } /** * Checks if current ByteBuf can accommodate the needed * amount of writable bytes. *

* Returns this ByteBuf, if it contains enough writable bytes. *

* Otherwise, creates a new ByteBuf which contains data from the * original ByteBuf and fits the parameters. Then recycles the * original ByteBuf. * * @param buf the ByteBuf to check * @param minSize the minimal size of the ByteBuf * @param newWriteRemaining amount of needed writable bytes * @return a ByteBuf which fits the parameters */ public static ByteBuf ensureWriteRemaining(ByteBuf buf, int minSize, int newWriteRemaining) { if (newWriteRemaining == 0) return buf; if (buf.writeRemaining() < newWriteRemaining || buf instanceof ByteBufSlice) { ByteBuf newBuf = allocate(max(minSize, newWriteRemaining + buf.readRemaining())); newBuf.put(buf); buf.recycle(); return newBuf; } return buf; } /** * Appends one ByteBuf to another ByteBuf. If target ByteBuf * can't accommodate the ByteBuf to be appended, a new ByteBuf * is created which contains both target and source ByteBufs data. * The source ByteBuf is recycled after append. *

* If target ByteBuf has no readable bytes, it is being recycled * and the source ByteBuf is returned. *

* Both ByteBufs must be not recycled before the operation. * * @param to the target ByteBuf to which another ByteBuf will be appended * @param from the source ByteBuf to be appended * @return ByteBuf which contains the result of the appending */ public static ByteBuf append(ByteBuf to, ByteBuf from) { checkArgument(!to.isRecycled() && !from.isRecycled()); if (to.readRemaining() == 0) { to.recycle(); return from; } to = ensureWriteRemaining(to, from.readRemaining()); to.put(from); from.recycle(); return to; } /** * Appends byte array to ByteBuf. If ByteBuf can't accommodate the * byte array, a new ByteBuf is created which contains all data from * the original ByteBuf and has enough capacity to accommodate the * byte array. *

* ByteBuf must be not recycled before the operation. * * @param to the target ByteBuf to which byte array will be appended * @param from the source byte array to be appended * @param offset the value of offset for the byte array * @param length amount of the bytes to be appended to the ByteBuf * The sum of the length and offset parameters can't * be greater than the whole length of the byte array * @return ByteBuf which contains the result of the appending */ public static ByteBuf append(ByteBuf to, byte[] from, int offset, int length) { checkArgument(!to.isRecycled()); to = ensureWriteRemaining(to, length); to.put(from, offset, length); return to; } public static ByteBuf append(ByteBuf to, byte[] from) { return append(to, from, 0, from.length); } /** * Clears all the slabs and stats. */ public static void clear() { for (int i = 0; i < ByteBufPool.NUMBER_OF_SLABS; i++) { slabs[i].clear(); created[i].set(0); reused[i].set(0); if (USE_WATCHDOG) slabStats[i].clear(); } allocateRegistry.clear(); recycleRegistry.clear(); } public static ByteBufPoolStats getStats() { return stats; } public interface ByteBufPoolStatsMXBean { int getCreatedItems(); int getReusedItems(); int getPoolItems(); long getPoolSize(); long getPoolSizeKB(); long getTotalSlabMins(); long getTotalEvicted(); List getPoolSlabs(); List queryUnrecycledBufs(int limit); void clear(); void clearRegistry(); } /** * Manages stats for this {@link ByteBufPool}. You can get the * amount of created and reused ByteBufs and amount of ByteBufs * stored in each of the slabs. *

* Also, you can get a String which contains information about * amount of created and stored in pool ByteBufs. *

* For memory control, you can get the size of your ByteBufPool in * Byte or KB as well as get information about the slabs themselves * (size, amount of ByteBufs created, reused, stored in pool, and * total size in KB) and unrecycled ByteBufs. *

* Finally, it allows to clear this ByteBufPool and its registry. */ public static final class ByteBufPoolStats implements ByteBufPoolStatsMXBean { @Override public int getCreatedItems() { return stream(created).mapToInt(AtomicInteger::get).sum(); } @Override public int getReusedItems() { return stream(reused).mapToInt(AtomicInteger::get).sum(); } @Override public int getPoolItems() { return stream(slabs).mapToInt(ObjectPool::size).sum(); } @SuppressWarnings("StringConcatenationInsideStringBufferAppend") public String getPoolItemsString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < ByteBufPool.NUMBER_OF_SLABS; ++i) { int createdItems = created[i].get(); int poolItems = slabs[i].size(); if (createdItems != poolItems) { sb.append(String.format("Slab %d (%d) ", i, (1 << i))) .append(" created: " + createdItems) .append(" pool: " + poolItems + "\n"); } } return sb.toString(); } @Override public long getPoolSize() { long result = 0; for (int i = 0; i < slabs.length - 1; i++) { long slabSize = 1L << i; result += slabSize * slabs[i].size(); } return result; } @Override public long getPoolSizeKB() { return getPoolSize() / 1024; } @Override public long getTotalSlabMins() { if (!USE_WATCHDOG) return -1; long totalSlabMins = 0; for (SlabStats slabStat : slabStats) { totalSlabMins += slabStat.min; } return totalSlabMins; } @Override public long getTotalEvicted() { if (!USE_WATCHDOG) return -1; long totalEvicted = 0; for (SlabStats slabStat : slabStats) { totalEvicted += slabStat.evictedTotal; } return totalEvicted; } public Map getUnrecycledBufs() { return new HashMap<>(allocateRegistry); } @Override public List queryUnrecycledBufs(int limit) { if (limit < 1) throw new IllegalArgumentException("Limit must be >= 1"); Map danglingBufs = getUnrecycledBufs(); return danglingBufs.values().stream().sorted(comparingLong(Entry::getTimestamp)).limit(limit).collect(toList()); } @Override public List getPoolSlabs() { List result = new ArrayList<>(slabs.length + 1); String header = "SlotSize,Created,Reused,InPool,Total(Kb)"; if (USE_WATCHDOG) header += ",Min,EstMin,Error,Evicted"; result.add(header); for (int i = 0; i < slabs.length; i++) { int idx = (i + 32) % slabs.length; long slabSize = idx == 32 ? 0 : 1L << idx; ObjectPool slab = slabs[idx]; int count = slab.size(); String slabInfo = slabSize + "," + (STATS ? created[idx] : "-") + "," + (STATS ? reused[idx] : "-") + "," + count + "," + slabSize * count / 1024; if (USE_WATCHDOG) { SlabStats slabStat = slabStats[idx]; slabInfo += "," + slabStat.min + "," + String.format("%.1f", slabStat.estimatedMin) + "," + String.format("%.1f", slabStat.estimatedError) + "," + slabStat.evictedTotal; } result.add(slabInfo); } return result; } @Override public void clear() { ByteBufPool.clear(); } @Override public void clearRegistry() { allocateRegistry.clear(); recycleRegistry.clear(); } } // region watchdog public static final class SlabStats { volatile int min = 0; double estimatedMin; int evictedTotal; int evictedLast; int evictedMax; double estimatedError; void clear() { estimatedMin = estimatedError = evictedTotal = evictedLast = evictedMax = 0; min = 0; } @Override public String toString() { return "SlabStats{" + "estimatedMin=" + estimatedMin + ", estimatedError=" + estimatedError + ", evictedTotal=" + evictedTotal + ", evictedLast=" + evictedLast + ", evictedMax=" + evictedMax + '}'; } } private static void updateStats() { for (int i = 0; i < slabs.length; i++) { SlabStats stats = slabStats[i]; ObjectPool slab = slabs[i]; int min = stats.min; stats.min = slab.size(); double error = Math.abs(stats.estimatedMin - min); stats.estimatedError += (error - stats.estimatedError) * SMOOTHING_COEFF; if (min < stats.estimatedMin) { stats.estimatedMin = min; } else { stats.estimatedMin += (min - stats.estimatedMin) * SMOOTHING_COEFF; } } } private static void evict() { for (int i = 0; i < slabs.length; i++) { ObjectPool slab = slabs[i]; SlabStats stats = slabStats[i]; int evictCount = (int) Math.round(stats.estimatedMin - stats.estimatedError * WATCHDOG_ERROR_MARGIN); stats.evictedLast = 0; for (int j = 0; j < evictCount; j++) { ByteBuf buf = slab.poll(); if (buf == null) { if (USE_WATCHDOG) stats.min = 0; break; } if (USE_WATCHDOG) { int slabSize = slab.size(); //noinspection NonAtomicOperationOnVolatileField stats.min = Math.min(stats.min, slabSize); } stats.estimatedMin--; stats.evictedLast++; if (REGISTRY) recycleRegistry.remove(buf); } stats.evictedTotal += stats.evictedLast; stats.evictedMax = Math.max(stats.evictedLast, stats.evictedMax); } } //endregion }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy