diff --git a/src/System.Runtime.Extensions/src/System/IO/StreamWriter.cs b/src/System.Runtime.Extensions/src/System/IO/StreamWriter.cs index 6c12d306596d..04497aa53aa9 100644 --- a/src/System.Runtime.Extensions/src/System/IO/StreamWriter.cs +++ b/src/System.Runtime.Extensions/src/System/IO/StreamWriter.cs @@ -331,13 +331,52 @@ public override void Write(char[] buffer) return; } - if (GetType() == typeof(StreamWriter)) + CheckAsyncTaskInProgress(); + + // Threshold of 4 was chosen after running perf tests + if (buffer.Length <= 4) { - WriteCore(new ReadOnlySpan(buffer)); + for (int i = 0; i < buffer.Length; i++) + { + if (_charPos == _charLen) + { + Flush(false, false); + } + + Debug.Assert(_charLen - _charPos > 0, "StreamWriter::Write(char[]) isn't making progress! This is most likely a race in user code."); + _charBuffer[_charPos] = buffer[i]; + _charPos++; + } } else { - base.Write(buffer); + int count = buffer.Length; + + int index = 0; + while (count > 0) + { + if (_charPos == _charLen) + { + Flush(false, false); + } + + int n = _charLen - _charPos; + if (n > count) + { + n = count; + } + + Debug.Assert(n > 0, "StreamWriter::Write(char[]) isn't making progress! This is most likely a race in user code."); + Buffer.BlockCopy(buffer, index * sizeof(char), _charBuffer, _charPos * sizeof(char), n * sizeof(char)); + _charPos += n; + index += n; + count -= n; + } + } + + if (_autoFlush) + { + Flush(true, false); } } @@ -359,29 +398,56 @@ public override void Write(char[] buffer, int index, int count) { throw new ArgumentException(SR.Argument_InvalidOffLen); } - if (GetType() == typeof(StreamWriter)) + + CheckAsyncTaskInProgress(); + + // Threshold of 4 was chosen after running perf tests + if (count <= 4) { - WriteCore(new ReadOnlySpan(buffer, index, count)); + while (count > 0) + { + if (_charPos == _charLen) + { + Flush(false, false); + } + + Debug.Assert(_charLen - _charPos > 0, "StreamWriter::Write(char[]) isn't making progress! This is most likely a race in user code."); + _charBuffer[_charPos] = buffer[index]; + _charPos++; + index++; + count--; + } } else { - base.Write(buffer, index, count); - } - } + while (count > 0) + { + if (_charPos == _charLen) + { + Flush(false, false); + } - public override void Write(ReadOnlySpan source) - { - if (GetType() == typeof(StreamWriter)) - { - WriteCore(source); + int n = _charLen - _charPos; + if (n > count) + { + n = count; + } + + Debug.Assert(n > 0, "StreamWriter::Write(char[], int, int) isn't making progress! This is most likely a race condition in user code."); + Buffer.BlockCopy(buffer, index * sizeof(char), _charBuffer, _charPos * sizeof(char), n * sizeof(char)); + _charPos += n; + index += n; + count -= n; + } } - else + + if (_autoFlush) { - base.Write(source); + Flush(true, false); } } - private void WriteCore(ReadOnlySpan source) + public override void Write(ReadOnlySpan source) { CheckAsyncTaskInProgress(); @@ -441,13 +507,52 @@ public override void Write(string value) return; } - if (GetType() == typeof(StreamWriter)) + CheckAsyncTaskInProgress(); + + int count = value.Length; + + // Threshold of 4 was chosen after running perf tests + if (count <= 4) { - WriteCore(value.AsReadOnlySpan()); + for (int i = 0; i < count; i++) + { + if (_charPos == _charLen) + { + Flush(false, false); + } + + Debug.Assert(_charLen - _charPos > 0, "StreamWriter::Write(String) isn't making progress! This is most likely a race condition in user code."); + _charBuffer[_charPos] = value[i]; + _charPos++; + } } else { - base.Write(value); + int index = 0; + while (count > 0) + { + if (_charPos == _charLen) + { + Flush(false, false); + } + + int n = _charLen - _charPos; + if (n > count) + { + n = count; + } + + Debug.Assert(n > 0, "StreamWriter::Write(String) isn't making progress! This is most likely a race condition in user code."); + value.CopyTo(index, _charBuffer, _charPos, n); + _charPos += n; + index += n; + count -= n; + } + } + + if (_autoFlush) + { + Flush(true, false); } } @@ -462,26 +567,53 @@ public override void WriteLine(string value) value = String.Empty; } - if (GetType() == typeof(StreamWriter)) + CheckAsyncTaskInProgress(); + + int count = value.Length; + int index = 0; + while (count > 0) + { + if (_charPos == _charLen) + { + Flush(false, false); + } + + int n = _charLen - _charPos; + if (n > count) + { + n = count; + } + + Debug.Assert(n > 0, "StreamWriter::WriteLine(String) isn't making progress! This is most likely a race condition in user code."); + value.CopyTo(index, _charBuffer, _charPos, n); + _charPos += n; + index += n; + count -= n; + } + + char[] coreNewLine = CoreNewLine; + for (int i = 0; i < coreNewLine.Length; i++) // Expect 2 iterations, no point calling BlockCopy { - WriteLineCore(value.AsReadOnlySpan()); + if (_charPos == _charLen) + { + Flush(false, false); + } + + _charBuffer[_charPos] = coreNewLine[i]; + _charPos++; } - else + + if (_autoFlush) { - base.WriteLine(value); + Flush(true, false); } } public override void WriteLine(ReadOnlySpan source) - { - WriteLineCore(source); - } - - private void WriteLineCore(ReadOnlySpan value) { CheckAsyncTaskInProgress(); - int count = value.Length; + int count = source.Length; int index = 0; while (count > 0) { @@ -497,7 +629,7 @@ private void WriteLineCore(ReadOnlySpan value) } Debug.Assert(n > 0, "StreamWriter::WriteLine(String) isn't making progress! This is most likely a race condition in user code."); - value.Slice(index, n).CopyTo(new Span(_charBuffer, _charPos, n)); + source.Slice(index, n).CopyTo(new Span(_charBuffer, _charPos, n)); _charPos += n; index += n; count -= n; diff --git a/src/System.Runtime.Extensions/tests/Performance/Perf.StreamWriter.cs b/src/System.Runtime.Extensions/tests/Performance/Perf.StreamWriter.cs index 466e3537aa64..122e70372ebf 100644 --- a/src/System.Runtime.Extensions/tests/Performance/Perf.StreamWriter.cs +++ b/src/System.Runtime.Extensions/tests/Performance/Perf.StreamWriter.cs @@ -138,6 +138,67 @@ public void WriteString(int writeLength) } } + [Benchmark] + [MemberData(nameof(WriteLengthMemberData))] + public void WriteLineString(int writeLength) + { + string value = new string('a', writeLength); + int innerIterations = MemoryStreamSize / writeLength; + int outerIteration = TotalWriteCount / innerIterations; + using (var stream = new MemoryStream(MemoryStreamSize)) + { + using (var writer = new StreamWriter(stream, new UTF8Encoding(false, true), DefaultStreamWriterBufferSize, true)) + { + foreach (var iteration in Benchmark.Iterations) + { + using (iteration.StartMeasurement()) + { + for (int i = 0; i < outerIteration; i++) + { + for (int j = 0; j < innerIterations; j++) + { + writer.WriteLine(value); + } + writer.Flush(); + stream.Position = 0; + } + } + } + } + } + } + + [Benchmark] + [MemberData(nameof(WriteLengthMemberData))] + public void WriteLineReadOnlySpan(int writeLength) + { + string value = new string('a', writeLength); + ReadOnlySpan span = value.AsReadOnlySpan(); + int innerIterations = MemoryStreamSize / writeLength; + int outerIteration = TotalWriteCount / innerIterations; + using (var stream = new MemoryStream(MemoryStreamSize)) + { + using (var writer = new StreamWriter(stream, new UTF8Encoding(false, true), DefaultStreamWriterBufferSize, true)) + { + foreach (var iteration in Benchmark.Iterations) + { + using (iteration.StartMeasurement()) + { + for (int i = 0; i < outerIteration; i++) + { + for (int j = 0; j < innerIterations; j++) + { + writer.WriteLine(span); + } + writer.Flush(); + stream.Position = 0; + } + } + } + } + } + } + public static IEnumerable WriteLengthMemberData() { yield return new object[] { 2 };