package com.pblabs.profiler; import java.io.EOFException; import java.util.HashMap; import org.slf4j.LoggerFactory; public class NetStringCache { private static final org.slf4j.Logger logger = LoggerFactory.getLogger(NetStringCache.class); /** * Number of bits to use for encoding string references. */ private int mStringRefBitCount = 10; private int mEntryCount; private CacheEntry mIDLookupTable[]; private HashMap<String, CacheEntry> mStringHashLookupTable; private CacheEntry mCacheEntries[], mLRUHead, mLRUTail; private int mWriteCount, mCachedWriteCount; private int mReadCount, mCachedReadCount; private float mBytesSubmitted, mBytesWritten; private float mBytesEmitted, mBytesRead; public NetStringCache() { mEntryCount = 1<<mStringRefBitCount; mIDLookupTable = new CacheEntry[mEntryCount]; mStringHashLookupTable = new HashMap<String, CacheEntry>(); mCacheEntries = new CacheEntry[mEntryCount]; // Sentinels for start/end of list. mLRUHead = new CacheEntry(); mLRUTail = new CacheEntry(); // Fill in all our cache entries. for(int i=0; i<mEntryCount; i++) { CacheEntry curEntry = mCacheEntries[i] = new CacheEntry(); // Get the preceding entry. CacheEntry prevEntry; if(i==0) prevEntry = mLRUHead; else prevEntry = mCacheEntries[i-1]; // Now set up links. curEntry.mLRUPrev = prevEntry; curEntry.mLRUNext = mLRUTail; prevEntry.mLRUNext = curEntry; // Fill in the ID. curEntry.mID = i; mIDLookupTable[i] = curEntry; } // Set up the end sentinel. mLRUTail.mLRUPrev = mCacheEntries[mEntryCount-1]; } /** * Read a string from a BitStream, updating internal cache * state as appropriate. * @param bs * @return */ public String read(BitStream bs) throws EOFException { boolean newString = false; int stringId = 0; mReadCount++; String readString = null; // First, check if this is an update or a cached entry. if(bs.readFlag()) { newString = true; // This is some new data... int startRead = bs.getCurrentPosition(); // Read the ID we're overwriting. int newId = bs.readInt(mStringRefBitCount); //logger.info("new id:"+newId); // And the string. readString = bs.readString(); //logger.info("read new string:"+readString); mBytesRead += (float)(bs.getCurrentPosition() / startRead) / 8.0; // Overwrite the cache. reuseEntry(lookupById(newId), readString); } else { // This is referring to cached data... int oldId = bs.readInt(mStringRefBitCount); //logger.info("old id:"+oldId); // Look it up in our cache. readString = lookupById(oldId).mValue; mBytesRead += (float)mStringRefBitCount / 8.0; mCachedReadCount++; } if (readString!=null) { mBytesEmitted += readString.getBytes().length; } else { /* logger.info("Null string"); if (newString) logger.info("String was new"); else { logger.info("String was supposed to be found, id="+stringId); } */ } return readString; } public void write(BitStream bs, String s) throws EOFException { mWriteCount++; mBytesSubmitted += s.getBytes().length; // Find the string in our hash. CacheEntry ce = lookupByString(s); Boolean found = true; if(ce == null) { // Don't know about this string, so set it up (and note we did so). found = false; ce = getOldestEntry(); reuseEntry(ce, s); } // Note the ID. int foundId = ce.mID; // Is it new? if(bs.writeFlag(!found)) { // Write the ID, and the string. bs.writeInt(foundId, mStringRefBitCount); bs.writeString(s); mBytesWritten += (mStringRefBitCount / 8.0) + s.getBytes().length; } else { // Great, write the ID and we're done. bs.writeInt(foundId, mStringRefBitCount); mCachedWriteCount++; mBytesWritten += (mStringRefBitCount / 8.0); } } /** * Take passed CacheEntry and bring it to the front of our cache. * @param ce */ private void bringToFront(CacheEntry ce) { // Unlink it from where it is the list... ce.mLRUPrev.mLRUNext = ce.mLRUNext; ce.mLRUNext.mLRUPrev = ce.mLRUPrev; // And link it at the head. ce.mLRUNext = mLRUHead.mLRUNext; ce.mLRUNext.mLRUPrev = ce; ce.mLRUPrev = mLRUHead; mLRUHead.mLRUNext = ce; } /** * Get the oldest entry, probably so you can overwrite it. */ private CacheEntry getOldestEntry() { return mLRUTail.mLRUPrev; } /** * Remove an entry from secondary data structures, assign a new string * to it, and reinsert it. */ private void reuseEntry(CacheEntry ce, String newString) { // Get it out of the string map. if(ce.mValue != null) mStringHashLookupTable.remove(ce.mValue); // Assign the new string. ce.mValue = newString; // Reinsert string map. mStringHashLookupTable.put(newString, ce); // Bring to front of cache. bringToFront(ce); } /** * Look up entry by string. */ private CacheEntry lookupByString(String s) { return mStringHashLookupTable.get(s); } /** * Look up entry by its id. */ private CacheEntry lookupById(int id) { return mIDLookupTable[id]; } public void reportStatistics() { logger.info("Report for NetStringCache " + this.toString()); logger.info(" - " + mReadCount + " reads, " + mCachedReadCount + " cached (" + ((float)mCachedReadCount / (float)mReadCount)*100.0 + "% cached.)"); logger.info(" - " + mWriteCount + " writes, " + mCachedWriteCount + " cached (" + ((float)mCachedWriteCount / (float)mWriteCount)*100.0 + "% cached.)"); // How much of the cache is used? int numUsed = 0; for(int i=0; i<mEntryCount; i++) { if(mIDLookupTable[i] == null || mIDLookupTable[i].mValue == null) continue; numUsed++; } logger.info(" - " + numUsed + " out of " + mEntryCount + " IDs in use (" + ((float)numUsed / (float)mEntryCount)*100.0 + "% utilization.)"); // Note efficiency on IO... logger.info(" - " + mBytesRead + " bytes read, " + mBytesEmitted + " bytes emitted." + "(factor of " + (mBytesEmitted/mBytesRead) + " advantage)"); logger.info(" - " + mBytesWritten + " bytes written, " + mBytesSubmitted + " bytes submitted." + "(factor of " + (mBytesSubmitted/mBytesWritten) + " advantage)"); } } class CacheEntry { CacheEntry mLRUNext, mLRUPrev; int mID; String mValue; }