/// <summary> /// This method is the meat of the lock-free aggregation logic. /// </summary> /// <param name="metricValue">Already filtered and conveted value to be tracked. /// We know that the value is not Double.NaN and not null and it passed trought any filters.</param> private void TrackFilteredConvertedValue(TBufferedValue metricValue) { // Get reference to the current buffer: MetricValuesBufferBase <TBufferedValue> buffer = _metricValuesBuffer; // Get the index at which to store metricValue into the buffer: int index = buffer.IncWriteIndex(); // Check to see whether we are past the end of the buffer. // If we are, it means that some *other* thread hit exactly the end (wrote the last value that fits into the buffer) and is currently flushing. // If we are, we will spin and wait. if (index >= buffer.Capacity) { #if DEBUG int startMillis = Environment.TickCount; #endif var spinWait = new SpinWait(); // It could be that the thread that was flushing is done and has updated the buffer pointer. // We refresh our local reference and see if we now have a valid index into the buffer. buffer = _metricValuesBuffer; index = buffer.IncWriteIndex(); while (index >= buffer.Capacity) { // Still not valid index into the buffer. Spin and try again. spinWait.SpinOnce(); #if DEBUG unchecked { Interlocked.Increment(ref s_countBufferWaitSpinCycles); } #endif // In tests (including stress tests) we always finished wating before 100 cycles. However, this is a protection // against en extreme case on a slow machine. We will back off and sleep for a few millisecs to give th emachine // a chance to finisah current tasks. if (spinWait.Count % 100 == 0) { Task.Delay(10).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter().GetResult(); } // Check to see whether the thread that was flushing is done and has updated the buffer pointer. // We refresh our local reference and see if we now have a valid index into the buffer. buffer = _metricValuesBuffer; index = buffer.IncWriteIndex(); } #if DEBUG unchecked { int periodMillis = Environment.TickCount - startMillis; int currentSpinMillis = s_timeBufferWaitSpinMillis; int prevSpinMillis = Interlocked.CompareExchange(ref s_timeBufferWaitSpinMillis, currentSpinMillis + periodMillis, currentSpinMillis); while (prevSpinMillis != currentSpinMillis) { currentSpinMillis = s_timeBufferWaitSpinMillis; prevSpinMillis = Interlocked.CompareExchange(ref s_timeBufferWaitSpinMillis, currentSpinMillis + periodMillis, currentSpinMillis); } Interlocked.Increment(ref s_countBufferWaitSpinEvents); } #endif } // Ok, so now we know that (0 <= index = buffer.Capacity). Write the value to the buffer: buffer.WriteValue(index, metricValue); // If this was the last value that fits into the buffer, we must flush the buffer: if (index == buffer.Capacity - 1) { // Before we begin flushing (which is can take time), we update the _metricValuesBuffer to a fresh buffer that is ready to take values. // That way threads do notneed to spin and wait until we flush and can begin writing values. // We try to recycle a previous buffer to lower stress on GC and to lower Gen-2 heap fragmentation. // The lifetime of an buffer can easily be a minute or so and then it can get into Gen-2 GC heap. // If we then, keep throwing such buffers away we can fragment the Gen-2 heap. To avoid this we employ // a simple form of best-effort object pooling. // Get buffer from pool and reset the pool: MetricValuesBufferBase <TBufferedValue> newBufer = Interlocked.Exchange(ref _metricValuesBufferRecycle, null); if (newBufer != null) { // If we were succesful in getting a recycled buffer from the pool, we will try to use it as the new buffer. // If we successfully the the recycled buffer to be the new buffer, we will reset it to prepare for data. // Otherwise we will just throw it away. MetricValuesBufferBase <TBufferedValue> prevBuffer = Interlocked.CompareExchange(ref _metricValuesBuffer, newBufer, buffer); if (prevBuffer == buffer) { newBufer.ResetIndices(); } } else { // If we were succesful in getting a recycled buffer from the pool, we will create a new one. newBufer = InvokeMetricValuesBufferFactory(); Interlocked.CompareExchange(ref _metricValuesBuffer, newBufer, buffer); } // Ok, now we have either set a new buffer that is ready to be used, or we have determined using CompareExchange // that another thread set a new buffer and we do not need to do it here. // Now we can actually flush the buffer: UpdateAggregate(buffer); // The buffer is now flushed. If the slot for the best-effor object pooling is free, use it: Interlocked.CompareExchange(ref _metricValuesBufferRecycle, buffer, null); } }