Merge pull request #2198 from aspnet/release/2.1

fix #2187 by passing min size through to buffer writer when encoding (#2190)
This commit is contained in:
Andrew Stanton-Nurse 2018-05-03 19:10:42 -07:00 committed by GitHub
commit 6429e8e8ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 205 additions and 73 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -6,7 +6,6 @@ using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@ -24,7 +23,7 @@ namespace Microsoft.AspNetCore.Internal
private readonly int _minimumSegmentSize;
private int _bytesWritten;
private List<byte[]> _fullSegments;
private List<CompletedBuffer> _completedSegments;
private byte[] _currentSegment;
private int _position;
@ -78,14 +77,14 @@ namespace Microsoft.AspNetCore.Internal
public void Reset()
{
if (_fullSegments != null)
if (_completedSegments != null)
{
for (var i = 0; i < _fullSegments.Count; i++)
for (var i = 0; i < _completedSegments.Count; i++)
{
ArrayPool<byte>.Shared.Return(_fullSegments[i]);
_completedSegments[i].Return();
}
_fullSegments.Clear();
_completedSegments.Clear();
}
if (_currentSegment != null)
@ -120,13 +119,13 @@ namespace Microsoft.AspNetCore.Internal
public void CopyTo(IBufferWriter<byte> destination)
{
if (_fullSegments != null)
if (_completedSegments != null)
{
// Copy full segments
var count = _fullSegments.Count;
// Copy completed segments
var count = _completedSegments.Count;
for (var i = 0; i < count; i++)
{
destination.Write(_fullSegments[i]);
destination.Write(_completedSegments[i].Span);
}
}
@ -135,9 +134,9 @@ namespace Microsoft.AspNetCore.Internal
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
{
if (_fullSegments == null)
if (_completedSegments == null)
{
// There is only one segment so write without async
// There is only one segment so write without awaiting.
return destination.WriteAsync(_currentSegment, 0, _position);
}
@ -146,43 +145,52 @@ namespace Microsoft.AspNetCore.Internal
private void EnsureCapacity(int sizeHint)
{
// TODO: Use sizeHint
if (_currentSegment != null && _position < _currentSegment.Length)
// This does the Right Thing. It only subtracts _position from the current segment length if it's non-null.
// If _currentSegment is null, it returns 0.
var remainingSize = _currentSegment?.Length - _position ?? 0;
// If the sizeHint is 0, any capacity will do
// Otherwise, the buffer must have enough space for the entire size hint, or we need to add a segment.
if ((sizeHint == 0 && remainingSize > 0) || (sizeHint > 0 && remainingSize >= sizeHint))
{
// We have capacity in the current segment
return;
}
AddSegment();
AddSegment(sizeHint);
}
private void AddSegment()
private void AddSegment(int sizeHint = 0)
{
if (_currentSegment != null)
{
// We're adding a segment to the list
if (_fullSegments == null)
if (_completedSegments == null)
{
_fullSegments = new List<byte[]>();
_completedSegments = new List<CompletedBuffer>();
}
_fullSegments.Add(_currentSegment);
// Position might be less than the segment length if there wasn't enough space to satisfy the sizeHint when
// GetMemory was called. In that case we'll take the current segment and call it "completed", but need to
// ignore any empty space in it.
_completedSegments.Add(new CompletedBuffer(_currentSegment, _position));
}
_currentSegment = ArrayPool<byte>.Shared.Rent(_minimumSegmentSize);
// Get a new buffer using the minimum segment size, unless the size hint is larger than a single segment.
_currentSegment = ArrayPool<byte>.Shared.Rent(Math.Max(_minimumSegmentSize, sizeHint));
_position = 0;
}
private async Task CopyToSlowAsync(Stream destination)
{
if (_fullSegments != null)
if (_completedSegments != null)
{
// Copy full segments
var count = _fullSegments.Count;
var count = _completedSegments.Count;
for (var i = 0; i < count; i++)
{
var segment = _fullSegments[i];
await destination.WriteAsync(segment, 0, segment.Length);
var segment = _completedSegments[i];
await destination.WriteAsync(segment.Buffer, 0, segment.Length);
}
}
@ -200,15 +208,15 @@ namespace Microsoft.AspNetCore.Internal
var totalWritten = 0;
if (_fullSegments != null)
if (_completedSegments != null)
{
// Copy full segments
var count = _fullSegments.Count;
var count = _completedSegments.Count;
for (var i = 0; i < count; i++)
{
var segment = _fullSegments[i];
segment.CopyTo(result, totalWritten);
totalWritten += segment.Length;
var segment = _completedSegments[i];
segment.Span.CopyTo(result.AsSpan(totalWritten));
totalWritten += segment.Span.Length;
}
}
@ -229,15 +237,15 @@ namespace Microsoft.AspNetCore.Internal
var totalWritten = 0;
if (_fullSegments != null)
if (_completedSegments != null)
{
// Copy full segments
var count = _fullSegments.Count;
var count = _completedSegments.Count;
for (var i = 0; i < count; i++)
{
var segment = _fullSegments[i];
segment.AsSpan().CopyTo(span.Slice(totalWritten));
totalWritten += segment.Length;
var segment = _completedSegments[i];
segment.Span.CopyTo(span.Slice(totalWritten));
totalWritten += segment.Span.Length;
}
}
@ -307,5 +315,27 @@ namespace Microsoft.AspNetCore.Internal
Reset();
}
}
/// <summary>
/// Holds a byte[] from the pool and a size value. Basically a Memory but guaranteed to be backed by an ArrayPool byte[], so that we know we can return it.
/// </summary>
private readonly struct CompletedBuffer
{
public byte[] Buffer { get; }
public int Length { get; }
public ReadOnlySpan<byte> Span => Buffer.AsSpan(0, Length);
public CompletedBuffer(byte[] buffer, int length)
{
Buffer = buffer;
Length = length;
}
public void Return()
{
ArrayPool<byte>.Shared.Return(Buffer);
}
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Internal
internal sealed class Utf8BufferTextWriter : TextWriter
{
private static readonly UTF8Encoding _utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
private static readonly int MaximumBytesPerUtf8Char = 4;
[ThreadStatic]
private static Utf8BufferTextWriter _cachedInstance;
@ -139,7 +140,12 @@ namespace Microsoft.AspNetCore.Internal
private void EnsureBuffer()
{
if (_memoryUsed == _memory.Length)
// We need at least enough bytes to encode a single UTF-8 character, or Encoder.Convert will throw.
// Normally, if there isn't enough space to write every character of a char buffer, Encoder.Convert just
// writes what it can. However, if it can't even write a single character, it throws. So if the buffer has only
// 2 bytes left and the next character to write is 3 bytes in UTF-8, an exception is thrown.
var remaining = _memory.Length - _memoryUsed;
if (remaining < MaximumBytesPerUtf8Char)
{
// Used up the memory from the buffer writer so advance and get more
if (_memoryUsed > 0)
@ -147,7 +153,7 @@ namespace Microsoft.AspNetCore.Internal
_bufferWriter.Advance(_memoryUsed);
}
_memory = _bufferWriter.GetMemory();
_memory = _bufferWriter.GetMemory(MaximumBytesPerUtf8Char);
_memoryUsed = 0;
}
}
@ -199,4 +205,4 @@ namespace Microsoft.AspNetCore.Internal
}
}
}
}
}

View File

@ -398,6 +398,33 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
}
#endif
[Fact]
public void GetMemoryAllocatesNewSegmentWhenInsufficientSpaceInCurrentSegment()
{
// Have the buffer writer rent only the minimum size segments from the pool.
using (var bufferWriter = new MemoryBufferWriter(MinimumSegmentSize))
{
var data = new byte[MinimumSegmentSize];
new Random().NextBytes(data);
// Write half the minimum segment size
bufferWriter.Write(data.AsSpan(0, MinimumSegmentSize / 2));
// Request a new buffer of MinimumSegmentSize
var buffer = bufferWriter.GetMemory(MinimumSegmentSize);
Assert.Equal(MinimumSegmentSize, buffer.Length);
// Write to the buffer
bufferWriter.Write(data);
// Verify the data was all written correctly
var expectedOutput = new byte[MinimumSegmentSize + (MinimumSegmentSize / 2)];
data.AsSpan(0, MinimumSegmentSize / 2).CopyTo(expectedOutput.AsSpan(0, MinimumSegmentSize / 2));
data.CopyTo(expectedOutput, MinimumSegmentSize / 2);
Assert.Equal(expectedOutput, bufferWriter.ToArray());
}
}
[Fact]
public void ResetResetsTheMemoryBufferWriter()
{
@ -418,4 +445,4 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
Assert.Equal(0, bufferWriter.Length);
}
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.SignalR.Internal;
@ -202,20 +203,21 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
textWriter.Write(chars);
textWriter.Flush();
Assert.Equal(6, bufferWriter.Segments.Count);
var segments = bufferWriter.GetSegments();
Assert.Equal(6, segments.Count);
Assert.Equal(1, bufferWriter.Position);
Assert.Equal((byte)'H', bufferWriter.Segments[0].Span[0]);
Assert.Equal((byte)'e', bufferWriter.Segments[0].Span[1]);
Assert.Equal((byte)'l', bufferWriter.Segments[1].Span[0]);
Assert.Equal((byte)'l', bufferWriter.Segments[1].Span[1]);
Assert.Equal((byte)'o', bufferWriter.Segments[2].Span[0]);
Assert.Equal((byte)' ', bufferWriter.Segments[2].Span[1]);
Assert.Equal((byte)'w', bufferWriter.Segments[3].Span[0]);
Assert.Equal((byte)'o', bufferWriter.Segments[3].Span[1]);
Assert.Equal((byte)'r', bufferWriter.Segments[4].Span[0]);
Assert.Equal((byte)'l', bufferWriter.Segments[4].Span[1]);
Assert.Equal((byte)'d', bufferWriter.Segments[5].Span[0]);
Assert.Equal((byte)'H', segments[0].Span[0]);
Assert.Equal((byte)'e', segments[0].Span[1]);
Assert.Equal((byte)'l', segments[1].Span[0]);
Assert.Equal((byte)'l', segments[1].Span[1]);
Assert.Equal((byte)'o', segments[2].Span[0]);
Assert.Equal((byte)' ', segments[2].Span[1]);
Assert.Equal((byte)'w', segments[3].Span[0]);
Assert.Equal((byte)'o', segments[3].Span[1]);
Assert.Equal((byte)'r', segments[4].Span[0]);
Assert.Equal((byte)'l', segments[4].Span[1]);
Assert.Equal((byte)'d', segments[5].Span[0]);
}
[Fact]
@ -242,35 +244,89 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
Assert.Same(textWriter1, textWriter2);
}
[Fact]
private void WriteMultiByteCharactersToSmallBuffers()
{
// Test string breakdown (char => UTF-8 hex values):
// a => 61
// い => E3-81-84
// b => 62
// ろ => E3-82-8D
// c => 63
// d => 64
// は => E3-81-AF
// に => E3-81-AB
// e => 65
// ほ => E3-81-BB
// f => 66
// へ => E3-81-B8
// ど => E3-81-A9
// g => 67
// h => 68
// i => 69
// \uD800\uDC00 => F0-90-80-80 (this is a surrogate pair that is represented as a single 4-byte UTF-8 encoding)
const string testString = "aいbろcdはにeほfへどghi\uD800\uDC00";
// By mixing single byte and multi-byte characters, we know that there will
// be spaces in the active segment that cannot fit the current character. This
// means we'll be testing the GetMemory(minimumSize) logic.
var bufferWriter = new TestMemoryBufferWriter(segmentSize: 5);
var writer = new Utf8BufferTextWriter();
writer.SetWriter(bufferWriter);
writer.Write(testString);
writer.Flush();
// Verify the output
var allSegments = bufferWriter.GetSegments().Select(s => s.ToArray()).ToArray();
Assert.Collection(allSegments,
seg => Assert.Equal(new byte[] { 0x61, 0xE3, 0x81, 0x84, 0x62 }, seg), // "aいb"
seg => Assert.Equal(new byte[] { 0xE3, 0x82, 0x8D, 0x63, 0x64 }, seg), // "ろcd"
seg => Assert.Equal(new byte[] { 0xE3, 0x81, 0xAF }, seg), // "は"
seg => Assert.Equal(new byte[] { 0xE3, 0x81, 0xAB, 0x65 }, seg), // "にe"
seg => Assert.Equal(new byte[] { 0xE3, 0x81, 0xBB, 0x66 }, seg), // "ほf"
seg => Assert.Equal(new byte[] { 0xE3, 0x81, 0xB8 }, seg), // "へ"
seg => Assert.Equal(new byte[] { 0xE3, 0x81, 0xA9, 0x67, 0x68 }, seg), // "どgh"
seg => Assert.Equal(new byte[] { 0x69, 0xF0, 0x90, 0x80, 0x80 }, seg)); // "i\uD800\uDC00"
Assert.Equal(testString, Encoding.UTF8.GetString(bufferWriter.ToArray()));
}
private sealed class TestMemoryBufferWriter : IBufferWriter<byte>
{
private readonly int _segmentSize;
internal List<Memory<byte>> Segments { get; }
private List<Memory<byte>> _completedSegments = new List<Memory<byte>>();
private int _totalLength;
public Memory<byte> CurrentSegment { get; private set; }
internal int Position { get; private set; }
public TestMemoryBufferWriter(int segmentSize = 2048)
{
_segmentSize = segmentSize;
Segments = new List<Memory<byte>>();
CurrentSegment = Memory<byte>.Empty;
}
public Memory<byte> CurrentSegment => Segments.Count > 0 ? Segments[Segments.Count - 1] : null;
public void Advance(int count)
{
Position += count;
_totalLength += count;
}
public Memory<byte> GetMemory(int sizeHint = 0)
{
// TODO: Use sizeHint
if (Segments.Count == 0 || Position == _segmentSize)
// Need special handling for sizeHint == 0, because for that we want to enter the if even if there are "sizeHint" (i.e. 0) bytes left :).
if ((sizeHint == 0 && CurrentSegment.Length == Position) || (CurrentSegment.Length - Position < sizeHint))
{
// TODO: Rent memory from a pool
Segments.Add(new Memory<byte>(new byte[_segmentSize]));
if (Position > 0)
{
// Complete the current segment
_completedSegments.Add(CurrentSegment.Slice(0, Position));
}
// Allocate a new segment and reset the position.
CurrentSegment = new Memory<byte>(new byte[_segmentSize]);
Position = 0;
}
@ -284,31 +340,44 @@ namespace Microsoft.AspNetCore.SignalR.Common.Tests.Internal.Protocol
public byte[] ToArray()
{
if (Segments.Count == 0)
if (CurrentSegment.IsEmpty && _completedSegments.Count == 0)
{
return Array.Empty<byte>();
}
var totalLength = (Segments.Count - 1) * _segmentSize;
totalLength += Position;
var result = new byte[totalLength];
var result = new byte[_totalLength];
var totalWritten = 0;
// Copy full segments
for (var i = 0; i < Segments.Count - 1; i++)
// Copy completed segments
foreach (var segment in _completedSegments)
{
Segments[i].CopyTo(result.AsMemory(totalWritten, _segmentSize));
segment.CopyTo(result.AsMemory(totalWritten, segment.Length));
totalWritten += _segmentSize;
totalWritten += segment.Length;
}
// Copy current incomplete segment
// Copy current segment
CurrentSegment.Slice(0, Position).CopyTo(result.AsMemory(totalWritten, Position));
return result;
}
public IList<Memory<byte>> GetSegments()
{
var list = new List<Memory<byte>>();
foreach (var segment in _completedSegments)
{
list.Add(segment);
}
if (CurrentSegment.Length > 0)
{
list.Add(CurrentSegment.Slice(0, Position));
}
return list;
}
}
}
}