public void ForceChange(int newThreadCount, HillClimbingStateTransition transition) { if (newThreadCount != m_lastThreadCount) { m_currentControlSetting += (newThreadCount - m_lastThreadCount); ChangeThreadCount(newThreadCount, transition); } }
private void ChangeThreadCount(int newThreadCount, HillClimbingStateTransition transition) { m_lastThreadCount = newThreadCount; m_currentSampleInterval = (uint)m_randomIntervalGenerator.Next(m_options.SampleIntervalLow, m_options.SampleIntervalHigh + 1); double throughput = (m_elapsedSinceLastChange > 0) ? (m_completionsSinceLastChange / m_elapsedSinceLastChange) : 0; m_options.FireEtwThreadPoolWorkerThreadAdjustmentAdjustment(newThreadCount, throughput, transition); m_elapsedSinceLastChange = 0; m_completionsSinceLastChange = 0; }
public int Update(int currentThreadCount, double sampleDuration, int numCompletions, out int pNewSampleInterval) { // // If someone changed the thread count without telling us, update our records accordingly. // if (currentThreadCount != m_lastThreadCount) { ForceChange(currentThreadCount, HillClimbingStateTransition.Initializing); } // // Update the cumulative stats for this thread count // m_elapsedSinceLastChange += sampleDuration; m_completionsSinceLastChange += numCompletions; // // Add in any data we've already collected about this sample // sampleDuration += m_accumulatedSampleDuration; numCompletions += m_accumulatedCompletionCount; // // We need to make sure we're collecting reasonably accurate data. Since we're just counting the end // of each work item, we are goinng to be missing some data about what really happened during the // sample interval. The count produced by each thread includes an initial work item that may have // started well before the start of the interval, and each thread may have been running some new // work item for some time before the end of the interval, which did not yet get counted. So // our count is going to be off by +/- threadCount workitems. // // The exception is that the thread that reported to us last time definitely wasn't running any work // at that time, and the thread that's reporting now definitely isn't running a work item now. So // we really only need to consider threadCount-1 threads. // // Thus the percent error in our count is +/- (threadCount-1)/numCompletions. // // We cannot rely on the frequency-domain analysis we'll be doing later to filter out this error, because // of the way it accumulates over time. If this sample is off by, say, 33% in the negative direction, // then the next one likely will be too. The one after that will include the sum of the completions // we missed in the previous samples, and so will be 33% positive. So every three samples we'll have // two "low" samples and one "high" sample. This will appear as periodic variation right in the frequency // range we're targeting, which will not be filtered by the frequency-domain translation. // if (m_totalSamples > 0 && ((currentThreadCount - 1.0) / numCompletions) >= m_options.MaxSampleError) { // not accurate enough yet. Let's accumulate the data so far, and tell the ThreadPool // to collect a little more. m_accumulatedSampleDuration = sampleDuration; m_accumulatedCompletionCount = numCompletions; pNewSampleInterval = 10; return(currentThreadCount); } // // We've got enouugh data for our sample; reset our accumulators for next time. // m_accumulatedSampleDuration = 0; m_accumulatedCompletionCount = 0; // // Add the current thread count and throughput sample to our history // double throughput = (double)numCompletions / sampleDuration; m_options.FireEtwThreadPoolWorkerThreadAdjustmentSample(throughput); int sampleIndex = (int)(m_totalSamples % m_options.SamplesToMeasure); m_samples[sampleIndex] = throughput; m_threadCounts[sampleIndex] = currentThreadCount; m_totalSamples++; // // Set up defaults for our metrics // Complex threadWaveComponent = 0; Complex throughputWaveComponent = 0; double throughputErrorEstimate = 0; Complex ratio = 0; double confidence = 0; HillClimbingStateTransition transition = HillClimbingStateTransition.Warmup; // // How many samples will we use? It must be at least the three wave periods we're looking for, and it must also be a whole // multiple of the primary wave's period; otherwise the frequency we're looking for will fall between two frequency bands // in the Fourier analysis, and we won't be able to measure it accurately. // int sampleCount = ((int)Math.Min(m_totalSamples - 1, m_options.SamplesToMeasure) / m_options.WavePeriod) * m_options.WavePeriod; if (sampleCount > m_options.WavePeriod) { // // Average the throughput and thread count samples, so we can scale the wave magnitudes later. // double sampleSum = 0; double threadSum = 0; for (int i = 0; i < sampleCount; i++) { sampleSum += m_samples[(m_totalSamples - sampleCount + i) % m_options.SamplesToMeasure]; threadSum += m_threadCounts[(m_totalSamples - sampleCount + i) % m_options.SamplesToMeasure]; } double averageThroughput = sampleSum / sampleCount; double averageThreadCount = threadSum / sampleCount; if (averageThroughput > 0 && averageThreadCount > 0) { // // Calculate the periods of the adjacent frequency bands we'll be using to measure noise levels. // We want the two adjacent Fourier frequency bands. // double adjacentPeriod1 = sampleCount / (((double)sampleCount / (double)m_options.WavePeriod) + 1); double adjacentPeriod2 = sampleCount / (((double)sampleCount / (double)m_options.WavePeriod) - 1); // // Get the the three different frequency components of the throughput (scaled by average // throughput). Our "error" estimate (the amount of noise that might be present in the // frequency band we're really interested in) is the average of the adjacent bands. // throughputWaveComponent = GetWaveComponent(m_samples, sampleCount, m_options.WavePeriod) / averageThroughput; throughputErrorEstimate = Complex.Abs(GetWaveComponent(m_samples, sampleCount, adjacentPeriod1) / averageThroughput); if (adjacentPeriod2 <= sampleCount) { throughputErrorEstimate = Math.Max(throughputErrorEstimate, Complex.Abs(GetWaveComponent(m_samples, sampleCount, adjacentPeriod2) / averageThroughput)); } // // Do the same for the thread counts, so we have something to compare to. We don't measure thread count // noise, because there is none; these are exact measurements. // threadWaveComponent = GetWaveComponent(m_threadCounts, sampleCount, m_options.WavePeriod) / averageThreadCount; // // Update our moving average of the throughput noise. We'll use this later as feedback to // determine the new size of the thread wave. // if (m_averageThroughputNoise == 0) { m_averageThroughputNoise = throughputErrorEstimate; } else { m_averageThroughputNoise = (m_options.ThroughputErrorSmoothingFactor * throughputErrorEstimate) + ((1.0 - m_options.ThroughputErrorSmoothingFactor) * m_averageThroughputNoise); } if (Complex.Abs(threadWaveComponent) > 0) { // // Adjust the throughput wave so it's centered around the target wave, and then calculate the adjusted throughput/thread ratio. // ratio = (throughputWaveComponent - (m_options.TargetThroughputRatio * threadWaveComponent)) / threadWaveComponent; transition = HillClimbingStateTransition.ClimbingMove; } else { ratio = 0; transition = HillClimbingStateTransition.Stabilizing; } // // Calculate how confident we are in the ratio. More noise == less confident. This has // the effect of slowing down movements that might be affected by random noise. // double noiseForConfidence = Math.Max(m_averageThroughputNoise, throughputErrorEstimate); if (noiseForConfidence > 0) { confidence = (Complex.Abs(threadWaveComponent) / noiseForConfidence) / m_options.TargetSignalToNoiseRatio; } else { confidence = 1.0; //there is no noise! } } } // // We use just the real part of the complex ratio we just calculated. If the throughput signal // is exactly in phase with the thread signal, this will be the same as taking the magnitude of // the complex move and moving that far up. If they're 180 degrees out of phase, we'll move // backward (because this indicates that our changes are having the opposite of the intended effect). // If they're 90 degrees out of phase, we won't move at all, because we can't tell wether we're // having a negative or positive effect on throughput. // double move = Math.Min(1.0, Math.Max(-1.0, ratio.Real)); // // Apply our confidence multiplier. // move *= Math.Min(1.0, Math.Max(0.0, confidence)); // // Now apply non-linear gain, such that values around zero are attenuated, while higher values // are enhanced. This allows us to move quickly if we're far away from the target, but more slowly // if we're getting close, giving us rapid ramp-up without wild oscillations around the target. // double gain = m_options.MaxChangePerSecond * sampleDuration; move = Math.Pow(Math.Abs(move), m_options.GainExponent) * (move >= 0.0 ? 1 : -1) * gain; move = Math.Min(move, m_options.MaxChangePerSample); // // If the result was positive, and CPU is > 95%, refuse the move. // if (move > 0.0 && m_options.CurrentCpuUtilization > CpuUtilizationHigh) { move = 0.0; } // // Apply the move to our control setting // m_currentControlSetting += move; // // Calculate the new thread wave magnitude, which is based on the moving average we've been keeping of // the throughput error. This average starts at zero, so we'll start with a nice safe little wave at first. // int newThreadWaveMagnitude = (int)(0.5 + (m_currentControlSetting * m_averageThroughputNoise * m_options.TargetSignalToNoiseRatio * m_options.ThreadMagnitudeMultiplier * 2.0)); newThreadWaveMagnitude = Math.Min(newThreadWaveMagnitude, m_options.MaxThreadWaveMagnitude); newThreadWaveMagnitude = Math.Max(newThreadWaveMagnitude, 1); // // Make sure our control setting is within the ThreadPool's limits // m_currentControlSetting = Math.Min(m_options.MaxLimitThreads - newThreadWaveMagnitude, m_currentControlSetting); m_currentControlSetting = Math.Max(m_options.MinLimitThreads, m_currentControlSetting); // // Calculate the new thread count (control setting + square wave) // int newThreadCount = (int)(m_currentControlSetting + newThreadWaveMagnitude * ((m_totalSamples / (m_options.WavePeriod / 2)) % 2)); // // Make sure the new thread count doesn't exceed the ThreadPool's limits // newThreadCount = Math.Min(m_options.MaxLimitThreads, newThreadCount); newThreadCount = Math.Max(m_options.MinLimitThreads, newThreadCount); // // Record these numbers for posterity // m_options.FireEtwThreadPoolWorkerThreadAdjustmentStats( sampleDuration, throughput, threadWaveComponent.Real, throughputWaveComponent.Real, throughputErrorEstimate, m_averageThroughputNoise, ratio.Real, confidence, m_currentControlSetting, (ushort)newThreadWaveMagnitude); // // If all of this caused an actual change in thread count, log that as well. // if (newThreadCount != currentThreadCount) { ChangeThreadCount(newThreadCount, transition); } // // Return the new thread count and sample interval. This is randomized to prevent correlations with other periodic // changes in throughput. Among other things, this prevents us from getting confused by Hill Climbing instances // running in other processes. // // If we're at minThreads, and we seem to be hurting performance by going higher, we can't go any lower to fix this. So // we'll simply stay at minThreads much longer, and only occasionally try a higher value. // if (ratio.Real < 0.0 && newThreadCount == m_options.MinLimitThreads) { pNewSampleInterval = (int)(0.5 + m_currentSampleInterval * (10.0 * Math.Max(-ratio.Real, 1.0))); } else { pNewSampleInterval = (int)m_currentSampleInterval; } return(newThreadCount); }
internal void FireEtwThreadPoolWorkerThreadAdjustmentAdjustment(int newThreadCount, double throughput, HillClimbingStateTransition transition) { }