diff --git a/BitFaster.Caching.UnitTests/Lfu/WeightedLfuCapacityPartitionTests.cs b/BitFaster.Caching.UnitTests/Lfu/WeightedLfuCapacityPartitionTests.cs new file mode 100644 index 00000000..279fb7a7 --- /dev/null +++ b/BitFaster.Caching.UnitTests/Lfu/WeightedLfuCapacityPartitionTests.cs @@ -0,0 +1,36 @@ +using BitFaster.Caching.Lfu; +using FluentAssertions; +using Xunit; + +namespace BitFaster.Caching.UnitTests.Lfu +{ + public class WeightedLfuCapacityPartitionTests + { + [Fact] + public void CapacityReturnsCapacity() + { + var partition = new WeightedLfuCapacityPartition(123); + partition.Capacity.Should().Be(123); + } + + [Fact] + public void MaximumEqualsCapacity() + { + var partition = new WeightedLfuCapacityPartition(100); + partition.Maximum.Should().Be(100); + } + + [Theory] + [InlineData(3, 1, 1)] + [InlineData(100, 1, 79)] + [InlineData(1000, 10, 792)] + public void CtorSetsExpectedWeightedMaximums(int capacity, long expectedWindowMaximum, long expectedMainProtectedMaximum) + { + var partition = new WeightedLfuCapacityPartition(capacity); + + partition.Maximum.Should().Be(capacity); + partition.WindowMaximum.Should().Be(expectedWindowMaximum); + partition.MainProtectedMaximum.Should().Be(expectedMainProtectedMaximum); + } + } +} diff --git a/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs b/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs index 398d1fb1..0bd38130 100644 --- a/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs +++ b/BitFaster.Caching/Lfu/ConcurrentLfuCore.cs @@ -51,17 +51,6 @@ internal struct ConcurrentLfuCore : IBoundedPolicy private const int DefaultBufferSize = 128; - // Weighted eviction tuning, matching Caffeine. - private const double MainPercentage = 0.99d; - private const double MainProtectedPercentage = 0.8d; - private const int AdmitHashDosThreshold = 6; - private const double HillClimberStepPercent = 0.0625d; - private const double HillClimberStepDecay = 0.98d; - private const double HillClimberRestartThreshold = 0.05d; - private const double HillClimberMinStep = 2.0d; - private const long SmallCacheThreshold = 512; - private const int QueueTransferThreshold = 1000; - private readonly ConcurrentDictionary dictionary; internal readonly StripedMpscBuffer readBuffer; @@ -71,25 +60,20 @@ internal struct ConcurrentLfuCore : IBoundedPolicy private readonly CmSketch cmSketch; - private readonly LfuNodeList windowLru; - private readonly LfuNodeList probationLru; - private readonly LfuNodeList protectedLru; + internal readonly LfuNodeList windowLru; + internal readonly LfuNodeList probationLru; + internal readonly LfuNodeList protectedLru; private readonly LfuCapacityPartition capacity; // Weighted eviction state. Used only when the node policy is weighted (IsWeighted == true); // the JIT elides the weighted branches in the count case since IsWeighted folds to a constant. + // The queue maximums and common hill climb state live in LfuCapacityPartition; the sizes below + // are runtime accounting and stay in the core (the weighted climb mutates them via ref). private static readonly bool IsWeighted = default(P).IsWeighted; private long weightedSize; - private long windowWeightedSize; - private long mainProtectedWeightedSize; - private long maximum; - private long windowMaximum; - private long mainProtectedMaximum; - private double stepSize; - private double previousHitRate; - private long previousHitCount; - private long previousMissCount; + internal long windowWeightedSize; + internal long mainProtectedWeightedSize; private readonly Random? random; internal readonly DrainStatus drainStatus = new(); @@ -145,19 +129,15 @@ public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler schedule this.probationLru = new LfuNodeList(); this.protectedLru = new LfuNodeList(); - this.capacity = new LfuCapacityPartition(capacity); - if (IsWeighted) { - // Mirror Caffeine's initial split: window ~1% of total weight, protected ~80% of main. - this.maximum = capacity; - this.windowMaximum = this.maximum - (long)(MainPercentage * this.maximum); - this.mainProtectedMaximum = (long)(MainProtectedPercentage * (this.maximum - this.windowMaximum)); - this.previousHitRate = 1.0d; - double initialStep = Math.Max(HillClimberStepPercent * this.maximum, HillClimberMinStep); - this.stepSize = (this.maximum <= SmallCacheThreshold) ? initialStep : -initialStep; + this.capacity = new WeightedLfuCapacityPartition(capacity); this.random = new Random(); } + else + { + this.capacity = new LfuCapacityPartition(capacity); + } this.scheduler = scheduler; @@ -179,8 +159,11 @@ public ConcurrentLfuCore(int concurrencyLevel, int capacity, IScheduler schedule internal long WeightedSize => this.weightedSize; internal long WindowWeightedSize => this.windowWeightedSize; internal long MainProtectedWeightedSize => this.mainProtectedWeightedSize; - internal long WindowMaximum => this.windowMaximum; - internal long MainProtectedMaximum => this.mainProtectedMaximum; + internal long WindowMaximum => WeightedCapacity.WindowMaximum; + internal long MainProtectedMaximum => WeightedCapacity.MainProtectedMaximum; + + private LfuCapacityPartition CountCapacity => this.capacity; + private WeightedLfuCapacityPartition WeightedCapacity => (WeightedLfuCapacityPartition)this.capacity; public Optional Metrics => new(this.metrics); @@ -690,12 +673,12 @@ private bool Maintenance(N? droppedWrite = null, ItemRemovedReason reason = Item if (IsWeighted) { - OptimizeWeightedPartitioning(); + WeightedCapacity.OptimizePartitioning(ref this, this.metrics, this.cmSketch.ResetSampleSize); ReFitProtectedWeighted(); } else { - this.capacity.OptimizePartitioning(this.metrics, this.cmSketch.ResetSampleSize); + CountCapacity.OptimizePartitioning(this.metrics, this.cmSketch.ResetSampleSize); ReFitProtected(); } @@ -832,6 +815,8 @@ private void OnWrite(N node) private void OnWriteWeighted(N node) { int weight = this.policy.GetWeight(node); + long max = WeightedCapacity.Maximum; + long windowMax = WeightedCapacity.WindowMaximum; switch (node.Position) { @@ -843,12 +828,12 @@ private void OnWriteWeighted(N node) this.weightedSize += weight; this.windowWeightedSize += weight; - if (weight > this.maximum) + if (weight > max) { this.windowLru.AddLast(node); Evict(node, ItemRemovedReason.Evicted); } - else if (weight > this.windowMaximum) + else if (weight > windowMax) { // too big for the window, place at the LRU position so it leaves next this.windowLru.AddFirst(node); @@ -864,11 +849,11 @@ private void OnWriteWeighted(N node) ApplyWeightDelta(node, weight, Position.Window); this.metrics.updatedCount++; - if (weight > this.maximum) + if (weight > max) { Evict(node, ItemRemovedReason.Evicted); } - else if (weight <= this.windowMaximum) + else if (weight <= windowMax) { this.windowLru.MoveToEnd(node); } @@ -883,7 +868,7 @@ private void OnWriteWeighted(N node) ApplyWeightDelta(node, weight, Position.Probation); this.metrics.updatedCount++; - if (weight <= this.maximum) + if (weight <= max) { PromoteProbationWeighted(node); } @@ -897,7 +882,7 @@ private void OnWriteWeighted(N node) ApplyWeightDelta(node, weight, Position.Protected); this.metrics.updatedCount++; - if (weight <= this.maximum) + if (weight <= max) { this.protectedLru.MoveToEnd(node); } @@ -953,7 +938,7 @@ private void PromoteProbation(LfuNode node) node.Position = Position.Protected; // If the protected space exceeds its maximum, the LRU items are demoted to the probation space. - if (this.protectedLru.Count > this.capacity.Protected) + if (this.protectedLru.Count > CountCapacity.Protected) { var demoted = this.protectedLru.First; this.protectedLru.RemoveFirst(); @@ -966,9 +951,10 @@ private void PromoteProbation(LfuNode node) private void PromoteProbationWeighted(LfuNode node) { int pw = this.policy.GetPolicyWeight(node); + long mainProtectedMax = WeightedCapacity.MainProtectedMaximum; // An entry that cannot fit in the protected space is kept in probation at the MRU position. - if (pw > this.mainProtectedMaximum) + if (pw > mainProtectedMax) { this.probationLru.MoveToEnd(node); return; @@ -980,7 +966,7 @@ private void PromoteProbationWeighted(LfuNode node) this.mainProtectedWeightedSize += pw; // If the protected space exceeds its maximum weight, demote LRU items to probation. - while (this.mainProtectedWeightedSize > this.mainProtectedMaximum) + while (this.mainProtectedWeightedSize > mainProtectedMax) { var demoted = this.protectedLru.First; if (demoted == null) @@ -1012,8 +998,9 @@ private void EvictEntries(ItemRemovedReason reason) { LfuNode? first = null; var node = this.windowLru.First; + long windowMax = WeightedCapacity.WindowMaximum; - while (this.windowWeightedSize > this.windowMaximum) + while (this.windowWeightedSize > windowMax) { if (node == null) { @@ -1053,8 +1040,9 @@ private void EvictFromMainWeighted(LfuNode? candidateNode, ItemRemovedReas var victim = this.probationLru.First; // victims are LRU position in probation var candidate = candidateNode; + long max = WeightedCapacity.Maximum; - while (this.weightedSize > this.maximum) + while (this.weightedSize > max) { // [A] search the admission window for additional candidates if (candidate == null && candidateQueue == ProbationQueue) @@ -1137,7 +1125,7 @@ private void EvictFromMainWeighted(LfuNode? candidateNode, ItemRemovedReas } // [H] evict immediately if the candidate's weight exceeds the maximum - if (this.policy.GetPolicyWeight(candidate) > this.maximum) + if (this.policy.GetPolicyWeight(candidate) > max) { var evict = candidate; candidate = candidate.Next; @@ -1174,7 +1162,7 @@ private bool AdmitCandidateWeighted(K candidateKey, K victimKey) // The maximum frequency is 15 and halved to 7 on reset to age. A candidate with a moderate // frequency is given a small chance to be admitted to defend against hash flooding. - if (candidateFreq >= AdmitHashDosThreshold) + if (candidateFreq >= WeightedLfuCapacityPartition.AdmitHashDosThreshold) { return (this.random!.Next() & 127) == 0; } @@ -1182,10 +1170,12 @@ private bool AdmitCandidateWeighted(K candidateKey, K victimKey) return false; } - private void ReFitProtectedWeighted() + internal void ReFitProtectedWeighted() { + long mainProtectedMax = WeightedCapacity.MainProtectedMaximum; + // If hill climbing decreased the protected maximum, demote overflow to probation. - while (this.mainProtectedWeightedSize > this.mainProtectedMaximum) + while (this.mainProtectedWeightedSize > mainProtectedMax) { var demoted = this.protectedLru.First; if (demoted == null) @@ -1201,157 +1191,11 @@ private void ReFitProtectedWeighted() } } - // Adapt the window and main space sizes (in weight units) using a hill climbing algorithm to - // iteratively improve hit rate. A larger window favors recency, a larger main favors frequency. - private void OptimizeWeightedPartitioning() - { - long adjustment = DetermineWeightedAdjustment(); - - if (adjustment > 0) - { - IncreaseWindow(adjustment); - } - else if (adjustment < 0) - { - DecreaseWindow(-adjustment); - } - } - - private long DetermineWeightedAdjustment() - { - long newHits = this.metrics.Hits; - long newMisses = this.metrics.Misses; - - long sampleHits = newHits - this.previousHitCount; - long sampleMisses = newMisses - this.previousMissCount; - long requestCount = sampleHits + sampleMisses; - - if (requestCount < this.cmSketch.ResetSampleSize) - { - return 0; - } - - double hitRate = (double)sampleHits / requestCount; - double hitRateChange = hitRate - this.previousHitRate; - double amount = (hitRateChange >= 0) ? this.stepSize : -this.stepSize; - double nextStepSize = (Math.Abs(hitRateChange) >= HillClimberRestartThreshold) - ? CopySign(Math.Max(HillClimberStepPercent * this.maximum, HillClimberMinStep), amount) - : HillClimberStepDecay * amount; - - this.previousHitRate = hitRate; - this.previousHitCount = newHits; - this.previousMissCount = newMisses; - this.stepSize = nextStepSize; - - return (long)amount; - } - - private void IncreaseWindow(long adjustment) - { - if (this.mainProtectedMaximum == 0) - { - return; - } - - long quota = Math.Min(adjustment, this.mainProtectedMaximum); - this.mainProtectedMaximum -= quota; - this.windowMaximum += quota; - - ReFitProtectedWeighted(); - - for (int i = 0; i < QueueTransferThreshold; i++) - { - var candidate = this.probationLru.First; - bool probation = true; - - if (candidate == null || quota < this.policy.GetPolicyWeight(candidate)) - { - candidate = this.protectedLru.First; - probation = false; - } - - if (candidate == null) - { - break; - } - - int weight = this.policy.GetPolicyWeight(candidate); - if (quota < weight) - { - break; - } - - quota -= weight; - - if (probation) - { - this.probationLru.Remove(candidate); - } - else - { - this.mainProtectedWeightedSize -= weight; - this.protectedLru.Remove(candidate); - } - - this.windowWeightedSize += weight; - this.windowLru.AddLast(candidate); - candidate.Position = Position.Window; - } - - // return unused quota - this.mainProtectedMaximum += quota; - this.windowMaximum -= quota; - } - - private void DecreaseWindow(long adjustment) - { - if (this.windowMaximum <= 1) - { - return; - } - - long quota = Math.Min(adjustment, Math.Max(0, this.windowMaximum - 1)); - this.mainProtectedMaximum += quota; - this.windowMaximum -= quota; - - for (int i = 0; i < QueueTransferThreshold; i++) - { - var candidate = this.windowLru.First; - if (candidate == null) - { - break; - } - - int weight = this.policy.GetPolicyWeight(candidate); - if (quota < weight) - { - break; - } - - quota -= weight; - - this.windowWeightedSize -= weight; - this.windowLru.Remove(candidate); - this.probationLru.AddLast(candidate); - candidate.Position = Position.Probation; - } - - // return unused quota - this.mainProtectedMaximum -= quota; - this.windowMaximum += quota; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double CopySign(double magnitude, double sign) - { - return (sign < 0) ? -Math.Abs(magnitude) : Math.Abs(magnitude); - } - private LfuNode? EvictFromWindow() { LfuNode? first = null; - while (this.windowLru.Count > this.capacity.Window) + while (this.windowLru.Count > CountCapacity.Window) { var node = this.windowLru.First; this.windowLru.RemoveFirst(); @@ -1505,7 +1349,7 @@ private void ReFitProtected() { // If hill climbing decreased protected, there may be too many items // - demote overflow to probation. - while (this.protectedLru.Count > this.capacity.Protected) + while (this.protectedLru.Count > CountCapacity.Protected) { var demoted = this.protectedLru.First; this.protectedLru.RemoveFirst(); diff --git a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs index e89e883a..31948789 100644 --- a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs +++ b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs @@ -4,16 +4,16 @@ namespace BitFaster.Caching.Lfu { /// - /// Represents the LFU capacity partition. Uses a hill climbing algorithm to optimze partition sizes over time. + /// Represents the count-based LFU capacity partition and provides the common hill climbing state + /// used by LFU capacity partitions. /// [DebuggerDisplay("{Capacity} ({Window}/{Protected}/{Probation})")] - public sealed class LfuCapacityPartition + public class LfuCapacityPartition { private readonly int max; - private int windowCapacity; - private int protectedCapacity; - private int probationCapacity; + private protected long windowMaximum; + private protected long mainProtectedMaximum; private double previousHitRate; private long previousHitCount; @@ -23,10 +23,11 @@ public sealed class LfuCapacityPartition private double stepSize; private const double HillClimberRestartThreshold = 0.05d; - private const double HillClimberStepPercent = 0.0625d; + private protected const double HillClimberStepPercent = 0.0625d; private const double HillClimberStepDecayRate = 0.98d; private const double DefaultMainPercentage = 0.99d; + private const double MainProtectedPercentage = 0.8d; private const double MaxMainPercentage = 0.999d; private const double MinMainPercentage = 0.2d; @@ -36,28 +37,32 @@ public sealed class LfuCapacityPartition /// /// The total capacity. public LfuCapacityPartition(int totalCapacity) + : this(ValidateCapacity(totalCapacity), HillClimberStepPercent) { - this.max = totalCapacity; - (windowCapacity, protectedCapacity, probationCapacity) = ComputeQueueCapacity(totalCapacity, DefaultMainPercentage); - InitializeStepSize(); + } - previousHitRate = 1.0; + private protected LfuCapacityPartition(int totalCapacity, double initialStepSize) + { + this.max = totalCapacity; + this.stepSize = initialStepSize; + this.previousHitRate = 1.0d; + SetMaximums(DefaultMainPercentage); } /// /// Gets the number of items permitted in the window LRU. /// - public int Window => this.windowCapacity; + public int Window => (int)this.windowMaximum; /// /// Gets the number of items permitted in the protected LRU. /// - public int Protected => this.protectedCapacity; + public int Protected => (int)this.mainProtectedMaximum; /// /// Gets the number of items permitted in the probation LRU. /// - public int Probation => this.probationCapacity; + public int Probation => this.max - this.Window - this.Protected; /// /// Gets the total capacity. @@ -74,43 +79,53 @@ public LfuCapacityPartition(int totalCapacity) /// window = recency-biased, main = frequency-biased. /// public void OptimizePartitioning(ICacheMetrics metrics, int sampleThreshold) + { + if (!TryGetAdjustment(metrics, sampleThreshold, HillClimberStepPercent, out double adjustment)) + { + return; + } + + this.mainRatio = Clamp(this.mainRatio - adjustment, MinMainPercentage, MaxMainPercentage); + SetMaximums(this.mainRatio); + } + + private protected bool TryGetAdjustment(ICacheMetrics metrics, int sampleThreshold, double restartStepSize, out double adjustment) { long newHits = metrics.Hits; long newMisses = metrics.Misses; - long sampleHits = newHits - previousHitCount; - long sampleMisses = newMisses - previousMissCount; + long sampleHits = newHits - this.previousHitCount; + long sampleMisses = newMisses - this.previousMissCount; long sampleCount = sampleHits + sampleMisses; if (sampleCount < sampleThreshold) { - return; + adjustment = 0; + return false; } double sampleHitRate = (double)sampleHits / sampleCount; - double hitRateChange = sampleHitRate - previousHitRate; - double amount = (hitRateChange >= 0) ? stepSize : -stepSize; + double hitRateChange = sampleHitRate - this.previousHitRate; + adjustment = (hitRateChange >= 0) ? this.stepSize : -this.stepSize; double nextStepSize = (Math.Abs(hitRateChange) >= HillClimberRestartThreshold) - ? HillClimberStepPercent * (amount >= 0 ? 1 : -1) - : HillClimberStepDecayRate * amount; - - stepSize = nextStepSize; + ? CopySign(restartStepSize, adjustment) + : HillClimberStepDecayRate * adjustment; - previousHitCount = newHits; - previousMissCount = newMisses; - previousHitRate = sampleHitRate; + this.stepSize = nextStepSize; - mainRatio -= amount; - mainRatio = Clamp(mainRatio, MinMainPercentage, MaxMainPercentage); + this.previousHitCount = newHits; + this.previousMissCount = newMisses; + this.previousHitRate = sampleHitRate; - (windowCapacity, protectedCapacity, probationCapacity) = ComputeQueueCapacity(max, mainRatio); + return true; } - private void InitializeStepSize() + private void SetMaximums(double mainPercentage) { - stepSize = HillClimberStepPercent; + this.windowMaximum = this.max - (long)(mainPercentage * this.max); + this.mainProtectedMaximum = (long)(MainProtectedPercentage * (this.max - this.windowMaximum)); } private static double Clamp(double input, double min, double max) @@ -118,16 +133,17 @@ private static double Clamp(double input, double min, double max) return Math.Max(min, Math.Min(input, max)); } - private static (int window, int mainProtected, int mainProbation) ComputeQueueCapacity(int capacity, double mainPercentage) + private static double CopySign(double magnitude, double sign) + { + return (sign < 0) ? -Math.Abs(magnitude) : Math.Abs(magnitude); + } + + private static int ValidateCapacity(int capacity) { if (capacity < 3) Throw.ArgOutOfRange(nameof(capacity), "Capacity must be greater than or equal to 3."); - int window = capacity - (int)(mainPercentage * capacity); - int mainProtected = (int)(0.8 * (capacity - window)); - int mainProbation = capacity - window - mainProtected; - - return (window, mainProtected, mainProbation); + return capacity; } } } diff --git a/BitFaster.Caching/Lfu/WeightedLfuCapacityPartition.cs b/BitFaster.Caching/Lfu/WeightedLfuCapacityPartition.cs new file mode 100644 index 00000000..e555c96d --- /dev/null +++ b/BitFaster.Caching/Lfu/WeightedLfuCapacityPartition.cs @@ -0,0 +1,182 @@ +using System; + +namespace BitFaster.Caching.Lfu +{ + /// + /// Represents the weighted LFU capacity partition. Holds the window/main weighted maximums and uses + /// a hill climbing algorithm to optimize the partition sizes (in weight units) over time. + /// + internal sealed class WeightedLfuCapacityPartition : LfuCapacityPartition + { + // Weighted eviction tuning, matching Caffeine. + internal const int AdmitHashDosThreshold = 6; + private const double HillClimberMinStep = 2.0d; + private const long SmallCacheThreshold = 512; + private const int QueueTransferThreshold = 1000; + + /// + /// Initializes a new instance of the WeightedLfuCapacityPartition class with the specified total weight capacity. + /// + /// The total weight capacity. + public WeightedLfuCapacityPartition(int totalCapacity) + : base(totalCapacity, GetInitialStepSize(totalCapacity)) + { + } + + /// + /// Gets the maximum total weight. + /// + public long Maximum => this.Capacity; + + /// + /// Gets the maximum weight permitted in the window. + /// + public long WindowMaximum => this.windowMaximum; + + /// + /// Gets the maximum weight permitted in the protected space. + /// + public long MainProtectedMaximum => this.mainProtectedMaximum; + + /// + /// Adapt the window and main space sizes (in weight units) using a hill climbing algorithm to + /// iteratively improve hit rate. A larger window favors recency, a larger main favors frequency. + /// + public void OptimizePartitioning(ref ConcurrentLfuCore cache, ICacheMetrics metrics, int sampleThreshold) + where K : notnull + where N : LfuNode + where P : struct, INodePolicy + where E : struct, IEventPolicy + { + if (!TryGetAdjustment(metrics, sampleThreshold, GetStepSize(this.Capacity), out double amount)) + { + return; + } + + long adjustment = (long)amount; + + if (adjustment > 0) + { + IncreaseWindow(ref cache, adjustment); + } + else if (adjustment < 0) + { + DecreaseWindow(ref cache, -adjustment); + } + } + + private void IncreaseWindow(ref ConcurrentLfuCore cache, long adjustment) + where K : notnull + where N : LfuNode + where P : struct, INodePolicy + where E : struct, IEventPolicy + { + if (this.mainProtectedMaximum == 0) + { + return; + } + + long quota = Math.Min(adjustment, this.mainProtectedMaximum); + this.mainProtectedMaximum -= quota; + this.windowMaximum += quota; + + cache.ReFitProtectedWeighted(); + + for (int i = 0; i < QueueTransferThreshold; i++) + { + var candidate = cache.probationLru.First; + bool probation = true; + + if (candidate == null || quota < cache.policy.GetPolicyWeight(candidate)) + { + candidate = cache.protectedLru.First; + probation = false; + } + + if (candidate == null) + { + break; + } + + int weight = cache.policy.GetPolicyWeight(candidate); + if (quota < weight) + { + break; + } + + quota -= weight; + + if (probation) + { + cache.probationLru.Remove(candidate); + } + else + { + cache.mainProtectedWeightedSize -= weight; + cache.protectedLru.Remove(candidate); + } + + cache.windowWeightedSize += weight; + cache.windowLru.AddLast(candidate); + candidate.Position = Position.Window; + } + + // return unused quota + this.mainProtectedMaximum += quota; + this.windowMaximum -= quota; + } + + private void DecreaseWindow(ref ConcurrentLfuCore cache, long adjustment) + where K : notnull + where N : LfuNode + where P : struct, INodePolicy + where E : struct, IEventPolicy + { + if (this.windowMaximum <= 1) + { + return; + } + + long quota = Math.Min(adjustment, Math.Max(0, this.windowMaximum - 1)); + this.mainProtectedMaximum += quota; + this.windowMaximum -= quota; + + for (int i = 0; i < QueueTransferThreshold; i++) + { + var candidate = cache.windowLru.First; + if (candidate == null) + { + break; + } + + int weight = cache.policy.GetPolicyWeight(candidate); + if (quota < weight) + { + break; + } + + quota -= weight; + + cache.windowWeightedSize -= weight; + cache.windowLru.Remove(candidate); + cache.probationLru.AddLast(candidate); + candidate.Position = Position.Probation; + } + + // return unused quota + this.mainProtectedMaximum -= quota; + this.windowMaximum += quota; + } + + private static double GetInitialStepSize(int totalCapacity) + { + double initialStep = GetStepSize(totalCapacity); + return (totalCapacity <= SmallCacheThreshold) ? initialStep : -initialStep; + } + + private static double GetStepSize(int totalCapacity) + { + return Math.Max(HillClimberStepPercent * totalCapacity, HillClimberMinStep); + } + } +}