From 4c114bb22e4d102b50d61c19a7cd26fa980cbc68 Mon Sep 17 00:00:00 2001 From: Alex Peck Date: Fri, 26 Jun 2026 11:41:01 -0700 Subject: [PATCH] Extract weighted partitioning into WeightedLfuCapacityPartition Move the weighted hill-climb (OptimizeWeightedPartitioning + Determine/Increase/Decrease/CopySign), the window/main weighted maximums, the climb state, and all weighted tuning consts out of ConcurrentLfuCore into a new WeightedLfuCapacityPartition, mirroring LfuCapacityPartition. Both implement a new internal ICapacityPartition. The climb reaches back into the core via a generic OptimizePartitioning(ref core, ...) method, matching the existing INodePolicy.ExpireEntries(ref core) pattern; weighted sizes remain in the core as accounting. Behavior-preserving: full suite green (1579). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Lfu/WeightedLfuCapacityPartitionTests.cs | 36 +++ BitFaster.Caching/Lfu/ConcurrentLfuCore.cs | 244 ++++-------------- BitFaster.Caching/Lfu/ICapacityPartition.cs | 13 + BitFaster.Caching/Lfu/LfuCapacityPartition.cs | 2 +- .../Lfu/WeightedLfuCapacityPartition.cs | 229 ++++++++++++++++ 5 files changed, 323 insertions(+), 201 deletions(-) create mode 100644 BitFaster.Caching.UnitTests/Lfu/WeightedLfuCapacityPartitionTests.cs create mode 100644 BitFaster.Caching/Lfu/ICapacityPartition.cs create mode 100644 BitFaster.Caching/Lfu/WeightedLfuCapacityPartition.cs 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..34246a3b 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; + private readonly ICapacityPartition 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 window/main weighted maximums and the hill climb live in WeightedLfuCapacityPartition; + // the sizes below are runtime accounting and stay in the core (the 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 => (LfuCapacityPartition)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/ICapacityPartition.cs b/BitFaster.Caching/Lfu/ICapacityPartition.cs new file mode 100644 index 00000000..088d92bd --- /dev/null +++ b/BitFaster.Caching/Lfu/ICapacityPartition.cs @@ -0,0 +1,13 @@ +namespace BitFaster.Caching.Lfu +{ + /// + /// Represents a capacity partition scheme for the internal window/main queues used by the LFU. + /// + internal interface ICapacityPartition + { + /// + /// Gets the total capacity. + /// + int Capacity { get; } + } +} diff --git a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs index e89e883a..0e7c33f3 100644 --- a/BitFaster.Caching/Lfu/LfuCapacityPartition.cs +++ b/BitFaster.Caching/Lfu/LfuCapacityPartition.cs @@ -7,7 +7,7 @@ namespace BitFaster.Caching.Lfu /// Represents the LFU capacity partition. Uses a hill climbing algorithm to optimze partition sizes over time. /// [DebuggerDisplay("{Capacity} ({Window}/{Protected}/{Probation})")] - public sealed class LfuCapacityPartition + public sealed class LfuCapacityPartition : ICapacityPartition { private readonly int max; diff --git a/BitFaster.Caching/Lfu/WeightedLfuCapacityPartition.cs b/BitFaster.Caching/Lfu/WeightedLfuCapacityPartition.cs new file mode 100644 index 00000000..60ea173f --- /dev/null +++ b/BitFaster.Caching/Lfu/WeightedLfuCapacityPartition.cs @@ -0,0 +1,229 @@ +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 : ICapacityPartition + { + private readonly int max; + + private long maximum; + private long windowMaximum; + private long mainProtectedMaximum; + + private double stepSize; + private double previousHitRate; + private long previousHitCount; + private long previousMissCount; + + // Weighted eviction tuning, matching Caffeine. + private const double MainPercentage = 0.99d; + private const double MainProtectedPercentage = 0.8d; + internal 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; + + /// + /// Initializes a new instance of the WeightedLfuCapacityPartition class with the specified total weight capacity. + /// + /// The total weight capacity. + public WeightedLfuCapacityPartition(int totalCapacity) + { + this.max = totalCapacity; + + // Mirror Caffeine's initial split: window ~1% of total weight, protected ~80% of main. + this.maximum = totalCapacity; + 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; + } + + /// + /// Gets the total weight capacity. + /// + public int Capacity => this.max; + + /// + /// Gets the maximum total weight. + /// + public long Maximum => this.maximum; + + /// + /// 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 + { + long adjustment = DetermineWeightedAdjustment(metrics, sampleThreshold); + + if (adjustment > 0) + { + IncreaseWindow(ref cache, adjustment); + } + else if (adjustment < 0) + { + DecreaseWindow(ref cache, -adjustment); + } + } + + private long DetermineWeightedAdjustment(ICacheMetrics metrics, int sampleThreshold) + { + long newHits = metrics.Hits; + long newMisses = metrics.Misses; + + long sampleHits = newHits - this.previousHitCount; + long sampleMisses = newMisses - this.previousMissCount; + long requestCount = sampleHits + sampleMisses; + + if (requestCount < sampleThreshold) + { + 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(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 CopySign(double magnitude, double sign) + { + return (sign < 0) ? -Math.Abs(magnitude) : Math.Abs(magnitude); + } + } +}