/** * Copyright (c) 2002-2012 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.neo4j.kernel.impl.cache; import java.util.Collection; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReferenceArray; import org.neo4j.kernel.impl.util.StringLogger; import org.neo4j.kernel.info.DiagnosticsPhase; import org.neo4j.kernel.info.DiagnosticsProvider; public class GCResistantCache<E extends EntityWithSize> implements Cache<E>, DiagnosticsProvider { public static final long MIN_SIZE = 1; private final AtomicReferenceArray<E> cache; private final long maxSize; private long closeToMaxSize; private long purgeStopSize; private long purgeHandoffSize; private final AtomicLong currentSize = new AtomicLong( 0 ); private final long minLogInterval; private final String name; private final AtomicLong highestIdSet = new AtomicLong(); // non thread safe, only ~statistics (atomic update will affect performance) private long hitCount = 0; private long missCount = 0; private long totalPuts = 0; private long collisions = 0; private long purgeCount = 0; private final StringLogger logger; private final AtomicBoolean purging = new AtomicBoolean(); private final AtomicInteger avertedPurgeWaits = new AtomicInteger(); private final AtomicInteger forcedPurgeWaits = new AtomicInteger(); private long purgeTime; GCResistantCache( AtomicReferenceArray<E> cache ) { this.cache = cache; this.minLogInterval = Long.MAX_VALUE; this.maxSize = 1024l*1024*1024; this.name = "test cache"; this.logger = null; calculateSizes(); } public GCResistantCache( long maxSizeInBytes, float arrayHeapFraction, long minLogInterval, String name, StringLogger logger ) { this.minLogInterval = minLogInterval; if ( arrayHeapFraction < 1 || arrayHeapFraction > 10 ) { throw new IllegalArgumentException( "The heap fraction used by a GC resistant cache must be between 1% and 10%, not " + arrayHeapFraction + "%" ); } long memToUse = (long)(((double)arrayHeapFraction) * Runtime.getRuntime().maxMemory() / 100); long maxElementCount = (int) ( memToUse / 8 ); if ( memToUse > Integer.MAX_VALUE ) { maxElementCount = Integer.MAX_VALUE; } if ( maxSizeInBytes < MIN_SIZE ) { throw new IllegalArgumentException( "Max size can not be " + maxSizeInBytes ); } this.cache = new AtomicReferenceArray<E>( (int) maxElementCount ); this.maxSize = maxSizeInBytes; this.name = name == null ? super.toString() : name; this.logger = logger == null ? StringLogger.SYSTEM : logger; calculateSizes(); } private void calculateSizes() { this.closeToMaxSize = (long)((double)maxSize * 0.95d); this.purgeStopSize = (long)((double)maxSize * 0.90d); this.purgeHandoffSize = (long)((double)maxSize * 1.05d); } private int getPosition( EntityWithSize obj ) { return (int) ( obj.getId() % cache.length() ); } private int getPosition( long id ) { return (int) ( id % cache.length() ); } private long putTimeStamp = 0; public void put( E obj ) { long time = System.currentTimeMillis(); if ( time - putTimeStamp > minLogInterval ) { putTimeStamp = time; printStatistics(); } int pos = getPosition( obj ); E oldObj = cache.get( pos ); if ( oldObj != obj ) { int objectSize = obj.size(); if ( cache.compareAndSet( pos, oldObj, obj ) ) { setHighest( pos ); int oldObjSize = 0; if ( oldObj != null ) { oldObjSize = oldObj.getRegisteredSize(); } long size = currentSize.addAndGet( objectSize - oldObjSize ); obj.setRegisteredSize( objectSize ); if ( oldObj != null ) { collisions++; } totalPuts++; if ( size > closeToMaxSize ) { purgeFrom( pos ); } } } } /** * Updates the highest set id if the given id is higher than any previously registered id. * Helps {@link #clear()} performance wise so that only the used part of the array is cleared. * @param id the id just put into the cache. */ private void setHighest( long id ) { while ( true ) { long highest = highestIdSet.get(); if ( id > highest ) { if ( highestIdSet.compareAndSet( highest, id ) ) break; } else break; } } public E remove( long id ) { int pos = getPosition( id ); E obj = cache.get(pos); if ( obj != null ) { if ( cache.compareAndSet( pos, obj, null ) ) { currentSize.addAndGet( obj.getRegisteredSize() * -1 ); } } return obj; } public E get( long id ) { int pos = getPosition( id ); E obj = cache.get( pos ); if ( obj != null && obj.getId() == id ) { hitCount++; return obj; } missCount++; return null; } private long lastPurgeLogTimestamp = 0; private void purgeFrom( int pos ) { long myCurrentSize = currentSize.get(); if ( myCurrentSize <= closeToMaxSize ) return; // if we're within 0.95 < size < 1.05 and someone else is purging then just return and let // the other one purge for us. if we're above 1.05 then wait for the purger to finish before returning. if ( purging.compareAndSet( false, true ) ) { // We're going to do the purge try { doPurge( pos ); } finally { purging.set( false ); } } else { // Someone else is currently doing a purge if ( myCurrentSize < purgeHandoffSize ) { // It's safe to just return and let the purger do its thing avertedPurgeWaits.incrementAndGet(); return; } else { // Wait for the current purge to complete. Some threads might slip through here before // A thread just entering doPurge above, but that's fine forcedPurgeWaits.incrementAndGet(); waitForCurrentPurgeToComplete(); } } } private synchronized void waitForCurrentPurgeToComplete() { // Just a nice way of saying "wait for the monitor on this object currently held by the thread doing a purge" } private synchronized void doPurge( int pos ) { long myCurrentSize = currentSize.get(); if ( myCurrentSize <= closeToMaxSize ) return; long startTime = System.currentTimeMillis(); purgeCount++; long sizeBefore = currentSize.get(); try { int index = 1; do { if ( ( pos - index ) >= 0 ) { int minusPos = pos - index; remove( minusPos ); if ( currentSize.get() <= purgeStopSize ) return; } if ( ( pos + index ) < cache.length() ) { int plusPos = pos + index; remove( plusPos ); if ( currentSize.get() <= purgeStopSize ) return; } index++; } while ( ( pos - index ) >= 0 || ( pos + index ) < cache.length() ); // current object larger than max size, clear it remove( pos ); } finally { long timestamp = System.currentTimeMillis(); purgeTime += (timestamp-startTime); if ( timestamp - lastPurgeLogTimestamp > minLogInterval ) { lastPurgeLogTimestamp = timestamp; long sizeAfter = currentSize.get(); String sizeBeforeStr = getSize( sizeBefore ); String sizeAfterStr = getSize( sizeAfter ); String diffStr = getSize( sizeBefore - sizeAfter ); String missPercentage = ((float) missCount / (float) (hitCount+missCount) * 100.0f) + "%"; String colPercentage = ((float) collisions / (float) totalPuts * 100.0f) + "%"; logger.logMessage( name + " purge (nr " + purgeCount + ") " + sizeBeforeStr + " -> " + sizeAfterStr + " (" + diffStr + ") " + missPercentage + " misses, " + colPercentage + " collisions (" + collisions + ").", true ); printAccurateStatistics(); } } } private void printAccurateStatistics() { int elementCount = 0; long actualSize = 0; long registeredSize = 0; for ( int i = 0; i < cache.length(); i++ ) { EntityWithSize obj = cache.get( i ); if ( obj != null ) { elementCount++; actualSize += obj.size(); registeredSize += obj.getRegisteredSize(); } } logger.logMessage( name + " purge (nr " + purgeCount + "): elementCount:" + elementCount + " and sizes actual:" + getSize( actualSize ) + ", perceived:" + getSize( currentSize.get() ) + " (diff:" + getSize(currentSize.get() - actualSize) + "), registered:" + getSize( registeredSize ), true ); } public void printStatistics() { logStatistics( logger ); // printAccurateStatistics(); } @Override public String getDiagnosticsIdentifier() { return getName(); } @Override public void acceptDiagnosticsVisitor( Object visitor ) { // accept no visitors. } @Override public void dump( DiagnosticsPhase phase, StringLogger log ) { if (phase.isExplicitlyRequested()) logStatistics(log); } private void logStatistics( StringLogger log ) { log.logMessage( this.toString(), true ); } @Override public String toString() { String currentSizeStr = getSize( currentSize.get() ); String missPercentage = ((float) missCount / (float) (hitCount+missCount) * 100.0f) + "%"; String colPercentage = ((float) collisions / (float) totalPuts * 100.0f) + "%"; return name + " array:" + cache.length() + " purge:" + purgeCount + " size:" + currentSizeStr + " misses:" + missPercentage + " collisions:" + colPercentage + " (" + collisions + ") av.purge waits:" + avertedPurgeWaits.get() + " purge waits:" + forcedPurgeWaits.get() + " avg. purge time:" + (purgeCount > 0 ? (purgeTime/purgeCount) + "ms" : "N/A"); } private String getSize( long size ) { if ( size > ( 1024 * 1024 * 1024 ) ) { float value = size / 1024.0f / 1024.0f / 1024.0f; return value + "Gb"; } if ( size > ( 1024 * 1024 ) ) { float value = size / 1024.0f / 1024.0f; return value + "Mb"; } if ( size > 1024 ) { float value = size / 1024.0f / 1024.0f; return value + "kb"; } return size + "b"; } public void clear() { for ( int i = 0; i <= highestIdSet.get() /*cache.length()*/; i++ ) { cache.set( i, null ); } currentSize.set( 0 ); highestIdSet.set( 0 ); } public void putAll( Collection<E> objects ) { for ( E obj : objects ) { put( obj ); } } @Override public String getName() { return name; } @Override public long size() { return currentSize.get(); } @Override public long hitCount() { return hitCount; } @Override public long missCount() { return missCount; } @Override public void updateSize( E obj, int newSize ) { int pos = getPosition( obj ); E existingObj = cache.get( pos ); if ( existingObj != obj ) { return; } long size = currentSize.addAndGet( (newSize - existingObj.getRegisteredSize()) ); obj.setRegisteredSize( newSize ); if ( size > closeToMaxSize ) { purgeFrom( pos ); } } }