3
3
4
4
using System ;
5
5
using System . Text ;
6
+ using System . Threading ;
6
7
using System . Threading . Channels ;
7
8
using System . Threading . Tasks ;
8
9
@@ -15,11 +16,20 @@ internal class ConsoleWriter : IDisposable
15
16
// So in the extreme case, this is about 1 second of buffer and should be less than 3MB
16
17
private const int DefaultBufferSize = 8000 ;
17
18
19
+ // Because we read the log lines in batches from the buffer and write them to the console in one go,
20
+ // we can influence the latency distribution by controlling how much of the buffer we will process in one pass.
21
+ // If we set this to 1, the P50 latency will be low, but the P99 latency will be high.
22
+ // If we set this to a large value, it keeps the P99 latency under control but the P50 degrades.
23
+ // In local testing with a console attached, processing 1/10th of the buffer size per iteration yields single digit P50 while keeping P99 under 100ms.
24
+ private const int SingleWriteBufferDenominator = 10 ;
25
+
18
26
private static readonly TimeSpan DisposeTimeout = TimeSpan . FromSeconds ( 5 ) ;
19
27
private static readonly TimeSpan DefaultConsoleBufferTimeout = TimeSpan . FromSeconds ( 1 ) ;
28
+ private readonly ManualResetEvent _writeResetEvent ;
20
29
private readonly Channel < string > _consoleBuffer ;
21
30
private readonly TimeSpan _consoleBufferTimeout ;
22
31
private readonly Action < Exception > _exceptionhandler ;
32
+ private readonly int _maxLinesPerWrite ;
23
33
private Task _consoleBufferReadLoop ;
24
34
private Action < string > _writeEvent ;
25
35
private bool _disposed ;
@@ -35,32 +45,29 @@ internal ConsoleWriter(IEnvironment environment, Action<Exception> exceptionHand
35
45
36
46
if ( consoleEnabled )
37
47
{
38
- // We are going to used stdout, but do we write directly or use a buffer?
39
- _consoleBuffer = environment . GetEnvironmentVariable ( EnvironmentSettingNames . ConsoleLoggingBufferSize ) switch
48
+ int maxBufferSize = environment . GetEnvironmentVariable ( EnvironmentSettingNames . ConsoleLoggingBufferSize ) switch
40
49
{
41
- "-1" => Channel . CreateUnbounded < string > ( new UnboundedChannelOptions ( ) { SingleReader = true , SingleWriter = false } ) , // buffer size of -1 indicates that buffer should be enabled but unbounded
42
- "0" => null , // buffer size of 0 indicates that buffer should be disabled
43
- var s when int . TryParse ( s , out int i ) && i > 0 => Channel . CreateBounded < string > ( i ) ,
44
- _ => Channel . CreateBounded < string > ( new BoundedChannelOptions ( DefaultBufferSize ) { SingleReader = true , SingleWriter = false } ) , // default behavior is to use buffer with default size
50
+ var s when int . TryParse ( s , out int i ) && i >= 0 => i ,
51
+ var s when int . TryParse ( s , out int i ) && i < 0 => throw new ArgumentOutOfRangeException ( nameof ( EnvironmentSettingNames . ConsoleLoggingBufferSize ) , "Console buffer size cannot be negative" ) ,
52
+ _ => DefaultBufferSize ,
45
53
} ;
46
54
47
- if ( _consoleBuffer == null )
55
+ if ( maxBufferSize == 0 )
48
56
{
57
+ // buffer size was set to zero - disable it
49
58
_writeEvent = Console . WriteLine ;
50
59
}
51
60
else
52
61
{
62
+ _consoleBuffer = Channel . CreateBounded < string > ( new BoundedChannelOptions ( maxBufferSize ) { SingleReader = true , SingleWriter = false } ) ;
53
63
_writeEvent = WriteToConsoleBuffer ;
54
64
_consoleBufferTimeout = consoleBufferTimeout ;
65
+ _writeResetEvent = new ManualResetEvent ( true ) ;
66
+ _maxLinesPerWrite = maxBufferSize / SingleWriteBufferDenominator ;
67
+
55
68
if ( autoStart )
56
69
{
57
- bool batched = environment . GetEnvironmentVariable ( EnvironmentSettingNames . ConsoleLoggingBufferBatched ) switch
58
- {
59
- "0" => false , // disable batching by setting to 0
60
- _ => true , // default behavior is batched
61
- } ;
62
-
63
- StartProcessingBuffer ( batched ) ;
70
+ StartProcessingBuffer ( ) ;
64
71
}
65
72
}
66
73
}
@@ -81,30 +88,60 @@ private void WriteToConsoleBuffer(string evt)
81
88
{
82
89
if ( _consoleBuffer . Writer . TryWrite ( evt ) == false )
83
90
{
84
- Console . WriteLine ( evt ) ;
91
+ _writeResetEvent . Reset ( ) ;
92
+ if ( _writeResetEvent . WaitOne ( _consoleBufferTimeout ) == false || _consoleBuffer . Writer . TryWrite ( evt ) == false )
93
+ {
94
+ // We have either timed out or the buffer was full again, so just write directly to console
95
+ Console . WriteLine ( evt ) ;
96
+ }
85
97
}
86
98
}
87
99
88
- internal void StartProcessingBuffer ( bool batched )
100
+ internal void StartProcessingBuffer ( )
89
101
{
90
102
// intentional no-op if the task is already running
91
103
if ( _consoleBufferReadLoop == null || _consoleBufferReadLoop . IsCompleted )
92
104
{
93
- _consoleBufferReadLoop = ProcessConsoleBufferAsync ( batched ) ;
105
+ _consoleBufferReadLoop = ProcessConsoleBufferAsync ( ) ;
94
106
}
95
107
}
96
108
97
- private async Task ProcessConsoleBufferAsync ( bool batched )
109
+ private async Task ProcessConsoleBufferAsync ( )
98
110
{
99
111
try
100
112
{
101
- if ( batched )
102
- {
103
- await ProcessConsoleBufferBatchedAsync ( ) ;
104
- }
105
- else
113
+ var builder = new StringBuilder ( ) ;
114
+
115
+ while ( await _consoleBuffer . Reader . WaitToReadAsync ( ) )
106
116
{
107
- await ProcessConsoleBufferNonBatchedAsync ( ) ;
117
+ if ( _consoleBuffer . Reader . TryRead ( out string line1 ) )
118
+ {
119
+ _writeResetEvent . Set ( ) ;
120
+
121
+ // Can we synchronously read multiple lines?
122
+ // If yes, use the string builder to batch them together into a single write
123
+ // If no, just write the single line without using the builder;
124
+ if ( _consoleBuffer . Reader . TryRead ( out string line2 ) )
125
+ {
126
+ builder . AppendLine ( line1 ) ;
127
+ builder . AppendLine ( line2 ) ;
128
+ int lines = 2 ;
129
+
130
+ while ( lines < _maxLinesPerWrite && _consoleBuffer . Reader . TryRead ( out string nextLine ) )
131
+ {
132
+ builder . AppendLine ( nextLine ) ;
133
+ lines ++ ;
134
+ }
135
+
136
+ _writeResetEvent . Set ( ) ;
137
+ Console . Write ( builder . ToString ( ) ) ;
138
+ builder . Clear ( ) ;
139
+ }
140
+ else
141
+ {
142
+ Console . WriteLine ( line1 ) ;
143
+ }
144
+ }
108
145
}
109
146
}
110
147
catch ( Exception ex )
@@ -120,48 +157,6 @@ private async Task ProcessConsoleBufferAsync(bool batched)
120
157
}
121
158
}
122
159
123
- private async Task ProcessConsoleBufferNonBatchedAsync ( )
124
- {
125
- await foreach ( var line in _consoleBuffer . Reader . ReadAllAsync ( ) )
126
- {
127
- Console . WriteLine ( line ) ;
128
- }
129
- }
130
-
131
- private async Task ProcessConsoleBufferBatchedAsync ( )
132
- {
133
- var builder = new StringBuilder ( ) ;
134
-
135
- while ( await _consoleBuffer . Reader . WaitToReadAsync ( ) )
136
- {
137
- if ( _consoleBuffer . Reader . TryRead ( out string line1 ) )
138
- {
139
- // Can we synchronously read multiple lines?
140
- // If yes, use the string builder to batch them together into a single write
141
- // If no, just write the single line without using the builder;
142
- if ( _consoleBuffer . Reader . TryRead ( out string line2 ) )
143
- {
144
- builder . AppendLine ( line1 ) ;
145
- builder . AppendLine ( line2 ) ;
146
- int lines = 2 ;
147
-
148
- while ( lines < DefaultBufferSize && _consoleBuffer . Reader . TryRead ( out string nextLine ) )
149
- {
150
- builder . AppendLine ( nextLine ) ;
151
- lines ++ ;
152
- }
153
-
154
- Console . Write ( builder . ToString ( ) ) ;
155
- builder . Clear ( ) ;
156
- }
157
- else
158
- {
159
- Console . WriteLine ( line1 ) ;
160
- }
161
- }
162
- }
163
- }
164
-
165
160
protected virtual void Dispose ( bool disposing )
166
161
{
167
162
if ( ! _disposed )
@@ -173,6 +168,8 @@ protected virtual void Dispose(bool disposing)
173
168
_consoleBuffer . Writer . TryComplete ( ) ;
174
169
_consoleBufferReadLoop . Wait ( DisposeTimeout ) ;
175
170
}
171
+
172
+ _writeResetEvent ? . Dispose ( ) ;
176
173
}
177
174
178
175
_disposed = true ;
0 commit comments