Less allocations in ViewBuffer when there is only one ViewBufferPage

This commit is contained in:
Yves57 2016-09-25 16:28:42 +02:00 committed by Pranav K
parent a623b4edd1
commit ed3b750ad2
3 changed files with 180 additions and 98 deletions

View File

@ -25,6 +25,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
private readonly IViewBufferScope _bufferScope; private readonly IViewBufferScope _bufferScope;
private readonly string _name; private readonly string _name;
private readonly int _pageSize; private readonly int _pageSize;
private ViewBufferPage _currentPage; // Limits allocation if the ViewBuffer has only one page (frequent case).
private List<ViewBufferPage> _multiplePages; // Allocated only if necessary
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="ViewBuffer"/>. /// Initializes a new instance of <see cref="ViewBuffer"/>.
@ -47,14 +49,45 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
_bufferScope = bufferScope; _bufferScope = bufferScope;
_name = name; _name = name;
_pageSize = pageSize; _pageSize = pageSize;
Pages = new List<ViewBufferPage>();
} }
/// <summary> /// <summary>
/// Gets the backing buffer. /// Get the <see cref="ViewBufferPage"/> count.
/// </summary> /// </summary>
public IList<ViewBufferPage> Pages { get; } public int Count
{
get
{
if (_multiplePages != null)
{
return _multiplePages.Count;
}
if (_currentPage != null)
{
return 1;
}
return 0;
}
}
/// <summary>
/// Gets a <see cref="ViewBufferPage"/>.
/// </summary>
public ViewBufferPage this[int index]
{
get
{
if (_multiplePages != null)
{
return _multiplePages[index];
}
if (index == 0 && _currentPage != null)
{
return _currentPage;
}
throw new IndexOutOfRangeException();
}
}
/// <inheritdoc /> /// <inheritdoc />
public IHtmlContentBuilder Append(string unencoded) public IHtmlContentBuilder Append(string unencoded)
@ -89,7 +122,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
return this; return this;
} }
AppendValue(new ViewBufferValue(encoded)); AppendValue(new ViewBufferValue(encoded));
return this; return this;
} }
@ -102,33 +135,34 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
private ViewBufferPage GetCurrentPage() private ViewBufferPage GetCurrentPage()
{ {
ViewBufferPage page; if (_currentPage == null || _currentPage.IsFull)
if (Pages.Count == 0)
{ {
page = new ViewBufferPage(_bufferScope.GetPage(_pageSize)); AddPage(new ViewBufferPage(_bufferScope.GetPage(_pageSize)));
Pages.Add(page);
} }
else return _currentPage;
}
private void AddPage(ViewBufferPage page)
{
if (_multiplePages != null)
{ {
page = Pages[Pages.Count - 1]; _multiplePages.Add(page);
if (page.IsFull) }
{ else if (_currentPage != null)
page = new ViewBufferPage(_bufferScope.GetPage(_pageSize)); {
Pages.Add(page); _multiplePages = new List<ViewBufferPage>(2);
} _multiplePages.Add(_currentPage);
_multiplePages.Add(page);
} }
return page; _currentPage = page;
} }
/// <inheritdoc /> /// <inheritdoc />
public IHtmlContentBuilder Clear() public IHtmlContentBuilder Clear()
{ {
if (Pages != null) _multiplePages = null;
{ _currentPage = null;
Pages.Clear();
}
return this; return this;
} }
@ -145,14 +179,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
throw new ArgumentNullException(nameof(encoder)); throw new ArgumentNullException(nameof(encoder));
} }
if (Pages == null) for (var i = 0; i < Count; i++)
{ {
return; var page = this[i];
}
for (var i = 0; i < Pages.Count; i++)
{
var page = Pages[i];
for (var j = 0; j < page.Count; j++) for (var j = 0; j < page.Count; j++)
{ {
var value = page.Buffer[j]; var value = page.Buffer[j];
@ -192,14 +221,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
throw new ArgumentNullException(nameof(encoder)); throw new ArgumentNullException(nameof(encoder));
} }
if (Pages == null) for (var i = 0; i < Count; i++)
{ {
return; var page = this[i];
}
for (var i = 0; i < Pages.Count; i++)
{
var page = Pages[i];
for (var j = 0; j < page.Count; j++) for (var j = 0; j < page.Count; j++)
{ {
var value = page.Buffer[j]; var value = page.Buffer[j];
@ -238,14 +262,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
throw new ArgumentNullException(nameof(destination)); throw new ArgumentNullException(nameof(destination));
} }
if (Pages == null) for (var i = 0; i < Count; i++)
{ {
return; var page = this[i];
}
for (var i = 0; i < Pages.Count; i++)
{
var page = Pages[i];
for (var j = 0; j < page.Count; j++) for (var j = 0; j < page.Count; j++)
{ {
var value = page.Buffer[j]; var value = page.Buffer[j];
@ -275,11 +294,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
throw new ArgumentNullException(nameof(destination)); throw new ArgumentNullException(nameof(destination));
} }
if (Pages == null)
{
return;
}
// Perf: We have an efficient implementation when the destination is another view buffer, // Perf: We have an efficient implementation when the destination is another view buffer,
// we can just insert our pages as-is. // we can just insert our pages as-is.
var other = destination as ViewBuffer; var other = destination as ViewBuffer;
@ -289,9 +303,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
return; return;
} }
for (var i = 0; i < Pages.Count; i++) for (var i = 0; i < Count; i++)
{ {
var page = Pages[i]; var page = this[i];
for (var j = 0; j < page.Count; j++) for (var j = 0; j < page.Count; j++)
{ {
var value = page.Buffer[j]; var value = page.Buffer[j];
@ -313,23 +327,23 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
} }
for (var i = 0; i < Pages.Count; i++) for (var i = 0; i < Count; i++)
{ {
var page = Pages[i]; var page = this[i];
Array.Clear(page.Buffer, 0, page.Count); Array.Clear(page.Buffer, 0, page.Count);
_bufferScope.ReturnSegment(page.Buffer); _bufferScope.ReturnSegment(page.Buffer);
} }
Pages.Clear(); Clear();
} }
private void MoveTo(ViewBuffer destination) private void MoveTo(ViewBuffer destination)
{ {
for (var i = 0; i < Pages.Count; i++) for (var i = 0; i < Count; i++)
{ {
var page = Pages[i]; var page = this[i];
var destinationPage = destination.Pages.Count == 0 ? null : destination.Pages[destination.Pages.Count - 1]; var destinationPage = destination.Count == 0 ? null : destination[destination.Count - 1];
// If the source page is less or equal to than half full, let's copy it's content to the destination // If the source page is less or equal to than half full, let's copy it's content to the destination
// page if possible. // page if possible.
@ -351,17 +365,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
// Now we can return the source page, and it can be reused in the scope of this request. // Now we can return the source page, and it can be reused in the scope of this request.
Array.Clear(page.Buffer, 0, page.Count); Array.Clear(page.Buffer, 0, page.Count);
_bufferScope.ReturnSegment(page.Buffer); _bufferScope.ReturnSegment(page.Buffer);
} }
else else
{ {
// Otherwise, let's just add the source page to the other buffer. // Otherwise, let's just add the source page to the other buffer.
destination.Pages.Add(page); destination.AddPage(page);
} }
} }
Pages.Clear(); Clear();
} }
private class EncodingWrapper : IHtmlContent private class EncodingWrapper : IHtmlContent

View File

@ -25,7 +25,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
buffer.Append("Hello world"); buffer.Append("Hello world");
// Assert // Assert
var page = Assert.Single(buffer.Pages); Assert.Equal(1, buffer.Count);
var page = buffer[0];
Assert.Equal(1, page.Count); Assert.Equal(1, page.Count);
Assert.IsAssignableFrom<IHtmlContent>(page.Buffer[0].Value); Assert.IsAssignableFrom<IHtmlContent>(page.Buffer[0].Value);
} }
@ -41,7 +42,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
buffer.AppendHtml(content); buffer.AppendHtml(content);
// Assert // Assert
var page = Assert.Single(buffer.Pages); Assert.Equal(1, buffer.Count);
var page = buffer[0];
Assert.Equal(1, page.Count); Assert.Equal(1, page.Count);
Assert.Same(content, page.Buffer[0].Value); Assert.Same(content, page.Buffer[0].Value);
} }
@ -57,13 +59,32 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
buffer.AppendHtml(value); buffer.AppendHtml(value);
// Assert // Assert
var page = Assert.Single(buffer.Pages); Assert.Equal(1, buffer.Count);
var page = buffer[0];
Assert.Equal(1, page.Count); Assert.Equal(1, page.Count);
Assert.Equal("Hello world", Assert.IsType<string>(page.Buffer[0].Value)); Assert.Equal("Hello world", Assert.IsType<string>(page.Buffer[0].Value));
} }
[Fact] [Fact]
public void Append_CreatesNewPages_WhenCurrentPageIsFull() public void Append_CreatesOnePage()
{
// Arrange
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32);
var expected = Enumerable.Range(0, 32).Select(i => i.ToString());
// Act
foreach (var item in expected)
{
buffer.AppendHtml(item);
}
// Assert
Assert.Equal(1, buffer.Count);
Assert.Equal(expected, buffer[0].Buffer.Select(v => v.Value));
}
[Fact]
public void Append_CreatesTwoPages()
{ {
// Arrange // Arrange
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32); var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32);
@ -78,8 +99,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
buffer.AppendHtml("world"); buffer.AppendHtml("world");
// Assert // Assert
Assert.Equal(2, buffer.Pages.Count); Assert.Equal(2, buffer.Count);
Assert.Collection(buffer.Pages, Assert.Collection(new[] { buffer[0], buffer[1] },
page => Assert.Equal(expected, page.Buffer.Select(v => v.Value)), page => Assert.Equal(expected, page.Buffer.Select(v => v.Value)),
page => page =>
{ {
@ -89,9 +110,43 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}); });
} }
[Fact]
public void Append_CreatesManyPages()
{
// Arrange
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32);
var expected0 = Enumerable.Range(0, 32).Select(i => i.ToString());
var expected1 = Enumerable.Range(32, 32).Select(i => i.ToString());
// Act
foreach (var item in expected0)
{
buffer.AppendHtml(item);
}
foreach (var item in expected1)
{
buffer.AppendHtml(item);
}
buffer.AppendHtml("Hello");
buffer.AppendHtml("world");
// Assert
Assert.Equal(3, buffer.Count);
Assert.Collection(new[] { buffer[0], buffer[1], buffer[2] },
page => Assert.Equal(expected0, page.Buffer.Select(v => v.Value)),
page => Assert.Equal(expected1, page.Buffer.Select(v => v.Value)),
page =>
{
var array = page.Buffer;
Assert.Equal("Hello", array[0].Value);
Assert.Equal("world", array[1].Value);
});
}
[Theory] [Theory]
[InlineData(1)] [InlineData(1)] // Create one page before clear
[InlineData(35)] [InlineData(35)] // Create two pages before clear
[InlineData(65)] // Create many pages before clear
public void Clear_ResetsBackingBufferAndIndex(int valuesToWrite) public void Clear_ResetsBackingBufferAndIndex(int valuesToWrite)
{ {
// Arrange // Arrange
@ -106,7 +161,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
buffer.AppendHtml("world"); buffer.AppendHtml("world");
// Assert // Assert
var page = Assert.Single(buffer.Pages); Assert.Equal(1, buffer.Count);
var page = buffer[0];
Assert.Equal(1, page.Count); Assert.Equal(1, page.Count);
Assert.Equal("world", page.Buffer[0].Value); Assert.Equal("world", page.Buffer[0].Value);
} }
@ -211,7 +267,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
buffer.CopyTo(destination); buffer.CopyTo(destination);
// Assert // Assert
Assert.Same(nested, buffer.Pages[0].Buffer[0].Value); Assert.Same(nested, buffer[0].Buffer[0].Value);
Assert.Equal("Hello", Assert.IsType<HtmlString>(nestedItems[0]).Value); Assert.Equal("Hello", Assert.IsType<HtmlString>(nestedItems[0]).Value);
Assert.Equal("Hello", Assert.IsType<HtmlString>(destinationItems[0]).Value); Assert.Equal("Hello", Assert.IsType<HtmlString>(destinationItems[0]).Value);
} }
@ -235,7 +291,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
// Assert // Assert
Assert.Empty(nestedItems); Assert.Empty(nestedItems);
Assert.Empty(buffer.Pages); Assert.Equal(0, buffer.Count);
Assert.Equal("Hello", Assert.IsType<HtmlString>(destinationItems[0]).Value); Assert.Equal("Hello", Assert.IsType<HtmlString>(destinationItems[0]).Value);
} }
@ -250,14 +306,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
other.AppendHtml("Hi"); other.AppendHtml("Hi");
var page = other.Pages[0]; var page = other[0];
// Act // Act
other.MoveTo(original); other.MoveTo(original);
// Assert // Assert
Assert.Empty(other.Pages); // Page was taken Assert.Equal(0, other.Count); // Page was taken
Assert.Same(page, Assert.Single(original.Pages)); Assert.Equal(1, original.Count);
Assert.Same(page, original[0]);
} }
[Fact] [Fact]
@ -276,15 +333,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
other.AppendHtml("Hi"); other.AppendHtml("Hi");
var page = other.Pages[0]; var page = other[0];
// Act // Act
other.MoveTo(original); other.MoveTo(original);
// Assert // Assert
Assert.Empty(other.Pages); // Page was taken Assert.Equal(0, other.Count); // Page was taken
Assert.Equal(2, original.Pages.Count); Assert.Equal(2, original.Count);
Assert.Same(page, original.Pages[1]); Assert.Same(page, original[1]);
} }
[Fact] [Fact]
@ -308,15 +365,15 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
other.AppendHtml($"other-{i}"); other.AppendHtml($"other-{i}");
} }
var page = other.Pages[0]; var page = other[0];
// Act // Act
other.MoveTo(original); other.MoveTo(original);
// Assert // Assert
Assert.Empty(other.Pages); // Page was taken Assert.Equal(0, other.Count); // Page was taken
Assert.Equal(2, original.Pages.Count); Assert.Equal(2, original.Count);
Assert.Same(page, original.Pages[1]); Assert.Same(page, original[1]);
} }
[Fact] [Fact]
@ -339,17 +396,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
other.AppendHtml($"other-{i}"); other.AppendHtml($"other-{i}");
} }
var page = other.Pages[0]; var page = other[0];
// Act // Act
other.MoveTo(original); other.MoveTo(original);
// Assert // Assert
Assert.Empty(other.Pages); // Other is cleared Assert.Equal(0, other.Count); // Other is cleared
Assert.Contains(page.Buffer, scope.ReturnedBuffers); // Buffer was returned Assert.Contains(page.Buffer, scope.ReturnedBuffers); // Buffer was returned
Assert.Equal(1, original.Count);
Assert.Collection( Assert.Collection(
Assert.Single(original.Pages).Buffer, original[0].Buffer,
item => Assert.Equal("original-0", item.Value), item => Assert.Equal("original-0", item.Value),
item => Assert.Equal("original-1", item.Value), item => Assert.Equal("original-1", item.Value),
item => Assert.Equal("other-0", item.Value), item => Assert.Equal("other-0", item.Value),
@ -376,24 +434,24 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
other.AppendHtml($"other-{i}"); other.AppendHtml($"other-{i}");
} }
var page = other.Pages[0]; var page = other[0];
other.MoveTo(original); other.MoveTo(original);
// Act // Act
original.AppendHtml("after-merge"); original.AppendHtml("after-merge");
// Assert // Assert
Assert.Empty(other.Pages); // Other is cleared Assert.Equal(0, other.Count); // Other is cleared
Assert.Equal(2, original.Pages.Count); Assert.Equal(2, original.Count);
Assert.Collection( Assert.Collection(
original.Pages[0].Buffer, original[0].Buffer,
item => Assert.Equal("original-0", item.Value), item => Assert.Equal("original-0", item.Value),
item => Assert.Equal("original-1", item.Value), item => Assert.Equal("original-1", item.Value),
item => Assert.Equal("original-2", item.Value), item => Assert.Equal("original-2", item.Value),
item => Assert.Null(item.Value)); item => Assert.Null(item.Value));
Assert.Collection( Assert.Collection(
original.Pages[1].Buffer, original[1].Buffer,
item => Assert.Equal("other-0", item.Value), item => Assert.Equal("other-0", item.Value),
item => Assert.Equal("other-1", item.Value), item => Assert.Equal("other-1", item.Value),
item => Assert.Equal("other-2", item.Value), item => Assert.Equal("other-2", item.Value),
@ -419,35 +477,39 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
other.AppendHtml($"other-{i}"); other.AppendHtml($"other-{i}");
} }
var pages = new List<ViewBufferPage>(other.Pages); var pages = new List<ViewBufferPage>();
for (var i = 0; i < other.Count; i++)
{
pages.Add(other[i]);
}
// Act // Act
other.MoveTo(original); other.MoveTo(original);
// Assert // Assert
Assert.Empty(other.Pages); // Other is cleared Assert.Equal(0, other.Count); // Other is cleared
Assert.Equal(4, original.Pages.Count); Assert.Equal(4, original.Count);
Assert.Collection( Assert.Collection(
original.Pages[0].Buffer, original[0].Buffer,
item => Assert.Equal("original-0", item.Value), item => Assert.Equal("original-0", item.Value),
item => Assert.Equal("original-1", item.Value), item => Assert.Equal("original-1", item.Value),
item => Assert.Null(item.Value), item => Assert.Null(item.Value),
item => Assert.Null(item.Value)); item => Assert.Null(item.Value));
Assert.Collection( Assert.Collection(
original.Pages[1].Buffer, original[1].Buffer,
item => Assert.Equal("other-0", item.Value), item => Assert.Equal("other-0", item.Value),
item => Assert.Equal("other-1", item.Value), item => Assert.Equal("other-1", item.Value),
item => Assert.Equal("other-2", item.Value), item => Assert.Equal("other-2", item.Value),
item => Assert.Equal("other-3", item.Value)); item => Assert.Equal("other-3", item.Value));
Assert.Collection( Assert.Collection(
original.Pages[2].Buffer, original[2].Buffer,
item => Assert.Equal("other-4", item.Value), item => Assert.Equal("other-4", item.Value),
item => Assert.Equal("other-5", item.Value), item => Assert.Equal("other-5", item.Value),
item => Assert.Equal("other-6", item.Value), item => Assert.Equal("other-6", item.Value),
item => Assert.Equal("other-7", item.Value)); item => Assert.Equal("other-7", item.Value));
Assert.Collection( Assert.Collection(
original.Pages[3].Buffer, original[3].Buffer,
item => Assert.Equal("other-8", item.Value), item => Assert.Equal("other-8", item.Value),
item => Assert.Null(item.Value), item => Assert.Null(item.Value),
item => Assert.Null(item.Value), item => Assert.Null(item.Value),

View File

@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
writer.Write(2.718m); writer.Write(2.718m);
// Assert // Assert
Assert.Empty(buffer.Pages); Assert.Equal(0, buffer.Count);
foreach (var item in expected) foreach (var item in expected)
{ {
inner.Verify(v => v.Write(item), Times.Once()); inner.Verify(v => v.Write(item), Times.Once());
@ -220,7 +220,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
private static object[] GetValues(ViewBuffer buffer) private static object[] GetValues(ViewBuffer buffer)
{ {
return buffer.Pages var pages = new List<ViewBufferPage>();
for (var i = 0; i < buffer.Count; i++)
{
pages.Add(buffer[i]);
}
return pages
.SelectMany(c => c.Buffer) .SelectMany(c => c.Buffer)
.Select(d => d.Value) .Select(d => d.Value)
.TakeWhile(d => d != null) .TakeWhile(d => d != null)