Extends DecayingBloomFilter to provide dual-buffered hash sets with automatic decay switching between active and previous buffers. Optimized for high-throughput scenarios with frequent insertions and periodic cleanup operations.
Memory usage analysis shows significant optimization potential:
- 32 bytes, peak 10 entries in 1m (320 on fast routers)
- 4 bytes, peak 150 entries in 10s (1600 on fast routers)
- 8 bytes, peak 1K entries in 2m (36K on fast routers)
- 16 bytes, peak 15K entries in 10m (15K on fast routers)
Performance characteristics:
- Zero false positive rate for ≤8 byte keys (lossy hash for larger keys)
- Over 8 bytes entries are reduced to a 64 bit hash, so a false positive needs a hash collision: about 5.4E-20 per pair, so n^2/2^65 across n entries
- About 1.93x faster than
DecayingBloomFilterfor 8 byte (long) entries and 2.46x faster for 16 byte entries - About 72 bytes of memory per 16 byte entry once loaded (measured: 9.01 MB for 131,000 entries), covering the map node, the
ArrayWrapperand its share of the table. At the 128K soft cap that is roughly 9 MB as a floor, not a ceiling: getInsertedCount() spans both buffers, so a window deferred past the cap can take this to about twice that - Space-proportional traffic handling
Uses read/write locks with SimpleTimer2 for thread safety and efficient buffer switching without synchronization overhead.
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final longMeasured retained cost per entry for 16 byte keys: about 72 bytes, covering the map node, theArrayWrapperand its share of the table.(package private) static final intMaximum entries before a forced early decay is attempted.Fields inherited from class DecayingBloomFilter
_context, _currentDuplicates, _decayEvent, _durationMs, _entryBytes, _keepDecaying, _log, _name, _reorganizeLock, MAX_MModifier and TypeFieldDescriptionprotected final I2PAppContextThe I2P app context.protected final AtomicLongCounter for current duplicates.protected final SimpleTimer2.TimedEventThe decay timer event.protected final intDuration of each filter in ms.protected final intSize of entries in bytes.protected booleanWhether decay is still running.protected final LogThe logger.protected final StringName for logging.protected final ReentrantReadWriteLockSynchronize against this lock when switching double buffers.static final intLargest exponent usable for a 32 byte key.Constructor Summary
ConstructorsConstructorDescriptionDecayingHashSet(I2PAppContext context, int durationMs, int entryBytes) Create a double-buffered hash set that will decay its entries over time.DecayingHashSet(I2PAppContext context, int durationMs, int entryBytes, String name) Decaying hash set with a custom name.DecayingHashSet(I2PAppContext context, int durationMs, int entryBytes, String name, int maxEntries) As above, with an explicit cap.Method Summary
All MethodsModifier and TypeMethodDescriptionbooleanadd(byte[] entry, int off, int len) Add the entry, returning whether it was a duplicate.booleanadd(long entry) Add the long entry, returning whether it was a duplicate.voidclear()Clear all filters and reset counts.protected voiddecay()Decay the filter, moving the current buffer to the previous.longBytes of memory the current contents occupy, at the measured rate of about 72 bytes per entry for 16 byte keys.doubleStubbed rate; only used for logging elsewhere.intUnsynchronized.intThe entry cap currently in force, across both buffers.booleanisKnown(long entry) Check whether the entry is already known, without adding it.voidsetMaxEntries(int maxEntries) Change the entry cap while running, for the router's low-memory path.voidSuper doesn't call clear, but neither do the users, so it seems like we should here.Methods inherited from class DecayingBloomFilter
add, checkEntryBytes, getCurrentDuplicateCount, getLastWindowCount, getM, getMemoryBytes, getReadLock, getWriteLock, mForBudget, mForEntries, releaseReadLock, releaseWriteLockModifier and TypeMethodDescriptionbooleanadd(byte[] entry) Add a byte array entry to the filter.protected static voidcheckEntryBytes(int entryBytes) Validate an entry size for a bloom filter.longCurrent number of duplicates detected.intEntries seen in the most recently completed decay window, which is the count this filter has to be sized against.intgetM()Filter size exponent.longBytes of memory this filter occupies, for both buffers.protected voidAcquire the read lock.protected booleanAcquire the write lock.static intmForBudget(long budgetBytes) Largest exponent whose pair of buffers fits in the given byte budget, capped at the largest a 32 byte key can be hashed into.static intmForEntries(int entries, double targetFpr) Smallest exponent whose false positive rate stays at or under the target for the given number of entries, at the optimal k for that exponent.protected voidRelease the read lock.protected voidRelease the write lock.
Field Details
DEFAULT_MAX_ENTRIES
static final int DEFAULT_MAX_ENTRIESMaximum entries before a forced early decay is attempted. This is a soft target, not a hard ceiling: memory is bounded by roughly two windows of traffic rather than by this number, because forcing a decay sooner than an entry's lifetime would drop live entries.Measured cost at this cap with 16 byte entries: about 9 MB, roughly 72 bytes per entry including the map node and the wrapper.
- See Also:
BYTES_PER_ENTRY
public static final long BYTES_PER_ENTRYMeasured retained cost per entry for 16 byte keys: about 72 bytes, covering the map node, theArrayWrapperand its share of the table. Measured 9.01 MB for 131,000 entries.- See Also:
Constructor Details
DecayingHashSet
Create a double-buffered hash set that will decay its entries over time.- Parameters:
context- the app context supplying the log and the clockdurationMs- entries last for at least this long, but no more than twice this longentryBytes- how large are the entries to be added? 1 to 32 bytes- Throws:
IllegalArgumentException- if entryBytes is not 1-32
DecayingHashSet
Decaying hash set with a custom name.- Parameters:
context- the app context supplying the log and the clockdurationMs- entries last for at least this long, but no more than twice this longentryBytes- how large are the entries to be added? 1 to 32 bytesname- the instance name, used only in log messages and stats- Throws:
IllegalArgumentException- if entryBytes is not 1-32
DecayingHashSet
DecayingHashSet(I2PAppContext context, int durationMs, int entryBytes, String name, int maxEntries) As above, with an explicit cap. Only for tests, which need to cross the over-cap path without inserting the fullDEFAULT_MAX_ENTRIESentries.- Parameters:
context- the app context supplying the log and the clockdurationMs- entries last for at least this long, but no more than twice this longentryBytes- how large are the entries to be added? 1 to 32 bytesname- the instance name, used only in log messages and statsmaxEntries- hard cap on entries across both buffers
Method Details
getInsertedCount
public int getInsertedCount()Unsynchronized. Read on the add() path by forceDecayIfOverCap(), as well as for logging, so it is a hot call.- Overrides:
getInsertedCountin classDecayingBloomFilter- Returns:
- how many entries are held across both generations of the filter
getMaxEntries
public int getMaxEntries()The entry cap currently in force, across both buffers.- Returns:
- the maximum number of entries held across both buffers before a decay is triggered
setMaxEntries
public void setMaxEntries(int maxEntries) Change the entry cap while running, for the router's low-memory path.Lowering it takes effect at once: the next add() over the new cap triggers a decay, which may retire entries that have not yet expired. Raising it does not resurrect anything already retired.
- Parameters:
maxEntries- the new cap, must be positive- Throws:
IllegalArgumentException- if maxEntries is not positive
getEstimatedMemoryBytes
public long getEstimatedMemoryBytes()Bytes of memory the current contents occupy, at the measured rate of about 72 bytes per entry for 16 byte keys. Reported so a caller sizing against a memory budget can see the cost it is actually paying.- Returns:
- estimated retained bytes
getFalsePositiveRate
public double getFalsePositiveRate()Stubbed rate; only used for logging elsewhere.- Overrides:
getFalsePositiveRatein classDecayingBloomFilter- Returns:
- the false positive rate
add
public boolean add(byte[] entry, int off, int len) Add the entry, returning whether it was a duplicate.- Overrides:
addin classDecayingBloomFilter- Parameters:
entry- the entry dataoff- index in entry of the first byte to filterlen- number of bytes to filter, must equal _entryBytes- Returns:
- true if the entry added is a duplicate
add
public boolean add(long entry) Add the long entry, returning whether it was a duplicate.- Overrides:
addin classDecayingBloomFilter- Parameters:
entry- the long value to add- Returns:
- true if the entry added is a duplicate. the number of low order bits used is determined by the entryBytes parameter used on creation of the filter.
isKnown
public boolean isKnown(long entry) Check whether the entry is already known, without adding it.- Overrides:
isKnownin classDecayingBloomFilter- Parameters:
entry- the long value to check- Returns:
- true if the entry is already known. this does NOT add the entry however.
clear
public void clear()Description copied from class:DecayingBloomFilterClear all filters and reset counts.- Overrides:
clearin classDecayingBloomFilter
stopDecaying
public void stopDecaying()Super doesn't call clear, but neither do the users, so it seems like we should here.- Overrides:
stopDecayingin classDecayingBloomFilter
decay
protected void decay()Description copied from class:DecayingBloomFilterDecay the filter, moving the current buffer to the previous.The buffer that becomes the new current one is reset to a freshly allocated array rather than zeroed in place. Zeroing in place is a memset of the whole filter held under the write lock, and every add() takes the read lock, so it stalls all inserts for its duration: measured at about 2.6ms at m=29. Allocating the replacement does the same zeroing without the lock and is in fact slightly cheaper at that size, so the lock is held only for the pointer swap. See
BloomSHA1.prepareReset()for the allocation/GC tradeoff this accepts.- Overrides:
decayin classDecayingBloomFilter