private static void doShrinkCycle() { var baselines = CalculateBaselines(); // sort all caches that have existed and not been grown for a full shrink cycle by score ascending // score = (1 + impact/baselineImpact) / (1 + cost/baselineCost) // calculate forever and this cycle and take the higher (better) answer var shrinkCandidates = (from hitInfo in Nrdo.GetCacheHitInfoUnsorted() where hitInfo.Cache.IsEnabled && hitInfo.lastShrinkCycleStats != null && (hitInfo.lastGrownStats == null || hitInfo.lastGrownStats.LatestGlobalStats.LatestOperationStamp < hitInfo.lastShrinkCycleStats.LatestGlobalStats.LatestOperationStamp) let stats = hitInfo.CacheStats.ToNow() let thisCycle = stats.Since(hitInfo.lastShrinkCycleStats) let sinceLastShrink = stats.Since(hitInfo.lastShrunkStats) let globalScore = (1 + Portion.SafeRatio(stats.Impact, baselines.Baseline.Impact)) / (1 + sinceLastShrink.CumulativeCost / baselines.Baseline.Cost) let cycleScore = (1 + Portion.SafeRatio(thisCycle.Impact, baselines.CycleBaseline.Impact)) - (1 + thisCycle.CumulativeCost / baselines.CycleBaseline.Cost) let score = Portion.Max(globalScore, cycleScore) orderby score ascending select new { hitInfo, score }).ToList(); // shrink the first 1/4 of the caches in that list as long as their score is strictly < 100% shrinkCandidates = shrinkCandidates.Take(shrinkCandidates.Count / 4).Where(c => c.score < Portion.Complete).ToList(); foreach (var candidate in shrinkCandidates) { if (candidate.hitInfo.IsList) { var listCache = (IListCache)candidate.hitInfo.Cache; if (listCache.Capacity == 0 && listCache.ItemCapacity == 1) { listCache.Clear(); } else if (listCache.ItemCapacity == 0 && listCache.Capacity == 1) { listCache.ItemCapacity = (int)candidate.hitInfo.CacheStats.AverageResultItems + 1; listCache.Capacity = 0; } else { listCache.Capacity = listCache.Capacity * 3 / 4; listCache.ItemCapacity = listCache.ItemCapacity * 3 / 4; } } else { if (candidate.hitInfo.Cache.Capacity <= 1) { candidate.hitInfo.Cache.Clear(); } else { candidate.hitInfo.Cache.Capacity = candidate.hitInfo.Cache.Capacity * 3 / 4; } } candidate.hitInfo.shrinkCount++; candidate.hitInfo.lastShrunkStats = candidate.hitInfo.CacheStats.ToNow(); } // store last shrink cycle info for each individual cache lastShrinkCycleStats = globalStats.ToNow(); foreach (var hitInfo in Nrdo.GetCacheHitInfoUnsorted()) { hitInfo.lastShrinkCycleStats = hitInfo.CacheStats.ToNow(); } }
private static void doGrowPulse() { // - Grow caches where ImpactGainHybrid is "high" both as a percentage of total DB time and in // absolute value as measured over the period *since this cache was last rebalanced*. // Algorithm: // - Take all caches where ImpactGainHybrid[since last grown] is greater than zero // - Sort by ImpactGainHybrid[since last grown]/Nrdo.TotalQueryTime[since last grown] // - Take all caches that existed at last cycle, haven't grown since then, and where ImpactGainHybrid[this cycle] is greater than zero // (using this pulse would be better but we don't have this cache's stats for that) // - Sort by ImpactGainHybrid[this cycle]/Nrdo.TotalQueryTime[this cycle] // - Grow any caches that are in the top 50% of either list and also have ImpactGainHybrid[since last grown] > CacheGrowThreshold var growCandidates = (from hitInfo in Nrdo.GetCacheHitInfoUnsorted() let stats = hitInfo.CacheStats.ToNow() let sinceGrow = stats.Since(hitInfo.lastGrownStats) let gainSinceGrow = sinceGrow.PotentialImpactGainHybrid where gainSinceGrow > TimeSpan.Zero let ratioSinceGrow = Portion.SafeRatio(gainSinceGrow, sinceGrow.LatestGlobalStats.TotalQueryTime) let thisCycle = hitInfo.lastShrinkCycleStats == null || (hitInfo.lastGrownStats != null && hitInfo.lastGrownStats.LatestGlobalStats.LatestOperationStamp >= hitInfo.lastShrinkCycleStats.LatestGlobalStats.LatestOperationStamp) ? null : stats.Since(hitInfo.lastShrinkCycleStats) let gainThisCycle = thisCycle == null ? TimeSpan.Zero : thisCycle.PotentialImpactGainHybrid let ratioThisCycle = thisCycle == null ? Portion.Zero : Portion.SafeRatio(gainThisCycle, thisCycle.LatestGlobalStats.TotalQueryTime) select new { hitInfo, stats, sinceGrow, gainSinceGrow, ratioSinceGrow, thisCycle, gainThisCycle, ratioThisCycle }).ToList(); if (!growCandidates.Any()) { return; } var sinceGrowRatioLimit = growCandidates.OrderBy(c => c.ratioSinceGrow).ElementAt(growCandidates.Count / 2).ratioSinceGrow; var cycleCandidates = growCandidates.Where(c => c.gainThisCycle > TimeSpan.Zero).ToList(); var cycleRatioLimit = cycleCandidates.Any() ? cycleCandidates.OrderBy(c => c.ratioThisCycle).ElementAt(cycleCandidates.Count / 2).ratioThisCycle : Portion.Complete; var growCaches = from cache in growCandidates where cache.gainSinceGrow > CacheGrowThreshold && (cache.ratioSinceGrow >= sinceGrowRatioLimit || (cache.gainThisCycle > TimeSpan.Zero && cache.ratioThisCycle >= cycleRatioLimit)) select cache; foreach (var candidate in growCaches) { // - Calculate GrowthFactor: 1 + Max(NonHitsOverCapacity / TotalQueries, 0.2) var growthFactor = 1 + Math.Max((double)candidate.sinceGrow.NonHitsOverCapacity / candidate.sinceGrow.TotalQueries, 0.2); // - For non-list caches, Capacity = Max(Capacity * GrowthFactor, Capacity + 2) if (!candidate.hitInfo.IsList) { var targetCapacity = Math.Max((int)(candidate.hitInfo.Cache.Capacity * growthFactor), candidate.hitInfo.Cache.Capacity + 2); candidate.hitInfo.Cache.Capacity = Math.Min(targetCapacity, Nrdo.MaxCacheCapacity); } else { var listSinceGrow = candidate.sinceGrow.ListStats; var listCache = (IListCache)candidate.hitInfo.Cache; // - For list caches, // - Calculate PotentialCost: Max(PeakItemCount, ItemCapacity, Capacity * WeightedResultItems) var potentialCost = Math.Max(Math.Max(listCache.ItemCapacity, listCache.PeakItemCount), listCache.Capacity * candidate.sinceGrow.WeightedResultItems); // - Calculate TargetCost = GrowthFactor * Max(PotentialCost, WeightedResultItems) var targetCost = Math.Min(growthFactor * Math.Max(potentialCost, candidate.hitInfo.CacheStats.WeightedResultItems), Nrdo.MaxCacheCapacity); // - Calculate TargetCapacity = Max(TargetCost / WeightedResultItems, Capacity + 1) var targetCapacity = Math.Max((int)(targetCost / candidate.hitInfo.CacheStats.WeightedResultItems), listCache.Capacity + 1); // - If ItemCapacity < TargetCapacity * AvgResultItems or (Capacity = 0 and Skipped = 0) if (listCache.ItemCapacity < targetCapacity * candidate.hitInfo.CacheStats.AverageResultItems || (listCache.Capacity == 0 && listSinceGrow.Skipped == 0)) { // - Set ItemCapacity = Max(TargetCost, ItemCapacity + 2) listCache.ItemCapacity = Math.Max((int)targetCost, listCache.ItemCapacity + 2); } else { // - Set Capacity = TargetCapacity and ItemCapacity = TargetCapacity * AvgResultItems listCache.Capacity = targetCapacity; listCache.ItemCapacity = (int)(targetCapacity * candidate.hitInfo.CacheStats.AverageResultItems); } } candidate.hitInfo.growCount++; candidate.hitInfo.lastGrownStats = candidate.hitInfo.CacheStats.ToNow(); } }