Files

248 lines
7.3 KiB
C#

using System;
using System.IO;
using System.Threading.Tasks;
namespace FluentTerminal.SystemTray
{
internal sealed class BufferedReader : IDisposable
{
private const int MaxTotalDelayMilliseconds = 100;
private const int WaitPeriodMilliseconds = 30;
private const int NearReadsPeriodMilliseconds = 20;
private const int NearReadsBufferingTrigger = 3;
private const int BufferSize = 204800;
private readonly object _lock = new object();
private readonly Stream _stream;
private readonly Action<byte[]> _callback;
private readonly bool _enableBuffer;
private bool _disposed;
private bool _paused;
private byte[] _buffer;
private int _bufferIndex;
private DateTime _lastRead;
private DateTime _sendingDeadline;
private DateTime _scheduledSend;
private int _nearReadingsCount;
private Task _sendingTask;
internal BufferedReader(Stream stream, Action<byte[]> callback, bool enableBuffer)
{
_stream = stream;
_callback = callback;
_enableBuffer = enableBuffer;
// ReSharper disable once AssignmentIsFullyDiscarded
_ = Task.Factory.StartNew(ReadingLoop, TaskCreationOptions.LongRunning);
}
internal void SetPaused(bool value)
{
lock (_lock) _paused = value;
}
public void Dispose()
{
lock (_lock) _disposed = true;
}
private async Task ReadingLoop()
{
while (true)
{
// Allow CPU to jump between TerminalSessions' ReadingLoop Tasks
await Task.Delay(1).ConfigureAwait(false);
bool paused;
lock (_lock)
{
if (_disposed) return;
paused = _paused;
}
if (paused)
{
await Task.Delay(WaitPeriodMilliseconds).ConfigureAwait(false);
continue;
}
var currentBuffer = new byte[BufferSize];
int read;
try
{
read = await _stream.ReadAsync(currentBuffer, 0, currentBuffer.Length).ConfigureAwait(false);
}
catch
{
read = 0;
}
if (read < 1)
{
// Expected to happen only when terminal is closed.
// Probably not recoverable, but we'll wait anyway 'till disposed.
await Task.Delay(50).ConfigureAwait(false);
continue;
}
if (!_enableBuffer)
{
// Buffering disabled. Just send.
_buffer = currentBuffer;
_bufferIndex = read;
SendBuffer();
continue;
}
var now = DateTime.UtcNow;
lock (_lock)
{
if (_buffer != null)
{
// We're already in buffered mode
if (_bufferIndex + read > BufferSize)
{
// No room in the buffer. Have to flush it.
SendBuffer();
_buffer = currentBuffer;
_bufferIndex = 0;
_scheduledSend = now.AddMilliseconds(WaitPeriodMilliseconds);
_sendingDeadline = now.AddMilliseconds(MaxTotalDelayMilliseconds);
}
else
{
// Copy to existing buffer
Buffer.BlockCopy(currentBuffer, 0, _buffer, _bufferIndex, read);
_bufferIndex += read;
_scheduledSend = now.AddMilliseconds(WaitPeriodMilliseconds);
}
if (now.Subtract(_lastRead).TotalMilliseconds < NearReadsPeriodMilliseconds)
{
// We should stop buffered mode
SendBuffer();
}
_lastRead = now;
continue;
}
if (now.Subtract(_lastRead).TotalMilliseconds < NearReadsPeriodMilliseconds)
{
_nearReadingsCount++;
}
else
{
_nearReadingsCount = 0;
}
_lastRead = now;
if (_nearReadingsCount >= NearReadsBufferingTrigger)
{
// We should enter buffered mode
_buffer = currentBuffer;
_bufferIndex = 0;
_sendingDeadline = now.AddMilliseconds(MaxTotalDelayMilliseconds);
_scheduledSend = now.AddMilliseconds(WaitPeriodMilliseconds);
if (_sendingTask == null) _sendingTask = SendAsync();
_nearReadingsCount = 0;
continue;
}
// Not in buffering mode. Just send.
_buffer = currentBuffer;
_bufferIndex = read;
SendBuffer();
}
}
}
private async Task SendAsync()
{
// Just to release the calling thread asap
await Task.Delay(5).ConfigureAwait(false);
while (true)
{
TimeSpan sleep;
lock (_lock)
{
if (_buffer == null)
{
_sendingTask = null;
return;
}
if (_paused)
{
sleep = TimeSpan.FromMilliseconds(WaitPeriodMilliseconds);
}
else
{
sleep = _scheduledSend < _sendingDeadline
? _scheduledSend.Subtract(DateTime.UtcNow)
: _sendingDeadline.Subtract(DateTime.UtcNow);
if (sleep.TotalMilliseconds < 5)
{
// Time to send
SendBuffer();
_sendingTask = null;
return;
}
}
}
await Task.Delay(sleep).ConfigureAwait(false);
}
}
// Has to be called from a locked code!
private void SendBuffer()
{
if (_bufferIndex == _buffer.Length)
{
_callback(_buffer);
}
else
{
var newBuffer = new byte[_bufferIndex];
Buffer.BlockCopy(_buffer, 0, newBuffer, 0, _bufferIndex);
_callback(newBuffer);
}
_buffer = null;
_bufferIndex = 0;
}
}
}