/* * Galaxy * Copyright (c) 2012-2014, Parallel Universe Software Co. All rights reserved. * * This program and the accompanying materials are dual-licensed under * either the terms of the Eclipse Public License v1.0 as published by * the Eclipse Foundation * * or (per the licensee's choosing) * * under the terms of the GNU Lesser General Public License version 3.0 * as published by the Free Software Foundation. */ package co.paralleluniverse.galaxy.core; import co.paralleluniverse.common.MonitoringType; import co.paralleluniverse.common.spring.Component; import co.paralleluniverse.common.util.DegenerateInvocationHandler; import java.beans.ConstructorProperties; import java.lang.reflect.Field; import java.lang.reflect.Proxy; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Off-heap (direct ByteBuffer) allocator. * * @author pron */ class OffHeapLocalStorage extends Component implements CacheStorage { private static final Logger LOG = LoggerFactory.getLogger(OffHeapLocalStorage.class); private static final int MIN_POWER = 3; // min size = 1 << 3 = 8 private static final Field VIEWD_BUFFER_FIELD; private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0); static { try { VIEWD_BUFFER_FIELD = Class.forName("java.nio.DirectByteBuffer").getDeclaredField("att"); // "viewedBuffer" in JDK6 VIEWD_BUFFER_FIELD.setAccessible(true); } catch (Exception ex) { throw new AssertionError(ex); } } private final int pageSize; // in KBs private final int maxItemSize; // in bytes private final AtomicLong totalSize = new AtomicLong(); private final PageGroup[] pageGroups; private int maxPagesForConcurrency = Runtime.getRuntime().availableProcessors() * 2; private final OffHeapLocalStorageMonitor monitor; @ConstructorProperties({"name", "pageSize", "maxItemSize", "monitoringType"}) public OffHeapLocalStorage(String name, int pageSize, int maxItemSize, MonitoringType monitoringType) { super(name); this.pageSize = pageSize; this.maxItemSize = nextPowerOfTwo(maxItemSize); int numGroups = 0; int tmpSize = 1 << MIN_POWER; while (tmpSize <= this.maxItemSize) { numGroups++; tmpSize <<= 1; } int[] sizes = new int[numGroups]; this.pageGroups = new PageGroup[numGroups]; for (int i = 0; i < pageGroups.length; i++) { final int size = 1 << (MIN_POWER + i); pageGroups[i] = new PageGroup(i, size); sizes[i] = size; } this.monitor = createMonitor(monitoringType, name, sizes); } private OffHeapLocalStorageMonitor createMonitor(MonitoringType monitoringType, String name, int[] sizes) { if (monitoringType == null) return (OffHeapLocalStorageMonitor) Proxy.newProxyInstance(OffHeapLocalStorage.class.getClassLoader(), new Class<?>[]{OffHeapLocalStorageMonitor.class}, DegenerateInvocationHandler.INSTANCE); else switch (monitoringType) { case JMX: return new JMXOffHeapLocalStorageMonitor(name, this, sizes); case METRICS: return new MetricsOffHeapLocalStorageMonitor(name, this, sizes); } throw new IllegalArgumentException("Unknown MonitoringType " + monitoringType); } public void setMaxPagesForConcurrency(int maxPagesForConcurrency) { assertDuringInitialization(); this.maxPagesForConcurrency = maxPagesForConcurrency; } @Override public ByteBuffer allocateStorage(int size) { if (size == 0) return EMPTY_BUFFER; if (size > maxItemSize) throw new IllegalArgumentException("Size " + size + " is larger than maximum size: " + maxItemSize); final int bin = getSizeIndex(size); size = pageGroups[bin].cellSize; monitor.allocated(bin, size); totalSize.addAndGet(size); final ByteBuffer buffer = pageGroups[bin].allocate(); buffer.position(0); return buffer; } @Override public void deallocateStorage(long id, ByteBuffer buffer) { if (buffer == EMPTY_BUFFER) return; final Page page = getPage(buffer); monitor.deallocated(page.getGroup().groupIndex, buffer.limit()); totalSize.addAndGet(-buffer.limit()); page.deallocate(buffer); } @Override public long getTotalAllocatedSize() { return totalSize.get(); } private class PageGroup { public final int groupIndex; public final int cellSize; private final List<Page> pages = new CopyOnWriteArrayList<Page>(); private final Lock allocationLock = new ReentrantLock(); private volatile int numPages = 0; public PageGroup(int index, int cellSize) { this.groupIndex = index; this.cellSize = cellSize; } ByteBuffer allocate() { final int threadHash = Thread.currentThread().hashCode(); ByteBuffer buffer = null; if (numPages > 0) buffer = allocate(threadHash % numPages); if (buffer == null) { allocationLock.lock(); try { if (numPages > 0) buffer = allocate(threadHash % numPages); if (buffer == null) { if (LOG.isDebugEnabled()) LOG.debug("Allocating a direct-memory page of size {} bytes. (totalSize: {} bytes)", pageSize * 1024, totalSize.get()); Page newPage = new Page(this, pageSize, cellSize, MIN_POWER + groupIndex); buffer = newPage.allocate(true); pages.add(newPage); numPages++; } } finally { allocationLock.unlock(); } } assert buffer != null; return buffer; } private ByteBuffer allocate(int start) { final int _numPages = numPages; // read at the beginning 'cause this may change boolean canGrowForConcurrency = _numPages < maxPagesForConcurrency; for (int i = 0; i < _numPages; i++) { final Page page = pages.get((start + i) % _numPages); final ByteBuffer buffer = page.allocate(!canGrowForConcurrency); if (buffer != null) return buffer; } return null; } } private static class Page { private final PageGroup group; private final int cellSize; // in bytes private final ByteBuffer buffer; private int head; private int freeCells; private final Lock lock = new ReentrantLock(); public Page(PageGroup group, int bufferKbSize, int cellSize, int power) { this.group = group; buffer = ByteBuffer.allocateDirect(bufferKbSize * 1024); buffer.order(ByteOrder.nativeOrder()); setViewed(buffer, this); this.cellSize = cellSize; this.freeCells = (bufferKbSize * 1024) >> power; // initialize free-list int prev = -1; for (int i = freeCells - 1; i >= 0; i--) { final int ptr = i << power; buffer.putInt(ptr, prev); prev = ptr; } this.head = 0; } public PageGroup getGroup() { return group; } ByteBuffer allocate(boolean doIt) { if (doIt) lock.lock(); else if (!lock.tryLock()) return null; // contention - return null and allocate a new page to reduce contention final int ptr; final ByteBuffer slice; try { ptr = head; if (ptr == -1) { assert freeCells == 0; return null; } head = buffer.getInt(ptr); freeCells--; slice = slice(buffer, ptr, cellSize); // we must slice inside the lock b/c slice modifies buffer's fields } finally { lock.unlock(); } slice.putInt(0, 0); return slice; } void deallocate(ByteBuffer slice) { assert getPage(slice) == this; final int ptr = getOffset(slice); lock.lock(); try { buffer.putInt(ptr, head); head = ptr; freeCells++; } finally { lock.unlock(); } } } private int getSizeIndex(int size) { for (int i = 0; i < pageGroups.length; i++) { if (size <= pageGroups[i].cellSize) return i; } throw new RuntimeException("Value " + size + " is too large! Must be smaller than " + pageGroups[pageGroups.length - 1].cellSize); } // taken from http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 private static int nextPowerOfTwo(int v) { assert v >= 0; v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } private static ByteBuffer slice(ByteBuffer buffer, int start, int length) { buffer.limit(start + length); buffer.position(start); final ByteBuffer slice = buffer.slice(); buffer.clear(); return slice; } private static Page getPage(ByteBuffer buffer) { return (Page) getViewed((ByteBuffer) getViewed(buffer)); } private static Object getViewed(ByteBuffer buffer) { return ((sun.nio.ch.DirectBuffer) buffer).attachment(); // java<7: viewedBuffer(); } private static int getOffset(ByteBuffer slice) { final sun.nio.ch.DirectBuffer _slice = (sun.nio.ch.DirectBuffer) slice; final sun.nio.ch.DirectBuffer parent = (sun.nio.ch.DirectBuffer) _slice.attachment(); // java<7: viewedBuffer(); return (int) (_slice.address() - parent.address()); } private static void setViewed(ByteBuffer buffer, Object object) { try { VIEWD_BUFFER_FIELD.set(buffer, object); } catch (Exception ex) { throw new AssertionError(ex); } } }