Admission decides whether an arriving item deserves cache space; eviction chooses the resident it would replace. This model keeps an LRU-ordered resident map and a separate four-row frequency sketch. Every lookup attempt and put records the key in the sketch, including a lookup that misses the resident map. A full-cache put compares the candidate's minimum row count with the LRU victim's estimate and admits only when the candidate estimate is strictly greater. Sketch counters saturate at fifteen and are halved after a fixed number of samples, limiting how long old traffic dominates. The digest is deterministic for the teaching trace. This is a simplified frequency-admission cache, not a complete windowed cache policy with adaptive regions or a measured hit-rate claim.
Frequency admission: compare an arrival with an eviction victim
Operational case
Residents 47 and 19 fill a two-entry cache. Four repeated reads of 47 make it a hot resident. Incident 83 arrives once and is rejected because its estimate does not exceed that of the least-recently-used victim. Five observations of incident 61 precede its put; the sketch now rates 61 above victim 19, so 61 is admitted and 19 is removed. The remaining residents are 47 and 61. Recording a request is distinct from admitting its payload, and a rejected candidate can still gain frequency evidence through later requests. Digest collisions may inflate an estimate, so this comparison is a heuristic rather than proof of future demand.
Working Python program
from collections import OrderedDict
import hashlib
class AgingFrequencySketch:
def __init__(self, width=16, rows=4, sample_size=64):
if min(width, rows, sample_size) < 1:
raise ValueError("sketch dimensions and sample size must be positive")
self.width = width
self.counters = [[0] * width for _ in range(rows)]
self.sample_size = sample_size
self.samples = 0
def _columns(self, incident_id):
digest = hashlib.blake2b(str(incident_id).encode(), digest_size=16).digest()
for row in range(len(self.counters)):
yield int.from_bytes(digest[row * 4:row * 4 + 4], "little") % self.width
def record(self, incident_id):
for counters, column in zip(self.counters, self._columns(incident_id)):
counters[column] = min(15, counters[column] + 1)
self.samples += 1
if self.samples == self.sample_size:
for counters in self.counters:
for column in range(self.width):
counters[column] //= 2
self.samples = 0
def estimate(self, incident_id):
return min(counters[column] for counters, column in zip(self.counters, self._columns(incident_id)))
class FrequencyAdmissionCache:
def __init__(self, capacity=2):
if capacity < 1:
raise ValueError("capacity must be positive")
self.capacity = capacity
self.order = OrderedDict()
self.sketch = AgingFrequencySketch()
def get(self, incident_id):
self.sketch.record(incident_id)
self.order.move_to_end(incident_id)
return self.order[incident_id]
def put(self, incident_id, payload):
self.sketch.record(incident_id)
if incident_id in self.order:
self.order[incident_id] = payload
self.order.move_to_end(incident_id)
return True
if len(self.order) == self.capacity:
victim_id = next(iter(self.order))
if self.sketch.estimate(incident_id) <= self.sketch.estimate(victim_id):
return False
self.order.popitem(last=False)
self.order[incident_id] = payload
return True
cache = FrequencyAdmissionCache(2)
cache.put(47, "incident-47")
cache.put(19, "incident-19")
for _ in range(4):
cache.get(47)
accepted_cold = cache.put(83, "incident-83")
for _ in range(5):
cache.sketch.record(61)
accepted_warm = cache.put(61, "incident-61")
print("cold=", accepted_cold, " warm=", accepted_warm,
" resident=", list(cache.order), sep="")Output
cold=False warm=True resident=[47, 61]Time, space, and tradeoff
With R rows, W counters per row, and key encoding length K, recording or estimating a key costs O(R + K) for bounded digest work; in this fixed model R is four. Every sample_size records the sketch halves all RW counters in O(RW) time, so an individual operation can have that spike. The cache map and recency update are expected O(1). Storage is O(RW + C) for C residents, excluding Python object overhead. Counter saturation and aging discard precise historical frequencies. A different hash policy, sample interval, or tie rule can change decisions, and a workload trace is needed to evaluate actual hit rates.
Common Mistakes
- Do not call the sketch estimate an exact request count.
- Do not admit every miss automatically when the cache is full.
- Do not compare a candidate with an arbitrary resident when the policy names the LRU victim.
- Do not omit the O(RW) counter-halving spike from worst-case cost.
Connected lessons
- Hashing
- Data Structures
- Count-min sketches: bounded-memory event estimates
- LFU caches: evict by frequency, then recency
- LRU caches: couple keyed lookup to recency order
- Projects
- Quizzes
Compare retention and timing contracts with CLOCK caches: revisit pages through reference bits, Segmented LRU: separate probation from protected reuse, Hashed timing wheels: bucket incident expiries by tick, then run the policy audit and contract quiz.
