/* For Copyright and License see LICENSE.txt and COPYING.txt in the root directory */ /** * */ package com.nerdscentral.audio.core; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.io.Serializable; import java.lang.management.ManagementFactory; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.ByteOrder; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileChannel.MapMode; import java.util.Stack; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.LongFunction; import com.nerdscentral.audio.Messages; import com.nerdscentral.data.OffHeapArray; import com.nerdscentral.data.UnsafeProvider; import com.nerdscentral.sython.SFPL_RuntimeException; import sun.misc.Unsafe; /** * @author a1t * */ public class SFData extends SFSignal implements Serializable { // JIT time alias in effect, maybe remove in the future private static final Unsafe unsafe = UnsafeProvider.unsafe; private static class MemoryZoneStack extends ThreadLocal<Stack<SFMemoryZone>> { @Override protected Stack<SFMemoryZone> initialValue() { return new Stack<>(); } } static class ByteBufferWrapper { private long address = 0; private final FileChannel underLyingFile; private final long fileOffset; // buffer is used to hold a reference to the buffer // but its data is read via unsafe private MappedByteBuffer buffer; public long getAddress() { if (address == 0) remap(); return address; } static Method getAddressMethod; void remap() { try { buffer = underLyingFile.map(MapMode.READ_WRITE, fileOffset, CHUNK_LEN << 3l); if (getAddressMethod == null) { getAddressMethod = buffer.getClass().getMethod("address"); //$NON-NLS-1$ getAddressMethod.setAccessible(true); } buffer.order(ByteOrder.nativeOrder()); address = (long) getAddressMethod.invoke(buffer); } catch (IOException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { throw new RuntimeException(e); } } public ByteBufferWrapper(FileChannel file, long offsetInFile) throws SecurityException, IllegalArgumentException { fileOffset = offsetInFile; underLyingFile = file; // Grab the chunk of file now so it does not get used twice in a race. remap(); } public void force() { buffer.force(); } } public static class NotCollectedException extends Exception { private static final long serialVersionUID = 1L; } // $NON-NLS-1$ private static final String SONIC_FIELD_TRUE = "true"; //$NON-NLS-1$ static final long CHUNK_SHIFT = 16; static final long CHUNK_LEN = (long) Math.pow(2, CHUNK_SHIFT); static final long CHUNK_MASK = CHUNK_LEN - 1; // How frequently to print of memory state as a modulus of the number of SFData objects created. private static final long MEM_REPORT_MODULUS = 10000; private static final long serialVersionUID = 1L; private final int length; ByteBufferWrapper[] chunks; private final long chunkIndex; // shadows chunkIndex for memory management as chunkIndex must // be final to enable optimisation private long allocked; private static final AtomicLong totalCount = new AtomicLong(); private static final AtomicLong freeCount = new AtomicLong(); private static final AtomicLong reportTicker = new AtomicLong(); private static ConcurrentLinkedDeque<ByteBufferWrapper> freeChunks = new ConcurrentLinkedDeque<>(); private static ConcurrentLinkedDeque<ByteBufferWrapper> allChunks = new ConcurrentLinkedDeque<>(); @SuppressWarnings("synthetic-access") private static final MemoryZoneStack memoryZoneStack = new MemoryZoneStack(); private final AtomicBoolean kept = new AtomicBoolean(false); private final AtomicBoolean dead = new AtomicBoolean(false); private final AtomicBoolean pinned = new AtomicBoolean(false); // TODO: The file channels are thread local but the chunks are not. Thus over time chunks will disperse // across threads and the locality will be broken. If we can make some mechanism to allow this were needed // but migrate chunks back to being thread local where possible to improve disk data locallity this would // be good. private static ConcurrentHashMap<File, FileChannel> fileMap = new ConcurrentHashMap<>(); private static AtomicBoolean isFlushing = new AtomicBoolean(false); protected static FileChannel getRotatingChannel() { String pid = ManagementFactory.getRuntimeMXBean().getName(); try { File dir = SFConstants.getRotatingTempDir(); FileChannel channel = fileMap.get(dir); if (channel != null) return channel; // OK, we do not have an existing swap file in this dir to make a new one. File coreFile = File.createTempFile("SonicFieldSwap" + pid, ".mem", dir); //$NON-NLS-1$//$NON-NLS-2$ coreFile.deleteOnExit(); // Now create the actual file @SuppressWarnings("resource") RandomAccessFile rfile = new RandomAccessFile(coreFile, "rw"); //$NON-NLS-1$ channel = rfile.getChannel(); fileMap.put(dir, channel); return channel; } catch (IOException e) { throw new RuntimeException(e); } } private static void maybeReportStats() { if (reportTicker.incrementAndGet() % MEM_REPORT_MODULUS == 0) { final LongFunction<Long> toGigs = chunks -> chunks * CHUNK_LEN * 8 / SFConstants.ONE_GIG; System.out.println(Messages.getString("SFData.20") + toGigs.apply(totalCount.get()) //$NON-NLS-1$ + Messages.getString("SFData.21") + toGigs.apply((freeCount.get()))); //$NON-NLS-1$ } } private void makeMap(long size) throws SecurityException, IllegalArgumentException, IOException { long countDown = size; int chunkCount = 0; while (countDown > 0) { ++chunkCount; countDown -= CHUNK_LEN; } countDown = size; chunks = new ByteBufferWrapper[chunkCount]; chunkCount = 0; while (countDown > 0) { ByteBufferWrapper chunk = freeChunks.poll(); if (chunk == null) break; chunks[chunkCount] = chunk; freeCount.decrementAndGet(); ++chunkCount; countDown -= CHUNK_LEN; } if (countDown > 0) { // If the GC has found something, make sure the finalizers are run here so we get // the chunks freed. System.runFinalization(); mapMoreData(countDown, chunkCount); } getChunkAddresses(); } private void getChunkAddresses() { // now we have the chunks we get the address of the underlying memory // of each and place that in the off heap lookup so we no longer reference // them via objects but purely as raw memory long offSet = 0; for (ByteBufferWrapper chunk : chunks) { unsafe.putAddress(chunkIndex + offSet, chunk.getAddress()); offSet += 8; } } private void mapMoreData(long countDown, int chunkCount) throws IOException { synchronized (fileMap) { // System.out.println("Adding " + (countDown / CHUNK_LEN) + " chunks"); while (countDown > 0) { @SuppressWarnings("resource") FileChannel channel = getRotatingChannel(); long from = channel.size(); ByteBufferWrapper chunk = chunks[chunkCount] = new ByteBufferWrapper(channel, from); allChunks.add(chunk); ++chunkCount; totalCount.addAndGet(1); countDown -= CHUNK_LEN; } } } /** * Flushes a snap shot of all current chunks to disk thus liberating the RAM with which they are associated to be used by * new chunks. This method is useful between big sections of processing where the data generated will just be stored to be * used in another processing section later. For example generate the left channel, flush(), generate the right, flush() * then mix. */ public static void flushAll() { // Snapshot the current container of all chunks so that // the loop does not get caught flushing as loads of chunks are being allocated // and so running for an unbounded amount of time. if (isFlushing.get()) return; isFlushing.set(true); int nThreads = SFConstants.TEMP_DATA_DIRS.length; AtomicBoolean failed = new AtomicBoolean(false); // Increment and get needs -1 to index from 0. AtomicInteger taken = new AtomicInteger(-1); Thread[] threads = new Thread[nThreads]; // Flush all in as mean threads as there are temp files so that we can optimise // disk bandwidth. for (int n = 0; n < nThreads; ++n) { Thread th = new Thread() { @Override @SuppressWarnings({ "synthetic-access" }) public void run() { try { ByteBufferWrapper[] a = new ByteBufferWrapper[0]; a = allChunks.toArray(a); System.out.println(Messages.getString("SFData.5") + a.length); //$NON-NLS-1$ int c = 0; int mod = taken.incrementAndGet(); for (ByteBufferWrapper chunk : a) { if (c % nThreads == mod) { chunk.force(); } ++c; if (c % 1000 == mod) System.out.println(Messages.getString("SFData.7") + c + Messages.getString("SFData.8")); //$NON-NLS-1$ //$NON-NLS-2$ } System.out.println(Messages.getString("SFData.12") + a.length); //$NON-NLS-1$ } catch (Throwable t) { t.printStackTrace(); failed.set(true); } finally { isFlushing.set(false); } } }; threads[n] = th; th.setDaemon(true); th.start(); if (failed.get()) { throw new RuntimeException("Flush all failed, see stderr."); } } for (Thread thts : threads) { boolean joined = false; while (!joined) { try { thts.join(); joined = true; } catch (InterruptedException e) { System.err.println("Non critical thread interup exception during flush - ignoring."); e.printStackTrace(); } } } } @Override public void clear() { checkLive(); for (long i = 0; i < chunks.length; ++i) { long address = unsafe.getAddress(chunkIndex + (i << 3l)); unsafe.setMemory(address, CHUNK_LEN << 3l, (byte) 0); } } @Override public void release() { checkLive(); if (pinned.get()) throw new RuntimeException("Trying to release pinned memory."); //$NON-NLS-1$ dead.set(true); synchronized (fileMap) { // If we are already released, do nothing; if (chunks == null) return; for (ByteBufferWrapper chunk : chunks) { freeChunks.add(chunk); freeCount.incrementAndGet(); } chunks = null; freeUnsafeMemory(); } } private void checkLive() { if (dead.get()) { throw new RuntimeException(Messages.getString("SFData.22")); //$NON-NLS-1$ } } private void freeUnsafeMemory() { synchronized (unsafe) { if (allocked != 0) unsafe.freeMemory(chunkIndex); allocked = 0; } } private SFData(int l) { allocked = 0; try { try { // Allocate the memory for the index - final so do it here long size = (1 + (l >> CHUNK_SHIFT)) << 3; allocked = chunkIndex = unsafe.allocateMemory(size); if (allocked == 0) { throw new RuntimeException("Out of memory allocating " + size); //$NON-NLS-1$ } makeMap(l); } catch (Exception e) { throw new RuntimeException(e); } this.length = l; } catch (Throwable t) { if (allocked != 0) { unsafe.freeMemory(allocked); allocked = 0; } throw t; } Stack<SFMemoryZone> stack = memoryZoneStack.get(); if (stack.size() > 0) { SFMemoryZone zone = stack.peek(); synchronized (zone.localData) { zone.localData.add(this); } } maybeReportStats(); } public final static SFData build(int l) { SFData ret = new SFData(l); ret.clear(); return ret; } /* (non-Javadoc) * @see com.nerdscentral.audio.SFSignal#replicate() */ @Override public final SFSignal replicate() { checkLive(); SFSignal data1 = new SFData(this.getLength()); for (int i = 0; i < this.getLength(); ++i) { data1.setSample(i, this.getSample(i)); } return data1; } private long getAddress(long index) { if (SFConstants.CHECK_MEMORY) { if (index < 0 || index > length) throw new RuntimeException("Access out of bounds."); //$NON-NLS-1$ } long pos = index & CHUNK_MASK; long bufPos = index >> CHUNK_SHIFT; long address = chunkIndex + (bufPos << 3); long addr = unsafe.getAddress(address) + (pos << 3l); if (SFConstants.CHECK_MEMORY) { if (addr == 0) throw new RuntimeException("Accessing released memory."); //$NON-NLS-1$ } return addr; } /* (non-Javadoc) * @see com.nerdscentral.audio.SFSignal#getSample(int) */ @Override public final double getSample(int index) { return unsafe.getDouble(getAddress(index)); } /* (non-Javadoc) * @see com.nerdscentral.audio.SFSignal#setSample(int, double) */ @Override public final double setSample(int index, double value) { unsafe.putDouble(getAddress(index), value); return value; } /* (non-Javadoc) * @see com.nerdscentral.audio.SFSignal#getLength() */ @Override public final int getLength() { checkLive(); return this.length; } public static final SFSignal build(float[] input) { // No nead to clear as we set the entire buffer. SFSignal data = new SFData(input.length); for (int i = 0; i < input.length; ++i) { data.setSample(i, input[i]); } return data; } public static SFSignal build(double[] input) { // No nead to clear as we set the entire buffer. SFSignal data = new SFData(input.length); for (int i = 0; i < input.length; ++i) { data.setSample(i, input[i]); } return data; } public static final SFSignal build(double[] input, int j) { // Use the factor which does a clear as we are not overwriting everything. SFSignal data = SFData.build(j); for (int i = 0; i < j; ++i) { data.setSample(i, input[i]); } return data; } public static final SFSignal build(OffHeapArray input, int j) { // Use the factor which does a clear as we are not overwriting everything. SFSignal data = SFData.build(j); input.checkBoundsDouble(0, j); for (int i = 0; i < j; ++i) { data.setSample(i, input.getDouble(i)); } return data; } /* (non-Javadoc) * @see com.nerdscentral.audio.SFSignal#setAt(int, com.nerdscentral.audio.SFData) */ @Override public void setAt(int pos, SFSignal data2) throws SFPL_RuntimeException { checkLive(); int pos2 = pos; if (pos2 + data2.getLength() > length) { System.out.println(Messages.getString("SFData.9") + pos2 + Messages.getString("SFData.10") + data2.getLength() //$NON-NLS-1$ //$NON-NLS-2$ + Messages.getString("SFData.11") + length); //$NON-NLS-1$ throw new SFPL_RuntimeException(Messages.getString("SFData.0")); //$NON-NLS-1$ } long end = pos2 + data2.getLength(); for (int index = pos2; index < end; ++index) { setSample(index, data2.getSample(index - pos2)); } } /* (non-Javadoc) * @see com.nerdscentral.audio.SFSignal#setFrom(int, com.nerdscentral.audio.SFData) */ @Override public void setFrom(int pos, SFSignal data2) throws SFPL_RuntimeException { checkLive(); int pos2 = pos; if (pos2 + length > data2.getLength()) throw new SFPL_RuntimeException(Messages.getString("SFData.1")); //$NON-NLS-1$ for (int index = 0; index < length; ++index) { setSample(index, data2.getSample(index - pos2)); } } /* (non-Javadoc) * @see com.nerdscentral.audio.SFSignal#toString() */ @Override public String toString() { checkLive(); return Messages.getString("SFData.2") + length + Messages.getString("SFData.3"); //$NON-NLS-1$ //$NON-NLS-2$ } public static SFSignal realise(SFSignal in) { if (in instanceof SFData) return in; int len = in.getLength(); SFSignal output = new SFData(len); for (int i = 0; i < len; ++i) { double sample = in.getSample(i); output.setSample(i, sample); } return output; } @Override public double[] getDataInternalOnly() { double[] ret = new double[length]; for (int index = 0; index < length; ++index) { ret[index] = getSample(index); } return ret; } @Override public boolean isRealised() { checkLive(); return true; } private static long getAbsLen(SFSignal a, long aOffset, SFSignal b, long bOffset) { // Is there enough room irrespective of chunks // How much space left in b long absLen = b.getLength() - bOffset; // How much we can copy from a long wantLen = a.getLength() - aOffset; // abs len is the amount we could copy ignoring chunks if (wantLen < absLen) absLen = wantLen; // Find the end of the next chunks long aEnd = CHUNK_LEN + (aOffset & ~CHUNK_MASK); long bEnd = CHUNK_LEN + (bOffset & ~CHUNK_MASK); long aDif = aEnd - aOffset; long bDif = bEnd - bOffset; long maxChunkLen = aDif > bDif ? bDif : aDif; if (absLen > maxChunkLen) { absLen = maxChunkLen; } return absLen; } // Mixes a onto b for as long as that can be done without flipping chuncks // returns how many doubles it managed to mix, this can be call repeatedly // to then mix two entire buffers. If return is zero then the end of the // buffer has been reached. private static long addChunks(SFData a, long aOffset, SFData b, long bOffset) { long absLen = getAbsLen(a, aOffset, b, bOffset); // Not absLen is the longest continuous mix we can make from // a to b. long aAddr = a.getAddress(aOffset); long bAddr = b.getAddress(bOffset); long end = absLen << 3l; for (long pos = 0; pos < end; pos += 8) { double aValue = unsafe.getDouble(aAddr + pos); long putAddr = bAddr + pos; double bValue = unsafe.getDouble(putAddr); // Due to no compile time polymorphism we have to duplicate // this method for each operator in the next line or suffer // virtual dispatch :| unsafe.putDouble(putAddr, aValue + bValue); } return absLen; } private static long copyChunks(SFData a, long aOffset, SFData b, long bOffset) { long absLen = getAbsLen(a, aOffset, b, bOffset); // Not absLen is the longest continuous mix we can make from // a to b. long aAddr = a.getAddress(aOffset); long bAddr = b.getAddress(bOffset); long end = absLen << 3l; for (long pos = 0; pos < end; pos += 8) { double aValue = unsafe.getDouble(aAddr + pos); long putAddr = bAddr + pos; // Due to no compile time polymorphism we have to duplicate // this method for each operator in the next line or suffer // virtual dispatch :| unsafe.putDouble(putAddr, aValue); } return absLen; } private static long multiplyChunks(SFData a, long aOffset, SFData b, long bOffset) { long absLen = getAbsLen(a, aOffset, b, bOffset); // Not absLen is the longest continuous mix we can make from // a to b. long aAddr = a.getAddress(aOffset); long bAddr = b.getAddress(bOffset); long end = absLen << 3l; for (long pos = 0; pos < end; pos += 8) { double aValue = unsafe.getDouble(aAddr + pos); long putAddr = bAddr + pos; double bValue = unsafe.getDouble(putAddr); // Due to no compile time polymorphism we have to duplicate // this method for each operator in the next line or suffer // virtual dispatch :| unsafe.putDouble(putAddr, aValue * bValue); } return absLen; } private static long divideChunks(SFData a, long aOffset, SFData b, long bOffset) { long absLen = getAbsLen(a, aOffset, b, bOffset); // Not absLen is the longest continuous mix we can make from // a to b. long aAddr = a.getAddress(aOffset); long bAddr = b.getAddress(bOffset); long end = absLen << 3l; for (long pos = 0; pos < end; pos += 8) { double aValue = unsafe.getDouble(aAddr + pos); long putAddr = bAddr + pos; double bValue = unsafe.getDouble(putAddr); // Due to no compile time polymorphism we have to duplicate // this method for each operator in the next line or suffer // virtual dispatch :| unsafe.putDouble(putAddr, aValue / bValue); } return absLen; } private static long subtractChunks(SFData a, long aOffset, SFData b, long bOffset) { long absLen = getAbsLen(a, aOffset, b, bOffset); // Not absLen is the longest continuous mix we can make from // a to b. long aAddr = a.getAddress(aOffset); long bAddr = b.getAddress(bOffset); long end = absLen << 3l; for (long pos = 0; pos < end; pos += 8) { double aValue = unsafe.getDouble(aAddr + pos); long putAddr = bAddr + pos; double bValue = unsafe.getDouble(putAddr); // Due to no compile time polymorphism we have to duplicate // this method for each operator in the next line or suffer // virtual dispatch :| unsafe.putDouble(putAddr, aValue - bValue); } return absLen; } /** * Stores useful information about a SFData signal so it can be returned as a single return from an optimised analysis run. * */ public static class Stats { public double maxValue = 0; public double minValue = 0; public double totalValue = 0; } private static long scanChunks(SFData b, long bOffset, Stats stats) { long absLen = b.getLength() - bOffset; // Not absLen is the longest continuous mix we can make from // a to b. long bAddr = b.getAddress(bOffset); for (long pos = 0; pos < absLen; pos += 8) { long putAddr = bAddr + pos; double bValue = unsafe.getDouble(putAddr); if (bValue > stats.maxValue) stats.maxValue = bValue; if (bValue < stats.minValue) stats.minValue = bValue; stats.totalValue += bValue; } return absLen; } public Stats getStats() { checkLive(); Stats stats = new Stats(); long done = 0; long thisAt = 0; while ((done = scanChunks(this, thisAt, stats)) != 0) { thisAt += done; } return stats; } public enum OPERATION { ADD, COPY, MULTIPLY, DIVIDE, SUBTRACT } // Note that this is laboriously layed out to give // separate call sites for each operation to help give // static dispatch after optimisation public void operateOnto(int at, SFSignal in, OPERATION operation) { checkLive(); if (in.isRealised()) { // TODO this is a bit ambiguous around the meaning of isRealise() but // in the current implementation where only SFData returns true for // isRealise() we are OK. long thisAt = at; long otherAt = 0; long done = 0; SFData data = (SFData) in; switch (operation) { case ADD: while ((done = addChunks(data, otherAt, this, thisAt)) != 0) { thisAt += done; otherAt += done; } break; case COPY: while ((done = copyChunks(data, otherAt, this, thisAt)) != 0) { thisAt += done; otherAt += done; } break; case MULTIPLY: while ((done = multiplyChunks(data, otherAt, this, thisAt)) != 0) { thisAt += done; otherAt += done; } break; case DIVIDE: while ((done = divideChunks(data, otherAt, this, thisAt)) != 0) { thisAt += done; otherAt += done; } break; case SUBTRACT: while ((done = subtractChunks(data, otherAt, this, thisAt)) != 0) { thisAt += done; otherAt += done; } break; } } else { int len = in.getLength(); if (len > getLength()) len = getLength(); switch (operation) { case ADD: for (int index = 0; index < len; ++index) { long address = getAddress(index + at); double value = unsafe.getDouble(address); unsafe.putDouble(address, in.getSample(index) + value); } break; case COPY: for (int index = 0; index < len; ++index) { long address = getAddress(index + at); unsafe.putDouble(address, in.getSample(index)); } break; case MULTIPLY: for (int index = 0; index < len; ++index) { long address = getAddress(index + at); double value = unsafe.getDouble(address); unsafe.putDouble(address, in.getSample(index) * value); } break; case DIVIDE: for (int index = 0; index < len; ++index) { long address = getAddress(index + at); double value = unsafe.getDouble(address); unsafe.putDouble(address, in.getSample(index) / value); } break; case SUBTRACT: for (int index = 0; index < len; ++index) { long address = getAddress(index + at); double value = unsafe.getDouble(address); unsafe.putDouble(address, in.getSample(index) - value); } break; } } } public static SFMemoryZone popZone() { // Release everything in the head of the zone thread local stack. SFMemoryZone zone = memoryZoneStack.get().pop(); // System.out.println("Popped: " + zone); synchronized (zone.localData) { for (SFData data : zone.localData) { // Pass ByteBufferWrapper[] someChunks = data.chunks; if (someChunks != null) { // Not already released. if (data.kept.get()) { data.kept.set(false); } else { if (!data.pinned.get()) data.release(); } } } zone.localData.clear(); } maybeReportStats(); return zone; } public static SFMemoryZone unstackZone() { return memoryZoneStack.get().pop(); } public static void stackZone(SFMemoryZone sfMemoryZone) { pushZone(sfMemoryZone); } public static SFMemoryZone peekZone() { Stack<SFMemoryZone> zone = memoryZoneStack.get(); if (zone.size() == 0) return null; return zone.peek(); } public static void pushZone(SFMemoryZone sfMemoryZone) { // Push to a thread local stack a collection which holds all new SFData objects. // So that they can be released explicitly. // System.out.println("Pushing: " + sfMemoryZone); memoryZoneStack.get().push(sfMemoryZone); } @Override public SFSignal pin() { checkLive(); pinned.set(true); return this; } @Override public SFSignal keep() { checkLive(); kept.set(true); return this; } }